// A certificate signing request (PKCS #10, RFC 2986) built with node:crypto and a minimal DER encoder: no openssl, // no Keychain Access. The private key is generated here and only ever written to a 0600 file in .storebound/signing/ // (and, when asked, imported into a keychain); the CSR carries only the public key. import { createPrivateKey, createPublicKey, generateKeyPairSync, type KeyObject, sign } from 'node:crypto'; function derLen(n: number): Buffer { if (n < 0x80) return Buffer.from([n]); const bytes: number[] = []; for (let v = n; v > 0; v = Math.floor(v / 256)) bytes.unshift(v & 255); return Buffer.from([0x80 | bytes.length, ...bytes]); } export const der = (tag: number, ...parts: Buffer[]): Buffer => { const body = Buffer.concat(parts); return Buffer.concat([Buffer.from([tag]), derLen(body.length), body]); }; const seq = (...p: Buffer[]) => der(0x30, ...p); const set = (...p: Buffer[]) => der(0x31, ...p); /** DER object identifier from its arcs, e.g. [2, 5, 4, 3] (commonName). */ export function oid(arcs: number[]): Buffer { const [a, b, ...rest] = arcs as [number, number, ...number[]]; const out = [40 * a + b]; for (const n of rest) { const groups: number[] = []; for (let v = n; ; v = Math.floor(v / 128)) { groups.unshift(v & 127); if (v < 128) break; } out.push(...groups.map((g, i) => (i < groups.length - 1 ? g | 128 : g))); } return der(0x06, Buffer.from(out)); } const OID = { commonName: [2, 5, 4, 3], countryName: [2, 5, 4, 6], emailAddress: [1, 2, 840, 113549, 1, 9, 1], sha256WithRSA: [1, 2, 840, 113549, 1, 1, 11] }; export interface CsrSubject { commonName: string; country?: string; email?: string } /** PEM CSR for the key. Subject: CN (and C / emailAddress when given; Apple ignores them for API-made certificates). */ export function buildCsr(key: KeyObject, subject: CsrSubject): string { const rdn = (o: number[], value: Buffer) => set(seq(oid(o), value)); const name = seq( ...(subject.email ? [rdn(OID.emailAddress, der(0x16, Buffer.from(subject.email, 'ascii')))] : []), rdn(OID.commonName, der(0x0c, Buffer.from(subject.commonName, 'utf8'))), ...(subject.country ? [rdn(OID.countryName, der(0x13, Buffer.from(subject.country, 'ascii')))] : []), ); const spki = createPublicKey(key).export({ type: 'spki', format: 'der' }); const info = seq(der(0x02, Buffer.from([0])), name, spki, der(0xa0)); const sig = sign('sha256', info, key); const csr = seq(info, seq(oid(OID.sha256WithRSA), der(0x05)), der(0x03, Buffer.from([0]), sig)); return `-----BEGIN CERTIFICATE REQUEST-----\n${csr.toString('base64').match(/.{1,64}/g)!.join('\n')}\n-----END CERTIFICATE REQUEST-----\n`; } /** New RSA 2048 key (Apple's signing certificates use RSA 2048) as PKCS #8 PEM plus its KeyObject. */ export function newSigningKey(): { pem: string; key: KeyObject } { const { privateKey } = generateKeyPairSync('rsa', { modulusLength: 2048 }); return { pem: privateKey.export({ type: 'pkcs8', format: 'pem' }).toString(), key: privateKey }; } /** The CSR's base64 body (what the App Store Connect API takes as csrContent). */ export const csrContent = (pem: string): string => pem.replace(/-----[^-]+-----/g, '').replace(/\s+/g, ''); /** SPKI (DER, base64) of a private key PEM: matches a certificate's public key. */ export const spkiOfKey = (pem: string): string => createPublicKey(createPrivateKey(pem)).export({ type: 'spki', format: 'der' }).toString('base64');