Closed8

ImHexでWebAuthnのAttestationObjectのパースをする

macopymacopy

WebAuthnのAttestation Objectをバイナリエディタで見たい

  • WebAuthnのサーバー実装を書いている
  • クライアントから送られたAttestation Objectのバイナリをサーバーでパースしたい

ImHex

可視化に使うバイナリエディタ https://github.com/WerWolv/ImHex

Attestation Objectのspec

https://developer.mozilla.org/en-US/docs/Web/API/AuthenticatorAttestationResponse/attestationObject

外側がCBORで、内側のauthDataがまたバイナリ

https://developer.mozilla.org/en-US/docs/Web/API/Web_Authentication_API/Authenticator_data

authDataの中身に公開鍵や鍵が保存されている方法などが記録されている

macopymacopy

CBOR部分のパース

以下のパターンでとりあえずパースできた。attestation objectに出てくるものに限っている。
参考

#include <std/mem.pat>
#include <std/core.pat>
#include <std/io.pat>
#define SHORT_COUNT_TINY_FIELD_MAX_NUM 23

enum MajorType : u8 {
  PositiveInteger = 0x00,
  NegativeInteger = 0x01,
  ByteString = 0x02,
  UTF8String = 0x03,
  Array = 0x04,
  Map = 0x05,
  SemanticTag = 0x06,
  Special = 0x07
};

enum ShortCount : u8 {
};

bitfield Header {
  short_count : 5;
  MajorType major_type : 3;
};

struct CBOR {
  Header header;
  if (
    header.major_type == MajorType::PositiveInteger &&
    header.short_count <= SHORT_COUNT_TINY_FIELD_MAX_NUM
  ) {
    u8 tiny_int = header.short_count;
    break;
  } else if (
    header.major_type == MajorType::NegativeInteger &&
    header.short_count <= SHORT_COUNT_TINY_FIELD_MAX_NUM
  ) {
    s8 tiny_int = -1 - header.short_count;
    break;
  } else if (
    header.major_type == MajorType::Special &&
    header.short_count <= SHORT_COUNT_TINY_FIELD_MAX_NUM
  ) {
    u8 special = header.short_count;
    break;
  } else if (
    header.major_type == MajorType::UTF8String ||
    header.major_type == MajorType::ByteString
  ) {
    u64 size = header.short_count;
    if (size > SHORT_COUNT_TINY_FIELD_MAX_NUM) {
      u8 extra_bytes = size - SHORT_COUNT_TINY_FIELD_MAX_NUM;
      if (extra_bytes == 1) {
        u8 extra_size;
        size = extra_size;
      } else if (extra_bytes == 2) {
        u16 extra_size;
        size = extra_size;
      } else if (extra_bytes == 3) {
        u32 extra_size;
        size = extra_size;
      } else if (extra_bytes == 4) {
        u64 extra_size;
        size = extra_size;
      }
    }    
    u8 end = $ + size;
    match (header.major_type) {
      (MajorType::UTF8String): char value[while($ < end)];
      (MajorType::ByteString): u8 value[while($ < end)];
    }
  } else if (header.major_type == MajorType::Map
  ) {
    u8 size = header.short_count;
    CBOR items[size*2];
  }
};

CBOR cbor[while(!std::mem::eof())] @ 0x00;
macopymacopy

AuthDataまでのパース

PublicKeyより下はCOSEフォーマットなので、それ以外はパースできた気がする。

#include <std/mem.pat>
#include <std/core.pat>
#include <std/io.pat>
#include <type/guid.pat>
#define SHORT_COUNT_TINY_FIELD_MAX_NUM 23

namespace authData {

  bitfield Flags {
    bool user_presence : 1;
    bool reserved1 : 1;
    bool user_verification : 1;
    bool backup_eligibility : 1;
    bool backup_state : 1;
    bool reserved5 : 1;
    bool attested_credential_data : 1;
    bool extension_data : 1;
  };

