🪪

[備忘録] @auth0/mdl で mDL を理解するためのメモ

2024/11/16に公開

はじめに

この記事は @auth0/mdl [1] という mDL (mobile Driver Licenses) の発行および検証が可能な Node.js のライブラリを動かしながら mDL の仕様に想いを馳せた際のただの個人メモです。ただの個人メモであり、内容の正確性は保証されず、定期的に内容の更新が行われる可能性が高い点に留意してください。

@auth0/mdl[1:1] は ISO/IEC 18013-5 [2] や ISO/IEC 18013-7 [3]といった仕様に基づいて実装されています。現時点において、前述の仕様はライセンスを購入した上で読む必要があるのため、無料で公開されており詳しい解説が記載されている記事 [4] などを適宜参照すると良いでしょう。

本記事では、諸々の整理にあたって以下の抽象化した mDL ecosystem の図をもとにおこないます。

mDL interfaces

なお、本記事では、credential の発行および検証について整理します。例えば、ISO/IEC 18013-5 [2:1] における Data Exchange のプロトコルとして Web API や OID4VP がありますが、これらの詳細や違いについては触れません。
あくまで Data model にフォーカスした上での整理であり、credential がどのように Issuing authority や mDL holder、mDL holder や mDL verifier 間で渡されるかについては本記事では一旦対象外とします。

本記事で試した際のコードは以下のリポジトリに動作確認可能な状態で配置します。

https://github.com/kg0r0/node-mdl-example

Issuing authority <-> mDL holder

ざっくりとですが、ISO/IEC 18013-5 における mdoc data model は以下のようになります。doctypeorg.iso.18013.5.1.mDL である必要があり、namespace に各属性情報が含まれます。
また、Issuing authority は発行するクレデンシャルに対して署名をおこないます。このときのデータは IssuerAuth と呼ばれ、mobile security object (MSO) という形式のデータとして扱われます。@auth0/mdl において、IssuerAuth の生成処理MSO の型定義からデータの形式を確認することができます。

@auth0/mdl[1:2] を使用した Issuing authority によるクレデンシャルの発行は以下のようにおこなうことができます。前述の通り、addIssuerNameSpace などで doctypenamespace を指定できることがわかります。また、IssuerAuth のための署名鍵や証明書、デバイスパブリックキーなども指定します。

import { MDoc, Document } from "@auth0/mdl";
import { inspect } from "node:util";

...

  {
    let issuerPrivateKey;
    let issuerCertificate;
    let devicePublicKey; // the public key for the device, as a JWK

    const document = await new Document('org.iso.18013.5.1.mDL')
      .addIssuerNameSpace('org.iso.18013.5.1', {
        family_name: 'Jones',
        given_name: 'Ava',
        birth_date: '2007-03-25',
      })
      .useDigestAlgorithm('SHA-256')
      .addValidityInfo({
        signed: new Date(),
      })
      .addDeviceKeyInfo({ deviceKey: devicePublicKey })
      .sign({
        issuerPrivateKey,
        issuerCertificate,
        alg: 'ES256',
      });

    issuerMDoc = new MDoc([document]).encode();
  }

上記のコードの出力は以下の通りになります。ここでは CBOR エンコードされている状態なので詳細はデコードしたうえで確認する必要があります。

Output:

[*] Issuing a credential:
<Buffer b9 00 03 67 76 65 72 73 69 6f 6e 63 31 2e 30 69 64 6f 63 75 6d 65 6e 74 73 81 a2 67 64 6f 63 54 79 70 65 75 6f 72 67 2e 69 73 6f 2e 31 38 30 31 33 2e ... 1755 more bytes>

以下が上記の出力をデコードした結果です。namespace や IssuerAuth が含まれていることが確認できます。

