hbsig 0.3.2 → 0.3.3

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 (95) hide show
  1. package/.babelrc-cjs +5 -0
  2. package/.babelrc-esm +5 -0
  3. package/README.md +1 -0
  4. package/dist/package.json +39 -0
  5. package/make.js +36 -0
  6. package/package.json +16 -17
  7. package/src/bin_to_str.js +46 -0
  8. package/src/collect-body-keys.js +436 -0
  9. package/src/commit.js +219 -0
  10. package/src/encode-array-item.js +112 -0
  11. package/src/encode-utils.js +191 -0
  12. package/src/encode.js +1256 -0
  13. package/src/erl_json.js +292 -0
  14. package/src/erl_str.js +1144 -0
  15. package/src/flat.js +250 -0
  16. package/src/http-message-signatures/httpbis.js +438 -0
  17. package/src/http-message-signatures/index.js +4 -0
  18. package/src/http-message-signatures/structured-header.js +105 -0
  19. package/src/httpsig.js +866 -0
  20. package/src/id.js +459 -0
  21. package/src/index.js +13 -0
  22. package/src/nocrypto.js +4 -0
  23. package/src/parser.js +171 -0
  24. package/src/send-utils.js +1132 -0
  25. package/src/send.js +142 -0
  26. package/src/signer-utils.js +375 -0
  27. package/src/signer.js +312 -0
  28. package/src/structured.js +496 -0
  29. package/src/test.js +2 -0
  30. package/src/utils.js +29 -0
  31. package/test/commit.test.js +41 -0
  32. package/test/erl_json.test.js +8 -0
  33. package/test/flat.test.js +27 -0
  34. package/test/httpsig.test.js +31 -0
  35. package/test/id.test.js +114 -0
  36. package/test/lib/all_cases.js +408 -0
  37. package/test/lib/cases.js +408 -0
  38. package/test/lib/erl_json_cases.js +161 -0
  39. package/test/lib/flat_cases.js +189 -0
  40. package/test/lib/gen.js +528 -0
  41. package/test/lib/httpsig_cases.js +313 -0
  42. package/test/lib/structured_cases.js +222 -0
  43. package/test/lib/test-utils.js +399 -0
  44. package/test/signer.test.js +48 -0
  45. package/test/structured.test.js +35 -0
  46. package/bin/install-deps +0 -0
  47. /package/{cjs → dist/cjs}/bin_to_str.js +0 -0
  48. /package/{cjs → dist/cjs}/collect-body-keys.js +0 -0
  49. /package/{cjs → dist/cjs}/commit.js +0 -0
  50. /package/{cjs → dist/cjs}/encode-array-item.js +0 -0
  51. /package/{cjs → dist/cjs}/encode-utils.js +0 -0
  52. /package/{cjs → dist/cjs}/encode.js +0 -0
  53. /package/{cjs → dist/cjs}/erl_json.js +0 -0
  54. /package/{cjs → dist/cjs}/erl_str.js +0 -0
  55. /package/{cjs → dist/cjs}/flat.js +0 -0
  56. /package/{cjs → dist/cjs}/http-message-signatures/httpbis.js +0 -0
  57. /package/{cjs → dist/cjs}/http-message-signatures/index.js +0 -0
  58. /package/{cjs → dist/cjs}/http-message-signatures/structured-header.js +0 -0
  59. /package/{cjs → dist/cjs}/httpsig.js +0 -0
  60. /package/{cjs → dist/cjs}/id.js +0 -0
  61. /package/{cjs → dist/cjs}/index.js +0 -0
  62. /package/{cjs → dist/cjs}/nocrypto.js +0 -0
  63. /package/{cjs → dist/cjs}/parser.js +0 -0
  64. /package/{cjs → dist/cjs}/send-utils.js +0 -0
  65. /package/{cjs → dist/cjs}/send.js +0 -0
  66. /package/{cjs → dist/cjs}/signer-utils.js +0 -0
  67. /package/{cjs → dist/cjs}/signer.js +0 -0
  68. /package/{cjs → dist/cjs}/structured.js +0 -0
  69. /package/{cjs → dist/cjs}/test.js +0 -0
  70. /package/{cjs → dist/cjs}/utils.js +0 -0
  71. /package/{esm → dist/esm}/bin_to_str.js +0 -0
  72. /package/{esm → dist/esm}/collect-body-keys.js +0 -0
  73. /package/{esm → dist/esm}/commit.js +0 -0
  74. /package/{esm → dist/esm}/encode-array-item.js +0 -0
  75. /package/{esm → dist/esm}/encode-utils.js +0 -0
  76. /package/{esm → dist/esm}/encode.js +0 -0
  77. /package/{esm → dist/esm}/erl_json.js +0 -0
  78. /package/{esm → dist/esm}/erl_str.js +0 -0
  79. /package/{esm → dist/esm}/flat.js +0 -0
  80. /package/{esm → dist/esm}/http-message-signatures/httpbis.js +0 -0
  81. /package/{esm → dist/esm}/http-message-signatures/index.js +0 -0
  82. /package/{esm → dist/esm}/http-message-signatures/structured-header.js +0 -0
  83. /package/{esm → dist/esm}/httpsig.js +0 -0
  84. /package/{esm → dist/esm}/id.js +0 -0
  85. /package/{esm → dist/esm}/index.js +0 -0
  86. /package/{esm → dist/esm}/nocrypto.js +0 -0
  87. /package/{esm → dist/esm}/package.json +0 -0
  88. /package/{esm → dist/esm}/parser.js +0 -0
  89. /package/{esm → dist/esm}/send-utils.js +0 -0
  90. /package/{esm → dist/esm}/send.js +0 -0
  91. /package/{esm → dist/esm}/signer-utils.js +0 -0
  92. /package/{esm → dist/esm}/signer.js +0 -0
  93. /package/{esm → dist/esm}/structured.js +0 -0
  94. /package/{esm → dist/esm}/test.js +0 -0
  95. /package/{esm → dist/esm}/utils.js +0 -0
