@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.
- package/Cargo.toml +5 -2
- package/index.d.ts +303 -3
- package/index.js +15 -1
- package/package.json +11 -11
- package/src/bls_signatures/bbs_bls_holder_bound_signature_2022/mod.rs +268 -0
- package/src/bls_signatures/bbs_bls_holder_bound_signature_2022/types.rs +26 -0
- package/src/bls_signatures/bbs_bls_holder_bound_signature_proof_2022/mod.rs +100 -0
- package/src/bls_signatures/bbs_bls_holder_bound_signature_proof_2022/types.rs +17 -0
- package/src/bls_signatures/bbs_bls_signature_2020/mod.rs +329 -0
- package/src/bls_signatures/bbs_bls_signature_2020/types.rs +37 -0
- package/src/bls_signatures/bbs_bls_signature_proof_2020/mod.rs +92 -0
- package/src/bls_signatures/bbs_bls_signature_proof_2020/types.rs +13 -0
- package/src/bls_signatures/bls_12381_g2_keypair/mod.rs +470 -0
- package/src/{types.rs → bls_signatures/bls_12381_g2_keypair/types.rs} +0 -11
- package/src/{validators.rs → bls_signatures/bls_12381_g2_keypair/validators.rs} +1 -1
- package/src/bls_signatures/bound_bls_12381_g2_keypair/mod.rs +70 -0
- package/src/bls_signatures/bound_bls_12381_g2_keypair/types.rs +11 -0
- package/src/bls_signatures/mod.rs +6 -0
- package/src/jsonld.rs +200 -0
- package/src/ld_signatures.rs +311 -0
- package/src/lib.rs +3 -463
- package/test-data/bbs.json +92 -0
- package/test-data/citizenVocab.json +57 -0
- package/test-data/controllerDocument.json +5 -0
- package/test-data/credentialsContext.json +315 -0
- package/test-data/deriveProofFrame.json +15 -0
- package/test-data/inputDocument.json +29 -0
- package/test-data/keyPair.json +6 -0
- package/test-data/suiteContext.json +82 -0
- package/test.mjs +1088 -22
- 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
|
+
}
|