@turnkey/api-key-stamper
Version:
API key stamper for @turnkey/http
113 lines (110 loc) • 4.11 kB
JavaScript
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