diff --git a/federation/cryptography/index.test.ts b/federation/cryptography/index.test.ts index e9056c7..a8fc226 100644 --- a/federation/cryptography/index.test.ts +++ b/federation/cryptography/index.test.ts @@ -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); + }); }); }); diff --git a/federation/cryptography/index.ts b/federation/cryptography/index.ts index 7a8d4d7..2add7c8 100644 --- a/federation/cryptography/index.ts +++ b/federation/cryptography/index.ts @@ -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; + 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; + date?: Date, + ): Promise<{ + headers: Headers; + signedString: string; + }>; async sign( requestOrMethod: Request | HttpVerb, url?: URL, body?: string, headers: Headers = new Headers(), - ): Promise { + 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, + }; } }