@coordination-network/credential 0.0.2 → 0.1.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/README.md +1 -0
- package/dist/credential-message.d.ts +5 -1
- package/dist/credential-message.js +17 -1
- 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 -0
- package/dist/index.js +1 -0
- package/dist/issue-credential.js +9 -0
- package/dist/revocation.d.ts +1 -0
- package/dist/revocation.js +1 -0
- package/dist/types.d.ts +2 -0
- package/dist/zk-proof.js +10 -3
- package/package.json +5 -5
- package/src/credential-message.ts +22 -2
- package/src/errors.ts +6 -1
- package/src/holder-ownership.ts +30 -0
- package/src/index.ts +1 -0
- package/src/issue-credential.ts +19 -1
- package/src/revocation.ts +1 -1
- package/src/types.ts +2 -0
- package/src/zk-proof.ts +9 -3
package/README.md
CHANGED
|
@@ -11,6 +11,7 @@ yarn add @coordination-network/credential
|
|
|
11
11
|
## Supported credential flow
|
|
12
12
|
|
|
13
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.
|
|
14
15
|
- `buildZkPresentationInputs` constructs private circuit inputs on the holder device.
|
|
15
16
|
- `ZkCredentialProofSystem` creates and verifies the zero-knowledge presentation.
|
|
16
17
|
- `ApprovedIssuerSet` and the revocation helpers provide the membership witnesses used by the circuit.
|
|
@@ -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/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
package/dist/index.js
CHANGED
package/dist/issue-credential.js
CHANGED
|
@@ -11,6 +11,7 @@ const credential_message_1 = require("./credential-message");
|
|
|
11
11
|
const crypto_1 = require("./crypto");
|
|
12
12
|
const errors_1 = require("./errors");
|
|
13
13
|
const revocation_1 = require("./revocation");
|
|
14
|
+
const holder_ownership_1 = require("./holder-ownership");
|
|
14
15
|
/** Validates an issuance request without signing it. */
|
|
15
16
|
async function validateZkCredentialIssue(params, approval) {
|
|
16
17
|
if (!(await approval.isApprovedNetworkMember(params.issuerNetworkId))) {
|
|
@@ -31,6 +32,7 @@ async function validateZkCredentialIssue(params, approval) {
|
|
|
31
32
|
accountPublicKey: params.accountPublicKey,
|
|
32
33
|
confirmationKey: (0, crypto_1.jwkToPublicKeyBytes)(params.confirmationKey),
|
|
33
34
|
holderKeyCommitment: holderKeyCommitmentBytes,
|
|
35
|
+
requestCommitment: (0, credential_message_1.issuanceRequestCommitment)(params.issuerNetworkId, params.approvalScope),
|
|
34
36
|
accountSignature: params.accountPopSignature,
|
|
35
37
|
deviceSignature: params.devicePopSignature
|
|
36
38
|
});
|
|
@@ -47,6 +49,13 @@ async function validateZkCredentialIssue(params, approval) {
|
|
|
47
49
|
if (!binding.valid) {
|
|
48
50
|
throw new errors_1.IssuanceError("BINDING_INVALID", "credential binding failed");
|
|
49
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
|
+
}
|
|
50
59
|
const ownershipOk = (0, revocation_1.verifyCommitmentOwnership)({
|
|
51
60
|
commitment: params.revocation.commitment,
|
|
52
61
|
publicKey: params.revocation.publicKey,
|
package/dist/revocation.d.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
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). */
|
package/dist/revocation.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
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;
|
package/dist/types.d.ts
CHANGED
|
@@ -40,6 +40,8 @@ export interface IssuanceRequest {
|
|
|
40
40
|
}
|
|
41
41
|
export interface ZkCredentialIssueParams extends IssuanceRequest {
|
|
42
42
|
identityPublicKey: EddsaPublicKey;
|
|
43
|
+
/** Signature over the issuer challenge proving control of `identityPublicKey`. */
|
|
44
|
+
identityOwnershipSignature: EddsaSignature;
|
|
43
45
|
}
|
|
44
46
|
export interface ZkCredentialBody {
|
|
45
47
|
programmeHash: bigint;
|
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);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@coordination-network/credential",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.1.1",
|
|
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,7 +30,7 @@
|
|
|
30
30
|
"test": "jest"
|
|
31
31
|
},
|
|
32
32
|
"devDependencies": {
|
|
33
|
-
"@coordination-network/zk-artifacts": "^0.
|
|
33
|
+
"@coordination-network/zk-artifacts": "^0.1.1",
|
|
34
34
|
"@types/jest": "^30.0.0",
|
|
35
35
|
"@types/node": "^24.0.0",
|
|
36
36
|
"@zk-kit/baby-jubjub": "1.0.3",
|
|
@@ -40,12 +40,12 @@
|
|
|
40
40
|
},
|
|
41
41
|
"dependencies": {
|
|
42
42
|
"@aztec/bb.js": "5.0.0",
|
|
43
|
-
"@coordination-network/identity": "^0.
|
|
44
|
-
"@coordination-network/issuer-set": "^0.
|
|
43
|
+
"@coordination-network/identity": "^0.1.1",
|
|
44
|
+
"@coordination-network/issuer-set": "^0.1.1",
|
|
45
45
|
"@noble/curves": "^1.9.7",
|
|
46
46
|
"@noble/hashes": "^1.8.0",
|
|
47
47
|
"@noir-lang/noir_js": "1.0.0-beta.22",
|
|
48
|
-
"@semaphore-protocol/identity": "^4.14.
|
|
48
|
+
"@semaphore-protocol/identity": "^4.14.5",
|
|
49
49
|
"poseidon-lite": "0.3.0",
|
|
50
50
|
"viem": "^2.55.11"
|
|
51
51
|
}
|
|
@@ -2,13 +2,17 @@ import { Identity } from "@semaphore-protocol/identity"
|
|
|
2
2
|
import { poseidon3 } from "poseidon-lite/poseidon3"
|
|
3
3
|
import { poseidon4 } from "poseidon-lite/poseidon4"
|
|
4
4
|
import { poseidon5 } from "poseidon-lite/poseidon5"
|
|
5
|
-
import { keccak256, stringToHex } from "viem"
|
|
5
|
+
import { concatHex, hexToBytes, keccak256, stringToHex, toHex } from "viem"
|
|
6
6
|
import { bytesToHex } from "@noble/hashes/utils"
|
|
7
7
|
import { jwkToPublicKeyBytes } from "./crypto"
|
|
8
|
-
import { Jwk, SerializedZkCredential, ZkCredential, ZkCredentialBody } from "./types"
|
|
8
|
+
import { ApprovalScope, Hex32, Jwk, SerializedZkCredential, ZkCredential, ZkCredentialBody } from "./types"
|
|
9
9
|
|
|
10
10
|
// keccak256(utf8("urn:cn:credential:v1")) >> 8
|
|
11
11
|
export const CREDENTIAL_DOMAIN = 363566488821926773809509531830640889725124777157045803797738936823559174026n
|
|
12
|
+
export const ISSUANCE_REQUEST_DOMAIN = "urn:cn:issuance-request:v1"
|
|
13
|
+
// Android Key Attestation challenge for the device signing key, as UTF-8 bytes. Fixed because the key is
|
|
14
|
+
// attested once, at generation; the device signature over the issuer nonce supplies freshness.
|
|
15
|
+
export const DEVICE_ATTESTATION_CHALLENGE = "urn:cn:device-attestation:v1"
|
|
12
16
|
export const DEVICE_KEY_DOMAIN = 320507632879070882430325612169297945123468706597652794395401074724051126576n
|
|
13
17
|
const FIELD_MODULUS = 21888242871839275222246405745257275088548364400416034343698204186575808495617n
|
|
14
18
|
export function stringToField(value: string): bigint {
|
|
@@ -25,6 +29,22 @@ export function countryCode(country: string): number {
|
|
|
25
29
|
return [...country].reduce((value, character) => (value << 8) | character.charCodeAt(0), 0)
|
|
26
30
|
}
|
|
27
31
|
|
|
32
|
+
/** What the device signature commits to besides the holder key: the issuer and the scope shown at consent. */
|
|
33
|
+
export function issuanceRequestCommitment(issuerNetworkId: Hex32, approvalScope: ApprovalScope): Uint8Array {
|
|
34
|
+
if (!/^0x[0-9a-fA-F]{64}$/.test(issuerNetworkId)) throw new Error("issuerNetworkId must be 32 bytes")
|
|
35
|
+
|
|
36
|
+
return hexToBytes(
|
|
37
|
+
keccak256(
|
|
38
|
+
concatHex([
|
|
39
|
+
stringToHex(ISSUANCE_REQUEST_DOMAIN),
|
|
40
|
+
issuerNetworkId,
|
|
41
|
+
toHex(programmeHash(approvalScope.programme), { size: 32 }),
|
|
42
|
+
toHex(countryCode(approvalScope.country), { size: 32 })
|
|
43
|
+
])
|
|
44
|
+
)
|
|
45
|
+
)
|
|
46
|
+
}
|
|
47
|
+
|
|
28
48
|
const bytesToBigint = (value: Uint8Array): bigint => BigInt(`0x${bytesToHex(value)}`)
|
|
29
49
|
|
|
30
50
|
export function deriveDeviceKeyCommitment(publicKey: Jwk): bigint {
|
package/src/errors.ts
CHANGED
|
@@ -1,5 +1,10 @@
|
|
|
1
1
|
export type IssuanceErrorCode =
|
|
2
|
-
|
|
2
|
+
| "INVALID_PARAMS"
|
|
3
|
+
| "ISSUER_NOT_APPROVED"
|
|
4
|
+
| "SUBJECT_MISMATCH"
|
|
5
|
+
| "BINDING_INVALID"
|
|
6
|
+
| "HOLDER_KEY_OWNERSHIP_INVALID"
|
|
7
|
+
| "COMMITMENT_OWNERSHIP_INVALID"
|
|
3
8
|
|
|
4
9
|
export class IssuanceError extends Error {
|
|
5
10
|
readonly code: IssuanceErrorCode
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { Identity } from "@semaphore-protocol/identity"
|
|
2
|
+
import { nonceToBigint } from "./revocation"
|
|
3
|
+
import type { EddsaPublicKey, EddsaSignature } from "./types"
|
|
4
|
+
|
|
5
|
+
/** Proves possession of the holder identity key bound into a credential request. */
|
|
6
|
+
export function signHolderKeyOwnership(
|
|
7
|
+
identitySeed: Uint8Array,
|
|
8
|
+
nonce: Uint8Array
|
|
9
|
+
): { publicKey: EddsaPublicKey; signature: EddsaSignature } {
|
|
10
|
+
if (identitySeed.length !== 32) throw new Error("identity seed must be 32 bytes")
|
|
11
|
+
const identity = new Identity(identitySeed)
|
|
12
|
+
const signature = identity.signMessage(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
|
+
|
|
19
|
+
/** Verifies that the requestor controls the supplied Baby Jubjub holder public key. */
|
|
20
|
+
export function verifyHolderKeyOwnership(params: {
|
|
21
|
+
publicKey: EddsaPublicKey
|
|
22
|
+
signature: EddsaSignature
|
|
23
|
+
nonce: Uint8Array
|
|
24
|
+
}): boolean {
|
|
25
|
+
try {
|
|
26
|
+
return Identity.verifySignature(nonceToBigint(params.nonce), params.signature, params.publicKey)
|
|
27
|
+
} catch {
|
|
28
|
+
return false
|
|
29
|
+
}
|
|
30
|
+
}
|
package/src/index.ts
CHANGED
package/src/issue-credential.ts
CHANGED
|
@@ -3,11 +3,18 @@ import { inPrimeOrderSubgroup } from "@coordination-network/issuer-set"
|
|
|
3
3
|
import { hexToBytes } from "@noble/hashes/utils"
|
|
4
4
|
import { Identity } from "@semaphore-protocol/identity"
|
|
5
5
|
import { CredentialSigner } from "./credential-signer"
|
|
6
|
-
import {
|
|
6
|
+
import {
|
|
7
|
+
countryCode,
|
|
8
|
+
deriveDeviceKeyCommitment,
|
|
9
|
+
issuanceRequestCommitment,
|
|
10
|
+
programmeHash,
|
|
11
|
+
zkCredentialCommitment
|
|
12
|
+
} from "./credential-message"
|
|
7
13
|
import { jwkToPublicKeyBytes } from "./crypto"
|
|
8
14
|
import { IssuanceError } from "./errors"
|
|
9
15
|
import { ApprovalResolver } from "./resolver"
|
|
10
16
|
import { verifyCommitmentOwnership } from "./revocation"
|
|
17
|
+
import { verifyHolderKeyOwnership } from "./holder-ownership"
|
|
11
18
|
import { ZkCredential, ZkCredentialBody, ZkCredentialIssueParams } from "./types"
|
|
12
19
|
|
|
13
20
|
/** Validates an issuance request without signing it. */
|
|
@@ -38,6 +45,7 @@ export async function validateZkCredentialIssue(
|
|
|
38
45
|
accountPublicKey: params.accountPublicKey,
|
|
39
46
|
confirmationKey: jwkToPublicKeyBytes(params.confirmationKey),
|
|
40
47
|
holderKeyCommitment: holderKeyCommitmentBytes,
|
|
48
|
+
requestCommitment: issuanceRequestCommitment(params.issuerNetworkId, params.approvalScope),
|
|
41
49
|
accountSignature: params.accountPopSignature,
|
|
42
50
|
deviceSignature: params.devicePopSignature
|
|
43
51
|
})
|
|
@@ -55,6 +63,16 @@ export async function validateZkCredentialIssue(
|
|
|
55
63
|
throw new IssuanceError("BINDING_INVALID", "credential binding failed")
|
|
56
64
|
}
|
|
57
65
|
|
|
66
|
+
if (
|
|
67
|
+
!verifyHolderKeyOwnership({
|
|
68
|
+
publicKey: params.identityPublicKey,
|
|
69
|
+
signature: params.identityOwnershipSignature,
|
|
70
|
+
nonce: params.nonce
|
|
71
|
+
})
|
|
72
|
+
) {
|
|
73
|
+
throw new IssuanceError("HOLDER_KEY_OWNERSHIP_INVALID", "holder key ownership proof failed")
|
|
74
|
+
}
|
|
75
|
+
|
|
58
76
|
const ownershipOk = verifyCommitmentOwnership({
|
|
59
77
|
commitment: params.revocation.commitment,
|
|
60
78
|
publicKey: params.revocation.publicKey,
|
package/src/revocation.ts
CHANGED
package/src/types.ts
CHANGED
|
@@ -47,6 +47,8 @@ export interface IssuanceRequest {
|
|
|
47
47
|
|
|
48
48
|
export interface ZkCredentialIssueParams extends IssuanceRequest {
|
|
49
49
|
identityPublicKey: EddsaPublicKey
|
|
50
|
+
/** Signature over the issuer challenge proving control of `identityPublicKey`. */
|
|
51
|
+
identityOwnershipSignature: EddsaSignature
|
|
50
52
|
}
|
|
51
53
|
|
|
52
54
|
export interface ZkCredentialBody {
|
package/src/zk-proof.ts
CHANGED
|
@@ -140,9 +140,15 @@ export class ZkCredentialProofSystem {
|
|
|
140
140
|
}
|
|
141
141
|
|
|
142
142
|
async generateProof(inputs: ZkPresentationInputs): Promise<ZkPresentationProof> {
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
143
|
+
try {
|
|
144
|
+
const { witness } = await this.noir.execute(inputs as unknown as InputMap)
|
|
145
|
+
const proof = await this.backend.generateProof(witness, this.proofOptions)
|
|
146
|
+
return { proof: proof.proof, publicInputs: [...proof.publicInputs] }
|
|
147
|
+
} catch {
|
|
148
|
+
// The circuit's assertion text distinguishes "not yet valid" from "expired".
|
|
149
|
+
// Both outcomes must be indistinguishable to the verifier that chose currentTime.
|
|
150
|
+
throw new Error("presentation proof failed")
|
|
151
|
+
}
|
|
146
152
|
}
|
|
147
153
|
|
|
148
154
|
async verifyProof(
|