@nuggetslife/vc 0.1.0 → 0.3.0
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 +1 -0
- package/W3C_CONFORMANCE.md +9 -6
- package/bench/frame_compare.mjs +7 -20
- package/bench/v2_internals.mjs +3 -5
- package/bench/vc_ops.mjs +4 -10
- package/index.d.ts +2 -2
- package/interop-allowlist.json +3 -0
- package/interop-smoke-allowlist.json +3 -0
- package/package.json +8 -7
- package/src/bls_signatures/bbs_bls_holder_bound_signature_2022/mod.rs +18 -27
- package/src/bls_signatures/bbs_bls_holder_bound_signature_proof_2022/mod.rs +9 -10
- package/src/bls_signatures/bbs_bls_signature_2020/mod.rs +21 -25
- package/src/bls_signatures/bbs_bls_signature_proof_2020/mod.rs +6 -12
- package/src/jsonld.rs +40 -75
- package/src/ld_signatures.rs +210 -224
- package/src/lib.rs +8 -0
- package/test-fixtures/interop/README.md +46 -0
- package/test-fixtures/interop/_contexts/README.md +51 -0
- package/test-fixtures/interop/_contexts/bbs-bound-v1.jsonld +92 -0
- package/test-fixtures/interop/_contexts/citizenship-v1.jsonld +58 -0
- package/test-fixtures/interop/_contexts/credentials-v1.jsonld +316 -0
- package/test-fixtures/interop/_contexts/elm-edc-ap.jsonld +809 -0
- package/test-fixtures/interop/_contexts/essif-schemas-vc-2020-v1.jsonld +47 -0
- package/test-fixtures/interop/_contexts/identity-v2.jsonld +195 -0
- package/test-fixtures/interop/_contexts/nuggets-identity-v1.jsonld +175 -0
- package/test-fixtures/interop/_contexts/nuggets-kyb-v1.jsonld +333 -0
- package/test-fixtures/interop/_contexts/openbadges-v3.jsonld +445 -0
- package/test-fixtures/interop/_contexts/security-bbs-v1.jsonld +93 -0
- package/test-fixtures/interop/ebsi/diploma-elm/README.md +29 -0
- package/test-fixtures/interop/ebsi/diploma-elm/expected.nq +70 -0
- package/test-fixtures/interop/ebsi/diploma-elm/frame.jsonld +24 -0
- package/test-fixtures/interop/ebsi/diploma-elm/input.jsonld +444 -0
- package/test-fixtures/interop/ebsi/diploma-simple/README.md +19 -0
- package/test-fixtures/interop/ebsi/diploma-simple/expected.nq +9 -0
- package/test-fixtures/interop/ebsi/diploma-simple/frame.jsonld +13 -0
- package/test-fixtures/interop/ebsi/diploma-simple/input.jsonld +24 -0
- package/test-fixtures/interop/idv2/full-disclosure/README.md +9 -0
- package/test-fixtures/interop/idv2/full-disclosure/expected.nq +59 -0
- package/test-fixtures/interop/idv2/full-disclosure/frame.jsonld +9 -0
- package/test-fixtures/interop/idv2/full-disclosure/input.jsonld +82 -0
- package/test-fixtures/interop/nuggets/identity-v1/README.md +17 -0
- package/test-fixtures/interop/nuggets/identity-v1/expected.nq +21 -0
- package/test-fixtures/interop/nuggets/identity-v1/frame.jsonld +17 -0
- package/test-fixtures/interop/nuggets/identity-v1/input.jsonld +31 -0
- package/test-fixtures/interop/nuggets/kyb-v1/README.md +15 -0
- package/test-fixtures/interop/nuggets/kyb-v1/expected.nq +18 -0
- package/test-fixtures/interop/nuggets/kyb-v1/frame.jsonld +24 -0
- package/test-fixtures/interop/nuggets/kyb-v1/input.jsonld +60 -0
- package/test-fixtures/interop/openbadges-v3/basic-achievement/README.md +17 -0
- package/test-fixtures/interop/openbadges-v3/basic-achievement/expected.nq +12 -0
- package/test-fixtures/interop/openbadges-v3/basic-achievement/frame.jsonld +17 -0
- package/test-fixtures/interop/openbadges-v3/basic-achievement/input.jsonld +25 -0
- package/test-fixtures/interop/openbadges-v3/with-allowed-values/README.md +11 -0
- package/test-fixtures/interop/openbadges-v3/with-allowed-values/expected.nq +25 -0
- package/test-fixtures/interop/openbadges-v3/with-allowed-values/frame.jsonld +22 -0
- package/test-fixtures/interop/openbadges-v3/with-allowed-values/input.jsonld +40 -0
- package/test-fixtures/interop/vp/single-vc-wrap/README.md +6 -0
- package/test-fixtures/interop/vp/single-vc-wrap/expected.nq +7 -0
- package/test-fixtures/interop/vp/single-vc-wrap/frame.jsonld +17 -0
- package/test-fixtures/interop/vp/single-vc-wrap/input.jsonld +27 -0
- package/test-fixtures/interop/w3c-vc-v1/permanent-resident-card/README.md +5 -0
- package/test-fixtures/interop/w3c-vc-v1/permanent-resident-card/expected.nq +13 -0
- package/test-fixtures/interop/w3c-vc-v1/permanent-resident-card/frame.jsonld +14 -0
- package/test-fixtures/interop/w3c-vc-v1/permanent-resident-card/input.jsonld +29 -0
- package/test_interop.mjs +184 -0
- package/test_interop_smoke.mjs +388 -0
- package/test_w3c_conformance.mjs +7 -0
- package/tools/regen_expected.mjs +108 -0
- package/w3c-baseline.json +63 -74
- package/w3c-denylist.json +6 -1
package/src/ld_signatures.rs
CHANGED
|
@@ -1,89 +1,79 @@
|
|
|
1
1
|
use std::collections::{BTreeMap, HashMap};
|
|
2
|
-
use std::num::NonZeroUsize;
|
|
3
2
|
use std::sync::Arc;
|
|
4
3
|
|
|
5
4
|
use base64::prelude::BASE64_STANDARD;
|
|
6
5
|
use base64::Engine;
|
|
7
|
-
use lru::LruCache;
|
|
8
6
|
use napi::bindgen_prelude::*;
|
|
9
7
|
use serde_json::Value;
|
|
10
8
|
|
|
11
9
|
use vc::{
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
bls_12381_g2_keypair::{types::KeyPairOptions, Bls12381G2KeyPair},
|
|
23
|
-
BbsBlsSignature2020 as BbsSignatureSuite, SignatureSuiteOptions,
|
|
24
|
-
},
|
|
25
|
-
AssertionProofPurpose,
|
|
26
|
-
},
|
|
10
|
+
bbs_signatures::bls12381::{
|
|
11
|
+
bls_blind_signature_commitment, bls_unblind_signature, bls_verify_blind_signature_proof,
|
|
12
|
+
},
|
|
13
|
+
did_resolver::resolver::{DidResolutionOptions, Resolver, ResolverRegistry},
|
|
14
|
+
document_loader::nuggets::{load_nuggets_context, DocumentLoader},
|
|
15
|
+
jsonld::signatures::{
|
|
16
|
+
self,
|
|
17
|
+
bbs::{
|
|
18
|
+
bls_12381_g2_keypair::{types::KeyPairOptions, Bls12381G2KeyPair},
|
|
19
|
+
BbsBlsSignature2020 as BbsSignatureSuite, SignatureSuiteOptions,
|
|
27
20
|
},
|
|
21
|
+
AssertionProofPurpose,
|
|
22
|
+
},
|
|
28
23
|
};
|
|
29
24
|
|
|
30
25
|
/// Create a DocumentLoader with nuggets contexts plus additional caller-provided contexts.
|
|
26
|
+
///
|
|
27
|
+
/// Extras are layered into the V1 context map AND retained separately so
|
|
28
|
+
/// the V2 dispatch chain (canonize_dispatch) can hand them to its own
|
|
29
|
+
/// build_loader. See clientffi#69 / PR #59.
|
|
31
30
|
pub(crate) fn create_document_loader(
|
|
32
|
-
|
|
33
|
-
) ->
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
31
|
+
additional_contexts: HashMap<String, Value>,
|
|
32
|
+
) -> Arc<DocumentLoader> {
|
|
33
|
+
let mut ctx = load_nuggets_context();
|
|
34
|
+
ctx.extend(additional_contexts.clone());
|
|
35
|
+
|
|
36
|
+
let resolver = Arc::new(tokio::sync::RwLock::new(Resolver::new(
|
|
37
|
+
ResolverRegistry::new(),
|
|
38
|
+
None,
|
|
39
|
+
)));
|
|
40
|
+
let opts = DidResolutionOptions::default();
|
|
41
|
+
Arc::new(DocumentLoader::with_additional_contexts(
|
|
42
|
+
ctx,
|
|
43
|
+
additional_contexts,
|
|
44
|
+
resolver,
|
|
45
|
+
opts,
|
|
46
|
+
))
|
|
48
47
|
}
|
|
49
48
|
|
|
50
49
|
/// Parse an optional nonce string: try base64 decode, fall back to raw bytes.
|
|
51
50
|
fn parse_nonce(options: &Value) -> Option<Vec<u8>> {
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
.decode(s)
|
|
58
|
-
.unwrap_or_else(|_| s.as_bytes().to_vec())
|
|
59
|
-
})
|
|
51
|
+
options.get("nonce").and_then(|v| v.as_str()).map(|s| {
|
|
52
|
+
BASE64_STANDARD
|
|
53
|
+
.decode(s)
|
|
54
|
+
.unwrap_or_else(|_| s.as_bytes().to_vec())
|
|
55
|
+
})
|
|
60
56
|
}
|
|
61
57
|
|
|
62
58
|
/// Parse the `contexts` field from options JSON into a HashMap.
|
|
63
59
|
pub(crate) fn parse_contexts(options: &Value) -> HashMap<String, Value> {
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
.map(|(k, v)| (k.clone(), v.clone()))
|
|
70
|
-
.collect()
|
|
71
|
-
})
|
|
72
|
-
.unwrap_or_default()
|
|
60
|
+
options
|
|
61
|
+
.get("contexts")
|
|
62
|
+
.and_then(|v| v.as_object())
|
|
63
|
+
.map(|obj| obj.iter().map(|(k, v)| (k.clone(), v.clone())).collect())
|
|
64
|
+
.unwrap_or_default()
|
|
73
65
|
}
|
|
74
66
|
|
|
75
|
-
/// Create a DocumentLoader
|
|
67
|
+
/// Create a DocumentLoader from an optional contexts Value.
|
|
76
68
|
///
|
|
77
69
|
/// This is the common pattern used by all class-based suite methods:
|
|
78
|
-
/// takes the `contexts` field from options and creates the loader
|
|
79
|
-
pub(crate) fn loader_from_contexts(
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
.unwrap_or_default();
|
|
86
|
-
create_document_loader(additional)
|
|
70
|
+
/// takes the `contexts` field from options and creates the loader.
|
|
71
|
+
pub(crate) fn loader_from_contexts(contexts: Option<&Value>) -> Arc<DocumentLoader> {
|
|
72
|
+
let additional = contexts
|
|
73
|
+
.and_then(|v| v.as_object())
|
|
74
|
+
.map(|obj| obj.iter().map(|(k, v)| (k.clone(), v.clone())).collect())
|
|
75
|
+
.unwrap_or_default();
|
|
76
|
+
create_document_loader(additional)
|
|
87
77
|
}
|
|
88
78
|
|
|
89
79
|
/// Sign a document with BbsBlsSignature2020 and embed the proof.
|
|
@@ -92,32 +82,32 @@ pub(crate) fn loader_from_contexts(
|
|
|
92
82
|
/// Output: signed document JSON with embedded proof
|
|
93
83
|
#[napi]
|
|
94
84
|
pub async fn ld_sign(options: Value) -> napi::Result<Value> {
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
85
|
+
let document = options
|
|
86
|
+
.get("document")
|
|
87
|
+
.cloned()
|
|
88
|
+
.ok_or_else(|| napi::Error::from_reason("missing 'document' in options"))?;
|
|
89
|
+
|
|
90
|
+
let key_pair_opts: KeyPairOptions =
|
|
91
|
+
serde_json::from_value(options.get("keyPair").cloned().unwrap_or(Value::Null))
|
|
92
|
+
.map_err(|e| napi::Error::from_reason(format!("failed to parse keyPair: {e}")))?;
|
|
93
|
+
|
|
94
|
+
let additional_contexts = parse_contexts(&options);
|
|
95
|
+
|
|
96
|
+
let key = Bls12381G2KeyPair::new(Some(key_pair_opts));
|
|
97
|
+
let loader = create_document_loader(additional_contexts);
|
|
98
|
+
|
|
99
|
+
let suite = BbsSignatureSuite::new(SignatureSuiteOptions {
|
|
100
|
+
key,
|
|
101
|
+
verification_method: None,
|
|
102
|
+
date: None,
|
|
103
|
+
canonize_options: None,
|
|
104
|
+
})
|
|
105
|
+
.await;
|
|
106
|
+
|
|
107
|
+
let purpose = AssertionProofPurpose::new();
|
|
108
|
+
signatures::sign(document, &suite, &purpose, loader)
|
|
109
|
+
.await
|
|
110
|
+
.map_err(|e| napi::Error::from_reason(format!("ld_sign failed: {e}")))
|
|
121
111
|
}
|
|
122
112
|
|
|
123
113
|
/// Verify a document with an embedded proof (auto-detects proof type).
|
|
@@ -126,23 +116,23 @@ pub async fn ld_sign(options: Value) -> napi::Result<Value> {
|
|
|
126
116
|
/// Output: `{ verified: boolean, error?: string }`
|
|
127
117
|
#[napi]
|
|
128
118
|
pub async fn ld_verify(options: Value) -> napi::Result<Value> {
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
119
|
+
let document = options
|
|
120
|
+
.get("document")
|
|
121
|
+
.cloned()
|
|
122
|
+
.ok_or_else(|| napi::Error::from_reason("missing 'document' in options"))?;
|
|
123
|
+
|
|
124
|
+
let additional_contexts = parse_contexts(&options);
|
|
125
|
+
|
|
126
|
+
let loader = create_document_loader(additional_contexts);
|
|
127
|
+
let purpose = AssertionProofPurpose::new();
|
|
128
|
+
let result = signatures::verify(&document, &purpose, loader)
|
|
129
|
+
.await
|
|
130
|
+
.map_err(|e| napi::Error::from_reason(format!("ld_verify failed: {e}")))?;
|
|
131
|
+
|
|
132
|
+
Ok(serde_json::json!({
|
|
133
|
+
"verified": result.verified,
|
|
134
|
+
"error": result.error,
|
|
135
|
+
}))
|
|
146
136
|
}
|
|
147
137
|
|
|
148
138
|
/// Derive a selective disclosure proof from a signed document.
|
|
@@ -151,24 +141,24 @@ pub async fn ld_verify(options: Value) -> napi::Result<Value> {
|
|
|
151
141
|
/// Output: derived document JSON with embedded proof
|
|
152
142
|
#[napi]
|
|
153
143
|
pub async fn ld_derive_proof(options: Value) -> napi::Result<Value> {
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
144
|
+
let document = options
|
|
145
|
+
.get("document")
|
|
146
|
+
.cloned()
|
|
147
|
+
.ok_or_else(|| napi::Error::from_reason("missing 'document' in options"))?;
|
|
158
148
|
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
149
|
+
let reveal_document = options
|
|
150
|
+
.get("revealDocument")
|
|
151
|
+
.cloned()
|
|
152
|
+
.ok_or_else(|| napi::Error::from_reason("missing 'revealDocument' in options"))?;
|
|
163
153
|
|
|
164
|
-
|
|
154
|
+
let nonce = parse_nonce(&options);
|
|
165
155
|
|
|
166
|
-
|
|
156
|
+
let additional_contexts = parse_contexts(&options);
|
|
167
157
|
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
158
|
+
let loader = create_document_loader(additional_contexts);
|
|
159
|
+
signatures::derive_proof(&document, &reveal_document, nonce, loader)
|
|
160
|
+
.await
|
|
161
|
+
.map_err(|e| napi::Error::from_reason(format!("ld_derive_proof failed: {e}")))
|
|
172
162
|
}
|
|
173
163
|
|
|
174
164
|
/// Standalone deriveProof function matching the JS reference's
|
|
@@ -178,18 +168,18 @@ pub async fn ld_derive_proof(options: Value) -> napi::Result<Value> {
|
|
|
178
168
|
/// Output: derived document JSON with embedded proof
|
|
179
169
|
#[napi]
|
|
180
170
|
pub async fn derive_proof(
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
171
|
+
proof_document: Value,
|
|
172
|
+
reveal_document: Value,
|
|
173
|
+
options: Value,
|
|
184
174
|
) -> napi::Result<Value> {
|
|
185
|
-
|
|
175
|
+
let nonce = parse_nonce(&options);
|
|
186
176
|
|
|
187
|
-
|
|
177
|
+
let additional_contexts = parse_contexts(&options);
|
|
188
178
|
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
179
|
+
let loader = create_document_loader(additional_contexts);
|
|
180
|
+
signatures::derive_proof(&proof_document, &reveal_document, nonce, loader)
|
|
181
|
+
.await
|
|
182
|
+
.map_err(|e| napi::Error::from_reason(format!("deriveProof failed: {e}")))
|
|
193
183
|
}
|
|
194
184
|
|
|
195
185
|
/// Create a blind signature commitment (holder side).
|
|
@@ -199,33 +189,32 @@ pub async fn derive_proof(
|
|
|
199
189
|
/// Output: `{ commitment, challengeHash, blindingFactor, proofOfHiddenMessages }` (all Uint8Array)
|
|
200
190
|
#[napi]
|
|
201
191
|
pub async fn create_commitment(
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
192
|
+
public_key: Uint8Array,
|
|
193
|
+
messages: Vec<Uint8Array>,
|
|
194
|
+
blinded: Vec<u32>,
|
|
195
|
+
nonce: Uint8Array,
|
|
196
|
+
known_message_count: u32,
|
|
207
197
|
) -> napi::Result<serde_json::Value> {
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
}))
|
|
198
|
+
let pk = public_key.to_vec();
|
|
199
|
+
let nonce_vec = nonce.to_vec();
|
|
200
|
+
let blinded_indices: Vec<usize> = blinded.iter().map(|&i| i as usize).collect();
|
|
201
|
+
let total_message_count = blinded_indices.len() + known_message_count as usize;
|
|
202
|
+
|
|
203
|
+
let mut msgs = BTreeMap::new();
|
|
204
|
+
for (idx, msg) in blinded_indices.iter().zip(messages.iter()) {
|
|
205
|
+
msgs.insert(*idx, msg.to_vec());
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
let result = bls_blind_signature_commitment(pk, msgs, nonce_vec, total_message_count)
|
|
209
|
+
.await
|
|
210
|
+
.map_err(|e| napi::Error::from_reason(format!("createCommitment failed: {e}")))?;
|
|
211
|
+
|
|
212
|
+
Ok(serde_json::json!({
|
|
213
|
+
"commitment": base64::prelude::BASE64_STANDARD.encode(&result.commitment),
|
|
214
|
+
"challengeHash": base64::prelude::BASE64_STANDARD.encode(&result.challenge_hash),
|
|
215
|
+
"blindingFactor": base64::prelude::BASE64_STANDARD.encode(&result.blinding_factor),
|
|
216
|
+
"proofOfHiddenMessages": base64::prelude::BASE64_STANDARD.encode(&result.proof_of_hidden_messages),
|
|
217
|
+
}))
|
|
229
218
|
}
|
|
230
219
|
|
|
231
220
|
/// Verify a blind signature commitment (issuer side).
|
|
@@ -233,29 +222,29 @@ pub async fn create_commitment(
|
|
|
233
222
|
/// Returns true if the commitment proof is valid.
|
|
234
223
|
#[napi]
|
|
235
224
|
pub async fn verify_commitment(
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
225
|
+
public_key: Uint8Array,
|
|
226
|
+
commitment: Uint8Array,
|
|
227
|
+
proof_of_hidden_messages: Uint8Array,
|
|
228
|
+
challenge_hash: Uint8Array,
|
|
229
|
+
blinded: Vec<u32>,
|
|
230
|
+
nonce: Uint8Array,
|
|
231
|
+
known_message_count: u32,
|
|
243
232
|
) -> napi::Result<bool> {
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
233
|
+
let pk = public_key.to_vec();
|
|
234
|
+
let blinded_indices: Vec<usize> = blinded.iter().map(|&i| i as usize).collect();
|
|
235
|
+
let total_message_count = blinded_indices.len() + known_message_count as usize;
|
|
236
|
+
|
|
237
|
+
bls_verify_blind_signature_proof(
|
|
238
|
+
pk,
|
|
239
|
+
commitment.to_vec(),
|
|
240
|
+
challenge_hash.to_vec(),
|
|
241
|
+
proof_of_hidden_messages.to_vec(),
|
|
242
|
+
blinded_indices,
|
|
243
|
+
nonce.to_vec(),
|
|
244
|
+
total_message_count,
|
|
245
|
+
)
|
|
246
|
+
.await
|
|
247
|
+
.map_err(|e| napi::Error::from_reason(format!("verifyCommitment failed: {e}")))
|
|
259
248
|
}
|
|
260
249
|
|
|
261
250
|
/// Unblind a blind signature (holder side).
|
|
@@ -264,14 +253,14 @@ pub async fn verify_commitment(
|
|
|
264
253
|
/// returns the unblinded standard signature.
|
|
265
254
|
#[napi]
|
|
266
255
|
pub async fn unblind_signature(
|
|
267
|
-
|
|
268
|
-
|
|
256
|
+
blind_signature: Uint8Array,
|
|
257
|
+
blinding_factor: Uint8Array,
|
|
269
258
|
) -> napi::Result<Uint8Array> {
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
259
|
+
let result = bls_unblind_signature(blind_signature.to_vec(), blinding_factor.to_vec())
|
|
260
|
+
.await
|
|
261
|
+
.map_err(|e| napi::Error::from_reason(format!("unblindSignature failed: {e}")))?;
|
|
273
262
|
|
|
274
|
-
|
|
263
|
+
Ok(Uint8Array::from(result))
|
|
275
264
|
}
|
|
276
265
|
|
|
277
266
|
/// Derive a selective disclosure proof from a holder-bound signed document.
|
|
@@ -281,49 +270,46 @@ pub async fn unblind_signature(
|
|
|
281
270
|
/// Output: derived document JSON with embedded proof
|
|
282
271
|
#[napi]
|
|
283
272
|
pub async fn derive_proof_holder_bound(
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
273
|
+
proof_document: Value,
|
|
274
|
+
reveal_document: Value,
|
|
275
|
+
options: Value,
|
|
287
276
|
) -> napi::Result<Value> {
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
)
|
|
327
|
-
.await
|
|
328
|
-
.map_err(|e| napi::Error::from_reason(format!("deriveProofHolderBound failed: {e}")))
|
|
277
|
+
let blinding_factor = options
|
|
278
|
+
.get("blindingFactor")
|
|
279
|
+
.and_then(|v| v.as_str())
|
|
280
|
+
.map(|s| {
|
|
281
|
+
base64::prelude::BASE64_STANDARD
|
|
282
|
+
.decode(s)
|
|
283
|
+
.map_err(|e| napi::Error::from_reason(format!("failed to decode blindingFactor: {e}")))
|
|
284
|
+
})
|
|
285
|
+
.transpose()?
|
|
286
|
+
.ok_or_else(|| napi::Error::from_reason("missing 'blindingFactor' in options"))?;
|
|
287
|
+
|
|
288
|
+
let blinded_messages: Vec<Vec<u8>> = options
|
|
289
|
+
.get("blindedMessages")
|
|
290
|
+
.and_then(|v| v.as_array())
|
|
291
|
+
.ok_or_else(|| napi::Error::from_reason("missing 'blindedMessages' in options"))?
|
|
292
|
+
.iter()
|
|
293
|
+
.map(|v| {
|
|
294
|
+
base64::prelude::BASE64_STANDARD
|
|
295
|
+
.decode(v.as_str().unwrap_or_default())
|
|
296
|
+
.map_err(|e| napi::Error::from_reason(format!("failed to decode blinded message: {e}")))
|
|
297
|
+
})
|
|
298
|
+
.collect::<napi::Result<_>>()?;
|
|
299
|
+
|
|
300
|
+
let nonce = parse_nonce(&options);
|
|
301
|
+
|
|
302
|
+
let additional_contexts = parse_contexts(&options);
|
|
303
|
+
|
|
304
|
+
let loader = create_document_loader(additional_contexts);
|
|
305
|
+
signatures::derive_proof_holder_bound(
|
|
306
|
+
&proof_document,
|
|
307
|
+
&reveal_document,
|
|
308
|
+
blinding_factor,
|
|
309
|
+
blinded_messages,
|
|
310
|
+
nonce,
|
|
311
|
+
loader,
|
|
312
|
+
)
|
|
313
|
+
.await
|
|
314
|
+
.map_err(|e| napi::Error::from_reason(format!("deriveProofHolderBound failed: {e}")))
|
|
329
315
|
}
|
package/src/lib.rs
CHANGED
|
@@ -3,6 +3,14 @@
|
|
|
3
3
|
#[macro_use]
|
|
4
4
|
extern crate napi_derive;
|
|
5
5
|
|
|
6
|
+
// mimalloc gives a sizeable perf win on darwin + linux-gnu (the dominant
|
|
7
|
+
// consumer platforms), but is not compatible with Alpine/musl at runtime
|
|
8
|
+
// (the static-link / TLS model differs). Fall back to the system allocator
|
|
9
|
+
// on musl so Alpine consumers don't regress at startup.
|
|
10
|
+
#[cfg(not(target_env = "musl"))]
|
|
11
|
+
#[global_allocator]
|
|
12
|
+
static GLOBAL: mimalloc::MiMalloc = mimalloc::MiMalloc;
|
|
13
|
+
|
|
6
14
|
pub mod bbs_2023;
|
|
7
15
|
pub mod bbs_ietf;
|
|
8
16
|
pub mod bls_signatures;
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
# Cross-stack BBS+ interop fixtures
|
|
2
|
+
|
|
3
|
+
Fixtures for `vc/js/test_interop.mjs` — the cross-stack interop harness comparing V2 native (Rust `json-ld@0.21.4`) against `jsonld.js@^8.3.3` (the de facto third-party reference). Each fixture is a per-VC-schema test case in the form `<family>/<name>/`.
|
|
4
|
+
|
|
5
|
+
## Layout
|
|
6
|
+
|
|
7
|
+
```
|
|
8
|
+
test-fixtures/interop/
|
|
9
|
+
├── _contexts/ # bundled JSON-LD contexts (see _contexts/README.md)
|
|
10
|
+
├── <family>/<name>/
|
|
11
|
+
│ ├── input.jsonld # the VC document
|
|
12
|
+
│ ├── frame.jsonld # BBS+-style reveal frame
|
|
13
|
+
│ ├── expected.nq # canonized N-Quads from jsonld.js (committed; ground truth)
|
|
14
|
+
│ └── README.md # provenance + what this fixture tests
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
`_<dir>` (underscore-prefixed) directories are excluded from fixture discovery.
|
|
18
|
+
|
|
19
|
+
## Workflow
|
|
20
|
+
|
|
21
|
+
**Adding a new fixture:**
|
|
22
|
+
|
|
23
|
+
1. Create `<family>/<name>/` with `input.jsonld`, `frame.jsonld`, and a brief `README.md` describing provenance.
|
|
24
|
+
2. If the input references a context not yet in `_contexts/`, add it (see `_contexts/README.md`).
|
|
25
|
+
3. Generate ground truth: `cd vc/js && node tools/regen_expected.mjs test-fixtures/interop/<family>/<name>`.
|
|
26
|
+
4. Run the harness: `node test_interop.mjs --check` — if `pass`, commit; if `fail`, see "When the harness fails" below.
|
|
27
|
+
|
|
28
|
+
**Regenerating after upstream context updates:** `node tools/regen_expected.mjs --all` from `vc/js/`. Re-run `--check` afterwards.
|
|
29
|
+
|
|
30
|
+
## When the harness fails
|
|
31
|
+
|
|
32
|
+
Three modes:
|
|
33
|
+
|
|
34
|
+
- **Regression** (a previously-passing fixture fails): a real V2 native bug. Investigate via `node test_interop.mjs --report` to see the unified diff. Fix in `vc/rs/src/jsonld_v2/frame_native.rs` (or wherever the divergence originates).
|
|
35
|
+
- **Expected fail** (fixture is in `interop-allowlist.json`): no action; the gap is tracked. The reason field + linked GitHub issue document next steps.
|
|
36
|
+
- **Improvement** (an allowlisted fixture starts passing): the harness exits 1 with a "newly passing — please ratchet" message. Remove the allowlist entry and close the linked GitHub issue.
|
|
37
|
+
|
|
38
|
+
The harness gates `vc-publish` via the `vc-interop-conformance` CI job, so a regression blocks release.
|
|
39
|
+
|
|
40
|
+
## Related files
|
|
41
|
+
|
|
42
|
+
- `vc/js/test_interop.mjs` — the harness (`--check`, `--report`)
|
|
43
|
+
- `vc/js/tools/regen_expected.mjs` — ground-truth generator (jsonld.js + URDNA2015, offline document loader)
|
|
44
|
+
- `vc/js/interop-allowlist.json` — `expect_fail` ratchet
|
|
45
|
+
- `vc/js/test-fixtures/interop/_contexts/README.md` — bundled context conventions + Rust sync targets
|
|
46
|
+
- `docs/plans/2026-05-05-sprint-4-phase-a.md` — Phase A implementation plan
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
# Interop fixture contexts
|
|
2
|
+
|
|
3
|
+
Bundled JSON-LD contexts for the cross-stack interop test harness (`vc/js/test_interop.mjs`) and its ground-truth generator (`vc/js/tools/regen_expected.mjs`).
|
|
4
|
+
|
|
5
|
+
## Convention
|
|
6
|
+
|
|
7
|
+
Each `<name>.jsonld` here is loaded by the harness's offline document loader and indexed by its embedded `__url__` marker. Shape:
|
|
8
|
+
|
|
9
|
+
```json
|
|
10
|
+
{
|
|
11
|
+
"__url__": "<the URL the input.jsonld references>",
|
|
12
|
+
"@context": <verbatim copy of the upstream context body>
|
|
13
|
+
}
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
The harness deletes `__url__` after reading and exposes the rest to JsonLd as a context document.
|
|
17
|
+
|
|
18
|
+
Files starting with `_` (this `README.md`, etc.) are not loaded — only `*.jsonld` and `*.json` siblings.
|
|
19
|
+
|
|
20
|
+
## Sync targets
|
|
21
|
+
|
|
22
|
+
Each bundle here is a hand-copy of a context body. Most mirror a constant in the Rust codebase ("Rust-mirror" bundles); a few mirror an external spec that Nuggets does not issue ("spec-sourced" bundles, e.g. OpenBadges v3 added in Sprint 4 Phase B for cross-stack interop coverage). **When the canonical source updates, this bundle must be updated in lockstep** — otherwise cross-stack interop silently drifts and the harness will start producing false confidence in jsonld.js parity. There is no automated drift check today; this README is the audit trail.
|
|
23
|
+
|
|
24
|
+
| bundle file | URL | Source |
|
|
25
|
+
|---|---|---|
|
|
26
|
+
| `credentials-v1.jsonld` | `https://www.w3.org/2018/credentials/v1` | Rust-mirror: `vc/rs/src/jsonld/signatures/bbs/data/sample_data.rs::CREDENTIALS_CONTEXT` |
|
|
27
|
+
| `citizenship-v1.jsonld` | `https://w3id.org/citizenship/v1` | Rust-mirror: `vc/rs/src/jsonld/signatures/bbs/data/sample_data.rs::CITIZEN_VOCAB` |
|
|
28
|
+
| `security-bbs-v1.jsonld` | `https://w3id.org/security/bbs/v1` | Rust-mirror: `vc/rs/src/jsonld/signatures/bbs/data/sample_data.rs::BBS` |
|
|
29
|
+
| `identity-v2.jsonld` | `https://schemas.nuggets.life/identityV2.json` | Rust-mirror: `vc/rs/src/document_loader/context/identity_v2.rs::IDENTITY_V2` |
|
|
30
|
+
| `bbs-bound-v1.jsonld` | `https://schemas.nuggets.life/bbsBoundv1.json` | Rust-mirror: `vc/rs/src/document_loader/context/bbs_bound_v1.rs::BBS_BOUND_V1` |
|
|
31
|
+
| `nuggets-identity-v1.jsonld` | `https://schemas.nuggets.life/identity.json` | Rust-mirror: `vc/rs/src/document_loader/context/identity.rs::IDENTITY` |
|
|
32
|
+
| `nuggets-kyb-v1.jsonld` | `https://schemas.nuggets.life/kybV1.json` | Rust-mirror: `vc/rs/src/document_loader/context/kyb.rs::KYB_V1` |
|
|
33
|
+
| `openbadges-v3.jsonld` | `https://purl.imsglobal.org/spec/ob/v3p0/context-3.0.3.json` | Spec-sourced (no Rust mirror): IMS Global OpenBadges v3.0.3 context, retrieved 2026-05-06 from the canonical PURL above. The `https://w3id.org/openbadges/v3` alias currently 302s to a 404 (`https://openbadgespec.org/v3`), so OB v3 fixtures cite the IMS PURL directly. |
|
|
34
|
+
| `essif-schemas-vc-2020-v1.jsonld` | `https://essif.europa.eu/schemas/vc/2020/v1` | Spec-sourced (no Rust mirror): EBSI/ESSIF schemas v1 context as distributed by the EBSI4Austria pilot at `https://github.com/danubetech/ebsi4austria-examples/blob/main/context/essif-schemas-vc-2020-v1.jsonld`, retrieved 2026-05-06. Used by `ebsi/diploma-simple`. |
|
|
35
|
+
| `elm-edc-ap.jsonld` | `http://data.europa.eu/snb/model/context/edc-ap` | Spec-sourced (no Rust mirror): European Learning Model v3 EDC-AP context, retrieved 2026-05-06 from `https://github.com/european-commission-empl/European-Learning-Model/blob/master/rdf/ap/edc/edc-ap-context.jsonld`. The canonical `data.europa.eu` URL currently sits behind an Azure WAF JS challenge (cannot be fetched cleanly with curl/WebFetch); the European Commission's official ELM repo is the verbatim source. Used by `ebsi/diploma-elm`. |
|
|
36
|
+
|
|
37
|
+
### Bundle classes
|
|
38
|
+
|
|
39
|
+
- **Rust-mirror** — the upstream-of-record is a Rust constant in this repo. The bundle is a verbatim copy and must be re-synced when the Rust source changes.
|
|
40
|
+
- **Spec-sourced** — the upstream-of-record is an external spec (e.g. OpenBadges v3). Nuggets does not issue these credentials; the bundle exists purely to feed cross-stack interop fixtures. Re-sync when the spec publishes a new revision.
|
|
41
|
+
|
|
42
|
+
## Adding a new context
|
|
43
|
+
|
|
44
|
+
When a new fixture references a context not yet bundled here:
|
|
45
|
+
|
|
46
|
+
1. **Rust-mirror case** — find the canonical body in the Rust codebase (search `vc/rs/src/document_loader/` and `vc/rs/src/jsonld/signatures/bbs/data/`). **Don't network-fetch.**
|
|
47
|
+
**Spec-sourced case** — fetch the canonical context from the spec's PURL/URL with `curl -sL` and save the body verbatim.
|
|
48
|
+
2. Create `_contexts/<short-name>.jsonld` with `{ "__url__": "<URL>", "@context": <body> }`.
|
|
49
|
+
3. Verify the body is byte-equal to the source (Rust constant or fetched spec body).
|
|
50
|
+
4. Add a row to the table above naming the source (Rust path + constant for mirrors; spec name + retrieval date for spec-sourced).
|
|
51
|
+
5. Re-run `node tools/regen_expected.mjs --all` so all `expected.nq` files account for the new context.
|