mirror of
https://github.com/versia-pub/api.git
synced 2025-12-06 08:28:19 +01:00
feat(federation): 👽 Update cryptography code to Versia 0.5
This commit is contained in:
parent
bac34b3f39
commit
afec384a51
|
|
@ -7,7 +7,7 @@ describe("SignatureValidator", () => {
|
|||
let publicKey: CryptoKey;
|
||||
let body: string;
|
||||
let signature: string;
|
||||
let nonce: string;
|
||||
let timestamp: Date;
|
||||
|
||||
beforeAll(async () => {
|
||||
const keys = await crypto.subtle.generateKey("Ed25519", true, [
|
||||
|
|
@ -25,8 +25,8 @@ describe("SignatureValidator", () => {
|
|||
"https://bob.org/users/6a18f2c3-120e-4949-bda4-2aa4c8264d51",
|
||||
).sign("GET", new URL("https://example.com"), body);
|
||||
|
||||
signature = headers.get("X-Signature") ?? "";
|
||||
nonce = headers.get("X-Nonce") ?? "";
|
||||
signature = headers.get("Versia-Signature") ?? "";
|
||||
timestamp = new Date(Number(headers.get("Versia-Signed-At")) * 1000);
|
||||
});
|
||||
|
||||
test("fromStringKey", async () => {
|
||||
|
|
@ -46,8 +46,8 @@ describe("SignatureValidator", () => {
|
|||
const request = new Request("https://example.com", {
|
||||
method: "GET",
|
||||
headers: {
|
||||
"X-Signature": signature,
|
||||
"X-Nonce": nonce,
|
||||
"Versia-Signature": signature,
|
||||
"Versia-Signed-At": String(timestamp.getTime() / 1000),
|
||||
},
|
||||
body: body,
|
||||
});
|
||||
|
|
@ -59,8 +59,8 @@ describe("SignatureValidator", () => {
|
|||
const request = new Request("https://example.com", {
|
||||
method: "GET",
|
||||
headers: {
|
||||
"X-Signature": "invalid",
|
||||
"X-Nonce": nonce,
|
||||
"Versia-Signature": "invalid",
|
||||
"Versia-Signed-At": String(timestamp.getTime() / 1000),
|
||||
},
|
||||
body: body,
|
||||
});
|
||||
|
|
@ -70,16 +70,16 @@ describe("SignatureValidator", () => {
|
|||
expect(isValid).toBe(false);
|
||||
});
|
||||
|
||||
test("should throw with missing nonce", async () => {
|
||||
test("should throw with missing timestamp", async () => {
|
||||
const request = new Request("https://example.com", {
|
||||
method: "GET",
|
||||
headers: {
|
||||
"X-Signature": signature,
|
||||
"Versia-Signature": signature,
|
||||
},
|
||||
body: body,
|
||||
});
|
||||
expect(() => validator.validate(request)).toThrow(
|
||||
"Headers are missing in request: X-Nonce",
|
||||
"Headers are missing in request: Versia-Signed-At",
|
||||
);
|
||||
});
|
||||
|
||||
|
|
@ -87,8 +87,8 @@ describe("SignatureValidator", () => {
|
|||
const request = new Request("https://example.com", {
|
||||
method: "GET",
|
||||
headers: {
|
||||
"X-Signature": signature,
|
||||
"X-Nonce": nonce,
|
||||
"Versia-Signature": signature,
|
||||
"Versia-Signed-At": String(timestamp.getTime() / 1000),
|
||||
},
|
||||
body: "different",
|
||||
});
|
||||
|
|
@ -101,8 +101,8 @@ describe("SignatureValidator", () => {
|
|||
const request = new Request("https://example.com", {
|
||||
method: "GET",
|
||||
headers: {
|
||||
"X-Signature": "thisIsNotbase64OhNo$^ù",
|
||||
"X-Nonce": nonce,
|
||||
"Versia-Signature": "thisIsNotbase64OhNo$^ù",
|
||||
"Versia-Signed-At": String(timestamp.getTime() / 1000),
|
||||
},
|
||||
body: body,
|
||||
});
|
||||
|
|
@ -151,11 +151,11 @@ describe("SignatureConstructor", () => {
|
|||
test("should correctly sign ", async () => {
|
||||
const url = new URL("https://example.com");
|
||||
headers = (await ctor.sign("GET", url, body)).headers;
|
||||
expect(headers.get("X-Signature")).toBeDefined();
|
||||
expect(headers.get("X-Nonce")).toBeDefined();
|
||||
expect(headers.get("Versia-Signature")).toBeDefined();
|
||||
expect(headers.get("Versia-Signed-At")).toBeDefined();
|
||||
|
||||
expect(headers.get("X-Nonce")?.length).toBeGreaterThan(10);
|
||||
expect(headers.get("X-Signature")?.length).toBeGreaterThan(10);
|
||||
expect(headers.get("Versia-Signed-At")?.length).toBeGreaterThan(10);
|
||||
expect(headers.get("Versia-Signature")?.length).toBeGreaterThan(10);
|
||||
});
|
||||
|
||||
test("should correctly sign a Request", async () => {
|
||||
|
|
@ -167,8 +167,8 @@ describe("SignatureConstructor", () => {
|
|||
const { request: newRequest } = await ctor.sign(request);
|
||||
|
||||
headers = newRequest.headers;
|
||||
expect(headers.get("X-Signature")).toBeDefined();
|
||||
expect(headers.get("X-Nonce")).toBeDefined();
|
||||
expect(headers.get("Versia-Signature")).toBeDefined();
|
||||
expect(headers.get("Versia-Signed-At")).toBeDefined();
|
||||
|
||||
expect(await newRequest.text()).toBe(body);
|
||||
});
|
||||
|
|
|
|||
|
|
@ -16,9 +16,6 @@ const base64ToArrayBuffer = (base64: string) =>
|
|||
const arrayBufferToBase64 = (arrayBuffer: ArrayBuffer) =>
|
||||
btoa(String.fromCharCode(...new Uint8Array(arrayBuffer)));
|
||||
|
||||
const uint8ArrayToBase64 = (uint8Array: Uint8Array) =>
|
||||
btoa(String.fromCharCode(...uint8Array));
|
||||
|
||||
const checkEvironmentSupport = () => {
|
||||
// Check if WebCrypto is supported
|
||||
if (!globalThis.crypto?.subtle) {
|
||||
|
|
@ -78,7 +75,7 @@ export class SignatureValidator {
|
|||
/**
|
||||
* Validates the signature of a request.
|
||||
* @param signature The signature string.
|
||||
* @param nonce Signature nonce.
|
||||
* @param timestamp Signature timestamp.
|
||||
* @param method The HTTP verb.
|
||||
* @param url The URL object.
|
||||
* @param body The request body.
|
||||
|
|
@ -86,15 +83,15 @@ export class SignatureValidator {
|
|||
* @throws TypeError if any required parameters are missing or empty.
|
||||
* @example
|
||||
* const signature = "k4QNt5Grl40KK8orIdiaq118Z+P5pa6vIeArq55wsvfL7wNy4cE3f2fhsGcpZql+PStm+x2ZjZIhudrAC/32Cg==";
|
||||
* const nonce = "bJzyhTNK2RXUCetKIpm0Fw==";
|
||||
* const date = new Date(1549312452000)
|
||||
* const method = "GET";
|
||||
* const url = new URL("https://example.com/users/ff54ee40-2ce9-4d2e-86ac-3cd06a1e1480");
|
||||
* const body = "{ ... }";
|
||||
* const isValid = await validator.validate(signature, nonce, method, url, body);
|
||||
* const isValid = await validator.validate(signature, date, method, url, body);
|
||||
*/
|
||||
async validate(
|
||||
signature: string,
|
||||
nonce: string,
|
||||
timestamp: Date,
|
||||
method: HttpVerb,
|
||||
url: URL,
|
||||
body: string,
|
||||
|
|
@ -102,25 +99,28 @@ export class SignatureValidator {
|
|||
|
||||
async validate(
|
||||
requestOrSignature: Request | string,
|
||||
nonce?: string,
|
||||
timestamp?: Date,
|
||||
method?: HttpVerb,
|
||||
url?: URL,
|
||||
body?: string,
|
||||
): Promise<boolean> {
|
||||
if (requestOrSignature instanceof Request) {
|
||||
const signature = requestOrSignature.headers.get("X-Signature");
|
||||
const nonce = requestOrSignature.headers.get("X-Nonce");
|
||||
const signature =
|
||||
requestOrSignature.headers.get("Versia-Signature");
|
||||
const timestampHeader =
|
||||
requestOrSignature.headers.get("Versia-Signed-At");
|
||||
const timestamp = new Date(Number(timestampHeader) * 1000);
|
||||
const url = new URL(requestOrSignature.url);
|
||||
const body = await requestOrSignature.text();
|
||||
const method = requestOrSignature.method as HttpVerb;
|
||||
|
||||
const missingHeaders = [
|
||||
!signature && "X-Signature",
|
||||
!nonce && "X-Nonce",
|
||||
!signature && "Versia-Signature",
|
||||
!timestampHeader && "Versia-Signed-At",
|
||||
].filter(Boolean);
|
||||
|
||||
// Check if all headers are present
|
||||
if (!(signature && nonce && method && url && body)) {
|
||||
if (!(signature && timestampHeader && method && url && body)) {
|
||||
// Say which headers are missing
|
||||
throw new TypeError(
|
||||
`Headers are missing in request: ${missingHeaders.join(
|
||||
|
|
@ -129,12 +129,12 @@ export class SignatureValidator {
|
|||
);
|
||||
}
|
||||
|
||||
return this.validate(signature, nonce, method, url, body);
|
||||
return this.validate(signature, timestamp, method, url, body);
|
||||
}
|
||||
|
||||
if (!(nonce && method && url && body)) {
|
||||
if (!(timestamp && method && url && body)) {
|
||||
throw new TypeError(
|
||||
"Missing or empty required parameters: nonce, method, url or body",
|
||||
"Missing or empty required parameters: timestamp, method, url or body",
|
||||
);
|
||||
}
|
||||
|
||||
|
|
@ -152,7 +152,7 @@ export class SignatureValidator {
|
|||
new TextEncoder().encode(body),
|
||||
);
|
||||
|
||||
const expectedSignedString = `${method.toLowerCase()} ${encodeURIComponent(url.pathname)} ${nonce} ${arrayBufferToBase64(digest)}`;
|
||||
const expectedSignedString = `${method.toLowerCase()} ${encodeURIComponent(url.pathname)} ${timestamp.getTime() / 1000} ${arrayBufferToBase64(digest)}`;
|
||||
|
||||
// Check if signed string is valid
|
||||
const isValid = await crypto.subtle.verify(
|
||||
|
|
@ -232,7 +232,7 @@ export class SignatureConstructor {
|
|||
* @param url The URL object.
|
||||
* @param body The request body.
|
||||
* @param headers The request headers.
|
||||
* @param nonce The signature nonce (optional).
|
||||
* @param timestamp The signature timestamp (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
|
||||
|
|
@ -246,7 +246,7 @@ export class SignatureConstructor {
|
|||
url: URL,
|
||||
body?: string,
|
||||
headers?: Headers,
|
||||
nonce?: string,
|
||||
timestamp?: Date,
|
||||
): Promise<{
|
||||
headers: Headers;
|
||||
signedString: string;
|
||||
|
|
@ -257,7 +257,7 @@ export class SignatureConstructor {
|
|||
url?: URL,
|
||||
body?: string,
|
||||
headers: Headers = new Headers(),
|
||||
nonce?: string,
|
||||
timestamp?: Date,
|
||||
): Promise<
|
||||
| {
|
||||
headers: Headers;
|
||||
|
|
@ -270,19 +270,23 @@ export class SignatureConstructor {
|
|||
> {
|
||||
if (requestOrMethod instanceof Request) {
|
||||
const request = requestOrMethod.clone();
|
||||
const signedAt = requestOrMethod.headers.get("Versia-Signed-At");
|
||||
|
||||
const { headers, signedString } = await this.sign(
|
||||
requestOrMethod.method as HttpVerb,
|
||||
new URL(requestOrMethod.url),
|
||||
await requestOrMethod.text(),
|
||||
requestOrMethod.headers,
|
||||
requestOrMethod.headers.get("X-Nonce") ?? undefined,
|
||||
signedAt ? new Date(Number(signedAt) * 1000) : undefined,
|
||||
);
|
||||
|
||||
request.headers.set("X-Nonce", headers.get("X-Nonce") ?? "");
|
||||
request.headers.set(
|
||||
"X-Signature",
|
||||
headers.get("X-Signature") ?? "",
|
||||
"Versia-Signed-At",
|
||||
headers.get("Versia-Signed-At") ?? "",
|
||||
);
|
||||
request.headers.set(
|
||||
"Versia-Signature",
|
||||
headers.get("Versia-Signature") ?? "",
|
||||
);
|
||||
|
||||
return { request, signedString };
|
||||
|
|
@ -294,9 +298,7 @@ export class SignatureConstructor {
|
|||
);
|
||||
}
|
||||
|
||||
const finalNonce =
|
||||
nonce ||
|
||||
uint8ArrayToBase64(crypto.getRandomValues(new Uint8Array(16)));
|
||||
const finalTimestamp = timestamp || new Date();
|
||||
|
||||
const digest = await crypto.subtle.digest(
|
||||
"SHA-256",
|
||||
|
|
@ -305,7 +307,7 @@ export class SignatureConstructor {
|
|||
|
||||
const signedString = `${requestOrMethod.toLowerCase()} ${encodeURIComponent(
|
||||
url.pathname,
|
||||
)} ${finalNonce} ${arrayBufferToBase64(digest)}`;
|
||||
)} ${finalTimestamp.getTime() / 1000} ${arrayBufferToBase64(digest)}`;
|
||||
|
||||
const signature = await crypto.subtle.sign(
|
||||
"Ed25519",
|
||||
|
|
@ -315,9 +317,12 @@ export class SignatureConstructor {
|
|||
|
||||
const signatureBase64 = arrayBufferToBase64(signature);
|
||||
|
||||
headers.set("X-Nonce", finalNonce);
|
||||
headers.set("X-Signature", signatureBase64);
|
||||
headers.set("X-Signed-By", this.authorUri.toString());
|
||||
headers.set(
|
||||
"Versia-Signed-At",
|
||||
String(finalTimestamp.getTime() / 1000),
|
||||
);
|
||||
headers.set("Versia-Signature", signatureBase64);
|
||||
headers.set("Versia-Signed-By", this.authorUri.toString());
|
||||
|
||||
return {
|
||||
headers,
|
||||
|
|
|
|||
Loading…
Reference in a new issue