@nuggetslife/vc 0.0.10 → 0.0.15

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.
Files changed (31) hide show
  1. package/Cargo.toml +5 -2
  2. package/index.d.ts +303 -3
  3. package/index.js +15 -1
  4. package/package.json +11 -11
  5. package/src/bls_signatures/bbs_bls_holder_bound_signature_2022/mod.rs +268 -0
  6. package/src/bls_signatures/bbs_bls_holder_bound_signature_2022/types.rs +26 -0
  7. package/src/bls_signatures/bbs_bls_holder_bound_signature_proof_2022/mod.rs +100 -0
  8. package/src/bls_signatures/bbs_bls_holder_bound_signature_proof_2022/types.rs +17 -0
  9. package/src/bls_signatures/bbs_bls_signature_2020/mod.rs +329 -0
  10. package/src/bls_signatures/bbs_bls_signature_2020/types.rs +37 -0
  11. package/src/bls_signatures/bbs_bls_signature_proof_2020/mod.rs +92 -0
  12. package/src/bls_signatures/bbs_bls_signature_proof_2020/types.rs +13 -0
  13. package/src/bls_signatures/bls_12381_g2_keypair/mod.rs +470 -0
  14. package/src/{types.rs → bls_signatures/bls_12381_g2_keypair/types.rs} +0 -11
  15. package/src/{validators.rs → bls_signatures/bls_12381_g2_keypair/validators.rs} +1 -1
  16. package/src/bls_signatures/bound_bls_12381_g2_keypair/mod.rs +70 -0
  17. package/src/bls_signatures/bound_bls_12381_g2_keypair/types.rs +11 -0
  18. package/src/bls_signatures/mod.rs +6 -0
  19. package/src/jsonld.rs +200 -0
  20. package/src/ld_signatures.rs +311 -0
  21. package/src/lib.rs +3 -463
  22. package/test-data/bbs.json +92 -0
  23. package/test-data/citizenVocab.json +57 -0
  24. package/test-data/controllerDocument.json +5 -0
  25. package/test-data/credentialsContext.json +315 -0
  26. package/test-data/deriveProofFrame.json +15 -0
  27. package/test-data/inputDocument.json +29 -0
  28. package/test-data/keyPair.json +6 -0
  29. package/test-data/suiteContext.json +82 -0
  30. package/test.mjs +1088 -22
  31. package/test_jsonld_crossverify.mjs +256 -0
