storebound/cli/lib/csr.ts
CRT 41319562d8 Signing, builds, uploads and an originality check
apple signing status|ensure: bundle id capabilities, a distribution certificate from a key made locally (kept in .storebound/signing, never printed), App Store profiles installed for Xcode; Developer ID 403 explained.
apple build / apple upload: xcodebuild archive + export with a generated ExportOptions.plist, next build number, App Store Connect API build upload (or altool), processing wait. Native, Capacitor, React Native, Flutter, Unity projects.
play build / play upload: Gradle bundleRelease with the upload key passed through the environment, next versionCode read back from the AAB, testing tracks only.
originality: offline Guideline 4.3 check (name, listing text, icon and screenshot perceptual hashes, project structure).
New skill build-and-sign; zero new dependencies.
2026-10-11 23:42:48 +08:00

57 lines
3.4 KiB
TypeScript

// 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');