get a distribution cert * 3. bundle key + cert into a PKCS#12 (openssl_pkcs12_export) * 4. register the bundle ID and create an App Store provisioning * profile tying it to that cert * * The resulting .p12 + .mobileprovision are stored once by * Mobile_App_Store and reused for every later build — Apple limits how * many distribution certificates an account can hold, so this must not * run on every build. * * @package Esipher */ namespace Esipher; if ( ! defined( 'ABSPATH' ) ) { exit; } /** * One static entry point: provision(). */ class Ios_Signing { /** * @param App_Store_Connect_Api $asc * @param string $bundle_id e.g. com.example.app. * @param string $app_name For labels. * * @return array{p12_base64:string,p12_password:string,mobileprovision_base64:string,profile_name:string}|\WP_Error */ public static function provision( App_Store_Connect_Api $asc, $bundle_id, $app_name ) { if ( ! function_exists( 'openssl_pkey_new' ) || ! function_exists( 'openssl_pkcs12_export' ) || ! function_exists( 'openssl_csr_new' ) ) { return new \WP_Error( 'esipher_ios_no_openssl', __( 'This server\'s PHP is missing OpenSSL functions needed to create the iOS signing key.', 'esipher' ) ); } $pkey = openssl_pkey_new( array( 'private_key_type' => OPENSSL_KEYTYPE_RSA, 'private_key_bits' => 2048, ) ); if ( false === $pkey ) { return new \WP_Error( 'esipher_ios_keygen', __( 'Could not generate the iOS signing key.', 'esipher' ) ); } $csr = openssl_csr_new( array( 'commonName' => 'Esipher iOS Distribution', 'organizationName' => 'Esipher' ), $pkey, array( 'digest_alg' => 'sha256' ) ); if ( false === $csr ) { return new \WP_Error( 'esipher_ios_csr', __( 'Could not build the certificate signing request.', 'esipher' ) ); } $csr_pem = ''; openssl_csr_export( $csr, $csr_pem ); $cert = $asc->create_distribution_certificate( $csr_pem ); if ( is_wp_error( $cert ) ) { return self::explain( $cert ); } $cert_pem = "-----BEGIN CERTIFICATE-----\n" . chunk_split( $cert['content'], 64, "\n" ) . "-----END CERTIFICATE-----\n"; $password = wp_generate_password( 28, false ); $p12 = ''; $ok = openssl_pkcs12_export( $cert_pem, $p12, $pkey, $password, array( 'friendly_name' => 'Esipher iOS Distribution' ) ); if ( ! $ok || '' === $p12 ) { return new \WP_Error( 'esipher_ios_p12', __( 'Could not package the iOS signing certificate.', 'esipher' ) ); } $bundle_res_id = $asc->ensure_bundle_id( $bundle_id, $app_name . ' (Esipher)' ); if ( is_wp_error( $bundle_res_id ) ) { return self::explain( $bundle_res_id ); } $profile_name = 'Esipher App Store ' . $bundle_id; // Drop any leftover profile of the same name (it points at the // now-replaced certificate). $asc->delete_profiles_named( $profile_name ); $profile = $asc->create_app_store_profile( $profile_name, $bundle_res_id, $cert['id'] ); if ( is_wp_error( $profile ) ) { return self::explain( $profile ); } return array( 'p12_base64' => base64_encode( $p12 ), // phpcs:ignore WordPress.PHP.DiscouragedPHPFunctions.obfuscation_base64_encode -- transporting a binary keystore as text. 'p12_password' => $password, 'mobileprovision_base64' => $profile, 'profile_name' => $profile_name, ); } /** * Turn a raw ASC error into something the admin can act on. * * @param \WP_Error $error * * @return \WP_Error */ private static function explain( \WP_Error $error ) { $code = (int) $error->get_error_code(); $msg = $error->get_error_message(); if ( 401 === $code || 403 === $code ) { return new \WP_Error( 'esipher_ios_auth', sprintf( /* translators: %s: underlying message. */ __( 'App Store Connect rejected the API key (%s). Check the Team ID, Key ID, Issuer ID and .p8 on the iOS tab, and that the key has the "App Manager" or "Developer" role.', 'esipher' ), $msg ) ); } $is_cert_limit = false !== stripos( $msg, 'certificate' ) && ( false !== stripos( $msg, 'maximum' ) || false !== stripos( $msg, 'already have' ) || false !== stripos( $msg, 'pending' ) ); if ( $is_cert_limit ) { return new \WP_Error( 'esipher_ios_cert_limit', __( 'Apple won\'t issue another distribution certificate — your account is at the limit, or has a pending request. In the Apple Developer portal → Certificates, revoke an unused "Apple Distribution" certificate (if you use it on a Mac, export it as a .p12 first), then click Rebuild here.', 'esipher' ) ); } return new \WP_Error( 'esipher_ios_provision', sprintf( /* translators: %s: underlying message. */ __( 'Could not set up iOS signing: %s', 'esipher' ), $msg ) ); } }