@coordination-network/credential 0.0.1 → 0.0.2
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/README.md +16 -45
- package/dist/crypto.d.ts +3 -11
- package/dist/crypto.js +1 -31
- package/dist/index.d.ts +0 -5
- package/dist/index.js +0 -5
- package/dist/issue-credential.d.ts +5 -1
- package/dist/issue-credential.js +15 -2
- package/dist/revocation.d.ts +5 -5
- package/dist/revocation.js +8 -12
- package/dist/signer.d.ts +2 -13
- package/dist/types.d.ts +0 -38
- package/dist/zk-proof.d.ts +10 -0
- package/dist/zk-proof.js +4 -0
- package/package.json +5 -8
- package/src/crypto.ts +3 -34
- package/src/index.ts +0 -5
- package/src/issue-credential.ts +24 -5
- package/src/revocation.ts +9 -13
- package/src/signer.ts +2 -14
- package/src/types.ts +0 -36
- package/src/zk-proof.ts +10 -0
- package/dist/decode.d.ts +0 -8
- package/dist/decode.js +0 -27
- package/dist/issue.d.ts +0 -9
- package/dist/issue.js +0 -65
- package/dist/present.d.ts +0 -32
- package/dist/present.js +0 -73
- package/dist/profile.d.ts +0 -32
- package/dist/profile.js +0 -122
- package/dist/verify.d.ts +0 -29
- package/dist/verify.js +0 -102
- package/src/decode.ts +0 -25
- package/src/issue.ts +0 -83
- package/src/present.ts +0 -101
- package/src/profile.ts +0 -133
- package/src/verify.ts +0 -118
package/src/revocation.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
|
+
import { inPrimeOrderSubgroup } from "@coordination-network/issuer-set"
|
|
1
2
|
import { Identity } from "@semaphore-protocol/identity"
|
|
2
3
|
import { bytesToHex, randomBytes } from "@noble/hashes/utils"
|
|
3
|
-
import { EddsaPublicKey, EddsaSignature
|
|
4
|
+
import { EddsaPublicKey, EddsaSignature } from "./types"
|
|
4
5
|
|
|
5
6
|
/**
|
|
6
7
|
* Device-side revocation material (spec 08). r is 32 random bytes, generated
|
|
@@ -51,8 +52,10 @@ export function signCommitmentOwnership(
|
|
|
51
52
|
}
|
|
52
53
|
|
|
53
54
|
/**
|
|
54
|
-
* Issuer-side check:
|
|
55
|
-
* the commitment from the public key, and
|
|
55
|
+
* Issuer-side check: requires a prime-order public key, verifies the EdDSA
|
|
56
|
+
* signature over the nonce, recomputes the commitment from the public key, and
|
|
57
|
+
* compares it to the claimed one. A low-order key is refused because its
|
|
58
|
+
* signatures need no secret.
|
|
56
59
|
*/
|
|
57
60
|
export function verifyCommitmentOwnership(params: {
|
|
58
61
|
commitment: bigint
|
|
@@ -61,6 +64,9 @@ export function verifyCommitmentOwnership(params: {
|
|
|
61
64
|
nonce: Uint8Array
|
|
62
65
|
}): boolean {
|
|
63
66
|
try {
|
|
67
|
+
if (!inPrimeOrderSubgroup(params.publicKey)) {
|
|
68
|
+
return false
|
|
69
|
+
}
|
|
64
70
|
if (!Identity.verifySignature(nonceToBigint(params.nonce), params.signature, params.publicKey)) {
|
|
65
71
|
return false
|
|
66
72
|
}
|
|
@@ -69,13 +75,3 @@ export function verifyCommitmentOwnership(params: {
|
|
|
69
75
|
return false
|
|
70
76
|
}
|
|
71
77
|
}
|
|
72
|
-
|
|
73
|
-
/** Builds the status.cn_zk_revocation claim for the signed body (spec 07). */
|
|
74
|
-
export function buildStatusClaim(commitment: bigint, revocationRegistry: string): StatusClaim {
|
|
75
|
-
return {
|
|
76
|
-
cn_zk_revocation: {
|
|
77
|
-
revocationRegistry,
|
|
78
|
-
revocationCommitment: commitment.toString(10)
|
|
79
|
-
}
|
|
80
|
-
}
|
|
81
|
-
}
|
package/src/signer.ts
CHANGED
|
@@ -1,21 +1,9 @@
|
|
|
1
1
|
import { Jwk } from "./types"
|
|
2
2
|
|
|
3
|
-
/**
|
|
4
|
-
|
|
5
|
-
* message mode (the service applies SHA-256 itself, never a precomputed
|
|
6
|
-
* digest) and returns the raw 64-byte r||s signature.
|
|
7
|
-
*/
|
|
8
|
-
export interface Signer {
|
|
3
|
+
/** Holder P-256 device key backed by a secure element in production. */
|
|
4
|
+
export interface DeviceSigner {
|
|
9
5
|
getPublicKeyJwk(): Promise<Jwk>
|
|
10
6
|
sign(message: Uint8Array): Promise<Uint8Array>
|
|
11
|
-
}
|
|
12
|
-
|
|
13
|
-
/**
|
|
14
|
-
* Holder P-256 device key: the secure element in the app, a software key in
|
|
15
|
-
* tests. Same contract as Signer; a separate type because the two keys must
|
|
16
|
-
* never be interchangeable in signatures.
|
|
17
|
-
*/
|
|
18
|
-
export interface DeviceSigner extends Signer {
|
|
19
7
|
/** Signs an already-computed 32-byte digest, without hashing it again. */
|
|
20
8
|
signDigest(digest: Uint8Array): Promise<Uint8Array>
|
|
21
9
|
}
|
package/src/types.ts
CHANGED
|
@@ -27,31 +27,6 @@ export interface EddsaSignature {
|
|
|
27
27
|
S: bigint
|
|
28
28
|
}
|
|
29
29
|
|
|
30
|
-
/**
|
|
31
|
-
* The custom status member inside the signed body (spec 07). The commitment
|
|
32
|
-
* is issuer-attested and visible to WS1 verifiers by design.
|
|
33
|
-
*/
|
|
34
|
-
export interface StatusClaim {
|
|
35
|
-
cn_zk_revocation: {
|
|
36
|
-
/** urn:cn:contract: URN of the on-chain revocation registry. */
|
|
37
|
-
revocationRegistry: string
|
|
38
|
-
revocationCommitment: string
|
|
39
|
-
}
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
/** The reconstructed claim set of a verified credential. */
|
|
43
|
-
export interface ApprovalCredentialClaims {
|
|
44
|
-
vct: string
|
|
45
|
-
iss: string
|
|
46
|
-
sub: string
|
|
47
|
-
jti: string
|
|
48
|
-
nbf: number
|
|
49
|
-
exp: number
|
|
50
|
-
cnf: { jwk: Jwk }
|
|
51
|
-
status: StatusClaim
|
|
52
|
-
approvalScope: ApprovalScope
|
|
53
|
-
}
|
|
54
|
-
|
|
55
30
|
export interface IssuanceRequest {
|
|
56
31
|
issuerNetworkId: Hex32
|
|
57
32
|
subjectNetworkId: Hex32
|
|
@@ -70,10 +45,6 @@ export interface IssuanceRequest {
|
|
|
70
45
|
}
|
|
71
46
|
}
|
|
72
47
|
|
|
73
|
-
export interface SdJwtIssueParams extends IssuanceRequest {
|
|
74
|
-
revocationRegistry: string
|
|
75
|
-
}
|
|
76
|
-
|
|
77
48
|
export interface ZkCredentialIssueParams extends IssuanceRequest {
|
|
78
49
|
identityPublicKey: EddsaPublicKey
|
|
79
50
|
}
|
|
@@ -107,10 +78,3 @@ export interface SerializedZkCredential {
|
|
|
107
78
|
issuerPublicKey: [string, string]
|
|
108
79
|
signature: { R8: [string, string]; S: string }
|
|
109
80
|
}
|
|
110
|
-
|
|
111
|
-
export type VerifyErrorCode =
|
|
112
|
-
"MALFORMED" | "ISSUER_NOT_APPROVED" | "KEY_NOT_FOUND" | "PROOF_INVALID" | "EXPIRED" | "NOT_YET_VALID" | "REVOKED"
|
|
113
|
-
|
|
114
|
-
/** Checks run in spec order (07, verification flow) and the first failure wins. */
|
|
115
|
-
export type VerifyResult =
|
|
116
|
-
{ ok: true; claims: ApprovalCredentialClaims } | { ok: false; code: VerifyErrorCode; message: string }
|
package/src/zk-proof.ts
CHANGED
|
@@ -25,6 +25,12 @@ export interface ExpectedZkPresentation {
|
|
|
25
25
|
country: string
|
|
26
26
|
}
|
|
27
27
|
|
|
28
|
+
/**
|
|
29
|
+
* `approvedIssuerRoot` and `revocationRoot` are the proof's public outputs and are
|
|
30
|
+
* not checked here. Check them against the registries before accepting the presentation:
|
|
31
|
+
* the issuer root against the approved-issuer set, the revocation root with the revocation
|
|
32
|
+
* registry's `isValidRoot`.
|
|
33
|
+
*/
|
|
28
34
|
export type ZkPresentationVerifyResult =
|
|
29
35
|
| { ok: true; approvedIssuerRoot: bigint; revocationRoot: bigint }
|
|
30
36
|
| { ok: false; code: "PUBLIC_INPUT_MISMATCH"; publicInput?: PublicInputName }
|
|
@@ -183,6 +189,10 @@ export async function generateZkPresentationProof(
|
|
|
183
189
|
}
|
|
184
190
|
}
|
|
185
191
|
|
|
192
|
+
/**
|
|
193
|
+
* Verifies a presentation proof against the expected public inputs and returns the roots
|
|
194
|
+
* from the proof. Check those roots against the registries; see {@link ZkPresentationVerifyResult}.
|
|
195
|
+
*/
|
|
186
196
|
export async function verifyZkPresentationProof(
|
|
187
197
|
circuit: CompiledCircuit,
|
|
188
198
|
proof: ZkPresentationProof,
|
package/dist/decode.d.ts
DELETED
|
@@ -1,8 +0,0 @@
|
|
|
1
|
-
import { ApprovalCredentialClaims } from "./types";
|
|
2
|
-
/**
|
|
3
|
-
* Decodes a compact SD-JWT and reconstructs the full claim set from its
|
|
4
|
-
* disclosures WITHOUT verifying the signature — for callers that already
|
|
5
|
-
* trust the token, such as the issuer reading back a token it just signed.
|
|
6
|
-
* Untrusted tokens go through verifyCredential/verifyPresentation instead.
|
|
7
|
-
*/
|
|
8
|
-
export declare function decodeSdJwtCredentialClaims(token: string): ApprovalCredentialClaims;
|
package/dist/decode.js
DELETED
|
@@ -1,27 +0,0 @@
|
|
|
1
|
-
"use strict";
|
|
2
|
-
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.decodeSdJwtCredentialClaims = decodeSdJwtCredentialClaims;
|
|
4
|
-
const decode_1 = require("@sd-jwt/decode");
|
|
5
|
-
const crypto_1 = require("./crypto");
|
|
6
|
-
const profile_1 = require("./profile");
|
|
7
|
-
/**
|
|
8
|
-
* Decodes a compact SD-JWT and reconstructs the full claim set from its
|
|
9
|
-
* disclosures WITHOUT verifying the signature — for callers that already
|
|
10
|
-
* trust the token, such as the issuer reading back a token it just signed.
|
|
11
|
-
* Untrusted tokens go through verifyCredential/verifyPresentation instead.
|
|
12
|
-
*/
|
|
13
|
-
function decodeSdJwtCredentialClaims(token) {
|
|
14
|
-
let reconstructed;
|
|
15
|
-
try {
|
|
16
|
-
const decoded = (0, decode_1.decodeSdJwtSync)(token, crypto_1.sdJwtHasher);
|
|
17
|
-
reconstructed = (0, decode_1.getClaimsSync)(decoded.jwt.payload, decoded.disclosures, crypto_1.sdJwtHasher);
|
|
18
|
-
}
|
|
19
|
-
catch {
|
|
20
|
-
throw new Error("token is not a decodable SD-JWT");
|
|
21
|
-
}
|
|
22
|
-
const checked = (0, profile_1.validateClaims)(reconstructed);
|
|
23
|
-
if (!checked.ok) {
|
|
24
|
-
throw new Error(checked.message);
|
|
25
|
-
}
|
|
26
|
-
return checked.claims;
|
|
27
|
-
}
|
package/dist/issue.d.ts
DELETED
|
@@ -1,9 +0,0 @@
|
|
|
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
DELETED
|
@@ -1,65 +0,0 @@
|
|
|
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
|
-
}
|
package/dist/present.d.ts
DELETED
|
@@ -1,32 +0,0 @@
|
|
|
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>;
|
package/dist/present.js
DELETED
|
@@ -1,73 +0,0 @@
|
|
|
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
|
-
}
|
package/dist/profile.d.ts
DELETED
|
@@ -1,32 +0,0 @@
|
|
|
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 {};
|
package/dist/profile.js
DELETED
|
@@ -1,122 +0,0 @@
|
|
|
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
|
-
}
|
package/dist/verify.d.ts
DELETED
|
@@ -1,29 +0,0 @@
|
|
|
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
DELETED
|
@@ -1,102 +0,0 @@
|
|
|
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
|
-
}
|