package/src/jsonld.rs ADDED
@@ -0,0 +1,200 @@
1
+ use serde_json::{json, Value};
2
+ use std::collections::HashMap;
3
+ use std::num::NonZeroUsize;
4
+ use std::sync::Arc;
5
+
6
+ use lru::LruCache;
7
+
8
+ use vc::{
9
+ document_loader::nuggets::{load_nuggets_context, DocumentLoader},
10
+ jsonld::{self, context_resolver::ContextResolver},
11
+ };
12
+
13
+ use crate::ld_signatures::parse_contexts;
14
+
15
+ /// JSON-LD processor — drop-in replacement for the jsonld.js API.
16
+ ///
17
+ /// Holds a `DocumentLoader` and `ContextResolver` internally so they are
18
+ /// created once and reused across all method calls.
19
+ ///
20
+ /// ```js
21
+ /// const jsonld = new JsonLd({ contexts: { "https://example.org/ctx": {...} } });
22
+ /// const expanded = await jsonld.expand(doc);
23
+ /// const compacted = await jsonld.compact(expanded, ctx);
24
+ /// ```
25
+ #[napi]
26
+ pub struct JsonLd {
27
+ loader: Arc<DocumentLoader>,
28
+ cr: Arc<ContextResolver>,
29
+ }
30
+
31
+ #[napi]
32
+ impl JsonLd {
33
+ /// Create a new JSON-LD processor.
34
+ ///
35
+ /// Accepts optional `{ contexts?: Record<string, any> }` to register
36
+ /// additional URL → document mappings in the document loader (on top
37
+ /// of the built-in nuggets contexts).
38
+ #[napi(constructor)]
39
+ pub fn new(options: Option<Value>) -> Self {
40
+ let additional: HashMap<String, Value> = options
41
+ .as_ref()
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
+
60
+ Self { loader, cr }
61
+ }
62
+
63
+ /// Expand a JSON-LD document.
64
+ ///
65
+ /// `options` can include: `base`, `expandContext`, `keepFreeFloatingNodes`,
66
+ /// `processingMode`.
67
+ ///
68
+ /// Returns an array of expanded JSON-LD objects.
69
+ #[napi]
70
+ pub async fn expand(
71
+ &self,
72
+ input: Value,
73
+ options: Option<Value>,
74
+ ) -> napi::Result<Value> {
75
+ let opts = options.unwrap_or(json!({}));
76
+ jsonld::expand(input, self.loader.clone(), self.cr.clone(), opts)
77
+ .await
78
+ .map_err(|e| napi::Error::from_reason(format!("expand failed: {e}")))
79
+ }
80
+
81
+ /// Compact a JSON-LD document using a context.
82
+ ///
83
+ /// `ctx` — the compaction context (object, array, or string URL).
84
+ /// `options` can include: `base`, `compactArrays`, `graph`,
85
+ /// `skipExpansion`, `processingMode`.
86
+ ///
87
+ /// Returns the compacted JSON-LD object.
88
+ #[napi]
89
+ pub async fn compact(
90
+ &self,
91
+ input: Value,
92
+ ctx: Value,
93
+ options: Option<Value>,
94
+ ) -> napi::Result<Value> {
95
+ let opts = options.unwrap_or(json!({}));
96
+ jsonld::compact_api(input, ctx, self.loader.clone(), self.cr.clone(), opts)
97
+ .await
98
+ .map_err(|e| napi::Error::from_reason(format!("compact failed: {e}")))
99
+ }
100
+
101
+ /// Flatten a JSON-LD document.
102
+ ///
103
+ /// `ctx` — optional context for compacting the flattened result.
104
+ /// Pass `null` to get the flattened expanded form.
105
+ /// `options` can include: `base`, `expandContext`, `processingMode`.
106
+ ///
107
+ /// Returns flattened array (no context) or compacted object (with context).
108
+ #[napi]
109
+ pub async fn flatten(
110
+ &self,
111
+ input: Value,
112
+ ctx: Option<Value>,
113
+ options: Option<Value>,
114
+ ) -> napi::Result<Value> {
115
+ let opts = options.unwrap_or(json!({}));
116
+ // Filter out null ctx (JS passes null, Rust expects None)
117
+ let ctx = ctx.filter(|v| !v.is_null());
118
+ jsonld::flatten(input, ctx, self.loader.clone(), self.cr.clone(), opts)
119
+ .await
120
+ .map_err(|e| napi::Error::from_reason(format!("flatten failed: {e}")))
121
+ }
122
+
123
+ /// Frame a JSON-LD document.
124
+ ///
125
+ /// `frame` — the framing template.
126
+ /// `options` can include: `base`, `embed`, `explicit`, `requireAll`,
127
+ /// `omitDefault`, `omitGraph`, `processingMode`.
128
+ ///
129
+ /// Returns the framed JSON-LD object.
130
+ #[napi]
131
+ pub async fn frame(
132
+ &self,
133
+ input: Value,
134
+ frame: Value,
135
+ options: Option<Value>,
136
+ ) -> napi::Result<Value> {
137
+ let opts = options.unwrap_or(json!({}));
138
+ jsonld::frame(input, frame, self.loader.clone(), self.cr.clone(), opts)
139
+ .await
140
+ .map_err(|e| napi::Error::from_reason(format!("frame failed: {e}")))
141
+ }
142
+
143
+ /// Convert a JSON-LD document to an RDF dataset.
144
+ ///
145
+ /// `options` can include: `base`, `expandContext`, `skipExpansion`,
146
+ /// `format` (`"application/n-quads"` for string output), `processingMode`.
147
+ ///
148
+ /// Returns an array of quads or an N-Quads string (depending on `format`).
149
+ #[napi(js_name = "toRDF")]
150
+ pub async fn to_rdf(
151
+ &self,
152
+ input: Value,
153
+ options: Option<Value>,
154
+ ) -> napi::Result<Value> {
155
+ let opts = options.unwrap_or(json!({}));
156
+ jsonld::to_rdf(input, self.loader.clone(), self.cr.clone(), opts)
157
+ .await
158
+ .map_err(|e| napi::Error::from_reason(format!("toRDF failed: {e}")))
159
+ }
160
+
161
+ /// Convert an RDF dataset to a JSON-LD document.
162
+ ///
163
+ /// `dataset` — N-Quads string or array of quads.
164
+ /// `options` can include: `useRdfType`, `useNativeTypes`, `rdfDirection`.
165
+ ///
166
+ /// Returns an array of JSON-LD objects.
167
+ #[napi(js_name = "fromRDF")]
168
+ pub fn from_rdf(&self, dataset: Value, options: Option<Value>) -> napi::Result<Value> {
169
+ let opts = options.unwrap_or(json!({}));
170
+ jsonld::from_rdf_api(dataset, opts)
171
+ .map_err(|e| napi::Error::from_reason(format!("fromRDF failed: {e}")))
172
+ }
173
+
174
+ /// Canonize (normalize) a JSON-LD document.
175
+ ///
176
+ /// `options` can include: `base`, `expandContext`, `skipExpansion`,
177
+ /// `inputFormat` (`"application/n-quads"`), `format`, `algorithm`,
178
+ /// `processingMode`.
179
+ ///
180
+ /// Returns the canonical N-Quads string.
181
+ #[napi]
182
+ pub async fn canonize(
183
+ &self,
184
+ input: Value,
185
+ options: Option<Value>,
186
+ ) -> napi::Result<Value> {
187
+ let mut opts = options.unwrap_or(json!({}));
188
+ // Defaults matching JS jsonld.js canonize()
189
+ if opts.get("algorithm").is_none() {
190
+ opts["algorithm"] = json!("RDFC-1.0");
191
+ }
192
+ if opts.get("format").is_none() {
193
+ opts["format"] = json!("application/n-quads");
194
+ }
195
+ let result = jsonld::canonize(input, self.loader.clone(), self.cr.clone(), opts)
196
+ .await
197
+ .map_err(|e| napi::Error::from_reason(format!("canonize failed: {e}")))?;
198
+ Ok(Value::String(result))
199
+ }
200
+ }
@@ -0,0 +1,311 @@
1
+ use std::collections::{BTreeMap, HashMap};
2
+ use std::num::NonZeroUsize;
3
+ use std::sync::Arc;
4
+
5
+ use base64::Engine;
6
+ use lru::LruCache;
7
+ use napi::bindgen_prelude::*;
8
+ use serde_json::Value;
9
+
10
+ use vc::{
11
+ bbs_signatures::bls12381::{
12
+ bls_blind_signature_commitment, bls_unblind_signature, bls_verify_blind_signature_proof,
13
+ },
14
+ did_resolver::resolver::{DidResolutionOptions, Resolver, ResolverRegistry},
15
+ document_loader::nuggets::{load_nuggets_context, DocumentLoader},
16
+ jsonld::{
17
+ context_resolver::ContextResolver,
18
+ signatures::{
19
+ self,
20
+ bbs::{
21
+ bls_12381_g2_keypair::{types::KeyPairOptions, Bls12381G2KeyPair},
22
+ BbsBlsSignature2020 as BbsSignatureSuite, SignatureSuiteOptions,
23
+ },
24
+ AssertionProofPurpose,
25
+ },
26
+ },
27
+ };
28
+
29
+ /// Create a DocumentLoader with nuggets contexts plus additional caller-provided contexts.
30
+ pub(crate) fn create_document_loader(
31
+ additional_contexts: HashMap<String, Value>,
32
+ ) -> (Arc<DocumentLoader>, Arc<ContextResolver>) {
33
+ let mut ctx = load_nuggets_context();
34
+ ctx.extend(additional_contexts);
35
+
36
+ let resolver = Arc::new(tokio::sync::RwLock::new(Resolver::new(
37
+ ResolverRegistry::new(),
38
+ None,
39
+ )));
40
+ let opts = DidResolutionOptions::default();
41
+ let loader = Arc::new(DocumentLoader::new(ctx, resolver, opts));
42
+ let cr = Arc::new(ContextResolver::new(LruCache::new(
43
+ NonZeroUsize::new(10).unwrap(),
44
+ )));
45
+
46
+ (loader, cr)
47
+ }
48
+
49
+ /// Parse the `contexts` field from options JSON into a HashMap.
50
+ pub(crate) fn parse_contexts(options: &Value) -> HashMap<String, Value> {
51
+ options
52
+ .get("contexts")
53
+ .and_then(|v| v.as_object())
54
+ .map(|obj| {
55
+ obj.iter()
56
+ .map(|(k, v)| (k.clone(), v.clone()))
57
+ .collect()
58
+ })
59
+ .unwrap_or_default()
60
+ }
61
+
62
+ /// Sign a document with BbsBlsSignature2020 and embed the proof.
63
+ ///
64
+ /// Input: `{ document, keyPair: {id, controller, publicKeyBase58, privateKeyBase58}, contexts? }`
65
+ /// Output: signed document JSON with embedded proof
66
+ #[napi]
67
+ pub async fn ld_sign(options: Value) -> napi::Result<Value> {
68
+ let document = options
69
+ .get("document")
70
+ .cloned()
71
+ .ok_or_else(|| napi::Error::from_reason("missing 'document' in options"))?;
72
+
73
+ let key_pair_opts: KeyPairOptions =
74
+ serde_json::from_value(options.get("keyPair").cloned().unwrap_or(Value::Null))
75
+ .map_err(|e| napi::Error::from_reason(format!("failed to parse keyPair: {e}")))?;
76
+
77
+ let additional_contexts = parse_contexts(&options);
78
+
79
+ let key = Bls12381G2KeyPair::new(Some(key_pair_opts));
80
+ let (loader, cr) = create_document_loader(additional_contexts);
81
+
82
+ let suite = BbsSignatureSuite::new(SignatureSuiteOptions {
83
+ key,
84
+ verification_method: None,
85
+ date: None,
86
+ canonize_options: None,
87
+ })
88
+ .await;
89
+
90
+ let purpose = AssertionProofPurpose::new();
91
+ signatures::sign(document, &suite, &purpose, loader, cr)
92
+ .await
93
+ .map_err(|e| napi::Error::from_reason(format!("ld_sign failed: {e}")))
94
+ }
95
+
96
+ /// Verify a document with an embedded proof (auto-detects proof type).
97
+ ///
98
+ /// Input: `{ document, contexts? }`
99
+ /// Output: `{ verified: boolean, error?: string }`
100
+ #[napi]
101
+ pub async fn ld_verify(options: Value) -> napi::Result<Value> {
102
+ let document = options
103
+ .get("document")
104
+ .cloned()
105
+ .ok_or_else(|| napi::Error::from_reason("missing 'document' in options"))?;
106
+
107
+ let additional_contexts = parse_contexts(&options);
108
+
109
+ let (loader, cr) = create_document_loader(additional_contexts);
110
+ let purpose = AssertionProofPurpose::new();
111
+ let result = signatures::verify(&document, &purpose, loader, cr)
112
+ .await
113
+ .map_err(|e| napi::Error::from_reason(format!("ld_verify failed: {e}")))?;
114
+
115
+ Ok(serde_json::json!({
116
+ "verified": result.verified,
117
+ "error": result.error,
118
+ }))
119
+ }
120
+
121
+ /// Derive a selective disclosure proof from a signed document.
122
+ ///
123
+ /// Input: `{ document, revealDocument, nonce?, contexts? }`
124
+ /// Output: derived document JSON with embedded proof
125
+ #[napi]
126
+ pub async fn ld_derive_proof(options: Value) -> napi::Result<Value> {
127
+ let document = options
128
+ .get("document")
129
+ .cloned()
130
+ .ok_or_else(|| napi::Error::from_reason("missing 'document' in options"))?;
131
+
132
+ let reveal_document = options
133
+ .get("revealDocument")
134
+ .cloned()
135
+ .ok_or_else(|| napi::Error::from_reason("missing 'revealDocument' in options"))?;
136
+
137
+ let nonce = options
138
+ .get("nonce")
139
+ .and_then(|v| v.as_str())
140
+ .map(|s| s.as_bytes().to_vec());
141
+
142
+ let additional_contexts = parse_contexts(&options);
143
+
144
+ let (loader, cr) = create_document_loader(additional_contexts);
145
+ signatures::derive_proof(&document, &reveal_document, nonce, loader, cr)
146
+ .await
147
+ .map_err(|e| napi::Error::from_reason(format!("ld_derive_proof failed: {e}")))
148
+ }
149
+
150
+ /// Standalone deriveProof function matching the JS reference's
151
+ /// `deriveProof(proofDocument, revealDocument, { suite, documentLoader, nonce })`.
152
+ ///
153
+ /// Input: proofDocument (signed doc), revealDocument (frame), options `{ contexts?, nonce? }`
154
+ /// Output: derived document JSON with embedded proof
155
+ #[napi]
156
+ pub async fn derive_proof(
157
+ proof_document: Value,
158
+ reveal_document: Value,
159
+ options: Value,
160
+ ) -> napi::Result<Value> {
161
+ let nonce = options
162
+ .get("nonce")
163
+ .and_then(|v| v.as_str())
164
+ .map(|s| s.as_bytes().to_vec());
165
+
166
+ let additional_contexts = parse_contexts(&options);
167
+
168
+ let (loader, cr) = create_document_loader(additional_contexts);
169
+ signatures::derive_proof(&proof_document, &reveal_document, nonce, loader, cr)
170
+ .await
171
+ .map_err(|e| napi::Error::from_reason(format!("deriveProof failed: {e}")))
172
+ }
173
+
174
+ /// Create a blind signature commitment (holder side).
175
+ ///
176
+ /// Input: issuerDID (for resolving public key), messages (blinded messages as Uint8Array[]),
177
+ /// blinded (indices), nonce, knownMessageCount, options `{ contexts? }`
178
+ /// Output: `{ commitment, challengeHash, blindingFactor, proofOfHiddenMessages }` (all Uint8Array)
179
+ #[napi]
180
+ pub async fn create_commitment(
181
+ public_key: Uint8Array,
182
+ messages: Vec<Uint8Array>,
183
+ blinded: Vec<u32>,
184
+ nonce: Uint8Array,
185
+ known_message_count: u32,
186
+ ) -> napi::Result<serde_json::Value> {
187
+ let pk = public_key.to_vec();
188
+ let nonce_vec = nonce.to_vec();
189
+ let blinded_indices: Vec<usize> = blinded.iter().map(|&i| i as usize).collect();
190
+ let total_message_count = blinded_indices.len() + known_message_count as usize;
191
+
192
+ let mut msgs = BTreeMap::new();
193
+ for (idx, msg) in blinded_indices.iter().zip(messages.iter()) {
194
+ msgs.insert(*idx, msg.to_vec());
195
+ }
196
+
197
+ let result =
198
+ bls_blind_signature_commitment(pk, msgs, nonce_vec, total_message_count)
199
+ .await
200
+ .map_err(|e| napi::Error::from_reason(format!("createCommitment failed: {e}")))?;
201
+
202
+ Ok(serde_json::json!({
203
+ "commitment": base64::prelude::BASE64_STANDARD.encode(&result.commitment),
204
+ "challengeHash": base64::prelude::BASE64_STANDARD.encode(&result.challenge_hash),
205
+ "blindingFactor": base64::prelude::BASE64_STANDARD.encode(&result.blinding_factor),
206
+ "proofOfHiddenMessages": base64::prelude::BASE64_STANDARD.encode(&result.proof_of_hidden_messages),
207
+ }))
208
+ }
209
+
210
+ /// Verify a blind signature commitment (issuer side).
211
+ ///
212
+ /// Returns true if the commitment proof is valid.
213
+ #[napi]
214
+ pub async fn verify_commitment(
215
+ public_key: Uint8Array,
216
+ commitment: Uint8Array,
217
+ proof_of_hidden_messages: Uint8Array,
218
+ challenge_hash: Uint8Array,
219
+ blinded: Vec<u32>,
220
+ nonce: Uint8Array,
221
+ known_message_count: u32,
222
+ ) -> napi::Result<bool> {
223
+ let pk = public_key.to_vec();
224
+ let blinded_indices: Vec<usize> = blinded.iter().map(|&i| i as usize).collect();
225
+ let total_message_count = blinded_indices.len() + known_message_count as usize;
226
+
227
+ bls_verify_blind_signature_proof(
228
+ pk,
229
+ commitment.to_vec(),
230
+ challenge_hash.to_vec(),
231
+ proof_of_hidden_messages.to_vec(),
232
+ blinded_indices,
233
+ nonce.to_vec(),
234
+ total_message_count,
235
+ )
236
+ .await
237
+ .map_err(|e| napi::Error::from_reason(format!("verifyCommitment failed: {e}")))
238
+ }
239
+
240
+ /// Unblind a blind signature (holder side).
241
+ ///
242
+ /// Takes the blind signature from the issuer and the holder's blinding factor,
243
+ /// returns the unblinded standard signature.
244
+ #[napi]
245
+ pub async fn unblind_signature(
246
+ blind_signature: Uint8Array,
247
+ blinding_factor: Uint8Array,
248
+ ) -> napi::Result<Uint8Array> {
249
+ let result = bls_unblind_signature(blind_signature.to_vec(), blinding_factor.to_vec())
250
+ .await
251
+ .map_err(|e| napi::Error::from_reason(format!("unblindSignature failed: {e}")))?;
252
+
253
+ Ok(Uint8Array::from(result))
254
+ }
255
+
256
+ /// Derive a selective disclosure proof from a holder-bound signed document.
257
+ ///
258
+ /// Input: proofDocument (signed doc), revealDocument (frame),
259
+ /// options `{ blindingFactor: Uint8Array, blindedMessages: Uint8Array[], contexts?, nonce? }`
260
+ /// Output: derived document JSON with embedded proof
261
+ #[napi]
262
+ pub async fn derive_proof_holder_bound(
263
+ proof_document: Value,
264
+ reveal_document: Value,
265
+ options: Value,
266
+ ) -> napi::Result<Value> {
267
+ let blinding_factor = options
268
+ .get("blindingFactor")
269
+ .and_then(|v| v.as_str())
270
+ .map(|s| {
271
+ base64::prelude::BASE64_STANDARD
272
+ .decode(s)
273
+ .map_err(|e| napi::Error::from_reason(format!("failed to decode blindingFactor: {e}")))
274
+ })
275
+ .transpose()?
276
+ .ok_or_else(|| napi::Error::from_reason("missing 'blindingFactor' in options"))?;
277
+
278
+ let blinded_messages: Vec<Vec<u8>> = options
279
+ .get("blindedMessages")
280
+ .and_then(|v| v.as_array())
281
+ .ok_or_else(|| napi::Error::from_reason("missing 'blindedMessages' in options"))?
282
+ .iter()
283
+ .map(|v| {
284
+ base64::prelude::BASE64_STANDARD
285
+ .decode(v.as_str().unwrap_or_default())
286
+ .map_err(|e| {
287
+ napi::Error::from_reason(format!("failed to decode blinded message: {e}"))
288
+ })
289
+ })
290
+ .collect::<napi::Result<_>>()?;
291
+
292
+ let nonce = options
293
+ .get("nonce")
294
+ .and_then(|v| v.as_str())
295
+ .map(|s| s.as_bytes().to_vec());
296
+
297
+ let additional_contexts = parse_contexts(&options);
298
+
299
+ let (loader, cr) = create_document_loader(additional_contexts);
300
+ signatures::derive_proof_holder_bound(
301
+ &proof_document,
302
+ &reveal_document,
303
+ blinding_factor,
304
+ blinded_messages,
305
+ nonce,
306
+ loader,
307
+ cr,
308
+ )
309
+ .await
310
+ .map_err(|e| napi::Error::from_reason(format!("deriveProofHolderBound failed: {e}")))
311
+ }