hbsig 0.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.
Files changed (44) hide show
  1. package/cjs/bin_to_str.js +44 -0
  2. package/cjs/collect-body-keys.js +470 -0
  3. package/cjs/encode-array-item.js +110 -0
  4. package/cjs/encode-utils.js +236 -0
  5. package/cjs/encode.js +1318 -0
  6. package/cjs/erl_json.js +317 -0
  7. package/cjs/erl_str.js +1037 -0
  8. package/cjs/flat.js +222 -0
  9. package/cjs/http-message-signatures/httpbis.js +489 -0
  10. package/cjs/http-message-signatures/index.js +25 -0
  11. package/cjs/http-message-signatures/structured-header.js +129 -0
  12. package/cjs/httpsig.js +716 -0
  13. package/cjs/httpsig2.js +1160 -0
  14. package/cjs/id.js +470 -0
  15. package/cjs/index.js +63 -0
  16. package/cjs/send.js +194 -0
  17. package/cjs/signer-utils.js +617 -0
  18. package/cjs/signer.js +606 -0
  19. package/cjs/structured.js +296 -0
  20. package/cjs/test.js +27 -0
  21. package/cjs/utils.js +42 -0
  22. package/esm/bin_to_str.js +46 -0
  23. package/esm/collect-body-keys.js +436 -0
  24. package/esm/encode-array-item.js +112 -0
  25. package/esm/encode-utils.js +185 -0
  26. package/esm/encode.js +1219 -0
  27. package/esm/erl_json.js +289 -0
  28. package/esm/erl_str.js +1139 -0
  29. package/esm/flat.js +196 -0
  30. package/esm/http-message-signatures/httpbis.js +438 -0
  31. package/esm/http-message-signatures/index.js +4 -0
  32. package/esm/http-message-signatures/structured-header.js +105 -0
  33. package/esm/httpsig.js +658 -0
  34. package/esm/httpsig2.js +1097 -0
  35. package/esm/id.js +459 -0
  36. package/esm/index.js +4 -0
  37. package/esm/package.json +3 -0
  38. package/esm/send.js +124 -0
  39. package/esm/signer-utils.js +494 -0
  40. package/esm/signer.js +452 -0
  41. package/esm/structured.js +269 -0
  42. package/esm/test.js +6 -0
  43. package/esm/utils.js +28 -0
  44. package/package.json +28 -0
