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.
- package/.babelrc-cjs +5 -0
- package/.babelrc-esm +5 -0
- package/README.md +1 -0
- package/dist/package.json +39 -0
- package/make.js +36 -0
- package/package.json +16 -17
- package/src/bin_to_str.js +46 -0
- package/src/collect-body-keys.js +436 -0
- package/src/commit.js +219 -0
- package/src/encode-array-item.js +112 -0
- package/src/encode-utils.js +191 -0
- package/src/encode.js +1256 -0
- package/src/erl_json.js +292 -0
- package/src/erl_str.js +1144 -0
- package/src/flat.js +250 -0
- package/src/http-message-signatures/httpbis.js +438 -0
- package/src/http-message-signatures/index.js +4 -0
- package/src/http-message-signatures/structured-header.js +105 -0
- package/src/httpsig.js +866 -0
- package/src/id.js +459 -0
- package/src/index.js +13 -0
- package/src/nocrypto.js +4 -0
- package/src/parser.js +171 -0
- package/src/send-utils.js +1132 -0
- package/src/send.js +142 -0
- package/src/signer-utils.js +375 -0
- package/src/signer.js +312 -0
- package/src/structured.js +496 -0
- package/src/test.js +2 -0
- package/src/utils.js +29 -0
- package/test/commit.test.js +41 -0
- package/test/erl_json.test.js +8 -0
- package/test/flat.test.js +27 -0
- package/test/httpsig.test.js +31 -0
- package/test/id.test.js +114 -0
- package/test/lib/all_cases.js +408 -0
- package/test/lib/cases.js +408 -0
- package/test/lib/erl_json_cases.js +161 -0
- package/test/lib/flat_cases.js +189 -0
- package/test/lib/gen.js +528 -0
- package/test/lib/httpsig_cases.js +313 -0
- package/test/lib/structured_cases.js +222 -0
- package/test/lib/test-utils.js +399 -0
- package/test/signer.test.js +48 -0
- package/test/structured.test.js +35 -0
- package/bin/install-deps +0 -0
- /package/{cjs → dist/cjs}/bin_to_str.js +0 -0
- /package/{cjs → dist/cjs}/collect-body-keys.js +0 -0
- /package/{cjs → dist/cjs}/commit.js +0 -0
- /package/{cjs → dist/cjs}/encode-array-item.js +0 -0
- /package/{cjs → dist/cjs}/encode-utils.js +0 -0
- /package/{cjs → dist/cjs}/encode.js +0 -0
- /package/{cjs → dist/cjs}/erl_json.js +0 -0
- /package/{cjs → dist/cjs}/erl_str.js +0 -0
- /package/{cjs → dist/cjs}/flat.js +0 -0
- /package/{cjs → dist/cjs}/http-message-signatures/httpbis.js +0 -0
- /package/{cjs → dist/cjs}/http-message-signatures/index.js +0 -0
- /package/{cjs → dist/cjs}/http-message-signatures/structured-header.js +0 -0
- /package/{cjs → dist/cjs}/httpsig.js +0 -0
- /package/{cjs → dist/cjs}/id.js +0 -0
- /package/{cjs → dist/cjs}/index.js +0 -0
- /package/{cjs → dist/cjs}/nocrypto.js +0 -0
- /package/{cjs → dist/cjs}/parser.js +0 -0
- /package/{cjs → dist/cjs}/send-utils.js +0 -0
- /package/{cjs → dist/cjs}/send.js +0 -0
- /package/{cjs → dist/cjs}/signer-utils.js +0 -0
- /package/{cjs → dist/cjs}/signer.js +0 -0
- /package/{cjs → dist/cjs}/structured.js +0 -0
- /package/{cjs → dist/cjs}/test.js +0 -0
- /package/{cjs → dist/cjs}/utils.js +0 -0
- /package/{esm → dist/esm}/bin_to_str.js +0 -0
- /package/{esm → dist/esm}/collect-body-keys.js +0 -0
- /package/{esm → dist/esm}/commit.js +0 -0
- /package/{esm → dist/esm}/encode-array-item.js +0 -0
- /package/{esm → dist/esm}/encode-utils.js +0 -0
- /package/{esm → dist/esm}/encode.js +0 -0
- /package/{esm → dist/esm}/erl_json.js +0 -0
- /package/{esm → dist/esm}/erl_str.js +0 -0
- /package/{esm → dist/esm}/flat.js +0 -0
- /package/{esm → dist/esm}/http-message-signatures/httpbis.js +0 -0
- /package/{esm → dist/esm}/http-message-signatures/index.js +0 -0
- /package/{esm → dist/esm}/http-message-signatures/structured-header.js +0 -0
- /package/{esm → dist/esm}/httpsig.js +0 -0
- /package/{esm → dist/esm}/id.js +0 -0
- /package/{esm → dist/esm}/index.js +0 -0
- /package/{esm → dist/esm}/nocrypto.js +0 -0
- /package/{esm → dist/esm}/package.json +0 -0
- /package/{esm → dist/esm}/parser.js +0 -0
- /package/{esm → dist/esm}/send-utils.js +0 -0
- /package/{esm → dist/esm}/send.js +0 -0
- /package/{esm → dist/esm}/signer-utils.js +0 -0
- /package/{esm → dist/esm}/signer.js +0 -0
- /package/{esm → dist/esm}/structured.js +0 -0
- /package/{esm → dist/esm}/test.js +0 -0
- /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 }
|