  struct AttestedCredentialData {
    type::GUID aaguid;
    be u16 credential_id_length;
    if (credential_id_length == 16) {
      type::GUID credential_id;
    } else {
      u64 credential_id_end = $ + credential_id_length;
      u8 credential_id[while($ < credential_id_end)];
    }
    u8 credential_public_key[while(!std::mem::eof())];
  };

  struct Data {
    u8 rpIdHash[32];
    Flags flags;
    u32 signCount;
    AttestedCredentialData attested_credential_data;
  };
}


enum MajorType : u8 {
  PositiveInteger = 0x00,
  NegativeInteger = 0x01,
  ByteString = 0x02,
  UTF8String = 0x03,
  Array = 0x04,
  Map = 0x05,
  SemanticTag = 0x06,
  Special = 0x07
};

enum ShortCount : u8 {
};

bitfield Header {
  short_count : 5;
  MajorType major_type : 3;
};

struct CBOR {
  Header header;
  if (
    header.major_type == MajorType::PositiveInteger &&
    header.short_count <= SHORT_COUNT_TINY_FIELD_MAX_NUM
  ) {
    u8 tiny_int = header.short_count;
    break;
  } else if (
    header.major_type == MajorType::NegativeInteger &&
    header.short_count <= SHORT_COUNT_TINY_FIELD_MAX_NUM
  ) {
    s8 tiny_int = -1 - header.short_count;
    break;
  } else if (
    header.major_type == MajorType::Special &&
    header.short_count <= SHORT_COUNT_TINY_FIELD_MAX_NUM
  ) {
    u8 special = header.short_count;
    break;
  } else if (
    header.major_type == MajorType::UTF8String ||
    header.major_type == MajorType::ByteString
  ) {
    u64 size = header.short_count;
    if (size > SHORT_COUNT_TINY_FIELD_MAX_NUM) {
      u8 extra_bytes = size - SHORT_COUNT_TINY_FIELD_MAX_NUM;
      if (extra_bytes == 1) {
        u8 extra_size;
        size = extra_size;
      } else if (extra_bytes == 2) {
        be u16 extra_size;
        size = extra_size;
      } else if (extra_bytes == 3) {
        be u32 extra_size;
        size = extra_size;
      } else if (extra_bytes == 4) {
        be u64 extra_size;
        size = extra_size;
      }
    }    
    u64 end = $ + size;
    match (header.major_type) {
      (MajorType::UTF8String): char value[while($ < end)];
      (MajorType::ByteString): authData::Data auth_data;
    }
  } else if (header.major_type == MajorType::Map
  ) {
    u8 size = header.short_count;
    CBOR items[size*2];
  }
};

CBOR cbor[while(!std::mem::eof())] @ 0x00;
macopymacopy

attestationObjectのattStmtはfmtがnoneの場合は空。1PasswordとiOSのKeychainで試したが、どちらもnoneが入っていた

macopymacopy

authData内のPublicKeyをCOSEとしてパース

COSEのRFC
参考: https://datatracker.ietf.org/doc/html/rfc8152
自動翻訳: https://tex2e.github.io/rfc-translater/html/rfc8152.html

今回のケースではCOSEはCBORのmapで格納されている。mapのキーは負数を含む整数である。手元のauthDataでは、1,3,-1,-2,-3のキーが確認できた。これらをラベルと呼ぶ。ラベルのそれぞれの意味は、

ラベル 意味 値の型 値の対応(例)
1 マップのキーのファミリー 整数 または 文字列 2=EC2
3 アルゴリズム 整数 または 文字列 -7=ECDSA w/ SHA-256
-1 楕円曲線暗号の種別(crv) 整数 1=P-256
-2 楕円曲線暗号のECポイントのx座標 バイト列 -
-3 楕円曲線暗号のECポイントのy座標 バイト列 -
macopymacopy

色々整理した結果こんな感じ

#include <std/mem.pat>
#include <std/core.pat>
#include <std/io.pat>
#include <type/guid.pat>
#define SHORT_COUNT_TINY_FIELD_MAX_NUM 23

using CBOR;
  
namespace authData {
  bitfield Flags {
    bool user_presence : 1;
    bool reserved1 : 1;
    bool user_verification : 1;
    bool backup_eligibility : 1;
    bool backup_state : 1;
    bool reserved5 : 1;
    bool attested_credential_data : 1;
    bool extension_data : 1;
  };

