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.
- package/cjs/bin_to_str.js +44 -0
- package/cjs/collect-body-keys.js +470 -0
- package/cjs/encode-array-item.js +110 -0
- package/cjs/encode-utils.js +236 -0
- package/cjs/encode.js +1318 -0
- package/cjs/erl_json.js +317 -0
- package/cjs/erl_str.js +1037 -0
- package/cjs/flat.js +222 -0
- package/cjs/http-message-signatures/httpbis.js +489 -0
- package/cjs/http-message-signatures/index.js +25 -0
- package/cjs/http-message-signatures/structured-header.js +129 -0
- package/cjs/httpsig.js +716 -0
- package/cjs/httpsig2.js +1160 -0
- package/cjs/id.js +470 -0
- package/cjs/index.js +63 -0
- package/cjs/send.js +194 -0
- package/cjs/signer-utils.js +617 -0
- package/cjs/signer.js +606 -0
- package/cjs/structured.js +296 -0
- package/cjs/test.js +27 -0
- package/cjs/utils.js +42 -0
- package/esm/bin_to_str.js +46 -0
- package/esm/collect-body-keys.js +436 -0
- package/esm/encode-array-item.js +112 -0
- package/esm/encode-utils.js +185 -0
- package/esm/encode.js +1219 -0
- package/esm/erl_json.js +289 -0
- package/esm/erl_str.js +1139 -0
- package/esm/flat.js +196 -0
- package/esm/http-message-signatures/httpbis.js +438 -0
- package/esm/http-message-signatures/index.js +4 -0
- package/esm/http-message-signatures/structured-header.js +105 -0
- package/esm/httpsig.js +658 -0
- package/esm/httpsig2.js +1097 -0
- package/esm/id.js +459 -0
- package/esm/index.js +4 -0
- package/esm/package.json +3 -0
- package/esm/send.js +124 -0
- package/esm/signer-utils.js +494 -0
- package/esm/signer.js +452 -0
- package/esm/structured.js +269 -0
- package/esm/test.js +6 -0
- package/esm/utils.js +28 -0
- 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
|
+
}
|