@coordination-network/credential 0.0.1 → 0.1.0
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 +17 -45
- package/dist/credential-message.d.ts +5 -1
- package/dist/credential-message.js +17 -1
- package/dist/crypto.d.ts +3 -11
- package/dist/crypto.js +1 -31
- package/dist/errors.d.ts +1 -1
- package/dist/holder-ownership.d.ts +12 -0
- package/dist/holder-ownership.js +26 -0
- package/dist/index.d.ts +1 -5
- package/dist/index.js +1 -5
- package/dist/issue-credential.d.ts +5 -1
- package/dist/issue-credential.js +24 -2
- package/dist/revocation.d.ts +6 -5
- package/dist/revocation.js +9 -12
- package/dist/signer.d.ts +2 -13
- package/dist/types.d.ts +2 -38
- package/dist/zk-proof.d.ts +10 -0
- package/dist/zk-proof.js +14 -3
- package/package.json +6 -9
- package/src/credential-message.ts +22 -2
- package/src/crypto.ts +3 -34
- package/src/errors.ts +6 -1
- package/src/holder-ownership.ts +30 -0
- package/src/index.ts +1 -5
- package/src/issue-credential.ts +43 -6
- package/src/revocation.ts +10 -14
- package/src/signer.ts +2 -14
- package/src/types.ts +2 -36
- package/src/zk-proof.ts +19 -3
- 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/README.md
CHANGED
|
@@ -1,9 +1,6 @@
|
|
|
1
1
|
# @coordination-network/credential
|
|
2
2
|
|
|
3
|
-
The credential SDK for the
|
|
4
|
-
|
|
5
|
-
- **SD-JWT VC**, for selective disclosure. The holder chooses which claims to reveal, and a key-binding JWT proves the presentation came from the device the credential was bound to.
|
|
6
|
-
- **ZK credential**, for unlinkable presentation. The holder proves in zero knowledge that an approved issuer signed a credential that has not been revoked, without revealing the credential, the issuer or themselves.
|
|
3
|
+
The zero-knowledge credential SDK for the Coordination Network. It issues signed credentials and builds or verifies presentations that prove approval and non-revocation without revealing the credential, its holder, or its issuer.
|
|
7
4
|
|
|
8
5
|
## Install
|
|
9
6
|
|
|
@@ -11,57 +8,32 @@ The credential SDK for the coordination network: issuing, verifying and presenti
|
|
|
11
8
|
yarn add @coordination-network/credential
|
|
12
9
|
```
|
|
13
10
|
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
## Usage
|
|
17
|
-
|
|
18
|
-
### Issuing
|
|
19
|
-
|
|
20
|
-
```ts
|
|
21
|
-
import { issueSdJwtCredential } from "@coordination-network/credential"
|
|
22
|
-
|
|
23
|
-
const token = await issueSdJwtCredential(params, signer, approvalResolver)
|
|
24
|
-
```
|
|
25
|
-
|
|
26
|
-
### Verifying
|
|
11
|
+
## Supported credential flow
|
|
27
12
|
|
|
28
|
-
|
|
29
|
-
|
|
13
|
+
- `issueZkCredential` validates the holder bindings and signs the credential commitments.
|
|
14
|
+
- `issuanceRequestCommitment` hashes the issuer and the approval scope into the value the wallet's device signature covers; `issueZkCredential` recomputes it from its own id and the scope it signs.
|
|
15
|
+
- `buildZkPresentationInputs` constructs private circuit inputs on the holder device.
|
|
16
|
+
- `ZkCredentialProofSystem` creates and verifies the zero-knowledge presentation.
|
|
17
|
+
- `ApprovedIssuerSet` and the revocation helpers provide the membership witnesses used by the circuit.
|
|
30
18
|
|
|
31
|
-
|
|
32
|
-
```
|
|
19
|
+
The previous SD-JWT prototype is intentionally not part of this package. It exposed stable subject, issuer, device-key, and revocation identifiers and did not provide the privacy properties of the supported ZK flow.
|
|
33
20
|
|
|
34
|
-
|
|
21
|
+
## Verifying a presentation
|
|
35
22
|
|
|
36
23
|
```ts
|
|
37
|
-
import {
|
|
38
|
-
|
|
39
|
-
const presentation = await presentSdJwtCredential(token, deviceSigner, {
|
|
40
|
-
audience: verifierId,
|
|
41
|
-
nonce,
|
|
42
|
-
disclose: ["programme", "country"]
|
|
43
|
-
})
|
|
24
|
+
import { verifyZkPresentationProof } from "@coordination-network/credential"
|
|
44
25
|
|
|
45
|
-
const result = await
|
|
26
|
+
const result = await verifyZkPresentationProof(circuit, proof, expected)
|
|
46
27
|
```
|
|
47
28
|
|
|
48
|
-
|
|
29
|
+
On success the result carries `approvedIssuerRoot` and `revocationRoot`, the proof's public outputs. The function does not check them, so check them against the registries before accepting the presentation: the issuer root against the approved-issuer set, and the revocation root with the revocation registry's `isValidRoot`.
|
|
49
30
|
|
|
50
|
-
|
|
51
|
-
import {
|
|
52
|
-
generateRevocationSecret,
|
|
53
|
-
deriveRevocationCommitment,
|
|
54
|
-
buildStatusClaim
|
|
55
|
-
} from "@coordination-network/credential"
|
|
56
|
-
|
|
57
|
-
const secret = generateRevocationSecret()
|
|
58
|
-
const commitment = deriveRevocationCommitment(secret)
|
|
59
|
-
```
|
|
31
|
+
## Revocation
|
|
60
32
|
|
|
61
|
-
The holder generates
|
|
33
|
+
The holder generates a secret with `generateRevocationSecret` and sends only its commitment to the issuer. `signCommitmentOwnership` and `verifyCommitmentOwnership` prove control of the commitment during issuance. A presentation later proves that commitment is absent from the revocation registry without disclosing it.
|
|
62
34
|
|
|
63
35
|
## Notes
|
|
64
36
|
|
|
65
|
-
-
|
|
66
|
-
-
|
|
67
|
-
- This package ships a CommonJS build
|
|
37
|
+
- Device signer interfaces are structural, so keys can remain in a hardware-backed keystore.
|
|
38
|
+
- Noir and Barretenberg are loaded lazily by the proof-system helpers.
|
|
39
|
+
- This package ships a CommonJS build; both `import` and `require` are supported.
|
|
@@ -1,9 +1,13 @@
|
|
|
1
|
-
import { Jwk, SerializedZkCredential, ZkCredential, ZkCredentialBody } from "./types";
|
|
1
|
+
import { ApprovalScope, Hex32, Jwk, SerializedZkCredential, ZkCredential, ZkCredentialBody } from "./types";
|
|
2
2
|
export declare const CREDENTIAL_DOMAIN = 363566488821926773809509531830640889725124777157045803797738936823559174026n;
|
|
3
|
+
export declare const ISSUANCE_REQUEST_DOMAIN = "urn:cn:issuance-request:v1";
|
|
4
|
+
export declare const DEVICE_ATTESTATION_CHALLENGE = "urn:cn:device-attestation:v1";
|
|
3
5
|
export declare const DEVICE_KEY_DOMAIN = 320507632879070882430325612169297945123468706597652794395401074724051126576n;
|
|
4
6
|
export declare function stringToField(value: string): bigint;
|
|
5
7
|
export declare function programmeHash(programme: string): bigint;
|
|
6
8
|
export declare function countryCode(country: string): number;
|
|
9
|
+
/** What the device signature commits to besides the holder key: the issuer and the scope shown at consent. */
|
|
10
|
+
export declare function issuanceRequestCommitment(issuerNetworkId: Hex32, approvalScope: ApprovalScope): Uint8Array;
|
|
7
11
|
export declare function deriveDeviceKeyCommitment(publicKey: Jwk): bigint;
|
|
8
12
|
export declare function zkCredentialCommitment(body: ZkCredentialBody): bigint;
|
|
9
13
|
export declare function serializeZkCredential(credential: ZkCredential): SerializedZkCredential;
|
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.DEVICE_KEY_DOMAIN = exports.CREDENTIAL_DOMAIN = void 0;
|
|
3
|
+
exports.DEVICE_KEY_DOMAIN = exports.DEVICE_ATTESTATION_CHALLENGE = exports.ISSUANCE_REQUEST_DOMAIN = exports.CREDENTIAL_DOMAIN = void 0;
|
|
4
4
|
exports.stringToField = stringToField;
|
|
5
5
|
exports.programmeHash = programmeHash;
|
|
6
6
|
exports.countryCode = countryCode;
|
|
7
|
+
exports.issuanceRequestCommitment = issuanceRequestCommitment;
|
|
7
8
|
exports.deriveDeviceKeyCommitment = deriveDeviceKeyCommitment;
|
|
8
9
|
exports.zkCredentialCommitment = zkCredentialCommitment;
|
|
9
10
|
exports.serializeZkCredential = serializeZkCredential;
|
|
@@ -18,6 +19,10 @@ const utils_1 = require("@noble/hashes/utils");
|
|
|
18
19
|
const crypto_1 = require("./crypto");
|
|
19
20
|
// keccak256(utf8("urn:cn:credential:v1")) >> 8
|
|
20
21
|
exports.CREDENTIAL_DOMAIN = 363566488821926773809509531830640889725124777157045803797738936823559174026n;
|
|
22
|
+
exports.ISSUANCE_REQUEST_DOMAIN = "urn:cn:issuance-request:v1";
|
|
23
|
+
// Android Key Attestation challenge for the device signing key, as UTF-8 bytes. Fixed because the key is
|
|
24
|
+
// attested once, at generation; the device signature over the issuer nonce supplies freshness.
|
|
25
|
+
exports.DEVICE_ATTESTATION_CHALLENGE = "urn:cn:device-attestation:v1";
|
|
21
26
|
exports.DEVICE_KEY_DOMAIN = 320507632879070882430325612169297945123468706597652794395401074724051126576n;
|
|
22
27
|
const FIELD_MODULUS = 21888242871839275222246405745257275088548364400416034343698204186575808495617n;
|
|
23
28
|
function stringToField(value) {
|
|
@@ -33,6 +38,17 @@ function countryCode(country) {
|
|
|
33
38
|
throw new Error("country must be an ISO 3166-1 alpha-3 code");
|
|
34
39
|
return [...country].reduce((value, character) => (value << 8) | character.charCodeAt(0), 0);
|
|
35
40
|
}
|
|
41
|
+
/** What the device signature commits to besides the holder key: the issuer and the scope shown at consent. */
|
|
42
|
+
function issuanceRequestCommitment(issuerNetworkId, approvalScope) {
|
|
43
|
+
if (!/^0x[0-9a-fA-F]{64}$/.test(issuerNetworkId))
|
|
44
|
+
throw new Error("issuerNetworkId must be 32 bytes");
|
|
45
|
+
return (0, viem_1.hexToBytes)((0, viem_1.keccak256)((0, viem_1.concatHex)([
|
|
46
|
+
(0, viem_1.stringToHex)(exports.ISSUANCE_REQUEST_DOMAIN),
|
|
47
|
+
issuerNetworkId,
|
|
48
|
+
(0, viem_1.toHex)(programmeHash(approvalScope.programme), { size: 32 }),
|
|
49
|
+
(0, viem_1.toHex)(countryCode(approvalScope.country), { size: 32 })
|
|
50
|
+
])));
|
|
51
|
+
}
|
|
36
52
|
const bytesToBigint = (value) => BigInt(`0x${(0, utils_1.bytesToHex)(value)}`);
|
|
37
53
|
function deriveDeviceKeyCommitment(publicKey) {
|
|
38
54
|
const encoded = (0, crypto_1.jwkToPublicKeyBytes)(publicKey);
|
package/dist/crypto.d.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { bytesToBase64url } from "@coordination-network/identity";
|
|
2
2
|
import { Jwk } from "./types";
|
|
3
|
-
import { DeviceSigner
|
|
3
|
+
import { DeviceSigner } from "./signer";
|
|
4
4
|
export { bytesToBase64url };
|
|
5
5
|
/**
|
|
6
6
|
* Padding is optional under RFC 4648 §5 and some clients send it anyway (Python's
|
|
@@ -25,23 +25,15 @@ export declare function jwkThumbprint(jwk: Jwk): Uint8Array;
|
|
|
25
25
|
* and JWS does not require it.
|
|
26
26
|
*/
|
|
27
27
|
export declare function verifyP256(message: Uint8Array, signature: Uint8Array, publicKey: Uint8Array): boolean;
|
|
28
|
-
/** Bridges a Signer to the @sd-jwt signer callback (base64url over utf8 input). */
|
|
29
|
-
export declare function createSdJwtSigner(signer: Signer): (data: string) => Promise<string>;
|
|
30
|
-
/** Builds the @sd-jwt verifier callback from a resolved issuer JWK. */
|
|
31
|
-
export declare function createSdJwtVerifier(jwk: Jwk): (data: string, signature: string) => boolean;
|
|
32
|
-
/** The sha-256 hasher the @sd-jwt config takes. */
|
|
33
|
-
export declare function sdJwtHasher(data: string | ArrayBuffer): Uint8Array;
|
|
34
|
-
/** Hex salts for disclosure digests. */
|
|
35
|
-
export declare function sdJwtSaltGenerator(length: number): string;
|
|
36
28
|
/** RFC 4122 v4 UUID from CSPRNG bytes — node:crypto is unavailable under React Native. */
|
|
37
29
|
export declare function randomUuid(): string;
|
|
38
30
|
/**
|
|
39
31
|
* Software P-256 signer. The issuer demo and every test use it; production
|
|
40
32
|
* issuers replace it with a KMS-backed Signer, apps with a secure-element
|
|
41
33
|
* DeviceSigner. Signs in message mode with low-s, raw 64-byte output —
|
|
42
|
-
* exactly the
|
|
34
|
+
* exactly the DeviceSigner contract.
|
|
43
35
|
*/
|
|
44
|
-
export declare class LocalP256Signer implements
|
|
36
|
+
export declare class LocalP256Signer implements DeviceSigner {
|
|
45
37
|
private readonly privateKey;
|
|
46
38
|
constructor(privateKey: Uint8Array);
|
|
47
39
|
static generate(): LocalP256Signer;
|
package/dist/crypto.js
CHANGED
|
@@ -7,18 +7,12 @@ exports.compressedPointToJwk = compressedPointToJwk;
|
|
|
7
7
|
exports.jwkToCompressedPoint = jwkToCompressedPoint;
|
|
8
8
|
exports.jwkThumbprint = jwkThumbprint;
|
|
9
9
|
exports.verifyP256 = verifyP256;
|
|
10
|
-
exports.createSdJwtSigner = createSdJwtSigner;
|
|
11
|
-
exports.createSdJwtVerifier = createSdJwtVerifier;
|
|
12
|
-
exports.sdJwtHasher = sdJwtHasher;
|
|
13
|
-
exports.sdJwtSaltGenerator = sdJwtSaltGenerator;
|
|
14
10
|
exports.randomUuid = randomUuid;
|
|
15
11
|
const nist_js_1 = require("@noble/curves/nist.js");
|
|
16
12
|
const sha2_1 = require("@noble/hashes/sha2");
|
|
17
13
|
const utils_1 = require("@noble/hashes/utils");
|
|
18
14
|
const identity_1 = require("@coordination-network/identity");
|
|
19
15
|
Object.defineProperty(exports, "bytesToBase64url", { enumerable: true, get: function () { return identity_1.bytesToBase64url; } });
|
|
20
|
-
/** Bridges the KMS-shaped Signer interface, the noble primitives, and the callback shapes @sd-jwt expects. */
|
|
21
|
-
const utf8 = (data) => new TextEncoder().encode(data);
|
|
22
16
|
/**
|
|
23
17
|
* Padding is optional under RFC 4648 §5 and some clients send it anyway (Python's
|
|
24
18
|
* urlsafe_b64encode, Java's URL encoder), so JWK coordinates are stripped before the strict decode
|
|
@@ -78,30 +72,6 @@ function verifyP256(message, signature, publicKey) {
|
|
|
78
72
|
return false;
|
|
79
73
|
}
|
|
80
74
|
}
|
|
81
|
-
/** Bridges a Signer to the @sd-jwt signer callback (base64url over utf8 input). */
|
|
82
|
-
function createSdJwtSigner(signer) {
|
|
83
|
-
return async (data) => (0, identity_1.bytesToBase64url)(await signer.sign(utf8(data)));
|
|
84
|
-
}
|
|
85
|
-
/** Builds the @sd-jwt verifier callback from a resolved issuer JWK. */
|
|
86
|
-
function createSdJwtVerifier(jwk) {
|
|
87
|
-
const publicKey = jwkToPublicKeyBytes(jwk);
|
|
88
|
-
return (data, signature) => {
|
|
89
|
-
try {
|
|
90
|
-
return verifyP256(utf8(data), base64urlToBytes(signature), publicKey);
|
|
91
|
-
}
|
|
92
|
-
catch {
|
|
93
|
-
return false;
|
|
94
|
-
}
|
|
95
|
-
};
|
|
96
|
-
}
|
|
97
|
-
/** The sha-256 hasher the @sd-jwt config takes. */
|
|
98
|
-
function sdJwtHasher(data) {
|
|
99
|
-
return (0, sha2_1.sha256)(typeof data === "string" ? data : new Uint8Array(data));
|
|
100
|
-
}
|
|
101
|
-
/** Hex salts for disclosure digests. */
|
|
102
|
-
function sdJwtSaltGenerator(length) {
|
|
103
|
-
return (0, utils_1.bytesToHex)((0, utils_1.randomBytes)(length));
|
|
104
|
-
}
|
|
105
75
|
/** RFC 4122 v4 UUID from CSPRNG bytes — node:crypto is unavailable under React Native. */
|
|
106
76
|
function randomUuid() {
|
|
107
77
|
const bytes = (0, utils_1.randomBytes)(16);
|
|
@@ -114,7 +84,7 @@ function randomUuid() {
|
|
|
114
84
|
* Software P-256 signer. The issuer demo and every test use it; production
|
|
115
85
|
* issuers replace it with a KMS-backed Signer, apps with a secure-element
|
|
116
86
|
* DeviceSigner. Signs in message mode with low-s, raw 64-byte output —
|
|
117
|
-
* exactly the
|
|
87
|
+
* exactly the DeviceSigner contract.
|
|
118
88
|
*/
|
|
119
89
|
class LocalP256Signer {
|
|
120
90
|
constructor(privateKey) {
|
package/dist/errors.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
export type IssuanceErrorCode = "INVALID_PARAMS" | "ISSUER_NOT_APPROVED" | "SUBJECT_MISMATCH" | "BINDING_INVALID" | "COMMITMENT_OWNERSHIP_INVALID";
|
|
1
|
+
export type IssuanceErrorCode = "INVALID_PARAMS" | "ISSUER_NOT_APPROVED" | "SUBJECT_MISMATCH" | "BINDING_INVALID" | "HOLDER_KEY_OWNERSHIP_INVALID" | "COMMITMENT_OWNERSHIP_INVALID";
|
|
2
2
|
export declare class IssuanceError extends Error {
|
|
3
3
|
readonly code: IssuanceErrorCode;
|
|
4
4
|
constructor(code: IssuanceErrorCode, message: string);
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import type { EddsaPublicKey, EddsaSignature } from "./types";
|
|
2
|
+
/** Proves possession of the holder identity key bound into a credential request. */
|
|
3
|
+
export declare function signHolderKeyOwnership(identitySeed: Uint8Array, nonce: Uint8Array): {
|
|
4
|
+
publicKey: EddsaPublicKey;
|
|
5
|
+
signature: EddsaSignature;
|
|
6
|
+
};
|
|
7
|
+
/** Verifies that the requestor controls the supplied Baby Jubjub holder public key. */
|
|
8
|
+
export declare function verifyHolderKeyOwnership(params: {
|
|
9
|
+
publicKey: EddsaPublicKey;
|
|
10
|
+
signature: EddsaSignature;
|
|
11
|
+
nonce: Uint8Array;
|
|
12
|
+
}): boolean;
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.signHolderKeyOwnership = signHolderKeyOwnership;
|
|
4
|
+
exports.verifyHolderKeyOwnership = verifyHolderKeyOwnership;
|
|
5
|
+
const identity_1 = require("@semaphore-protocol/identity");
|
|
6
|
+
const revocation_1 = require("./revocation");
|
|
7
|
+
/** Proves possession of the holder identity key bound into a credential request. */
|
|
8
|
+
function signHolderKeyOwnership(identitySeed, nonce) {
|
|
9
|
+
if (identitySeed.length !== 32)
|
|
10
|
+
throw new Error("identity seed must be 32 bytes");
|
|
11
|
+
const identity = new identity_1.Identity(identitySeed);
|
|
12
|
+
const signature = identity.signMessage((0, revocation_1.nonceToBigint)(nonce));
|
|
13
|
+
return {
|
|
14
|
+
publicKey: [identity.publicKey[0], identity.publicKey[1]],
|
|
15
|
+
signature: { R8: [signature.R8[0], signature.R8[1]], S: signature.S }
|
|
16
|
+
};
|
|
17
|
+
}
|
|
18
|
+
/** Verifies that the requestor controls the supplied Baby Jubjub holder public key. */
|
|
19
|
+
function verifyHolderKeyOwnership(params) {
|
|
20
|
+
try {
|
|
21
|
+
return identity_1.Identity.verifySignature((0, revocation_1.nonceToBigint)(params.nonce), params.signature, params.publicKey);
|
|
22
|
+
}
|
|
23
|
+
catch {
|
|
24
|
+
return false;
|
|
25
|
+
}
|
|
26
|
+
}
|
package/dist/index.d.ts
CHANGED
|
@@ -11,9 +11,5 @@ export * from "./zk-proof";
|
|
|
11
11
|
export * from "./resolver";
|
|
12
12
|
export * from "./registry-client";
|
|
13
13
|
export * from "./crypto";
|
|
14
|
-
export * from "./decode";
|
|
15
|
-
export * from "./profile";
|
|
16
|
-
export * from "./issue";
|
|
17
|
-
export * from "./verify";
|
|
18
|
-
export * from "./present";
|
|
19
14
|
export * from "./revocation";
|
|
15
|
+
export * from "./holder-ownership";
|
package/dist/index.js
CHANGED
|
@@ -27,9 +27,5 @@ __exportStar(require("./zk-proof"), exports);
|
|
|
27
27
|
__exportStar(require("./resolver"), exports);
|
|
28
28
|
__exportStar(require("./registry-client"), exports);
|
|
29
29
|
__exportStar(require("./crypto"), exports);
|
|
30
|
-
__exportStar(require("./decode"), exports);
|
|
31
|
-
__exportStar(require("./profile"), exports);
|
|
32
|
-
__exportStar(require("./issue"), exports);
|
|
33
|
-
__exportStar(require("./verify"), exports);
|
|
34
|
-
__exportStar(require("./present"), exports);
|
|
35
30
|
__exportStar(require("./revocation"), exports);
|
|
31
|
+
__exportStar(require("./holder-ownership"), exports);
|
|
@@ -1,4 +1,8 @@
|
|
|
1
1
|
import { CredentialSigner } from "./credential-signer";
|
|
2
2
|
import { ApprovalResolver } from "./resolver";
|
|
3
|
-
import { ZkCredential, ZkCredentialIssueParams } from "./types";
|
|
3
|
+
import { ZkCredential, ZkCredentialBody, ZkCredentialIssueParams } from "./types";
|
|
4
|
+
/** Validates an issuance request without signing it. */
|
|
5
|
+
export declare function validateZkCredentialIssue(params: ZkCredentialIssueParams, approval: ApprovalResolver): Promise<ZkCredentialBody>;
|
|
6
|
+
/** Signs a request that has already passed {@link validateZkCredentialIssue}. */
|
|
7
|
+
export declare function signValidatedZkCredential(body: ZkCredentialBody, signer: CredentialSigner): Promise<ZkCredential>;
|
|
4
8
|
export declare function issueZkCredential(params: ZkCredentialIssueParams, signer: CredentialSigner, approval: ApprovalResolver): Promise<ZkCredential>;
|
package/dist/issue-credential.js
CHANGED
|
@@ -1,20 +1,28 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.validateZkCredentialIssue = validateZkCredentialIssue;
|
|
4
|
+
exports.signValidatedZkCredential = signValidatedZkCredential;
|
|
3
5
|
exports.issueZkCredential = issueZkCredential;
|
|
4
6
|
const identity_1 = require("@coordination-network/identity");
|
|
7
|
+
const issuer_set_1 = require("@coordination-network/issuer-set");
|
|
5
8
|
const utils_1 = require("@noble/hashes/utils");
|
|
6
9
|
const identity_2 = require("@semaphore-protocol/identity");
|
|
7
10
|
const credential_message_1 = require("./credential-message");
|
|
8
11
|
const crypto_1 = require("./crypto");
|
|
9
12
|
const errors_1 = require("./errors");
|
|
10
13
|
const revocation_1 = require("./revocation");
|
|
11
|
-
|
|
14
|
+
const holder_ownership_1 = require("./holder-ownership");
|
|
15
|
+
/** Validates an issuance request without signing it. */
|
|
16
|
+
async function validateZkCredentialIssue(params, approval) {
|
|
12
17
|
if (!(await approval.isApprovedNetworkMember(params.issuerNetworkId))) {
|
|
13
18
|
throw new errors_1.IssuanceError("ISSUER_NOT_APPROVED", `issuer ${params.issuerNetworkId} is not an approved network member`);
|
|
14
19
|
}
|
|
15
20
|
if (!(params.validFrom < params.validUntil)) {
|
|
16
21
|
throw new errors_1.IssuanceError("INVALID_PARAMS", "validFrom must be before validUntil");
|
|
17
22
|
}
|
|
23
|
+
if (!(0, issuer_set_1.inPrimeOrderSubgroup)(params.identityPublicKey)) {
|
|
24
|
+
throw new errors_1.IssuanceError("INVALID_PARAMS", "identity public key is not in the prime-order subgroup");
|
|
25
|
+
}
|
|
18
26
|
const holderKeyCommitment = identity_2.Identity.generateCommitment(params.identityPublicKey);
|
|
19
27
|
const holderKeyCommitmentBytes = (0, utils_1.hexToBytes)(holderKeyCommitment.toString(16).padStart(64, "0"));
|
|
20
28
|
let binding;
|
|
@@ -24,6 +32,7 @@ async function issueZkCredential(params, signer, approval) {
|
|
|
24
32
|
accountPublicKey: params.accountPublicKey,
|
|
25
33
|
confirmationKey: (0, crypto_1.jwkToPublicKeyBytes)(params.confirmationKey),
|
|
26
34
|
holderKeyCommitment: holderKeyCommitmentBytes,
|
|
35
|
+
requestCommitment: (0, credential_message_1.issuanceRequestCommitment)(params.issuerNetworkId, params.approvalScope),
|
|
27
36
|
accountSignature: params.accountPopSignature,
|
|
28
37
|
deviceSignature: params.devicePopSignature
|
|
29
38
|
});
|
|
@@ -40,6 +49,13 @@ async function issueZkCredential(params, signer, approval) {
|
|
|
40
49
|
if (!binding.valid) {
|
|
41
50
|
throw new errors_1.IssuanceError("BINDING_INVALID", "credential binding failed");
|
|
42
51
|
}
|
|
52
|
+
if (!(0, holder_ownership_1.verifyHolderKeyOwnership)({
|
|
53
|
+
publicKey: params.identityPublicKey,
|
|
54
|
+
signature: params.identityOwnershipSignature,
|
|
55
|
+
nonce: params.nonce
|
|
56
|
+
})) {
|
|
57
|
+
throw new errors_1.IssuanceError("HOLDER_KEY_OWNERSHIP_INVALID", "holder key ownership proof failed");
|
|
58
|
+
}
|
|
43
59
|
const ownershipOk = (0, revocation_1.verifyCommitmentOwnership)({
|
|
44
60
|
commitment: params.revocation.commitment,
|
|
45
61
|
publicKey: params.revocation.publicKey,
|
|
@@ -49,7 +65,7 @@ async function issueZkCredential(params, signer, approval) {
|
|
|
49
65
|
if (!ownershipOk) {
|
|
50
66
|
throw new errors_1.IssuanceError("COMMITMENT_OWNERSHIP_INVALID", "revocation commitment ownership proof failed");
|
|
51
67
|
}
|
|
52
|
-
|
|
68
|
+
return {
|
|
53
69
|
programmeHash: (0, credential_message_1.programmeHash)(params.approvalScope.programme),
|
|
54
70
|
countryCode: (0, credential_message_1.countryCode)(params.approvalScope.country),
|
|
55
71
|
notBefore: params.validFrom,
|
|
@@ -58,9 +74,15 @@ async function issueZkCredential(params, signer, approval) {
|
|
|
58
74
|
holderKeyCommitment,
|
|
59
75
|
revocationCommitment: params.revocation.commitment
|
|
60
76
|
};
|
|
77
|
+
}
|
|
78
|
+
/** Signs a request that has already passed {@link validateZkCredentialIssue}. */
|
|
79
|
+
async function signValidatedZkCredential(body, signer) {
|
|
61
80
|
return {
|
|
62
81
|
body,
|
|
63
82
|
issuerPublicKey: await signer.getPublicKey(),
|
|
64
83
|
signature: await signer.sign((0, credential_message_1.zkCredentialCommitment)(body))
|
|
65
84
|
};
|
|
66
85
|
}
|
|
86
|
+
async function issueZkCredential(params, signer, approval) {
|
|
87
|
+
return signValidatedZkCredential(await validateZkCredentialIssue(params, approval), signer);
|
|
88
|
+
}
|
package/dist/revocation.d.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import { EddsaPublicKey, EddsaSignature
|
|
1
|
+
import { EddsaPublicKey, EddsaSignature } from "./types";
|
|
2
|
+
export declare function nonceToBigint(nonce: Uint8Array): bigint;
|
|
2
3
|
/** Generates a fresh 32-byte revocation secret r. Device-side only. */
|
|
3
4
|
export declare function generateRevocationSecret(): Uint8Array;
|
|
4
5
|
/** commitment = new Identity(r).commitment (Semaphore V4). */
|
|
@@ -12,8 +13,10 @@ export declare function signCommitmentOwnership(r: Uint8Array, nonce: Uint8Array
|
|
|
12
13
|
signature: EddsaSignature;
|
|
13
14
|
};
|
|
14
15
|
/**
|
|
15
|
-
* Issuer-side check:
|
|
16
|
-
* the commitment from the public key, and
|
|
16
|
+
* Issuer-side check: requires a prime-order public key, verifies the EdDSA
|
|
17
|
+
* signature over the nonce, recomputes the commitment from the public key, and
|
|
18
|
+
* compares it to the claimed one. A low-order key is refused because its
|
|
19
|
+
* signatures need no secret.
|
|
17
20
|
*/
|
|
18
21
|
export declare function verifyCommitmentOwnership(params: {
|
|
19
22
|
commitment: bigint;
|
|
@@ -21,5 +24,3 @@ export declare function verifyCommitmentOwnership(params: {
|
|
|
21
24
|
signature: EddsaSignature;
|
|
22
25
|
nonce: Uint8Array;
|
|
23
26
|
}): 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;
|
package/dist/revocation.js
CHANGED
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.nonceToBigint = nonceToBigint;
|
|
3
4
|
exports.generateRevocationSecret = generateRevocationSecret;
|
|
4
5
|
exports.deriveRevocationCommitment = deriveRevocationCommitment;
|
|
5
6
|
exports.signCommitmentOwnership = signCommitmentOwnership;
|
|
6
7
|
exports.verifyCommitmentOwnership = verifyCommitmentOwnership;
|
|
7
|
-
|
|
8
|
+
const issuer_set_1 = require("@coordination-network/issuer-set");
|
|
8
9
|
const identity_1 = require("@semaphore-protocol/identity");
|
|
9
10
|
const utils_1 = require("@noble/hashes/utils");
|
|
10
11
|
/**
|
|
@@ -47,11 +48,16 @@ function signCommitmentOwnership(r, nonce) {
|
|
|
47
48
|
};
|
|
48
49
|
}
|
|
49
50
|
/**
|
|
50
|
-
* Issuer-side check:
|
|
51
|
-
* the commitment from the public key, and
|
|
51
|
+
* Issuer-side check: requires a prime-order public key, verifies the EdDSA
|
|
52
|
+
* signature over the nonce, recomputes the commitment from the public key, and
|
|
53
|
+
* compares it to the claimed one. A low-order key is refused because its
|
|
54
|
+
* signatures need no secret.
|
|
52
55
|
*/
|
|
53
56
|
function verifyCommitmentOwnership(params) {
|
|
54
57
|
try {
|
|
58
|
+
if (!(0, issuer_set_1.inPrimeOrderSubgroup)(params.publicKey)) {
|
|
59
|
+
return false;
|
|
60
|
+
}
|
|
55
61
|
if (!identity_1.Identity.verifySignature(nonceToBigint(params.nonce), params.signature, params.publicKey)) {
|
|
56
62
|
return false;
|
|
57
63
|
}
|
|
@@ -61,12 +67,3 @@ function verifyCommitmentOwnership(params) {
|
|
|
61
67
|
return false;
|
|
62
68
|
}
|
|
63
69
|
}
|
|
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
CHANGED
|
@@ -1,19 +1,8 @@
|
|
|
1
1
|
import { Jwk } from "./types";
|
|
2
|
-
/**
|
|
3
|
-
|
|
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 {
|
|
2
|
+
/** Holder P-256 device key backed by a secure element in production. */
|
|
3
|
+
export interface DeviceSigner {
|
|
8
4
|
getPublicKeyJwk(): Promise<Jwk>;
|
|
9
5
|
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
6
|
/** Signs an already-computed 32-byte digest, without hashing it again. */
|
|
18
7
|
signDigest(digest: Uint8Array): Promise<Uint8Array>;
|
|
19
8
|
}
|
package/dist/types.d.ts
CHANGED
|
@@ -21,31 +21,6 @@ export interface EddsaSignature {
|
|
|
21
21
|
R8: [bigint, bigint];
|
|
22
22
|
S: bigint;
|
|
23
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
24
|
export interface IssuanceRequest {
|
|
50
25
|
issuerNetworkId: Hex32;
|
|
51
26
|
subjectNetworkId: Hex32;
|
|
@@ -63,11 +38,10 @@ export interface IssuanceRequest {
|
|
|
63
38
|
ownershipSignature: EddsaSignature;
|
|
64
39
|
};
|
|
65
40
|
}
|
|
66
|
-
export interface SdJwtIssueParams extends IssuanceRequest {
|
|
67
|
-
revocationRegistry: string;
|
|
68
|
-
}
|
|
69
41
|
export interface ZkCredentialIssueParams extends IssuanceRequest {
|
|
70
42
|
identityPublicKey: EddsaPublicKey;
|
|
43
|
+
/** Signature over the issuer challenge proving control of `identityPublicKey`. */
|
|
44
|
+
identityOwnershipSignature: EddsaSignature;
|
|
71
45
|
}
|
|
72
46
|
export interface ZkCredentialBody {
|
|
73
47
|
programmeHash: bigint;
|
|
@@ -99,13 +73,3 @@ export interface SerializedZkCredential {
|
|
|
99
73
|
S: string;
|
|
100
74
|
};
|
|
101
75
|
}
|
|
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/zk-proof.d.ts
CHANGED
|
@@ -12,6 +12,12 @@ export interface ExpectedZkPresentation {
|
|
|
12
12
|
programme: string;
|
|
13
13
|
country: string;
|
|
14
14
|
}
|
|
15
|
+
/**
|
|
16
|
+
* `approvedIssuerRoot` and `revocationRoot` are the proof's public outputs and are
|
|
17
|
+
* not checked here. Check them against the registries before accepting the presentation:
|
|
18
|
+
* the issuer root against the approved-issuer set, the revocation root with the revocation
|
|
19
|
+
* registry's `isValidRoot`.
|
|
20
|
+
*/
|
|
15
21
|
export type ZkPresentationVerifyResult = {
|
|
16
22
|
ok: true;
|
|
17
23
|
approvedIssuerRoot: bigint;
|
|
@@ -41,5 +47,9 @@ export declare class ZkCredentialProofSystem {
|
|
|
41
47
|
destroy(): Promise<void>;
|
|
42
48
|
}
|
|
43
49
|
export declare function generateZkPresentationProof(circuit: CompiledCircuit, inputs: ZkPresentationInputs, options?: ZkProofSystemOptions): Promise<ZkPresentationProof>;
|
|
50
|
+
/**
|
|
51
|
+
* Verifies a presentation proof against the expected public inputs and returns the roots
|
|
52
|
+
* from the proof. Check those roots against the registries; see {@link ZkPresentationVerifyResult}.
|
|
53
|
+
*/
|
|
44
54
|
export declare function verifyZkPresentationProof(circuit: CompiledCircuit, proof: ZkPresentationProof, expected: ExpectedZkPresentation, options?: ZkProofSystemOptions): Promise<ZkPresentationVerifyResult>;
|
|
45
55
|
export {};
|
package/dist/zk-proof.js
CHANGED
|
@@ -77,9 +77,16 @@ class ZkCredentialProofSystem {
|
|
|
77
77
|
return new ZkCredentialProofSystem(new Noir(circuit), new UltraHonkBackend(circuit.bytecode, api), api, options.proof);
|
|
78
78
|
}
|
|
79
79
|
async generateProof(inputs) {
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
80
|
+
try {
|
|
81
|
+
const { witness } = await this.noir.execute(inputs);
|
|
82
|
+
const proof = await this.backend.generateProof(witness, this.proofOptions);
|
|
83
|
+
return { proof: proof.proof, publicInputs: [...proof.publicInputs] };
|
|
84
|
+
}
|
|
85
|
+
catch {
|
|
86
|
+
// The circuit's assertion text distinguishes "not yet valid" from "expired".
|
|
87
|
+
// Both outcomes must be indistinguishable to the verifier that chose currentTime.
|
|
88
|
+
throw new Error("presentation proof failed");
|
|
89
|
+
}
|
|
83
90
|
}
|
|
84
91
|
async verifyProof(proof, expected) {
|
|
85
92
|
const mismatch = publicInputMismatch(proof, expected);
|
|
@@ -119,6 +126,10 @@ async function generateZkPresentationProof(circuit, inputs, options = {}) {
|
|
|
119
126
|
await proofSystem.destroy();
|
|
120
127
|
}
|
|
121
128
|
}
|
|
129
|
+
/**
|
|
130
|
+
* Verifies a presentation proof against the expected public inputs and returns the roots
|
|
131
|
+
* from the proof. Check those roots against the registries; see {@link ZkPresentationVerifyResult}.
|
|
132
|
+
*/
|
|
122
133
|
async function verifyZkPresentationProof(circuit, proof, expected, options = {}) {
|
|
123
134
|
const proofSystem = await ZkCredentialProofSystem.create(circuit, options);
|
|
124
135
|
try {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@coordination-network/credential",
|
|
3
|
-
"version": "0.0
|
|
3
|
+
"version": "0.1.0",
|
|
4
4
|
"description": "Verifiable credential SDK for the coordination network: issuance, verification, holder binding and revocation helpers.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"repository": {
|
|
@@ -30,25 +30,22 @@
|
|
|
30
30
|
"test": "jest"
|
|
31
31
|
},
|
|
32
32
|
"devDependencies": {
|
|
33
|
-
"@coordination-network/zk-artifacts": "^0.0
|
|
34
|
-
"@sd-jwt/core": "^0.19.0",
|
|
35
|
-
"@sd-jwt/types": "^0.19.0",
|
|
33
|
+
"@coordination-network/zk-artifacts": "^0.1.0",
|
|
36
34
|
"@types/jest": "^30.0.0",
|
|
37
35
|
"@types/node": "^24.0.0",
|
|
36
|
+
"@zk-kit/baby-jubjub": "1.0.3",
|
|
38
37
|
"jest": "^30.4.2",
|
|
39
38
|
"ts-jest": "^29.4.11",
|
|
40
39
|
"typescript": "^6.0.3"
|
|
41
40
|
},
|
|
42
41
|
"dependencies": {
|
|
43
42
|
"@aztec/bb.js": "5.0.0",
|
|
44
|
-
"@coordination-network/identity": "^0.0
|
|
45
|
-
"@coordination-network/issuer-set": "^0.0
|
|
43
|
+
"@coordination-network/identity": "^0.1.0",
|
|
44
|
+
"@coordination-network/issuer-set": "^0.1.0",
|
|
46
45
|
"@noble/curves": "^1.9.7",
|
|
47
46
|
"@noble/hashes": "^1.8.0",
|
|
48
47
|
"@noir-lang/noir_js": "1.0.0-beta.22",
|
|
49
|
-
"@
|
|
50
|
-
"@sd-jwt/sd-jwt-vc": "^0.19.0",
|
|
51
|
-
"@semaphore-protocol/identity": "^4.14.3",
|
|
48
|
+
"@semaphore-protocol/identity": "^4.14.4",
|
|
52
49
|
"poseidon-lite": "0.3.0",
|
|
53
50
|
"viem": "^2.55.11"
|
|
54
51
|
}
|