UNPKG

@turnkey/api-key-stamper

Version:

API key stamper for @turnkey/http

113 lines (110 loc) 4.11 kB
import { convertTurnkeyApiKeyToJwk } from './utils.mjs'; import { uint8ArrayToHexString } from '@turnkey/encoding'; /// <reference lib="dom" /> const signWithApiKey = async (input) => { const { content, publicKey, privateKey } = input; const key = await importTurnkeyApiKey({ uncompressedPrivateKeyHex: privateKey, compressedPublicKeyHex: publicKey, }); return await signMessage({ key, content }); }; /** * Imports a P-256 Turnkey API key into a WebCrypto `CryptoKey`. * * @param {Object} input - The Turnkey API key components. * @param {string} input.uncompressedPrivateKeyHex - Hexadecimal-encoded uncompressed private key (32-byte scalar). * @param {string} input.compressedPublicKeyHex - Hexadecimal-encoded compressed public key (33 bytes). * @returns {Promise<CryptoKey>} A `CryptoKey` object representing a P-256 key. */ async function importTurnkeyApiKey(input) { const { uncompressedPrivateKeyHex, compressedPublicKeyHex } = input; const jwk = convertTurnkeyApiKeyToJwk({ uncompressedPrivateKeyHex, compressedPublicKeyHex, }); return await crypto.subtle.importKey("jwk", jwk, { name: "ECDSA", namedCurve: "P-256", }, false, // not extractable ["sign"]); } async function signMessage(input) { const { key, content } = input; const signatureIeee1363 = await crypto.subtle.sign({ name: "ECDSA", hash: "SHA-256", }, key, new TextEncoder().encode(content)); const signatureDer = convertEcdsaIeee1363ToDer(new Uint8Array(signatureIeee1363)); return uint8ArrayToHexString(signatureDer); } /** * `SubtleCrypto.sign(...)` outputs signature in IEEE P1363 format: * - https://developer.mozilla.org/en-US/docs/Web/API/SubtleCrypto/sign#ecdsa * * Turnkey expects the signature encoding to be DER-encoded ASN.1: * - https://github.com/tkhq/tkcli/blob/7f0159af5a73387ff050647180d1db4d3a3aa033/src/internal/apikey/apikey.go#L149 * * Code modified from https://github.com/google/tink/blob/6f74b99a2bfe6677e3670799116a57268fd067fa/javascript/subtle/elliptic_curves.ts#L114 * * Transform an ECDSA signature in IEEE 1363 encoding to DER encoding. * * @param ieee the ECDSA signature in IEEE encoding * @return ECDSA signature in DER encoding */ function convertEcdsaIeee1363ToDer(ieee) { if (ieee.length % 2 != 0 || ieee.length == 0 || ieee.length > 132) { throw new Error("Invalid IEEE P1363 signature encoding. Length: " + ieee.length); } const r = toUnsignedBigNum(ieee.subarray(0, ieee.length / 2)); const s = toUnsignedBigNum(ieee.subarray(ieee.length / 2, ieee.length)); let offset = 0; const length = 1 + 1 + r.length + 1 + 1 + s.length; let der; if (length >= 128) { der = new Uint8Array(length + 3); der[offset++] = 48; der[offset++] = 128 + 1; der[offset++] = length; } else { der = new Uint8Array(length + 2); der[offset++] = 48; der[offset++] = length; } der[offset++] = 2; der[offset++] = r.length; der.set(r, offset); offset += r.length; der[offset++] = 2; der[offset++] = s.length; der.set(s, offset); return der; } /** * Code modified from https://github.com/google/tink/blob/6f74b99a2bfe6677e3670799116a57268fd067fa/javascript/subtle/elliptic_curves.ts#L311 * * Transform a big integer in big endian to minimal unsigned form which has * no extra zero at the beginning except when the highest bit is set. */ function toUnsignedBigNum(bytes) { // Remove zero prefixes. let start = 0; while (start < bytes.length && bytes[start] == 0) { start++; } if (start == bytes.length) { start = bytes.length - 1; } let extraZero = 0; // If the 1st bit is not zero, add 1 zero byte. if ((bytes[start] & 128) == 128) { // Add extra zero. extraZero = 1; } const res = new Uint8Array(bytes.length - start + extraZero); res.set(bytes.subarray(start), extraZero); return res; } export { signWithApiKey }; //# sourceMappingURL=webcrypto.mjs.map