@coordination-network/credential 0.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (63) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +67 -0
  3. package/dist/credential-message.d.ts +11 -0
  4. package/dist/credential-message.js +164 -0
  5. package/dist/credential-signer.d.ts +11 -0
  6. package/dist/credential-signer.js +17 -0
  7. package/dist/crypto.d.ts +51 -0
  8. package/dist/crypto.js +141 -0
  9. package/dist/decode.d.ts +8 -0
  10. package/dist/decode.js +27 -0
  11. package/dist/errors.d.ts +5 -0
  12. package/dist/errors.js +11 -0
  13. package/dist/index.d.ts +19 -0
  14. package/dist/index.js +35 -0
  15. package/dist/issue-credential.d.ts +4 -0
  16. package/dist/issue-credential.js +66 -0
  17. package/dist/issue.d.ts +9 -0
  18. package/dist/issue.js +65 -0
  19. package/dist/issuer-set.d.ts +2 -0
  20. package/dist/issuer-set.js +11 -0
  21. package/dist/present.d.ts +32 -0
  22. package/dist/present.js +73 -0
  23. package/dist/presentation-inputs.d.ts +77 -0
  24. package/dist/presentation-inputs.js +139 -0
  25. package/dist/profile.d.ts +32 -0
  26. package/dist/profile.js +122 -0
  27. package/dist/registry-client.d.ts +12 -0
  28. package/dist/registry-client.js +33 -0
  29. package/dist/resolver.d.ts +37 -0
  30. package/dist/resolver.js +37 -0
  31. package/dist/revocation.d.ts +25 -0
  32. package/dist/revocation.js +72 -0
  33. package/dist/signer.d.ts +19 -0
  34. package/dist/signer.js +2 -0
  35. package/dist/types.d.ts +111 -0
  36. package/dist/types.js +3 -0
  37. package/dist/verify.d.ts +29 -0
  38. package/dist/verify.js +102 -0
  39. package/dist/witness.d.ts +12 -0
  40. package/dist/witness.js +62 -0
  41. package/dist/zk-proof.d.ts +45 -0
  42. package/dist/zk-proof.js +130 -0
  43. package/package.json +55 -0
  44. package/src/credential-message.ts +173 -0
  45. package/src/credential-signer.ts +24 -0
  46. package/src/crypto.ts +146 -0
  47. package/src/decode.ts +25 -0
  48. package/src/errors.ts +12 -0
  49. package/src/index.ts +19 -0
  50. package/src/issue-credential.ts +78 -0
  51. package/src/issue.ts +83 -0
  52. package/src/issuer-set.ts +10 -0
  53. package/src/present.ts +101 -0
  54. package/src/presentation-inputs.ts +245 -0
  55. package/src/profile.ts +133 -0
  56. package/src/registry-client.ts +37 -0
  57. package/src/resolver.ts +62 -0
  58. package/src/revocation.ts +81 -0
  59. package/src/signer.ts +21 -0
  60. package/src/types.ts +116 -0
  61. package/src/verify.ts +118 -0
  62. package/src/witness.ts +69 -0
  63. package/src/zk-proof.ts +198 -0
