@coordination-network/credential 0.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +67 -0
- package/dist/credential-message.d.ts +11 -0
- package/dist/credential-message.js +164 -0
- package/dist/credential-signer.d.ts +11 -0
- package/dist/credential-signer.js +17 -0
- package/dist/crypto.d.ts +51 -0
- package/dist/crypto.js +141 -0
- package/dist/decode.d.ts +8 -0
- package/dist/decode.js +27 -0
- package/dist/errors.d.ts +5 -0
- package/dist/errors.js +11 -0
- package/dist/index.d.ts +19 -0
- package/dist/index.js +35 -0
- package/dist/issue-credential.d.ts +4 -0
- package/dist/issue-credential.js +66 -0
- package/dist/issue.d.ts +9 -0
- package/dist/issue.js +65 -0
- package/dist/issuer-set.d.ts +2 -0
- package/dist/issuer-set.js +11 -0
- package/dist/present.d.ts +32 -0
- package/dist/present.js +73 -0
- package/dist/presentation-inputs.d.ts +77 -0
- package/dist/presentation-inputs.js +139 -0
- package/dist/profile.d.ts +32 -0
- package/dist/profile.js +122 -0
- package/dist/registry-client.d.ts +12 -0
- package/dist/registry-client.js +33 -0
- package/dist/resolver.d.ts +37 -0
- package/dist/resolver.js +37 -0
- package/dist/revocation.d.ts +25 -0
- package/dist/revocation.js +72 -0
- package/dist/signer.d.ts +19 -0
- package/dist/signer.js +2 -0
- package/dist/types.d.ts +111 -0
- package/dist/types.js +3 -0
- package/dist/verify.d.ts +29 -0
- package/dist/verify.js +102 -0
- package/dist/witness.d.ts +12 -0
- package/dist/witness.js +62 -0
- package/dist/zk-proof.d.ts +45 -0
- package/dist/zk-proof.js +130 -0
- package/package.json +55 -0
- package/src/credential-message.ts +173 -0
- package/src/credential-signer.ts +24 -0
- package/src/crypto.ts +146 -0
- package/src/decode.ts +25 -0
- package/src/errors.ts +12 -0
- package/src/index.ts +19 -0
- package/src/issue-credential.ts +78 -0
- package/src/issue.ts +83 -0
- package/src/issuer-set.ts +10 -0
- package/src/present.ts +101 -0
- package/src/presentation-inputs.ts +245 -0
- package/src/profile.ts +133 -0
- package/src/registry-client.ts +37 -0
- package/src/resolver.ts +62 -0
- package/src/revocation.ts +81 -0
- package/src/signer.ts +21 -0
- package/src/types.ts +116 -0
- package/src/verify.ts +118 -0
- package/src/witness.ts +69 -0
- package/src/zk-proof.ts +198 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Ethereum Foundation
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
# @coordination-network/credential
|
|
2
|
+
|
|
3
|
+
The credential SDK for the coordination network: issuing, verifying and presenting the delegated user approval credential, in both of the forms the network uses.
|
|
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.
|
|
7
|
+
|
|
8
|
+
## Install
|
|
9
|
+
|
|
10
|
+
```bash
|
|
11
|
+
yarn add @coordination-network/credential
|
|
12
|
+
```
|
|
13
|
+
|
|
14
|
+
The zero-knowledge paths load [`@noir-lang/noir_js`](https://noir-lang.org) and [`@aztec/bb.js`](https://github.com/AztecProtocol/aztec-packages) lazily, so an application that only issues or verifies SD-JWT credentials never pulls the proving stack into its startup path.
|
|
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
|
|
27
|
+
|
|
28
|
+
```ts
|
|
29
|
+
import { verifySdJwtCredential } from "@coordination-network/credential"
|
|
30
|
+
|
|
31
|
+
const result = await verifySdJwtCredential(token, resolver)
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
### Presenting
|
|
35
|
+
|
|
36
|
+
```ts
|
|
37
|
+
import { presentSdJwtCredential, verifySdJwtPresentation } from "@coordination-network/credential"
|
|
38
|
+
|
|
39
|
+
const presentation = await presentSdJwtCredential(token, deviceSigner, {
|
|
40
|
+
audience: verifierId,
|
|
41
|
+
nonce,
|
|
42
|
+
disclose: ["programme", "country"]
|
|
43
|
+
})
|
|
44
|
+
|
|
45
|
+
const result = await verifySdJwtPresentation(presentation, resolver, { audience: verifierId, nonce })
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
### Revocation
|
|
49
|
+
|
|
50
|
+
```ts
|
|
51
|
+
import {
|
|
52
|
+
generateRevocationSecret,
|
|
53
|
+
deriveRevocationCommitment,
|
|
54
|
+
buildStatusClaim
|
|
55
|
+
} from "@coordination-network/credential"
|
|
56
|
+
|
|
57
|
+
const secret = generateRevocationSecret()
|
|
58
|
+
const commitment = deriveRevocationCommitment(secret)
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
The holder generates the secret and shows the issuer the commitment, with `signCommitmentOwnership` / `verifyCommitmentOwnership` proving they hold the preimage. The issuer embeds the commitment in the credential's status claim. Revoking later means inserting that commitment into [`@coordination-network/revocation-registry`](../revocation-registry), and proving a credential is still valid means proving the commitment is absent from it. |
|
|
62
|
+
|
|
63
|
+
## Notes
|
|
64
|
+
|
|
65
|
+
- The signer interfaces are structural, so keys can live in a KMS, a hardware wallet or a device keystore without this package knowing.
|
|
66
|
+
- `ApprovalResolver` and `IssuerSet` are the two seams onto network membership. [`@coordination-network/issuer-set`](../issuer-set) provides an implementation of the latter.
|
|
67
|
+
- This package ships a CommonJS build. Both `import` and `require` work.
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { Jwk, SerializedZkCredential, ZkCredential, ZkCredentialBody } from "./types";
|
|
2
|
+
export declare const CREDENTIAL_DOMAIN = 363566488821926773809509531830640889725124777157045803797738936823559174026n;
|
|
3
|
+
export declare const DEVICE_KEY_DOMAIN = 320507632879070882430325612169297945123468706597652794395401074724051126576n;
|
|
4
|
+
export declare function stringToField(value: string): bigint;
|
|
5
|
+
export declare function programmeHash(programme: string): bigint;
|
|
6
|
+
export declare function countryCode(country: string): number;
|
|
7
|
+
export declare function deriveDeviceKeyCommitment(publicKey: Jwk): bigint;
|
|
8
|
+
export declare function zkCredentialCommitment(body: ZkCredentialBody): bigint;
|
|
9
|
+
export declare function serializeZkCredential(credential: ZkCredential): SerializedZkCredential;
|
|
10
|
+
export declare function parseZkCredential(value: unknown): ZkCredential;
|
|
11
|
+
export declare function verifyZkCredential(credential: ZkCredential): boolean;
|
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.DEVICE_KEY_DOMAIN = exports.CREDENTIAL_DOMAIN = void 0;
|
|
4
|
+
exports.stringToField = stringToField;
|
|
5
|
+
exports.programmeHash = programmeHash;
|
|
6
|
+
exports.countryCode = countryCode;
|
|
7
|
+
exports.deriveDeviceKeyCommitment = deriveDeviceKeyCommitment;
|
|
8
|
+
exports.zkCredentialCommitment = zkCredentialCommitment;
|
|
9
|
+
exports.serializeZkCredential = serializeZkCredential;
|
|
10
|
+
exports.parseZkCredential = parseZkCredential;
|
|
11
|
+
exports.verifyZkCredential = verifyZkCredential;
|
|
12
|
+
const identity_1 = require("@semaphore-protocol/identity");
|
|
13
|
+
const poseidon3_1 = require("poseidon-lite/poseidon3");
|
|
14
|
+
const poseidon4_1 = require("poseidon-lite/poseidon4");
|
|
15
|
+
const poseidon5_1 = require("poseidon-lite/poseidon5");
|
|
16
|
+
const viem_1 = require("viem");
|
|
17
|
+
const utils_1 = require("@noble/hashes/utils");
|
|
18
|
+
const crypto_1 = require("./crypto");
|
|
19
|
+
// keccak256(utf8("urn:cn:credential:v1")) >> 8
|
|
20
|
+
exports.CREDENTIAL_DOMAIN = 363566488821926773809509531830640889725124777157045803797738936823559174026n;
|
|
21
|
+
exports.DEVICE_KEY_DOMAIN = 320507632879070882430325612169297945123468706597652794395401074724051126576n;
|
|
22
|
+
const FIELD_MODULUS = 21888242871839275222246405745257275088548364400416034343698204186575808495617n;
|
|
23
|
+
function stringToField(value) {
|
|
24
|
+
return BigInt((0, viem_1.keccak256)((0, viem_1.stringToHex)(value))) >> 8n;
|
|
25
|
+
}
|
|
26
|
+
function programmeHash(programme) {
|
|
27
|
+
if (programme.length === 0)
|
|
28
|
+
throw new Error("programme must not be empty");
|
|
29
|
+
return stringToField(programme);
|
|
30
|
+
}
|
|
31
|
+
function countryCode(country) {
|
|
32
|
+
if (!/^[A-Z]{3}$/.test(country))
|
|
33
|
+
throw new Error("country must be an ISO 3166-1 alpha-3 code");
|
|
34
|
+
return [...country].reduce((value, character) => (value << 8) | character.charCodeAt(0), 0);
|
|
35
|
+
}
|
|
36
|
+
const bytesToBigint = (value) => BigInt(`0x${(0, utils_1.bytesToHex)(value)}`);
|
|
37
|
+
function deriveDeviceKeyCommitment(publicKey) {
|
|
38
|
+
const encoded = (0, crypto_1.jwkToPublicKeyBytes)(publicKey);
|
|
39
|
+
const x = encoded.slice(1, 33);
|
|
40
|
+
const y = encoded.slice(33, 65);
|
|
41
|
+
return (0, poseidon5_1.poseidon5)([
|
|
42
|
+
exports.DEVICE_KEY_DOMAIN,
|
|
43
|
+
bytesToBigint(x.slice(0, 16)),
|
|
44
|
+
bytesToBigint(x.slice(16, 32)),
|
|
45
|
+
bytesToBigint(y.slice(0, 16)),
|
|
46
|
+
bytesToBigint(y.slice(16, 32))
|
|
47
|
+
]);
|
|
48
|
+
}
|
|
49
|
+
function zkCredentialCommitment(body) {
|
|
50
|
+
const policy = (0, poseidon4_1.poseidon4)([
|
|
51
|
+
body.programmeHash,
|
|
52
|
+
BigInt(body.countryCode),
|
|
53
|
+
BigInt(body.notBefore),
|
|
54
|
+
BigInt(body.expiresAt)
|
|
55
|
+
]);
|
|
56
|
+
const keys = (0, poseidon3_1.poseidon3)([body.deviceKeyCommitment, body.holderKeyCommitment, body.revocationCommitment]);
|
|
57
|
+
return (0, poseidon3_1.poseidon3)([exports.CREDENTIAL_DOMAIN, policy, keys]);
|
|
58
|
+
}
|
|
59
|
+
const decimal = (value, name) => {
|
|
60
|
+
if (typeof value !== "string" || !/^(0|[1-9][0-9]*)$/.test(value))
|
|
61
|
+
throw new Error(`${name} must be decimal`);
|
|
62
|
+
const parsed = BigInt(value);
|
|
63
|
+
if (parsed >= FIELD_MODULUS)
|
|
64
|
+
throw new Error(`${name} must fit the BN254 field`);
|
|
65
|
+
return parsed;
|
|
66
|
+
};
|
|
67
|
+
const timestamp = (value, name) => {
|
|
68
|
+
if (typeof value !== "number" || !Number.isSafeInteger(value) || value < 0) {
|
|
69
|
+
throw new Error(`${name} must be a non-negative integer`);
|
|
70
|
+
}
|
|
71
|
+
return value;
|
|
72
|
+
};
|
|
73
|
+
const encodedCountryCode = (value) => {
|
|
74
|
+
if (typeof value !== "number" || !Number.isSafeInteger(value) || value < 0 || value > 0xffffff) {
|
|
75
|
+
throw new Error("credential.body.countryCode must be a three-byte integer");
|
|
76
|
+
}
|
|
77
|
+
return value;
|
|
78
|
+
};
|
|
79
|
+
const record = (value, name) => {
|
|
80
|
+
if (typeof value !== "object" || value === null || Array.isArray(value)) {
|
|
81
|
+
throw new Error(`${name} must be an object`);
|
|
82
|
+
}
|
|
83
|
+
return value;
|
|
84
|
+
};
|
|
85
|
+
const exactKeys = (value, keys, name) => {
|
|
86
|
+
const actual = Object.keys(value).sort();
|
|
87
|
+
const expected = [...keys].sort();
|
|
88
|
+
if (actual.length !== expected.length || actual.some((key, index) => key !== expected[index])) {
|
|
89
|
+
throw new Error(`${name} has unexpected fields`);
|
|
90
|
+
}
|
|
91
|
+
};
|
|
92
|
+
function serializeZkCredential(credential) {
|
|
93
|
+
return {
|
|
94
|
+
body: {
|
|
95
|
+
programmeHash: credential.body.programmeHash.toString(),
|
|
96
|
+
countryCode: credential.body.countryCode,
|
|
97
|
+
notBefore: credential.body.notBefore,
|
|
98
|
+
expiresAt: credential.body.expiresAt,
|
|
99
|
+
deviceKeyCommitment: credential.body.deviceKeyCommitment.toString(),
|
|
100
|
+
holderKeyCommitment: credential.body.holderKeyCommitment.toString(),
|
|
101
|
+
revocationCommitment: credential.body.revocationCommitment.toString()
|
|
102
|
+
},
|
|
103
|
+
issuerPublicKey: credential.issuerPublicKey.map(String),
|
|
104
|
+
signature: {
|
|
105
|
+
R8: credential.signature.R8.map(String),
|
|
106
|
+
S: credential.signature.S.toString()
|
|
107
|
+
}
|
|
108
|
+
};
|
|
109
|
+
}
|
|
110
|
+
function parseZkCredential(value) {
|
|
111
|
+
const credential = record(value, "credential");
|
|
112
|
+
exactKeys(credential, ["body", "issuerPublicKey", "signature"], "credential");
|
|
113
|
+
const body = record(credential.body, "credential.body");
|
|
114
|
+
exactKeys(body, [
|
|
115
|
+
"programmeHash",
|
|
116
|
+
"countryCode",
|
|
117
|
+
"notBefore",
|
|
118
|
+
"expiresAt",
|
|
119
|
+
"deviceKeyCommitment",
|
|
120
|
+
"holderKeyCommitment",
|
|
121
|
+
"revocationCommitment"
|
|
122
|
+
], "credential.body");
|
|
123
|
+
if (!Array.isArray(credential.issuerPublicKey) || credential.issuerPublicKey.length !== 2) {
|
|
124
|
+
throw new Error("credential.issuerPublicKey must contain two values");
|
|
125
|
+
}
|
|
126
|
+
const signature = record(credential.signature, "credential.signature");
|
|
127
|
+
exactKeys(signature, ["R8", "S"], "credential.signature");
|
|
128
|
+
if (!Array.isArray(signature.R8) || signature.R8.length !== 2) {
|
|
129
|
+
throw new Error("credential.signature.R8 must contain two values");
|
|
130
|
+
}
|
|
131
|
+
const parsed = {
|
|
132
|
+
body: {
|
|
133
|
+
programmeHash: decimal(body.programmeHash, "credential.body.programmeHash"),
|
|
134
|
+
countryCode: encodedCountryCode(body.countryCode),
|
|
135
|
+
notBefore: timestamp(body.notBefore, "credential.body.notBefore"),
|
|
136
|
+
expiresAt: timestamp(body.expiresAt, "credential.body.expiresAt"),
|
|
137
|
+
deviceKeyCommitment: decimal(body.deviceKeyCommitment, "credential.body.deviceKeyCommitment"),
|
|
138
|
+
holderKeyCommitment: decimal(body.holderKeyCommitment, "credential.body.holderKeyCommitment"),
|
|
139
|
+
revocationCommitment: decimal(body.revocationCommitment, "credential.body.revocationCommitment")
|
|
140
|
+
},
|
|
141
|
+
issuerPublicKey: [
|
|
142
|
+
decimal(credential.issuerPublicKey[0], "credential.issuerPublicKey[0]"),
|
|
143
|
+
decimal(credential.issuerPublicKey[1], "credential.issuerPublicKey[1]")
|
|
144
|
+
],
|
|
145
|
+
signature: {
|
|
146
|
+
R8: [
|
|
147
|
+
decimal(signature.R8[0], "credential.signature.R8[0]"),
|
|
148
|
+
decimal(signature.R8[1], "credential.signature.R8[1]")
|
|
149
|
+
],
|
|
150
|
+
S: decimal(signature.S, "credential.signature.S")
|
|
151
|
+
}
|
|
152
|
+
};
|
|
153
|
+
if (parsed.body.notBefore >= parsed.body.expiresAt)
|
|
154
|
+
throw new Error("credential validity window is invalid");
|
|
155
|
+
return parsed;
|
|
156
|
+
}
|
|
157
|
+
function verifyZkCredential(credential) {
|
|
158
|
+
try {
|
|
159
|
+
return identity_1.Identity.verifySignature(zkCredentialCommitment(credential.body), credential.signature, credential.issuerPublicKey);
|
|
160
|
+
}
|
|
161
|
+
catch {
|
|
162
|
+
return false;
|
|
163
|
+
}
|
|
164
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { EddsaPublicKey, EddsaSignature } from "./types";
|
|
2
|
+
export interface CredentialSigner {
|
|
3
|
+
getPublicKey(): Promise<EddsaPublicKey>;
|
|
4
|
+
sign(message: bigint): Promise<EddsaSignature>;
|
|
5
|
+
}
|
|
6
|
+
export declare class LocalCredentialSigner implements CredentialSigner {
|
|
7
|
+
private readonly identity;
|
|
8
|
+
constructor(secret: string | Uint8Array);
|
|
9
|
+
getPublicKey(): Promise<EddsaPublicKey>;
|
|
10
|
+
sign(message: bigint): Promise<EddsaSignature>;
|
|
11
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.LocalCredentialSigner = void 0;
|
|
4
|
+
const identity_1 = require("@semaphore-protocol/identity");
|
|
5
|
+
class LocalCredentialSigner {
|
|
6
|
+
constructor(secret) {
|
|
7
|
+
this.identity = new identity_1.Identity(secret);
|
|
8
|
+
}
|
|
9
|
+
async getPublicKey() {
|
|
10
|
+
return [this.identity.publicKey[0], this.identity.publicKey[1]];
|
|
11
|
+
}
|
|
12
|
+
async sign(message) {
|
|
13
|
+
const signature = this.identity.signMessage(message);
|
|
14
|
+
return { R8: [signature.R8[0], signature.R8[1]], S: signature.S };
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
exports.LocalCredentialSigner = LocalCredentialSigner;
|
package/dist/crypto.d.ts
ADDED
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import { bytesToBase64url } from "@coordination-network/identity";
|
|
2
|
+
import { Jwk } from "./types";
|
|
3
|
+
import { DeviceSigner, Signer } from "./signer";
|
|
4
|
+
export { bytesToBase64url };
|
|
5
|
+
/**
|
|
6
|
+
* Padding is optional under RFC 4648 §5 and some clients send it anyway (Python's
|
|
7
|
+
* urlsafe_b64encode, Java's URL encoder), so JWK coordinates are stripped before the strict decode
|
|
8
|
+
* rather than rejected.
|
|
9
|
+
*/
|
|
10
|
+
export declare function base64urlToBytes(encoded: string): Uint8Array;
|
|
11
|
+
/** Uncompressed SEC1 point (65 bytes, 0x04 || x || y) from a P-256 JWK. */
|
|
12
|
+
export declare function jwkToPublicKeyBytes(jwk: Jwk): Uint8Array;
|
|
13
|
+
/**
|
|
14
|
+
* Registry keys are stored as the compressed SEC1 point (33 bytes, spec 07).
|
|
15
|
+
* Both converters go through the curve, so an off-curve point throws instead
|
|
16
|
+
* of round-tripping garbage.
|
|
17
|
+
*/
|
|
18
|
+
export declare function compressedPointToJwk(point: Uint8Array): Jwk;
|
|
19
|
+
export declare function jwkToCompressedPoint(jwk: Jwk): Uint8Array;
|
|
20
|
+
/** RFC 7638 JWK thumbprint: SHA-256 over the ordered required members. */
|
|
21
|
+
export declare function jwkThumbprint(jwk: Jwk): Uint8Array;
|
|
22
|
+
/**
|
|
23
|
+
* ECDSA P-256 verification in message mode. lowS is not enforced on verify:
|
|
24
|
+
* we produce low-s signatures ourselves, but external signers (KMS) may not,
|
|
25
|
+
* and JWS does not require it.
|
|
26
|
+
*/
|
|
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
|
+
/** RFC 4122 v4 UUID from CSPRNG bytes — node:crypto is unavailable under React Native. */
|
|
37
|
+
export declare function randomUuid(): string;
|
|
38
|
+
/**
|
|
39
|
+
* Software P-256 signer. The issuer demo and every test use it; production
|
|
40
|
+
* issuers replace it with a KMS-backed Signer, apps with a secure-element
|
|
41
|
+
* DeviceSigner. Signs in message mode with low-s, raw 64-byte output —
|
|
42
|
+
* exactly the Signer contract.
|
|
43
|
+
*/
|
|
44
|
+
export declare class LocalP256Signer implements Signer, DeviceSigner {
|
|
45
|
+
private readonly privateKey;
|
|
46
|
+
constructor(privateKey: Uint8Array);
|
|
47
|
+
static generate(): LocalP256Signer;
|
|
48
|
+
getPublicKeyJwk(): Promise<Jwk>;
|
|
49
|
+
sign(message: Uint8Array): Promise<Uint8Array>;
|
|
50
|
+
signDigest(digest: Uint8Array): Promise<Uint8Array>;
|
|
51
|
+
}
|
package/dist/crypto.js
ADDED
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.LocalP256Signer = exports.bytesToBase64url = void 0;
|
|
4
|
+
exports.base64urlToBytes = base64urlToBytes;
|
|
5
|
+
exports.jwkToPublicKeyBytes = jwkToPublicKeyBytes;
|
|
6
|
+
exports.compressedPointToJwk = compressedPointToJwk;
|
|
7
|
+
exports.jwkToCompressedPoint = jwkToCompressedPoint;
|
|
8
|
+
exports.jwkThumbprint = jwkThumbprint;
|
|
9
|
+
exports.verifyP256 = verifyP256;
|
|
10
|
+
exports.createSdJwtSigner = createSdJwtSigner;
|
|
11
|
+
exports.createSdJwtVerifier = createSdJwtVerifier;
|
|
12
|
+
exports.sdJwtHasher = sdJwtHasher;
|
|
13
|
+
exports.sdJwtSaltGenerator = sdJwtSaltGenerator;
|
|
14
|
+
exports.randomUuid = randomUuid;
|
|
15
|
+
const nist_js_1 = require("@noble/curves/nist.js");
|
|
16
|
+
const sha2_1 = require("@noble/hashes/sha2");
|
|
17
|
+
const utils_1 = require("@noble/hashes/utils");
|
|
18
|
+
const identity_1 = require("@coordination-network/identity");
|
|
19
|
+
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
|
+
/**
|
|
23
|
+
* Padding is optional under RFC 4648 §5 and some clients send it anyway (Python's
|
|
24
|
+
* urlsafe_b64encode, Java's URL encoder), so JWK coordinates are stripped before the strict decode
|
|
25
|
+
* rather than rejected.
|
|
26
|
+
*/
|
|
27
|
+
function base64urlToBytes(encoded) {
|
|
28
|
+
const unpadded = encoded.replace(/=+$/, "");
|
|
29
|
+
const invalid = unpadded.match(/[^A-Za-z0-9_-]/);
|
|
30
|
+
if (invalid)
|
|
31
|
+
throw new Error(`invalid base64url character: ${invalid[0]}`);
|
|
32
|
+
return (0, identity_1.base64urlToBytes)(unpadded);
|
|
33
|
+
}
|
|
34
|
+
/** Uncompressed SEC1 point (65 bytes, 0x04 || x || y) from a P-256 JWK. */
|
|
35
|
+
function jwkToPublicKeyBytes(jwk) {
|
|
36
|
+
const x = base64urlToBytes(jwk.x);
|
|
37
|
+
const y = base64urlToBytes(jwk.y);
|
|
38
|
+
if (jwk.kty !== "EC" || jwk.crv !== "P-256" || x.length !== 32 || y.length !== 32) {
|
|
39
|
+
throw new Error("expected a P-256 EC JWK with 32-byte coordinates");
|
|
40
|
+
}
|
|
41
|
+
const point = new Uint8Array(65);
|
|
42
|
+
point[0] = 0x04;
|
|
43
|
+
point.set(x, 1);
|
|
44
|
+
point.set(y, 33);
|
|
45
|
+
return point;
|
|
46
|
+
}
|
|
47
|
+
/**
|
|
48
|
+
* Registry keys are stored as the compressed SEC1 point (33 bytes, spec 07).
|
|
49
|
+
* Both converters go through the curve, so an off-curve point throws instead
|
|
50
|
+
* of round-tripping garbage.
|
|
51
|
+
*/
|
|
52
|
+
function compressedPointToJwk(point) {
|
|
53
|
+
const raw = nist_js_1.p256.ProjectivePoint.fromHex(point).toRawBytes(false);
|
|
54
|
+
return {
|
|
55
|
+
kty: "EC",
|
|
56
|
+
crv: "P-256",
|
|
57
|
+
x: (0, identity_1.bytesToBase64url)(raw.slice(1, 33)),
|
|
58
|
+
y: (0, identity_1.bytesToBase64url)(raw.slice(33, 65))
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
function jwkToCompressedPoint(jwk) {
|
|
62
|
+
return nist_js_1.p256.ProjectivePoint.fromHex(jwkToPublicKeyBytes(jwk)).toRawBytes(true);
|
|
63
|
+
}
|
|
64
|
+
/** RFC 7638 JWK thumbprint: SHA-256 over the ordered required members. */
|
|
65
|
+
function jwkThumbprint(jwk) {
|
|
66
|
+
return (0, sha2_1.sha256)(JSON.stringify({ crv: jwk.crv, kty: jwk.kty, x: jwk.x, y: jwk.y }));
|
|
67
|
+
}
|
|
68
|
+
/**
|
|
69
|
+
* ECDSA P-256 verification in message mode. lowS is not enforced on verify:
|
|
70
|
+
* we produce low-s signatures ourselves, but external signers (KMS) may not,
|
|
71
|
+
* and JWS does not require it.
|
|
72
|
+
*/
|
|
73
|
+
function verifyP256(message, signature, publicKey) {
|
|
74
|
+
try {
|
|
75
|
+
return nist_js_1.p256.verify(signature, (0, sha2_1.sha256)(message), publicKey, { lowS: false });
|
|
76
|
+
}
|
|
77
|
+
catch {
|
|
78
|
+
return false;
|
|
79
|
+
}
|
|
80
|
+
}
|
|
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
|
+
/** RFC 4122 v4 UUID from CSPRNG bytes — node:crypto is unavailable under React Native. */
|
|
106
|
+
function randomUuid() {
|
|
107
|
+
const bytes = (0, utils_1.randomBytes)(16);
|
|
108
|
+
bytes[6] = (bytes[6] & 0x0f) | 0x40;
|
|
109
|
+
bytes[8] = (bytes[8] & 0x3f) | 0x80;
|
|
110
|
+
const hex = (0, utils_1.bytesToHex)(bytes);
|
|
111
|
+
return `${hex.slice(0, 8)}-${hex.slice(8, 12)}-${hex.slice(12, 16)}-${hex.slice(16, 20)}-${hex.slice(20)}`;
|
|
112
|
+
}
|
|
113
|
+
/**
|
|
114
|
+
* Software P-256 signer. The issuer demo and every test use it; production
|
|
115
|
+
* issuers replace it with a KMS-backed Signer, apps with a secure-element
|
|
116
|
+
* DeviceSigner. Signs in message mode with low-s, raw 64-byte output —
|
|
117
|
+
* exactly the Signer contract.
|
|
118
|
+
*/
|
|
119
|
+
class LocalP256Signer {
|
|
120
|
+
constructor(privateKey) {
|
|
121
|
+
if (privateKey.length !== 32) {
|
|
122
|
+
throw new Error("P-256 private key must be 32 bytes");
|
|
123
|
+
}
|
|
124
|
+
this.privateKey = privateKey;
|
|
125
|
+
}
|
|
126
|
+
static generate() {
|
|
127
|
+
return new LocalP256Signer(nist_js_1.p256.utils.randomSecretKey());
|
|
128
|
+
}
|
|
129
|
+
async getPublicKeyJwk() {
|
|
130
|
+
return compressedPointToJwk(nist_js_1.p256.getPublicKey(this.privateKey, true));
|
|
131
|
+
}
|
|
132
|
+
async sign(message) {
|
|
133
|
+
return nist_js_1.p256.sign((0, sha2_1.sha256)(message), this.privateKey, { lowS: true }).toCompactRawBytes();
|
|
134
|
+
}
|
|
135
|
+
async signDigest(digest) {
|
|
136
|
+
if (digest.length !== 32)
|
|
137
|
+
throw new Error("P-256 digest must be 32 bytes");
|
|
138
|
+
return nist_js_1.p256.sign(digest, this.privateKey, { lowS: true }).toCompactRawBytes();
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
exports.LocalP256Signer = LocalP256Signer;
|
package/dist/decode.d.ts
ADDED
|
@@ -0,0 +1,8 @@
|
|
|
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
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
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/errors.d.ts
ADDED
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
export type IssuanceErrorCode = "INVALID_PARAMS" | "ISSUER_NOT_APPROVED" | "SUBJECT_MISMATCH" | "BINDING_INVALID" | "COMMITMENT_OWNERSHIP_INVALID";
|
|
2
|
+
export declare class IssuanceError extends Error {
|
|
3
|
+
readonly code: IssuanceErrorCode;
|
|
4
|
+
constructor(code: IssuanceErrorCode, message: string);
|
|
5
|
+
}
|
package/dist/errors.js
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.IssuanceError = void 0;
|
|
4
|
+
class IssuanceError extends Error {
|
|
5
|
+
constructor(code, message) {
|
|
6
|
+
super(message);
|
|
7
|
+
this.name = "IssuanceError";
|
|
8
|
+
this.code = code;
|
|
9
|
+
}
|
|
10
|
+
}
|
|
11
|
+
exports.IssuanceError = IssuanceError;
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
export * from "./errors";
|
|
2
|
+
export * from "./types";
|
|
3
|
+
export * from "./signer";
|
|
4
|
+
export * from "./credential-message";
|
|
5
|
+
export * from "./credential-signer";
|
|
6
|
+
export * from "./issue-credential";
|
|
7
|
+
export * from "./issuer-set";
|
|
8
|
+
export * from "./presentation-inputs";
|
|
9
|
+
export * from "./witness";
|
|
10
|
+
export * from "./zk-proof";
|
|
11
|
+
export * from "./resolver";
|
|
12
|
+
export * from "./registry-client";
|
|
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
|
+
export * from "./revocation";
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __exportStar = (this && this.__exportStar) || function(m, exports) {
|
|
14
|
+
for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p);
|
|
15
|
+
};
|
|
16
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
17
|
+
__exportStar(require("./errors"), exports);
|
|
18
|
+
__exportStar(require("./types"), exports);
|
|
19
|
+
__exportStar(require("./signer"), exports);
|
|
20
|
+
__exportStar(require("./credential-message"), exports);
|
|
21
|
+
__exportStar(require("./credential-signer"), exports);
|
|
22
|
+
__exportStar(require("./issue-credential"), exports);
|
|
23
|
+
__exportStar(require("./issuer-set"), exports);
|
|
24
|
+
__exportStar(require("./presentation-inputs"), exports);
|
|
25
|
+
__exportStar(require("./witness"), exports);
|
|
26
|
+
__exportStar(require("./zk-proof"), exports);
|
|
27
|
+
__exportStar(require("./resolver"), exports);
|
|
28
|
+
__exportStar(require("./registry-client"), exports);
|
|
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
|
+
__exportStar(require("./revocation"), exports);
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
import { CredentialSigner } from "./credential-signer";
|
|
2
|
+
import { ApprovalResolver } from "./resolver";
|
|
3
|
+
import { ZkCredential, ZkCredentialIssueParams } from "./types";
|
|
4
|
+
export declare function issueZkCredential(params: ZkCredentialIssueParams, signer: CredentialSigner, approval: ApprovalResolver): Promise<ZkCredential>;
|