Created
May 2, 2025 11:39
-
-
Save fl0wo/beede95aaa74bab53fe124552af5d8e6 to your computer and use it in GitHub Desktop.
Ed25519 and ECDSA signature algorithm for Coinbase APIs.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| import { createPrivateKey } from "crypto"; | |
| import { SignJWT, importPKCS8, importJWK, JWTPayload } from "jose"; | |
| const pemFooter = "-----END EC PRIVATE KEY-----"; | |
| /** | |
| * A class that builds JWTs for authenticating with the Coinbase Platform APIs. | |
| */ | |
| export class CoinbaseAuthenticator { | |
| private apiKey: string; | |
| private privateKey: string; | |
| private source: string; | |
| private sourceVersion?: string; | |
| /** | |
| * Initializes the Authenticator. | |
| * | |
| * @param {string} apiKey - The API key name. | |
| * @param {string} privateKey - The private key associated with the API key. | |
| * @param {string} source - The source of the request. | |
| * @param {string} [sourceVersion] - The version of the source. | |
| */ | |
| constructor(apiKey: string, privateKey: string, source: string, sourceVersion?: string) { | |
| this.apiKey = apiKey; | |
| this.privateKey = privateKey; | |
| this.source = source; | |
| this.sourceVersion = sourceVersion; | |
| } | |
| /** | |
| * Builds the JWT for the given API endpoint URL. | |
| * | |
| * @param {string} url - URL of the API endpoint. | |
| * @param {string} [method] - HTTP method of the request. | |
| * @returns {Promise<string>} A JWT token. | |
| * @throws {InvalidAPIKeyFormatError} If the private key is not in the correct format or signing fails. | |
| */ | |
| async buildJWT(url: string, method: string = "GET"): Promise<string> { | |
| const urlObject = new URL(url); | |
| const uri = `${method} ${urlObject.host}${urlObject.pathname}`; | |
| const now = Math.floor(Date.now() / 1000); | |
| const claims: JWTPayload = { | |
| sub: this.apiKey, | |
| iss: "cdp", | |
| aud: ["cdp_service"], | |
| uris: [uri], | |
| }; | |
| if (this.privateKey.startsWith("-----BEGIN")) { | |
| // console.log('Building JWT with EC key'); | |
| return this.buildECJWT(claims, now); | |
| } else { | |
| // console.log('Building JWT with Edwards key'); | |
| return this.buildEdwardsJWT(claims, now); | |
| } | |
| } | |
| /** | |
| * Builds a JWT using an EC key. | |
| * | |
| * @param {JWTPayload} claims - The JWT claims. | |
| * @param {number} now - The current timestamp (in seconds). | |
| * @returns {Promise<string>} A JWT token signed with an EC key. | |
| * @throws {InvalidAPIKeyFormatError} If the key conversion, import, or signing fails. | |
| */ | |
| private async buildECJWT(claims: JWTPayload, now: number): Promise<string> { | |
| // Ensure the PEM is valid and let jose import it. | |
| const pemPrivateKey = this.extractPemKey(this.privateKey); | |
| let pkcs8Key: string; | |
| try { | |
| const keyObj = createPrivateKey(pemPrivateKey); | |
| pkcs8Key = keyObj.export({ type: "pkcs8", format: "pem" }).toString(); | |
| } catch (error) { | |
| throw new InvalidAPIKeyFormatError("Could not convert the EC private key to PKCS8 format"); | |
| } | |
| let ecKey; | |
| try { | |
| ecKey = await importPKCS8(pkcs8Key, "ES256"); | |
| } catch (error) { | |
| throw new InvalidAPIKeyFormatError("Could not import the EC private key"); | |
| } | |
| try { | |
| return await new SignJWT(claims) | |
| .setProtectedHeader({ alg: "ES256", kid: this.apiKey, typ: "JWT", nonce: this.nonce() }) | |
| .setIssuedAt(now) | |
| .setNotBefore(now) | |
| .setExpirationTime(now + 60) | |
| .sign(ecKey); | |
| } catch (err) { | |
| throw new InvalidAPIKeyFormatError("Could not sign the JWT with the EC key"); | |
| } | |
| } | |
| /** | |
| * Builds a JWT using an Ed25519 key. | |
| * | |
| * @param {JWTPayload} claims - The JWT claims. | |
| * @param {number} now - The current timestamp (in seconds). | |
| * @returns {Promise<string>} A JWT token signed with an Ed25519 key. | |
| * @throws {InvalidAPIKeyFormatError} If the key parsing, import, or signing fails. | |
| */ | |
| private async buildEdwardsJWT(claims: JWTPayload, now: number): Promise<string> { | |
| // Expect a base64 encoded 64-byte string (32 bytes seed + 32 bytes public key) | |
| const decoded = Buffer.from(this.privateKey, "base64"); | |
| if (decoded.length !== 64) { | |
| throw new InvalidAPIKeyFormatError("Could not parse the private key"); | |
| } | |
| const seed = decoded.subarray(0, 32); | |
| const publicKey = decoded.subarray(32); | |
| const jwk = { | |
| kty: "OKP", | |
| crv: "Ed25519", | |
| d: seed.toString("base64url"), | |
| x: publicKey.toString("base64url"), | |
| }; | |
| let key; | |
| try { | |
| key = await importJWK(jwk, "EdDSA"); | |
| } catch (error) { | |
| throw new InvalidAPIKeyFormatError("Could not import the Ed25519 private key"); | |
| } | |
| try { | |
| return await new SignJWT(claims) | |
| .setProtectedHeader({ alg: "EdDSA", kid: this.apiKey, typ: "JWT", nonce: this.nonce() }) | |
| .setIssuedAt(now) | |
| .setNotBefore(now) | |
| .setExpirationTime(now + 60) | |
| .sign(key); | |
| } catch (err) { | |
| throw new InvalidAPIKeyFormatError("Could not sign the JWT with the Ed25519 key"); | |
| } | |
| } | |
| /** | |
| * Extracts and verifies the PEM key from the given private key string. | |
| * | |
| * @param {string} privateKeyString - The private key string. | |
| * @returns {string} The original PEM key string if valid. | |
| * @throws {InvalidAPIKeyFormatError} If the private key string is not in the correct PEM format. | |
| */ | |
| private extractPemKey(privateKeyString: string): string { | |
| if ( | |
| privateKeyString.includes("-----BEGIN EC PRIVATE KEY-----") && | |
| privateKeyString.includes(pemFooter) | |
| ) { | |
| return privateKeyString; | |
| } | |
| throw new InvalidAPIKeyFormatError("Invalid private key format"); | |
| } | |
| /** | |
| * Generates a random nonce for the JWT. | |
| * | |
| * @returns {string} The generated nonce. | |
| */ | |
| private nonce(): string { | |
| const range = "0123456789"; | |
| let result = ""; | |
| for (let i = 0; i < 16; i++) { | |
| result += range.charAt(Math.floor(Math.random() * range.length)); | |
| } | |
| return result; | |
| } | |
| } | |
| /** | |
| * Error thrown when API key format is invalid. | |
| */ | |
| export class InvalidAPIKeyFormatError extends Error { | |
| constructor(message: string) { | |
| super(message); | |
| this.name = "InvalidAPIKeyFormatError"; | |
| } | |
| } |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment