1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
// SPDX-License-Identifier: 0BSD
import type { KeyLike } from "node:crypto";
import * as jose from "npm:jose";
import { base64pad as base64 } from "npm:multiformats/bases/base64";
export type SignatureOptions = {
pkcs8: string;
publicKeyId: string | URL;
algorithm?: "rsa-sha256";
signFn?: (message: string, pkcs8: string, algorithm: SignatureOptions["algorithm"]) => Promise<string>;
};
export function createSignedFetch(
options: SignatureOptions & {
fetch?: typeof fetch;
},
) {
const fetch = options.fetch ?? globalThis.fetch;
return async function signedFetch(
input: Request | URL | string,
init?: RequestInit & Partial<SignatureOptions>,
): Promise<Response> {
const signedReq = await signRequest(input, {
...options,
...init,
});
return fetch(signedReq);
};
}
export async function signRequest(
input: Request | URL | string,
init: RequestInit & SignatureOptions,
): Promise<Request> {
const req = new Request(input, init);
if (!req.headers.has("date")) {
req.headers.set("date", new Date().toUTCString());
}
if (!req.headers.has("host")) {
req.headers.set("host", new URL(req.url).host);
}
const message = createMessage(req);
const {
pkcs8,
publicKeyId,
algorithm = "rsa-sha256",
signFn = webcryptoSign,
} = init;
const signature = await signFn(message, pkcs8, algorithm);
req.headers.set(
"signature",
createSignatureHeader(signature, publicKeyId, algorithm),
);
return req;
}
function createMessage(req: Request) {
const { pathname, search } = new URL(req.url);
const message = Object.entries({
date: req.headers.get("date"),
host: req.headers.get("host"),
"(request-target)": `get ${pathname}${search}`,
})
.map(([key, value]) => `${key}: ${value}`)
.join("\n");
return message;
}
function createSignatureHeader(
signature: string,
publicKeyId: string | URL,
algorithm: SignatureOptions["algorithm"],
) {
return Object.entries({
publicKeyId: new URL(publicKeyId).href,
headers: "date host (request-target)",
algorithm,
signature,
})
.map(([key, value]) => `${key}=${JSON.stringify(value)}`)
.join(",");
}
export async function nodeSign(
message: string,
privateKey: KeyLike,
algorithm: SignatureOptions["algorithm"],
) {
if (algorithm === "rsa-sha256") {
const crypto = await import("node:crypto");
const sign = crypto.createSign("SHA256");
sign.write(message);
sign.end();
const signature = sign.sign(privateKey, "base64");
return signature;
}
throw new TypeError(`error: unsupported algorithm: ${algorithm}`);
}
export async function webcryptoSign(