  struct AttestedCredentialData {
    be type::GUID aaguid;
    be u16 credential_id_length;
    if (credential_id_length == 16) {
      be type::GUID credential_id;
    } else {
      u8 credential_id_end = $ + credential_id_length;
      u8 credential_id[while($ < credential_id_end)];
    }
    CBOR credential_data;
  };

  struct Data {
    u8 rpIdHash[32];
    Flags flags;
    u32 signCount;
    AttestedCredentialData attested_credential_data;
  };
}

bool inAuthData;
enum MajorType : u8 {
  PositiveInteger = 0x00,
  NegativeInteger = 0x01,
  ByteString = 0x02,
  UTF8String = 0x03,
  Array = 0x04,
  Map = 0x05,
  SemanticTag = 0x06,
  Special = 0x07
};

enum ShortCount : u8 {
};

bitfield Header {
  short_count : 5;
  MajorType major_type : 3;
};

struct CBOR {
  Header header;
  if (
    header.major_type == MajorType::PositiveInteger &&
    header.short_count <= SHORT_COUNT_TINY_FIELD_MAX_NUM
  ) {
    u8 tiny_int = header.short_count;
  } else if (
    header.major_type == MajorType::NegativeInteger &&
    header.short_count <= SHORT_COUNT_TINY_FIELD_MAX_NUM
  ) {
    s8 tiny_int = -1 - header.short_count;
  } else if (
    header.major_type == MajorType::Special &&
    header.short_count <= SHORT_COUNT_TINY_FIELD_MAX_NUM
  ) {
    u8 special = header.short_count;
  } else if (
    header.major_type == MajorType::UTF8String ||
    header.major_type == MajorType::ByteString
  ) {
    u64 size = header.short_count;
    if (size > SHORT_COUNT_TINY_FIELD_MAX_NUM) {
      u8 extra_bytes = size - SHORT_COUNT_TINY_FIELD_MAX_NUM;
      if (extra_bytes == 1) {
        u8 extra_size;
        size = extra_size;
      } else if (extra_bytes == 2) {
        be u16 extra_size;
        size = extra_size;
      } else if (extra_bytes == 3) {
        be u32 extra_size;
        size = extra_size;
      } else if (extra_bytes == 4) {
        be u64 extra_size;
        size = extra_size;
      }
    }    
    u64 end = $ + size;
    if (header.major_type == MajorType::UTF8String) {
      char value[while($ < end)];
      if (value == "authData") {
        inAuthData = true;
      }
    } else if (header.major_type == MajorType::ByteString) {
      if (inAuthData) {
        inAuthData = false;
        authData::Data auth_data;
      } else {
        u8 value[while($ < end)];
      }
    }
  } else if (header.major_type == MajorType::Map
  ) {
    u8 size = header.short_count;
    CBOR items[size*2];
  }
}[[format_read("cbor_read")]];

fn cbor_read(CBOR c) {
  if (std::core::has_member(c, "tiny_int")) {
    return std::format("CBOR:{}={}", major_type_name(c.header.major_type), c.tiny_int);
  }
  if (std::core::has_member(c, "value")) {
    return std::format("CBOR:{}={}", major_type_name(c.header.major_type), c.value);
  }
  return std::format("CBOR:{}", major_type_name(c.header.major_type));
};

fn major_type_name(MajorType mt) {
  match(mt) {
    (MajorType::PositiveInteger): return "PositiveInteger";
    (MajorType::NegativeInteger): return "Integer";
    (MajorType::Special): return "Special";
    (MajorType::UTF8String): return "UTF8String";
    (MajorType::ByteString): return "ByteString";
    (MajorType::Array): return "Array";
    (MajorType::Map): return "Map";
    (MajorType::SemanticTag): return "SemanticTag";
  }
};

CBOR cbor[while(!std::mem::eof())] @ 0x00;

大体パースしたいところまでできた気がする。

このスクラップは4ヶ月前にクローズされました