@smartledger/bsv 4.6.0 → 5.0.1

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.
@@ -4,12 +4,28 @@
4
4
  * VC-JWT Module
5
5
  * W3C Verifiable Credentials using JWT/JWS
6
6
  * Supports ES256 (P-256) and ES256K (secp256k1)
7
+ *
8
+ * Signatures are emitted and verified as raw r||s (IEEE P1363) per the JOSE
9
+ * specs (RFC 7515/7518; RFC 8812 for ES256K). This is what every standards
10
+ * compliant JWT/JWS library (jose, jsonwebtoken, ...) expects. Tokens issued
11
+ * by versions <= 4.6.0 used Node's default DER encoding and are NOT JOSE
12
+ * compatible; pass `{ allowLegacyDER: true }` to verifyVcJwt to accept them
13
+ * during migration.
7
14
  */
8
15
 
9
16
  var crypto = require('crypto')
10
17
 
18
+ // Supported JWS algorithms mapped to their expected JWK curve.
19
+ // (RFC 7518 §3.1 for ES256/P-256; RFC 8812 for ES256K/secp256k1.)
20
+ var ALG_TO_CRV = {
21
+ ES256: 'P-256',
22
+ ES256K: 'secp256k1'
23
+ }
24
+
25
+ var DEFAULT_ALLOWED_ALGS = ['ES256', 'ES256K']
26
+
11
27
  // Base64URL encoding
