Skip to content

Commit

Permalink
implement webVerifyDraftSignature (experimental)
Browse files Browse the repository at this point in the history
  • Loading branch information
tamaina committed Mar 2, 2024
1 parent 3901611 commit bfb9942
Show file tree
Hide file tree
Showing 15 changed files with 777 additions and 44 deletions.
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ Since ActivityPub also needs digest validation, this library also implements fun
### With http-signature
Previously, we used `http-signature` (`@peertube/http-signature` to be exact) to parse and verify (Draft) signatures, and this library replaces those implementations as well.

This is because `TritonDataCenter/node-sshpk` (formerly `joient/node-sshpk`), on which http-signature depends, is slower than `node:crypto`.
This is because `TritonDataCenter/node-sshpk` (formerly `joient/node-sshpk`), on which http-signature depends, is slower than `crypto`.

## ActivityPub Compatibility
One of the motivations for creating this package is to make Misskey compatible with the Ed25519 signature instead of RSA. In doing so, there is a need to ensure compatibility.
Expand Down
3 changes: 3 additions & 0 deletions build.js
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,9 @@ const buildOptionsBase = {
absWorkingDir: __dirname,
outbase: `${__dirname}/src`,
outdir: `${__dirname}/dist`,
external: [
'@lapo/asn1js'
],
loader: {
'.ts': 'ts'
},
Expand Down
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 "node:crypto";
import { BinaryLike } from "crypto";
export declare function verifyDigestHeader(request: IncomingRequest, rawBody: BinaryLike, failOnNoDigest?: boolean, errorLogger?: ((message: any) => any)): boolean;
5 changes: 5 additions & 0 deletions dist/draft/verify.d.ts
Original file line number Diff line number Diff line change
@@ -1,2 +1,7 @@
import type { ParsedDraftSignature } from '../types.js';
export declare function verifyDraftSignature(parsed: ParsedDraftSignature['value'], publicKeyPem: string, errorLogger?: ((message: any) => any)): boolean;
/**
* Experimental
* @experimental Testing Web Crypto API
*/
export declare function webVerifyDraftSignature(parsed: ParsedDraftSignature['value'], publicKeyPem: string, errorLogger?: ((message: any) => any)): Promise<boolean>;
175 changes: 163 additions & 12 deletions dist/index.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -67,17 +67,18 @@ __export(src_exports, {
validateRequestAndGetSignatureHeader: () => validateRequestAndGetSignatureHeader,
verifyDigestHeader: () => verifyDigestHeader,
verifyDraftSignature: () => verifyDraftSignature,
verifyRFC3230DigestHeader: () => verifyRFC3230DigestHeader
verifyRFC3230DigestHeader: () => verifyRFC3230DigestHeader,
webVerifyDraftSignature: () => webVerifyDraftSignature
});
module.exports = __toCommonJS(src_exports);

// src/draft/sign.ts
var crypto2 = __toESM(require("node:crypto"), 1);
var crypto3 = __toESM(require("node:crypto"), 1);

// src/utils.ts
var crypto = __toESM(require("node:crypto"), 1);
var crypto2 = __toESM(require("node:crypto"), 1);
function prepareSignInfo(privateKeyPem, hash = null) {
const keyObject = crypto.createPrivateKey(privateKeyPem);
const keyObject = crypto2.createPrivateKey(privateKeyPem);
if (keyObject.asymmetricKeyType === "rsa") {
const hashAlgo = hash || "sha256";
return {
Expand Down Expand Up @@ -174,7 +175,7 @@ function genDraftSigningString(request, includeHeaders, additional) {
return results.join("\n");
}
function genDraftSignature(signingString, privateKey, hashAlgorithm) {
const r = crypto2.sign(hashAlgorithm, Buffer.from(signingString), privateKey);
const r = crypto3.sign(hashAlgorithm, Buffer.from(signingString), privateKey);
return r.toString("base64");
}
function genDraftSignatureHeader(includeHeaders, keyId, signature, algorithm) {
Expand Down Expand Up @@ -437,9 +438,9 @@ function parseRequestSignature(request, options) {
}

// src/keypair.ts
var crypto3 = __toESM(require("node:crypto"), 1);
var crypto4 = __toESM(require("node:crypto"), 1);
var util = __toESM(require("node:util"), 1);
var generateKeyPair2 = util.promisify(crypto3.generateKeyPair);
var generateKeyPair2 = util.promisify(crypto4.generateKeyPair);
async function genRsaKeyPair(modulusLength = 4096) {
return await generateKeyPair2("rsa", {
modulusLength,
Expand Down Expand Up @@ -499,7 +500,7 @@ async function genEd448KeyPair() {
});
}
function toSpkiPublicKey(publicKey) {
return crypto3.createPublicKey(publicKey).export({
return crypto4.createPublicKey(publicKey).export({
type: "spki",
format: "pem"
});
Expand Down Expand Up @@ -641,14 +642,163 @@ function detectAndVerifyAlgorithm(algorithm, publicKey, errorLogger) {
}

// src/draft/verify.ts
var crypto4 = __toESM(require("node:crypto"), 1);
var ncrypto = __toESM(require("node:crypto"), 1);

// src/shared/spki-algo.ts
var import_asn1js = __toESM(require("@lapo/asn1js"), 1);
var import_hex = __toESM(require("@lapo/asn1js/hex.js"), 1);
var import_base64 = __toESM(require("@lapo/asn1js/base64.js"), 1);
var SpkiParseError = class extends Error {
constructor(message) {
super(message);
}
};
function getPublicKeyAlgorithmNameFromOid(oidStr) {
const oid = oidStr.split("\n")[0].trim();
if (oid === "1.2.840.113549.1.1.1")
return "RSASSA-PKCS1-v1_5";
if (oid === "1.2.840.10040.4.1")
return "DSA";
if (oid === "1.2.840.10046.2.1")
return "DH";
if (oid === "2.16.840.1.101.2.1.1.22")
return "KEA";
if (oid === "1.2.840.10045.2.1")
return "EC";
if (oid === "1.3.101.112")
return "Ed25519";
if (oid === "1.3.101.113")
return "Ed448";
throw new SpkiParseError("Unknown Public Key Algorithm OID");
}
function getNistCurveFromOid(oidStr) {
const oid = oidStr.split("\n")[0].trim();
if (oid === "1.2.840.10045.3.1.1")
return "P-192";
if (oid === "1.3.132.0.33")
return "P-224";
if (oid === "1.2.840.10045.3.1.7")
return "P-256";
if (oid === "1.3.132.0.34")
return "P-384";
if (oid === "1.3.132.0.35")
return "P-521";
throw new SpkiParseError("Unknown Named Curve OID");
}
function asn1BinaryToArrayBuffer(enc) {
if (typeof enc === "string") {
return Uint8Array.from(enc, (s) => s.charCodeAt(0)).buffer;
}
if (enc instanceof ArrayBuffer) {
return enc;
} else if (enc instanceof Uint8Array) {
return enc.buffer;
} else if (Array.isArray(enc)) {
return new Uint8Array(enc).buffer;
}
throw new SpkiParseError("Invalid SPKI (invalid ASN1 Stream data)");
}
var reHex = /^\s*(?:[0-9A-Fa-f][0-9A-Fa-f]\s*)+$/;
function parseSpki(input) {
const der = typeof input === "string" ? reHex.test(input) ? import_hex.default.decode(input) : import_base64.default.unarmor(input) : input;
const parsed = import_asn1js.default.decode(der);
if (!parsed.sub || parsed.sub.length === 0 || parsed.sub.length > 2)
throw new SpkiParseError("Invalid SPKI (invalid sub)");
const algorithmIdentifierSub = parsed.sub && parsed.sub[0] && parsed.sub[0].sub;
if (!algorithmIdentifierSub)
throw new SpkiParseError("Invalid SPKI (no AlgorithmIdentifier)");
if (algorithmIdentifierSub.length === 0)
throw new SpkiParseError("Invalid SPKI (invalid AlgorithmIdentifier sub length, zero)");
if (algorithmIdentifierSub.length > 2)
throw new SpkiParseError("Invalid SPKI (invalid AlgorithmIdentifier sub length, too many)");
if (algorithmIdentifierSub[0].tag.tagNumber !== 6)
throw new SpkiParseError("Invalid SPKI (invalid AlgorithmIdentifier.sub[0] type)");
const algorithm = algorithmIdentifierSub[0]?.content() ?? null;
if (typeof algorithm !== "string")
throw new SpkiParseError("Invalid SPKI (invalid algorithm content)");
const parameter = algorithmIdentifierSub[1]?.content() ?? null;
return {
der: asn1BinaryToArrayBuffer(parsed.stream.enc),
algorithm,
parameter
};
}
function genKeyImportParams(parsed, defaults = {
hash: "SHA-256",
ec: "DSA"
}) {
const algorithm = getPublicKeyAlgorithmNameFromOid(parsed.algorithm);
if (!algorithm)
throw new SpkiParseError("Unknown algorithm");
if (algorithm === "RSASSA-PKCS1-v1_5") {
return { name: "RSASSA-PKCS1-v1_5", hash: defaults.hash };
}
if (algorithm === "EC") {
if (typeof parsed.parameter !== "string")
throw new SpkiParseError("Invalid EC parameter");
return {
name: `EC${defaults.ec}`,
namedCurve: getNistCurveFromOid(parsed.parameter)
};
}
if (algorithm === "Ed25519") {
return { name: "Ed25519" };
}
if (algorithm === "Ed448") {
return { name: "Ed448" };
}
throw new SpkiParseError("Unknown algorithm");
}
function genVerifyAlgorithm(parsed, defaults = {
hash: "SHA-256",
ec: "DSA"
}) {
const algorithm = getPublicKeyAlgorithmNameFromOid(parsed.algorithm);
if (!algorithm)
throw new SpkiParseError("Unknown algorithm");
if (algorithm === "RSASSA-PKCS1-v1_5") {
return { name: "RSASSA-PKCS1-v1_5" };
}
if (algorithm === "EC") {
return {
name: `EC${defaults.ec}`,
hash: defaults.hash
};
}
if (algorithm === "Ed25519") {
return { name: "Ed25519" };
}
if (algorithm === "Ed448") {
return {
name: "Ed448",
context: void 0
// TODO?
};
}
throw new SpkiParseError("Unknown algorithm");
}

// src/draft/verify.ts
function verifyDraftSignature(parsed, publicKeyPem, errorLogger) {
const publicKey = crypto4.createPublicKey(publicKeyPem);
const publicKey = ncrypto.createPublicKey(publicKeyPem);
try {
const detected = detectAndVerifyAlgorithm(parsed.params.algorithm, publicKey);
if (!detected)
return false;
return crypto4.verify(detected.hashAlg, Buffer.from(parsed.signingString), publicKey, Buffer.from(parsed.params.signature, "base64"));
return ncrypto.verify(detected.hashAlg, Buffer.from(parsed.signingString), publicKey, Buffer.from(parsed.params.signature, "base64"));
} catch (e) {
if (errorLogger)
errorLogger(e);
return false;
}
}
var encoder = new TextEncoder();
async function webVerifyDraftSignature(parsed, publicKeyPem, errorLogger) {
try {
const parsedSpki = parseSpki(publicKeyPem);
const publicKey = await crypto.subtle.importKey("spki", parsedSpki.der, genKeyImportParams(parsedSpki), false, ["verify"]);
const verify2 = await crypto.subtle.verify(genVerifyAlgorithm(parsedSpki), publicKey, encoder.encode(parsed.params.signature), encoder.encode(parsed.signingString));
return verify2;
} catch (e) {
if (errorLogger)
errorLogger(e);
Expand Down Expand Up @@ -694,5 +844,6 @@ function verifyDraftSignature(parsed, publicKeyPem, errorLogger) {
validateRequestAndGetSignatureHeader,
verifyDigestHeader,
verifyDraftSignature,
verifyRFC3230DigestHeader
verifyRFC3230DigestHeader,
webVerifyDraftSignature
});
Loading

0 comments on commit bfb9942

Please sign in to comment.