package/src/send.js ADDED
@@ -0,0 +1,142 @@
1
+ import base64url from "base64url"
2
+ import { httpbis } from "./http-message-signatures/index.js"
3
+ import { parseItem, serializeList } from "structured-headers"
4
+ import { httpsig_from } from "./httpsig.js"
5
+ import { structured_to } from "./structured.js"
6
+ import { result } from "./send-utils.js"
7
+
8
+ const {
9
+ augmentHeaders,
10
+ createSignatureBase,
11
+ createSigningParameters,
12
+ formatSignatureBase,
13
+ } = httpbis
14
+
15
+ const toMsg = async req => {
16
+ let msg = {}
17
+ req?.headers?.forEach((v, k) => {
18
+ msg[k] = v
19
+ })
20
+ if (req.body) {
21
+ const arrayBuffer = await req.arrayBuffer()
22
+ msg.body =
23
+ typeof Buffer !== "undefined"
24
+ ? Buffer.from(arrayBuffer) // Node.js
25
+ : new Uint8Array(arrayBuffer) // Browser
26
+ }
27
+
28
+ return msg
29
+ }
30
+
31
+ export async function send(signedMsg, fetchImpl = fetch) {
32
+ const fetchOptions = {
33
+ method: signedMsg.method,
34
+ headers: signedMsg.headers,
35
+ redirect: "follow",
36
+ }
37
+ if (
38
+ signedMsg.body !== undefined &&
39
+ signedMsg.method !== "GET" &&
40
+ signedMsg.method !== "HEAD"
41
+ ) {
42
+ fetchOptions.body = signedMsg.body
43
+ }
44
+ const response = await fetchImpl(signedMsg.url, fetchOptions)
45
+ if (response.status >= 400) {
46
+ throw new Error(`${response.status}: ${await response.text()}`)
47
+ }
48
+ return await result(response)
49
+ }
50
+
51
+ export const httpSigName = address => {
52
+ const decoded = base64url.toBuffer(address)
53
+ const hexString = [...decoded.subarray(1, 9)]
54
+ .map(byte => byte.toString(16).padStart(2, "0"))
55
+ .join("")
56
+ // Use 'comm-' prefix to match HyperBEAM's siginfo_to_commitments pattern
57
+ // (it strips <<"comm-", Rest/binary>> before parsing structured fields)
58
+ return `comm-${hexString}`
59
+ }
60
+
61
+ const toView = value => {
62
+ if (ArrayBuffer.isView(value)) {
63
+ return Buffer.from(value.buffer, value.byteOffset, value.byteLength)
64
+ } else if (typeof value === "string") return base64url.toBuffer(value)
65
+
66
+ throw new Error(
67
+ "Value must be Uint8Array, ArrayBuffer, or base64url-encoded string"
68
+ )
69
+ }
70
+
71
+ export const toHttpSigner = signer => {
72
+ const params = ["alg", "keyid"].sort()
73
+ return async ({ request, fields }) => {
74
+ let signatureBase
75
+ let signatureInput
76
+ let createCalled = false
77
+
78
+ const create = injected => {
79
+ createCalled = true
80
+
81
+ const { publicKey, alg = "rsa-pss-sha512" } = injected
82
+
83
+ const publicKeyBuffer = toView(publicKey)
84
+ // Use standard base64 encoding for keyid to be compatible with HyperBEAM's
85
+ // base64:decode in dev_codec_httpsig_keyid:apply_scheme
86
+ // Include "publickey:" prefix so HyperBEAM knows the key scheme
87
+ const keyidBase64 = `publickey:${publicKeyBuffer.toString("base64")}`
88
+
89
+ const signingParameters = createSigningParameters({
90
+ params,
91
+ paramValues: {
92
+ keyid: keyidBase64,
93
+ alg,
94
+ },
95
+ })
96
+
97
+ // SORT THE FIELDS HERE to match Erlang's lists:sort(maps:keys(Enc))
98
+ const sortedFields = [...fields].sort()
99
+
100
+ const signatureBaseArray = createSignatureBase(
101
+ { fields: sortedFields },
102
+ request
103
+ )
104
+ signatureInput = serializeList([
105
+ [
106
+ signatureBaseArray.map(([item]) => parseItem(item)),
107
+ signingParameters,
108
+ ],
109
+ ])
110
+
111
+ signatureBaseArray.push(['"@signature-params"', [signatureInput]])
112
+ signatureBase = formatSignatureBase(signatureBaseArray)
113
+ return new TextEncoder().encode(signatureBase)
114
+ }
115
+ const result = await signer(create, "httpsig")
116
+ if (!createCalled) {
117
+ throw new Error(
118
+ "create() must be invoked in order to construct the data to sign"
119
+ )
120
+ }
121
+
122
+ if (!result.signature || !result.address) {
123
+ throw new Error("Signer must return signature and address")
124
+ }
125
+
126
+ const signatureBuffer = toView(result.signature)
127
+ const signedHeaders = augmentHeaders(
128
+ request.headers,
129
+ signatureBuffer,
130
+ signatureInput,
131
+ httpSigName(result.address)
132
+ )
133
+ const finalHeaders = {}
134
+ for (const [key, value] of Object.entries(signedHeaders)) {
135
+ if (key === "Signature" || key === "Signature-Input") {
136
+ finalHeaders[key.toLowerCase()] = value
137
+ } else finalHeaders[key] = value
138
+ }
139
+
140
+ return { ...request, headers: finalHeaders }
141
+ }
142
+ }
@@ -0,0 +1,375 @@
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
+ import { decodeSigInput, extractPubKey } from "./parser.js"
6
+
7
+ const {
8
+ augmentHeaders,
9
+ createSignatureBase,
10
+ createSigningParameters,
11
+ formatSignatureBase,
12
+ } = httpbis
13
+
14
+ const { verifyMessage } = httpbis
15
+
16
+ /**
17
+ * Convert JWK modulus (n) to PEM format public key
18
+ * @param {Buffer} nBuffer - The modulus buffer
19
+ * @returns {string} PEM formatted public key
20
+ */
21
+ function jwkModulusToPem(nBuffer) {
22
+ // RSA public key with standard exponent
23
+ const rsaPublicKey = crypto.createPublicKey({
24
+ key: {
25
+ kty: "RSA",
26
+ n: base64url.encode(nBuffer),
27
+ e: "AQAB", // Standard exponent 65537
28
+ },
29
+ format: "jwk",
30
+ })
31
+
32
+ return rsaPublicKey.export({ type: "spki", format: "pem" })
33
+ }
34
+
35
+ /**
36
+ * Extract signature name from headers
37
+ * @param {Object} headers - Request headers
38
+ * @returns {string|null} Signature name or null
39
+ */
40
+ function extractSignatureName(headers) {
41
+ const signatureHeader = headers["signature"] || headers["Signature"]
42
+ if (!signatureHeader) return null
43
+
44
+ // Extract signature name (e.g., "http-sig-xxxxxxxx")
45
+ // Handle both "name:" and "name=" formats
46
+ const match = signatureHeader.match(/^([^:=]+)[:=]/)
47
+ return match ? match[1] : null
48
+ }
49
+
50
+ /**
51
+ * Verify an HTTP signed message using http-message-signatures
52
+ *
53
+ * @param {Object} signedMessage - The signed message to verify
54
+ * @param {string} signedMessage.url - Request URL
55
+ * @param {string} signedMessage.method - HTTP method
56
+ * @param {Object} signedMessage.headers - Headers including signature
57
+ * @param {string} [signedMessage.body] - Request body
58
+ * @param {string|Buffer} [publicKey] - Optional public key (if not provided, extracts from keyid)
59
+ * @returns {Object} Verification result
60
+ */
61
+ export async function verify(signedMessage, publicKey) {
62
+ try {
63
+ const { url, method, headers, body } = signedMessage
64
+
65
+ // Determine which public key to use
66
+ let keyLookup
67
+
68
+ if (publicKey) {
69
+ // Use provided public key
70
+ const pem =
71
+ typeof publicKey === "string" ? publicKey : jwkModulusToPem(publicKey)
72
+
73
+ keyLookup = async keyId => {
74
+ return {
75
+ id: keyId,
76
+ algs: ["rsa-pss-sha512", "rsa-pss-sha256", "rsa-v1_5-sha256"],
77
+ verify: async (data, signature, parameters) => {
78
+ const verifier = crypto.createVerify(
79
+ `RSA-SHA${parameters.alg.includes("512") ? "512" : "256"}`
80
+ )
81
+ verifier.update(data)
82
+
83
+ if (parameters.alg.startsWith("rsa-pss")) {
84
+ return verifier.verify(
85
+ {
86
+ key: pem,
87
+ padding: crypto.constants.RSA_PKCS1_PSS_PADDING,
88
+ saltLength: crypto.constants.RSA_PSS_SALTLEN_DIGEST,
89
+ },
90
+ signature
91
+ )
92
+ } else {
93
+ return verifier.verify(pem, signature)
94
+ }
95
+ },
96
+ }
97
+ }
98
+ } else {
99
+ // Extract public key from keyid
100
+ const signatureName = extractSignatureName(headers)
101
+ const extractedKey = extractPubKey(headers, signatureName)
102
+ if (!extractedKey) {
103
+ return {
104
+ valid: false,
105
+ error: "No public key provided and none found in signature",
106
+ }
107
+ }
108
+
109
+ const pem = jwkModulusToPem(extractedKey)
110
+
111
+ keyLookup = async keyId => {
112
+ // The library might pass the keyId in different formats, so be flexible
113
+ return {
114
+ id: keyId,
115
+ algs: ["rsa-pss-sha512", "rsa-pss-sha256", "rsa-v1_5-sha256"],
116
+ verify: async (data, signature, parameters) => {
117
+ try {
118
+ const verifier = crypto.createVerify(
119
+ `RSA-SHA${parameters.alg.includes("512") ? "512" : "256"}`
120
+ )
121
+ verifier.update(data)
122
+
123
+ let verified
124
+ if (parameters.alg.startsWith("rsa-pss")) {
125
+ verified = verifier.verify(
126
+ {
127
+ key: pem,
128
+ padding: crypto.constants.RSA_PKCS1_PSS_PADDING,
129
+ saltLength: crypto.constants.RSA_PSS_SALTLEN_DIGEST,
130
+ },
131
+ signature
132
+ )
133
+ } else {
134
+ verified = verifier.verify(pem, signature)
135
+ }
136
+
137
+ return verified
138
+ } catch (error) {
139
+ console.error("Verification error:", error)
140
+ return false
141
+ }
142
+ },
143
+ }
144
+ }
145
+ }
146
+
147
+ // Create request object for verification
148
+ const request = {
149
+ method,
150
+ url,
151
+ headers: { ...headers },
152
+ }
153
+
154
+ // Extract additional info from headers
155
+ const signatureName = extractSignatureName(headers)
156
+ const extractedPublicKey = extractPubKey(headers, signatureName)
157
+
158
+ // Extract algorithm from signature-input
159
+ const signatureInputHeader =
160
+ headers["signature-input"] || headers["Signature-Input"]
161
+ const decodedSigInput = decodeSigInput(signatureInputHeader, signatureName)
162
+ const algorithm = decodedSigInput?.params?.alg
163
+
164
+ // Verify using the library
165
+ let verified = false
166
+ let verificationError = null
167
+
168
+ try {
169
+ const verificationResult = await verifyMessage(
170
+ {
171
+ keyLookup,
172
+ requiredFields: [], // Don't require specific fields
173
+ },
174
+ request
175
+ )
176
+ // If we get here without throwing, verification succeeded
177
+ verified = true
178
+ } catch (verifyError) {
179
+ // Verification failed
180
+ verificationError = verifyError.message
181
+ verified = false
182
+ }
183
+
184
+ return {
185
+ valid: true, // The signature format is valid
186
+ verified, // Whether the cryptographic verification passed
187
+ signatureName,
188
+ keyId: extractedPublicKey
189
+ ? base64url.encode(extractedPublicKey)
190
+ : undefined,
191
+ algorithm,
192
+ publicKeyFromHeader: extractedPublicKey,
193
+ decodedSignatureInput: decodedSigInput,
194
+ ...(verificationError && { error: verificationError }),
195
+ }
196
+ } catch (error) {
197
+ return {
198
+ valid: false,
199
+ error: error.message,
200
+ }
201
+ }
202
+ }
203
+
204
+ /**
205
+ * Extract public key from a signed message
206
+ *
207
+ * @param {Object} signedMessage - The signed message
208
+ * @returns {Buffer|null} The public key buffer or null
209
+ */
210
+ function extractPublicKeyFromMessage(signedMessage) {
211
+ const signatureName = extractSignatureName(signedMessage.headers)
212
+ return extractPubKey(signedMessage.headers, signatureName)
213
+ }
214
+
215
+ export async function send(signedMsg, fetchImpl = fetch) {
216
+ const fetchOptions = {
217
+ method: signedMsg.method,
218
+ headers: signedMsg.headers,
219
+ redirect: "follow",
220
+ }
221
+
222
+ // Only add body if it exists and method supports it
223
+ if (
224
+ signedMsg.body !== undefined &&
225
+ signedMsg.method !== "GET" &&
226
+ signedMsg.method !== "HEAD"
227
+ ) {
228
+ fetchOptions.body = signedMsg.body
229
+ }
230
+ const response = await fetchImpl(signedMsg.url, fetchOptions)
231
+
232
+ if (response.status >= 400) {
233
+ throw new Error(`${response.status}: ${await response.text()}`)
234
+ }
235
+
236
+ return {
237
+ headers: response.headers,
238
+ body: await response.text(),
239
+ status: response.status,
240
+ }
241
+ }
242
+
243
+ /**
244
+ * Convert value to Buffer
245
+ */
246
+ const toView = value => {
247
+ if (ArrayBuffer.isView(value)) {
248
+ return Buffer.from(value.buffer, value.byteOffset, value.byteLength)
249
+ } else if (typeof value === "string") {
250
+ return base64url.toBuffer(value)
251
+ }
252
+ throw new Error(
253
+ "Value must be Uint8Array, ArrayBuffer, or base64url-encoded string"
254
+ )
255
+ }
256
+
257
+ /**
258
+ * Generate HTTP signature name from address
259
+ */
260
+ const httpSigName = address => {
261
+ const decoded = base64url.toBuffer(address)
262
+ const hexString = [...decoded.subarray(1, 9)]
263
+ .map(byte => byte.toString(16).padStart(2, "0"))
264
+ .join("")
265
+ return `http-sig-${hexString}`
266
+ }
267
+
268
+ /**
269
+ * Create HTTP signer wrapper
270
+ */
271
+ export const toHttpSigner = signer => {
272
+ const params = ["alg", "keyid"].sort()
273
+
274
+ return async ({ request, fields }) => {
275
+ let signatureBase
276
+ let signatureInput
277
+ let createCalled = false
278
+
279
+ const create = injected => {
280
+ createCalled = true
281
+
282
+ const { publicKey, alg = "rsa-pss-sha512" } = injected
283
+
284
+ const publicKeyBuffer = toView(publicKey)
285
+ // Use standard base64 encoding for keyid to be compatible with HyperBEAM's
286
+ // base64:decode in dev_codec_httpsig_keyid:apply_scheme
287
+ // Include "publickey:" prefix so HyperBEAM knows the key scheme
288
+ const keyidBase64 = `publickey:${publicKeyBuffer.toString("base64")}`
289
+
290
+ const signingParameters = createSigningParameters({
291
+ params,
292
+ paramValues: {
293
+ keyid: keyidBase64,
294
+ alg,
295
+ },
296
+ })
297
+
298
+ const signatureBaseArray = createSignatureBase({ fields }, request)
299
+ signatureInput = serializeList([
300
+ [
301
+ signatureBaseArray.map(([item]) => parseItem(item)),
302
+ signingParameters,
303
+ ],
304
+ ])
305
+
306
+ signatureBaseArray.push(['"@signature-params"', [signatureInput]])
307
+ signatureBase = formatSignatureBase(signatureBaseArray)
308
+
309
+ return new TextEncoder().encode(signatureBase)
310
+ }
311
+
312
+ const result = await signer(create, "httpsig")
313
+
314
+ if (!createCalled) {
315
+ throw new Error(
316
+ "create() must be invoked in order to construct the data to sign"
317
+ )
318
+ }
319
+
320
+ if (!result.signature || !result.address) {
321
+ throw new Error("Signer must return signature and address")
322
+ }
323
+
324
+ const signatureBuffer = toView(result.signature)
325
+ const signedHeaders = augmentHeaders(
326
+ request.headers,
327
+ signatureBuffer,
328
+ signatureInput,
329
+ httpSigName(result.address)
330
+ )
331
+
332
+ // Only lowercase the signature headers
333
+ const finalHeaders = {}
334
+ for (const [key, value] of Object.entries(signedHeaders)) {
335
+ if (key === "Signature" || key === "Signature-Input") {
336
+ finalHeaders[key.toLowerCase()] = value
337
+ } else {
338
+ finalHeaders[key] = value
339
+ }
340
+ }
341
+
342
+ return {
343
+ ...request,
344
+ headers: finalHeaders,
345
+ }
346
+ }
347
+ }
348
+
349
+ /**
350
+ * Utility function to extract the message ID from a signed message
351
+ * Based on the original code's hash calculation
352
+ */
353
+ async function getMessageId(signedMessage) {
354
+ // Extract signature from the Signature header
355
+ const signatureHeader =
356
+ signedMessage.headers.Signature || signedMessage.headers.signature
357
+ const match = signatureHeader.match(/Signature:\s*'http-sig-[^:]+:([^']+)'/)
358
+ const signature = match ? match[1] : null
359
+
360
+ if (!signature) {
361
+ throw new Error("Could not extract signature from headers")
362
+ }
363
+
364
+ // Hash the signature to get message ID
365
+ const encoder = new TextEncoder()
366
+ const data = encoder.encode(signature)
367
+ const hashBuffer = await crypto.subtle.digest("SHA-256", data)
368
+ const hashArray = Array.from(new Uint8Array(hashBuffer))
369
+ const hashBase64 = btoa(String.fromCharCode(...hashArray))
370
+
371
+ return hashBase64
372
+ }
373
+
374
+ // Re-export functions from parser.js for backward compatibility
375
+ export { decodeSigInput, extractPubKey }