/** * Multiformats codec "jwk_jcs-pub". * Proposed: https://github.com/multiformats/multicodec/pull/307 * * Implements the BlockCodec interface. * @see https://github.com/multiformats/js-multiformats/blob/master/src/codecs/interface.ts */ import type { JsonWebKey } from "did-resolver"; import type { ByteView } from "multiformats/codecs/interface"; import type { Jsonify, JsonObject } from "type-fest"; import isPlainObject from "lodash.isplainobject"; import { fromString, toString } from "uint8arrays"; export const name = "jwk_jcs-pub"; export const code = 0xeb_51; type ECJsonWebKey = JsonWebKey & { crv: string; kty: "EC"; x: string; y: string; }; type OKPJsonWebKey = JsonWebKey & { crv: string; kty: "OKP"; x: string; }; type RSAJsonWebKey = JsonWebKey & { e: string; kty: "RSA"; n: string; }; type SupportedJsonWebKey = ECJsonWebKey | OKPJsonWebKey | RSAJsonWebKey; /** * Decodes an array of bytes into a JWK. Throws an error if the JWK is not valid. * @param bytes - The array of bytes to decode. * @returns The corresponding JSON Web Key. */ export function decode(bytes: ByteView): JsonWebKey { const jwk = JSON.parse(toString(bytes)) as Jsonify; validateJwk(jwk); if (JSON.stringify(jwk) !== JSON.stringify(canonicaliseJwk(jwk))) { throw new Error("The JWK embedded in the DID is not correctly formatted"); } return jwk; } /** * Encodes a JWK into a Uint8Array. Only the required JWK members are encoded. * @see https://www.rfc-editor.org/rfc/rfc7518#section-6 * @see https://www.rfc-editor.org/rfc/rfc8037#section-2 * @see https://github.com/panva/jose/blob/3b8aa47b92d07a711bf5c3125276cc9a011794a4/src/jwk/thumbprint.ts#L37 * @param jwk - JSON Web Key. * @returns Uint8Array-encoded JWK. */ export function encode(jwk: unknown): Uint8Array { validateJwk(jwk); // Keep only the JWK required members const components = canonicaliseJwk(jwk); return fromString(JSON.stringify(components), "utf8"); } /** * Checks if the JWK is valid. It must contain all the required members. * @see https://www.rfc-editor.org/rfc/rfc7518#section-6 * @see https://www.rfc-editor.org/rfc/rfc8037#section-2 * @param jwk - The JWK to check. */ export function validateJwk(jwk: unknown): asserts jwk is SupportedJsonWebKey { validatePlainObject(jwk); // Check JWK required members based on the key type switch (jwk["kty"]) { /** * @see https://www.rfc-editor.org/rfc/rfc7518#section-6.2.1 */ case "EC": { check(jwk["crv"], '"crv" (Curve) Parameter'); check(jwk["x"], '"x" (X Coordinate) Parameter'); check(jwk["y"], '"y" (Y Coordinate) Parameter'); break; } /** * @see https://www.rfc-editor.org/rfc/rfc8037#section-2 */ case "OKP": { check(jwk["crv"], '"crv" (Subtype of Key Pair) Parameter'); check(jwk["x"], '"x" (Public Key) Parameter'); break; } /** * @see https://www.rfc-editor.org/rfc/rfc7518#section-6.3.1 */ case "RSA": { check(jwk["e"], '"e" (Exponent) Parameter'); check(jwk["n"], '"n" (Modulus) Parameter'); break; } default: { throw new Error('"kty" (Key Type) Parameter missing or unsupported'); } } } /** * Extracts the required members of the JWK and canonicalises it. * This method is not part of the BlockCodec interface. * @param jwk - The JWK to canonicalise. * @returns The JWK with only the required members, ordered lexicographically. */ function canonicaliseJwk(jwk: SupportedJsonWebKey): SupportedJsonWebKey { let components: SupportedJsonWebKey; // "default" case is not needed switch (jwk.kty) { case "EC": { components = { crv: jwk.crv, kty: jwk.kty, x: jwk.x, y: jwk.y }; break; } case "OKP": { components = { crv: jwk.crv, kty: jwk.kty, x: jwk.x }; break; } case "RSA": { components = { e: jwk.e, kty: jwk.kty, n: jwk.n }; break; } } return components; } /** * Checks if the value is a non-empty string. * @param value - The value to check. * @param description - Description of the value to check. */ function check(value: unknown, description: string): asserts value is string { if (typeof value !== "string" || !value) { throw new Error(`${description} missing or invalid`); } } /** * Checks if the value is a valid JSON object. * @param value - The value to check. */ function validatePlainObject(value: unknown): asserts value is JsonObject { if (!isPlainObject(value)) { throw new Error("JWK must be an object"); } }