@nuggetslife/vc 0.0.21 → 0.0.23
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/index.d.ts +114 -0
- package/index.js +16 -1
- package/package.json +8 -11
- package/src/bls_signatures/bbs_bls_holder_bound_signature_2022/mod.rs +7 -38
- package/src/bls_signatures/bbs_bls_holder_bound_signature_proof_2022/mod.rs +3 -13
- package/src/bls_signatures/bbs_bls_signature_2020/mod.rs +4 -34
- package/src/bls_signatures/bbs_bls_signature_2020/types.rs +0 -1
- package/src/bls_signatures/bbs_bls_signature_proof_2020/mod.rs +3 -13
- package/src/bls_signatures/bls_12381_g2_keypair/mod.rs +16 -16
- package/src/bls_signatures/bound_bls_12381_g2_keypair/mod.rs +3 -3
- package/src/jose.rs +415 -0
- package/src/jsonld.rs +4 -26
- package/src/ld_signatures.rs +29 -24
- package/src/lib.rs +1 -0
- package/test.mjs +38 -0
- package/test_fuzz.mjs +202 -0
- package/test_jose.mjs +497 -0
- package/test_jsonld_crossverify.mjs +1 -1
package/src/jose.rs
ADDED
|
@@ -0,0 +1,415 @@
|
|
|
1
|
+
use serde_json::Value;
|
|
2
|
+
|
|
3
|
+
use vc::jose::{
|
|
4
|
+
functions::{
|
|
5
|
+
jose_compact_sign_json, jose_compact_verify_json, jose_decrypt, jose_decrypt_json,
|
|
6
|
+
jose_encrypt, jose_flattened_sign_json, jose_general_encrypt_json, jose_general_sign_json,
|
|
7
|
+
jose_generate_key_pair, jose_generate_key_pair_jwk, jose_verify_json,
|
|
8
|
+
},
|
|
9
|
+
types::{
|
|
10
|
+
TOKEN_TYPE_DIDCOM_ENCRYPTED, TOKEN_TYPE_DIDCOM_SIGNED, TOKEN_TYPE_JWT,
|
|
11
|
+
},
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
// ---------------------------------------------------------------------------
|
|
15
|
+
// Enums — numeric values match @nuggetslife/ffi-jose exactly
|
|
16
|
+
// ---------------------------------------------------------------------------
|
|
17
|
+
|
|
18
|
+
#[napi]
|
|
19
|
+
pub enum JoseNamedCurve {
|
|
20
|
+
P256 = 0,
|
|
21
|
+
P384 = 1,
|
|
22
|
+
P521 = 2,
|
|
23
|
+
Secp256k1 = 3,
|
|
24
|
+
Ed25519 = 4,
|
|
25
|
+
Ed448 = 5,
|
|
26
|
+
X25519 = 6,
|
|
27
|
+
X448 = 7,
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
impl JoseNamedCurve {
|
|
31
|
+
fn to_rust_string(&self) -> &'static str {
|
|
32
|
+
match self {
|
|
33
|
+
JoseNamedCurve::P256 => "P-256",
|
|
34
|
+
JoseNamedCurve::P384 => "P-384",
|
|
35
|
+
JoseNamedCurve::P521 => "P-521",
|
|
36
|
+
JoseNamedCurve::Secp256k1 => "SECP256K1",
|
|
37
|
+
JoseNamedCurve::Ed25519 => "ED25519",
|
|
38
|
+
JoseNamedCurve::Ed448 => "ED448",
|
|
39
|
+
JoseNamedCurve::X25519 => "X25519",
|
|
40
|
+
JoseNamedCurve::X448 => "X448",
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
#[napi]
|
|
46
|
+
pub enum JoseContentEncryption {
|
|
47
|
+
A128gcm = 0,
|
|
48
|
+
A192gcm = 1,
|
|
49
|
+
A256gcm = 2,
|
|
50
|
+
A128cbcHs256 = 3,
|
|
51
|
+
A192cbcHs384 = 4,
|
|
52
|
+
A256cbcHs512 = 5,
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
impl JoseContentEncryption {
|
|
56
|
+
fn to_rust_string(&self) -> &'static str {
|
|
57
|
+
match self {
|
|
58
|
+
JoseContentEncryption::A128gcm => "A128GCM",
|
|
59
|
+
JoseContentEncryption::A192gcm => "A192GCM",
|
|
60
|
+
JoseContentEncryption::A256gcm => "A256GCM",
|
|
61
|
+
JoseContentEncryption::A128cbcHs256 => "A128CBC-HS256",
|
|
62
|
+
JoseContentEncryption::A192cbcHs384 => "A192CBC-HS384",
|
|
63
|
+
JoseContentEncryption::A256cbcHs512 => "A256CBC-HS512",
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
#[napi]
|
|
69
|
+
pub enum JoseKeyEncryption {
|
|
70
|
+
Dir = 0,
|
|
71
|
+
EcdhEs = 1,
|
|
72
|
+
EcdhEsA128kw = 2,
|
|
73
|
+
EcdhEsA192kw = 3,
|
|
74
|
+
EcdhEsA256kw = 4,
|
|
75
|
+
Rsa1_5 = 5,
|
|
76
|
+
RsaOaep = 6,
|
|
77
|
+
RsaOaep256 = 7,
|
|
78
|
+
RsaOaep384 = 8,
|
|
79
|
+
RsaOaep512 = 9,
|
|
80
|
+
Pbes2Hs256A128kw = 10,
|
|
81
|
+
Pbes2Hs384A192kw = 11,
|
|
82
|
+
Pbes2Hs512A256kw = 12,
|
|
83
|
+
A128kw = 13,
|
|
84
|
+
A192kw = 14,
|
|
85
|
+
A256kw = 15,
|
|
86
|
+
A128gcmkw = 16,
|
|
87
|
+
A192gcmkw = 17,
|
|
88
|
+
A256gcmkw = 18,
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
impl JoseKeyEncryption {
|
|
92
|
+
fn to_rust_string(&self) -> &'static str {
|
|
93
|
+
match self {
|
|
94
|
+
JoseKeyEncryption::Dir => "dir",
|
|
95
|
+
JoseKeyEncryption::EcdhEs => "ECDH-ES",
|
|
96
|
+
JoseKeyEncryption::EcdhEsA128kw => "ECDH-ES+A128KW",
|
|
97
|
+
JoseKeyEncryption::EcdhEsA192kw => "ECDH-ES+A192KW",
|
|
98
|
+
JoseKeyEncryption::EcdhEsA256kw => "ECDH-ES+A256KW",
|
|
99
|
+
JoseKeyEncryption::Rsa1_5 => "RSA1_5",
|
|
100
|
+
JoseKeyEncryption::RsaOaep => "RSA-OAEP",
|
|
101
|
+
JoseKeyEncryption::RsaOaep256 => "RSA-OAEP-256",
|
|
102
|
+
JoseKeyEncryption::RsaOaep384 => "RSA-OAEP-384",
|
|
103
|
+
JoseKeyEncryption::RsaOaep512 => "RSA-OAEP-512",
|
|
104
|
+
JoseKeyEncryption::Pbes2Hs256A128kw => "PBES2-HS256+A128KW",
|
|
105
|
+
JoseKeyEncryption::Pbes2Hs384A192kw => "PBES2-HS384+A192KW",
|
|
106
|
+
JoseKeyEncryption::Pbes2Hs512A256kw => "PBES2-HS512+A256KW",
|
|
107
|
+
JoseKeyEncryption::A128kw => "A128KW",
|
|
108
|
+
JoseKeyEncryption::A192kw => "A192KW",
|
|
109
|
+
JoseKeyEncryption::A256kw => "A256KW",
|
|
110
|
+
JoseKeyEncryption::A128gcmkw => "A128GCMKW",
|
|
111
|
+
JoseKeyEncryption::A192gcmkw => "A192GCMKW",
|
|
112
|
+
JoseKeyEncryption::A256gcmkw => "A256GCMKW",
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
#[napi]
|
|
118
|
+
pub enum JoseSigningAlgorithm {
|
|
119
|
+
Es256 = 0,
|
|
120
|
+
Es384 = 1,
|
|
121
|
+
Es512 = 2,
|
|
122
|
+
Es256k = 3,
|
|
123
|
+
Eddsa = 4,
|
|
124
|
+
Hs256 = 5,
|
|
125
|
+
Hs384 = 6,
|
|
126
|
+
Hs512 = 7,
|
|
127
|
+
Rs256 = 8,
|
|
128
|
+
Rs384 = 9,
|
|
129
|
+
Rs512 = 10,
|
|
130
|
+
Ps256 = 11,
|
|
131
|
+
Ps384 = 12,
|
|
132
|
+
Ps512 = 13,
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
impl JoseSigningAlgorithm {
|
|
136
|
+
fn to_rust_string(&self) -> &'static str {
|
|
137
|
+
match self {
|
|
138
|
+
JoseSigningAlgorithm::Es256 => "ES256",
|
|
139
|
+
JoseSigningAlgorithm::Es384 => "ES384",
|
|
140
|
+
JoseSigningAlgorithm::Es512 => "ES512",
|
|
141
|
+
JoseSigningAlgorithm::Es256k => "ES256K",
|
|
142
|
+
JoseSigningAlgorithm::Eddsa => "EDDSA",
|
|
143
|
+
JoseSigningAlgorithm::Hs256 => "HS256",
|
|
144
|
+
JoseSigningAlgorithm::Hs384 => "HS384",
|
|
145
|
+
JoseSigningAlgorithm::Hs512 => "HS512",
|
|
146
|
+
JoseSigningAlgorithm::Rs256 => "RS256",
|
|
147
|
+
JoseSigningAlgorithm::Rs384 => "RS384",
|
|
148
|
+
JoseSigningAlgorithm::Rs512 => "RS512",
|
|
149
|
+
JoseSigningAlgorithm::Ps256 => "PS256",
|
|
150
|
+
JoseSigningAlgorithm::Ps384 => "PS384",
|
|
151
|
+
JoseSigningAlgorithm::Ps512 => "PS512",
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
// ---------------------------------------------------------------------------
|
|
157
|
+
// Result types
|
|
158
|
+
// ---------------------------------------------------------------------------
|
|
159
|
+
|
|
160
|
+
#[napi(object)]
|
|
161
|
+
pub struct JoseEncryptResult {
|
|
162
|
+
pub ciphertext: String,
|
|
163
|
+
pub tag: Option<String>,
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
// ---------------------------------------------------------------------------
|
|
167
|
+
// Key Generation (Task 9)
|
|
168
|
+
// ---------------------------------------------------------------------------
|
|
169
|
+
|
|
170
|
+
/// Generate a JWK key pair and return the JWK (public + private) as a JSON object.
|
|
171
|
+
/// Matches ffi-jose `generateJWK({ namedCurve })`.
|
|
172
|
+
#[napi]
|
|
173
|
+
pub fn generate_jwk(named_curve: JoseNamedCurve) -> napi::Result<Value> {
|
|
174
|
+
let curve = named_curve.to_rust_string().to_string();
|
|
175
|
+
let jwk_str = jose_generate_key_pair_jwk(curve)
|
|
176
|
+
.map_err(|e| napi::Error::from_reason(format!("generateJWK failed: {e}")))?;
|
|
177
|
+
serde_json::from_str(&jwk_str)
|
|
178
|
+
.map_err(|e| napi::Error::from_reason(format!("generateJWK parse failed: {e}")))
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
/// Generate a full key pair (JWK, PEM, DER formats) and return as a JSON object.
|
|
182
|
+
/// Matches ffi-jose `generateKeyPair(type, { namedCurve })`.
|
|
183
|
+
#[napi]
|
|
184
|
+
pub fn generate_key_pair(named_curve: JoseNamedCurve) -> napi::Result<Value> {
|
|
185
|
+
let curve = named_curve.to_rust_string().to_string();
|
|
186
|
+
let kp_str = jose_generate_key_pair(curve)
|
|
187
|
+
.map_err(|e| napi::Error::from_reason(format!("generateKeyPair failed: {e}")))?;
|
|
188
|
+
serde_json::from_str(&kp_str)
|
|
189
|
+
.map_err(|e| napi::Error::from_reason(format!("generateKeyPair parse failed: {e}")))
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
// ---------------------------------------------------------------------------
|
|
193
|
+
// Symmetric Encrypt/Decrypt (Task 10)
|
|
194
|
+
// ---------------------------------------------------------------------------
|
|
195
|
+
|
|
196
|
+
/// Low-level symmetric encryption. Key and IV as hex strings, plaintext as base64.
|
|
197
|
+
/// Matches ffi-jose `encrypt(enc, plaintext, cek, iv, aad, didcomm)`.
|
|
198
|
+
#[napi(js_name = "joseEncrypt")]
|
|
199
|
+
pub fn jose_encrypt_napi(
|
|
200
|
+
enc: JoseContentEncryption,
|
|
201
|
+
key: String,
|
|
202
|
+
iv: String,
|
|
203
|
+
message: String,
|
|
204
|
+
aad: Option<String>,
|
|
205
|
+
) -> napi::Result<JoseEncryptResult> {
|
|
206
|
+
let result = jose_encrypt(
|
|
207
|
+
enc.to_rust_string().to_string(),
|
|
208
|
+
key,
|
|
209
|
+
iv,
|
|
210
|
+
message,
|
|
211
|
+
aad.unwrap_or_default(),
|
|
212
|
+
)
|
|
213
|
+
.map_err(|e| napi::Error::from_reason(format!("joseEncrypt failed: {e}")))?;
|
|
214
|
+
|
|
215
|
+
Ok(JoseEncryptResult {
|
|
216
|
+
ciphertext: result.ciphertext,
|
|
217
|
+
tag: result.tag,
|
|
218
|
+
})
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
/// Low-level symmetric decryption. Returns base64-encoded plaintext.
|
|
222
|
+
/// Matches ffi-jose `decrypt(enc, cek, ciphertext, iv, tag, aad)`.
|
|
223
|
+
#[napi(js_name = "joseDecrypt")]
|
|
224
|
+
pub fn jose_decrypt_napi(
|
|
225
|
+
enc: JoseContentEncryption,
|
|
226
|
+
key: String,
|
|
227
|
+
iv: String,
|
|
228
|
+
ciphertext: String,
|
|
229
|
+
aad: Option<String>,
|
|
230
|
+
tag: Option<String>,
|
|
231
|
+
) -> napi::Result<String> {
|
|
232
|
+
jose_decrypt(
|
|
233
|
+
enc.to_rust_string().to_string(),
|
|
234
|
+
key,
|
|
235
|
+
iv,
|
|
236
|
+
ciphertext,
|
|
237
|
+
aad.unwrap_or_default(),
|
|
238
|
+
tag,
|
|
239
|
+
)
|
|
240
|
+
.map_err(|e| napi::Error::from_reason(format!("joseDecrypt failed: {e}")))
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
// ---------------------------------------------------------------------------
|
|
244
|
+
// JWE — JSON Web Encryption (Task 11)
|
|
245
|
+
// ---------------------------------------------------------------------------
|
|
246
|
+
|
|
247
|
+
/// Resolve the `typ` header from the `didcomm` flag.
|
|
248
|
+
fn resolve_typ_encrypted(didcomm: Option<bool>) -> &'static str {
|
|
249
|
+
if didcomm.unwrap_or(false) {
|
|
250
|
+
TOKEN_TYPE_DIDCOM_ENCRYPTED
|
|
251
|
+
} else {
|
|
252
|
+
TOKEN_TYPE_JWT
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
/// Resolve the `typ` header from the `didcomm` flag for signed tokens.
|
|
257
|
+
fn resolve_typ_signed(didcomm: Option<bool>) -> &'static str {
|
|
258
|
+
if didcomm.unwrap_or(false) {
|
|
259
|
+
TOKEN_TYPE_DIDCOM_SIGNED
|
|
260
|
+
} else {
|
|
261
|
+
TOKEN_TYPE_JWT
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
/// Encrypt a JSON payload for one or more recipients using JWE General JSON serialization.
|
|
266
|
+
/// Matches ffi-jose `generalEncryptJson(alg, enc, payload, recipients, didcomm)`.
|
|
267
|
+
#[napi]
|
|
268
|
+
pub fn general_encrypt_json(
|
|
269
|
+
alg: JoseKeyEncryption,
|
|
270
|
+
enc: JoseContentEncryption,
|
|
271
|
+
payload: Value,
|
|
272
|
+
recipients: Vec<Value>,
|
|
273
|
+
didcomm: Option<bool>,
|
|
274
|
+
) -> napi::Result<Value> {
|
|
275
|
+
let typ = resolve_typ_encrypted(didcomm);
|
|
276
|
+
let payload_str = serde_json::to_string(&payload)
|
|
277
|
+
.map_err(|e| napi::Error::from_reason(format!("generalEncryptJson serialize payload: {e}")))?;
|
|
278
|
+
let recipients_str = serde_json::to_string(&recipients)
|
|
279
|
+
.map_err(|e| napi::Error::from_reason(format!("generalEncryptJson serialize recipients: {e}")))?;
|
|
280
|
+
|
|
281
|
+
let result = jose_general_encrypt_json(
|
|
282
|
+
alg.to_rust_string().to_string(),
|
|
283
|
+
enc.to_rust_string().to_string(),
|
|
284
|
+
typ.to_string(),
|
|
285
|
+
payload_str,
|
|
286
|
+
recipients_str,
|
|
287
|
+
None,
|
|
288
|
+
)
|
|
289
|
+
.map_err(|e| napi::Error::from_reason(format!("generalEncryptJson failed: {e}")))?;
|
|
290
|
+
|
|
291
|
+
serde_json::from_str(&result)
|
|
292
|
+
.map_err(|e| napi::Error::from_reason(format!("generalEncryptJson parse result: {e}")))
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
/// Decrypt a JWE JSON object using a JWK private key.
|
|
296
|
+
/// Matches ffi-jose `decryptJson(jwe, jwk)`.
|
|
297
|
+
#[napi]
|
|
298
|
+
pub fn decrypt_json(jwe: Value, jwk: Value) -> napi::Result<Value> {
|
|
299
|
+
let jwe_str = serde_json::to_string(&jwe)
|
|
300
|
+
.map_err(|e| napi::Error::from_reason(format!("decryptJson serialize jwe: {e}")))?;
|
|
301
|
+
let jwk_str = serde_json::to_string(&jwk)
|
|
302
|
+
.map_err(|e| napi::Error::from_reason(format!("decryptJson serialize jwk: {e}")))?;
|
|
303
|
+
|
|
304
|
+
let result = jose_decrypt_json(jwe_str, jwk_str)
|
|
305
|
+
.map_err(|e| napi::Error::from_reason(format!("decryptJson failed: {e}")))?;
|
|
306
|
+
|
|
307
|
+
serde_json::from_str(&result)
|
|
308
|
+
.map_err(|e| napi::Error::from_reason(format!("decryptJson parse result: {e}")))
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
// ---------------------------------------------------------------------------
|
|
312
|
+
// JWS — JSON Web Signature (Task 12)
|
|
313
|
+
// ---------------------------------------------------------------------------
|
|
314
|
+
|
|
315
|
+
/// Sign a JSON payload using JWS Compact serialization.
|
|
316
|
+
/// Matches ffi-jose `compactSignJson(alg, payload, jwk, didcomm)`.
|
|
317
|
+
#[napi]
|
|
318
|
+
pub fn compact_sign_json(
|
|
319
|
+
alg: JoseSigningAlgorithm,
|
|
320
|
+
payload: Value,
|
|
321
|
+
jwk: Value,
|
|
322
|
+
didcomm: Option<bool>,
|
|
323
|
+
) -> napi::Result<String> {
|
|
324
|
+
let typ = resolve_typ_signed(didcomm);
|
|
325
|
+
let payload_str = serde_json::to_string(&payload)
|
|
326
|
+
.map_err(|e| napi::Error::from_reason(format!("compactSignJson serialize payload: {e}")))?;
|
|
327
|
+
let jwk_str = serde_json::to_string(&jwk)
|
|
328
|
+
.map_err(|e| napi::Error::from_reason(format!("compactSignJson serialize jwk: {e}")))?;
|
|
329
|
+
|
|
330
|
+
jose_compact_sign_json(
|
|
331
|
+
alg.to_rust_string().to_string(),
|
|
332
|
+
typ.to_string(),
|
|
333
|
+
payload_str,
|
|
334
|
+
jwk_str,
|
|
335
|
+
)
|
|
336
|
+
.map_err(|e| napi::Error::from_reason(format!("compactSignJson failed: {e}")))
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
/// Verify a JWS Compact serialization and return the payload.
|
|
340
|
+
/// Matches ffi-jose `compactJsonVerify(jws, jwk)`.
|
|
341
|
+
#[napi]
|
|
342
|
+
pub fn compact_json_verify(jws: String, jwk: Value) -> napi::Result<Value> {
|
|
343
|
+
let jwk_str = serde_json::to_string(&jwk)
|
|
344
|
+
.map_err(|e| napi::Error::from_reason(format!("compactJsonVerify serialize jwk: {e}")))?;
|
|
345
|
+
|
|
346
|
+
let result = jose_compact_verify_json(jws, jwk_str)
|
|
347
|
+
.map_err(|e| napi::Error::from_reason(format!("compactJsonVerify failed: {e}")))?;
|
|
348
|
+
|
|
349
|
+
serde_json::from_str(&result)
|
|
350
|
+
.map_err(|e| napi::Error::from_reason(format!("compactJsonVerify parse result: {e}")))
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
/// Sign a JSON payload using JWS Flattened JSON serialization.
|
|
354
|
+
/// Matches ffi-jose `flattenedSignJson(alg, payload, jwk, didcomm)`.
|
|
355
|
+
#[napi]
|
|
356
|
+
pub fn flattened_sign_json(
|
|
357
|
+
alg: JoseSigningAlgorithm,
|
|
358
|
+
payload: Value,
|
|
359
|
+
jwk: Value,
|
|
360
|
+
didcomm: Option<bool>,
|
|
361
|
+
) -> napi::Result<Value> {
|
|
362
|
+
let typ = resolve_typ_signed(didcomm);
|
|
363
|
+
let payload_str = serde_json::to_string(&payload)
|
|
364
|
+
.map_err(|e| napi::Error::from_reason(format!("flattenedSignJson serialize payload: {e}")))?;
|
|
365
|
+
let jwk_str = serde_json::to_string(&jwk)
|
|
366
|
+
.map_err(|e| napi::Error::from_reason(format!("flattenedSignJson serialize jwk: {e}")))?;
|
|
367
|
+
|
|
368
|
+
let result = jose_flattened_sign_json(
|
|
369
|
+
alg.to_rust_string().to_string(),
|
|
370
|
+
typ.to_string(),
|
|
371
|
+
payload_str,
|
|
372
|
+
jwk_str,
|
|
373
|
+
)
|
|
374
|
+
.map_err(|e| napi::Error::from_reason(format!("flattenedSignJson failed: {e}")))?;
|
|
375
|
+
|
|
376
|
+
serde_json::from_str(&result)
|
|
377
|
+
.map_err(|e| napi::Error::from_reason(format!("flattenedSignJson parse result: {e}")))
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
/// Verify a JWS Flattened or General JSON serialization and return the payload.
|
|
381
|
+
/// Matches ffi-jose `jsonVerify(jws, jwk)`.
|
|
382
|
+
#[napi]
|
|
383
|
+
pub fn json_verify(jws: Value, jwk: Value) -> napi::Result<Value> {
|
|
384
|
+
let jws_str = serde_json::to_string(&jws)
|
|
385
|
+
.map_err(|e| napi::Error::from_reason(format!("jsonVerify serialize jws: {e}")))?;
|
|
386
|
+
let jwk_str = serde_json::to_string(&jwk)
|
|
387
|
+
.map_err(|e| napi::Error::from_reason(format!("jsonVerify serialize jwk: {e}")))?;
|
|
388
|
+
|
|
389
|
+
let result = jose_verify_json(jws_str, jwk_str)
|
|
390
|
+
.map_err(|e| napi::Error::from_reason(format!("jsonVerify failed: {e}")))?;
|
|
391
|
+
|
|
392
|
+
serde_json::from_str(&result)
|
|
393
|
+
.map_err(|e| napi::Error::from_reason(format!("jsonVerify parse result: {e}")))
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
/// Sign a JSON payload using JWS General JSON serialization with multiple signers.
|
|
397
|
+
/// Matches ffi-jose `generalSignJson(payload, jwks, didcomm)`.
|
|
398
|
+
#[napi]
|
|
399
|
+
pub fn general_sign_json(
|
|
400
|
+
payload: Value,
|
|
401
|
+
jwks: Vec<Value>,
|
|
402
|
+
didcomm: Option<bool>,
|
|
403
|
+
) -> napi::Result<Value> {
|
|
404
|
+
let typ = resolve_typ_signed(didcomm);
|
|
405
|
+
let payload_str = serde_json::to_string(&payload)
|
|
406
|
+
.map_err(|e| napi::Error::from_reason(format!("generalSignJson serialize payload: {e}")))?;
|
|
407
|
+
let jwks_str = serde_json::to_string(&jwks)
|
|
408
|
+
.map_err(|e| napi::Error::from_reason(format!("generalSignJson serialize jwks: {e}")))?;
|
|
409
|
+
|
|
410
|
+
let result = jose_general_sign_json(typ.to_string(), payload_str, jwks_str)
|
|
411
|
+
.map_err(|e| napi::Error::from_reason(format!("generalSignJson failed: {e}")))?;
|
|
412
|
+
|
|
413
|
+
serde_json::from_str(&result)
|
|
414
|
+
.map_err(|e| napi::Error::from_reason(format!("generalSignJson parse result: {e}")))
|
|
415
|
+
}
|
package/src/jsonld.rs
CHANGED
|
@@ -1,16 +1,12 @@
|
|
|
1
1
|
use serde_json::{json, Value};
|
|
2
|
-
use std::collections::HashMap;
|
|
3
|
-
use std::num::NonZeroUsize;
|
|
4
2
|
use std::sync::Arc;
|
|
5
3
|
|
|
6
|
-
use lru::LruCache;
|
|
7
|
-
|
|
8
4
|
use vc::{
|
|
9
|
-
document_loader::nuggets::
|
|
5
|
+
document_loader::nuggets::DocumentLoader,
|
|
10
6
|
jsonld::{self, context_resolver::ContextResolver},
|
|
11
7
|
};
|
|
12
8
|
|
|
13
|
-
use crate::ld_signatures::
|
|
9
|
+
use crate::ld_signatures::loader_from_contexts;
|
|
14
10
|
|
|
15
11
|
/// JSON-LD processor — drop-in replacement for the jsonld.js API.
|
|
16
12
|
///
|
|
@@ -37,26 +33,8 @@ impl JsonLd {
|
|
|
37
33
|
/// of the built-in nuggets contexts).
|
|
38
34
|
#[napi(constructor)]
|
|
39
35
|
pub fn new(options: Option<Value>) -> Self {
|
|
40
|
-
let
|
|
41
|
-
|
|
42
|
-
.map(|o| parse_contexts(o))
|
|
43
|
-
.unwrap_or_default();
|
|
44
|
-
|
|
45
|
-
let mut ctx = load_nuggets_context();
|
|
46
|
-
ctx.extend(additional);
|
|
47
|
-
|
|
48
|
-
let resolver = Arc::new(tokio::sync::RwLock::new(
|
|
49
|
-
vc::did_resolver::resolver::Resolver::new(
|
|
50
|
-
vc::did_resolver::resolver::ResolverRegistry::new(),
|
|
51
|
-
None,
|
|
52
|
-
),
|
|
53
|
-
));
|
|
54
|
-
let opts = vc::did_resolver::resolver::DidResolutionOptions::default();
|
|
55
|
-
let loader = Arc::new(DocumentLoader::new(ctx, resolver, opts));
|
|
56
|
-
let cr = Arc::new(ContextResolver::new(LruCache::new(
|
|
57
|
-
NonZeroUsize::new(10).unwrap(),
|
|
58
|
-
)));
|
|
59
|
-
|
|
36
|
+
let contexts = options.as_ref().and_then(|o| o.get("contexts"));
|
|
37
|
+
let (loader, cr) = loader_from_contexts(contexts);
|
|
60
38
|
Self { loader, cr }
|
|
61
39
|
}
|
|
62
40
|
|
package/src/ld_signatures.rs
CHANGED
|
@@ -47,6 +47,18 @@ pub(crate) fn create_document_loader(
|
|
|
47
47
|
(loader, cr)
|
|
48
48
|
}
|
|
49
49
|
|
|
50
|
+
/// Parse an optional nonce string: try base64 decode, fall back to raw bytes.
|
|
51
|
+
fn parse_nonce(options: &Value) -> Option<Vec<u8>> {
|
|
52
|
+
options
|
|
53
|
+
.get("nonce")
|
|
54
|
+
.and_then(|v| v.as_str())
|
|
55
|
+
.map(|s| {
|
|
56
|
+
BASE64_STANDARD
|
|
57
|
+
.decode(s)
|
|
58
|
+
.unwrap_or_else(|_| s.as_bytes().to_vec())
|
|
59
|
+
})
|
|
60
|
+
}
|
|
61
|
+
|
|
50
62
|
/// Parse the `contexts` field from options JSON into a HashMap.
|
|
51
63
|
pub(crate) fn parse_contexts(options: &Value) -> HashMap<String, Value> {
|
|
52
64
|
options
|
|
@@ -60,6 +72,20 @@ pub(crate) fn parse_contexts(options: &Value) -> HashMap<String, Value> {
|
|
|
60
72
|
.unwrap_or_default()
|
|
61
73
|
}
|
|
62
74
|
|
|
75
|
+
/// Create a DocumentLoader + ContextResolver from an optional contexts Value.
|
|
76
|
+
///
|
|
77
|
+
/// This is the common pattern used by all class-based suite methods:
|
|
78
|
+
/// takes the `contexts` field from options and creates the loader pair.
|
|
79
|
+
pub(crate) fn loader_from_contexts(
|
|
80
|
+
contexts: Option<&Value>,
|
|
81
|
+
) -> (Arc<DocumentLoader>, Arc<ContextResolver>) {
|
|
82
|
+
let additional = contexts
|
|
83
|
+
.and_then(|v| v.as_object())
|
|
84
|
+
.map(|obj| obj.iter().map(|(k, v)| (k.clone(), v.clone())).collect())
|
|
85
|
+
.unwrap_or_default();
|
|
86
|
+
create_document_loader(additional)
|
|
87
|
+
}
|
|
88
|
+
|
|
63
89
|
/// Sign a document with BbsBlsSignature2020 and embed the proof.
|
|
64
90
|
///
|
|
65
91
|
/// Input: `{ document, keyPair: {id, controller, publicKeyBase58, privateKeyBase58}, contexts? }`
|
|
@@ -135,14 +161,7 @@ pub async fn ld_derive_proof(options: Value) -> napi::Result<Value> {
|
|
|
135
161
|
.cloned()
|
|
136
162
|
.ok_or_else(|| napi::Error::from_reason("missing 'revealDocument' in options"))?;
|
|
137
163
|
|
|
138
|
-
let nonce = options
|
|
139
|
-
.get("nonce")
|
|
140
|
-
.and_then(|v| v.as_str())
|
|
141
|
-
.map(|s| {
|
|
142
|
-
BASE64_STANDARD
|
|
143
|
-
.decode(s)
|
|
144
|
-
.unwrap_or_else(|_| s.as_bytes().to_vec())
|
|
145
|
-
});
|
|
164
|
+
let nonce = parse_nonce(&options);
|
|
146
165
|
|
|
147
166
|
let additional_contexts = parse_contexts(&options);
|
|
148
167
|
|
|
@@ -163,14 +182,7 @@ pub async fn derive_proof(
|
|
|
163
182
|
reveal_document: Value,
|
|
164
183
|
options: Value,
|
|
165
184
|
) -> napi::Result<Value> {
|
|
166
|
-
let nonce = options
|
|
167
|
-
.get("nonce")
|
|
168
|
-
.and_then(|v| v.as_str())
|
|
169
|
-
.map(|s| {
|
|
170
|
-
BASE64_STANDARD
|
|
171
|
-
.decode(s)
|
|
172
|
-
.unwrap_or_else(|_| s.as_bytes().to_vec())
|
|
173
|
-
});
|
|
185
|
+
let nonce = parse_nonce(&options);
|
|
174
186
|
|
|
175
187
|
let additional_contexts = parse_contexts(&options);
|
|
176
188
|
|
|
@@ -298,14 +310,7 @@ pub async fn derive_proof_holder_bound(
|
|
|
298
310
|
})
|
|
299
311
|
.collect::<napi::Result<_>>()?;
|
|
300
312
|
|
|
301
|
-
let nonce = options
|
|
302
|
-
.get("nonce")
|
|
303
|
-
.and_then(|v| v.as_str())
|
|
304
|
-
.map(|s| {
|
|
305
|
-
BASE64_STANDARD
|
|
306
|
-
.decode(s)
|
|
307
|
-
.unwrap_or_else(|_| s.as_bytes().to_vec())
|
|
308
|
-
});
|
|
313
|
+
let nonce = parse_nonce(&options);
|
|
309
314
|
|
|
310
315
|
let additional_contexts = parse_contexts(&options);
|
|
311
316
|
|
package/src/lib.rs
CHANGED
package/test.mjs
CHANGED
|
@@ -240,6 +240,44 @@ test('full demo_single flow: sign → verify → derive → verify derived', asy
|
|
|
240
240
|
assert.equal(derivedVerifyResult.verified, true, `derived verify failed: ${derivedVerifyResult.error}`);
|
|
241
241
|
});
|
|
242
242
|
|
|
243
|
+
test('demo_multi flow: sign twice → verify → derive → verify derived', async () => {
|
|
244
|
+
// Step 1: First sign
|
|
245
|
+
const signed = await ldSign({
|
|
246
|
+
document: inputDocument,
|
|
247
|
+
keyPair,
|
|
248
|
+
contexts,
|
|
249
|
+
});
|
|
250
|
+
assert.ok(signed.proof, 'signed document has proof');
|
|
251
|
+
assert.ok(!Array.isArray(signed.proof), 'single sign produces a single proof (not array)');
|
|
252
|
+
|
|
253
|
+
// Step 2: Second sign (creates multi-proof array)
|
|
254
|
+
const multiSigned = await ldSign({
|
|
255
|
+
document: signed,
|
|
256
|
+
keyPair,
|
|
257
|
+
contexts,
|
|
258
|
+
});
|
|
259
|
+
assert.ok(Array.isArray(multiSigned.proof), 'double sign produces proof array');
|
|
260
|
+
assert.equal(multiSigned.proof.length, 2, 'proof array has 2 proofs');
|
|
261
|
+
assert.equal(multiSigned.proof[0].type, 'BbsBlsSignature2020');
|
|
262
|
+
assert.equal(multiSigned.proof[1].type, 'BbsBlsSignature2020');
|
|
263
|
+
|
|
264
|
+
// Step 3: Verify multi-proof document
|
|
265
|
+
const verifyResult = await ldVerify({ document: multiSigned, contexts });
|
|
266
|
+
assert.equal(verifyResult.verified, true, `multi-proof verify failed: ${verifyResult.error}`);
|
|
267
|
+
|
|
268
|
+
// Step 4: Derive selective disclosure from multi-proof document
|
|
269
|
+
const derived = await ldDeriveProof({
|
|
270
|
+
document: multiSigned,
|
|
271
|
+
revealDocument,
|
|
272
|
+
contexts,
|
|
273
|
+
});
|
|
274
|
+
assert.ok(derived.proof, 'derived document has proof');
|
|
275
|
+
|
|
276
|
+
// Step 5: Verify derived proof
|
|
277
|
+
const derivedVerifyResult = await ldVerify({ document: derived, contexts });
|
|
278
|
+
assert.equal(derivedVerifyResult.verified, true, `multi-proof derived verify failed: ${derivedVerifyResult.error}`);
|
|
279
|
+
});
|
|
280
|
+
|
|
243
281
|
|
|
244
282
|
//
|
|
245
283
|
// BbsBlsSignature2020 class-based API tests
|