@nuggetslife/vc 0.0.23 → 0.0.24
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/generate_golden_files.mjs +181 -0
- package/index.d.ts +40 -0
- package/index.js +12 -1
- package/package.json +7 -7
- package/src/bbs_2023.rs +71 -0
- package/src/bbs_ietf.rs +255 -0
- package/src/lib.rs +3 -0
- package/src/sd_jwt.rs +133 -0
- package/test-data/golden/inputDocument-minimal.json +17 -0
- package/test-data/golden/inputDocument-rich.json +29 -0
- package/test-data/golden/mattrglobal-derived-prc.json +36 -0
- package/test-data/golden/mattrglobal-signed-minimal.json +30 -0
- package/test-data/golden/mattrglobal-signed-prc.json +42 -0
- package/test-data/golden/mattrglobal-signed-rich.json +42 -0
- package/test_backward_compat.mjs +287 -0
- package/test_bbs_2023.mjs +195 -0
- package/test_bbs_ietf.mjs +168 -0
- package/test_sd_jwt.mjs +197 -0
package/src/sd_jwt.rs
ADDED
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
use serde_json::Value;
|
|
2
|
+
|
|
3
|
+
use vc::sd_jwt::holder::sd_jwt_present;
|
|
4
|
+
use vc::sd_jwt::issuer::sd_jwt_issue;
|
|
5
|
+
use vc::sd_jwt::verifier::sd_jwt_verify;
|
|
6
|
+
|
|
7
|
+
// ---------------------------------------------------------------------------
|
|
8
|
+
// Result types
|
|
9
|
+
// ---------------------------------------------------------------------------
|
|
10
|
+
|
|
11
|
+
#[napi(object)]
|
|
12
|
+
pub struct SdJwtDisclosure {
|
|
13
|
+
pub salt: String,
|
|
14
|
+
pub claim_name: String,
|
|
15
|
+
pub claim_value: Value,
|
|
16
|
+
pub encoded: String,
|
|
17
|
+
pub digest: String,
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
#[napi(object)]
|
|
21
|
+
pub struct SdJwtIssueOutput {
|
|
22
|
+
pub sd_jwt: String,
|
|
23
|
+
pub disclosures: Vec<SdJwtDisclosure>,
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
#[napi(object)]
|
|
27
|
+
pub struct SdJwtPresentOutput {
|
|
28
|
+
pub presentation: String,
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
#[napi(object)]
|
|
32
|
+
pub struct SdJwtVerifyOutput {
|
|
33
|
+
pub verified: bool,
|
|
34
|
+
pub claims: Value,
|
|
35
|
+
pub error: Option<String>,
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
// ---------------------------------------------------------------------------
|
|
39
|
+
// SD-JWT Issue
|
|
40
|
+
// ---------------------------------------------------------------------------
|
|
41
|
+
|
|
42
|
+
#[napi(js_name = "sdJwtIssue")]
|
|
43
|
+
pub fn sd_jwt_issue_napi(
|
|
44
|
+
claims: Value,
|
|
45
|
+
disclosable: Vec<String>,
|
|
46
|
+
jwk: Value,
|
|
47
|
+
alg: String,
|
|
48
|
+
holder_jwk: Option<Value>,
|
|
49
|
+
decoy_count: Option<u32>,
|
|
50
|
+
) -> napi::Result<SdJwtIssueOutput> {
|
|
51
|
+
let jwk_str = serde_json::to_string(&jwk)
|
|
52
|
+
.map_err(|e| napi::Error::from_reason(format!("sdJwtIssue serialize jwk: {e}")))?;
|
|
53
|
+
|
|
54
|
+
let result = sd_jwt_issue(
|
|
55
|
+
claims,
|
|
56
|
+
disclosable,
|
|
57
|
+
jwk_str,
|
|
58
|
+
alg,
|
|
59
|
+
holder_jwk,
|
|
60
|
+
decoy_count.unwrap_or(0) as usize,
|
|
61
|
+
)
|
|
62
|
+
.map_err(|e| napi::Error::from_reason(format!("sdJwtIssue failed: {e}")))?;
|
|
63
|
+
|
|
64
|
+
Ok(SdJwtIssueOutput {
|
|
65
|
+
sd_jwt: result.sd_jwt,
|
|
66
|
+
disclosures: result
|
|
67
|
+
.disclosures
|
|
68
|
+
.into_iter()
|
|
69
|
+
.map(|d| SdJwtDisclosure {
|
|
70
|
+
salt: d.salt,
|
|
71
|
+
claim_name: d.claim_name,
|
|
72
|
+
claim_value: d.claim_value,
|
|
73
|
+
encoded: d.encoded,
|
|
74
|
+
digest: d.digest,
|
|
75
|
+
})
|
|
76
|
+
.collect(),
|
|
77
|
+
})
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// ---------------------------------------------------------------------------
|
|
81
|
+
// SD-JWT Present
|
|
82
|
+
// ---------------------------------------------------------------------------
|
|
83
|
+
|
|
84
|
+
#[napi(js_name = "sdJwtPresent")]
|
|
85
|
+
pub fn sd_jwt_present_napi(
|
|
86
|
+
sd_jwt: String,
|
|
87
|
+
disclosures_to_reveal: Vec<String>,
|
|
88
|
+
kb_jwk: Option<Value>,
|
|
89
|
+
kb_alg: Option<String>,
|
|
90
|
+
audience: Option<String>,
|
|
91
|
+
nonce: Option<String>,
|
|
92
|
+
) -> napi::Result<SdJwtPresentOutput> {
|
|
93
|
+
let kb_jwk_str = kb_jwk
|
|
94
|
+
.map(|v| serde_json::to_string(&v))
|
|
95
|
+
.transpose()
|
|
96
|
+
.map_err(|e| napi::Error::from_reason(format!("sdJwtPresent serialize kbJwk: {e}")))?;
|
|
97
|
+
|
|
98
|
+
let result = sd_jwt_present(sd_jwt, disclosures_to_reveal, kb_jwk_str, kb_alg, audience, nonce)
|
|
99
|
+
.map_err(|e| napi::Error::from_reason(format!("sdJwtPresent failed: {e}")))?;
|
|
100
|
+
|
|
101
|
+
Ok(SdJwtPresentOutput {
|
|
102
|
+
presentation: result.presentation,
|
|
103
|
+
})
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
// ---------------------------------------------------------------------------
|
|
107
|
+
// SD-JWT Verify
|
|
108
|
+
// ---------------------------------------------------------------------------
|
|
109
|
+
|
|
110
|
+
#[napi(js_name = "sdJwtVerify")]
|
|
111
|
+
pub fn sd_jwt_verify_napi(
|
|
112
|
+
presentation: String,
|
|
113
|
+
issuer_jwk: Value,
|
|
114
|
+
holder_jwk: Option<Value>,
|
|
115
|
+
audience: Option<String>,
|
|
116
|
+
nonce: Option<String>,
|
|
117
|
+
) -> napi::Result<SdJwtVerifyOutput> {
|
|
118
|
+
let issuer_jwk_str = serde_json::to_string(&issuer_jwk)
|
|
119
|
+
.map_err(|e| napi::Error::from_reason(format!("sdJwtVerify serialize issuerJwk: {e}")))?;
|
|
120
|
+
let holder_jwk_str = holder_jwk
|
|
121
|
+
.map(|v| serde_json::to_string(&v))
|
|
122
|
+
.transpose()
|
|
123
|
+
.map_err(|e| napi::Error::from_reason(format!("sdJwtVerify serialize holderJwk: {e}")))?;
|
|
124
|
+
|
|
125
|
+
let result = sd_jwt_verify(presentation, issuer_jwk_str, holder_jwk_str, audience, nonce)
|
|
126
|
+
.map_err(|e| napi::Error::from_reason(format!("sdJwtVerify failed: {e}")))?;
|
|
127
|
+
|
|
128
|
+
Ok(SdJwtVerifyOutput {
|
|
129
|
+
verified: result.verified,
|
|
130
|
+
claims: result.claims,
|
|
131
|
+
error: result.error,
|
|
132
|
+
})
|
|
133
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
{
|
|
2
|
+
"@context": [
|
|
3
|
+
"https://www.w3.org/2018/credentials/v1",
|
|
4
|
+
"https://w3id.org/citizenship/v1",
|
|
5
|
+
"https://w3id.org/security/bbs/v1"
|
|
6
|
+
],
|
|
7
|
+
"id": "https://issuer.example.com/credentials/minimal-001",
|
|
8
|
+
"type": ["VerifiableCredential", "PermanentResidentCard"],
|
|
9
|
+
"issuer": "did:example:489398593",
|
|
10
|
+
"issuanceDate": "2024-01-15T10:00:00Z",
|
|
11
|
+
"credentialSubject": {
|
|
12
|
+
"id": "did:example:user123",
|
|
13
|
+
"type": ["PermanentResident", "Person"],
|
|
14
|
+
"givenName": "Alice",
|
|
15
|
+
"familyName": "Johnson"
|
|
16
|
+
}
|
|
17
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
{
|
|
2
|
+
"@context": [
|
|
3
|
+
"https://www.w3.org/2018/credentials/v1",
|
|
4
|
+
"https://w3id.org/citizenship/v1",
|
|
5
|
+
"https://w3id.org/security/bbs/v1"
|
|
6
|
+
],
|
|
7
|
+
"id": "https://issuer.example.com/credentials/rich-001",
|
|
8
|
+
"type": ["VerifiableCredential", "PermanentResidentCard"],
|
|
9
|
+
"issuer": "did:example:489398593",
|
|
10
|
+
"identifier": "RICH-001",
|
|
11
|
+
"name": "Extended Resident Card",
|
|
12
|
+
"description": "Credential with many fields to test BBS+ signatures over larger statement counts.",
|
|
13
|
+
"issuanceDate": "2023-06-20T08:30:00Z",
|
|
14
|
+
"expirationDate": "2033-06-20T08:30:00Z",
|
|
15
|
+
"credentialSubject": {
|
|
16
|
+
"id": "did:example:user456",
|
|
17
|
+
"type": ["PermanentResident", "Person"],
|
|
18
|
+
"givenName": "Robert",
|
|
19
|
+
"familyName": "Williams",
|
|
20
|
+
"gender": "Male",
|
|
21
|
+
"image": "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNk+M9QDwADhgGAWjR9awAAAABJRU5ErkJggg==",
|
|
22
|
+
"residentSince": "2018-03-15",
|
|
23
|
+
"lprCategory": "C12",
|
|
24
|
+
"lprNumber": "888-777-666",
|
|
25
|
+
"commuterClassification": "C2",
|
|
26
|
+
"birthCountry": "Canada",
|
|
27
|
+
"birthDate": "1985-11-23"
|
|
28
|
+
}
|
|
29
|
+
}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
{
|
|
2
|
+
"@context": [
|
|
3
|
+
"https://www.w3.org/2018/credentials/v1",
|
|
4
|
+
"https://w3id.org/citizenship/v1",
|
|
5
|
+
"https://w3id.org/security/bbs/v1"
|
|
6
|
+
],
|
|
7
|
+
"id": "https://issuer.oidp.uscis.gov/credentials/83627465",
|
|
8
|
+
"type": [
|
|
9
|
+
"PermanentResidentCard",
|
|
10
|
+
"VerifiableCredential"
|
|
11
|
+
],
|
|
12
|
+
"description": "Government of Example Permanent Resident Card.",
|
|
13
|
+
"identifier": "83627465",
|
|
14
|
+
"name": "Permanent Resident Card",
|
|
15
|
+
"credentialSubject": {
|
|
16
|
+
"id": "did:example:b34ca6cd37bbf23",
|
|
17
|
+
"type": [
|
|
18
|
+
"Person",
|
|
19
|
+
"PermanentResident"
|
|
20
|
+
],
|
|
21
|
+
"familyName": "SMITH",
|
|
22
|
+
"gender": "Male",
|
|
23
|
+
"givenName": "JOHN"
|
|
24
|
+
},
|
|
25
|
+
"expirationDate": "2029-12-03T12:19:52Z",
|
|
26
|
+
"issuanceDate": "2019-12-03T12:19:52Z",
|
|
27
|
+
"issuer": "did:example:489398593",
|
|
28
|
+
"proof": {
|
|
29
|
+
"type": "BbsBlsSignatureProof2020",
|
|
30
|
+
"created": "2026-02-24T18:31:50Z",
|
|
31
|
+
"nonce": "Mb2PKdz8oLnBh2DaIYDN2qp0B/BzqlZ0bomtWLnPkCuUX+LOLozA67gzRX8IFW+prJ4=",
|
|
32
|
+
"proofPurpose": "assertionMethod",
|
|
33
|
+
"proofValue": "ABkB/wbvlXznxDJB6mUBWnQAVCFn1BOG3oGhhrmH4CkfgHwIko6cE2ib8BSnHoIgsvxJbf8ql85AoX28XZNkogIRP9k1rvTS0TN1OBMx20q0+4FQX/jhGs7tVgXoeDJsTIe/kFtOhpoixTV0hNpMpwR3/zF+npQFipsk3Awc0v0Ne/Xc5ExLytwpS+UdtYnCBZWbgTAfAAAAdK8ARGBjzOUY0MsGvW37vMsegFaUHN8iu+vjk/RSzcb45sCwzA3I/dZIfonaPMKmBQAAAAIdxKYEnrgvyA3UPoQR6yAx9mBquPkFq8y9EnMloOj6eVi3eLIrFfaChASa2INbf43O09CGGXcC7ocGmG6GEg8GrgHAIPHm7ecOwUFEhJT8CC5j7t2z4MFrltfk1xW+PD+gsZ4WSjuTuqhHOBJNWXMcAAAACT/T2kIOroFI9tWJrprwQ+NK9W59cgn7V6UV/ynxlhBnDvFcCd7hYv5yDUuQuz9AiSxEveiMViWkHTBDFZ/Z/LhKYHOGGCKG9CJ9OVstwzzfe1jpfTYy4ZnLuuoJCSvcOm72DGumCHr2JVolSYEv5WKxXbN4MY46zL7hLBY6p88zXi7wzo8Odj+jMcptD0vLsKv0FjdSRK4XVXW2FEIy4IASrRFPuZo0Pb30wG3MzFnq7c17sSLUz/2EA929J2YxfmzulFTvsJm4LfwCJ7VA0iY6Suq1qBVy+fJnFMlqDwGoBl4izvhq9J9jBFVysZBdKlK4rwJtj0WqY75lnCPT/30ap7eC0YNnULtSHn3aYNF2JlO8wyu8zDODuXvRF2i5Kw==",
|
|
34
|
+
"verificationMethod": "did:example:489398593#test"
|
|
35
|
+
}
|
|
36
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
{
|
|
2
|
+
"@context": [
|
|
3
|
+
"https://www.w3.org/2018/credentials/v1",
|
|
4
|
+
"https://w3id.org/citizenship/v1",
|
|
5
|
+
"https://w3id.org/security/bbs/v1"
|
|
6
|
+
],
|
|
7
|
+
"id": "https://issuer.example.com/credentials/minimal-001",
|
|
8
|
+
"type": [
|
|
9
|
+
"VerifiableCredential",
|
|
10
|
+
"PermanentResidentCard"
|
|
11
|
+
],
|
|
12
|
+
"issuer": "did:example:489398593",
|
|
13
|
+
"issuanceDate": "2024-01-15T10:00:00Z",
|
|
14
|
+
"credentialSubject": {
|
|
15
|
+
"id": "did:example:user123",
|
|
16
|
+
"type": [
|
|
17
|
+
"PermanentResident",
|
|
18
|
+
"Person"
|
|
19
|
+
],
|
|
20
|
+
"givenName": "Alice",
|
|
21
|
+
"familyName": "Johnson"
|
|
22
|
+
},
|
|
23
|
+
"proof": {
|
|
24
|
+
"type": "BbsBlsSignature2020",
|
|
25
|
+
"created": "2026-02-24T18:31:50Z",
|
|
26
|
+
"proofPurpose": "assertionMethod",
|
|
27
|
+
"proofValue": "h9O11RGxkmaZQhOZQQRkuy0mGJ+1krOlSO9UMAc1zQf8O7wv2Fi3Ev8UMZxmX2Q4MKVg9X44OT7IyqxonIH7xLuNJjBkG7KYma9urLMBCo4x5JoTWPaG1p7URXapIpy1ng+avITVXJin9XQoxPxyNA==",
|
|
28
|
+
"verificationMethod": "did:example:489398593#test"
|
|
29
|
+
}
|
|
30
|
+
}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
{
|
|
2
|
+
"@context": [
|
|
3
|
+
"https://www.w3.org/2018/credentials/v1",
|
|
4
|
+
"https://w3id.org/citizenship/v1",
|
|
5
|
+
"https://w3id.org/security/bbs/v1"
|
|
6
|
+
],
|
|
7
|
+
"id": "https://issuer.oidp.uscis.gov/credentials/83627465",
|
|
8
|
+
"type": [
|
|
9
|
+
"VerifiableCredential",
|
|
10
|
+
"PermanentResidentCard"
|
|
11
|
+
],
|
|
12
|
+
"issuer": "did:example:489398593",
|
|
13
|
+
"identifier": "83627465",
|
|
14
|
+
"name": "Permanent Resident Card",
|
|
15
|
+
"description": "Government of Example Permanent Resident Card.",
|
|
16
|
+
"issuanceDate": "2019-12-03T12:19:52Z",
|
|
17
|
+
"expirationDate": "2029-12-03T12:19:52Z",
|
|
18
|
+
"credentialSubject": {
|
|
19
|
+
"id": "did:example:b34ca6cd37bbf23",
|
|
20
|
+
"type": [
|
|
21
|
+
"PermanentResident",
|
|
22
|
+
"Person"
|
|
23
|
+
],
|
|
24
|
+
"givenName": "JOHN",
|
|
25
|
+
"familyName": "SMITH",
|
|
26
|
+
"gender": "Male",
|
|
27
|
+
"image": "data:image/png;base64,iVBORw0KGgokJggg==",
|
|
28
|
+
"residentSince": "2015-01-01",
|
|
29
|
+
"lprCategory": "C09",
|
|
30
|
+
"lprNumber": "999-999-999",
|
|
31
|
+
"commuterClassification": "C1",
|
|
32
|
+
"birthCountry": "Bahamas",
|
|
33
|
+
"birthDate": "1958-07-17"
|
|
34
|
+
},
|
|
35
|
+
"proof": {
|
|
36
|
+
"type": "BbsBlsSignature2020",
|
|
37
|
+
"created": "2026-02-24T18:31:50Z",
|
|
38
|
+
"proofPurpose": "assertionMethod",
|
|
39
|
+
"proofValue": "lvx5LJssQ6BVqMX3931+8EApBMmkYwydWl7xUeNBMnVNZSamwHmS5WLlqicauVofYUHlKfzccE4m7waZyoLEkBLFiK2g54Q2i+CdtYBgDdkUDsoULSBMcH1MwGHwdjfXpldFNFrHFx/IAvLVniyeMQ==",
|
|
40
|
+
"verificationMethod": "did:example:489398593#test"
|
|
41
|
+
}
|
|
42
|
+
}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
{
|
|
2
|
+
"@context": [
|
|
3
|
+
"https://www.w3.org/2018/credentials/v1",
|
|
4
|
+
"https://w3id.org/citizenship/v1",
|
|
5
|
+
"https://w3id.org/security/bbs/v1"
|
|
6
|
+
],
|
|
7
|
+
"id": "https://issuer.example.com/credentials/rich-001",
|
|
8
|
+
"type": [
|
|
9
|
+
"VerifiableCredential",
|
|
10
|
+
"PermanentResidentCard"
|
|
11
|
+
],
|
|
12
|
+
"issuer": "did:example:489398593",
|
|
13
|
+
"identifier": "RICH-001",
|
|
14
|
+
"name": "Extended Resident Card",
|
|
15
|
+
"description": "Credential with many fields to test BBS+ signatures over larger statement counts.",
|
|
16
|
+
"issuanceDate": "2023-06-20T08:30:00Z",
|
|
17
|
+
"expirationDate": "2033-06-20T08:30:00Z",
|
|
18
|
+
"credentialSubject": {
|
|
19
|
+
"id": "did:example:user456",
|
|
20
|
+
"type": [
|
|
21
|
+
"PermanentResident",
|
|
22
|
+
"Person"
|
|
23
|
+
],
|
|
24
|
+
"givenName": "Robert",
|
|
25
|
+
"familyName": "Williams",
|
|
26
|
+
"gender": "Male",
|
|
27
|
+
"image": "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNk+M9QDwADhgGAWjR9awAAAABJRU5ErkJggg==",
|
|
28
|
+
"residentSince": "2018-03-15",
|
|
29
|
+
"lprCategory": "C12",
|
|
30
|
+
"lprNumber": "888-777-666",
|
|
31
|
+
"commuterClassification": "C2",
|
|
32
|
+
"birthCountry": "Canada",
|
|
33
|
+
"birthDate": "1985-11-23"
|
|
34
|
+
},
|
|
35
|
+
"proof": {
|
|
36
|
+
"type": "BbsBlsSignature2020",
|
|
37
|
+
"created": "2026-02-24T18:31:50Z",
|
|
38
|
+
"proofPurpose": "assertionMethod",
|
|
39
|
+
"proofValue": "h9Ax+SZHQsWCv0rXIFglLNAIJSNXF8yp7+MC+tlZsSzp2CWFPhRv8/dkgLTt9DRdBZv2+0Ch0WCIYewp/jE2bGDy4XALHFGj8hM5lW5hB4kio0Kglkol4OlKw+eZ8ujstHAB9XhFu7/XwAcKOB02TQ==",
|
|
40
|
+
"verificationMethod": "did:example:489398593#test"
|
|
41
|
+
}
|
|
42
|
+
}
|
|
@@ -0,0 +1,287 @@
|
|
|
1
|
+
// Backward compatibility tests: verify that VCs signed by the old @mattrglobal
|
|
2
|
+
// library still verify correctly with the new NAPI implementation.
|
|
3
|
+
//
|
|
4
|
+
// These tests use static "golden file" fixtures generated by
|
|
5
|
+
// generate_golden_files.mjs using @mattrglobal/jsonld-signatures-bbs.
|
|
6
|
+
// The golden files represent VCs that may already exist in user wallets.
|
|
7
|
+
//
|
|
8
|
+
// Key findings:
|
|
9
|
+
// - BbsBlsSignature2020 (original signed VCs) are fully cross-compatible
|
|
10
|
+
// - BbsBlsSignatureProof2020 (derived proofs) are NOT cross-compatible between
|
|
11
|
+
// implementations, but this is acceptable because derived proofs are generated
|
|
12
|
+
// on-the-fly for presentations and never stored in wallets. The critical path
|
|
13
|
+
// is: load wallet VC → derive new proof with current implementation → verify.
|
|
14
|
+
//
|
|
15
|
+
// Run: cd vc/js && node test_backward_compat.mjs
|
|
16
|
+
|
|
17
|
+
import test from 'node:test';
|
|
18
|
+
import assert from 'node:assert';
|
|
19
|
+
import { readFileSync } from 'node:fs';
|
|
20
|
+
import { resolve, dirname } from 'node:path';
|
|
21
|
+
import { fileURLToPath } from 'node:url';
|
|
22
|
+
import { createRequire } from 'node:module';
|
|
23
|
+
|
|
24
|
+
// NAPI binding (new implementation)
|
|
25
|
+
import { ldVerify, ldDeriveProof } from './index.js';
|
|
26
|
+
|
|
27
|
+
// @mattrglobal library (old implementation) — for generation verification
|
|
28
|
+
const require = createRequire(import.meta.url);
|
|
29
|
+
const { BbsBlsSignature2020 } = require('@mattrglobal/jsonld-signatures-bbs');
|
|
30
|
+
const { verify: mattrVerify, purposes, extendContextLoader } = require('jsonld-signatures');
|
|
31
|
+
|
|
32
|
+
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
33
|
+
const dataDir = resolve(__dirname, 'test-data');
|
|
34
|
+
const goldenDir = resolve(dataDir, 'golden');
|
|
35
|
+
|
|
36
|
+
const loadJson = (name) => JSON.parse(readFileSync(resolve(dataDir, name), 'utf8'));
|
|
37
|
+
const loadGolden = (name) => JSON.parse(readFileSync(resolve(goldenDir, name), 'utf8'));
|
|
38
|
+
|
|
39
|
+
// Load context files
|
|
40
|
+
const keyPair = loadJson('keyPair.json');
|
|
41
|
+
const controllerDocument = loadJson('controllerDocument.json');
|
|
42
|
+
const citizenVocab = loadJson('citizenVocab.json');
|
|
43
|
+
const bbsContext = loadJson('bbs.json');
|
|
44
|
+
const credentialsContext = loadJson('credentialsContext.json');
|
|
45
|
+
const suiteContext = loadJson('suiteContext.json');
|
|
46
|
+
const revealDocument = loadJson('deriveProofFrame.json');
|
|
47
|
+
|
|
48
|
+
// NAPI contexts map
|
|
49
|
+
const napiContexts = {
|
|
50
|
+
"did:example:489398593#test": keyPair,
|
|
51
|
+
"did:example:489398593": controllerDocument,
|
|
52
|
+
"https://w3id.org/citizenship/v1": citizenVocab,
|
|
53
|
+
"https://w3id.org/security/bbs/v1": bbsContext,
|
|
54
|
+
"https://www.w3.org/2018/credentials/v1": credentialsContext,
|
|
55
|
+
"https://w3id.org/security/suites/jws-2020/v1": suiteContext,
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
// @mattrglobal document loader
|
|
59
|
+
const customDocLoader = (url) => {
|
|
60
|
+
const context = napiContexts[url];
|
|
61
|
+
if (context) return { contextUrl: null, document: context, documentUrl: url };
|
|
62
|
+
throw new Error(`Attempted to remote load context: '${url}'`);
|
|
63
|
+
};
|
|
64
|
+
const documentLoader = extendContextLoader(customDocLoader);
|
|
65
|
+
|
|
66
|
+
// Load golden files (signed by @mattrglobal)
|
|
67
|
+
const signedPRC = loadGolden('mattrglobal-signed-prc.json');
|
|
68
|
+
const signedMinimal = loadGolden('mattrglobal-signed-minimal.json');
|
|
69
|
+
const signedRich = loadGolden('mattrglobal-signed-rich.json');
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
//
|
|
73
|
+
// Wallet VC verification — the critical path
|
|
74
|
+
// VCs in user wallets have BbsBlsSignature2020 proofs signed by the old library.
|
|
75
|
+
// The new NAPI implementation must verify these without re-signing.
|
|
76
|
+
//
|
|
77
|
+
|
|
78
|
+
test('wallet-compat: NAPI ldVerify accepts @mattrglobal-signed VC', async () => {
|
|
79
|
+
const result = await ldVerify({
|
|
80
|
+
document: signedPRC,
|
|
81
|
+
contexts: napiContexts,
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
assert.equal(result.verified, true,
|
|
85
|
+
`NAPI ldVerify failed on @mattrglobal-signed VC: ${result.error || 'unknown error'}`);
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
test('wallet-compat: @mattrglobal verifies its own golden-file VC', async () => {
|
|
89
|
+
const result = await mattrVerify(signedPRC, {
|
|
90
|
+
suite: new BbsBlsSignature2020(),
|
|
91
|
+
purpose: new purposes.AssertionProofPurpose(),
|
|
92
|
+
documentLoader,
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
assert.equal(result.verified, true,
|
|
96
|
+
`@mattrglobal verify failed on its own golden-file VC: ${JSON.stringify(result.error)}`);
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
//
|
|
101
|
+
// Selective disclosure — the real-world flow for wallet VCs:
|
|
102
|
+
// 1. Load signed VC from wallet (signed by old library)
|
|
103
|
+
// 2. Derive a proof using the current (NAPI) implementation
|
|
104
|
+
// 3. Verify the derived proof using the current (NAPI) implementation
|
|
105
|
+
//
|
|
106
|
+
|
|
107
|
+
test('wallet-compat: NAPI ldDeriveProof works on @mattrglobal-signed VC', async () => {
|
|
108
|
+
const derived = await ldDeriveProof({
|
|
109
|
+
document: signedPRC,
|
|
110
|
+
revealDocument,
|
|
111
|
+
contexts: napiContexts,
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
assert.ok(derived, 'ldDeriveProof returned a result');
|
|
115
|
+
assert.ok(derived.proof, 'derived document has a proof');
|
|
116
|
+
assert.equal(derived.proof.type, 'BbsBlsSignatureProof2020');
|
|
117
|
+
|
|
118
|
+
// Verify selective disclosure: only requested fields present
|
|
119
|
+
assert.equal(derived.credentialSubject.givenName, 'JOHN');
|
|
120
|
+
assert.equal(derived.credentialSubject.familyName, 'SMITH');
|
|
121
|
+
assert.equal(derived.credentialSubject.gender, 'Male');
|
|
122
|
+
assert.equal(derived.credentialSubject.birthDate, undefined, 'birthDate should be hidden');
|
|
123
|
+
assert.equal(derived.credentialSubject.lprNumber, undefined, 'lprNumber should be hidden');
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
test('wallet-compat: NAPI verifies its own derived proof from @mattrglobal-signed VC', async () => {
|
|
127
|
+
const derived = await ldDeriveProof({
|
|
128
|
+
document: signedPRC,
|
|
129
|
+
revealDocument,
|
|
130
|
+
contexts: napiContexts,
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
const result = await ldVerify({
|
|
134
|
+
document: derived,
|
|
135
|
+
contexts: napiContexts,
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
assert.equal(result.verified, true,
|
|
139
|
+
`NAPI ldVerify failed on NAPI-derived proof: ${result.error || 'unknown error'}`);
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
|
|
143
|
+
//
|
|
144
|
+
// Tamper detection — NAPI must reject modified @mattrglobal-signed VCs
|
|
145
|
+
//
|
|
146
|
+
|
|
147
|
+
test('wallet-compat: NAPI rejects tampered @mattrglobal-signed VC', async () => {
|
|
148
|
+
const tampered = JSON.parse(JSON.stringify(signedPRC));
|
|
149
|
+
tampered.credentialSubject.givenName = 'TAMPERED';
|
|
150
|
+
|
|
151
|
+
const result = await ldVerify({
|
|
152
|
+
document: tampered,
|
|
153
|
+
contexts: napiContexts,
|
|
154
|
+
});
|
|
155
|
+
|
|
156
|
+
assert.equal(result.verified, false, 'tampered VC should not verify');
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
test('wallet-compat: NAPI rejects @mattrglobal-signed VC with altered proof', async () => {
|
|
160
|
+
const altered = JSON.parse(JSON.stringify(signedPRC));
|
|
161
|
+
// Flip a character in the proof value
|
|
162
|
+
altered.proof.proofValue = 'X' + altered.proof.proofValue.slice(1);
|
|
163
|
+
|
|
164
|
+
// The Rust BBS crate may panic on malformed proof data rather than returning
|
|
165
|
+
// verified: false. Both behaviors correctly reject the tampered proof.
|
|
166
|
+
try {
|
|
167
|
+
const result = await ldVerify({
|
|
168
|
+
document: altered,
|
|
169
|
+
contexts: napiContexts,
|
|
170
|
+
});
|
|
171
|
+
assert.equal(result.verified, false, 'altered proof should not verify');
|
|
172
|
+
} catch (err) {
|
|
173
|
+
// Panic or error thrown — proof was rejected, which is the correct behavior
|
|
174
|
+
assert.ok(true, `proof correctly rejected with error: ${err.message}`);
|
|
175
|
+
}
|
|
176
|
+
});
|
|
177
|
+
|
|
178
|
+
|
|
179
|
+
//
|
|
180
|
+
// Multiple selective disclosure frames — verify different field combinations
|
|
181
|
+
// work when deriving from an @mattrglobal-signed VC
|
|
182
|
+
//
|
|
183
|
+
|
|
184
|
+
test('wallet-compat: derive minimal proof (type only) from @mattrglobal-signed VC', async () => {
|
|
185
|
+
const minimalFrame = {
|
|
186
|
+
"@context": signedPRC["@context"],
|
|
187
|
+
"type": ["VerifiableCredential", "PermanentResidentCard"],
|
|
188
|
+
"credentialSubject": {
|
|
189
|
+
"@explicit": true,
|
|
190
|
+
"type": ["PermanentResident", "Person"],
|
|
191
|
+
}
|
|
192
|
+
};
|
|
193
|
+
|
|
194
|
+
const derived = await ldDeriveProof({
|
|
195
|
+
document: signedPRC,
|
|
196
|
+
revealDocument: minimalFrame,
|
|
197
|
+
contexts: napiContexts,
|
|
198
|
+
});
|
|
199
|
+
|
|
200
|
+
assert.ok(derived.proof, 'derived document has a proof');
|
|
201
|
+
assert.equal(derived.proof.type, 'BbsBlsSignatureProof2020');
|
|
202
|
+
// Only type should be present, no PII fields
|
|
203
|
+
assert.equal(derived.credentialSubject.givenName, undefined);
|
|
204
|
+
assert.equal(derived.credentialSubject.familyName, undefined);
|
|
205
|
+
|
|
206
|
+
// Verify the minimal derived proof
|
|
207
|
+
const result = await ldVerify({ document: derived, contexts: napiContexts });
|
|
208
|
+
assert.equal(result.verified, true,
|
|
209
|
+
`minimal derived proof failed verification: ${result.error || 'unknown error'}`);
|
|
210
|
+
});
|
|
211
|
+
|
|
212
|
+
|
|
213
|
+
//
|
|
214
|
+
// VC variant tests — different field counts exercise different BBS+ statement counts.
|
|
215
|
+
// BBS+ signatures encode each field as a separate "message" (statement). Verifying
|
|
216
|
+
// VCs with different field counts ensures the NAPI implementation handles varying
|
|
217
|
+
// statement counts correctly, matching the old library's behavior.
|
|
218
|
+
//
|
|
219
|
+
|
|
220
|
+
test('variant: NAPI ldVerify accepts @mattrglobal-signed minimal VC (few fields)', async () => {
|
|
221
|
+
const result = await ldVerify({
|
|
222
|
+
document: signedMinimal,
|
|
223
|
+
contexts: napiContexts,
|
|
224
|
+
});
|
|
225
|
+
|
|
226
|
+
assert.equal(result.verified, true,
|
|
227
|
+
`NAPI ldVerify failed on minimal VC: ${result.error || 'unknown error'}`);
|
|
228
|
+
});
|
|
229
|
+
|
|
230
|
+
test('variant: NAPI ldVerify accepts @mattrglobal-signed rich VC (many fields)', async () => {
|
|
231
|
+
const result = await ldVerify({
|
|
232
|
+
document: signedRich,
|
|
233
|
+
contexts: napiContexts,
|
|
234
|
+
});
|
|
235
|
+
|
|
236
|
+
assert.equal(result.verified, true,
|
|
237
|
+
`NAPI ldVerify failed on rich VC: ${result.error || 'unknown error'}`);
|
|
238
|
+
});
|
|
239
|
+
|
|
240
|
+
test('variant: NAPI ldDeriveProof + ldVerify on minimal VC', async () => {
|
|
241
|
+
// Minimal VC only has givenName and familyName — derive with type-only frame
|
|
242
|
+
const minimalFrame = {
|
|
243
|
+
"@context": signedMinimal["@context"],
|
|
244
|
+
"type": ["VerifiableCredential", "PermanentResidentCard"],
|
|
245
|
+
"credentialSubject": {
|
|
246
|
+
"@explicit": true,
|
|
247
|
+
"type": ["PermanentResident", "Person"],
|
|
248
|
+
"givenName": {},
|
|
249
|
+
}
|
|
250
|
+
};
|
|
251
|
+
|
|
252
|
+
const derived = await ldDeriveProof({
|
|
253
|
+
document: signedMinimal,
|
|
254
|
+
revealDocument: minimalFrame,
|
|
255
|
+
contexts: napiContexts,
|
|
256
|
+
});
|
|
257
|
+
|
|
258
|
+
assert.ok(derived.proof, 'derived document has a proof');
|
|
259
|
+
assert.equal(derived.credentialSubject.givenName, 'Alice');
|
|
260
|
+
assert.equal(derived.credentialSubject.familyName, undefined, 'familyName should be hidden');
|
|
261
|
+
|
|
262
|
+
const result = await ldVerify({ document: derived, contexts: napiContexts });
|
|
263
|
+
assert.equal(result.verified, true,
|
|
264
|
+
`minimal VC derived proof failed: ${result.error || 'unknown error'}`);
|
|
265
|
+
});
|
|
266
|
+
|
|
267
|
+
test('variant: NAPI ldDeriveProof + ldVerify on rich VC', async () => {
|
|
268
|
+
// Rich VC has many fields — derive revealing only a few
|
|
269
|
+
const derived = await ldDeriveProof({
|
|
270
|
+
document: signedRich,
|
|
271
|
+
revealDocument,
|
|
272
|
+
contexts: napiContexts,
|
|
273
|
+
});
|
|
274
|
+
|
|
275
|
+
assert.ok(derived.proof, 'derived document has a proof');
|
|
276
|
+
assert.equal(derived.credentialSubject.givenName, 'Robert');
|
|
277
|
+
assert.equal(derived.credentialSubject.familyName, 'Williams');
|
|
278
|
+
assert.equal(derived.credentialSubject.gender, 'Male');
|
|
279
|
+
// Fields not in reveal frame should be hidden
|
|
280
|
+
assert.equal(derived.credentialSubject.birthDate, undefined, 'birthDate should be hidden');
|
|
281
|
+
assert.equal(derived.credentialSubject.lprNumber, undefined, 'lprNumber should be hidden');
|
|
282
|
+
assert.equal(derived.credentialSubject.birthCountry, undefined, 'birthCountry should be hidden');
|
|
283
|
+
|
|
284
|
+
const result = await ldVerify({ document: derived, contexts: napiContexts });
|
|
285
|
+
assert.equal(result.verified, true,
|
|
286
|
+
`rich VC derived proof failed: ${result.error || 'unknown error'}`);
|
|
287
|
+
});
|