@@ -0,0 +1,494 @@
1
+ import base64url from "base64url"
2
+ import crypto from "crypto"
3
+ import { httpbis } from "./http-message-signatures/index.js"
4
+ import { parseItem, serializeList } from "structured-headers"
5
+ const {
6
+ augmentHeaders,
7
+ createSignatureBase,
8
+ createSigningParameters,
9
+ formatSignatureBase,
10
+ } = httpbis
11
+
12
+ const { verifyMessage } = httpbis
13
+
14
+ /**
15
+ * Decode signature-input header to extract all components
16
+ * Handles format: signature-name=(components);params
17
+ *
18
+ * @param {string} signatureInput - The signature-input header value
19
+ * @param {string} [signatureName] - Optional specific signature name to decode
20
+ * @returns {Object} Decoded signature input with components and parameters
21
+ */
22
+ export function decodeSigInput(signatureInput, signatureName = null) {
23
+ if (!signatureInput) {
24
+ return null
25
+ }
26
+
27
+ try {
28
+ // If signature name is provided, extract just that section
29
+ let inputToDecode = signatureInput
30
+ if (signatureName) {
31
+ // Find the section for this specific signature
32
+ const startIndex = signatureInput.indexOf(signatureName)
33
+ if (startIndex === -1) {
34
+ return null
35
+ }
36
+
37
+ // Extract from signature name to the next signature (if any) or end
38
+ const nextSigMatch = signatureInput
39
+ .substring(startIndex + signatureName.length)
40
+ .match(/,\s*[a-zA-Z0-9-]+=/)
41
+ const endIndex = nextSigMatch
42
+ ? startIndex + signatureName.length + nextSigMatch.index
43
+ : signatureInput.length
44
+
45
+ inputToDecode = signatureInput.substring(startIndex, endIndex).trim()
46
+ }
47
+
48
+ // Parse each signature entry
49
+ const signatures = {}
50
+
51
+ // Split by signature entries (handle multiple signatures)
52
+ const entries = inputToDecode.split(/,(?=\s*[a-zA-Z0-9-]+=)/)
53
+
54
+ for (const entry of entries) {
55
+ const trimmedEntry = entry.trim()
56
+
57
+ // Match signature-name=(components);params format
58
+ const match = trimmedEntry.match(/^([a-zA-Z0-9-]+)=\(([^)]*)\)(.*)$/)
59
+ if (!match) {
60
+ continue
61
+ }
62
+
63
+ const sigName = match[1]
64
+ const componentsStr = match[2]
65
+ const paramsStr = match[3]
66
+
67
+ // Parse components (space-separated, may be quoted)
68
+ const components = []
69
+ const componentRegex = /"[^"]+"|[^\s]+/g
70
+ let componentMatch
71
+ while ((componentMatch = componentRegex.exec(componentsStr)) !== null) {
72
+ components.push(componentMatch[0].replace(/"/g, ""))
73
+ }
74
+
75
+ // Parse parameters (semicolon-separated key="value" pairs)
76
+ const params = {}
77
+ if (paramsStr) {
78
+ const paramPairs = paramsStr.split(";").filter(p => p.trim())
79
+ for (const pair of paramPairs) {
80
+ const [key, ...valueParts] = pair.split("=")
81
+ if (key && valueParts.length > 0) {
82
+ const value = valueParts.join("=").replace(/^"|"$/g, "")
83
+ params[key.trim()] = value
84
+ }
85
+ }
86
+ }
87
+
88
+ signatures[sigName] = {
89
+ components,
90
+ params,
91
+ raw: trimmedEntry,
92
+ }
93
+ }
94
+
95
+ // If specific signature was requested, return just that one
96
+ if (signatureName && signatures[signatureName]) {
97
+ return signatures[signatureName]
98
+ }
99
+
100
+ // Return all signatures or the first one if no specific name was given
101
+ return signatureName ? null : signatures
102
+ } catch (error) {
103
+ console.error("Error decoding signature-input:", error)
104
+ return null
105
+ }
106
+ }
107
+
108
+ /**
109
+ * Convert JWK modulus (n) to PEM format public key
110
+ * @param {Buffer} nBuffer - The modulus buffer
111
+ * @returns {string} PEM formatted public key
112
+ */
113
+ function jwkModulusToPem(nBuffer) {
114
+ // RSA public key with standard exponent
115
+ const rsaPublicKey = crypto.createPublicKey({
116
+ key: {
117
+ kty: "RSA",
118
+ n: base64url.encode(nBuffer),
119
+ e: "AQAB", // Standard exponent 65537
120
+ },
121
+ format: "jwk",
122
+ })
123
+
124
+ return rsaPublicKey.export({ type: "spki", format: "pem" })
125
+ }
126
+
127
+ /**
128
+ * Extract signature name from headers
129
+ * @param {Object} headers - Request headers
130
+ * @returns {string|null} Signature name or null
131
+ */
132
+ function extractSignatureName(headers) {
133
+ const signatureHeader = headers["signature"] || headers["Signature"]
134
+ if (!signatureHeader) return null
135
+
136
+ // Extract signature name (e.g., "http-sig-xxxxxxxx")
137
+ // Handle both "name:" and "name=" formats
138
+ const match = signatureHeader.match(/^([^:=]+)[:=]/)
139
+ return match ? match[1] : null
140
+ }
141
+
142
+ /**
143
+ * Verify an HTTP signed message using http-message-signatures
144
+ *
145
+ * @param {Object} signedMessage - The signed message to verify
146
+ * @param {string} signedMessage.url - Request URL
147
+ * @param {string} signedMessage.method - HTTP method
148
+ * @param {Object} signedMessage.headers - Headers including signature
149
+ * @param {string} [signedMessage.body] - Request body
150
+ * @param {string|Buffer} [publicKey] - Optional public key (if not provided, extracts from keyid)
151
+ * @returns {Object} Verification result
152
+ */
153
+ export async function verify(signedMessage, publicKey) {
154
+ try {
155
+ const { url, method, headers, body } = signedMessage
156
+
157
+ // Determine which public key to use
158
+ let keyLookup
159
+
160
+ if (publicKey) {
161
+ // Use provided public key
162
+ const pem =
163
+ typeof publicKey === "string" ? publicKey : jwkModulusToPem(publicKey)
164
+
165
+ keyLookup = async keyId => {
166
+ return {
167
+ id: keyId,
168
+ algs: ["rsa-pss-sha512", "rsa-pss-sha256", "rsa-v1_5-sha256"],
169
+ verify: async (data, signature, parameters) => {
170
+ const verifier = crypto.createVerify(
171
+ `RSA-SHA${parameters.alg.includes("512") ? "512" : "256"}`
172
+ )
173
+ verifier.update(data)
174
+
175
+ if (parameters.alg.startsWith("rsa-pss")) {
176
+ return verifier.verify(
177
+ {
178
+ key: pem,
179
+ padding: crypto.constants.RSA_PKCS1_PSS_PADDING,
180
+ saltLength: crypto.constants.RSA_PSS_SALTLEN_DIGEST,
181
+ },
182
+ signature
183
+ )
184
+ } else {
185
+ return verifier.verify(pem, signature)
186
+ }
187
+ },
188
+ }
189
+ }
190
+ } else {
191
+ // Extract public key from keyid
192
+ const signatureName = extractSignatureName(headers)
193
+ const extractedKey = extractPublicKeyFromHeaders(headers, signatureName)
194
+ if (!extractedKey) {
195
+ return {
196
+ valid: false,
197
+ error: "No public key provided and none found in signature",
198
+ }
199
+ }
200
+
201
+ const pem = jwkModulusToPem(extractedKey)
202
+
203
+ keyLookup = async keyId => {
204
+ // The library might pass the keyId in different formats, so be flexible
205
+ return {
206
+ id: keyId,
207
+ algs: ["rsa-pss-sha512", "rsa-pss-sha256", "rsa-v1_5-sha256"],
208
+ verify: async (data, signature, parameters) => {
209
+ try {
210
+ const verifier = crypto.createVerify(
211
+ `RSA-SHA${parameters.alg.includes("512") ? "512" : "256"}`
212
+ )
213
+ verifier.update(data)
214
+
215
+ let verified
216
+ if (parameters.alg.startsWith("rsa-pss")) {
217
+ verified = verifier.verify(
218
+ {
219
+ key: pem,
220
+ padding: crypto.constants.RSA_PKCS1_PSS_PADDING,
221
+ saltLength: crypto.constants.RSA_PSS_SALTLEN_DIGEST,
222
+ },
223
+ signature
224
+ )
225
+ } else {
226
+ verified = verifier.verify(pem, signature)
227
+ }
228
+
229
+ return verified
230
+ } catch (error) {
231
+ console.error("Verification error:", error)
232
+ return false
233
+ }
234
+ },
235
+ }
236
+ }
237
+ }
238
+
239
+ // Create request object for verification
240
+ const request = {
241
+ method,
242
+ url,
243
+ headers: { ...headers },
244
+ }
245
+
246
+ // Extract additional info from headers
247
+ const signatureName = extractSignatureName(headers)
248
+ const extractedPublicKey = extractPublicKeyFromHeaders(
249
+ headers,
250
+ signatureName
251
+ )
252
+
253
+ // Extract algorithm from signature-input
254
+ const signatureInputHeader =
255
+ headers["signature-input"] || headers["Signature-Input"]
256
+ const decodedSigInput = decodeSigInput(signatureInputHeader, signatureName)
257
+ const algorithm = decodedSigInput?.params?.alg
258
+
259
+ // Verify using the library
260
+ let verified = false
261
+ let verificationError = null
262
+
263
+ try {
264
+ const verificationResult = await verifyMessage(
265
+ {
266
+ keyLookup,
267
+ requiredFields: [], // Don't require specific fields
268
+ },
269
+ request
270
+ )
271
+ // If we get here without throwing, verification succeeded
272
+ verified = true
273
+ } catch (verifyError) {
274
+ // Verification failed
275
+ verificationError = verifyError.message
276
+ verified = false
277
+ }
278
+
279
+ return {
280
+ valid: true, // The signature format is valid
281
+ verified, // Whether the cryptographic verification passed
282
+ signatureName,
283
+ keyId: extractedPublicKey
284
+ ? base64url.encode(extractedPublicKey)
285
+ : undefined,
286
+ algorithm,
287
+ publicKeyFromHeader: extractedPublicKey,
288
+ decodedSignatureInput: decodedSigInput,
289
+ ...(verificationError && { error: verificationError }),
290
+ }
291
+ } catch (error) {
292
+ return {
293
+ valid: false,
294
+ error: error.message,
295
+ }
296
+ }
297
+ }
298
+
299
+ /**
300
+ * Extract public key from signature-input header
301
+ * @param {Object} headers - Request headers
302
+ * @param {string} [signatureName] - Optional signature name to look for
303
+ * @returns {Buffer|null} Public key buffer or null
304
+ */
305
+ export function extractPublicKeyFromHeaders(headers, signatureName) {
306
+ const signatureInput =
307
+ headers["signature-input"] || headers["Signature-Input"]
308
+ if (!signatureInput) return null
309
+
310
+ // Use the decoder to properly parse the signature-input
311
+ const decoded = decodeSigInput(signatureInput, signatureName)
312
+
313
+ if (!decoded) return null
314
+
315
+ // If we decoded a specific signature, use its keyid
316
+ const keyid =
317
+ signatureName && decoded.params
318
+ ? decoded.params.keyid
319
+ : Object.values(decoded)[0]?.params?.keyid
320
+
321
+ if (!keyid) return null
322
+
323
+ try {
324
+ return base64url.toBuffer(keyid)
325
+ } catch (error) {
326
+ return null
327
+ }
328
+ }
329
+
330
+ /**
331
+ * Extract public key from a signed message
332
+ *
333
+ * @param {Object} signedMessage - The signed message
334
+ * @returns {Buffer|null} The public key buffer or null
335
+ */
336
+ function extractPublicKeyFromMessage(signedMessage) {
337
+ const signatureName = extractSignatureName(signedMessage.headers)
338
+ return extractPublicKeyFromHeaders(signedMessage.headers, signatureName)
339
+ }
340
+
341
+ export async function send(signedMsg, fetchImpl = fetch) {
342
+ const fetchOptions = {
343
+ method: signedMsg.method,
344
+ headers: signedMsg.headers,
345
+ redirect: "follow",
346
+ }
347
+
348
+ // Only add body if it exists and method supports it
349
+ if (
350
+ signedMsg.body !== undefined &&
351
+ signedMsg.method !== "GET" &&
352
+ signedMsg.method !== "HEAD"
353
+ ) {
354
+ fetchOptions.body = signedMsg.body
355
+ }
356
+ const response = await fetchImpl(signedMsg.url, fetchOptions)
357
+
358
+ if (response.status >= 400) {
359
+ throw new Error(`${response.status}: ${await response.text()}`)
360
+ }
361
+
362
+ return {
363
+ headers: response.headers,
364
+ body: await response.text(),
365
+ status: response.status,
366
+ }
367
+ }
368
+
369
+ /**
370
+ * Convert value to Buffer
371
+ */
372
+ const toView = value => {
373
+ if (ArrayBuffer.isView(value)) {
374
+ return Buffer.from(value.buffer, value.byteOffset, value.byteLength)
375
+ } else if (typeof value === "string") {
376
+ return base64url.toBuffer(value)
377
+ }
378
+ throw new Error(
379
+ "Value must be Uint8Array, ArrayBuffer, or base64url-encoded string"
380
+ )
381
+ }
382
+
383
+ /**
384
+ * Generate HTTP signature name from address
385
+ */
386
+ const httpSigName = address => {
387
+ const decoded = base64url.toBuffer(address)
388
+ const hexString = [...decoded.subarray(1, 9)]
389
+ .map(byte => byte.toString(16).padStart(2, "0"))
390
+ .join("")
391
+ return `http-sig-${hexString}`
392
+ }
393
+
394
+ /**
395
+ * Create HTTP signer wrapper
396
+ */
397
+ export const toHttpSigner = signer => {
398
+ const params = ["alg", "keyid"].sort()
399
+
400
+ return async ({ request, fields }) => {
401
+ let signatureBase
402
+ let signatureInput
403
+ let createCalled = false
404
+
405
+ const create = injected => {
406
+ createCalled = true
407
+
408
+ const { publicKey, alg = "rsa-pss-sha512" } = injected
409
+
410
+ const publicKeyBuffer = toView(publicKey)
411
+
412
+ const signingParameters = createSigningParameters({
413
+ params,
414
+ paramValues: {
415
+ keyid: base64url.encode(publicKeyBuffer),
416
+ alg,
417
+ },
418
+ })
419
+
420
+ const signatureBaseArray = createSignatureBase({ fields }, request)
421
+ signatureInput = serializeList([
422
+ [
423
+ signatureBaseArray.map(([item]) => parseItem(item)),
424
+ signingParameters,
425
+ ],
426
+ ])
427
+
428
+ signatureBaseArray.push(['"@signature-params"', [signatureInput]])
429
+ signatureBase = formatSignatureBase(signatureBaseArray)
430
+
431
+ return new TextEncoder().encode(signatureBase)
432
+ }
433
+
434
+ const result = await signer(create, "httpsig")
435
+
436
+ if (!createCalled) {
437
+ throw new Error(
438
+ "create() must be invoked in order to construct the data to sign"
439
+ )
440
+ }
441
+
442
+ if (!result.signature || !result.address) {
443
+ throw new Error("Signer must return signature and address")
444
+ }
445
+
446
+ const signatureBuffer = toView(result.signature)
447
+ const signedHeaders = augmentHeaders(
448
+ request.headers,
449
+ signatureBuffer,
450
+ signatureInput,
451
+ httpSigName(result.address)
452
+ )
453
+
454
+ // Only lowercase the signature headers
455
+ const finalHeaders = {}
456
+ for (const [key, value] of Object.entries(signedHeaders)) {
457
+ if (key === "Signature" || key === "Signature-Input") {
458
+ finalHeaders[key.toLowerCase()] = value
459
+ } else {
460
+ finalHeaders[key] = value
461
+ }
462
+ }
463
+
464
+ return {
465
+ ...request,
466
+ headers: finalHeaders,
467
+ }
468
+ }
469
+ }
470
+
471
+ /**
472
+ * Utility function to extract the message ID from a signed message
473
+ * Based on the original code's hash calculation
474
+ */
475
+ async function getMessageId(signedMessage) {
476
+ // Extract signature from the Signature header
477
+ const signatureHeader =
478
+ signedMessage.headers.Signature || signedMessage.headers.signature
479
+ const match = signatureHeader.match(/Signature:\s*'http-sig-[^:]+:([^']+)'/)
480
+ const signature = match ? match[1] : null
481
+
482
+ if (!signature) {
483
+ throw new Error("Could not extract signature from headers")
484
+ }
485
+
486
+ // Hash the signature to get message ID
487
+ const encoder = new TextEncoder()
488
+ const data = encoder.encode(signature)
489
+ const hashBuffer = await crypto.subtle.digest("SHA-256", data)
490
+ const hashArray = Array.from(new Uint8Array(hashBuffer))
491
+ const hashBase64 = btoa(String.fromCharCode(...hashArray))
492
+
493
+ return hashBase64
494
+ }