[*] CBOR decoded credential:
[
  {
    version: '1.0',
    documents: [
      {
        docType: 'org.iso.18013.5.1.mDL',
        issuerSigned: { // Returned data elements signed by the issuer
          nameSpaces: { // Returned data elements
            'org.iso.18013.5.1': [
              Tagged {
                tag: 24,
                value: <Buffer a4 68 64 69 67 65 73 74 49 44 00 71 65 6c 65 6d 65 6e 74 49 64 65 6e 74 69 66 69 65 72 6b 66 61 6d 69 6c 79 5f 6e 61 6d 65 6c 65 6c 65 6d 65 6e 74 56 ... 51 more bytes>,
                err: undefined
              },
              Tagged {
                tag: 24,
                value: <Buffer a4 68 64 69 67 65 73 74 49 44 01 71 65 6c 65 6d 65 6e 74 49 64 65 6e 74 69 66 69 65 72 6a 67 69 76 65 6e 5f 6e 61 6d 65 6c 65 6c 65 6d 65 6e 74 56 61 ... 48 more bytes>,
                err: undefined
              },
              Tagged {
                tag: 24,
                value: <Buffer a4 68 64 69 67 65 73 74 49 44 02 71 65 6c 65 6d 65 6e 74 49 64 65 6e 74 69 66 69 65 72 6a 62 69 72 74 68 5f 64 61 74 65 6c 65 6c 65 6d 65 6e 74 56 61 ... 55 more bytes>,
                err: undefined
              },
              Tagged {
                tag: 24,
                value: <Buffer a4 68 64 69 67 65 73 74 49 44 03 71 65 6c 65 6d 65 6e 74 49 64 65 6e 74 69 66 69 65 72 6b 61 67 65 5f 6f 76 65 72 5f 32 31 6c 65 6c 65 6d 65 6e 74 56 ... 46 more bytes>,
                err: undefined
              },
              Tagged {
                tag: 24,
                value: <Buffer a4 68 64 69 67 65 73 74 49 44 04 71 65 6c 65 6d 65 6e 74 49 64 65 6e 74 69 66 69 65 72 6b 61 67 65 5f 6f 76 65 72 5f 31 37 6c 65 6c 65 6d 65 6e 74 56 ... 46 more bytes>,
                err: undefined
              }
            ]
          },
          issuerAuth: [ // Contains the mobile security object (MSO) for issuer data authentication
            <Buffer a1 01 26>,
            Map(2) {
              4 => <Buffer 31 32 33 34>,
              33 => [
                <Buffer 30 82 02 2a 30 82 01 d0 a0 03 02 01 02 02 14 57 c6 cc d3 08 bd e4 3e ca 37 44 f2 a8 71 38 da bb b8 84 e8 30 0a 06 08 2a 86 48 ce 3d 04 03 02 30 53 31 ... 508 more bytes>
              ]
            },
            <Buffer d8 18 59 02 04 b9 00 06 67 76 65 72 73 69 6f 6e 63 31 2e 30 6f 64 69 67 65 73 74 41 6c 67 6f 72 69 74 68 6d 67 53 48 41 2d 32 35 36 6c 76 61 6c 75 65 ... 471 more bytes>,
            <Buffer c8 ea f2 7b 60 15 b8 05 31 b7 0d 91 a3 71 4b b3 ef ba 0d ba 2f 1f 90 f2 05 88 a7 77 79 85 ac 17 34 6b 58 3a ae 92 5c c2 18 1c 13 b9 c6 0f cb 12 03 27 ... 14 more bytes>
          ]
        }
      }
    ],
    status: 0
  }
]

mDL holder <-> mDL verifier

ざっくりとですが、ここでは、mDL verifier が mDL holder に対して提示して欲しい属性情報などを伝達し、mDL holder が mDL verifier に返すデータを生成して返します。なお、前述の通り、mDL verifier と mDL holder 間でどのようなリクエストやレスポンスによって提示する属性情報などを伝達するかについては一旦本記事においてはスコープ外とします。実際には、例えば OpenID for Verifiable Presentations (OID4VP)[5] や、その中で使われている DIF Presentation Exchange の Presentation Definition[6] に基づいておこなわれたりします。

データ形式にフォーカスした場合、先ほど Issuing authority によって発行されたクレデンシャルおよび、mDL holder によって生成された追加の署名データーを付与したレスポンスを mDL verifier に送信するというのが主な処理になります。このときのレスポンスは DeviceResponse と ISO/IEC 18013-5[2:2] では定義されている (気がします)。@auth0/mdl[1:3] においても DeviceResponse クラスの実装からどういったデータが含まれることになるか確認することができます。

なお、@auth0/mdl[1:4] を使用した mDL holder によるレスポンスの生成は以下の通り行うことができます。

import { MDoc, Document } from "@auth0/mdl";
import { inspect } from "node:util";

...

  {
    let devicePrivateKey; // the private key for the device, as a JWK

    // Parameters coming from the OID4VP transaction
    let mdocGeneratedNonce, clientId, responseUri, verifierGeneratedNonce;
    let presentationDefinition = {
      id: 'family_name_only',
      input_descriptors: [
        {
          id: 'org.iso.18013.5.1.mDL',
          format: { mso_mdoc: { alg: ['EdDSA', 'ES256'] } },
          constraints: {
            limit_disclosure: 'required',
            fields: [{
                path: ["$['org.iso.18013.5.1']['family_name']"],
                intent_to_retain: false,
              }],
          },
        },
      ],
    };

    deviceResponseMDoc = await DeviceResponse.from(issuerMDoc)
      .usingPresentationDefinition(presentationDefinition)
      .usingSessionTranscriptForOID4VP(mdocGeneratedNonce, clientId, responseUri, verifierGeneratedNonce)
      .authenticateWithSignature(devicePrivateKey, 'ES256')
      .sign();
  }