@@ -0,0 +1,66 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.issueZkCredential = issueZkCredential;
4
+ const identity_1 = require("@coordination-network/identity");
5
+ const utils_1 = require("@noble/hashes/utils");
6
+ const identity_2 = require("@semaphore-protocol/identity");
7
+ const credential_message_1 = require("./credential-message");
8
+ const crypto_1 = require("./crypto");
9
+ const errors_1 = require("./errors");
10
+ const revocation_1 = require("./revocation");
11
+ async function issueZkCredential(params, signer, approval) {
12
+ if (!(await approval.isApprovedNetworkMember(params.issuerNetworkId))) {
13
+ throw new errors_1.IssuanceError("ISSUER_NOT_APPROVED", `issuer ${params.issuerNetworkId} is not an approved network member`);
14
+ }
15
+ if (!(params.validFrom < params.validUntil)) {
16
+ throw new errors_1.IssuanceError("INVALID_PARAMS", "validFrom must be before validUntil");
17
+ }
18
+ const holderKeyCommitment = identity_2.Identity.generateCommitment(params.identityPublicKey);
19
+ const holderKeyCommitmentBytes = (0, utils_1.hexToBytes)(holderKeyCommitment.toString(16).padStart(64, "0"));
20
+ let binding;
21
+ try {
22
+ binding = (0, identity_1.verifyCredentialBinding)({
23
+ nonce: params.nonce,
24
+ accountPublicKey: params.accountPublicKey,
25
+ confirmationKey: (0, crypto_1.jwkToPublicKeyBytes)(params.confirmationKey),
26
+ holderKeyCommitment: holderKeyCommitmentBytes,
27
+ accountSignature: params.accountPopSignature,
28
+ deviceSignature: params.devicePopSignature
29
+ });
30
+ }
31
+ catch (error) {
32
+ if (error instanceof identity_1.InvalidBindingInputError) {
33
+ throw new errors_1.IssuanceError("INVALID_PARAMS", `malformed binding input: ${error.message}`);
34
+ }
35
+ throw error;
36
+ }
37
+ if (binding.networkId !== params.subjectNetworkId.toLowerCase()) {
38
+ throw new errors_1.IssuanceError("SUBJECT_MISMATCH", "subjectNetworkId does not match the account public key");
39
+ }
40
+ if (!binding.valid) {
41
+ throw new errors_1.IssuanceError("BINDING_INVALID", "credential binding failed");
42
+ }
43
+ const ownershipOk = (0, revocation_1.verifyCommitmentOwnership)({
44
+ commitment: params.revocation.commitment,
45
+ publicKey: params.revocation.publicKey,
46
+ signature: params.revocation.ownershipSignature,
47
+ nonce: params.nonce
48
+ });
49
+ if (!ownershipOk) {
50
+ throw new errors_1.IssuanceError("COMMITMENT_OWNERSHIP_INVALID", "revocation commitment ownership proof failed");
51
+ }
52
+ const body = {
53
+ programmeHash: (0, credential_message_1.programmeHash)(params.approvalScope.programme),
54
+ countryCode: (0, credential_message_1.countryCode)(params.approvalScope.country),
55
+ notBefore: params.validFrom,
56
+ expiresAt: params.validUntil,
57
+ deviceKeyCommitment: (0, credential_message_1.deriveDeviceKeyCommitment)(params.confirmationKey),
58
+ holderKeyCommitment,
59
+ revocationCommitment: params.revocation.commitment
60
+ };
61
+ return {
62
+ body,
63
+ issuerPublicKey: await signer.getPublicKey(),
64
+ signature: await signer.sign((0, credential_message_1.zkCredentialCommitment)(body))
65
+ };
66
+ }
@@ -0,0 +1,9 @@
1
+ import { ApprovalResolver } from "./resolver";
2
+ import { Signer } from "./signer";
3
+ import { SdJwtIssueParams } from "./types";
4
+ /**
5
+ * Issues the delegated user approval credential (spec 07, issuance flow).
6
+ * Throws IssuanceError, without signing anything, if the issuer isn't
7
+ * approved or any challenge signature fails.
8
+ */
9
+ export declare function issueSdJwtCredential(params: SdJwtIssueParams, signer: Signer, approval: ApprovalResolver): Promise<string>;
package/dist/issue.js ADDED
@@ -0,0 +1,65 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.issueSdJwtCredential = issueSdJwtCredential;
4
+ const sd_jwt_vc_1 = require("@sd-jwt/sd-jwt-vc");
5
+ const identity_1 = require("@coordination-network/identity");
6
+ const crypto_1 = require("./crypto");
7
+ const errors_1 = require("./errors");
8
+ const profile_1 = require("./profile");
9
+ const revocation_1 = require("./revocation");
10
+ /**
11
+ * Issues the delegated user approval credential (spec 07, issuance flow).
12
+ * Throws IssuanceError, without signing anything, if the issuer isn't
13
+ * approved or any challenge signature fails.
14
+ */
15
+ async function issueSdJwtCredential(params, signer, approval) {
16
+ if (!(await approval.isApprovedNetworkMember(params.issuerNetworkId))) {
17
+ throw new errors_1.IssuanceError("ISSUER_NOT_APPROVED", `issuer ${params.issuerNetworkId} is not an approved network member`);
18
+ }
19
+ if (!(params.validFrom < params.validUntil)) {
20
+ throw new errors_1.IssuanceError("INVALID_PARAMS", "validFrom must be before validUntil");
21
+ }
22
+ let binding;
23
+ try {
24
+ binding = (0, identity_1.verifyCredentialBinding)({
25
+ nonce: params.nonce,
26
+ accountPublicKey: params.accountPublicKey,
27
+ confirmationKey: (0, crypto_1.jwkToPublicKeyBytes)(params.confirmationKey),
28
+ accountSignature: params.accountPopSignature,
29
+ deviceSignature: params.devicePopSignature
30
+ });
31
+ }
32
+ catch (error) {
33
+ if (error instanceof identity_1.InvalidBindingInputError) {
34
+ throw new errors_1.IssuanceError("INVALID_PARAMS", `malformed binding input: ${error.message}`);
35
+ }
36
+ throw error;
37
+ }
38
+ if (binding.networkId !== params.subjectNetworkId.toLowerCase()) {
39
+ throw new errors_1.IssuanceError("SUBJECT_MISMATCH", "subjectNetworkId does not match the account public key");
40
+ }
41
+ if (!binding.valid) {
42
+ throw new errors_1.IssuanceError("BINDING_INVALID", "credential binding failed");
43
+ }
44
+ const ownershipOk = (0, revocation_1.verifyCommitmentOwnership)({
45
+ commitment: params.revocation.commitment,
46
+ publicKey: params.revocation.publicKey,
47
+ signature: params.revocation.ownershipSignature,
48
+ nonce: params.nonce
49
+ });
50
+ if (!ownershipOk) {
51
+ throw new errors_1.IssuanceError("COMMITMENT_OWNERSHIP_INVALID", "revocation commitment ownership proof failed");
52
+ }
53
+ const payload = (0, profile_1.buildPayload)({ ...params, subjectNetworkId: binding.networkId }, `urn:uuid:${(0, crypto_1.randomUuid)()}`);
54
+ const sdjwt = new sd_jwt_vc_1.SDJwtVcInstance({
55
+ signer: (0, crypto_1.createSdJwtSigner)(signer),
56
+ signAlg: "ES256",
57
+ hasher: crypto_1.sdJwtHasher,
58
+ hashAlg: "sha-256",
59
+ saltGenerator: crypto_1.sdJwtSaltGenerator
60
+ });
61
+ // Cast: the library types `status` as its own status-list reference, but
62
+ // our custom cn_zk_revocation member is deliberate (spec 07) and the
63
+ // status-list fetcher only triggers on status.status_list.
64
+ return sdjwt.issue({ ...payload }, profile_1.DISCLOSURE_FRAME);
65
+ }
@@ -0,0 +1,2 @@
1
+ export { ApprovedIssuerSet, ISSUER_LEAF_DOMAIN, ISSUER_SET_MAX_DEPTH, issuerLeaf } from "@coordination-network/issuer-set";
2
+ export type { IssuerMembershipProof, IssuerSet } from "@coordination-network/issuer-set";
@@ -0,0 +1,11 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.issuerLeaf = exports.ISSUER_SET_MAX_DEPTH = exports.ISSUER_LEAF_DOMAIN = exports.ApprovedIssuerSet = void 0;
4
+ // The approved-issuer set now lives in @coordination-network/issuer-set, which the ApprovedIssuerSet.sol
5
+ // contract's TypeScript mirror also builds on. Re-exported here so every existing import of this module
6
+ // keeps working unchanged.
7
+ var issuer_set_1 = require("@coordination-network/issuer-set");
8
+ Object.defineProperty(exports, "ApprovedIssuerSet", { enumerable: true, get: function () { return issuer_set_1.ApprovedIssuerSet; } });
9
+ Object.defineProperty(exports, "ISSUER_LEAF_DOMAIN", { enumerable: true, get: function () { return issuer_set_1.ISSUER_LEAF_DOMAIN; } });
10
+ Object.defineProperty(exports, "ISSUER_SET_MAX_DEPTH", { enumerable: true, get: function () { return issuer_set_1.ISSUER_SET_MAX_DEPTH; } });
11
+ Object.defineProperty(exports, "issuerLeaf", { enumerable: true, get: function () { return issuer_set_1.issuerLeaf; } });
@@ -0,0 +1,32 @@
1
+ import { Resolver } from "./resolver";
2
+ import { DeviceSigner } from "./signer";
3
+ import { VerifyResult } from "./types";
4
+ export interface PresentOptions {
5
+ /** Verifier URN, pinned as the KB-JWT audience. */
6
+ audience: string;
7
+ /** Verifier-issued nonce, echoed in the KB-JWT. */
8
+ nonce: string;
9
+ /** Which approvalScope members to disclose. Both when omitted. */
10
+ disclose?: ("programme" | "country")[];
11
+ }
12
+ export interface VerifyPresentationOptions {
13
+ /** The verifier's own URN; the KB-JWT audience must match it. */
14
+ audience: string;
15
+ /** The nonce this verifier handed out; must be fresh and match. */
16
+ nonce: string;
17
+ /** Verification time, unix seconds. Defaults to now. */
18
+ currentDate?: number;
19
+ }
20
+ /**
21
+ * Builds a presentation from a stored credential: the JWS, the selected
22
+ * disclosures, and a KB-JWT signed by the device key binding the verifier's
23
+ * audience, the nonce and the sd_hash of the exact presented token (spec 07,
24
+ * verification flow step 7).
25
+ */
26
+ export declare function presentSdJwtCredential(token: string, deviceSigner: DeviceSigner, options: PresentOptions): Promise<string>;
27
+ /**
28
+ * Verifies a presentation: the full verifyCredential pipeline, plus the
29
+ * KB-JWT under the credential's cnf.jwk with nonce, sd_hash (both checked by
30
+ * the library) and audience (checked here) pinned.
31
+ */
32
+ export declare function verifySdJwtPresentation(presentation: string, resolver: Resolver, options: VerifyPresentationOptions): Promise<VerifyResult>;
@@ -0,0 +1,73 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.presentSdJwtCredential = presentSdJwtCredential;
4
+ exports.verifySdJwtPresentation = verifySdJwtPresentation;
5
+ const sd_jwt_vc_1 = require("@sd-jwt/sd-jwt-vc");
6
+ const crypto_1 = require("./crypto");
7
+ const verify_1 = require("./verify");
8
+ /**
9
+ * Builds a presentation from a stored credential: the JWS, the selected
10
+ * disclosures, and a KB-JWT signed by the device key binding the verifier's
11
+ * audience, the nonce and the sd_hash of the exact presented token (spec 07,
12
+ * verification flow step 7).
13
+ */
14
+ async function presentSdJwtCredential(token, deviceSigner, options) {
15
+ const disclose = options.disclose ?? ["programme", "country"];
16
+ const frame = { approvalScope: Object.fromEntries(disclose.map((member) => [member, true])) };
17
+ const sdjwt = new sd_jwt_vc_1.SDJwtVcInstance({
18
+ hasher: crypto_1.sdJwtHasher,
19
+ hashAlg: "sha-256",
20
+ saltGenerator: crypto_1.sdJwtSaltGenerator,
21
+ kbSigner: (0, crypto_1.createSdJwtSigner)(deviceSigner),
22
+ kbSignAlg: "ES256"
23
+ });
24
+ return sdjwt.present(token, frame, {
25
+ kb: {
26
+ payload: {
27
+ aud: options.audience,
28
+ nonce: options.nonce,
29
+ iat: Math.floor(Date.now() / 1000)
30
+ }
31
+ }
32
+ });
33
+ }
34
+ /**
35
+ * Verifies a presentation: the full verifyCredential pipeline, plus the
36
+ * KB-JWT under the credential's cnf.jwk with nonce, sd_hash (both checked by
37
+ * the library) and audience (checked here) pinned.
38
+ */
39
+ async function verifySdJwtPresentation(presentation, resolver, options) {
40
+ const now = options.currentDate ?? Math.floor(Date.now() / 1000);
41
+ let audience;
42
+ const { result } = await (0, verify_1.runPipeline)(presentation, resolver, now, async (issuerJwk, clampedNow) => {
43
+ const sdjwt = new sd_jwt_vc_1.SDJwtVcInstance({
44
+ verifier: (0, crypto_1.createSdJwtVerifier)(issuerJwk),
45
+ hasher: crypto_1.sdJwtHasher,
46
+ hashAlg: "sha-256",
47
+ kbVerifier: (data, signature, payload) => {
48
+ const cnfJwk = payload.cnf?.jwk;
49
+ if (!cnfJwk)
50
+ return false;
51
+ try {
52
+ return (0, crypto_1.createSdJwtVerifier)(cnfJwk)(data, signature);
53
+ }
54
+ catch {
55
+ return false;
56
+ }
57
+ }
58
+ });
59
+ const { payload, kb } = await sdjwt.verify(presentation, {
60
+ currentDate: clampedNow,
61
+ keyBindingNonce: options.nonce
62
+ });
63
+ audience = kb?.payload.aud;
64
+ return payload;
65
+ });
66
+ if (!result.ok) {
67
+ return result;
68
+ }
69
+ if (audience !== options.audience) {
70
+ return { ok: false, code: "PROOF_INVALID", message: "KB-JWT audience does not match this verifier" };
71
+ }
72
+ return result;
73
+ }
@@ -0,0 +1,77 @@
1
+ import { IssuerSet } from "./issuer-set";
2
+ import { DeviceSigner } from "./signer";
3
+ export declare const REVOCATION_MAX_DEPTH = 20;
4
+ export declare const PRESENTATION_DOMAIN: bigint;
5
+ export interface ZkPresentationInputs {
6
+ current_time: string;
7
+ challenge: string;
8
+ audience: string;
9
+ required_programme_hash: string;
10
+ required_country_code: string;
11
+ credential: {
12
+ programme_hash: string;
13
+ country_code: string;
14
+ not_before: string;
15
+ expires_at: string;
16
+ device_key_commitment: string;
17
+ holder_key_commitment: string;
18
+ revocation_commitment: string;
19
+ };
20
+ issuer_pub_x: string;
21
+ issuer_pub_y: string;
22
+ issuer_sig_r8_x: string;
23
+ issuer_sig_r8_y: string;
24
+ issuer_sig_s: string;
25
+ issuer_depth: string;
26
+ issuer_indices: boolean[];
27
+ issuer_siblings: string[];
28
+ device_pub_x: string[];
29
+ device_pub_y: string[];
30
+ device_signature: string[];
31
+ identity_pub_x: string;
32
+ identity_pub_y: string;
33
+ identity_sig_r8_x: string;
34
+ identity_sig_r8_y: string;
35
+ identity_sig_s: string;
36
+ revocation_pub_x: string;
37
+ revocation_pub_y: string;
38
+ revocation_sig_r8_x: string;
39
+ revocation_sig_r8_y: string;
40
+ revocation_sig_s: string;
41
+ revocation_low_leaf_value: string;
42
+ revocation_low_leaf_next: string;
43
+ revocation_depth: string;
44
+ revocation_indices: boolean[];
45
+ revocation_siblings: string[];
46
+ }
47
+ export interface ZkPresentationHolder {
48
+ identitySeed: Uint8Array;
49
+ revocationSecret: Uint8Array;
50
+ deviceSigner: DeviceSigner;
51
+ }
52
+ export interface IndexedTree {
53
+ root: bigint;
54
+ generateProof(value: bigint): {
55
+ proofType: 0 | 1;
56
+ root: bigint;
57
+ leaf: {
58
+ value: bigint;
59
+ nextValue: bigint;
60
+ };
61
+ leafIndex: number;
62
+ siblings: bigint[];
63
+ };
64
+ }
65
+ export interface ZkPresentationChainState {
66
+ approvedIssuers: IssuerSet;
67
+ revocations: IndexedTree;
68
+ }
69
+ export interface ZkPresentationRequest {
70
+ challenge: Uint8Array;
71
+ audience: string;
72
+ currentTime: number;
73
+ programme: string;
74
+ country: string;
75
+ }
76
+ export declare const challengeToField: (bytes: Uint8Array) => bigint;
77
+ export declare function buildZkPresentationInputs(serializedCredential: unknown, holder: ZkPresentationHolder, chain: ZkPresentationChainState, request: ZkPresentationRequest): Promise<ZkPresentationInputs>;
@@ -0,0 +1,139 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.challengeToField = exports.PRESENTATION_DOMAIN = exports.REVOCATION_MAX_DEPTH = void 0;
4
+ exports.buildZkPresentationInputs = buildZkPresentationInputs;
5
+ const identity_1 = require("@semaphore-protocol/identity");
6
+ const poseidon2_1 = require("poseidon-lite/poseidon2");
7
+ const poseidon4_1 = require("poseidon-lite/poseidon4");
8
+ const utils_1 = require("@noble/hashes/utils");
9
+ const credential_message_1 = require("./credential-message");
10
+ const crypto_1 = require("./crypto");
11
+ const issuer_set_1 = require("./issuer-set");
12
+ exports.REVOCATION_MAX_DEPTH = 20;
13
+ // keccak256(utf8("urn:cn:presentation:v1")) >> 8
14
+ exports.PRESENTATION_DOMAIN = BigInt("47717037732995119958831198142288202228464361330145547649000821135537239485");
15
+ const challengeToField = (bytes) => {
16
+ if (bytes.length !== 31)
17
+ throw new Error("presentation challenge must be exactly 31 bytes");
18
+ return BigInt(`0x${(0, utils_1.bytesToHex)(bytes)}`);
19
+ };
20
+ exports.challengeToField = challengeToField;
21
+ const fieldToBytes = (value) => (0, utils_1.hexToBytes)(value.toString(16).padStart(64, "0"));
22
+ const pathIndices = (leafIndex, depth) => Array.from({ length: depth }, (_, index) => ((leafIndex >> index) & 1) === 1);
23
+ function padPath(leafIndex, siblings, maxDepth, label) {
24
+ const depth = siblings.length;
25
+ if (depth > maxDepth) {
26
+ throw new Error(`${label} path depth ${depth} exceeds the circuit maximum of ${maxDepth}`);
27
+ }
28
+ const pad = (values, filler) => [...values, ...Array(maxDepth - values.length).fill(filler)];
29
+ return {
30
+ depth: depth.toString(),
31
+ indices: pad(pathIndices(leafIndex, depth), false),
32
+ siblings: pad(siblings, 0n).map(String)
33
+ };
34
+ }
35
+ function issuerPath(issuerSet, publicKey) {
36
+ const proof = issuerSet.generateProof(publicKey);
37
+ if (proof.root !== issuerSet.root)
38
+ throw new Error("issuer proof root does not match the approved issuer set");
39
+ const padded = padPath(proof.pathIndex, proof.siblings, issuer_set_1.ISSUER_SET_MAX_DEPTH, "issuer");
40
+ const depth = proof.siblings.length;
41
+ if (!Number.isSafeInteger(proof.pathIndex) || proof.pathIndex < 0 || proof.pathIndex >= 2 ** depth) {
42
+ throw new Error("issuer proof has an invalid path index");
43
+ }
44
+ let node = (0, issuer_set_1.issuerLeaf)(publicKey);
45
+ for (let level = 0; level < depth; level += 1) {
46
+ node = padded.indices[level]
47
+ ? (0, poseidon2_1.poseidon2)([proof.siblings[level], node])
48
+ : (0, poseidon2_1.poseidon2)([node, proof.siblings[level]]);
49
+ }
50
+ if (node !== proof.root)
51
+ throw new Error("issuer proof is invalid");
52
+ return padded;
53
+ }
54
+ async function buildZkPresentationInputs(serializedCredential, holder, chain, request) {
55
+ const credential = (0, credential_message_1.parseZkCredential)(serializedCredential);
56
+ if (!(0, credential_message_1.verifyZkCredential)(credential))
57
+ throw new Error("credential issuer signature is invalid");
58
+ const { body } = credential;
59
+ const identity = new identity_1.Identity(holder.identitySeed);
60
+ if (identity.commitment !== body.holderKeyCommitment) {
61
+ throw new Error("identity seed does not open the credential's holder-key commitment");
62
+ }
63
+ const revocation = new identity_1.Identity(holder.revocationSecret);
64
+ if (revocation.commitment !== body.revocationCommitment) {
65
+ throw new Error("revocation secret does not open the credential's revocation commitment");
66
+ }
67
+ const devicePublicKey = await holder.deviceSigner.getPublicKeyJwk();
68
+ if ((0, credential_message_1.deriveDeviceKeyCommitment)(devicePublicKey) !== body.deviceKeyCommitment) {
69
+ throw new Error("device signer does not open the credential's device-key commitment");
70
+ }
71
+ const issuer = issuerPath(chain.approvedIssuers, credential.issuerPublicKey);
72
+ const revocationProof = chain.revocations.root === 0n
73
+ ? {
74
+ proofType: 1,
75
+ root: 0n,
76
+ leaf: { value: 0n, nextValue: 0n },
77
+ leafIndex: 0,
78
+ siblings: []
79
+ }
80
+ : chain.revocations.generateProof(body.revocationCommitment);
81
+ if (revocationProof.root !== chain.revocations.root) {
82
+ throw new Error("revocation proof root does not match the supplied revocation tree");
83
+ }
84
+ if (revocationProof.proofType !== 1)
85
+ throw new Error("credential is revoked");
86
+ const revocationPath = padPath(revocationProof.leafIndex, revocationProof.siblings, exports.REVOCATION_MAX_DEPTH, "revocation");
87
+ const challenge = (0, exports.challengeToField)(request.challenge);
88
+ const audience = (0, credential_message_1.stringToField)(request.audience);
89
+ const credentialCommitment = (0, credential_message_1.zkCredentialCommitment)(body);
90
+ const sessionMessage = (0, poseidon4_1.poseidon4)([exports.PRESENTATION_DOMAIN, challenge, audience, credentialCommitment]);
91
+ const identitySignature = identity.signMessage(sessionMessage);
92
+ const revocationSignature = revocation.signMessage(sessionMessage);
93
+ const deviceSignature = await holder.deviceSigner.signDigest(fieldToBytes(sessionMessage));
94
+ if (deviceSignature.length !== 64)
95
+ throw new Error("device signer must return a compact 64-byte signature");
96
+ const encodedDeviceKey = (0, crypto_1.jwkToPublicKeyBytes)(devicePublicKey);
97
+ return {
98
+ current_time: String(request.currentTime),
99
+ challenge: String(challenge),
100
+ audience: String(audience),
101
+ required_programme_hash: String((0, credential_message_1.programmeHash)(request.programme)),
102
+ required_country_code: String((0, credential_message_1.countryCode)(request.country)),
103
+ credential: {
104
+ programme_hash: String(body.programmeHash),
105
+ country_code: String(body.countryCode),
106
+ not_before: String(body.notBefore),
107
+ expires_at: String(body.expiresAt),
108
+ device_key_commitment: String(body.deviceKeyCommitment),
109
+ holder_key_commitment: String(body.holderKeyCommitment),
110
+ revocation_commitment: String(body.revocationCommitment)
111
+ },
112
+ issuer_pub_x: String(credential.issuerPublicKey[0]),
113
+ issuer_pub_y: String(credential.issuerPublicKey[1]),
114
+ issuer_sig_r8_x: String(credential.signature.R8[0]),
115
+ issuer_sig_r8_y: String(credential.signature.R8[1]),
116
+ issuer_sig_s: String(credential.signature.S),
117
+ issuer_depth: issuer.depth,
118
+ issuer_indices: issuer.indices,
119
+ issuer_siblings: issuer.siblings,
120
+ device_pub_x: Array.from(encodedDeviceKey.slice(1, 33), String),
121
+ device_pub_y: Array.from(encodedDeviceKey.slice(33, 65), String),
122
+ device_signature: Array.from(deviceSignature, String),
123
+ identity_pub_x: String(identity.publicKey[0]),
124
+ identity_pub_y: String(identity.publicKey[1]),
125
+ identity_sig_r8_x: String(identitySignature.R8[0]),
126
+ identity_sig_r8_y: String(identitySignature.R8[1]),
127
+ identity_sig_s: String(identitySignature.S),
128
+ revocation_pub_x: String(revocation.publicKey[0]),
129
+ revocation_pub_y: String(revocation.publicKey[1]),
130
+ revocation_sig_r8_x: String(revocationSignature.R8[0]),
131
+ revocation_sig_r8_y: String(revocationSignature.R8[1]),
132
+ revocation_sig_s: String(revocationSignature.S),
133
+ revocation_low_leaf_value: String(revocationProof.leaf.value),
134
+ revocation_low_leaf_next: String(revocationProof.leaf.nextValue),
135
+ revocation_depth: revocationPath.depth,
136
+ revocation_indices: revocationPath.indices,
137
+ revocation_siblings: revocationPath.siblings
138
+ };
139
+ }
@@ -0,0 +1,32 @@
1
+ import { ApprovalCredentialClaims, Hex32, SdJwtIssueParams } from "./types";
2
+ /**
3
+ * The claims profile of the delegated user approval credential (spec 07):
4
+ * payload construction, the disclosure frame, and the shape validator that
5
+ * backs the MALFORMED verdict.
6
+ */
7
+ export declare const APPROVAL_CREDENTIAL_VCT = "urn:cn:credential:approval:v1";
8
+ /** Only the two approvalScope members are disclosable; everything else is plain. */
9
+ export declare const DISCLOSURE_FRAME: {
10
+ readonly approvalScope: {
11
+ readonly _sd: readonly ["programme", "country"];
12
+ };
13
+ };
14
+ export declare function networkIdToUrn(id: Hex32): string;
15
+ export declare function urnToNetworkId(urn: string): Hex32 | null;
16
+ /** Assembles the full signed payload; the disclosure frame is applied at issue time. */
17
+ export declare function buildPayload(params: SdJwtIssueParams, jti: string): ApprovalCredentialClaims;
18
+ type ClaimsCheck = {
19
+ ok: true;
20
+ claims: ApprovalCredentialClaims;
21
+ } | {
22
+ ok: false;
23
+ message: string;
24
+ };
25
+ /**
26
+ * Validates the reconstructed payload of a verified token against the
27
+ * profile. Strict on both sides: a missing required claim and an unknown
28
+ * plain claim both fail (spec 07; a tampered disclosure surfaces here as a
29
+ * missing approvalScope member).
30
+ */
31
+ export declare function validateClaims(payload: unknown): ClaimsCheck;
32
+ export {};
@@ -0,0 +1,122 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.DISCLOSURE_FRAME = exports.APPROVAL_CREDENTIAL_VCT = void 0;
4
+ exports.networkIdToUrn = networkIdToUrn;
5
+ exports.urnToNetworkId = urnToNetworkId;
6
+ exports.buildPayload = buildPayload;
7
+ exports.validateClaims = validateClaims;
8
+ const revocation_1 = require("./revocation");
9
+ /**
10
+ * The claims profile of the delegated user approval credential (spec 07):
11
+ * payload construction, the disclosure frame, and the shape validator that
12
+ * backs the MALFORMED verdict.
13
+ */
14
+ exports.APPROVAL_CREDENTIAL_VCT = "urn:cn:credential:approval:v1";
15
+ /** Only the two approvalScope members are disclosable; everything else is plain. */
16
+ exports.DISCLOSURE_FRAME = {
17
+ approvalScope: { _sd: ["programme", "country"] }
18
+ };
19
+ const NETWORK_ID_RE = /^0x[0-9a-f]{64}$/;
20
+ const NETWORK_URN_RE = /^urn:cn:network:[0-9a-f]{64}$/;
21
+ const CONTRACT_URN_RE = /^urn:cn:contract:.+$/;
22
+ const UUID_URN_RE = /^urn:uuid:[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/;
23
+ function networkIdToUrn(id) {
24
+ const normalized = id.toLowerCase();
25
+ if (!NETWORK_ID_RE.test(normalized)) {
26
+ throw new Error("networkId must be a 0x-prefixed 32-byte hex string");
27
+ }
28
+ return `urn:cn:network:${normalized.slice(2)}`;
29
+ }
30
+ function urnToNetworkId(urn) {
31
+ return NETWORK_URN_RE.test(urn) ? `0x${urn.slice("urn:cn:network:".length)}` : null;
32
+ }
33
+ /** Assembles the full signed payload; the disclosure frame is applied at issue time. */
34
+ function buildPayload(params, jti) {
35
+ return {
36
+ vct: exports.APPROVAL_CREDENTIAL_VCT,
37
+ iss: networkIdToUrn(params.issuerNetworkId),
38
+ sub: networkIdToUrn(params.subjectNetworkId),
39
+ jti,
40
+ nbf: params.validFrom,
41
+ exp: params.validUntil,
42
+ cnf: { jwk: params.confirmationKey },
43
+ status: (0, revocation_1.buildStatusClaim)(params.revocation.commitment, params.revocationRegistry),
44
+ approvalScope: { ...params.approvalScope }
45
+ };
46
+ }
47
+ const ALLOWED_TOP_LEVEL = new Set(["vct", "iss", "sub", "jti", "nbf", "exp", "cnf", "status", "approvalScope"]);
48
+ function isRecord(value) {
49
+ return typeof value === "object" && value !== null && !Array.isArray(value);
50
+ }
51
+ function isJwk(value) {
52
+ if (!isRecord(value))
53
+ return false;
54
+ const keys = Object.keys(value);
55
+ return (keys.length === 4 &&
56
+ value.kty === "EC" &&
57
+ value.crv === "P-256" &&
58
+ typeof value.x === "string" &&
59
+ typeof value.y === "string");
60
+ }
61
+ function isStatusClaim(value) {
62
+ if (!isRecord(value) || Object.keys(value).length !== 1)
63
+ return false;
64
+ const entry = value.cn_zk_revocation;
65
+ if (!isRecord(entry) || Object.keys(entry).length !== 2)
66
+ return false;
67
+ return (typeof entry.revocationRegistry === "string" &&
68
+ CONTRACT_URN_RE.test(entry.revocationRegistry) &&
69
+ typeof entry.revocationCommitment === "string" &&
70
+ /^[0-9]+$/.test(entry.revocationCommitment));
71
+ }
72
+ function isApprovalScope(value) {
73
+ if (!isRecord(value))
74
+ return false;
75
+ const keys = Object.keys(value);
76
+ return (keys.length === 2 &&
77
+ typeof value.programme === "string" &&
78
+ value.programme.length > 0 &&
79
+ typeof value.country === "string" &&
80
+ /^[A-Z]{3}$/.test(value.country));
81
+ }
82
+ /**
83
+ * Validates the reconstructed payload of a verified token against the
84
+ * profile. Strict on both sides: a missing required claim and an unknown
85
+ * plain claim both fail (spec 07; a tampered disclosure surfaces here as a
86
+ * missing approvalScope member).
87
+ */
88
+ function validateClaims(payload) {
89
+ if (!isRecord(payload)) {
90
+ return { ok: false, message: "payload is not an object" };
91
+ }
92
+ for (const key of Object.keys(payload)) {
93
+ if (!ALLOWED_TOP_LEVEL.has(key)) {
94
+ return { ok: false, message: `unknown claim: ${key}` };
95
+ }
96
+ }
97
+ if (payload.vct !== exports.APPROVAL_CREDENTIAL_VCT) {
98
+ return { ok: false, message: "vct missing or not the approval credential type" };
99
+ }
100
+ if (typeof payload.iss !== "string" || urnToNetworkId(payload.iss) === null) {
101
+ return { ok: false, message: "iss missing or not a urn:cn:network URN" };
102
+ }
103
+ if (typeof payload.sub !== "string" || urnToNetworkId(payload.sub) === null) {
104
+ return { ok: false, message: "sub missing or not a urn:cn:network URN" };
105
+ }
106
+ if (typeof payload.jti !== "string" || !UUID_URN_RE.test(payload.jti)) {
107
+ return { ok: false, message: "jti missing or not a urn:uuid URN" };
108
+ }
109
+ if (typeof payload.nbf !== "number" || typeof payload.exp !== "number") {
110
+ return { ok: false, message: "nbf/exp missing or not NumericDate values" };
111
+ }
112
+ if (!isRecord(payload.cnf) || Object.keys(payload.cnf).length !== 1 || !isJwk(payload.cnf.jwk)) {
113
+ return { ok: false, message: "cnf.jwk missing or not a P-256 JWK" };
114
+ }
115
+ if (!isStatusClaim(payload.status)) {
116
+ return { ok: false, message: "status.cn_zk_revocation missing or malformed" };
117
+ }
118
+ if (!isApprovalScope(payload.approvalScope)) {
119
+ return { ok: false, message: "approvalScope missing programme/country" };
120
+ }
121
+ return { ok: true, claims: payload };
122
+ }
@@ -0,0 +1,12 @@
1
+ import { Hex32 } from "./types";
2
+ /**
3
+ * Chain-backed read of NetworkMemberRegistry.isApproved (spec, network member
4
+ * registry). Satisfies the approval half of Resolver so it can back the
5
+ * issuance and verification governance gates once a registry is deployed.
6
+ */
7
+ export declare class NetworkMemberRegistryClient {
8
+ private readonly registryAddress;
9
+ private readonly client;
10
+ constructor(registryAddress: `0x${string}`, rpcUrl: string);
11
+ isApprovedNetworkMember(id: Hex32): Promise<boolean>;
12
+ }