sdjwt_oid4vc 0.1.1 copy "sdjwt_oid4vc: ^0.1.1" to clipboard
sdjwt_oid4vc: ^0.1.1 copied to clipboard

SD-JWT VC + OpenID4VCI/OpenID4VP protocol library for Dart wallets (holder role). Key- and HTTP-agnostic, pure logic, fully testable without hardware.

example/sdjwt_oid4vc_example.dart

// SD-JWT VC + OpenID4VCI/OpenID4VP — the holder (wallet) side, end to end.
//
// PURPOSE
// This library is what an EUDI-style wallet imports to:
//   1. receive a Verifiable Credential from an issuer (OpenID4VCI),
//   2. prove the issuer signed it (and that it is currently valid),
//   3. check it has not been revoked (Token Status List),
//   4. authenticate a verifier and present only the claims it asks for, with
//      holder key binding (OpenID4VP).
// It is holder-only, key-agnostic (you inject an `Es256Signer` — hardware in
// production) and HTTP-agnostic (inject an `Oid4vcHttp`, or use the default).
//
// This file plays issuer + holder + verifier in one process, so it runs with no
// network and no hardware:
//
//   dart run example/sdjwt_oid4vc_example.dart
//
// The calls a real wallet makes are flagged inline with  [WALLET USES] .

import 'dart:convert';

import 'package:archive/archive.dart';
import 'package:sdjwt_oid4vc/sdjwt_oid4vc.dart';
import 'package:sdjwt_oid4vc/testing.dart';

const _issuerId = 'https://issuer.example';
const _vct = 'https://issuer.example/extras-salariat/v1';
const _verifierId = 'https://verifier.example';
const _statusUri = 'https://issuer.example/status/1';

int _epoch(DateTime dt) => dt.toUtc().millisecondsSinceEpoch ~/ 1000;

Future<void> main() async {
  // The three independent keys of the EUDI trust model. In a real wallet the
  // holder key is hardware-backed behind the same `Es256Signer` interface.
  final holder = SoftwareEs256Signer.generate(); // (3) holder key
  final issuer = SoftwareEs256Signer.generate(); // (1) issuer seal
  final verifier = SoftwareEs256Signer.generate(); // (2) relying-party key

  // An in-memory directory serving the issuer's keys + status list, so the
  // example needs no network. A real wallet just passes `DefaultOid4vcHttp()`.
  final http = _Directory(issuer, await _statusListToken(issuer));

  // --- 1. Issuance --------------------------------------------------------
  // A real wallet does NOT mint here — it redeems an offer, supplying only the
  // holder signer:
  //   final compact = await Oid4vciClient(DefaultOid4vcHttp())
  //       .redeemOffer(offerUriOrJson: deepLink, txCode: code, signer: holder);
  // [WALLET USES] Oid4vciClient.redeemOffer
  final compact = await SdJwt.issue(
    claims: {
      'iss': _issuerId,
      'vct': _vct,
      'cnf': {'jwk': holder.publicJwkSync()}, // binds the holder key
      'iat': _epoch(DateTime.utc(2024)),
      'exp': _epoch(DateTime.utc(2099)), // validity window
      'given_name': 'Ada',
      'family_name': 'Lovelace',
      'employment_status': 'active',
      'status': {
        'status_list': {'uri': _statusUri, 'idx': 0}, // revocation pointer
      },
    },
    header: const {'kid': 'issuer-1'},
    selectivelyDisclosable: {'given_name', 'family_name', 'employment_status'},
    signer: issuer,
  );
  // [WALLET USES] SdJwt.parse — turn the stored compact string into a credential.
  final credential = SdJwt.parse(compact);
  print('1. Received credential: vct=${credential.vct}');

  // --- 2. Trust the issuer ------------------------------------------------
  // [WALLET USES] SdJwtVc.verifyIssuer — prove the issuer signed it and (with
  // enforceValidity) that it is inside its nbf..exp window right now.
  final trusted = await credential.verifyIssuer(
    IssuerTrust.issuerMetadata(), // key from <iss>/.well-known/jwt-vc-issuer
    http: http,
    enforceValidity: true,
    now: () => _epoch(DateTime.utc(2030)),
  );
  print('2. Issuer signature valid & in-window: $trusted');
  // With issuer *certificates* instead of metadata, choose a trust mode:
  //   IssuerTrust.signatureOnly()                          // integrity only
  //   IssuerTrust.x5cChain(trustAnchors: euTrustedListDer) // chain to the LOTL

  // --- 3. Revocation (Token Status List) ----------------------------------
  // [WALLET USES] SdJwtVc.statusListRef + StatusListResolver.resolve
  final status = await StatusListResolver(http).resolve(
    credential.statusListRef!,
    trust:
        IssuerTrust.issuerMetadata(), // also verifies the status token's seal
  );
  print('3. Revocation status: ${status.kind} (valid? ${status.isValid})');

  // --- 4. Authenticate the verifier --------------------------------------
  // The verifier sends a *signed* request object (JAR). Authenticate it before
  // presenting anything.
  final vp = Oid4vpClient(http);
  // [WALLET USES] Oid4vpClient.parseRequest / fetchRequest
  final request = vp.parseRequest(await _verifierRequest(verifier));
  // [WALLET USES] PresentationRequest.signature — the request's signing material.
  final rpAuthentic =
      request.signature!.verifyWithJwk(verifier.publicJwkSync());
  print('4. Verifier (RP) request signature authentic: $rpAuthentic');
  // In production: take request.signature.x5c, validate that certificate against
  // your reader trust list, check its SAN against request.clientIdValue, then
  // confirm integrity with request.signature.verifyWithX5cLeaf().

  // --- 5. Present only what was asked ------------------------------------
  // [WALLET USES] Oid4vpClient.match — pick the credential + claims for the query.
  final match = vp.match(request, [credential])!;
  print('5. Verifier asked for: ${match.requestedClaims}');
  // [WALLET USES] Oid4vpClient.buildVpToken — selective disclosure + a fresh
  // KB-JWT bound to the verifier's nonce/audience and signed by the holder key.
  // (For nested/array claims, use credential.present(disclosePaths: ...).)
  final vpToken = await vp.buildVpToken(
    credential: match.credential,
    revealClaims: match.requestedClaims,
    req: request,
    signer: holder,
  );
  // [WALLET USES] Oid4vpClient.submit — POST the vp_token to the response_uri:
  //   await vp.submit(req: request, vpToken: vpToken);

  // --- 6. What the verifier receives -------------------------------------
  final shown = SdJwt.parse(vpToken).resolveClaims();
  print('6. Disclosed: employment_status=${shown['employment_status']}');
  print('   given_name hidden?  ${!shown.containsKey('given_name')}');
  print('   family_name hidden? ${!shown.containsKey('family_name')}');
}