この時点で新しく出てきた概念としては Session Transcript があります。ISO/IEC 18013-5[2:3] において、Session Transcript は device retrieval のための複数のセキュリティメカニズムに使うことができるとされ、DeviceEngagementBytes, EReaderKeyBytes, Handover といったデータから構成されています。
なお、現時点において自分が確認する限り、具体的にどの値を使用して Session Transcript を生成するべきかはクレデンシャルを伝達するプロトコルに依存して異なります。例えば、@auth0/mdl[1:5]実装や、Digital Credentials API[7] のデモとしても使用されている実装をみる限りでも異なる Session Transcript の生成処理が確認できます (OID4VP に関していえば ISO/IEC 18013-7[3:1] の Annex B あたりに細かいことが書かれていました。)。

上記のコードの出力は以下の通りになります。ISO/IEC 18013-5[2:4] における DeviceResponse の documents の中身については、DeviceSignedDocumentクラスとして実装されているようで、IssuerSigned および DeviceSigned を含んでいることが確認できます。

Output:

MDoc {
  documents: [
    DeviceSignedDocument {
      docType: 'org.iso.18013.5.1.mDL', // Document type returned
      issuerSigned: [Object],           // Returned data elements signed by the issuer
      deviceSigned: [Object]            // Returned data elements signed by the mdoc
    }
  ],
  version: '1.0',
  status: 0,
  documentErrors: []
}

mDL verifier <-> Issuing authority

最後に mDL verifier は mDL holder から受け取った DeviceResponse を検証します。
verify() の中では、主に verifyIssuerSignature() および verifyDeviceSignature() が呼び出されます。
Issuer Signature を検証することで、mdoc が意図した Issuer から発行されたものであることを検証できます。また、Device Signature を検証することで mdoc が意図した Holder から発行されたものであることを検証することができます (Verifiable Credentials Data Model において、Verifiable Credential の Proof および、Verifiable Presentation の Proof を検証するのと似たような感じだと思っています)。

import { Verifier } from "@auth0/mdl";
import { inspect } from "node:util";
import fs from "node:fs";

(async () => {
  const encodedDeviceResponse = Buffer.from(encodedDeviceResponseHex, 'hex');
  const encodedSessionTranscript = Buffer.from(encodedSessionTranscriptHex, 'hex');
  const ephemeralReaderKey = Buffer.from(ephemeralReaderKeyHex, 'hex');

  const trustedCerts = [fs.readFileSync('./caCert1.pem')/*, ... */];
  const verifier = new Verifier(trustedCerts);
  const mdoc = await verifier.verify(encodedDeviceResponse, {
    ephemeralReaderKey,
    encodedSessionTranscript,
  });

  //at this point the issuer and device signature are valids.
  inspect(mdoc);
})();

検証が成功すると以下のような結果を得ることができます。Presentation Definition で指定した属性情報などが取得できていることが確認できます。

Output:

