Skip to content

Commit

Permalink
Make digest use Web Crypto API
Browse files Browse the repository at this point in the history
0.0.1
  • Loading branch information
tamaina committed Mar 3, 2024
1 parent 33d18a2 commit 1c71f6e
Show file tree
Hide file tree
Showing 16 changed files with 133 additions and 160 deletions.
16 changes: 12 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -82,7 +82,11 @@ Parse and verify in fastify web server, implements ActivityPub inbox
```ts
import Fastify from 'fastify';
import fastifyRawBody from 'fastify-raw-body';
import { parseRequestSignature, verifyDraftSignature, verifyDigestHeader } from '@misskey-dev/node-http-message-signatures';
import {
verifyDigestHeader,
parseRequestSignature,
verifyDraftSignature,
} from '@misskey-dev/node-http-message-signatures';

/**
* Prepare keyId-publicKeyPem Map
Expand All @@ -101,7 +105,7 @@ await fastify.register(fastifyRawBody, {
runFirst: true,
});
fastify.post('/inbox', { config: { rawBody: true } }, async (request, reply) => {
const verifyDigest = verifyDigestHeader(request.raw, request.rawBody, true);
const verifyDigest = await verifyDigestHeader(request.raw, request.rawBody, true);
if (!verifyDigest) {
reply.code(401);
return;
Expand Down Expand Up @@ -133,7 +137,11 @@ fastify.post('/inbox', { config: { rawBody: true } }, async (request, reply) =>

### Sign and Post
```ts
import { signAsDraftToRequest, genRFC3230DigestHeader, RequestLike } from '@misskey-dev/node-http-message-signatures';
import {
genRFC3230DigestHeader,
signAsDraftToRequest,
RequestLike,
} from '@misskey-dev/node-http-message-signatures';

