@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.
- package/LICENSE +21 -0
- package/README.md +67 -0
- package/dist/credential-message.d.ts +11 -0
- package/dist/credential-message.js +164 -0
- package/dist/credential-signer.d.ts +11 -0
- package/dist/credential-signer.js +17 -0
- package/dist/crypto.d.ts +51 -0
- package/dist/crypto.js +141 -0
- package/dist/decode.d.ts +8 -0
- package/dist/decode.js +27 -0
- package/dist/errors.d.ts +5 -0
- package/dist/errors.js +11 -0
- package/dist/index.d.ts +19 -0
- package/dist/index.js +35 -0
- package/dist/issue-credential.d.ts +4 -0
- package/dist/issue-credential.js +66 -0
- package/dist/issue.d.ts +9 -0
- package/dist/issue.js +65 -0
- package/dist/issuer-set.d.ts +2 -0
- package/dist/issuer-set.js +11 -0
- package/dist/present.d.ts +32 -0
- package/dist/present.js +73 -0
- package/dist/presentation-inputs.d.ts +77 -0
- package/dist/presentation-inputs.js +139 -0
- package/dist/profile.d.ts +32 -0
- package/dist/profile.js +122 -0
- package/dist/registry-client.d.ts +12 -0
- package/dist/registry-client.js +33 -0
- package/dist/resolver.d.ts +37 -0
- package/dist/resolver.js +37 -0
- package/dist/revocation.d.ts +25 -0
- package/dist/revocation.js +72 -0
- package/dist/signer.d.ts +19 -0
- package/dist/signer.js +2 -0
- package/dist/types.d.ts +111 -0
- package/dist/types.js +3 -0
- package/dist/verify.d.ts +29 -0
- package/dist/verify.js +102 -0
- package/dist/witness.d.ts +12 -0
- package/dist/witness.js +62 -0
- package/dist/zk-proof.d.ts +45 -0
- package/dist/zk-proof.js +130 -0
- package/package.json +55 -0
- package/src/credential-message.ts +173 -0
- package/src/credential-signer.ts +24 -0
- package/src/crypto.ts +146 -0
- package/src/decode.ts +25 -0
- package/src/errors.ts +12 -0
- package/src/index.ts +19 -0
- package/src/issue-credential.ts +78 -0
- package/src/issue.ts +83 -0
- package/src/issuer-set.ts +10 -0
- package/src/present.ts +101 -0
- package/src/presentation-inputs.ts +245 -0
- package/src/profile.ts +133 -0
- package/src/registry-client.ts +37 -0
- package/src/resolver.ts +62 -0
- package/src/revocation.ts +81 -0
- package/src/signer.ts +21 -0
- package/src/types.ts +116 -0
- package/src/verify.ts +118 -0
- package/src/witness.ts +69 -0
- package/src/zk-proof.ts +198 -0
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.NetworkMemberRegistryClient = void 0;
|
|
4
|
+
const viem_1 = require("viem");
|
|
5
|
+
const REGISTRY_ABI = [
|
|
6
|
+
{
|
|
7
|
+
type: "function",
|
|
8
|
+
name: "isApproved",
|
|
9
|
+
stateMutability: "view",
|
|
10
|
+
inputs: [{ name: "memberNetworkId", type: "bytes32" }],
|
|
11
|
+
outputs: [{ type: "bool" }]
|
|
12
|
+
}
|
|
13
|
+
];
|
|
14
|
+
/**
|
|
15
|
+
* Chain-backed read of NetworkMemberRegistry.isApproved (spec, network member
|
|
16
|
+
* registry). Satisfies the approval half of Resolver so it can back the
|
|
17
|
+
* issuance and verification governance gates once a registry is deployed.
|
|
18
|
+
*/
|
|
19
|
+
class NetworkMemberRegistryClient {
|
|
20
|
+
constructor(registryAddress, rpcUrl) {
|
|
21
|
+
this.registryAddress = registryAddress;
|
|
22
|
+
this.client = (0, viem_1.createPublicClient)({ transport: (0, viem_1.http)(rpcUrl) });
|
|
23
|
+
}
|
|
24
|
+
async isApprovedNetworkMember(id) {
|
|
25
|
+
return this.client.readContract({
|
|
26
|
+
address: this.registryAddress,
|
|
27
|
+
abi: REGISTRY_ABI,
|
|
28
|
+
functionName: "isApproved",
|
|
29
|
+
args: [id]
|
|
30
|
+
});
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
exports.NetworkMemberRegistryClient = NetworkMemberRegistryClient;
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { Hex32, Jwk } from "./types";
|
|
2
|
+
/**
|
|
3
|
+
* Pluggable resolution of urn:cn: identifiers to registry state (spec 07,
|
|
4
|
+
* trust model). In-memory for now; registry contracts back a chain
|
|
5
|
+
* implementation later.
|
|
6
|
+
*/
|
|
7
|
+
export interface Resolver {
|
|
8
|
+
isApprovedNetworkMember(id: Hex32): Promise<boolean>;
|
|
9
|
+
getCredentialPublicKey(id: Hex32): Promise<Jwk | null>;
|
|
10
|
+
isRevoked(commitment: bigint): Promise<boolean>;
|
|
11
|
+
}
|
|
12
|
+
/** The approval half of Resolver — all issuance needs, and all the chain-backed registry client provides. */
|
|
13
|
+
export type ApprovalResolver = Pick<Resolver, "isApprovedNetworkMember">;
|
|
14
|
+
/**
|
|
15
|
+
* Write side of the revocation registry. Separate from Resolver because
|
|
16
|
+
* verifiers never hold it.
|
|
17
|
+
*/
|
|
18
|
+
export interface RevocationWriter {
|
|
19
|
+
revoke(commitment: bigint): Promise<void>;
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* In-memory registry state for tests and the demo issuer service. The seed
|
|
23
|
+
* methods stand in for the one-time on-chain writes (member admission, key
|
|
24
|
+
* publication); the async interface methods mirror the chain reads.
|
|
25
|
+
*/
|
|
26
|
+
export declare class InMemoryResolver implements Resolver, RevocationWriter {
|
|
27
|
+
private readonly approved;
|
|
28
|
+
private readonly credentialKeys;
|
|
29
|
+
private readonly revoked;
|
|
30
|
+
approveMember(id: Hex32): void;
|
|
31
|
+
removeMember(id: Hex32): void;
|
|
32
|
+
setCredentialPublicKey(id: Hex32, jwk: Jwk): void;
|
|
33
|
+
isApprovedNetworkMember(id: Hex32): Promise<boolean>;
|
|
34
|
+
getCredentialPublicKey(id: Hex32): Promise<Jwk | null>;
|
|
35
|
+
isRevoked(commitment: bigint): Promise<boolean>;
|
|
36
|
+
revoke(commitment: bigint): Promise<void>;
|
|
37
|
+
}
|
package/dist/resolver.js
ADDED
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.InMemoryResolver = void 0;
|
|
4
|
+
/**
|
|
5
|
+
* In-memory registry state for tests and the demo issuer service. The seed
|
|
6
|
+
* methods stand in for the one-time on-chain writes (member admission, key
|
|
7
|
+
* publication); the async interface methods mirror the chain reads.
|
|
8
|
+
*/
|
|
9
|
+
class InMemoryResolver {
|
|
10
|
+
constructor() {
|
|
11
|
+
this.approved = new Set();
|
|
12
|
+
this.credentialKeys = new Map();
|
|
13
|
+
this.revoked = new Set();
|
|
14
|
+
}
|
|
15
|
+
approveMember(id) {
|
|
16
|
+
this.approved.add(id);
|
|
17
|
+
}
|
|
18
|
+
removeMember(id) {
|
|
19
|
+
this.approved.delete(id);
|
|
20
|
+
}
|
|
21
|
+
setCredentialPublicKey(id, jwk) {
|
|
22
|
+
this.credentialKeys.set(id, jwk);
|
|
23
|
+
}
|
|
24
|
+
async isApprovedNetworkMember(id) {
|
|
25
|
+
return this.approved.has(id);
|
|
26
|
+
}
|
|
27
|
+
async getCredentialPublicKey(id) {
|
|
28
|
+
return this.credentialKeys.get(id) ?? null;
|
|
29
|
+
}
|
|
30
|
+
async isRevoked(commitment) {
|
|
31
|
+
return this.revoked.has(commitment.toString());
|
|
32
|
+
}
|
|
33
|
+
async revoke(commitment) {
|
|
34
|
+
this.revoked.add(commitment.toString());
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
exports.InMemoryResolver = InMemoryResolver;
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { EddsaPublicKey, EddsaSignature, StatusClaim } from "./types";
|
|
2
|
+
/** Generates a fresh 32-byte revocation secret r. Device-side only. */
|
|
3
|
+
export declare function generateRevocationSecret(): Uint8Array;
|
|
4
|
+
/** commitment = new Identity(r).commitment (Semaphore V4). */
|
|
5
|
+
export declare function deriveRevocationCommitment(r: Uint8Array): bigint;
|
|
6
|
+
/**
|
|
7
|
+
* Signs the issuer's challenge nonce with the Baby Jubjub EdDSA key behind
|
|
8
|
+
* r, proving the device owns the commitment it asks the issuer to embed.
|
|
9
|
+
*/
|
|
10
|
+
export declare function signCommitmentOwnership(r: Uint8Array, nonce: Uint8Array): {
|
|
11
|
+
publicKey: EddsaPublicKey;
|
|
12
|
+
signature: EddsaSignature;
|
|
13
|
+
};
|
|
14
|
+
/**
|
|
15
|
+
* Issuer-side check: verifies the EdDSA signature over the nonce, recomputes
|
|
16
|
+
* the commitment from the public key, and compares it to the claimed one.
|
|
17
|
+
*/
|
|
18
|
+
export declare function verifyCommitmentOwnership(params: {
|
|
19
|
+
commitment: bigint;
|
|
20
|
+
publicKey: EddsaPublicKey;
|
|
21
|
+
signature: EddsaSignature;
|
|
22
|
+
nonce: Uint8Array;
|
|
23
|
+
}): boolean;
|
|
24
|
+
/** Builds the status.cn_zk_revocation claim for the signed body (spec 07). */
|
|
25
|
+
export declare function buildStatusClaim(commitment: bigint, revocationRegistry: string): StatusClaim;
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.generateRevocationSecret = generateRevocationSecret;
|
|
4
|
+
exports.deriveRevocationCommitment = deriveRevocationCommitment;
|
|
5
|
+
exports.signCommitmentOwnership = signCommitmentOwnership;
|
|
6
|
+
exports.verifyCommitmentOwnership = verifyCommitmentOwnership;
|
|
7
|
+
exports.buildStatusClaim = buildStatusClaim;
|
|
8
|
+
const identity_1 = require("@semaphore-protocol/identity");
|
|
9
|
+
const utils_1 = require("@noble/hashes/utils");
|
|
10
|
+
/**
|
|
11
|
+
* Device-side revocation material (spec 08). r is 32 random bytes, generated
|
|
12
|
+
* on the device, never leaving it. Everything below takes r as Uint8Array,
|
|
13
|
+
* never as a hex string: the two encodings derive different commitments
|
|
14
|
+
* (Semaphore hashes the raw input), and the regression test holds that line.
|
|
15
|
+
*/
|
|
16
|
+
function assertSecret(r) {
|
|
17
|
+
if (r.length !== 32) {
|
|
18
|
+
throw new Error("revocation secret must be 32 bytes");
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
function nonceToBigint(nonce) {
|
|
22
|
+
if (nonce.length === 0) {
|
|
23
|
+
throw new Error("nonce must not be empty");
|
|
24
|
+
}
|
|
25
|
+
return BigInt(`0x${(0, utils_1.bytesToHex)(nonce)}`);
|
|
26
|
+
}
|
|
27
|
+
/** Generates a fresh 32-byte revocation secret r. Device-side only. */
|
|
28
|
+
function generateRevocationSecret() {
|
|
29
|
+
return (0, utils_1.randomBytes)(32);
|
|
30
|
+
}
|
|
31
|
+
/** commitment = new Identity(r).commitment (Semaphore V4). */
|
|
32
|
+
function deriveRevocationCommitment(r) {
|
|
33
|
+
assertSecret(r);
|
|
34
|
+
return new identity_1.Identity(r).commitment;
|
|
35
|
+
}
|
|
36
|
+
/**
|
|
37
|
+
* Signs the issuer's challenge nonce with the Baby Jubjub EdDSA key behind
|
|
38
|
+
* r, proving the device owns the commitment it asks the issuer to embed.
|
|
39
|
+
*/
|
|
40
|
+
function signCommitmentOwnership(r, nonce) {
|
|
41
|
+
assertSecret(r);
|
|
42
|
+
const identity = new identity_1.Identity(r);
|
|
43
|
+
const signature = identity.signMessage(nonceToBigint(nonce));
|
|
44
|
+
return {
|
|
45
|
+
publicKey: [identity.publicKey[0], identity.publicKey[1]],
|
|
46
|
+
signature: { R8: [signature.R8[0], signature.R8[1]], S: signature.S }
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
/**
|
|
50
|
+
* Issuer-side check: verifies the EdDSA signature over the nonce, recomputes
|
|
51
|
+
* the commitment from the public key, and compares it to the claimed one.
|
|
52
|
+
*/
|
|
53
|
+
function verifyCommitmentOwnership(params) {
|
|
54
|
+
try {
|
|
55
|
+
if (!identity_1.Identity.verifySignature(nonceToBigint(params.nonce), params.signature, params.publicKey)) {
|
|
56
|
+
return false;
|
|
57
|
+
}
|
|
58
|
+
return identity_1.Identity.generateCommitment(params.publicKey) === params.commitment;
|
|
59
|
+
}
|
|
60
|
+
catch {
|
|
61
|
+
return false;
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
/** Builds the status.cn_zk_revocation claim for the signed body (spec 07). */
|
|
65
|
+
function buildStatusClaim(commitment, revocationRegistry) {
|
|
66
|
+
return {
|
|
67
|
+
cn_zk_revocation: {
|
|
68
|
+
revocationRegistry,
|
|
69
|
+
revocationCommitment: commitment.toString(10)
|
|
70
|
+
}
|
|
71
|
+
};
|
|
72
|
+
}
|
package/dist/signer.d.ts
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { Jwk } from "./types";
|
|
2
|
+
/**
|
|
3
|
+
* Issuer signing key, KMS-shaped. sign receives the JWS signing input in
|
|
4
|
+
* message mode (the service applies SHA-256 itself, never a precomputed
|
|
5
|
+
* digest) and returns the raw 64-byte r||s signature.
|
|
6
|
+
*/
|
|
7
|
+
export interface Signer {
|
|
8
|
+
getPublicKeyJwk(): Promise<Jwk>;
|
|
9
|
+
sign(message: Uint8Array): Promise<Uint8Array>;
|
|
10
|
+
}
|
|
11
|
+
/**
|
|
12
|
+
* Holder P-256 device key: the secure element in the app, a software key in
|
|
13
|
+
* tests. Same contract as Signer; a separate type because the two keys must
|
|
14
|
+
* never be interchangeable in signatures.
|
|
15
|
+
*/
|
|
16
|
+
export interface DeviceSigner extends Signer {
|
|
17
|
+
/** Signs an already-computed 32-byte digest, without hashing it again. */
|
|
18
|
+
signDigest(digest: Uint8Array): Promise<Uint8Array>;
|
|
19
|
+
}
|
package/dist/signer.js
ADDED
package/dist/types.d.ts
ADDED
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
/** Credential and issuance types. */
|
|
2
|
+
/** A 32-byte value as a 0x-prefixed hex string (e.g. a networkId). */
|
|
3
|
+
export type Hex32 = `0x${string}`;
|
|
4
|
+
/** An EC public key in JWK form (RFC 7517). This profile uses P-256 only. */
|
|
5
|
+
export interface Jwk {
|
|
6
|
+
kty: "EC";
|
|
7
|
+
crv: "P-256";
|
|
8
|
+
x: string;
|
|
9
|
+
y: string;
|
|
10
|
+
}
|
|
11
|
+
export interface ApprovalScope {
|
|
12
|
+
/** Programme the approval covers. */
|
|
13
|
+
programme: string;
|
|
14
|
+
/** ISO 3166-1 alpha-3 country code. */
|
|
15
|
+
country: string;
|
|
16
|
+
}
|
|
17
|
+
/** Baby Jubjub EdDSA public key, shaped as Semaphore V4 exposes it. */
|
|
18
|
+
export type EddsaPublicKey = [bigint, bigint];
|
|
19
|
+
/** Baby Jubjub EdDSA signature, shaped as Semaphore V4 exposes it. */
|
|
20
|
+
export interface EddsaSignature {
|
|
21
|
+
R8: [bigint, bigint];
|
|
22
|
+
S: bigint;
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* The custom status member inside the signed body (spec 07). The commitment
|
|
26
|
+
* is issuer-attested and visible to WS1 verifiers by design.
|
|
27
|
+
*/
|
|
28
|
+
export interface StatusClaim {
|
|
29
|
+
cn_zk_revocation: {
|
|
30
|
+
/** urn:cn:contract: URN of the on-chain revocation registry. */
|
|
31
|
+
revocationRegistry: string;
|
|
32
|
+
revocationCommitment: string;
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
/** The reconstructed claim set of a verified credential. */
|
|
36
|
+
export interface ApprovalCredentialClaims {
|
|
37
|
+
vct: string;
|
|
38
|
+
iss: string;
|
|
39
|
+
sub: string;
|
|
40
|
+
jti: string;
|
|
41
|
+
nbf: number;
|
|
42
|
+
exp: number;
|
|
43
|
+
cnf: {
|
|
44
|
+
jwk: Jwk;
|
|
45
|
+
};
|
|
46
|
+
status: StatusClaim;
|
|
47
|
+
approvalScope: ApprovalScope;
|
|
48
|
+
}
|
|
49
|
+
export interface IssuanceRequest {
|
|
50
|
+
issuerNetworkId: Hex32;
|
|
51
|
+
subjectNetworkId: Hex32;
|
|
52
|
+
approvalScope: ApprovalScope;
|
|
53
|
+
validFrom: number;
|
|
54
|
+
validUntil: number;
|
|
55
|
+
confirmationKey: Jwk;
|
|
56
|
+
accountPublicKey: Uint8Array;
|
|
57
|
+
nonce: Uint8Array;
|
|
58
|
+
devicePopSignature: Uint8Array;
|
|
59
|
+
accountPopSignature: Uint8Array;
|
|
60
|
+
revocation: {
|
|
61
|
+
commitment: bigint;
|
|
62
|
+
publicKey: EddsaPublicKey;
|
|
63
|
+
ownershipSignature: EddsaSignature;
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
export interface SdJwtIssueParams extends IssuanceRequest {
|
|
67
|
+
revocationRegistry: string;
|
|
68
|
+
}
|
|
69
|
+
export interface ZkCredentialIssueParams extends IssuanceRequest {
|
|
70
|
+
identityPublicKey: EddsaPublicKey;
|
|
71
|
+
}
|
|
72
|
+
export interface ZkCredentialBody {
|
|
73
|
+
programmeHash: bigint;
|
|
74
|
+
countryCode: number;
|
|
75
|
+
notBefore: number;
|
|
76
|
+
expiresAt: number;
|
|
77
|
+
deviceKeyCommitment: bigint;
|
|
78
|
+
holderKeyCommitment: bigint;
|
|
79
|
+
revocationCommitment: bigint;
|
|
80
|
+
}
|
|
81
|
+
export interface ZkCredential {
|
|
82
|
+
body: ZkCredentialBody;
|
|
83
|
+
issuerPublicKey: EddsaPublicKey;
|
|
84
|
+
signature: EddsaSignature;
|
|
85
|
+
}
|
|
86
|
+
export interface SerializedZkCredential {
|
|
87
|
+
body: {
|
|
88
|
+
programmeHash: string;
|
|
89
|
+
countryCode: number;
|
|
90
|
+
notBefore: number;
|
|
91
|
+
expiresAt: number;
|
|
92
|
+
deviceKeyCommitment: string;
|
|
93
|
+
holderKeyCommitment: string;
|
|
94
|
+
revocationCommitment: string;
|
|
95
|
+
};
|
|
96
|
+
issuerPublicKey: [string, string];
|
|
97
|
+
signature: {
|
|
98
|
+
R8: [string, string];
|
|
99
|
+
S: string;
|
|
100
|
+
};
|
|
101
|
+
}
|
|
102
|
+
export type VerifyErrorCode = "MALFORMED" | "ISSUER_NOT_APPROVED" | "KEY_NOT_FOUND" | "PROOF_INVALID" | "EXPIRED" | "NOT_YET_VALID" | "REVOKED";
|
|
103
|
+
/** Checks run in spec order (07, verification flow) and the first failure wins. */
|
|
104
|
+
export type VerifyResult = {
|
|
105
|
+
ok: true;
|
|
106
|
+
claims: ApprovalCredentialClaims;
|
|
107
|
+
} | {
|
|
108
|
+
ok: false;
|
|
109
|
+
code: VerifyErrorCode;
|
|
110
|
+
message: string;
|
|
111
|
+
};
|
package/dist/types.js
ADDED
package/dist/verify.d.ts
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { Resolver } from "./resolver";
|
|
2
|
+
import { Hex32, Jwk, VerifyResult } from "./types";
|
|
3
|
+
export interface VerifyOptions {
|
|
4
|
+
/** Verification time, unix seconds. Defaults to now. */
|
|
5
|
+
currentDate?: number;
|
|
6
|
+
}
|
|
7
|
+
/**
|
|
8
|
+
* The shared verification pipeline (spec 07, verification flow), check order
|
|
9
|
+
* first-failure-wins: decode/structure -> issuer approved -> key fetch ->
|
|
10
|
+
* signature and disclosure digests -> claims shape -> validity window ->
|
|
11
|
+
* revocation. A disclosure whose digest matches nothing is silently dropped
|
|
12
|
+
* by the library (phase 1 spike), so it surfaces in the claims-shape step as
|
|
13
|
+
* a missing approvalScope member and fails MALFORMED.
|
|
14
|
+
*
|
|
15
|
+
* The library's own nbf/exp check runs against a clamped timestamp so the
|
|
16
|
+
* window verdict (EXPIRED vs NOT_YET_VALID) always comes from our check,
|
|
17
|
+
* after the signature has been verified.
|
|
18
|
+
*/
|
|
19
|
+
declare function runPipeline(token: string, resolver: Resolver, now: number, verifyWithLibrary: (issuerJwk: Jwk, clampedNow: number) => Promise<Record<string, unknown>>): Promise<{
|
|
20
|
+
result: VerifyResult;
|
|
21
|
+
issuerId?: Hex32;
|
|
22
|
+
}>;
|
|
23
|
+
/**
|
|
24
|
+
* Verifies a compact SD-JWT credential with all its disclosures — the
|
|
25
|
+
* holder's verify-on-receipt and any check that does not involve a live
|
|
26
|
+
* presentation. Key binding is not checked here; that is verifyPresentation.
|
|
27
|
+
*/
|
|
28
|
+
export declare function verifySdJwtCredential(token: string, resolver: Resolver, options?: VerifyOptions): Promise<VerifyResult>;
|
|
29
|
+
export { runPipeline };
|
package/dist/verify.js
ADDED
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.verifySdJwtCredential = verifySdJwtCredential;
|
|
4
|
+
exports.runPipeline = runPipeline;
|
|
5
|
+
const sd_jwt_vc_1 = require("@sd-jwt/sd-jwt-vc");
|
|
6
|
+
const crypto_1 = require("./crypto");
|
|
7
|
+
const profile_1 = require("./profile");
|
|
8
|
+
const fail = (code, message) => ({ ok: false, code, message });
|
|
9
|
+
/**
|
|
10
|
+
* The shared verification pipeline (spec 07, verification flow), check order
|
|
11
|
+
* first-failure-wins: decode/structure -> issuer approved -> key fetch ->
|
|
12
|
+
* signature and disclosure digests -> claims shape -> validity window ->
|
|
13
|
+
* revocation. A disclosure whose digest matches nothing is silently dropped
|
|
14
|
+
* by the library (phase 1 spike), so it surfaces in the claims-shape step as
|
|
15
|
+
* a missing approvalScope member and fails MALFORMED.
|
|
16
|
+
*
|
|
17
|
+
* The library's own nbf/exp check runs against a clamped timestamp so the
|
|
18
|
+
* window verdict (EXPIRED vs NOT_YET_VALID) always comes from our check,
|
|
19
|
+
* after the signature has been verified.
|
|
20
|
+
*/
|
|
21
|
+
async function runPipeline(token, resolver, now, verifyWithLibrary) {
|
|
22
|
+
let payload;
|
|
23
|
+
try {
|
|
24
|
+
const jwt = token.split("~")[0];
|
|
25
|
+
const segments = jwt.split(".");
|
|
26
|
+
if (segments.length !== 3) {
|
|
27
|
+
return { result: fail("MALFORMED", "not a compact SD-JWT") };
|
|
28
|
+
}
|
|
29
|
+
payload = JSON.parse(new TextDecoder().decode((0, crypto_1.base64urlToBytes)(segments[1])));
|
|
30
|
+
if (typeof payload !== "object" || payload === null) {
|
|
31
|
+
return { result: fail("MALFORMED", "JWS payload is not an object") };
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
catch {
|
|
35
|
+
return { result: fail("MALFORMED", "token could not be decoded") };
|
|
36
|
+
}
|
|
37
|
+
const issuerId = typeof payload.iss === "string" ? (0, profile_1.urnToNetworkId)(payload.iss) : null;
|
|
38
|
+
if (issuerId === null) {
|
|
39
|
+
return { result: fail("MALFORMED", "iss missing or not a urn:cn:network URN") };
|
|
40
|
+
}
|
|
41
|
+
if (!(await resolver.isApprovedNetworkMember(issuerId))) {
|
|
42
|
+
return {
|
|
43
|
+
result: fail("ISSUER_NOT_APPROVED", `issuer ${payload.iss} is not an approved network member`),
|
|
44
|
+
issuerId
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
const issuerJwk = await resolver.getCredentialPublicKey(issuerId);
|
|
48
|
+
if (issuerJwk === null) {
|
|
49
|
+
return { result: fail("KEY_NOT_FOUND", `no credential public key published for ${payload.iss}`), issuerId };
|
|
50
|
+
}
|
|
51
|
+
// Clamp the library's clock inside the token's window; our own check below decides the verdict.
|
|
52
|
+
let clampedNow = now;
|
|
53
|
+
if (typeof payload.exp === "number" && clampedNow > payload.exp)
|
|
54
|
+
clampedNow = payload.exp;
|
|
55
|
+
if (typeof payload.nbf === "number" && clampedNow < payload.nbf)
|
|
56
|
+
clampedNow = payload.nbf;
|
|
57
|
+
let reconstructed;
|
|
58
|
+
try {
|
|
59
|
+
reconstructed = await verifyWithLibrary(issuerJwk, clampedNow);
|
|
60
|
+
}
|
|
61
|
+
catch (error) {
|
|
62
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
63
|
+
if (message.includes("not yet valid"))
|
|
64
|
+
return { result: fail("NOT_YET_VALID", message), issuerId };
|
|
65
|
+
if (message.includes("expired"))
|
|
66
|
+
return { result: fail("EXPIRED", message), issuerId };
|
|
67
|
+
return { result: fail("PROOF_INVALID", message), issuerId };
|
|
68
|
+
}
|
|
69
|
+
const checked = (0, profile_1.validateClaims)(reconstructed);
|
|
70
|
+
if (!checked.ok) {
|
|
71
|
+
return { result: fail("MALFORMED", checked.message), issuerId };
|
|
72
|
+
}
|
|
73
|
+
const claims = checked.claims;
|
|
74
|
+
if (now < claims.nbf) {
|
|
75
|
+
return { result: fail("NOT_YET_VALID", "credential is not yet within its validity window"), issuerId };
|
|
76
|
+
}
|
|
77
|
+
if (now > claims.exp) {
|
|
78
|
+
return { result: fail("EXPIRED", "credential validity window has passed"), issuerId };
|
|
79
|
+
}
|
|
80
|
+
if (await resolver.isRevoked(BigInt(claims.status.cn_zk_revocation.revocationCommitment))) {
|
|
81
|
+
return { result: fail("REVOKED", "credential commitment is in the revocation registry"), issuerId };
|
|
82
|
+
}
|
|
83
|
+
return { result: { ok: true, claims }, issuerId };
|
|
84
|
+
}
|
|
85
|
+
/**
|
|
86
|
+
* Verifies a compact SD-JWT credential with all its disclosures — the
|
|
87
|
+
* holder's verify-on-receipt and any check that does not involve a live
|
|
88
|
+
* presentation. Key binding is not checked here; that is verifyPresentation.
|
|
89
|
+
*/
|
|
90
|
+
async function verifySdJwtCredential(token, resolver, options) {
|
|
91
|
+
const now = options?.currentDate ?? Math.floor(Date.now() / 1000);
|
|
92
|
+
const { result } = await runPipeline(token, resolver, now, async (issuerJwk, clampedNow) => {
|
|
93
|
+
const sdjwt = new sd_jwt_vc_1.SDJwtVcInstance({
|
|
94
|
+
verifier: (0, crypto_1.createSdJwtVerifier)(issuerJwk),
|
|
95
|
+
hasher: crypto_1.sdJwtHasher,
|
|
96
|
+
hashAlg: "sha-256"
|
|
97
|
+
});
|
|
98
|
+
const { payload } = await sdjwt.verify(token, { currentDate: clampedNow });
|
|
99
|
+
return payload;
|
|
100
|
+
});
|
|
101
|
+
return result;
|
|
102
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import type { CompiledCircuit } from "@noir-lang/noir_js";
|
|
2
|
+
import { ZkPresentationInputs } from "./presentation-inputs";
|
|
3
|
+
type CircuitAbi = CompiledCircuit["abi"];
|
|
4
|
+
/**
|
|
5
|
+
* Flattens {@link ZkPresentationInputs} into the ordered `string[]` witness a
|
|
6
|
+
* native Noir prover consumes, walking the compiled circuit's ABI (public and
|
|
7
|
+
* private parameters, in declaration order) rather than the JS object's own
|
|
8
|
+
* key order — a struct field reorder in the circuit changes the ABI and so
|
|
9
|
+
* changes this output, instead of silently desyncing from it.
|
|
10
|
+
*/
|
|
11
|
+
export declare function zkPresentationInputsToWitness(abi: CircuitAbi, inputs: ZkPresentationInputs): string[];
|
|
12
|
+
export {};
|
package/dist/witness.js
ADDED
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.zkPresentationInputsToWitness = zkPresentationInputsToWitness;
|
|
4
|
+
const DECIMAL_RE = /^(0|[1-9][0-9]*)$/;
|
|
5
|
+
function scalarWitness(value, path) {
|
|
6
|
+
if (typeof value !== "string" || !DECIMAL_RE.test(value)) {
|
|
7
|
+
throw new Error(`zkPresentationInputsToWitness: ${path} must be a canonical decimal string`);
|
|
8
|
+
}
|
|
9
|
+
return value;
|
|
10
|
+
}
|
|
11
|
+
function booleanWitness(value, path) {
|
|
12
|
+
if (typeof value !== "boolean") {
|
|
13
|
+
throw new Error(`zkPresentationInputsToWitness: ${path} must be a boolean`);
|
|
14
|
+
}
|
|
15
|
+
return value ? "1" : "0";
|
|
16
|
+
}
|
|
17
|
+
/** Depth-first ABI walk (spec: field/integer -> decimal string, boolean -> "1"/"0", array/struct recurse). */
|
|
18
|
+
function emit(type, value, path, witness) {
|
|
19
|
+
switch (type.kind) {
|
|
20
|
+
case "field":
|
|
21
|
+
case "integer":
|
|
22
|
+
witness.push(scalarWitness(value, path));
|
|
23
|
+
return;
|
|
24
|
+
case "boolean":
|
|
25
|
+
witness.push(booleanWitness(value, path));
|
|
26
|
+
return;
|
|
27
|
+
case "array":
|
|
28
|
+
if (!Array.isArray(value) || value.length !== type.length) {
|
|
29
|
+
throw new Error(`zkPresentationInputsToWitness: ${path} must be an array of length ${type.length}`);
|
|
30
|
+
}
|
|
31
|
+
value.forEach((element, index) => emit(type.type, element, `${path}[${index}]`, witness));
|
|
32
|
+
return;
|
|
33
|
+
case "struct": {
|
|
34
|
+
if (typeof value !== "object" || value === null || Array.isArray(value)) {
|
|
35
|
+
throw new Error(`zkPresentationInputsToWitness: ${path} must be an object`);
|
|
36
|
+
}
|
|
37
|
+
const fields = value;
|
|
38
|
+
type.fields.forEach((field) => emit(field.type, fields[field.name], `${path}.${field.name}`, witness));
|
|
39
|
+
return;
|
|
40
|
+
}
|
|
41
|
+
default:
|
|
42
|
+
throw new Error(`zkPresentationInputsToWitness: ${path} has unsupported ABI type "${type.kind}"`);
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
/**
|
|
46
|
+
* Flattens {@link ZkPresentationInputs} into the ordered `string[]` witness a
|
|
47
|
+
* native Noir prover consumes, walking the compiled circuit's ABI (public and
|
|
48
|
+
* private parameters, in declaration order) rather than the JS object's own
|
|
49
|
+
* key order — a struct field reorder in the circuit changes the ABI and so
|
|
50
|
+
* changes this output, instead of silently desyncing from it.
|
|
51
|
+
*/
|
|
52
|
+
function zkPresentationInputsToWitness(abi, inputs) {
|
|
53
|
+
const record = { ...inputs };
|
|
54
|
+
const witness = [];
|
|
55
|
+
for (const parameter of abi.parameters) {
|
|
56
|
+
if (!(parameter.name in record)) {
|
|
57
|
+
throw new Error(`zkPresentationInputsToWitness: unknown ABI parameter "${parameter.name}"`);
|
|
58
|
+
}
|
|
59
|
+
emit(parameter.type, record[parameter.name], parameter.name, witness);
|
|
60
|
+
}
|
|
61
|
+
return witness;
|
|
62
|
+
}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import type { BackendOptions, ProofData, UltraHonkBackendOptions } from "@aztec/bb.js";
|
|
2
|
+
import type { CompiledCircuit } from "@noir-lang/noir_js";
|
|
3
|
+
import { ZkPresentationInputs } from "./presentation-inputs";
|
|
4
|
+
declare const PUBLIC_INPUT_NAMES: readonly ["current_time", "challenge", "audience", "required_programme_hash", "required_country_code"];
|
|
5
|
+
type PublicInputName = (typeof PUBLIC_INPUT_NAMES)[number];
|
|
6
|
+
export type ZkCredentialCircuit = CompiledCircuit;
|
|
7
|
+
export type ZkPresentationProof = ProofData;
|
|
8
|
+
export interface ExpectedZkPresentation {
|
|
9
|
+
currentTime: number;
|
|
10
|
+
challenge: Uint8Array;
|
|
11
|
+
audience: string;
|
|
12
|
+
programme: string;
|
|
13
|
+
country: string;
|
|
14
|
+
}
|
|
15
|
+
export type ZkPresentationVerifyResult = {
|
|
16
|
+
ok: true;
|
|
17
|
+
approvedIssuerRoot: bigint;
|
|
18
|
+
revocationRoot: bigint;
|
|
19
|
+
} | {
|
|
20
|
+
ok: false;
|
|
21
|
+
code: "PUBLIC_INPUT_MISMATCH";
|
|
22
|
+
publicInput?: PublicInputName;
|
|
23
|
+
} | {
|
|
24
|
+
ok: false;
|
|
25
|
+
code: "PROOF_INVALID";
|
|
26
|
+
};
|
|
27
|
+
export interface ZkProofSystemOptions {
|
|
28
|
+
backend?: BackendOptions;
|
|
29
|
+
proof?: UltraHonkBackendOptions;
|
|
30
|
+
}
|
|
31
|
+
export declare const assertZkCredentialCircuitAbi: (circuit: CompiledCircuit) => void;
|
|
32
|
+
export declare class ZkCredentialProofSystem {
|
|
33
|
+
private readonly noir;
|
|
34
|
+
private readonly backend;
|
|
35
|
+
private readonly api;
|
|
36
|
+
private readonly proofOptions?;
|
|
37
|
+
private constructor();
|
|
38
|
+
static create(circuit: CompiledCircuit, options?: ZkProofSystemOptions): Promise<ZkCredentialProofSystem>;
|
|
39
|
+
generateProof(inputs: ZkPresentationInputs): Promise<ZkPresentationProof>;
|
|
40
|
+
verifyProof(proof: ZkPresentationProof, expected: ExpectedZkPresentation): Promise<ZkPresentationVerifyResult>;
|
|
41
|
+
destroy(): Promise<void>;
|
|
42
|
+
}
|
|
43
|
+
export declare function generateZkPresentationProof(circuit: CompiledCircuit, inputs: ZkPresentationInputs, options?: ZkProofSystemOptions): Promise<ZkPresentationProof>;
|
|
44
|
+
export declare function verifyZkPresentationProof(circuit: CompiledCircuit, proof: ZkPresentationProof, expected: ExpectedZkPresentation, options?: ZkProofSystemOptions): Promise<ZkPresentationVerifyResult>;
|
|
45
|
+
export {};
|