[*] Verifying a credential:
MDoc {
  documents: [
    DeviceSignedDocument {
      docType: 'org.iso.18013.5.1.mDL',
      issuerSigned: [Object],
      deviceSigned: [Object]
    }
  ],
  version: '1.0',
  status: 0,
  documentErrors: []
}
[*] Getting diagnostic information:
{
  general: { version: '1.0', type: 'DeviceResponse', status: 0, documents: 1 },
  validityInfo: {
    signed: 2024-11-16T09:58:53.000Z,
    validFrom: 2024-11-16T09:58:53.000Z,
    validUntil: 2025-11-16T09:58:53.000Z,
    expectedUpdate: undefined
  },
  issuerCertificate: {
    subjectName: 'C=US, ST=New York, L=Albany, O=NY DMV, OU=NY DMV',
    pem: '-----BEGIN CERTIFICATE-----\n' +
      'MIICKjCCAdCgAwIBAgIUV8bM0wi95D7KN0TyqHE42ru4hOgwCgYIKoZIzj0EAwIw\n' +
      'UzELMAkGA1UEBhMCVVMxETAPBgNVBAgMCE5ldyBZb3JrMQ8wDQYDVQQHDAZBbGJh\n' +
      'bnkxDzANBgNVBAoMBk5ZIERNVjEPMA0GA1UECwwGTlkgRE1WMB4XDTIzMDkxNDE0\n' +
      'NTUxOFoXDTMzMDkxMTE0NTUxOFowUzELMAkGA1UEBhMCVVMxETAPBgNVBAgMCE5l\n' +
      'dyBZb3JrMQ8wDQYDVQQHDAZBbGJhbnkxDzANBgNVBAoMBk5ZIERNVjEPMA0GA1UE\n' +
      'CwwGTlkgRE1WMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEiTwtg0eQbcbNabf2\n' +
      'Nq9L/VM/lhhPCq2s0Qgw2kRx29tgrBcNHPxTT64tnc1Ij3dH/fl42SXqMenpCDw4\n' +
      'K6ntU6OBgTB/MB0GA1UdDgQWBBSrbS4DuR1JIkAzj7zK3v2TM+r2xzAfBgNVHSME\n' +
      'GDAWgBSrbS4DuR1JIkAzj7zK3v2TM+r2xzAPBgNVHRMBAf8EBTADAQH/MCwGCWCG\n' +
      'SAGG+EIBDQQfFh1PcGVuU1NMIEdlbmVyYXRlZCBDZXJ0aWZpY2F0ZTAKBggqhkjO\n' +
      'PQQDAgNIADBFAiAJ/Qyrl7A+ePZOdNfc7ohmjEdqCvxaos6//gfTvncuqQIhANo4\n' +
      'q8mKCA9J8k/+zh//yKbN1bLAtdqPx7dnrDqV3Lg+\n' +
      '-----END CERTIFICATE-----',
    notBefore: 2023-09-14T14:55:18.000Z,
    notAfter: 2033-09-11T14:55:18.000Z,
    serialNumber: '57c6ccd308bde43eca3744f2a87138dabbb884e8',
    thumbprint: '77cc9f091ee425664ce71735670c56e7260a6f3b'
  },
  issuerSignature: {
    alg: 'ES256',
    isValid: true,
    reasons: [],
    digests: { 'org.iso.18013.5.1': 5 }
  },
  deviceKey: {
    jwk: {
      kty: 'EC',
      x: 'iBh5ynojixm_D0wfjADpouGbp6b3Pq6SuFHU3htQhVk',
      y: 'oxS1OAORJ7XNUHNfVFGeM8E0RQVFxWA62fJj-sxW03c',
      crv: 'P-256',
      d: 'eRpAZr3eV5xMMnPG3kWjg90Y-bBff9LqmlQuk49HUtA'
    }
  },
  deviceSignature: { alg: 'ES256', isValid: true, reasons: [] },
  dataIntegrity: { disclosedAttributes: '1 of 5', isValid: true, reasons: [] },
  attributes: [
    {
      ns: 'org.iso.18013.5.1',
      id: 'family_name',
      value: 'Jones',
      isValid: true,
      matchCertificate: undefined
    }
  ],
  deviceAttributes: []
}

おわりに

改めて記載しますが、これはただのメモです。mDL では他の仕様には出てこない用語などもあり、処理の流れや詳細を忘れがちなので既存の実装を触りながら色々とメモとして残しみました。
途中で力尽きて一部説明が雑になったりしている部分があるので気が向いたら継続的に内容を更新するかもしれません。未来の自分の何かしらの役に立てば幸いです。

脚注
  1. @auth0/mdl https://www.npmjs.com/package/@auth0/mdl ↩︎ ↩︎ ↩︎ ↩︎ ↩︎ ↩︎

  2. ISO/IEC 18013-5:2021 Personal identification — ISO-compliant driving licence Part 5: Mobile driving licence (mDL) application Authentication Method Reference Values https://www.iso.org/standard/69084.html ↩︎ ↩︎ ↩︎ ↩︎ ↩︎

  3. ISO/IEC TS 18013-7:2024 Personal identification — ISO-compliant driving licence Part 7: Mobile driving licence (mDL) add-on functions https://www.iso.org/standard/82772.html ↩︎ ↩︎

  4. Mobile Driver's License Implementation Guidelines, Version 1.3 https://www.aamva.org/assets/best-practices,-guides,-standards,-manuals,-whitepapers/mobile-driver-s-license-implementation-guidelines-1-2 ↩︎

  5. OpenID for Verifiable Presentations - https://openid.net/specs/openid-4-verifiable-presentations-1_0.html ↩︎

  6. DIF Presentation Exchange https://identity.foundation/presentation-exchange/#presentation-definition ↩︎

  7. Introducing the Digital Credentials API origin trial https://developer.chrome.com/blog/digital-credentials-api-origin-trial ↩︎

GitHubで編集を提案

Discussion