@fedify/fedify
Version:
An ActivityPub server framework
153 lines (152 loc) • 5.77 kB
JavaScript
import * as dntShim from "../_dnt.shims.js";
import { concat } from "../deps/jsr.io/@std/bytes/1.0.5/concat.js";
import { decodeBase64, encodeBase64 } from "../deps/jsr.io/@std/encoding/1.0.7/base64.js";
import { decodeBase64Url } from "../deps/jsr.io/@std/encoding/1.0.7/base64url.js";
import { decodeHex } from "../deps/jsr.io/@std/encoding/1.0.7/hex.js";
import { Integer, Sequence } from "asn1js";
import { decode, encode } from "./multibase/index.js";
import { addPrefix, getCodeFromData, rmPrefix } from "multicodec";
import { createPublicKey } from "node:crypto";
import { PublicKeyInfo } from "pkijs";
import { validateCryptoKey } from "../sig/key.js";
const algorithms = {
"1.2.840.113549.1.1.1": { name: "RSASSA-PKCS1-v1_5", hash: "SHA-256" },
"1.3.101.112": "Ed25519",
};
/**
* Imports a PEM-SPKI formatted public key.
* @param pem The PEM-SPKI formatted public key.
* @returns The imported public key.
* @throws {TypeError} If the key is invalid or unsupported.
* @since 0.5.0
*/
export async function importSpki(pem) {
pem = pem.replace(/(?:-----(?:BEGIN|END) PUBLIC KEY-----|\s)/g, "");
let spki;
try {
spki = decodeBase64(pem);
}
catch (_) {
throw new TypeError("Invalid PEM-SPKI format.");
}
const pki = PublicKeyInfo.fromBER(spki);
const oid = pki.algorithm.algorithmId;
const algorithm = algorithms[oid];
if (algorithm == null) {
throw new TypeError("Unsupported algorithm: " + oid);
}
return await dntShim.crypto.subtle.importKey("spki", spki, algorithm, true, ["verify"]);
}
/**
* Exports a public key in PEM-SPKI format.
* @param key The public key to export.
* @returns The exported public key in PEM-SPKI format.
* @throws {TypeError} If the key is invalid or unsupported.
* @since 0.5.0
*/
export async function exportSpki(key) {
validateCryptoKey(key);
const spki = await dntShim.crypto.subtle.exportKey("spki", key);
let pem = encodeBase64(spki);
pem = (pem.match(/.{1,64}/g) || []).join("\n");
return `-----BEGIN PUBLIC KEY-----\n${pem}\n-----END PUBLIC KEY-----\n`;
}
/**
* Imports a PEM-PKCS#1 formatted public key.
* @param pem The PEM-PKCS#1 formatted public key.
* @returns The imported public key.
* @throws {TypeError} If the key is invalid or unsupported.
* @since 1.5.0
*/
export function importPkcs1(pem) {
const key = createPublicKey({ key: pem, format: "pem", type: "pkcs1" });
const spki = key.export({ type: "spki", format: "pem" });
return importSpki(spki);
}
const PKCS1_HEADER = /^\s*-----BEGIN\s+RSA\s+PUBLIC\s+KEY-----\s*\n/;
/**
* Imports a PEM formatted public key (SPKI or PKCS#1).
* @param pem The PEM formatted public key to import (SPKI or PKCS#1).
* @returns The imported public key.
* @throws {TypeError} If the key is invalid or unsupported.
* @since 1.5.0
*/
export function importPem(pem) {
return PKCS1_HEADER.test(pem) ? importPkcs1(pem) : importSpki(pem);
}
/**
* Imports a [Multibase]-encoded public key.
*
* [Multibase]: https://www.w3.org/TR/vc-data-integrity/#multibase-0
* @param key The Multibase-encoded public key.
* @returns The imported public key.
* @throws {TypeError} If the key is invalid or unsupported.
* @since 0.10.0
*/
export async function importMultibaseKey(key) {
const decoded = decode(key);
const code = getCodeFromData(decoded);
const content = rmPrefix(decoded);
if (code === 0x1205) { // rsa-pub
const keyObject = createPublicKey({
// deno-lint-ignore no-explicit-any
key: content,
format: "der",
type: "pkcs1",
});
const spki = keyObject.export({ type: "spki", format: "der" }).buffer;
return await dntShim.crypto.subtle.importKey("spki", new Uint8Array(spki), { name: "RSASSA-PKCS1-v1_5", hash: "SHA-256" }, true, ["verify"]);
}
else if (code === 0xed) { // ed25519-pub
return await dntShim.crypto.subtle.importKey("raw", content, "Ed25519", true, ["verify"]);
}
else {
throw new TypeError("Unsupported key type: 0x" + code.toString(16));
}
}
/**
* Exports a public key in [Multibase] format.
*
* [Multibase]: https://www.w3.org/TR/vc-data-integrity/#multibase-0
* @param key The public key to export.
* @returns The exported public key in Multibase format.
* @throws {TypeError} If the key is invalid or unsupported.
* @since 0.10.0
*/
export async function exportMultibaseKey(key) {
let content;
let code;
if (key.algorithm.name === "Ed25519") {
content = await dntShim.crypto.subtle.exportKey("raw", key);
code = 0xed; // ed25519-pub
}
else if (key.algorithm.name === "RSASSA-PKCS1-v1_5" &&
key.algorithm.hash.name ===
"SHA-256") {
const jwk = await dntShim.crypto.subtle.exportKey("jwk", key);
const n = concat([new Uint8Array([0]), decodeBase64Url(jwk.n)]);
const sequence = new Sequence({
value: [
new Integer({
isHexOnly: true,
valueHex: n,
}),
new Integer({
isHexOnly: true,
valueHex: decodeBase64Url(jwk.e),
}),
],
});
content = sequence.toBER(false);
code = 0x1205; // rsa-pub
}
else {
throw new TypeError("Unsupported key type: " + JSON.stringify(key.algorithm));
}
const codeHex = code.toString(16);
const codeBytes = decodeHex(codeHex.length % 2 < 1 ? codeHex : "0" + codeHex);
const prefixed = addPrefix(codeBytes, new Uint8Array(content));
const encoded = encode("base58btc", prefixed);
return new TextDecoder().decode(encoded);
}
// cSpell: ignore multicodec pkijs