12
- function base64UrlEncode(buffer) {
28
+ function base64UrlEncode (buffer) {
13
29
  return buffer.toString('base64')
14
30
  .replace(/\+/g, '-')
15
31
  .replace(/\//g, '_')
@@ -17,7 +33,7 @@ function base64UrlEncode(buffer) {
17
33
  }
18
34
 
19
35
  // Base64URL decoding
20
- function base64UrlDecode(str) {
36
+ function base64UrlDecode (str) {
21
37
  str = str.replace(/-/g, '+').replace(/_/g, '/')
22
38
  while (str.length % 4) {
23
39
  str += '='
@@ -26,12 +42,19 @@ function base64UrlDecode(str) {
26
42
  }
27
43
 
28
44
  // Issue a Verifiable Credential as JWT
29
- async function issueVcJwt(params) {
45
+ async function issueVcJwt (params) {
30
46
  if (!params.issuerDid || !params.subjectId || !params.credentialSubject || !params.privateJwk) {
31
47
  throw new Error('issuerDid, subjectId, credentialSubject, and privateJwk are required')
32
48
  }
33
49
 
34
50
  var alg = params.alg || 'ES256'
51
+ if (!ALG_TO_CRV[alg]) {
52
+ throw new Error('Unsupported algorithm: ' + alg)
53
+ }
54
+ if (params.privateJwk.crv && params.privateJwk.crv !== ALG_TO_CRV[alg]) {
55
+ throw new Error('Key curve ' + params.privateJwk.crv + ' does not match alg ' + alg)
56
+ }
57
+
35
58
  var kid = params.kid || params.privateJwk.kid
36
59
  var types = params.types || ['VerifiableCredential']
37
60
  var expSeconds = params.expSeconds || (365 * 24 * 60 * 60) // 1 year default
@@ -73,20 +96,20 @@ async function issueVcJwt(params) {
73
96
  var payloadB64 = base64UrlEncode(Buffer.from(JSON.stringify(jwtPayload)))
74
97
  var signingInput = headerB64 + '.' + payloadB64
75
98
 
76
- // Sign with private key
99
+ // Sign with private key.
77
100
  var privateKey = crypto.createPrivateKey({
78
101
  key: params.privateJwk,
79
102
  format: 'jwk'
80
103
  })
81
104
 
82
- var signature
83
- if (alg === 'ES256') {
84
- signature = crypto.sign('sha256', Buffer.from(signingInput), privateKey)
85
- } else if (alg === 'ES256K') {
86
- signature = crypto.sign('sha256', Buffer.from(signingInput), privateKey)
87
- } else {
88
- throw new Error('Unsupported algorithm: ' + alg)
89
- }
105
+ // JWS (RFC 7515/7518) requires ECDSA signatures as the raw r||s concatenation
106
+ // (IEEE P1363), NOT the DER encoding Node emits by default. Without
107
+ // dsaEncoding:'ieee-p1363' the token fails verification in every standards
108
+ // compliant JOSE library.
109
+ var signature = crypto.sign('sha256', Buffer.from(signingInput), {
110
+ key: privateKey,
111
+ dsaEncoding: 'ieee-p1363'
112
+ })
90
113
 
91
114
  var signatureB64 = base64UrlEncode(signature)
92
115
  var jwt = signingInput + '.' + signatureB64
@@ -95,11 +118,14 @@ async function issueVcJwt(params) {
95
118
  }
96
119
 
97
120
  // Verify a VC-JWT
98
- async function verifyVcJwt(jwt, opts) {
121
+ async function verifyVcJwt (jwt, opts) {
99
122
  opts = opts || {}
100
123
 
101
124
  try {
102
125
  // Parse JWT
126
+ if (typeof jwt !== 'string') {
127
+ return { valid: false, error: 'Invalid JWT format' }
128
+ }
103
129
  var parts = jwt.split('.')
104
130
  if (parts.length !== 3) {
105
131
  return { valid: false, error: 'Invalid JWT format' }
@@ -113,14 +139,27 @@ async function verifyVcJwt(jwt, opts) {
113
139
  var payload = JSON.parse(base64UrlDecode(payloadB64).toString())
114
140
  var signature = base64UrlDecode(signatureB64)
115
141
 
142
+ // Pin the algorithm: never trust header.alg blindly. Reject anything
143
+ // outside the supported set (or outside opts.allowedAlgs when provided).
144
+ // This is the primary defense against JWT algorithm-substitution attacks.
145
+ var allowedAlgs = opts.allowedAlgs || DEFAULT_ALLOWED_ALGS
146
+ if (!header.alg || allowedAlgs.indexOf(header.alg) === -1 || !ALG_TO_CRV[header.alg]) {
147
+ return { valid: false, error: 'Algorithm not allowed: ' + header.alg, header: header, payload: payload }
148
+ }
149
+
116
150
  // Check expiration
117
151
  var now = Math.floor(Date.now() / 1000)
118
152
  var clockTolerance = opts.clockToleranceSec || 60
119
-
153
+
120
154
  if (payload.exp && payload.exp < (now - clockTolerance)) {
121
155
  return { valid: false, error: 'JWT expired', header: header, payload: payload }
122
156
  }
123
157
 
158
+ // Reject not-yet-valid tokens (nbf), if present.
159
+ if (payload.nbf && payload.nbf > (now + clockTolerance)) {
160
+ return { valid: false, error: 'JWT not yet valid', header: header, payload: payload }
161
+ }
162
+
124
163
  // Check issuer if expected
125
164
  if (opts.expectedIssuerDid && payload.iss !== opts.expectedIssuerDid) {
126
165
  return { valid: false, error: 'Unexpected issuer', header: header, payload: payload }
@@ -133,22 +172,28 @@ async function verifyVcJwt(jwt, opts) {
133
172
  if (!resolved || !resolved.jwks || !resolved.jwks.keys) {
134
173
  return { valid: false, error: 'Failed to resolve issuer DID', header: header, payload: payload }
135
174
  }
136
-
175
+
137
176
  // Find matching key by kid
138
- var matchingKey = resolved.jwks.keys.find(function(k) {
177
+ var matchingKey = resolved.jwks.keys.find(function (k) {
139
178
  return k.kid === header.kid
140
179
  })
141
-
180
+
142
181
  if (!matchingKey) {
143
182
  return { valid: false, error: 'Key not found in JWKS', header: header, payload: payload }
144
183
  }
145
-
184
+
146
185
  publicKey = matchingKey
147
186
  } else {
148
187
  // Without resolver, verification cannot proceed
149
188
  return { valid: false, error: 'DID resolver required for verification', header: header, payload: payload }
150
189
  }
151
190
 
191
+ // Ensure the resolving key's curve matches the pinned algorithm, so an
192
+ // ES256K signature can't be checked against a P-256 key or vice versa.
193
+ if (publicKey.crv && publicKey.crv !== ALG_TO_CRV[header.alg]) {
194
+ return { valid: false, error: 'Key curve does not match algorithm', header: header, payload: payload }
195
+ }
196
+
152
197
  // Verify signature
153
198
  var signingInput = headerB64 + '.' + payloadB64
154
199
  var pubKey = crypto.createPublicKey({
@@ -156,13 +201,29 @@ async function verifyVcJwt(jwt, opts) {
156
201
  format: 'jwk'
157
202
  })
158
203
 
204
+ // Signature is raw r||s (IEEE P1363) per JWS — decode it as such.
159
205
  var isValid = crypto.verify(
160
206
  'sha256',
161
207
  Buffer.from(signingInput),
162
- pubKey,
208
+ { key: pubKey, dsaEncoding: 'ieee-p1363' },
163
209
  signature
164
210
  )
165
211
 
212
+ // Migration path: tokens issued by <= 4.6.0 carry a DER-encoded signature.
213
+ // Only accepted when the caller explicitly opts in.
214
+ if (!isValid && opts.allowLegacyDER) {
215
+ try {
216
+ isValid = crypto.verify(
217
+ 'sha256',
218
+ Buffer.from(signingInput),
219
+ { key: pubKey, dsaEncoding: 'der' },
220
+ signature
221
+ )
222
+ } catch (e) {
223
+ isValid = false
224
+ }
225
+ }
226
+
166
227
  if (!isValid) {
167
228
  return { valid: false, error: 'Invalid signature', header: header, payload: payload }
168
229
  }
@@ -172,7 +233,6 @@ async function verifyVcJwt(jwt, opts) {
172
233
  header: header,
173
234
  payload: payload
174
235
  }
175
-
176
236
  } catch (error) {
177
237
  return {
178
238
  valid: false,
@@ -185,5 +245,6 @@ module.exports = {
185
245
  issueVcJwt: issueVcJwt,
186
246
  verifyVcJwt: verifyVcJwt,
187
247
  base64UrlEncode: base64UrlEncode,
188
- base64UrlDecode: base64UrlDecode
189
- }
248
+ base64UrlDecode: base64UrlDecode,
249
+ ALG_TO_CRV: ALG_TO_CRV
250
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@smartledger/bsv",
3
- "version": "4.6.0",
3
+ "version": "5.0.1",
4
4
  "description": "🚀 Complete Bitcoin SV development framework with legally-recognizable DID:web + W3C VC-JWT toolkit, Legal Token Protocol (LTP), Global Digital Attestation Framework (GDAF), StatusList2021 revocation, and 16 flexible loading options. Standards-based credentials with ES256/ES256K support, on-chain BSV anchoring, and comprehensive Bitcoin SV API. Perfect for legal tokens, verifiable credentials, DeFi, smart contracts, and secure Bitcoin applications.",
5
5
  "author": "SmartLedger Technology <hello@smartledger.technology> (https://smartledger.technology)",
6
6
  "homepage": "https://github.com/codenlighten/smartledger-bsv#readme",
@@ -146,14 +146,7 @@
146
146
  "pels",
147
147
  "bip143",
148
148
  "preimage",
149
- "shamir-secret-sharing",
150
- "threshold-cryptography",
151
149
  "secret-splitting",
152
- "global-digital-attestation",
153
- "gdaf",
154
- "verifiable-credentials",
155
- "w3c-credentials",
156
- "decentralized-identity",
157
150
  "did-resolution",
158
151
  "zero-knowledge-proofs",
159
152
  "blockchain-anchoring",
@@ -198,7 +191,6 @@
198
191
  "request": "browser-request"
199
192
  },
200
193
  "dependencies": {
201
- "@smartledger/bsv": "^4.4.0",
202
194
  "aes-js": "^3.1.2",
203
195
  "bn.js": "=4.11.9",
204
196
  "bs58": "=4.0.1",
@@ -206,11 +198,13 @@
206
198
  "elliptic": "6.6.1",
207
199
  "hash.js": "^1.1.7",
208
200
  "inherits": "2.0.3",
201
+ "secrets.js-grempe": "^2.0.0",
209
202
  "unorm": "1.4.1"
210
203
  },
211
204
  "devDependencies": {
212
205
  "brfs": "2.0.1",
213
206
  "chai": "4.2.0",
207
+ "jose": "^5.10.0",
214
208
  "mocha": "^8.4.0",
215
209
  "nyc": "^14.1.1",
216
210
  "sinon": "7.2.3",