/**
* Prepare keyId-privateKeyPem Map
Expand Down Expand Up @@ -168,7 +176,7 @@ export async function send(url: string, body: string, keyId: string) {
// TODO
} else {
// Draft
request.headers['Digest'] = genRFC3230DigestHeader(body);
request.headers['Digest'] = await genRFC3230DigestHeader(body);

await signAsDraftToRequest(request, { keyId, privateKeyPem }, includeHeaders);

Expand Down
15 changes: 3 additions & 12 deletions dist/digest/digest-rfc3230.d.ts
Original file line number Diff line number Diff line change
@@ -1,14 +1,5 @@
/// <reference types="node" />
import { DigestSource } from './utils';
import { DigestHashAlgorithm, IncomingRequest } from '../types';
import { BinaryLike } from 'node:crypto';
export declare const digestHashAlgosForDecoding: {
readonly SHA: "sha1";
readonly 'SHA-1': "sha1";
readonly 'SHA-256': "sha256";
readonly 'SHA-384': "sha384";
readonly 'SHA-512': "sha512";
readonly MD5: "md5";
};
export declare function genRFC3230DigestHeader(body: string, hashAlgorithm?: DigestHashAlgorithm): string;
export declare function genRFC3230DigestHeader(body: string, hashAlgorithm: DigestHashAlgorithm): Promise<string>;
export declare const digestHeaderRegEx: RegExp;
export declare function verifyRFC3230DigestHeader(request: IncomingRequest, rawBody: BinaryLike, failOnNoDigest?: boolean, errorLogger?: ((message: any) => any)): boolean;
export declare function verifyRFC3230DigestHeader(request: IncomingRequest, rawBody: DigestSource, failOnNoDigest?: boolean, errorLogger?: ((message: any) => any)): Promise<boolean>;
2 changes: 1 addition & 1 deletion dist/digest/digest.d.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
/// <reference types="node" />
import { IncomingRequest } from "src/types";
import { BinaryLike } from "crypto";
export declare function verifyDigestHeader(request: IncomingRequest, rawBody: BinaryLike, failOnNoDigest?: boolean, errorLogger?: ((message: any) => any)): boolean;
export declare function verifyDigestHeader(request: IncomingRequest, rawBody: BinaryLike, failOnNoDigest?: boolean, errorLogger?: ((message: any) => any)): Promise<boolean>;
9 changes: 5 additions & 4 deletions dist/digest/utils.d.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
/// <reference types="node" />
import { BinaryLike } from 'node:crypto';
import { webcrypto as crypto } from 'node:crypto';
import { DigestHashAlgorithm } from '../types';
export declare function createBase64Digest(body: BinaryLike, hash: DigestHashAlgorithm): string;
export declare function createBase64Digest<Ks extends DigestHashAlgorithm[]>(body: BinaryLike, hash: Ks): Map<Ks[number], string>;
export declare function createBase64Digest(body: BinaryLike): string;
export type DigestSource = crypto.BufferSource | string;
export declare function createBase64Digest(body: DigestSource, hash: DigestHashAlgorithm): Promise<string>;
export declare function createBase64Digest<Ks extends DigestHashAlgorithm[]>(body: DigestSource, hash: Ks): Promise<Map<Ks[number], string>>;
export declare function createBase64Digest(body: DigestSource): Promise<string>;
53 changes: 20 additions & 33 deletions dist/index.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,6 @@ __export(src_exports, {
checkClockSkew: () => checkClockSkew,
decodeBase64ToUint8Array: () => decodeBase64ToUint8Array,
decodePem: () => decodePem,
digestHashAlgosForDecoding: () => digestHashAlgosForDecoding,
digestHeaderRegEx: () => digestHeaderRegEx,
encodeArrayBufferToBase64: () => encodeArrayBufferToBase64,
exportPrivateKeyPem: () => exportPrivateKeyPem,
Expand Down Expand Up @@ -308,12 +307,7 @@ function encodeArrayBufferToBase64(buffer) {
return btoa(binary);
}
function decodeBase64ToUint8Array(base64) {
const binary = atob(base64);
const uint8Array = new Uint8Array(binary.length);
for (let i = 0; i < binary.length; i++) {
uint8Array[i] = binary.charCodeAt(i);
}
return uint8Array;
return Uint8Array.from(atob(base64), (s) => s.charCodeAt(0));
}
var KeyValidationError = class extends Error {
constructor(message) {
Expand Down Expand Up @@ -792,34 +786,28 @@ async function genEd448KeyPair(keyUsage) {

// src/digest/utils.ts
var import_node_crypto = require("node:crypto");
function createBase64Digest(body, hash = "sha256") {
async function createBase64Digest(body, hash = "SHA-256") {
if (Array.isArray(hash)) {
return new Map(hash.map((h) => [h, createBase64Digest(body, h)]));
return new Map(await Promise.all(hash.map((h) => {
return (async () => [h, await createBase64Digest(body, h)])();
})));
}
if (hash === "SHA") {
hash = "SHA-1";
}
return (0, import_node_crypto.createHash)(hash).update(body).digest("base64");
if (typeof body === "string") {
body = new TextEncoder().encode(body);
}
const hashAb = await import_node_crypto.webcrypto.subtle.digest(hash, body);
return encodeArrayBufferToBase64(hashAb);
}

// 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)}`;
async function genRFC3230DigestHeader(body, hashAlgorithm) {
return `${hashAlgorithm}=${await createBase64Digest(body, hashAlgorithm)}`;
}
var digestHeaderRegEx = /^([a-zA-Z0-9\-]+)=([^\,]+)/;
function verifyRFC3230DigestHeader(request, rawBody, failOnNoDigest = true, errorLogger) {
async function verifyRFC3230DigestHeader(request, rawBody, failOnNoDigest = true, errorLogger) {
let digestHeader = lcObjectGet(request.headers, "digest");
if (!digestHeader) {
if (failOnNoDigest) {
Expand All @@ -844,13 +832,13 @@ function verifyRFC3230DigestHeader(request, rawBody, failOnNoDigest = true, erro
errorLogger("Invalid Digest header format");
return false;
}
const algo = digestHashAlgosForDecoding[match[1].toUpperCase()];
const algo = match[1];
if (!algo) {
if (errorLogger)
errorLogger(`Invalid Digest header algorithm: ${match[1]}`);
return false;
}
const hash = createBase64Digest(rawBody, algo);
const hash = await createBase64Digest(rawBody, algo);
if (hash !== value) {
if (errorLogger)
errorLogger(`Digest header hash mismatch`);
Expand All @@ -860,12 +848,12 @@ function verifyRFC3230DigestHeader(request, rawBody, failOnNoDigest = true, erro
}

// src/digest/digest.ts
function verifyDigestHeader(request, rawBody, failOnNoDigest = true, errorLogger) {
async 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);
return await verifyRFC3230DigestHeader(request, rawBody, failOnNoDigest, errorLogger);
}
if (failOnNoDigest) {
if (errorLogger)
Expand Down Expand Up @@ -977,7 +965,6 @@ async function verifyDraftSignature(parsed, publicKeyPem, errorLogger) {
checkClockSkew,
decodeBase64ToUint8Array,
decodePem,
digestHashAlgosForDecoding,
digestHeaderRegEx,
encodeArrayBufferToBase64,
exportPrivateKeyPem,
Expand Down
54 changes: 21 additions & 33 deletions dist/index.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -210,12 +210,7 @@ function encodeArrayBufferToBase64(buffer) {
return btoa(binary);
}
function decodeBase64ToUint8Array(base64) {
const binary = atob(base64);
const uint8Array = new Uint8Array(binary.length);
for (let i = 0; i < binary.length; i++) {
uint8Array[i] = binary.charCodeAt(i);
}
return uint8Array;
return Uint8Array.from(atob(base64), (s) => s.charCodeAt(0));
}
var KeyValidationError = class extends Error {
constructor(message) {
Expand Down Expand Up @@ -693,35 +688,29 @@ async function genEd448KeyPair(keyUsage) {
}

// src/digest/utils.ts
import { createHash } from "node:crypto";
function createBase64Digest(body, hash = "sha256") {
import { webcrypto as crypto } from "node:crypto";
async function createBase64Digest(body, hash = "SHA-256") {
if (Array.isArray(hash)) {
return new Map(hash.map((h) => [h, createBase64Digest(body, h)]));
return new Map(await Promise.all(hash.map((h) => {
return (async () => [h, await createBase64Digest(body, h)])();
})));
}
if (hash === "SHA") {
hash = "SHA-1";
}
return createHash(hash).update(body).digest("base64");
if (typeof body === "string") {
body = new TextEncoder().encode(body);
}
const hashAb = await crypto.subtle.digest(hash, body);
return encodeArrayBufferToBase64(hashAb);
}

// 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)}`;
async function genRFC3230DigestHeader(body, hashAlgorithm) {
return `${hashAlgorithm}=${await createBase64Digest(body, hashAlgorithm)}`;
}
var digestHeaderRegEx = /^([a-zA-Z0-9\-]+)=([^\,]+)/;
function verifyRFC3230DigestHeader(request, rawBody, failOnNoDigest = true, errorLogger) {
async function verifyRFC3230DigestHeader(request, rawBody, failOnNoDigest = true, errorLogger) {
let digestHeader = lcObjectGet(request.headers, "digest");
if (!digestHeader) {
if (failOnNoDigest) {
Expand All @@ -746,13 +735,13 @@ function verifyRFC3230DigestHeader(request, rawBody, failOnNoDigest = true, erro
errorLogger("Invalid Digest header format");
return false;
}
const algo = digestHashAlgosForDecoding[match[1].toUpperCase()];
const algo = match[1];
if (!algo) {
if (errorLogger)
errorLogger(`Invalid Digest header algorithm: ${match[1]}`);
return false;
}
const hash = createBase64Digest(rawBody, algo);
const hash = await createBase64Digest(rawBody, algo);
if (hash !== value) {
if (errorLogger)
errorLogger(`Digest header hash mismatch`);
Expand All @@ -762,12 +751,12 @@ function verifyRFC3230DigestHeader(request, rawBody, failOnNoDigest = true, erro
}

// src/digest/digest.ts
function verifyDigestHeader(request, rawBody, failOnNoDigest = true, errorLogger) {
async 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);
return await verifyRFC3230DigestHeader(request, rawBody, failOnNoDigest, errorLogger);
}
if (failOnNoDigest) {
if (errorLogger)
Expand Down Expand Up @@ -878,7 +867,6 @@ export {
checkClockSkew,
decodeBase64ToUint8Array,
decodePem,
digestHashAlgosForDecoding,
digestHeaderRegEx,
encodeArrayBufferToBase64,
exportPrivateKeyPem,
Expand Down
2 changes: 1 addition & 1 deletion dist/types.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,7 @@ export type PrivateKey = {
export type KeyAlgorithmName = 'RSASSA-PKCS1-v1_5' | 'DSA' | 'DH' | 'KEA' | 'EC' | 'Ed25519' | 'Ed448';
export type ECNamedCurve = 'P-192' | 'P-224' | 'P-256' | 'P-384' | 'P-521';
export type SignatureHashAlgorithmUpperSnake = 'SHA-1' | 'SHA-256' | 'SHA-384' | 'SHA-512' | null;
export type DigestHashAlgorithm = 'sha1' | 'sha256' | 'sha384' | 'sha512' | 'md5';
export type DigestHashAlgorithm = 'SHA' | 'SHA-1' | 'SHA-256' | 'SHA-384' | 'SHA-512';
export type DraftSignatureAlgorithm = 'rsa-sha1' | 'rsa-sha256' | 'rsa-sha384' | 'rsa-sha512' | 'ecdsa-sha1' | 'ecdsa-sha256' | 'ecdsa-sha384' | 'ecdsa-sha512' | 'ed25519-sha512' | 'ed25519' | 'ed448';
export type ParsedDraftSignature = {
version: 'draft';
Expand Down
9 changes: 7 additions & 2 deletions dist/utils.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,13 +16,18 @@ export declare function objectLcKeys<T extends Record<string, any>>(src: T): Set
* convert number to Uint8Array, for ASN.1 length field
*/
export declare function numberToUint8Array(num: number | bigint): Uint8Array;
/**
* Generate ASN.1 length field
* @param length Length of the content
* @returns ASN.1 length field
*/
export declare function genASN1Length(length: number | bigint): Uint8Array;
/**
* For web
* ArrayBuffer to base64
*/
export declare function encodeArrayBufferToBase64(buffer: ArrayBuffer): string;
/**
* for Web
* base64 to Uint8Array
*/
export declare function decodeBase64ToUint8Array(base64: string): Uint8Array;
export declare class KeyValidationError extends Error {
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@misskey-dev/node-http-message-signatures",
"version": "0.0.0-alpha.17",
"version": "0.0.1",
"description": "",
"type": "module",
"keywords": [
Expand Down
38 changes: 18 additions & 20 deletions src/digest/digest-rfc3230.test.ts
Original file line number Diff line number Diff line change
@@ -1,14 +1,9 @@
import { digestHeaderRegEx, verifyRFC3230DigestHeader } from './digest-rfc3230';
import { verifyDigestHeader } from './digest';
import { createBase64Digest } from './utils';

describe('rfc3230', () => {
describe('regex', () => {
test('normal MD5', () => {
const result = digestHeaderRegEx.exec('MD5=foo');
expect(result).toBeTruthy();
expect(result![1]).toBe('MD5');
expect(result![2]).toBe('foo');
});
test('normal SHA-1', () => {
const result = digestHeaderRegEx.exec('SHA=foo');
expect(result).toBeTruthy();
Expand All @@ -34,29 +29,32 @@ describe('rfc3230', () => {
});

describe(verifyRFC3230DigestHeader, () => {
test('normal MD5', () => {
const request = {
headers: {
'digest': `MD5=${createBase64Digest('foo', 'md5')}`,
},
} as any;
expect(verifyRFC3230DigestHeader(request, 'foo')).toBe(true);
});
test('normal SHA-1', () => {
test('normal SHA-1', async () => {
const request = {
headers: {
'digest': `SHA=${createBase64Digest('foo', 'sha1')}`,
'digest': `SHA=${await createBase64Digest('foo', 'SHA-1')}`,
},
} as any;
expect(verifyRFC3230DigestHeader(request, 'foo')).toBe(true);
expect(await verifyRFC3230DigestHeader(request, 'foo')).toBe(true);
});
test('normal SHA-256', () => {
test('normal SHA-256', async () => {
const request = {
headers: {
'digest': `SHA-256=${createBase64Digest('foo', 'sha256')}`,
'digest': `SHA-256=${await createBase64Digest('foo', 'SHA-256')}`,
},
} as any;
expect(verifyRFC3230DigestHeader(request, 'foo')).toBe(true);
expect(await verifyRFC3230DigestHeader(request, 'foo')).toBe(true);
});
});
});

describe(verifyDigestHeader, () => {
test('RFC3230', async () => {
const request = {
headers: {
'digest': `SHA-256=${await createBase64Digest('foo', 'SHA-256')}`,
},
} as any;
expect(await verifyDigestHeader(request, 'foo')).toBe(true);
});
});
Loading

0 comments on commit 1c71f6e

Please sign in to comment.