feat(federation): Add new signed string output to signer

This commit is contained in:
Jesse Wierzbinski 2024-05-23 19:43:09 -10:00
parent 28e701bc13
commit 8860d09eb4
No known key found for this signature in database
2 changed files with 57 additions and 25 deletions

View file

@ -20,7 +20,7 @@ describe("SignatureValidator", () => {
body = JSON.stringify({ key: "value" });
const headers = await new SignatureConstructor(
const { headers } = await new SignatureConstructor(
privateKey,
"https://bob.org/users/6a18f2c3-120e-4949-bda4-2aa4c8264d51",
).sign("GET", new URL("https://example.com"), body);
@ -157,7 +157,7 @@ describe("SignatureConstructor", () => {
describe("Signing", () => {
test("should correctly sign ", async () => {
const url = new URL("https://example.com");
headers = await ctor.sign("GET", url, body);
headers = (await ctor.sign("GET", url, body)).headers;
expect(headers.get("Signature")).toBeDefined();
expect(headers.get("Date")).toBeDefined();
@ -187,7 +187,7 @@ describe("SignatureConstructor", () => {
method: "GET",
body: body,
});
const newRequest = await ctor.sign(request);
const { request: newRequest } = await ctor.sign(request);
headers = newRequest.headers;
expect(headers.get("Signature")).toBeDefined();
@ -195,5 +195,12 @@ describe("SignatureConstructor", () => {
expect(await newRequest.text()).toBe(body);
});
test("signing should also output a signed string", async () => {
const url = new URL("https://example.com");
const { signedString } = await ctor.sign("GET", url, body);
expect(signedString).toBeString();
expect(signedString.length).toBeGreaterThan(10);
});
});
});

View file

@ -231,12 +231,15 @@ export class SignatureConstructor {
/**
* Signs a request.
* @param request The request object to sign.
* @returns A Promise that resolves to the signed request.
* @returns A Promise that resolves to the signed request, plus the signed string.
* @example
* const request = new Request();
* const signedRequest = await constructor.sign(request);
* const { request: signedRequest } = await constructor.sign(request);
*/
async sign(request: Request): Promise<Request>;
async sign(request: Request): Promise<{
request: Request;
signedString: string;
}>;
/**
* Signs a request.
@ -244,41 +247,59 @@ export class SignatureConstructor {
* @param url The URL object.
* @param body The request body.
* @param headers The request headers.
* @returns A Promise that resolves to the signed headers.
* @param date The date that the request was signed (optional)
* @returns A Promise that resolves to the signed headers, and the signed string.
* @throws TypeError if any required parameters are missing or empty.
* @example
* const method = "GET";
* const url = new URL("https://example.com");
* const body = "request body";
* const signedHeaders = await constructor.sign(method, url, body);
* const { headers: signedHeaders } = await constructor.sign(method, url, body);
*/
async sign(
method: HttpVerb,
url: URL,
body: string,
headers?: Headers,
): Promise<Headers>;
date?: Date,
): Promise<{
headers: Headers;
signedString: string;
}>;
async sign(
requestOrMethod: Request | HttpVerb,
url?: URL,
body?: string,
headers: Headers = new Headers(),
): Promise<Request | Headers> {
date?: Date,
): Promise<
| {
headers: Headers;
signedString: string;
}
| {
request: Request;
signedString: string;
}
> {
if (requestOrMethod instanceof Request) {
const request = requestOrMethod.clone();
const headers = await this.sign(
const { headers, signedString } = await this.sign(
requestOrMethod.method as HttpVerb,
new URL(requestOrMethod.url),
await requestOrMethod.text(),
requestOrMethod.headers,
requestOrMethod.headers.get("Date")
? new Date(requestOrMethod.headers.get("Date") ?? "")
: undefined,
);
request.headers.set("Date", headers.get("Date") ?? "");
request.headers.set("Signature", headers.get("Signature") ?? "");
return request;
return { request, signedString };
}
if (!url || !body || !headers) {
@ -287,38 +308,42 @@ export class SignatureConstructor {
);
}
const date = new Date().toISOString();
const finalDate = date?.toISOString() ?? new Date().toISOString();
const digest = await crypto.subtle.digest(
"SHA-256",
new TextEncoder().encode(body),
);
const signedString =
`(request-target): ${requestOrMethod.toLowerCase()} ${
url.pathname
}\n` +
`host: ${url.host}\n` +
`date: ${finalDate}\n` +
`digest: SHA-256=${Buffer.from(new Uint8Array(digest)).toString(
"base64",
)}\n`;
const signature = await crypto.subtle.sign(
"Ed25519",
this.privateKey,
new TextEncoder().encode(
`(request-target): ${requestOrMethod.toLowerCase()} ${
url.pathname
}\n` +
`host: ${url.host}\n` +
`date: ${date}\n` +
`digest: SHA-256=${Buffer.from(
new Uint8Array(digest),
).toString("base64")}\n`,
),
new TextEncoder().encode(signedString),
);
const signatureBase64 = Buffer.from(new Uint8Array(signature)).toString(
"base64",
);
headers.set("Date", date);
headers.set("Date", finalDate);
headers.set(
"Signature",
`keyId="${this.keyId}",algorithm="ed25519",headers="(request-target) host date digest",signature="${signatureBase64}"`,
);
return headers;
return {
headers,
signedString,
};
}
}