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.
57 lines
3.4 KiB
TypeScript
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');
|