@sphereon/ssi-sdk-ext.key-utils
Version:
Sphereon SSI-SDK plugin for key creation.
1 lines • 102 kB
Source Map (JSON)
{"version":3,"sources":["../src/index.ts","../src/functions.ts","../src/digest-methods.ts","../src/jwk-jcs.ts","../src/types/key-util-types.ts","../src/conversion.ts"],"sourcesContent":["/**\n * Provides `did:jwk` {@link @veramo/did-provider-jwk#JwkDIDProvider | identifier provider }\n * for the {@link @veramo/did-manager#DIDManager}\n *\n * @packageDocumentation\n */\nexport * from './functions'\nexport * from './conversion'\nexport * from './jwk-jcs'\nexport * from './types'\nexport * from './digest-methods'\n","import { randomBytes } from '@ethersproject/random'\n// Do not change these require statements to imports before we change to ESM. Breaks external CJS packages depending on this module\nimport { bls12_381 } from '@noble/curves/bls12-381'\nimport { ed25519, x25519 } from '@noble/curves/ed25519'\nimport { p256 } from '@noble/curves/p256'\nimport { p384 } from '@noble/curves/p384'\nimport { p521 } from '@noble/curves/p521'\nimport { secp256k1 } from '@noble/curves/secp256k1'\nimport { sha256, sha384, sha512 } from '@noble/hashes/sha2'\nimport {\n cryptoSubtleImportRSAKey,\n generateRSAKeyAsPEM,\n hexToBase64,\n hexToPEM,\n PEMToJwk,\n privateKeyHexFromPEM,\n} from '@sphereon/ssi-sdk-ext.x509-utils'\nimport { JoseCurve, JoseSignatureAlgorithm, type JWK, JwkKeyType, Loggers } from '@sphereon/ssi-types'\nimport { generateKeyPair as generateSigningKeyPair } from '@stablelib/ed25519'\nimport type { IAgentContext, IKey, IKeyManager, ManagedKeyInfo, MinimalImportableKey } from '@veramo/core'\nimport debug from 'debug'\n\nimport type { JsonWebKey } from 'did-resolver'\nimport elliptic from 'elliptic'\nimport * as rsa from 'micro-rsa-dsa-dh/rsa.js'\n\n// @ts-ignore\nimport { Crypto } from 'node'\n// @ts-ignore\nimport * as u8a from 'uint8arrays'\nimport { digestMethodParams } from './digest-methods'\nimport { validateJwk } from './jwk-jcs'\nimport {\n DigestAlgorithm,\n ENC_KEY_ALGS,\n type IImportProvidedOrGeneratedKeyArgs,\n JwkKeyUse,\n type KeyTypeFromCryptographicSuiteArgs,\n SIG_KEY_ALGS,\n type SignatureAlgorithmFromKeyArgs,\n type SignatureAlgorithmFromKeyTypeArgs,\n type TKeyType,\n} from './types'\n\nconst { fromString, toString } = u8a\n\nexport const logger = Loggers.DEFAULT.get('sphereon:key-utils')\n\n/**\n * Function that returns the provided KMS name or the default KMS name if none is provided.\n * The default KMS is either explicitly defined during agent construction, or the first KMS available in the system\n * @param context\n * @param kms. Optional KMS to use. If provided will be the returned name. Otherwise the default KMS will be returned\n */\nexport const getKms = async (context: IAgentContext<any>, kms?: string): Promise<string> => {\n if (kms) {\n return kms\n }\n if (!context.agent.availableMethods().includes('keyManagerGetDefaultKeyManagementSystem')) {\n throw Error('Cannot determine default KMS if not provided and a non Sphereon Key Manager is being used')\n }\n return context.agent.keyManagerGetDefaultKeyManagementSystem()\n}\n\n/**\n * Generates a random Private Hex Key for the specified key type\n * @param type The key type\n * @return The private key in Hex form\n */\nexport const generatePrivateKeyHex = async (type: TKeyType): Promise<string> => {\n switch (type) {\n case 'Ed25519': {\n const keyPairEd25519 = generateSigningKeyPair()\n return toString(keyPairEd25519.secretKey, 'base16')\n }\n // The Secp256 types use the same method to generate the key\n case 'Secp256r1':\n case 'Secp256k1': {\n const privateBytes = randomBytes(32)\n return toString(privateBytes, 'base16')\n }\n case 'RSA': {\n const pem = await generateRSAKeyAsPEM('RSA-PSS', 'SHA-256', 2048)\n return privateKeyHexFromPEM(pem)\n }\n default:\n throw Error(`not_supported: Key type ${type} not yet supported for this did:jwk implementation`)\n }\n}\n\nconst keyMetaAlgorithmsFromKeyType = (type: string | TKeyType) => {\n switch (type) {\n case 'Ed25519':\n return ['Ed25519', 'EdDSA']\n case 'ES256K':\n case 'Secp256k1':\n return ['ES256K', 'ES256K-R', 'eth_signTransaction', 'eth_signTypedData', 'eth_signMessage', 'eth_rawSign']\n case 'Secp256r1':\n return ['ES256']\n case 'X25519':\n return ['ECDH', 'ECDH-ES', 'ECDH-1PU']\n case 'RSA':\n return ['RS256', 'RS512', 'PS256', 'PS512']\n }\n return [type]\n}\n\n/**\n * We optionally generate and then import our own keys.\n *\n * @param args The key arguments\n * @param context The Veramo agent context\n * @private\n */\nexport async function importProvidedOrGeneratedKey(\n args: IImportProvidedOrGeneratedKeyArgs & {\n kms: string\n },\n context: IAgentContext<IKeyManager>,\n): Promise<IKey> {\n // @ts-ignore\n const type = args.options?.type ?? args.options?.key?.type ?? args.options?.keyType ?? 'Secp256r1'\n const key = args?.options?.key\n if (key) {\n key.meta = {\n ...key.meta,\n providerName: args.providerName,\n }\n\n // Make sure x509 options are also set on the metadata as that is what the kms will look for\n if (args.options?.x509) {\n key.meta = {\n ...key.meta,\n x509: {\n ...args.options.x509,\n ...key.meta?.x509,\n },\n }\n }\n }\n\n if (args.options && args.options?.use === JwkKeyUse.Encryption && !ENC_KEY_ALGS.includes(type)) {\n throw new Error(`${type} keys are not valid for encryption`)\n }\n\n let privateKeyHex: string | undefined = undefined\n if (key) {\n privateKeyHex = key.privateKeyHex ?? key.meta?.x509?.privateKeyHex\n if ((!privateKeyHex || privateKeyHex.trim() === '') && key?.meta?.x509?.privateKeyPEM) {\n // If we do not have a privateKeyHex but do have a PEM\n privateKeyHex = privateKeyHexFromPEM(key.meta.x509.privateKeyPEM)\n }\n }\n if (privateKeyHex) {\n return context.agent.keyManagerImport({\n ...key,\n kms: args.kms,\n type,\n privateKeyHex: privateKeyHex!,\n })\n }\n\n return context.agent.keyManagerCreate({\n type,\n kms: args.kms,\n meta: {\n ...key?.meta,\n algorithms: keyMetaAlgorithmsFromKeyType(type),\n ...(key?.meta?.keyAlias ? {} : { keyAlias: args.alias }),\n },\n })\n}\n\nexport const calculateJwkThumbprintForKey = (args: {\n key: IKey | MinimalImportableKey | ManagedKeyInfo\n digestAlgorithm?: 'sha256' | 'sha512'\n}): string => {\n const { key } = args\n\n const jwk = key.publicKeyHex\n ? toJwk(key.publicKeyHex, key.type, { key: key, isPrivateKey: false })\n : 'privateKeyHex' in key && key.privateKeyHex\n ? toJwk(key.privateKeyHex, key.type, { isPrivateKey: true })\n : undefined\n if (!jwk) {\n throw Error(`Could not determine jwk from key ${key.kid}`)\n }\n return calculateJwkThumbprint({ jwk, digestAlgorithm: args.digestAlgorithm })\n}\n\nconst assertJwkClaimPresent = (value: unknown, description: string) => {\n if (typeof value !== 'string' || !value) {\n throw new Error(`${description} missing or invalid`)\n }\n}\nexport const toBase64url = (input: string): string => toString(fromString(input), 'base64url')\n\n/**\n * Calculate the JWK thumbprint\n * @param args\n */\nexport const calculateJwkThumbprint = (args: { jwk: JWK; digestAlgorithm?: DigestAlgorithm }): string => {\n const digestAlgorithm = normalizeHashAlgorithm(args.digestAlgorithm ?? 'SHA-256')\n const jwk = sanitizedJwk(args.jwk)\n let components\n switch (jwk.kty) {\n case 'EC':\n assertJwkClaimPresent(jwk.crv, '\"crv\" (Curve) Parameter')\n assertJwkClaimPresent(jwk.x, '\"x\" (X Coordinate) Parameter')\n assertJwkClaimPresent(jwk.y, '\"y\" (Y Coordinate) Parameter')\n components = { crv: jwk.crv, kty: jwk.kty, x: jwk.x, y: jwk.y }\n break\n case 'OKP':\n assertJwkClaimPresent(jwk.crv, '\"crv\" (Subtype of Key Pair) Parameter')\n assertJwkClaimPresent(jwk.x, '\"x\" (Public Key) Parameter')\n components = { crv: jwk.crv, kty: jwk.kty, x: jwk.x }\n break\n case 'RSA':\n assertJwkClaimPresent(jwk.e, '\"e\" (Exponent) Parameter')\n assertJwkClaimPresent(jwk.n, '\"n\" (Modulus) Parameter')\n components = { e: jwk.e, kty: jwk.kty, n: jwk.n }\n break\n case 'oct':\n assertJwkClaimPresent(jwk.k, '\"k\" (Key Value) Parameter')\n components = { k: jwk.k, kty: jwk.kty }\n break\n default:\n throw new Error('\"kty\" (Key Type) Parameter missing or unsupported')\n }\n const data = JSON.stringify(components)\n return digestMethodParams(digestAlgorithm).digestMethod(data, 'base64url')\n}\n\nexport const toJwkFromKey = (\n key: IKey | MinimalImportableKey | ManagedKeyInfo,\n opts?: {\n use?: JwkKeyUse\n noKidThumbprint?: boolean\n },\n): JWK => {\n const isPrivateKey = 'privateKeyHex' in key\n return toJwk(key.publicKeyHex!, key.type, { ...opts, key, isPrivateKey })\n}\n\n/**\n * Converts a public key in hex format to a JWK\n * @param publicKeyHex public key in hex\n * @param type The type of the key (Ed25519, Secp256k1/r1)\n * @param opts. Options, like the optional use for the key (sig/enc)\n * @return The JWK\n */\nexport const toJwk = (\n publicKeyHex: string,\n type: TKeyType,\n opts?: { use?: JwkKeyUse; key?: IKey | MinimalImportableKey; isPrivateKey?: boolean; noKidThumbprint?: boolean },\n): JWK => {\n const { key, noKidThumbprint = false } = opts ?? {}\n if (key && key.publicKeyHex !== publicKeyHex && opts?.isPrivateKey !== true) {\n throw Error(`Provided key with id ${key.kid}, has a different public key hex ${key.publicKeyHex} than supplied public key ${publicKeyHex}`)\n }\n let jwk: JWK\n switch (type) {\n case 'Ed25519':\n jwk = toEd25519OrX25519Jwk(publicKeyHex, { ...opts, crv: JoseCurve.Ed25519 })\n break\n case 'X25519':\n jwk = toEd25519OrX25519Jwk(publicKeyHex, { ...opts, crv: JoseCurve.X25519 })\n break\n case 'Secp256k1':\n jwk = toSecp256k1Jwk(publicKeyHex, opts)\n break\n case 'Secp256r1':\n jwk = toSecp256r1Jwk(publicKeyHex, opts)\n break\n case 'RSA':\n jwk = toRSAJwk(publicKeyHex, opts)\n break\n default:\n throw new Error(`not_supported: Key type ${type} not yet supported for this did:jwk implementation`)\n }\n if (!jwk.kid && !noKidThumbprint) {\n jwk['kid'] = calculateJwkThumbprint({ jwk })\n }\n return sanitizedJwk(jwk)\n}\n\n/**\n * Convert a JWK to a raw hex key.\n * Currently supports `RSA` and `EC` keys. Extendable for other key types.\n * @param jwk - The JSON Web Key object.\n * @returns A string representing the key in raw hexadecimal format.\n */\nexport const jwkToRawHexKey = async (jwk: JWK): Promise<string> => {\n // TODO: Probably makes sense to have an option to do the same for private keys\n jwk = sanitizedJwk(jwk)\n if (jwk.kty === 'RSA') {\n return rsaJwkToRawHexKey(jwk)\n } else if (jwk.kty === 'EC') {\n return ecJwkToRawHexKey(jwk)\n } else if (jwk.kty === 'OKP') {\n return okpJwkToRawHexKey(jwk)\n } else if (jwk.kty === 'oct') {\n return octJwkToRawHexKey(jwk)\n } else {\n throw new Error(`Unsupported key type: ${jwk.kty}`)\n }\n}\n\n/**\n * Convert an RSA JWK to a raw hex key.\n * @param jwk - The RSA JWK object.\n * @returns A string representing the RSA key in raw hexadecimal format.\n */\nexport function rsaJwkToRawHexKey(jwk: JsonWebKey): string {\n /**\n * Encode an integer value (given as a Uint8Array) into DER INTEGER:\n * 0x02 || length || value (with a leading 0x00 if the high bit is set).\n */\n function encodeInteger(bytes: Uint8Array): Uint8Array {\n // if high bit set, prefix a 0x00\n if (bytes[0] & 0x80) {\n bytes = Uint8Array.from([0x00, ...bytes])\n }\n const len = encodeLength(bytes.length)\n return Uint8Array.from([0x02, ...len, ...bytes])\n }\n\n /**\n * Encode length per DER rules:\n * - If <128, one byte\n * - Else 0x80|numBytes followed by big-endian length\n */\n function encodeLength(len: any) {\n if (len < 0x80) {\n return Uint8Array.of(len)\n }\n let hex = len.toString(16)\n if (hex.length % 2 === 1) {\n hex = '0' + hex\n }\n const lenBytes = Uint8Array.from(hex.match(/.{2}/g)!.map((h: any) => parseInt(h, 16)))\n return Uint8Array.of(0x80 | lenBytes.length, ...lenBytes)\n }\n\n /**\n * Wrap one or more DER elements in a SEQUENCE:\n * 0x30 || totalLength || concatenatedElements\n */\n function encodeSequence(elements: any) {\n const content = elements.reduce((acc: any, elm: any) => Uint8Array.from([...acc, ...elm]), new Uint8Array())\n const len = encodeLength(content.length)\n return Uint8Array.from([0x30, ...len, ...content])\n }\n\n /**\n * Convert a Base64-URL string into a Uint8Array (handles padding & “-_/”).\n */\n function base64UrlToBytes(b64url: string): Uint8Array {\n return fromString(b64url, 'base64url')\n }\n\n jwk = sanitizedJwk(jwk)\n if (!jwk.n || !jwk.e) {\n throw new Error(\"RSA JWK must contain 'n' and 'e' properties.\")\n }\n const modulusBytes = base64UrlToBytes(jwk.n)\n const exponentBytes = base64UrlToBytes(jwk.e)\n const sequence = encodeSequence([encodeInteger(modulusBytes), encodeInteger(exponentBytes)])\n const result = toString(sequence, 'hex')\n return result\n /*\n // We are converting from base64 to base64url to be sure. The spec uses base64url, but in the wild we sometimes encounter a base64 string\n const modulus = fromString(jwk.n.replace(/\\+/g, '-').replace(/\\//g, '_').replace(/=+$/, ''), 'base64url') // 'n' is the modulus\n const exponent = fromString(jwk.e.replace(/\\+/g, '-').replace(/\\//g, '_').replace(/=+$/, ''), 'base64url') // 'e' is the exponent\n\n return toString(modulus, 'hex') + toString(exponent, 'hex')*/\n}\n\n/**\n * Convert an EC JWK to a raw hex key.\n * @param jwk - The EC JWK object.\n * @returns A string representing the EC key in raw hexadecimal format.\n */\nfunction ecJwkToRawHexKey(jwk: JsonWebKey): string {\n jwk = sanitizedJwk(jwk)\n if (!jwk.x || !jwk.y) {\n throw new Error(\"EC JWK must contain 'x' and 'y' properties.\")\n }\n\n // We are converting from base64 to base64url to be sure. The spec uses base64url, but in the wild we sometimes encounter a base64 string\n const x = fromString(jwk.x.replace(/\\+/g, '-').replace(/\\//g, '_').replace(/=+$/, ''), 'base64url')\n const y = fromString(jwk.y.replace(/\\+/g, '-').replace(/\\//g, '_').replace(/=+$/, ''), 'base64url')\n\n return '04' + toString(x, 'hex') + toString(y, 'hex')\n}\n\n/**\n * Convert an EC JWK to a raw hex key.\n * @param jwk - The EC JWK object.\n * @returns A string representing the EC key in raw hexadecimal format.\n */\nfunction okpJwkToRawHexKey(jwk: JsonWebKey): string {\n jwk = sanitizedJwk(jwk)\n if (!jwk.x) {\n throw new Error(\"OKP JWK must contain 'x' property.\")\n }\n\n // We are converting from base64 to base64url to be sure. The spec uses base64url, but in the wild we sometimes encounter a base64 string\n const x = fromString(jwk.x.replace(/\\+/g, '-').replace(/\\//g, '_').replace(/=+$/, ''), 'base64url')\n\n return toString(x, 'hex')\n}\n\n/**\n * Convert an octet JWK to a raw hex key.\n * @param jwk - The octet JWK object.\n * @returns A string representing the octet key in raw hexadecimal format.\n */\nfunction octJwkToRawHexKey(jwk: JsonWebKey): string {\n jwk = sanitizedJwk(jwk)\n if (!jwk.k) {\n throw new Error(\"Octet JWK must contain 'k' property.\")\n }\n\n // We are converting from base64 to base64url to be sure. The spec uses base64url, but in the wild we sometimes encounter a base64 string\n const key = fromString(jwk.k.replace(/\\+/g, '-').replace(/\\//g, '_').replace(/=+$/, ''), 'base64url')\n\n return toString(key, 'hex')\n}\n\nexport function x25519PublicHexFromPrivateHex(privateKeyHex: string): string {\n if (!/^[0-9a-fA-F]{64}$/.test(privateKeyHex)) {\n throw new Error('Private key must be 32-byte hex (64 chars)')\n }\n\n const priv = Uint8Array.from(Buffer.from(privateKeyHex, 'hex'))\n const pub = x25519.getPublicKey(priv)\n\n return Buffer.from(pub).toString('hex')\n}\n\n/**\n * Determines the use param based upon the key/signature type or supplied use value.\n *\n * @param type The key type\n * @param suppliedUse A supplied use. Will be used in case it is present\n */\nexport const jwkDetermineUse = (type: TKeyType, suppliedUse?: JwkKeyUse): JwkKeyUse | undefined => {\n return suppliedUse\n ? suppliedUse\n : SIG_KEY_ALGS.includes(type)\n ? JwkKeyUse.Signature\n : ENC_KEY_ALGS.includes(type)\n ? JwkKeyUse.Encryption\n : undefined\n}\n\n/**\n * Assert the key has a proper length\n *\n * @param keyHex Input key\n * @param expectedKeyLength Expected key length(s)\n */\nconst assertProperKeyLength = (keyHex: string, expectedKeyLength: number | number[]) => {\n if (Array.isArray(expectedKeyLength)) {\n if (!expectedKeyLength.includes(keyHex.length)) {\n throw Error(\n `Invalid key length. Needs to be a hex string with length from ${JSON.stringify(expectedKeyLength)} instead of ${\n keyHex.length\n }. Input: ${keyHex}`,\n )\n }\n } else if (keyHex.length !== expectedKeyLength) {\n throw Error(`Invalid key length. Needs to be a hex string with length ${expectedKeyLength} instead of ${keyHex.length}. Input: ${keyHex}`)\n }\n}\n\n/**\n * Generates a JWK from a Secp256k1 public key\n * @param keyHex Secp256k1 public or private key in hex\n * @param use The use for the key\n * @return The JWK\n */\nconst toSecp256k1Jwk = (keyHex: string, opts?: { use?: JwkKeyUse; isPrivateKey?: boolean }): JWK => {\n const { use } = opts ?? {}\n logger.debug(`toSecp256k1Jwk keyHex: ${keyHex}, length: ${keyHex.length}`)\n if (opts?.isPrivateKey) {\n assertProperKeyLength(keyHex, [64])\n } else {\n assertProperKeyLength(keyHex, [66, 130])\n }\n\n const secp256k1 = new elliptic.ec('secp256k1')\n const keyBytes = fromString(keyHex, 'base16')\n const keyPair = opts?.isPrivateKey ? secp256k1.keyFromPrivate(keyBytes) : secp256k1.keyFromPublic(keyBytes)\n const pubPoint = keyPair.getPublic()\n\n return sanitizedJwk({\n alg: JoseSignatureAlgorithm.ES256K,\n ...(use !== undefined && { use }),\n kty: JwkKeyType.EC,\n crv: JoseCurve.secp256k1,\n x: hexToBase64(pubPoint.getX().toString('hex').padStart(64, '0'), 'base64url'),\n y: hexToBase64(pubPoint.getY().toString('hex').padStart(64, '0'), 'base64url'),\n ...(opts?.isPrivateKey && { d: hexToBase64(keyPair.getPrivate('hex'), 'base64url') }),\n })\n}\n\n/**\n * Generates a JWK from a Secp256r1 public key\n * @param keyHex Secp256r1 public key in hex\n * @param use The use for the key\n * @return The JWK\n */\nconst toSecp256r1Jwk = (keyHex: string, opts?: { use?: JwkKeyUse; isPrivateKey?: boolean }): JWK => {\n const { use } = opts ?? {}\n logger.debug(`toSecp256r1Jwk keyHex: ${keyHex}, length: ${keyHex.length}`)\n if (opts?.isPrivateKey) {\n assertProperKeyLength(keyHex, [64])\n } else {\n assertProperKeyLength(keyHex, [66, 130])\n }\n\n const secp256r1 = new elliptic.ec('p256')\n const keyBytes = fromString(keyHex, 'base16')\n logger.debug(`keyBytes length: ${keyBytes}`)\n const keyPair = opts?.isPrivateKey ? secp256r1.keyFromPrivate(keyBytes) : secp256r1.keyFromPublic(keyBytes)\n const pubPoint = keyPair.getPublic()\n return sanitizedJwk({\n alg: JoseSignatureAlgorithm.ES256,\n ...(use !== undefined && { use }),\n kty: JwkKeyType.EC,\n crv: JoseCurve.P_256,\n x: hexToBase64(pubPoint.getX().toString('hex').padStart(64, '0'), 'base64url'),\n y: hexToBase64(pubPoint.getY().toString('hex').padStart(64, '0'), 'base64url'),\n ...(opts?.isPrivateKey && { d: hexToBase64(keyPair.getPrivate('hex'), 'base64url') }),\n })\n}\n\n/**\n * Generates a JWK from an Ed25519/X25519 public key\n * @param publicKeyHex Ed25519/X25519 public key in hex\n * @param opts\n * @return The JWK\n */\nconst toEd25519OrX25519Jwk = (\n publicKeyHex: string,\n opts: {\n use?: JwkKeyUse\n crv: JoseCurve.Ed25519 | JoseCurve.X25519\n },\n): JWK => {\n assertProperKeyLength(publicKeyHex, 64)\n const { use } = opts ?? {}\n return sanitizedJwk({\n alg: JoseSignatureAlgorithm.EdDSA,\n ...(use !== undefined && { use }),\n kty: JwkKeyType.OKP,\n crv: opts?.crv ?? JoseCurve.Ed25519,\n x: hexToBase64(publicKeyHex, 'base64url'),\n })\n}\n\nconst toRSAJwk = (publicKeyHex: string, opts?: { use?: JwkKeyUse; key?: IKey | MinimalImportableKey }): JWK => {\n function parseDerIntegers(pubKeyHex: string): { modulus: string; exponent: string } {\n const bytes = Buffer.from(pubKeyHex, 'hex')\n let offset = 0\n\n // 1) Outer SEQUENCE\n if (bytes[offset++] !== 0x30) throw new Error('Not a SEQUENCE')\n let len = bytes[offset++]\n if (len & 0x80) {\n const nBytes = len & 0x7f\n len = 0\n for (let i = 0; i < nBytes; i++) {\n len = (len << 8) + bytes[offset++]\n }\n }\n\n // 2) Look at next tag: INTEGER(0x02) means raw PKCS#1,\n // otherwise assume X.509/SPKI wrapper.\n if (bytes[offset] !== 0x02) {\n // --- skip AlgorithmIdentifier SEQUENCE ---\n if (bytes[offset++] !== 0x30) throw new Error('Expected alg-ID SEQUENCE')\n let algLen = bytes[offset++]\n if (algLen & 0x80) {\n const nB = algLen & 0x7f\n algLen = 0\n for (let i = 0; i < nB; i++) algLen = (algLen << 8) + bytes[offset++]\n }\n offset += algLen\n\n // --- skip BIT STRING wrapper ---\n if (bytes[offset++] !== 0x03) throw new Error('Expected BIT STRING')\n let bitLen = bytes[offset++]\n if (bitLen & 0x80) {\n const nB = bitLen & 0x7f\n bitLen = 0\n for (let i = 0; i < nB; i++) bitLen = (bitLen << 8) + bytes[offset++]\n }\n // skip the “unused bits” byte\n offset += 1\n\n // now the next byte should be 0x30 for the inner SEQUENCE\n if (bytes[offset++] !== 0x30) throw new Error('Expected inner SEQUENCE')\n let innerLen = bytes[offset++]\n if (innerLen & 0x80) {\n const nB = innerLen & 0x7f\n innerLen = 0\n for (let i = 0; i < nB; i++) innerLen = (innerLen << 8) + bytes[offset++]\n }\n }\n\n // 3) Parse modulus INTEGER\n if (bytes[offset++] !== 0x02) throw new Error('Expected INTEGER for modulus')\n let modLen = bytes[offset++]\n if (modLen & 0x80) {\n const nB = modLen & 0x7f\n modLen = 0\n for (let i = 0; i < nB; i++) modLen = (modLen << 8) + bytes[offset++]\n }\n let modulusBytes = bytes.slice(offset, offset + modLen)\n offset += modLen\n\n // strip leading zero if present (unsigned integer in JWK)\n if (modulusBytes[0] === 0x00) {\n modulusBytes = modulusBytes.slice(1)\n }\n\n // 4) Parse exponent INTEGER\n if (bytes[offset++] !== 0x02) throw new Error('Expected INTEGER for exponent')\n let expLen = bytes[offset++]\n if (expLen & 0x80) {\n const nB = expLen & 0x7f\n expLen = 0\n for (let i = 0; i < nB; i++) expLen = (expLen << 8) + bytes[offset++]\n }\n const exponentBytes = bytes.slice(offset, offset + expLen)\n\n return {\n modulus: modulusBytes.toString('hex'),\n exponent: exponentBytes.toString('hex'),\n }\n }\n\n const meta = opts?.key?.meta\n if (meta?.publicKeyJwk || meta?.publicKeyPEM) {\n if (meta?.publicKeyJwk) {\n return meta.publicKeyJwk as JWK\n }\n const publicKeyPEM = meta?.publicKeyPEM ?? hexToPEM(publicKeyHex, 'public')\n const jwk = PEMToJwk(publicKeyPEM, 'public') as JWK\n return jwk\n }\n\n const { modulus, exponent } = parseDerIntegers(publicKeyHex)\n const sanitized = sanitizedJwk({\n kty: 'RSA',\n n: hexToBase64(modulus, 'base64url'),\n e: hexToBase64(exponent, 'base64url'),\n })\n return sanitized\n}\n\nexport const padLeft = (args: { data: string; size?: number; padString?: string }): string => {\n const { data } = args\n const size = args.size ?? 32\n const padString = args.padString ?? '0'\n if (data.length >= size) {\n return data\n }\n\n if (padString && padString.length === 0) {\n throw Error(`Pad string needs to have at least a length of 1`)\n }\n const length = padString.length\n return padString.repeat((size - data.length) / length) + data\n}\n\nenum OIDType {\n Secp256k1,\n Secp256r1,\n Ed25519,\n}\n\nconst OID: Record<OIDType, Uint8Array> = {\n [OIDType.Secp256k1]: new Uint8Array([0x06, 0x07, 0x2a, 0x86, 0x48, 0xce, 0x3d, 0x02, 0x01]),\n [OIDType.Secp256r1]: new Uint8Array([0x06, 0x08, 0x2a, 0x86, 0x48, 0xce, 0x3d, 0x03, 0x01, 0x07]),\n [OIDType.Ed25519]: new Uint8Array([0x06, 0x03, 0x2b, 0x65, 0x70]),\n}\n\nconst compareUint8Arrays = (a: Uint8Array, b: Uint8Array): boolean => {\n if (a.length !== b.length) {\n return false\n }\n for (let i = 0; i < a.length; i++) {\n if (a[i] !== b[i]) {\n return false\n }\n }\n return true\n}\n\nconst findSubarray = (haystack: Uint8Array, needle: Uint8Array): number => {\n for (let i = 0; i <= haystack.length - needle.length; i++) {\n if (compareUint8Arrays(haystack.subarray(i, i + needle.length), needle)) {\n return i\n }\n }\n return -1\n}\n\nconst getTargetOID = (keyType: TKeyType) => {\n switch (keyType) {\n case 'Secp256k1':\n return OID[OIDType.Secp256k1]\n case 'Secp256r1':\n return OID[OIDType.Secp256r1]\n case 'Ed25519':\n return OID[OIDType.Ed25519]\n default:\n throw new Error(`Unsupported key type: ${keyType}`)\n }\n}\n\nexport const isAsn1Der = (key: Uint8Array): boolean => key[0] === 0x30\n\nexport const asn1DerToRawPublicKey = (derKey: Uint8Array, keyType: TKeyType): Uint8Array => {\n if (!isAsn1Der(derKey)) {\n throw new Error('Invalid DER encoding: Expected to start with sequence tag')\n }\n\n let index = 2\n if (derKey[1] & 0x80) {\n const lengthBytesCount = derKey[1] & 0x7f\n index += lengthBytesCount\n }\n const targetOid = getTargetOID(keyType)\n const oidIndex = findSubarray(derKey, targetOid)\n if (oidIndex === -1) {\n throw new Error(`OID for ${keyType} not found in DER encoding`)\n }\n\n index = oidIndex + targetOid.length\n\n while (index < derKey.length && derKey[index] !== 0x03) {\n index++\n }\n\n if (index >= derKey.length) {\n throw new Error('Invalid DER encoding: Bit string not found')\n }\n\n // Skip the bit string tag (0x03) and length byte\n index += 2\n\n // Skip the unused bits count byte\n index++\n\n return derKey.slice(index)\n}\n\nexport const isRawCompressedPublicKey = (key: Uint8Array): boolean => key.length === 33 && (key[0] === 0x02 || key[0] === 0x03)\n\nexport const toRawCompressedHexPublicKey = (rawPublicKey: Uint8Array, keyType: TKeyType): string => {\n if (isRawCompressedPublicKey(rawPublicKey)) {\n return hexStringFromUint8Array(rawPublicKey)\n }\n\n if (keyType === 'Secp256k1' || keyType === 'Secp256r1') {\n if (rawPublicKey[0] === 0x04 && rawPublicKey.length === 65) {\n const xCoordinate = rawPublicKey.slice(1, 33)\n const yCoordinate = rawPublicKey.slice(33)\n const prefix = new Uint8Array([yCoordinate[31] % 2 === 0 ? 0x02 : 0x03])\n const resultKey = hexStringFromUint8Array(new Uint8Array([...prefix, ...xCoordinate]))\n logger.debug(`converted public key ${hexStringFromUint8Array(rawPublicKey)} to ${resultKey}`)\n return resultKey\n }\n return toString(rawPublicKey, 'base16')\n } else if (keyType === 'Ed25519') {\n // Ed25519 keys are always in compressed form\n return toString(rawPublicKey, 'base16')\n }\n\n throw new Error(`Unsupported key type: ${keyType}`)\n}\n\nexport const hexStringFromUint8Array = (value: Uint8Array): string => toString(value, 'base16')\n\nexport const signatureAlgorithmFromKey = async (args: SignatureAlgorithmFromKeyArgs): Promise<JoseSignatureAlgorithm> => {\n const { key } = args\n return signatureAlgorithmFromKeyType({ type: key.type, algorithms: key.meta?.algorithms })\n}\n\nexport function signatureAlgorithmToJoseAlgorithm(alg: string): JoseSignatureAlgorithm {\n switch (alg) {\n case 'RSA_SHA256':\n return JoseSignatureAlgorithm.RS256\n case 'RSA_SHA384':\n return JoseSignatureAlgorithm.RS384\n case 'RSA_SHA512':\n return JoseSignatureAlgorithm.RS512\n case 'RSA_SSA_PSS_SHA256_MGF1':\n return JoseSignatureAlgorithm.PS256\n case 'RSA_SSA_PSS_SHA384_MGF1':\n return JoseSignatureAlgorithm.PS384\n case 'RSA_SSA_PSS_SHA512_MGF1':\n return JoseSignatureAlgorithm.PS512\n case 'ECDSA_SHA256':\n return JoseSignatureAlgorithm.ES256\n case 'ECDSA_SHA384':\n return JoseSignatureAlgorithm.ES384\n case 'ECDSA_SHA512':\n return JoseSignatureAlgorithm.ES512\n case 'ES256K':\n return JoseSignatureAlgorithm.ES256K\n case 'ED25519':\n case 'Ed25519':\n case 'EdDSA':\n return JoseSignatureAlgorithm.EdDSA\n default:\n // If already in JOSE format, return as-is\n return alg as JoseSignatureAlgorithm\n }\n}\n\nexport const signatureAlgorithmFromKeyType = (args: SignatureAlgorithmFromKeyTypeArgs): JoseSignatureAlgorithm => {\n const { type, algorithms } = args\n\n // If algorithms metadata is provided, use the first one\n if (algorithms && algorithms.length > 0) {\n return signatureAlgorithmToJoseAlgorithm(algorithms[0])\n }\n\n // Fallback to type-based defaults\n switch (type) {\n case 'Ed25519':\n case 'X25519':\n return JoseSignatureAlgorithm.EdDSA\n case 'Secp256r1':\n return JoseSignatureAlgorithm.ES256\n case 'Secp384r1':\n return JoseSignatureAlgorithm.ES384\n case 'Secp521r1':\n return JoseSignatureAlgorithm.ES512\n case 'Secp256k1':\n return JoseSignatureAlgorithm.ES256K\n case 'RSA':\n return JoseSignatureAlgorithm.RS256 // Default to RS256 instead of PS256\n default:\n throw new Error(`Key type '${type}' not supported`)\n }\n}\n\n// TODO improve this conversion for jwt and jsonld, not a fan of current structure\nexport const keyTypeFromCryptographicSuite = (args: KeyTypeFromCryptographicSuiteArgs): TKeyType => {\n const { crv, kty, alg } = args\n\n switch (alg) {\n case 'RSASSA-PSS':\n case 'RS256':\n case 'RS384':\n case 'RS512':\n case 'PS256':\n case 'PS384':\n case 'PS512':\n return 'RSA'\n }\n\n switch (crv) {\n case 'EdDSA':\n case 'Ed25519':\n case 'Ed25519Signature2018':\n case 'Ed25519Signature2020':\n case 'Ed25519VerificationKey2018':\n case 'Ed25519VerificationKey2020':\n case 'JcsEd25519Signature2020':\n return 'Ed25519'\n case 'X25519':\n case 'X25519KeyAgreementKey2019':\n case 'X25519KeyAgreementKey2020':\n return 'X25519'\n case 'JsonWebSignature2020':\n case 'ES256':\n case 'ECDSA':\n case 'P-256':\n return 'Secp256r1'\n case 'ES384':\n case 'P-384':\n return 'Secp384r1'\n case 'ES512':\n case 'P-521':\n return 'Secp521r1'\n case 'EcdsaSecp256k1Signature2019':\n case 'secp256k1':\n case 'ES256K':\n case 'EcdsaSecp256k1VerificationKey2019':\n case 'EcdsaSecp256k1RecoveryMethod2020':\n case 'Secp256k1VerificationKey2018':\n return 'Secp256k1'\n }\n if (kty) {\n return kty as TKeyType\n }\n\n throw new Error(`Cryptographic suite '${crv}' not supported`)\n}\n\nexport function removeNulls<T>(obj: T | any) {\n Object.keys(obj).forEach((key) => {\n if (obj[key] && typeof obj[key] === 'object') removeNulls(obj[key])\n else if (obj[key] == null) delete obj[key]\n })\n return obj\n}\n\nexport const globalCrypto = (setGlobal: boolean, suppliedCrypto?: Crypto): Crypto => {\n let webcrypto: Crypto\n if (typeof suppliedCrypto !== 'undefined') {\n webcrypto = suppliedCrypto\n } else if (typeof crypto !== 'undefined') {\n webcrypto = crypto\n } else if (typeof global.crypto !== 'undefined') {\n webcrypto = global.crypto\n } else {\n // @ts-ignore\n if (typeof global.window?.crypto?.subtle !== 'undefined') {\n // @ts-ignore\n webcrypto = global.window.crypto\n } else {\n webcrypto = import('crypto') as Crypto\n }\n }\n if (setGlobal) {\n global.crypto = webcrypto\n }\n\n return webcrypto\n}\n\nexport const sanitizedJwk = (input: JWK | JsonWebKey): JWK => {\n const inputJwk = typeof input['toJsonDTO'] === 'function' ? input['toJsonDTO']() : ({ ...input } as JWK) // KMP code can expose this. It converts a KMP JWK with mangled names into a clean JWK\n\n const jwk = {\n ...inputJwk,\n ...(inputJwk.x && { x: base64ToBase64Url(inputJwk.x as string) }),\n ...(inputJwk.y && { y: base64ToBase64Url(inputJwk.y as string) }),\n ...(inputJwk.d && { d: base64ToBase64Url(inputJwk.d as string) }),\n ...(inputJwk.n && { n: base64ToBase64Url(inputJwk.n as string) }),\n ...(inputJwk.e && { e: base64ToBase64Url(inputJwk.e as string) }),\n ...(inputJwk.k && { k: base64ToBase64Url(inputJwk.k as string) }),\n } as JWK\n\n return removeNulls(jwk)\n}\n\nexport const base64ToBase64Url = (input: string): string => {\n return input.replace(/\\+/g, '-').replace(/\\//g, '_').replace(/=+$/, '')\n}\n\n/**\n *\n */\nexport async function verifyRawSignature({\n data,\n signature,\n key: inputKey,\n opts,\n}: {\n data: Uint8Array\n signature: Uint8Array\n key: JWK\n opts?: {\n signatureAlg?: JoseSignatureAlgorithm\n }\n}) {\n /**\n * Converts a Base64URL-encoded JWK property to a BigInt.\n * @param jwkProp - The Base64URL-encoded string.\n * @returns The BigInt representation of the decoded value.\n */\n function jwkPropertyToBigInt(jwkProp: string): bigint {\n // Decode Base64URL to Uint8Array\n const byteArray = fromString(jwkProp, 'base64url')\n\n // Convert Uint8Array to hexadecimal string and then to BigInt\n const hex = toString(byteArray, 'hex')\n return BigInt(`0x${hex}`)\n }\n\n try {\n debug(`verifyRawSignature for: ${inputKey}`)\n const jwk = sanitizedJwk(inputKey)\n validateJwk(jwk, { crvOptional: true })\n const keyType = keyTypeFromCryptographicSuite({ crv: jwk.crv, kty: jwk.kty, alg: jwk.alg })\n const publicKeyHex = await jwkToRawHexKey(jwk)\n\n // TODO: We really should look at the signature alg first if provided! From key type should be the last resort\n switch (keyType) {\n case 'Secp256k1':\n return secp256k1.verify(signature, data, publicKeyHex, { format: 'compact', prehash: true })\n case 'Secp256r1':\n return p256.verify(signature, data, publicKeyHex, { format: 'compact', prehash: true })\n case 'Secp384r1':\n return p384.verify(signature, data, publicKeyHex, { format: 'compact', prehash: true })\n case 'Secp521r1':\n return p521.verify(signature, data, publicKeyHex, { format: 'compact', prehash: true })\n case 'Ed25519':\n return ed25519.verify(signature, data, fromString(publicKeyHex, 'hex'))\n case 'Bls12381G1':\n case 'Bls12381G2':\n return bls12_381.verify(signature, data, fromString(publicKeyHex, 'hex'))\n case 'RSA': {\n const signatureAlgorithm = opts?.signatureAlg ?? (jwk.alg as JoseSignatureAlgorithm | undefined) ?? JoseSignatureAlgorithm.PS256\n const hashAlg =\n signatureAlgorithm === JoseSignatureAlgorithm.RS512 || signatureAlgorithm === JoseSignatureAlgorithm.PS512\n ? sha512\n : signatureAlgorithm === JoseSignatureAlgorithm.RS384 || signatureAlgorithm === JoseSignatureAlgorithm.PS384\n ? sha384\n : sha256\n switch (signatureAlgorithm) {\n case JoseSignatureAlgorithm.RS256:\n return rsa.PKCS1_SHA256.verify(\n {\n n: jwkPropertyToBigInt(jwk.n!),\n e: jwkPropertyToBigInt(jwk.e!),\n },\n data,\n signature,\n )\n case JoseSignatureAlgorithm.RS384:\n return rsa.PKCS1_SHA384.verify(\n {\n n: jwkPropertyToBigInt(jwk.n!),\n e: jwkPropertyToBigInt(jwk.e!),\n },\n data,\n signature,\n )\n case JoseSignatureAlgorithm.RS512:\n return rsa.PKCS1_SHA512.verify(\n {\n n: jwkPropertyToBigInt(jwk.n!),\n e: jwkPropertyToBigInt(jwk.e!),\n },\n data,\n signature,\n )\n case JoseSignatureAlgorithm.PS256:\n case JoseSignatureAlgorithm.PS384:\n case JoseSignatureAlgorithm.PS512:\n if (typeof crypto !== 'undefined' && typeof crypto.subtle !== 'undefined') {\n const key = await cryptoSubtleImportRSAKey(jwk, 'RSA-PSS')\n const saltLength =\n signatureAlgorithm === JoseSignatureAlgorithm.PS256 ? 32 : signatureAlgorithm === JoseSignatureAlgorithm.PS384 ? 48 : 64\n return crypto.subtle.verify({ name: 'rsa-pss', hash: hashAlg, saltLength }, key, signature, data)\n }\n\n // FIXME\n console.warn(`Using fallback for RSA-PSS verify signature, which is known to be flaky!!`)\n return rsa.PSS(hashAlg, rsa.mgf1(hashAlg)).verify(\n {\n n: jwkPropertyToBigInt(jwk.n!),\n e: jwkPropertyToBigInt(jwk.e!),\n },\n data,\n signature,\n )\n }\n }\n }\n\n throw Error(`Unsupported key type for signature validation: ${keyType}`)\n } catch (error: any) {\n logger.error(`Error: ${error}`)\n throw error\n }\n}\n\n/**\n * Minimal DER parser to unwrap X.509/SPKI‐wrapped RSA keys\n * into raw PKCS#1 RSAPublicKey format, using only Uint8Array.\n */\n\n/**\n * Read a DER length at the given offset.\n * @param bytes – full DER buffer\n * @param offset – index of the length byte\n * @returns the parsed length, and how many bytes were used to encode it\n */\nfunction readLength(bytes: Uint8Array, offset: number): { length: number; lengthBytes: number } {\n const first = bytes[offset]\n if (first < 0x80) {\n return { length: first, lengthBytes: 1 }\n }\n const numBytes = first & 0x7f\n let length = 0\n for (let i = 0; i < numBytes; i++) {\n length = (length << 8) | bytes[offset + 1 + i]\n }\n return { length, lengthBytes: 1 + numBytes }\n}\n\n/**\n * Ensure the given DER‐encoded RSA public key (Uint8Array)\n * is raw PKCS#1. If it's X.509/SPKI‐wrapped, we strip the wrapper.\n *\n * @param derBytes – DER‐encoded public key, either PKCS#1 or X.509/SPKI\n * @returns DER‐encoded PKCS#1 RSAPublicKey\n */\nexport function toPkcs1(derBytes: Uint8Array): Uint8Array {\n if (derBytes[0] !== 0x30) {\n throw new Error('Invalid DER: expected SEQUENCE')\n }\n\n // Parse outer SEQUENCE length\n const { lengthBytes: outerLenBytes } = readLength(derBytes, 1)\n const outerHeaderLen = 1 + outerLenBytes\n const innerTag = derBytes[outerHeaderLen]\n\n // If next tag is INTEGER (0x02), it's already raw PKCS#1\n if (innerTag === 0x02) {\n return derBytes\n }\n\n // Otherwise expect X.509/SPKI: SEQUENCE { algId, BIT STRING }\n if (innerTag !== 0x30) {\n throw new Error('Unexpected DER tag, not PKCS#1 or SPKI')\n }\n\n // Skip the algId SEQUENCE\n const { length: algLen, lengthBytes: algLenBytes } = readLength(derBytes, outerHeaderLen + 1)\n const algHeaderLen = 1 + algLenBytes\n const algIdEnd = outerHeaderLen + algHeaderLen + algLen\n\n // Next tag should be BIT STRING (0x03)\n if (derBytes[algIdEnd] !== 0x03) {\n throw new Error('Expected BIT STRING after algId')\n }\n\n const { length: bitStrLen, lengthBytes: bitStrLenBytes } = readLength(derBytes, algIdEnd + 1)\n const bitStrHeaderLen = 1 + bitStrLenBytes\n const bitStrStart = algIdEnd + bitStrHeaderLen\n\n // First byte of the BIT STRING is the \"unused bits\" count; usually 0x00\n const unusedBits = derBytes[bitStrStart]\n if (unusedBits !== 0x00) {\n throw new Error(`Unexpected unused bits: ${unusedBits}`)\n }\n\n // The rest is the PKCS#1 DER\n const pkcs1Start = bitStrStart + 1\n const pkcs1Len = bitStrLen - 1\n\n return derBytes.slice(pkcs1Start, pkcs1Start + pkcs1Len)\n}\n\n/**\n * Ensure the given DER‐encoded RSA public key in Hex\n * is raw PKCS#1. If it's X.509/SPKI‐wrapped, we strip the wrapper.\n *\n * @param derBytes – DER‐encoded public key, either PKCS#1 or X.509/SPKI\n * @returns DER‐encoded PKCS#1 RSAPublicKey in hex\n */\nexport function toPkcs1FromHex(publicKeyHex: string) {\n const pkcs1 = toPkcs1(fromString(publicKeyHex, 'hex'))\n return toString(pkcs1, 'hex')\n}\n\nexport function joseAlgorithmToDigest(alg: string): DigestAlgorithm {\n // Normalize the algorithm string by converting to uppercase and removing hyphens\n const normalized = alg.toUpperCase().replace(/-/g, '')\n\n switch (normalized) {\n case 'RS256':\n case 'ES256':\n case 'ES256K':\n case 'PS256':\n case 'HS256':\n return 'SHA-256'\n case 'RS384':\n case 'ES384':\n case 'PS384':\n case 'HS384':\n return 'SHA-384'\n case 'RS512':\n case 'ES512':\n case 'PS512':\n case 'HS512':\n return 'SHA-512'\n case 'EDDSA':\n case 'ED25519':\n return 'SHA-512'\n default:\n throw new Error(`Unsupported JOSE algorithm: ${alg}. Cannot determine digest algorithm.`)\n }\n}\n\nexport function isHash(input: string): boolean {\n const length = input.length\n // SHA-256: 64 hex chars, SHA-384: 96 hex chars, SHA-512: 128 hex chars\n if (length !== 64 && length !== 96 && length !== 128) {\n return false\n }\n return input.match(/^([0-9A-Fa-f])+$/g) !== null\n}\n\nexport function isHashString(input: Uint8Array): boolean {\n const length = input.length\n // SHA-256: 32 bytes, SHA-384: 48 bytes, SHA-512: 64 bytes\n if (length !== 32 && length !== 48 && length !== 64) {\n return false\n }\n\n // A hash digest is raw binary data (any byte values 0x00-0xFF are valid).\n // We should NOT check if bytes are ASCII hex characters, as that would only detect\n // hex-encoded strings, not actual binary hash digests.\n // Instead, we use a heuristic: if the data looks like it has high entropy\n // and is the right length, we assume it's already a hash.\n\n // Simple heuristic: Check if data is all printable ASCII (which would indicate it's NOT a hash)\n // Printable ASCII is roughly 0x20-0x7E\n let printableCount = 0\n for (let i = 0; i < length; i++) {\n const byte = input[i]\n if (byte === undefined) {\n return false\n }\n // Count printable ASCII characters\n if (byte >= 0x20 && byte <= 0x7e) {\n printableCount++\n }\n }\n\n // If more than 90% of bytes are printable ASCII, it's likely NOT a raw binary hash\n // Raw binary hashes should have a more uniform distribution across all byte values\n const printableRatio = printableCount / length\n return printableRatio < 0.9\n}\n\nexport type HashAlgorithm = 'SHA-256' | 'sha256' | 'SHA-384' | 'sha384' | 'SHA-512' | 'sha512'\n\nexport function normalizeHashAlgorithm(alg?: HashAlgorithm): 'SHA-256' | 'SHA-384' | 'SHA-512' {\n if (!alg) {\n return 'SHA-256'\n }\n const upper = alg.toUpperCase()\n if (upper.includes('256')) return 'SHA-256'\n if (upper.includes('384')) return 'SHA-384'\n if (upper.includes('512')) return 'SHA-512'\n throw new Error(`Invalid hash algorithm: ${alg}`)\n}\n\nexport function isSameHash(left: HashAlgorithm, right: HashAlgorithm): boolean {\n return normalizeHashAlgorithm(left) === normalizeHashAlgorithm(right)\n}\n","import { sha256 } from '@noble/hashes/sha256'\nimport { sha384, sha512 } from '@noble/hashes/sha512'\nimport type { HasherSync } from '@sphereon/ssi-types'\n// @ts-ignore\nimport * as u8a from 'uint8arrays'\nimport { normalizeHashAlgorithm } from './functions'\nimport { DigestAlgorithm } from './types'\nconst { fromString, toString, SupportedEncodings } = u8a\n\nexport type TDigestMethod = (input: string, encoding?: typeof SupportedEncodings) => string\n\nexport const digestMethodParams = (\n hashAlgorithm: DigestAlgorithm,\n): { hashAlgorithm: DigestAlgorithm; digestMethod: TDigestMethod; hash: (data: Uint8Array) => Uint8Array } => {\n switch (normalizeHashAlgorithm(hashAlgorithm)) {\n case 'SHA-256':\n return { hashAlgorithm: 'SHA-256', digestMethod: sha256DigestMethod, hash: sha256 }\n case 'SHA-384':\n return { hashAlgorithm: 'SHA-384', digestMethod: sha384DigestMethod, hash: sha384 }\n case 'SHA-512':\n return { hashAlgorithm: 'SHA-512', digestMethod: sha512DigestMethod, hash: sha512 }\n }\n}\n\nexport const shaHasher: HasherSync = (input: string | ArrayBuffer | SharedArrayBuffer, alg: string): Uint8Array => {\n const hashAlgorithm: DigestAlgorithm = alg.includes('384') ? 'SHA-384' : alg.includes('512') ? 'SHA-512' : 'SHA-256'\n return digestMethodParams(hashAlgorithm).hash(typeof input === 'string' ? fromString(input, 'utf-8') : new Uint8Array(input))\n}\n\nconst sha256DigestMethod = (input: string, encoding: typeof SupportedEncodings = 'base16'): string => {\n return toString(sha256(fromString(input, 'utf-8')), encoding)\n}\n\nconst sha384DigestMethod = (input: string, encoding: typeof SupportedEncodings = 'base16'): string => {\n return toString(sha384(fromString(input, 'utf-8')), encoding)\n}\n\nconst sha512DigestMethod = (input: string, encoding: typeof SupportedEncodings = 'base16'): string => {\n return toString(sha512(fromString(input, 'utf-8')), encoding)\n}\n\n/*\n// PKCS#1 (PSS) mask generation function\nfunction pss_mgf1_str(seed, len, hash) {\n var mask = '', i = 0;\n\n while (mask.length < len) {\n mask += hextorstr(hash(rstrtohex(seed + String.fromCharCode.apply(String, [\n (i & 0xff000000) >> 24,\n (i & 0x00ff0000) >> 16,\n (i & 0x0000ff00) >> 8,\n i & 0x000000ff]))));\n i += 1;\n }\n\n return mask;\n}\n\n */\n\n/*\n\n/!**\n * Generate mask of specified length.\n *\n * @param {String} seed The seed for mask generation.\n * @param maskLen Number of bytes to generate.\n * @return {String} The generated mask.\n *!/\nexport const mgf1 = (dm: TDigestMethod, seed: string, maskLen: number) => {\n /!* 2. Let T be the empty octet string. *!/\n var t = new forge.util.ByteBuffer();\n\n /!* 3. For counter from 0 to ceil(maskLen / hLen), do the following: *!/\n var len = Math.ceil(maskLen / md.digestLength);\n for(var i = 0; i < len; i++) {\n /!* a. Convert counter to an octet string C of length 4 octets *!/\n var c = new forge.util.ByteBuffer();\n c.putInt32(i);\n\n /!* b. Concatenate the hash of the seed mgfSeed and C to the octet\n * string T: *!/\n md.start();\n md.update(seed + c.getBytes());\n t.putBuffer(md.digest());\n }\n\n /!* Output the leading maskLen octets of T as the octet string mask. *!/\n t.truncate(t.length() - maskLen);\n return t.getBytes();\n}\n*/\n","import { JsonWebKey, JWK } from '@sphereon/ssi-types'\n// @ts-ignore\nimport type { ByteView } from 'multiformats/codecs/interface'\n// @ts-ignore\nimport { TextDecoder, TextEncoder } from 'web-encoding'\n\nconst textEncoder = new TextEncoder()\nconst textDecoder = new TextDecoder()\n\n/**\n * Checks if the value is a non-empty string.\n *\n * @param value - The value to check.\n * @param description - Description of the value to check.\n * @param optional\n */\nfunction check(value: unknown, description: string, optional: boolean = false) {\n if (optional && !value) {\n return\n }\n if (typeof value !== 'string' || !value) {\n throw new Error(`${description} missing or invalid`)\n }\n}\n\n/**\n * Checks if the value is a valid JSON object.\n *\n * @param value - The value to check.\n */\nfunction assertObject(value: unknown) {\n if (!value || typeof value !== 'object') {\n throw new Error('Value must be an object')\n }\n}\n\n/**\n * Checks if the JWK is valid. It must contain all the required members.\n *\n * @see https://www.rfc-editor.org/rfc/rfc7518#section-6\n * @see https://www.rfc-editor.org/rfc/rfc8037#section-2\n *\n * @param jwk - The JWK to check.\n * @param opts\n */\nexport function validateJwk(jwk: any, opts?: { crvOptional?: boolean }) {\n assertObject(jwk)\n const { crvOptional = false } = opts ?? {}\n check(jwk.kty, '\"kty\" (Key Type) Parameter', false)\n\n // Check JWK required members based on the key type\n switch (jwk.kty) {\n /**\n * @see https://www.rfc-editor.org/rfc/rfc7518#section-6.2.1\n */\n case 'EC':\n check(jwk.crv, '\"crv\" (Curve) Parameter', crvOptional)\n check(jwk.x, '\"x\" (X Coordinate) Parameter')\n check(jwk.y, '\"y\" (Y Coordinate) Parameter')\n break\n /**\n * @see https://www.rfc-editor.org/rfc/rfc8037#section-2\n */\n case 'OKP':\n check(jwk.crv, '\"crv\" (Subtype of Key Pair) Parameter', crvOptional) // Shouldn't this one always be true as crv is not always present?\n check(jwk.x, '\"x\" (Public Key) Parameter')\n break\n /**\n * @see https://www.rfc-editor.org/rfc/rfc7518#section-6.3.1\n */\n case 'RSA':\n check(jwk.e, '\"e\" (Exponent) Parameter')\n check(jwk.n, '\"n\" (Modulus) Parameter')\n break\n default:\n throw new Error('\"kty\" (Key Type) Parameter missing or unsupported')\n }\n}\n\n/**\n * Extracts the required members of the JWK and canonicalizes it.\n *\n * @param jwk - The JWK to canonicalize.\n * @returns The JWK with only the required members, ordered lexicographically.\n */\nexport function minimalJwk(jwk: any): JWK {\n // \"default\" case is not needed\n // eslint-disable-next-line default-case\n switch (jwk.kty) {\n case 'EC':\n return { ...(jwk.crv && { crv: jwk.crv }), kty: jwk.kty, x: jwk.x, y: jwk.y }\n case 'OKP':\n return { ...(jwk.crv && { crv: jwk.crv }),