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" }); body = JSON.stringify({ key: "value" });
const headers = await new SignatureConstructor( const { headers } = await new SignatureConstructor(
privateKey, privateKey,
"https://bob.org/users/6a18f2c3-120e-4949-bda4-2aa4c8264d51", "https://bob.org/users/6a18f2c3-120e-4949-bda4-2aa4c8264d51",
).sign("GET", new URL("https://example.com"), body); ).sign("GET", new URL("https://example.com"), body);
@ -157,7 +157,7 @@ describe("SignatureConstructor", () => {
describe("Signing", () => { describe("Signing", () => {
test("should correctly sign ", async () => { test("should correctly sign ", async () => {
const url = new URL("https://example.com"); 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("Signature")).toBeDefined();
expect(headers.get("Date")).toBeDefined(); expect(headers.get("Date")).toBeDefined();
@ -187,7 +187,7 @@ describe("SignatureConstructor", () => {
method: "GET", method: "GET",
body: body, body: body,
}); });
const newRequest = await ctor.sign(request); const { request: newRequest } = await ctor.sign(request);
headers = newRequest.headers; headers = newRequest.headers;
expect(headers.get("Signature")).toBeDefined(); expect(headers.get("Signature")).toBeDefined();
@ -195,5 +195,12 @@ describe("SignatureConstructor", () => {
expect(await newRequest.text()).toBe(body); 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. * Signs a request.
* @param request The request object to sign. * @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 * @example
* const request = new Request(); * 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. * Signs a request.
@ -244,41 +247,59 @@ export class SignatureConstructor {
* @param url The URL object. * @param url The URL object.
* @param body The request body. * @param body The request body.
* @param headers The request headers. * @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. * @throws TypeError if any required parameters are missing or empty.
* @example * @example
* const method = "GET"; * const method = "GET";
* const url = new URL("https://example.com"); * const url = new URL("https://example.com");
* const body = "request body"; * const body = "request body";
* const signedHeaders = await constructor.sign(method, url, body); * const { headers: signedHeaders } = await constructor.sign(method, url, body);
*/ */
async sign( async sign(
method: HttpVerb, method: HttpVerb,
url: URL, url: URL,
body: string, body: string,
headers?: Headers, headers?: Headers,
): Promise<Headers>; date?: Date,
): Promise<{
headers: Headers;
signedString: string;
}>;
async sign( async sign(
requestOrMethod: Request | HttpVerb, requestOrMethod: Request | HttpVerb,
url?: URL, url?: URL,
body?: string, body?: string,
headers: Headers = new Headers(), headers: Headers = new Headers(),
): Promise<Request | Headers> { date?: Date,
): Promise<
| {
headers: Headers;
signedString: string;
}
| {
request: Request;
signedString: string;
}
> {
if (requestOrMethod instanceof Request) { if (requestOrMethod instanceof Request) {
const request = requestOrMethod.clone(); const request = requestOrMethod.clone();
const headers = await this.sign( const { headers, signedString } = await this.sign(
requestOrMethod.method as HttpVerb, requestOrMethod.method as HttpVerb,
new URL(requestOrMethod.url), new URL(requestOrMethod.url),
await requestOrMethod.text(), await requestOrMethod.text(),
requestOrMethod.headers, requestOrMethod.headers,
requestOrMethod.headers.get("Date")
? new Date(requestOrMethod.headers.get("Date") ?? "")
: undefined,
); );
request.headers.set("Date", headers.get("Date") ?? ""); request.headers.set("Date", headers.get("Date") ?? "");
request.headers.set("Signature", headers.get("Signature") ?? ""); request.headers.set("Signature", headers.get("Signature") ?? "");
return request; return { request, signedString };
} }
if (!url || !body || !headers) { 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( const digest = await crypto.subtle.digest(
"SHA-256", "SHA-256",
new TextEncoder().encode(body), 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( const signature = await crypto.subtle.sign(
"Ed25519", "Ed25519",
this.privateKey, this.privateKey,
new TextEncoder().encode( new TextEncoder().encode(signedString),
`(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`,
),
); );
const signatureBase64 = Buffer.from(new Uint8Array(signature)).toString( const signatureBase64 = Buffer.from(new Uint8Array(signature)).toString(
"base64", "base64",
); );
headers.set("Date", date); headers.set("Date", finalDate);
headers.set( headers.set(
"Signature", "Signature",
`keyId="${this.keyId}",algorithm="ed25519",headers="(request-target) host date digest",signature="${signatureBase64}"`, `keyId="${this.keyId}",algorithm="ed25519",headers="(request-target) host date digest",signature="${signatureBase64}"`,
); );
return headers; return {
headers,
signedString,
};
} }
} }