From 437ee2b89ba7c9b6f60ee6ad0766c8d5ff54122a Mon Sep 17 00:00:00 2001 From: tamaina Date: Fri, 1 Mar 2024 14:36:55 +0000 Subject: [PATCH] build --- dist/digest/digest-rfc3230.d.ts | 6 + dist/digest/digest.d.ts | 4 + dist/digest/utils.d.ts | 6 + dist/draft/parse.d.ts | 23 ++- dist/draft/sign.d.ts | 8 +- dist/draft/verify.d.ts | 4 +- dist/index.cjs | 291 +++++++++++++++++++++++++------- dist/index.d.ts | 46 +---- dist/index.mjs | 273 +++++++++++++++++++++++------- dist/parse.d.ts | 17 +- dist/types.d.ts | 7 +- dist/utils.d.ts | 10 +- 12 files changed, 528 insertions(+), 167 deletions(-) create mode 100644 dist/digest/digest-rfc3230.d.ts create mode 100644 dist/digest/digest.d.ts create mode 100644 dist/digest/utils.d.ts diff --git a/dist/digest/digest-rfc3230.d.ts b/dist/digest/digest-rfc3230.d.ts new file mode 100644 index 0000000..e0d1e1f --- /dev/null +++ b/dist/digest/digest-rfc3230.d.ts @@ -0,0 +1,6 @@ +/// +import { DigestHashAlgorithm, IncomingRequest } from '../types'; +import { BinaryLike } from 'node:crypto'; +export declare function genRFC3230DigestHeader(body: string, hashAlgorithm?: DigestHashAlgorithm): string; +export declare const digestHeaderRegEx: RegExp; +export declare function verifyRFC3230DigestHeader(request: IncomingRequest, rawBody: BinaryLike, failOnNoDigest?: boolean, errorLogger?: ((message: any) => any)): boolean; diff --git a/dist/digest/digest.d.ts b/dist/digest/digest.d.ts new file mode 100644 index 0000000..cab9202 --- /dev/null +++ b/dist/digest/digest.d.ts @@ -0,0 +1,4 @@ +/// +import { IncomingRequest } from "src/types"; +import { BinaryLike } from "node:crypto"; +export declare function verifyDigestHeader(request: IncomingRequest, rawBody: BinaryLike, failOnNoDigest?: boolean, errorLogger?: ((message: any) => any)): boolean; diff --git a/dist/digest/utils.d.ts b/dist/digest/utils.d.ts new file mode 100644 index 0000000..c45fba8 --- /dev/null +++ b/dist/digest/utils.d.ts @@ -0,0 +1,6 @@ +/// +import { BinaryLike } from 'node:crypto'; +import { DigestHashAlgorithm } from '../types'; +export declare function createBase64Digest(body: BinaryLike, hash: DigestHashAlgorithm): string; +export declare function createBase64Digest(body: BinaryLike, hash: Ks): Map; +export declare function createBase64Digest(body: BinaryLike): string; diff --git a/dist/draft/parse.d.ts b/dist/draft/parse.d.ts index cb8de83..6e3d412 100644 --- a/dist/draft/parse.d.ts +++ b/dist/draft/parse.d.ts @@ -1,7 +1,24 @@ import { RequestParseOptions } from "../parse.js"; -import type { DraftParsedSignature, IncomingRequest } from '../types.js'; +import type { ParsedDraftSignature, IncomingRequest } from '../types.js'; export declare class SignatureHeaderContentLackedError extends Error { constructor(lackedContent: string); } -export declare function parseDraftRequestSignatureHeader(signatureHeader: string): Record; -export declare function parseDraftRequest(request: IncomingRequest, options?: RequestParseOptions): DraftParsedSignature; +export declare class SignatureHeaderClockInvalidError extends Error { + constructor(prop: 'created' | 'expires'); +} +export declare const DraftSignatureHeaderKeys: readonly ["keyId", "algorithm", "created", "expires", "opaque", "headers", "signature"]; +export type DraftSignatureHeaderParsedRaw = { + [key in typeof DraftSignatureHeaderKeys[number]]?: string; +}; +export type DraftSignatureHeaderParsed = { + keyId: string; + algorithm: string; + signature: string; + headers: string[]; + created?: string; + expires?: string; + opaque?: string; +}; +export declare function parseDraftRequestSignatureHeader(signatureHeader: string): DraftSignatureHeaderParsedRaw; +export declare function validateAndProcessParsedDraftSignatureHeader(parsed: DraftSignatureHeaderParsedRaw, options?: RequestParseOptions): DraftSignatureHeaderParsed; +export declare function parseDraftRequest(request: IncomingRequest, options?: RequestParseOptions): ParsedDraftSignature; diff --git a/dist/draft/sign.d.ts b/dist/draft/sign.d.ts index eab5bbf..9b43056 100644 --- a/dist/draft/sign.d.ts +++ b/dist/draft/sign.d.ts @@ -1,5 +1,11 @@ import type { PrivateKey, RequestLike, SignatureAlgorithm, SignatureHashAlgorithm } from '../types.js'; -export declare function genDraftSigningString(request: RequestLike, includeHeaders: string[]): string; +export declare function genDraftSigningString(request: RequestLike, includeHeaders: string[], additional?: { + keyId: string; + algorithm: string; + created?: string; + expires?: string; + opaque?: string; +}): string; export declare function genDraftSignature(signingString: string, privateKey: string, hashAlgorithm: SignatureHashAlgorithm | null): string; export declare function genDraftAuthorizationHeader(includeHeaders: string[], keyId: string, signature: string, hashAlgorithm?: SignatureAlgorithm): string; export declare function genDraftSignatureHeader(includeHeaders: string[], keyId: string, signature: string, algorithm: string): string; diff --git a/dist/draft/verify.d.ts b/dist/draft/verify.d.ts index 97016b9..d901be2 100644 --- a/dist/draft/verify.d.ts +++ b/dist/draft/verify.d.ts @@ -1,2 +1,2 @@ -import type { DraftParsedSignature } from '../types.js'; -export declare function verifySignature(parsed: DraftParsedSignature['value'], publicKeyPem: string, errorLogger?: ((message: any) => any)): boolean; +import type { ParsedDraftSignature } from '../types.js'; +export declare function verifyDraftSignature(parsed: ParsedDraftSignature['value'], publicKeyPem: string, errorLogger?: ((message: any) => any)): boolean; diff --git a/dist/index.cjs b/dist/index.cjs index f728573..dbb920d 100644 --- a/dist/index.cjs +++ b/dist/index.cjs @@ -31,45 +31,47 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru var src_exports = {}; __export(src_exports, { ClockSkewInvalidError: () => ClockSkewInvalidError, - HttpSignatureDraft: () => HttpSignatureDraft, + DraftSignatureHeaderKeys: () => DraftSignatureHeaderKeys, InvalidRequestError: () => InvalidRequestError, RequestHasMultipleDateHeadersError: () => RequestHasMultipleDateHeadersError, RequestHasMultipleSignatureHeadersError: () => RequestHasMultipleSignatureHeadersError, + SignatureHeaderClockInvalidError: () => SignatureHeaderClockInvalidError, + SignatureHeaderContentLackedError: () => SignatureHeaderContentLackedError, SignatureHeaderNotFoundError: () => SignatureHeaderNotFoundError, SignatureMissmatchWithProvidedAlgorithmError: () => SignatureMissmatchWithProvidedAlgorithmError, checkClockSkew: () => checkClockSkew, detectAndVerifyAlgorithm: () => detectAndVerifyAlgorithm, + digestHeaderRegEx: () => digestHeaderRegEx, + genDraftAuthorizationHeader: () => genDraftAuthorizationHeader, + genDraftSignature: () => genDraftSignature, + genDraftSignatureHeader: () => genDraftSignatureHeader, + genDraftSigningString: () => genDraftSigningString, genEcKeyPair: () => genEcKeyPair, genEd25519KeyPair: () => genEd25519KeyPair, genEd448KeyPair: () => genEd448KeyPair, + genRFC3230DigestHeader: () => genRFC3230DigestHeader, genRsaKeyPair: () => genRsaKeyPair, getDraftAlgoString: () => getDraftAlgoString, + lcObjectGet: () => lcObjectGet, lcObjectKey: () => lcObjectKey, - parseRequest: () => parseRequest, + objectLcKeys: () => objectLcKeys, + parseDraftRequest: () => parseDraftRequest, + parseDraftRequestSignatureHeader: () => parseDraftRequestSignatureHeader, + parseRequestSignature: () => parseRequestSignature, prepareSignInfo: () => prepareSignInfo, + requestIsRFC9421: () => requestIsRFC9421, + signAsDraftToRequest: () => signAsDraftToRequest, signatureHeaderIsDraft: () => signatureHeaderIsDraft, toSpkiPublicKey: () => toSpkiPublicKey, - validateRequestAndGetSignatureHeader: () => validateRequestAndGetSignatureHeader + validateAndProcessParsedDraftSignatureHeader: () => validateAndProcessParsedDraftSignatureHeader, + validateRequestAndGetSignatureHeader: () => validateRequestAndGetSignatureHeader, + verifyDigestHeader: () => verifyDigestHeader, + verifyDraftSignature: () => verifyDraftSignature, + verifyRFC3230DigestHeader: () => verifyRFC3230DigestHeader }); module.exports = __toCommonJS(src_exports); -// src/draft/parse.ts -var parse_exports = {}; -__export(parse_exports, { - SignatureHeaderContentLackedError: () => SignatureHeaderContentLackedError, - parseDraftRequest: () => parseDraftRequest, - parseDraftRequestSignatureHeader: () => parseDraftRequestSignatureHeader -}); - // src/draft/sign.ts -var sign_exports = {}; -__export(sign_exports, { - genDraftAuthorizationHeader: () => genDraftAuthorizationHeader, - genDraftSignature: () => genDraftSignature, - genDraftSignatureHeader: () => genDraftSignatureHeader, - genDraftSigningString: () => genDraftSigningString, - signAsDraftToRequest: () => signAsDraftToRequest -}); var crypto2 = __toESM(require("node:crypto"), 1); // src/utils.ts @@ -127,19 +129,45 @@ function lcObjectKey(src) { return dst; }, {}); } +function lcObjectGet(src, key) { + key = key.toLowerCase(); + for (const [k, v] of Object.entries(src)) { + if (k.toLowerCase() === key) + return v; + } + return void 0; +} +function objectLcKeys(src) { + return Object.keys(src).reduce((dst, key) => { + if (key === "__proto__") + return dst; + dst.add(key.toLowerCase()); + return dst; + }, /* @__PURE__ */ new Set()); +} // src/draft/sign.ts -function genDraftSigningString(request, includeHeaders) { - request.headers = lcObjectKey(request.headers); +function genDraftSigningString(request, includeHeaders, additional) { + const headers = lcObjectKey(request.headers); const results = []; for (const key of includeHeaders.map((x) => x.toLowerCase())) { if (key === "(request-target)") { results.push(`(request-target): ${request.method.toLowerCase()} ${request.url.startsWith("/") ? request.url : new URL(request.url).pathname}`); + } else if (key === "(keyid)") { + results.push(`(keyid): ${additional?.keyId}`); + } else if (key === "(algorithm)") { + results.push(`(algorithm): ${additional?.algorithm}`); + } else if (key === "(created)") { + results.push(`(created): ${additional?.created}`); + } else if (key === "(expires)") { + results.push(`(expires): ${additional?.expires}`); + } else if (key === "(opaque)") { + results.push(`(opaque): ${additional?.opaque}`); } else { - if (key === "date" && !request.headers["date"] && request.headers["x-date"]) { - results.push(`date: ${request.headers["x-date"]}`); + if (key === "date" && !headers["date"] && headers["x-date"]) { + results.push(`date: ${headers["x-date"]}`); } else { - results.push(`${key}: ${request.headers[key]}`); + results.push(`${key}: ${headers[key]}`); } } } @@ -158,9 +186,10 @@ function genDraftSignatureHeader(includeHeaders, keyId, signature, algorithm) { function signAsDraftToRequest(request, key, includeHeaders, opts = {}) { const hashAlgorithm = opts?.hashAlgorithm || "sha256"; const signInfo = prepareSignInfo(key.privateKeyPem, hashAlgorithm); - const signingString = genDraftSigningString(request, includeHeaders); + const algoString = getDraftAlgoString(signInfo); + const signingString = genDraftSigningString(request, includeHeaders, { keyId: key.keyId, algorithm: algoString }); const signature = genDraftSignature(signingString, key.privateKeyPem, signInfo.hashAlg); - const signatureHeader = genDraftSignatureHeader(includeHeaders, key.keyId, signature, getDraftAlgoString(signInfo)); + const signatureHeader = genDraftSignatureHeader(includeHeaders, key.keyId, signature, algoString); Object.assign(request.headers, { Signature: signatureHeader }); @@ -177,6 +206,12 @@ var SignatureHeaderContentLackedError = class extends Error { super(`Signature header content lacked: ${lackedContent}`); } }; +var SignatureHeaderClockInvalidError = class extends Error { + constructor(prop) { + super(`Clock skew is invalid (${prop})`); + } +}; +var DraftSignatureHeaderKeys = ["keyId", "algorithm", "created", "expires", "opaque", "headers", "signature"]; function parseDraftRequestSignatureHeader(signatureHeader) { const result = {}; let prevStatus = "none"; @@ -237,26 +272,71 @@ function parseDraftRequestSignatureHeader(signatureHeader) { } return result; } -function validateAndProcessParsedDraftSignatureHeader(parsed, headers) { +function validateAndProcessParsedDraftSignatureHeader(parsed, options) { if (!parsed.keyId) throw new SignatureHeaderContentLackedError("keyId"); if (!parsed.algorithm) throw new SignatureHeaderContentLackedError("algorithm"); if (!parsed.signature) throw new SignatureHeaderContentLackedError("signature"); - if (!parsed.headers && !headers) + if (!parsed.headers) throw new SignatureHeaderContentLackedError("headers"); + const headersArray = parsed.headers.split(" "); + if (options?.requiredInputs?.draft) { + for (const requiredInput of options.requiredInputs.draft) { + if (requiredInput === "x-date" || requiredInput === "date") { + if (headersArray.includes("date")) + continue; + if (headersArray.includes("x-date")) + continue; + throw new SignatureHeaderContentLackedError(`headers.${requiredInput}`); + } + if (!headersArray.includes(requiredInput)) + throw new SignatureHeaderContentLackedError(`headers.${requiredInput}`); + } + } + if (parsed.created) { + const createdSec = parseInt(parsed.created); + if (isNaN(createdSec)) + throw new SignatureHeaderClockInvalidError("created"); + const nowTime = (options?.clockSkew?.now || /* @__PURE__ */ new Date()).getTime(); + if (createdSec * 1e3 > nowTime + (options?.clockSkew?.forward ?? 100)) { + throw new SignatureHeaderClockInvalidError("created"); + } + } + if (parsed.expires) { + const expiresSec = parseInt(parsed.expires); + if (isNaN(expiresSec)) + throw new SignatureHeaderClockInvalidError("expires"); + const nowTime = (options?.clockSkew?.now || /* @__PURE__ */ new Date()).getTime(); + if (expiresSec * 1e3 < nowTime - (options?.clockSkew?.forward ?? 100)) { + throw new SignatureHeaderClockInvalidError("expires"); + } + } return { keyId: parsed.keyId, algorithm: parsed.algorithm.toLowerCase(), signature: parsed.signature, - headers: parsed.headers ? parsed.headers.split(" ") : headers + headers: headersArray, + created: parsed.created, + expires: parsed.expires, + opaque: parsed.opaque }; } function parseDraftRequest(request, options) { const signatureHeader = validateRequestAndGetSignatureHeader(request, options?.clockSkew); - const parsedSignatureHeader = validateAndProcessParsedDraftSignatureHeader(parseDraftRequestSignatureHeader(signatureHeader), options?.headers); - const signingString = genDraftSigningString(request, parsedSignatureHeader.headers); + const parsedSignatureHeader = validateAndProcessParsedDraftSignatureHeader(parseDraftRequestSignatureHeader(signatureHeader), options); + const signingString = genDraftSigningString( + request, + parsedSignatureHeader.headers, + { + keyId: parsedSignatureHeader.keyId, + algorithm: parsedSignatureHeader.algorithm, + created: parsedSignatureHeader.created, + expires: parsedSignatureHeader.expires, + opaque: parsedSignatureHeader.opaque + } + ); return { version: "draft", value: { @@ -298,6 +378,9 @@ var ClockSkewInvalidError = class extends Error { function signatureHeaderIsDraft(signatureHeader) { return signatureHeader.includes('signature="'); } +function requestIsRFC9421(request) { + return objectLcKeys(request.headers).has("signature-input"); +} function checkClockSkew(reqDate, nowDate, delay = 300 * 1e3, forward = 100) { const reqTime = reqDate.getTime(); const nowTime = nowDate.getTime(); @@ -309,19 +392,20 @@ function checkClockSkew(reqDate, nowDate, delay = 300 * 1e3, forward = 100) { function validateRequestAndGetSignatureHeader(request, clock) { if (!request.headers) throw new SignatureHeaderNotFoundError(); - const signatureHeader = request.headers["signature"] || request.headers["Signature"]; + const headers = lcObjectKey(request.headers); + const signatureHeader = headers["signature"]; if (!signatureHeader) throw new SignatureHeaderNotFoundError(); if (Array.isArray(signatureHeader)) throw new RequestHasMultipleSignatureHeadersError(); - if (request.headers["date"]) { - if (Array.isArray(request.headers["date"])) + if (headers["date"]) { + if (Array.isArray(headers["date"])) throw new RequestHasMultipleDateHeadersError(); - checkClockSkew(new Date(request.headers["date"]), clock?.now || /* @__PURE__ */ new Date(), clock?.delay, clock?.forward); - } else if (request.headers["x-date"]) { - if (Array.isArray(request.headers["x-date"])) + checkClockSkew(new Date(headers["date"]), clock?.now || /* @__PURE__ */ new Date(), clock?.delay, clock?.forward); + } else if (headers["x-date"]) { + if (Array.isArray(headers["x-date"])) throw new RequestHasMultipleDateHeadersError(); - checkClockSkew(new Date(request.headers["x-date"]), clock?.now || /* @__PURE__ */ new Date(), clock?.delay, clock?.forward); + checkClockSkew(new Date(headers["x-date"]), clock?.now || /* @__PURE__ */ new Date(), clock?.delay, clock?.forward); } if (!request.method) throw new InvalidRequestError("Request method not found"); @@ -329,13 +413,14 @@ function validateRequestAndGetSignatureHeader(request, clock) { throw new InvalidRequestError("Request URL not found"); return signatureHeader; } -function parseRequest(request, options) { +function parseRequestSignature(request, options) { const signatureHeader = validateRequestAndGetSignatureHeader(request, options?.clockSkew); - if (signatureHeaderIsDraft(signatureHeader)) { - return parseDraftRequest(request, options); - } else { + if (requestIsRFC9421(request)) { throw new Error("Not implemented"); + } else if (signatureHeaderIsDraft(signatureHeader)) { + return parseDraftRequest(request, options); } + return null; } // src/keypair.ts @@ -407,6 +492,91 @@ function toSpkiPublicKey(publicKey) { }); } +// src/digest/utils.ts +var import_node_crypto = require("node:crypto"); +function createBase64Digest(body, hash = "sha256") { + if (Array.isArray(hash)) { + return new Map(hash.map((h) => [h, createBase64Digest(body, h)])); + } + return (0, import_node_crypto.createHash)(hash).update(body).digest("base64"); +} + +// src/digest/digest-rfc3230.ts +var digestHashAlgosForEncoding = { + "sha1": "SHA", + "sha256": "SHA-256", + "sha384": "SHA-384", + "sha512": "SHA-512", + "md5": "MD5" +}; +var digestHashAlgosForDecoding = { + "SHA": "sha1", + "SHA-1": "sha1", + "SHA-256": "sha256", + "SHA-384": "sha384", + "SHA-512": "sha512", + "MD5": "md5" +}; +function genRFC3230DigestHeader(body, hashAlgorithm = "sha256") { + return `${digestHashAlgosForEncoding[hashAlgorithm]}=${createBase64Digest(body, hashAlgorithm)}`; +} +var digestHeaderRegEx = /^([a-zA-Z0-9\-]+)=([^\,]+)/; +function verifyRFC3230DigestHeader(request, rawBody, failOnNoDigest = true, errorLogger) { + let digestHeader = lcObjectGet(request.headers, "digest"); + if (!digestHeader) { + if (failOnNoDigest) { + if (errorLogger) + errorLogger("Digest header not found"); + return false; + } + return true; + } + if (Array.isArray(digestHeader)) { + digestHeader = digestHeader[0]; + } + const match = digestHeader.match(digestHeaderRegEx); + if (!match) { + if (errorLogger) + errorLogger("Invalid Digest header format"); + return false; + } + const value = match[2]; + if (!value) { + if (errorLogger) + errorLogger("Invalid Digest header format"); + return false; + } + const algo = digestHashAlgosForDecoding[match[1].toUpperCase()]; + if (!algo) { + if (errorLogger) + errorLogger(`Invalid Digest header algorithm: ${match[1]}`); + return false; + } + const hash = createBase64Digest(rawBody, algo); + if (hash !== value) { + if (errorLogger) + errorLogger(`Digest header hash mismatch`); + return false; + } + return true; +} + +// src/digest/digest.ts +function verifyDigestHeader(request, rawBody, failOnNoDigest = true, errorLogger) { + const headerKeys = objectLcKeys(request.headers); + if (headerKeys.has("content-digest")) { + throw new Error("Not implemented yet"); + } else if (headerKeys.has("digest")) { + return verifyRFC3230DigestHeader(request, rawBody, failOnNoDigest, errorLogger); + } + if (failOnNoDigest) { + if (errorLogger) + errorLogger("Content-Digest or Digest header not found"); + return false; + } + return true; +} + // src/shared/verify.ts var SignatureMissmatchWithProvidedAlgorithmError = class extends Error { constructor(providedAlgorithm, detectedAlgorithm, realKeyType) { @@ -456,12 +626,8 @@ function detectAndVerifyAlgorithm(algorithm, publicKey) { } // src/draft/verify.ts -var verify_exports = {}; -__export(verify_exports, { - verifySignature: () => verifySignature -}); var crypto4 = __toESM(require("node:crypto"), 1); -function verifySignature(parsed, publicKeyPem, errorLogger) { +function verifyDraftSignature(parsed, publicKeyPem, errorLogger) { const publicKey = crypto4.createPublicKey(publicKeyPem); try { const detected = detectAndVerifyAlgorithm(parsed.params.algorithm, publicKey); @@ -472,33 +638,44 @@ function verifySignature(parsed, publicKeyPem, errorLogger) { return false; } } - -// src/index.ts -var HttpSignatureDraft = { - ...parse_exports, - ...sign_exports, - ...verify_exports -}; // Annotate the CommonJS export names for ESM import in node: 0 && (module.exports = { ClockSkewInvalidError, - HttpSignatureDraft, + DraftSignatureHeaderKeys, InvalidRequestError, RequestHasMultipleDateHeadersError, RequestHasMultipleSignatureHeadersError, + SignatureHeaderClockInvalidError, + SignatureHeaderContentLackedError, SignatureHeaderNotFoundError, SignatureMissmatchWithProvidedAlgorithmError, checkClockSkew, detectAndVerifyAlgorithm, + digestHeaderRegEx, + genDraftAuthorizationHeader, + genDraftSignature, + genDraftSignatureHeader, + genDraftSigningString, genEcKeyPair, genEd25519KeyPair, genEd448KeyPair, + genRFC3230DigestHeader, genRsaKeyPair, getDraftAlgoString, + lcObjectGet, lcObjectKey, - parseRequest, + objectLcKeys, + parseDraftRequest, + parseDraftRequestSignatureHeader, + parseRequestSignature, prepareSignInfo, + requestIsRFC9421, + signAsDraftToRequest, signatureHeaderIsDraft, toSpkiPublicKey, - validateRequestAndGetSignatureHeader + validateAndProcessParsedDraftSignatureHeader, + validateRequestAndGetSignatureHeader, + verifyDigestHeader, + verifyDraftSignature, + verifyRFC3230DigestHeader }); diff --git a/dist/index.d.ts b/dist/index.d.ts index ece2da8..d1d667f 100644 --- a/dist/index.d.ts +++ b/dist/index.d.ts @@ -2,44 +2,14 @@ export type * from './types.js'; export * from './parse.js'; export * from './utils.js'; export * from './keypair.js'; +export * from './digest/digest.js'; +export * from './digest/digest-rfc3230.js'; export * from './shared/verify.js'; -import * as draftParse from './draft/parse.js'; -export declare const HttpSignatureDraft: { - verifySignature(parsed: { - scheme: "Signature"; - params: { - keyId: string; - algorithm?: string | undefined; - headers: string[]; - signature: string; - }; - signingString: string; - algorithm?: string | undefined; - keyId: string; - }, publicKeyPem: string, errorLogger?: ((message: any) => any) | undefined): boolean; - genDraftSigningString(request: import("./types.js").RequestLike, includeHeaders: string[]): string; - genDraftSignature(signingString: string, privateKey: string, hashAlgorithm: import("./types.js").SignatureHashAlgorithm): string; - genDraftAuthorizationHeader(includeHeaders: string[], keyId: string, signature: string, hashAlgorithm?: import("./types.js").SignatureAlgorithm): string; - genDraftSignatureHeader(includeHeaders: string[], keyId: string, signature: string, algorithm: string): string; - signAsDraftToRequest(request: import("./types.js").RequestLike, key: import("./types.js").PrivateKey, includeHeaders: string[], opts?: { - hashAlgorithm?: import("./types.js").SignatureHashAlgorithm | undefined; - }): { - signingString: string; - signature: string; - signatureHeader: string; - }; - parseDraftRequestSignatureHeader(signatureHeader: string): Record; - parseDraftRequest(request: import("./types.js").IncomingRequest, options?: import("./parse.js").RequestParseOptions | undefined): import("./types.js").DraftParsedSignature; - SignatureHeaderContentLackedError: typeof draftParse.SignatureHeaderContentLackedError; -}; +export * from './draft/parse.js'; +export * from './draft/sign.js'; +export * from './draft/verify.js'; /** -import * as rfc9421Parse from './rfc9421/parse.js'; -import * as rfc9421Sign from './rfc9421/sign.js'; -import * as rfc9421Verify from './rfc9421/verify.js'; - -export const RFC9421 = { - ...rfc9421Parse, - ...rfc9421Sign, - ...rfc9421Verify, -}; +export * from './rfc9421/parse.js'; +export * from './rfc9421/sign.js'; +export * from './rfc9421/verify.js'; */ diff --git a/dist/index.mjs b/dist/index.mjs index a0dfeff..0d7b30c 100644 --- a/dist/index.mjs +++ b/dist/index.mjs @@ -1,26 +1,4 @@ -var __defProp = Object.defineProperty; -var __export = (target, all) => { - for (var name in all) - __defProp(target, name, { get: all[name], enumerable: true }); -}; - -// src/draft/parse.ts -var parse_exports = {}; -__export(parse_exports, { - SignatureHeaderContentLackedError: () => SignatureHeaderContentLackedError, - parseDraftRequest: () => parseDraftRequest, - parseDraftRequestSignatureHeader: () => parseDraftRequestSignatureHeader -}); - // src/draft/sign.ts -var sign_exports = {}; -__export(sign_exports, { - genDraftAuthorizationHeader: () => genDraftAuthorizationHeader, - genDraftSignature: () => genDraftSignature, - genDraftSignatureHeader: () => genDraftSignatureHeader, - genDraftSigningString: () => genDraftSigningString, - signAsDraftToRequest: () => signAsDraftToRequest -}); import * as crypto2 from "node:crypto"; // src/utils.ts @@ -78,19 +56,45 @@ function lcObjectKey(src) { return dst; }, {}); } +function lcObjectGet(src, key) { + key = key.toLowerCase(); + for (const [k, v] of Object.entries(src)) { + if (k.toLowerCase() === key) + return v; + } + return void 0; +} +function objectLcKeys(src) { + return Object.keys(src).reduce((dst, key) => { + if (key === "__proto__") + return dst; + dst.add(key.toLowerCase()); + return dst; + }, /* @__PURE__ */ new Set()); +} // src/draft/sign.ts -function genDraftSigningString(request, includeHeaders) { - request.headers = lcObjectKey(request.headers); +function genDraftSigningString(request, includeHeaders, additional) { + const headers = lcObjectKey(request.headers); const results = []; for (const key of includeHeaders.map((x) => x.toLowerCase())) { if (key === "(request-target)") { results.push(`(request-target): ${request.method.toLowerCase()} ${request.url.startsWith("/") ? request.url : new URL(request.url).pathname}`); + } else if (key === "(keyid)") { + results.push(`(keyid): ${additional?.keyId}`); + } else if (key === "(algorithm)") { + results.push(`(algorithm): ${additional?.algorithm}`); + } else if (key === "(created)") { + results.push(`(created): ${additional?.created}`); + } else if (key === "(expires)") { + results.push(`(expires): ${additional?.expires}`); + } else if (key === "(opaque)") { + results.push(`(opaque): ${additional?.opaque}`); } else { - if (key === "date" && !request.headers["date"] && request.headers["x-date"]) { - results.push(`date: ${request.headers["x-date"]}`); + if (key === "date" && !headers["date"] && headers["x-date"]) { + results.push(`date: ${headers["x-date"]}`); } else { - results.push(`${key}: ${request.headers[key]}`); + results.push(`${key}: ${headers[key]}`); } } } @@ -109,9 +113,10 @@ function genDraftSignatureHeader(includeHeaders, keyId, signature, algorithm) { function signAsDraftToRequest(request, key, includeHeaders, opts = {}) { const hashAlgorithm = opts?.hashAlgorithm || "sha256"; const signInfo = prepareSignInfo(key.privateKeyPem, hashAlgorithm); - const signingString = genDraftSigningString(request, includeHeaders); + const algoString = getDraftAlgoString(signInfo); + const signingString = genDraftSigningString(request, includeHeaders, { keyId: key.keyId, algorithm: algoString }); const signature = genDraftSignature(signingString, key.privateKeyPem, signInfo.hashAlg); - const signatureHeader = genDraftSignatureHeader(includeHeaders, key.keyId, signature, getDraftAlgoString(signInfo)); + const signatureHeader = genDraftSignatureHeader(includeHeaders, key.keyId, signature, algoString); Object.assign(request.headers, { Signature: signatureHeader }); @@ -128,6 +133,12 @@ var SignatureHeaderContentLackedError = class extends Error { super(`Signature header content lacked: ${lackedContent}`); } }; +var SignatureHeaderClockInvalidError = class extends Error { + constructor(prop) { + super(`Clock skew is invalid (${prop})`); + } +}; +var DraftSignatureHeaderKeys = ["keyId", "algorithm", "created", "expires", "opaque", "headers", "signature"]; function parseDraftRequestSignatureHeader(signatureHeader) { const result = {}; let prevStatus = "none"; @@ -188,26 +199,71 @@ function parseDraftRequestSignatureHeader(signatureHeader) { } return result; } -function validateAndProcessParsedDraftSignatureHeader(parsed, headers) { +function validateAndProcessParsedDraftSignatureHeader(parsed, options) { if (!parsed.keyId) throw new SignatureHeaderContentLackedError("keyId"); if (!parsed.algorithm) throw new SignatureHeaderContentLackedError("algorithm"); if (!parsed.signature) throw new SignatureHeaderContentLackedError("signature"); - if (!parsed.headers && !headers) + if (!parsed.headers) throw new SignatureHeaderContentLackedError("headers"); + const headersArray = parsed.headers.split(" "); + if (options?.requiredInputs?.draft) { + for (const requiredInput of options.requiredInputs.draft) { + if (requiredInput === "x-date" || requiredInput === "date") { + if (headersArray.includes("date")) + continue; + if (headersArray.includes("x-date")) + continue; + throw new SignatureHeaderContentLackedError(`headers.${requiredInput}`); + } + if (!headersArray.includes(requiredInput)) + throw new SignatureHeaderContentLackedError(`headers.${requiredInput}`); + } + } + if (parsed.created) { + const createdSec = parseInt(parsed.created); + if (isNaN(createdSec)) + throw new SignatureHeaderClockInvalidError("created"); + const nowTime = (options?.clockSkew?.now || /* @__PURE__ */ new Date()).getTime(); + if (createdSec * 1e3 > nowTime + (options?.clockSkew?.forward ?? 100)) { + throw new SignatureHeaderClockInvalidError("created"); + } + } + if (parsed.expires) { + const expiresSec = parseInt(parsed.expires); + if (isNaN(expiresSec)) + throw new SignatureHeaderClockInvalidError("expires"); + const nowTime = (options?.clockSkew?.now || /* @__PURE__ */ new Date()).getTime(); + if (expiresSec * 1e3 < nowTime - (options?.clockSkew?.forward ?? 100)) { + throw new SignatureHeaderClockInvalidError("expires"); + } + } return { keyId: parsed.keyId, algorithm: parsed.algorithm.toLowerCase(), signature: parsed.signature, - headers: parsed.headers ? parsed.headers.split(" ") : headers + headers: headersArray, + created: parsed.created, + expires: parsed.expires, + opaque: parsed.opaque }; } function parseDraftRequest(request, options) { const signatureHeader = validateRequestAndGetSignatureHeader(request, options?.clockSkew); - const parsedSignatureHeader = validateAndProcessParsedDraftSignatureHeader(parseDraftRequestSignatureHeader(signatureHeader), options?.headers); - const signingString = genDraftSigningString(request, parsedSignatureHeader.headers); + const parsedSignatureHeader = validateAndProcessParsedDraftSignatureHeader(parseDraftRequestSignatureHeader(signatureHeader), options); + const signingString = genDraftSigningString( + request, + parsedSignatureHeader.headers, + { + keyId: parsedSignatureHeader.keyId, + algorithm: parsedSignatureHeader.algorithm, + created: parsedSignatureHeader.created, + expires: parsedSignatureHeader.expires, + opaque: parsedSignatureHeader.opaque + } + ); return { version: "draft", value: { @@ -249,6 +305,9 @@ var ClockSkewInvalidError = class extends Error { function signatureHeaderIsDraft(signatureHeader) { return signatureHeader.includes('signature="'); } +function requestIsRFC9421(request) { + return objectLcKeys(request.headers).has("signature-input"); +} function checkClockSkew(reqDate, nowDate, delay = 300 * 1e3, forward = 100) { const reqTime = reqDate.getTime(); const nowTime = nowDate.getTime(); @@ -260,19 +319,20 @@ function checkClockSkew(reqDate, nowDate, delay = 300 * 1e3, forward = 100) { function validateRequestAndGetSignatureHeader(request, clock) { if (!request.headers) throw new SignatureHeaderNotFoundError(); - const signatureHeader = request.headers["signature"] || request.headers["Signature"]; + const headers = lcObjectKey(request.headers); + const signatureHeader = headers["signature"]; if (!signatureHeader) throw new SignatureHeaderNotFoundError(); if (Array.isArray(signatureHeader)) throw new RequestHasMultipleSignatureHeadersError(); - if (request.headers["date"]) { - if (Array.isArray(request.headers["date"])) + if (headers["date"]) { + if (Array.isArray(headers["date"])) throw new RequestHasMultipleDateHeadersError(); - checkClockSkew(new Date(request.headers["date"]), clock?.now || /* @__PURE__ */ new Date(), clock?.delay, clock?.forward); - } else if (request.headers["x-date"]) { - if (Array.isArray(request.headers["x-date"])) + checkClockSkew(new Date(headers["date"]), clock?.now || /* @__PURE__ */ new Date(), clock?.delay, clock?.forward); + } else if (headers["x-date"]) { + if (Array.isArray(headers["x-date"])) throw new RequestHasMultipleDateHeadersError(); - checkClockSkew(new Date(request.headers["x-date"]), clock?.now || /* @__PURE__ */ new Date(), clock?.delay, clock?.forward); + checkClockSkew(new Date(headers["x-date"]), clock?.now || /* @__PURE__ */ new Date(), clock?.delay, clock?.forward); } if (!request.method) throw new InvalidRequestError("Request method not found"); @@ -280,13 +340,14 @@ function validateRequestAndGetSignatureHeader(request, clock) { throw new InvalidRequestError("Request URL not found"); return signatureHeader; } -function parseRequest(request, options) { +function parseRequestSignature(request, options) { const signatureHeader = validateRequestAndGetSignatureHeader(request, options?.clockSkew); - if (signatureHeaderIsDraft(signatureHeader)) { - return parseDraftRequest(request, options); - } else { + if (requestIsRFC9421(request)) { throw new Error("Not implemented"); + } else if (signatureHeaderIsDraft(signatureHeader)) { + return parseDraftRequest(request, options); } + return null; } // src/keypair.ts @@ -358,6 +419,91 @@ function toSpkiPublicKey(publicKey) { }); } +// src/digest/utils.ts +import { createHash } from "node:crypto"; +function createBase64Digest(body, hash = "sha256") { + if (Array.isArray(hash)) { + return new Map(hash.map((h) => [h, createBase64Digest(body, h)])); + } + return createHash(hash).update(body).digest("base64"); +} + +// src/digest/digest-rfc3230.ts +var digestHashAlgosForEncoding = { + "sha1": "SHA", + "sha256": "SHA-256", + "sha384": "SHA-384", + "sha512": "SHA-512", + "md5": "MD5" +}; +var digestHashAlgosForDecoding = { + "SHA": "sha1", + "SHA-1": "sha1", + "SHA-256": "sha256", + "SHA-384": "sha384", + "SHA-512": "sha512", + "MD5": "md5" +}; +function genRFC3230DigestHeader(body, hashAlgorithm = "sha256") { + return `${digestHashAlgosForEncoding[hashAlgorithm]}=${createBase64Digest(body, hashAlgorithm)}`; +} +var digestHeaderRegEx = /^([a-zA-Z0-9\-]+)=([^\,]+)/; +function verifyRFC3230DigestHeader(request, rawBody, failOnNoDigest = true, errorLogger) { + let digestHeader = lcObjectGet(request.headers, "digest"); + if (!digestHeader) { + if (failOnNoDigest) { + if (errorLogger) + errorLogger("Digest header not found"); + return false; + } + return true; + } + if (Array.isArray(digestHeader)) { + digestHeader = digestHeader[0]; + } + const match = digestHeader.match(digestHeaderRegEx); + if (!match) { + if (errorLogger) + errorLogger("Invalid Digest header format"); + return false; + } + const value = match[2]; + if (!value) { + if (errorLogger) + errorLogger("Invalid Digest header format"); + return false; + } + const algo = digestHashAlgosForDecoding[match[1].toUpperCase()]; + if (!algo) { + if (errorLogger) + errorLogger(`Invalid Digest header algorithm: ${match[1]}`); + return false; + } + const hash = createBase64Digest(rawBody, algo); + if (hash !== value) { + if (errorLogger) + errorLogger(`Digest header hash mismatch`); + return false; + } + return true; +} + +// src/digest/digest.ts +function verifyDigestHeader(request, rawBody, failOnNoDigest = true, errorLogger) { + const headerKeys = objectLcKeys(request.headers); + if (headerKeys.has("content-digest")) { + throw new Error("Not implemented yet"); + } else if (headerKeys.has("digest")) { + return verifyRFC3230DigestHeader(request, rawBody, failOnNoDigest, errorLogger); + } + if (failOnNoDigest) { + if (errorLogger) + errorLogger("Content-Digest or Digest header not found"); + return false; + } + return true; +} + // src/shared/verify.ts var SignatureMissmatchWithProvidedAlgorithmError = class extends Error { constructor(providedAlgorithm, detectedAlgorithm, realKeyType) { @@ -407,12 +553,8 @@ function detectAndVerifyAlgorithm(algorithm, publicKey) { } // src/draft/verify.ts -var verify_exports = {}; -__export(verify_exports, { - verifySignature: () => verifySignature -}); import * as crypto4 from "node:crypto"; -function verifySignature(parsed, publicKeyPem, errorLogger) { +function verifyDraftSignature(parsed, publicKeyPem, errorLogger) { const publicKey = crypto4.createPublicKey(publicKeyPem); try { const detected = detectAndVerifyAlgorithm(parsed.params.algorithm, publicKey); @@ -423,32 +565,43 @@ function verifySignature(parsed, publicKeyPem, errorLogger) { return false; } } - -// src/index.ts -var HttpSignatureDraft = { - ...parse_exports, - ...sign_exports, - ...verify_exports -}; export { ClockSkewInvalidError, - HttpSignatureDraft, + DraftSignatureHeaderKeys, InvalidRequestError, RequestHasMultipleDateHeadersError, RequestHasMultipleSignatureHeadersError, + SignatureHeaderClockInvalidError, + SignatureHeaderContentLackedError, SignatureHeaderNotFoundError, SignatureMissmatchWithProvidedAlgorithmError, checkClockSkew, detectAndVerifyAlgorithm, + digestHeaderRegEx, + genDraftAuthorizationHeader, + genDraftSignature, + genDraftSignatureHeader, + genDraftSigningString, genEcKeyPair, genEd25519KeyPair, genEd448KeyPair, + genRFC3230DigestHeader, genRsaKeyPair, getDraftAlgoString, + lcObjectGet, lcObjectKey, - parseRequest, + objectLcKeys, + parseDraftRequest, + parseDraftRequestSignatureHeader, + parseRequestSignature, prepareSignInfo, + requestIsRFC9421, + signAsDraftToRequest, signatureHeaderIsDraft, toSpkiPublicKey, - validateRequestAndGetSignatureHeader + validateAndProcessParsedDraftSignatureHeader, + validateRequestAndGetSignatureHeader, + verifyDigestHeader, + verifyDraftSignature, + verifyRFC3230DigestHeader }; diff --git a/dist/parse.d.ts b/dist/parse.d.ts index ac5d16d..7f51800 100644 --- a/dist/parse.d.ts +++ b/dist/parse.d.ts @@ -1,6 +1,12 @@ import type { ClockSkewSettings, IncomingRequest } from './types.js'; export type RequestParseOptions = { - headers?: string[]; + /** + * Headers should be included in the signature string + */ + requiredInputs?: { + draft?: string[]; + rfc9421?: string[]; + }; clockSkew?: ClockSkewSettings; }; export declare class SignatureHeaderNotFoundError extends Error { @@ -25,6 +31,10 @@ export declare class ClockSkewInvalidError extends Error { * @returns boolean */ export declare function signatureHeaderIsDraft(signatureHeader: string): boolean; +/** + * Check if request is based on RFC 9421 + */ +export declare function requestIsRFC9421(request: IncomingRequest): boolean; /** * Check the clock skew of the request * @param reqDate Request date @@ -35,9 +45,8 @@ export declare function signatureHeaderIsDraft(signatureHeader: string): boolean export declare function checkClockSkew(reqDate: Date, nowDate: Date, delay?: number, forward?: number): void; export declare function validateRequestAndGetSignatureHeader(request: IncomingRequest, clock?: ClockSkewSettings): string; /** - * Parse the request headers - * DraftとRFCをうまく区別してリクエストをパースする + * Parse request headers with Draft and RFC discrimination * @param request http.IncomingMessage | http2.Http2ServerRequest * @param options */ -export declare function parseRequest(request: IncomingRequest, options?: RequestParseOptions): import("./types.js").DraftParsedSignature; +export declare function parseRequestSignature(request: IncomingRequest, options?: RequestParseOptions): import("./types.js").ParsedDraftSignature | null; diff --git a/dist/types.d.ts b/dist/types.d.ts index 86de945..2e398b2 100644 --- a/dist/types.d.ts +++ b/dist/types.d.ts @@ -6,6 +6,7 @@ export type RequestLike = { url: string; method: string; headers: Record; + body?: string; }; export type IncomingRequest = RequestLike | IncomingMessage | Http2ServerRequest; export type ClockSkewSettings = { @@ -46,8 +47,9 @@ export type PrivateKey = { keyId: string; }; export type SignatureHashAlgorithm = 'sha1' | 'sha256' | 'sha384' | 'sha512' | null; +export type DigestHashAlgorithm = 'sha1' | 'sha256' | 'sha384' | 'sha512' | 'md5'; export type SignatureAlgorithm = 'rsa-sha1' | 'rsa-sha256' | 'rsa-sha384' | 'rsa-sha512' | 'ecdsa-sha1' | 'ecdsa-sha256' | 'ecdsa-sha384' | 'ecdsa-sha512' | 'ed25519-sha512' | 'ed25519' | 'ed448'; -export type DraftParsedSignature = { +export type ParsedDraftSignature = { version: 'draft'; /** * Compatible with @peertube/http-signature @@ -58,6 +60,7 @@ export type DraftParsedSignature = { params: { keyId: string; /** + * lower-case * @example 'rsa-sha256' */ algorithm?: string; @@ -69,9 +72,11 @@ export type DraftParsedSignature = { }; signingString: string; /** + * UPPER-CASE * @example 'RSA-SHA256' */ algorithm?: string; keyId: string; }; }; +export type ParsedSignature = ParsedDraftSignature; diff --git a/dist/utils.d.ts b/dist/utils.d.ts index 171286f..934f4ba 100644 --- a/dist/utils.d.ts +++ b/dist/utils.d.ts @@ -9,4 +9,12 @@ export declare function getDraftAlgoString(signInfo: SignInfo): string; /** * Convert object keys to lowercase */ -export declare function lcObjectKey>(src: T): T; +export declare function lcObjectKey>(src: T): T; +/** + * Get value from object, key is case-insensitive + */ +export declare function lcObjectGet>(src: T, key: string): T[keyof T] | undefined; +/** + * Get the Set of keys of the object, lowercased + */ +export declare function objectLcKeys>(src: T): Set;