/// Builds a signed status list token where index 0 is "valid" (bit 0). The
/// issuer signs it; a real issuer publishes it at the credential's status URI.
Future<String> _statusListToken(SoftwareEs256Signer issuer) async {
  final lst =
      b64uEncode(const ZLibEncoder().encode([0x00])); // 1 byte, all zero
  final signingInput = Jws.signingInput(
    const {'alg': 'ES256', 'typ': 'statuslist+jwt'},
    {
      'iss': _issuerId,
      'sub': _statusUri,
      'status_list': {'bits': 1, 'lst': lst},
    },
  );
  return '$signingInput.${await issuer.signEs256(signingInput)}';
}

/// Builds the verifier's signed Request Object (JAR) asking for one claim.
Future<String> _verifierRequest(SoftwareEs256Signer verifier) async {
  final signingInput = Jws.signingInput(
    const {'alg': 'ES256', 'typ': 'oauth-authz-req+jwt'},
    {
      'client_id': _verifierId,
      'nonce': 'verifier-nonce-1',
      'response_uri': '$_verifierId/response',
      'dcql_query': {
        'credentials': [
          {
            'id': 'c1',
            'format': 'dc+sd-jwt',
            'meta': {
              'vct_values': [_vct],
            },
            'claims': [
              {
                'path': ['employment_status'],
              },
            ],
          },
        ],
      },
    },
  );
  return '$signingInput.${await verifier.signEs256(signingInput)}';
}

/// A two-route [Oid4vcHttp] serving the issuer's JWK set and its status list in
/// memory, so the example verifies real signatures with no network. A real
/// wallet uses [DefaultOid4vcHttp].
class _Directory implements Oid4vcHttp {
  _Directory(this._issuer, this._statusListToken);

  final SoftwareEs256Signer _issuer;
  final String _statusListToken;

  @override
  Future<HttpResp> get(Uri url, {Map<String, String>? headers}) async {
    if (url.path == '/status/1') return HttpResp(200, _statusListToken);
    // Everything else: the jwt-vc-issuer metadata (the issuer's JWK set).
    return HttpResp(
      200,
      jsonEncode({
        'issuer': _issuerId,
        'jwks': {
          'keys': [
            {..._issuer.publicJwkSync(), 'kid': 'issuer-1'},
          ],
        },
      }),
    );
  }

  @override
  Future<HttpResp> postForm(
    Uri url,
    Map<String, String> form, {
    Map<String, String>? headers,
  }) =>
      throw UnimplementedError();

  @override
  Future<HttpResp> postJson(
    Uri url,
    Object body, {
    Map<String, String>? headers,
  }) =>
      throw UnimplementedError();
}
0
likes
0
points
91
downloads

Publisher

unverified uploader

Weekly Downloads

SD-JWT VC + OpenID4VCI/OpenID4VP protocol library for Dart wallets (holder role). Key- and HTTP-agnostic, pure logic, fully testable without hardware.

Repository (GitHub)
View/report issues

Topics

#sd-jwt #openid4vci #openid4vp #verifiable-credentials #eudi

License

unknown (license)

Dependencies

archive, asn1lib, crypto, http, pointycastle

More

Packages that depend on sdjwt_oid4vc