diff --git a/.vscode/settings.json b/.vscode/settings.json index a19eaafb..eecf355d 100755 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -58,6 +58,7 @@ "redirections", "rels", "setext", + "spki", "subproperty", "superproperty", "unfollow", diff --git a/CHANGES.md b/CHANGES.md index 7fa88598..02b2ecac 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -13,6 +13,11 @@ Version 0.5.0 To be released. + - Removed dependency on *jose*. + + - Added `exportSpki()` function. + - Added `importSpki()` function. + Version 0.4.0 ------------- diff --git a/codegen/__snapshots__/class.test.ts.snap b/codegen/__snapshots__/class.test.ts.snap index 07d7d33a..517f22be 100644 --- a/codegen/__snapshots__/class.test.ts.snap +++ b/codegen/__snapshots__/class.test.ts.snap @@ -6,9 +6,9 @@ import { Temporal } from \\"@js-temporal/polyfill\\"; import jsonld from \\"jsonld\\"; import { type LanguageTag, parseLanguageTag } from \\"@phensley/language-tag\\"; -import { exportSPKI, importSPKI } from \\"jose\\"; import { type DocumentLoader, fetchDocumentLoader } from \\"../runtime/docloader.ts\\"; +import { exportSpki, importSpki } from \\"../runtime/key.ts\\"; import { LanguageString } from \\"../runtime/langstr.ts\\"; @@ -178,7 +178,7 @@ get publicKey(): (CryptoKey | null) { array = []; for (const v of this.#_2fE2QMDdg6KFGqa4NEC3TmjApSAD) { array.push( - { \\"@value\\": await exportSPKI(v) } + { \\"@value\\": await exportSpki(v) } ); } if (array.length > 0) values[\\"https://w3id.org/security#publicKeyPem\\"] = array; @@ -263,9 +263,7 @@ get publicKey(): (CryptoKey | null) { for (const v of values[\\"https://w3id.org/security#publicKeyPem\\"] ?? []) { if (v == null) continue; - _2fE2QMDdg6KFGqa4NEC3TmjApSAD.push(await importSPKI(v[\\"@value\\"], \\"RS256\\", { - extractable: true - })) + _2fE2QMDdg6KFGqa4NEC3TmjApSAD.push(await importSpki(v[\\"@value\\"])) } instance.#_2fE2QMDdg6KFGqa4NEC3TmjApSAD = _2fE2QMDdg6KFGqa4NEC3TmjApSAD; diff --git a/codegen/class.ts b/codegen/class.ts index 3034c71c..5666e717 100644 --- a/codegen/class.ts +++ b/codegen/class.ts @@ -73,9 +73,9 @@ export async function* generateClasses( yield 'import jsonld from "jsonld";\n'; yield `import { type LanguageTag, parseLanguageTag } from "@phensley/language-tag";\n`; - yield `import { exportSPKI, importSPKI } from "jose";\n`; yield `import { type DocumentLoader, fetchDocumentLoader } from "${runtimePath}/docloader.ts";\n`; + yield `import { exportSpki, importSpki } from "${runtimePath}/key.ts";\n`; yield `import { LanguageString } from "${runtimePath}/langstr.ts";\n`; yield "\n\n"; const sorted = sortTopologically(types); diff --git a/codegen/type.ts b/codegen/type.ts index 70135853..9c128e67 100644 --- a/codegen/type.ts +++ b/codegen/type.ts @@ -199,17 +199,14 @@ const scalarTypes: Record = { return `${v} instanceof CryptoKey`; }, encoder(v) { - return `{ "@value": await exportSPKI(${v}) }`; + return `{ "@value": await exportSpki(${v}) }`; }, dataCheck(v) { return `typeof ${v} === "object" && "@value" in ${v} && typeof ${v}["@value"] === "string"`; }, decoder(v) { - // TODO: support other than RSASSA-PKCS1-v1_5: - return `await importSPKI(${v}["@value"], "RS256", { - extractable: true - })`; + return `await importSpki(${v}["@value"])`; }, }, "fedify:units": { diff --git a/deno.json b/deno.json index 373e23b5..6dba9401 100644 --- a/deno.json +++ b/deno.json @@ -37,7 +37,6 @@ "@std/url": "jsr:@std/url@^0.220.1", "@std/yaml": "jsr:@std/yaml@^0.220.1", "fast-check": "npm:fast-check@^3.17.0", - "jose": "npm:jose@^5.2.3", "jsonld": "npm:jsonld@^8.3.2", "mock_fetch": "https://deno.land/x/mock_fetch@0.3.0/mod.ts", "uri-template-router": "npm:uri-template-router@^0.0.16", diff --git a/examples/blog/import_map.g.json b/examples/blog/import_map.g.json index 62149c23..9a6c76ff 100644 --- a/examples/blog/import_map.g.json +++ b/examples/blog/import_map.g.json @@ -25,7 +25,6 @@ "@std/url": "jsr:@std/url@^0.220.1", "@std/yaml": "jsr:@std/yaml@^0.220.1", "fast-check": "npm:fast-check@^3.17.0", - "jose": "npm:jose@^5.2.3", "jsonld": "npm:jsonld@^8.3.2", "mock_fetch": "https://deno.land/x/mock_fetch@0.3.0/mod.ts", "uri-template-router": "npm:uri-template-router@^0.0.16", diff --git a/runtime/key.test.ts b/runtime/key.test.ts new file mode 100644 index 00000000..5aa67991 --- /dev/null +++ b/runtime/key.test.ts @@ -0,0 +1,43 @@ +import { assertEquals } from "@std/assert"; +import { exportJwk, importJwk } from "../httpsig/key.ts"; +import { exportSpki, importSpki } from "./key.ts"; + +// cSpell: disable +const pem = "-----BEGIN PUBLIC KEY-----\n" + + "MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAxsRuvCkgJtflBTl4OVsm\n" + + "nt/J1mQfZasfJtN33dcZ3d1lJroxmgmMu69zjGEAwkNbMQaWNLqC4eogkJaeJ4RR\n" + + "5MHYXkL9nNilVoTkjX5BVit3puzs7XJ7WQnKQgQMI+ezn24GHsZ/v1JIo77lerX5\n" + + "k4HNwTNVt+yaZVQWaOMR3+6FwziQR6kd0VuG9/a9dgAnz2cEoORRC1i4W7IZaB1s\n" + + "Znh1WbHbevlGd72HSXll5rocPIHn8gq6xpBgpHwRphlRsgn4KHaJ6brXDIJjrnQh\n" + + "Ie/YUBOGj/ImSEXhRwlFerKsoAVnZ0Hwbfa46qk44TAt8CyoPMWmpK6pt0ng4pQ2\n" + + "uwIDAQAB\n" + + "-----END PUBLIC KEY-----\n"; +// cSpell: enable + +const jwk = { + alg: "RS256", + // cSpell: disable + e: "AQAB", + // cSpell: enable + ext: true, + key_ops: ["verify"], + kty: "RSA", + // cSpell: disable + n: "xsRuvCkgJtflBTl4OVsmnt_J1mQfZasfJtN33dcZ3d1lJroxmgmMu69zjGEAwkNbMQaWN" + + "LqC4eogkJaeJ4RR5MHYXkL9nNilVoTkjX5BVit3puzs7XJ7WQnKQgQMI-ezn24GHsZ_v1J" + + "Io77lerX5k4HNwTNVt-yaZVQWaOMR3-6FwziQR6kd0VuG9_a9dgAnz2cEoORRC1i4W7IZa" + + "B1sZnh1WbHbevlGd72HSXll5rocPIHn8gq6xpBgpHwRphlRsgn4KHaJ6brXDIJjrnQhIe_" + + "YUBOGj_ImSEXhRwlFerKsoAVnZ0Hwbfa46qk44TAt8CyoPMWmpK6pt0ng4pQ2uw", + // cSpell: enable +}; + +Deno.test("importSpki()", async () => { + const key = await importSpki(pem); + assertEquals(await exportJwk(key), jwk); +}); + +Deno.test("exportSpki()", async () => { + const key = await importJwk(jwk, "public"); + const spki = await exportSpki(key); + assertEquals(spki, pem); +}); diff --git a/runtime/key.ts b/runtime/key.ts new file mode 100644 index 00000000..6ddd0912 --- /dev/null +++ b/runtime/key.ts @@ -0,0 +1,31 @@ +import { decodeBase64, encodeBase64 } from "@std/encoding/base64"; + +/** + * Imports a PEM-SPKI formatted public key. + * @param pem The PEM-SPKI formatted public key. + * @returns The imported public key. + */ +export async function importSpki(pem: string): Promise { + pem = pem.replace(/(?:-----(?:BEGIN|END) PUBLIC KEY-----|\s)/g, ""); + const spki = decodeBase64(pem); + // TODO: support other than RSASSA-PKCS1-v1_5: + return await crypto.subtle.importKey( + "spki", + spki, + { name: "RSASSA-PKCS1-v1_5", hash: "SHA-256" }, + 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. + */ +export async function exportSpki(key: CryptoKey): Promise { + const spki = await 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`; +} diff --git a/runtime/mod.ts b/runtime/mod.ts index 3df4c9d7..31da6796 100644 --- a/runtime/mod.ts +++ b/runtime/mod.ts @@ -5,4 +5,5 @@ * @module */ export * from "./docloader.ts"; +export * from "./key.ts"; export * from "./langstr.ts"; diff --git a/vocab/vocab.test.ts b/vocab/vocab.test.ts index c5d99f17..9f9b926e 100644 --- a/vocab/vocab.test.ts +++ b/vocab/vocab.test.ts @@ -266,6 +266,7 @@ Deno.test("Person.fromJsonLd()", async () => { "publicKey": { "id": "https://todon.eu/users/hongminhee#main-key", "owner": "https://todon.eu/users/hongminhee", + // cSpell: disable "publicKeyPem": "-----BEGIN PUBLIC KEY-----\n" + "MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAxsRuvCkgJtflBTl4OVsm\n" + "nt/J1mQfZasfJtN33dcZ3d1lJroxmgmMu69zjGEAwkNbMQaWNLqC4eogkJaeJ4RR\n" + @@ -275,6 +276,7 @@ Deno.test("Person.fromJsonLd()", async () => { "Ie/YUBOGj/ImSEXhRwlFerKsoAVnZ0Hwbfa46qk44TAt8CyoPMWmpK6pt0ng4pQ2\n" + "uwIDAQAB\n" + "-----END PUBLIC KEY-----\n", + // cSpell: enable }, }, { documentLoader: mockDocumentLoader }); assertEquals( @@ -296,7 +298,11 @@ Deno.test("Key.publicKey", async () => { kty: "RSA", alg: "RS256", // cSpell: disable - n: "xsRuvCkgJtflBTl4OVsmnt_J1mQfZasfJtN33dcZ3d1lJroxmgmMu69zjGEAwkNbMQaWNLqC4eogkJaeJ4RR5MHYXkL9nNilVoTkjX5BVit3puzs7XJ7WQnKQgQMI-ezn24GHsZ_v1JIo77lerX5k4HNwTNVt-yaZVQWaOMR3-6FwziQR6kd0VuG9_a9dgAnz2cEoORRC1i4W7IZaB1sZnh1WbHbevlGd72HSXll5rocPIHn8gq6xpBgpHwRphlRsgn4KHaJ6brXDIJjrnQhIe_YUBOGj_ImSEXhRwlFerKsoAVnZ0Hwbfa46qk44TAt8CyoPMWmpK6pt0ng4pQ2uw", + n: "xsRuvCkgJtflBTl4OVsmnt_J1mQfZasfJtN33dcZ3d1lJroxmgmMu69zjGEAwkNbMQaWN" + + "LqC4eogkJaeJ4RR5MHYXkL9nNilVoTkjX5BVit3puzs7XJ7WQnKQgQMI-ezn24GHsZ_v1J" + + "Io77lerX5k4HNwTNVt-yaZVQWaOMR3-6FwziQR6kd0VuG9_a9dgAnz2cEoORRC1i4W7IZa" + + "B1sZnh1WbHbevlGd72HSXll5rocPIHn8gq6xpBgpHwRphlRsgn4KHaJ6brXDIJjrnQhIe_" + + "YUBOGj_ImSEXhRwlFerKsoAVnZ0Hwbfa46qk44TAt8CyoPMWmpK6pt0ng4pQ2uw", e: "AQAB", // cSpell: enable key_ops: ["verify"], @@ -324,7 +330,7 @@ Deno.test("Key.publicKey", async () => { "Ie/YUBOGj/ImSEXhRwlFerKsoAVnZ0Hwbfa46qk44TAt8CyoPMWmpK6pt0ng4pQ2\n" + "uwIDAQAB\n" + // cSpell: enable - "-----END PUBLIC KEY-----", + "-----END PUBLIC KEY-----\n", type: "CryptographicKey", }); const loadedKey = await CryptographicKey.fromJsonLd(jsonLd, {