$cert) { $x5c[$id] = '-----BEGIN CERTIFICATE-----' . "\n" . chunk_split( (string) $cert, 64, "\n" ) . '-----END CERTIFICATE-----'; $x509 = openssl_x509_read($x5c[$id]); if ($x509 === false) { throw new InvalidArgumentException('Unable to load the certificate chain'); } $parsed = openssl_x509_parse($x509); if ($parsed === false) { throw new InvalidArgumentException('Unable to load the certificate chain'); } } return self::loadKeyFromCertificate(reset($x5c)); } private static function loadKeyFromDER(string $der, ?string $password = null): array { $pem = self::convertDerToPem($der); return self::loadKeyFromPEM($pem, $password); } private static function loadKeyFromPEM(string $pem, ?string $password = null): array { if (! extension_loaded('openssl')) { throw new RuntimeException('Please install the OpenSSL extension'); } if (preg_match('#DEK-Info: (.+),(.+)#', $pem, $matches) === 1) { $pem = self::decodePem($pem, $matches, $password); } if (preg_match('#BEGIN ENCRYPTED PRIVATE KEY(.+)(.+)#', $pem) === 1) { $decrypted = openssl_pkey_get_private($pem, $password); if ($decrypted === false) { throw new InvalidArgumentException('Unable to decrypt the key.'); } openssl_pkey_export($decrypted, $pem); } self::sanitizePEM($pem); $res = openssl_pkey_get_private($pem); if ($res === false) { $res = openssl_pkey_get_public($pem); } if ($res === false) { throw new InvalidArgumentException('Unable to load the key.'); } $details = openssl_pkey_get_details($res); if (! is_array($details) || ! array_key_exists('type', $details)) { throw new InvalidArgumentException('Unable to get details of the key'); } return match ($details['type']) { OPENSSL_KEYTYPE_EC => self::tryToLoadECKey($pem), OPENSSL_KEYTYPE_RSA => RSAKey::createFromPEM($pem)->toArray(), -1 => self::tryToLoadOtherKeyTypes($pem), default => throw new InvalidArgumentException('Unsupported key type'), }; } /** * This method tries to load Ed448, X488, Ed25519 and X25519 keys. */ private static function tryToLoadECKey(string $input): array { try { return ECKey::createFromPEM($input)->toArray(); } catch (Throwable) { } try { return self::tryToLoadOtherKeyTypes($input); } catch (Throwable) { } throw new InvalidArgumentException('Unable to load the key.'); } /** * This method tries to load Ed448, X488, Ed25519 and X25519 keys. */ private static function tryToLoadOtherKeyTypes(string $input): array { $pem = PEM::fromString($input); return match ($pem->type()) { PEM::TYPE_PUBLIC_KEY => self::loadPublicKey($pem), PEM::TYPE_PRIVATE_KEY => self::loadPrivateKey($pem), default => throw new InvalidArgumentException('Unsupported key type'), }; } /** * @return array */ private static function loadPrivateKey(PEM $pem): array { try { $key = PrivateKey::fromPEM($pem); switch ($key->algorithmIdentifier()->oid()) { case AlgorithmIdentifier::OID_RSASSA_PSS_ENCRYPTION: assert($key instanceof RSASSAPSSPrivateKey); return [ 'kty' => 'RSA', 'n' => self::convertDecimalToBas64Url($key->modulus()), 'e' => self::convertDecimalToBas64Url($key->publicExponent()), 'd' => self::convertDecimalToBas64Url($key->privateExponent()), 'dp' => self::convertDecimalToBas64Url($key->exponent1()), 'dq' => self::convertDecimalToBas64Url($key->exponent2()), 'p' => self::convertDecimalToBas64Url($key->prime1()), 'q' => self::convertDecimalToBas64Url($key->prime2()), 'qi' => self::convertDecimalToBas64Url($key->coefficient()), ]; case AlgorithmIdentifier::OID_ED25519: case AlgorithmIdentifier::OID_ED448: case AlgorithmIdentifier::OID_X25519: case AlgorithmIdentifier::OID_X448: $curve = self::getCurve($key->algorithmIdentifier()->oid()); $values = [ 'kty' => 'OKP', 'crv' => $curve, 'd' => Base64UrlSafe::encodeUnpadded($key->privateKeyData()), ]; return self::populatePoints($key, $values); default: throw new InvalidArgumentException('Unsupported key type'); } } catch (Throwable $e) { throw new InvalidArgumentException('Unable to load the key.', 0, $e); } } /** * @return array */ private static function loadPublicKey(PEM $pem): array { $key = PublicKey::fromPEM($pem); switch ($key->algorithmIdentifier()->oid()) { case AlgorithmIdentifier::OID_ED25519: case AlgorithmIdentifier::OID_ED448: case AlgorithmIdentifier::OID_X25519: case AlgorithmIdentifier::OID_X448: $curve = self::getCurve($key->algorithmIdentifier()->oid()); self::checkType($curve); return [ 'kty' => 'OKP', 'crv' => $curve, 'x' => Base64UrlSafe::encodeUnpadded((string) $key->subjectPublicKey()), ]; default: throw new InvalidArgumentException('Unsupported key type'); } } private static function convertDecimalToBas64Url(string $decimal): string { return Base64UrlSafe::encodeUnpadded(BigInteger::fromBase($decimal, 10)->toBytes()); } /** * @param array $values * @return array */ private static function populatePoints(PrivateKey $key, array $values): array { $crv = $values['crv'] ?? null; assert(is_string($crv), 'Unsupported key type.'); $x = self::getPublicKey($key, $crv); if ($x !== null) { $values['x'] = Base64UrlSafe::encodeUnpadded($x); } return $values; } private static function getPublicKey(PrivateKey $key, string $crv): ?string { switch ($crv) { case 'Ed25519': return Ed25519::publickey_from_secretkey($key->privateKeyData()); case 'X25519': if (extension_loaded('sodium')) { return sodium_crypto_scalarmult_base($key->privateKeyData()); } // no break default: return null; } } private static function checkType(string $curve): void { $curves = ['Ed448ph', 'Ed25519ph', 'Ed448', 'Ed25519', 'X448', 'X25519']; in_array($curve, $curves, true) || throw new InvalidArgumentException('Unsupported key type.'); } /** * This method modifies the PEM to get 64 char lines and fix bug with old OpenSSL versions. */ private static function getCurve(string $oid): string { return match ($oid) { '1.3.101.115' => 'Ed448ph', '1.3.101.114' => 'Ed25519ph', '1.3.101.113' => 'Ed448', '1.3.101.112' => 'Ed25519', '1.3.101.111' => 'X448', '1.3.101.110' => 'X25519', default => throw new InvalidArgumentException('Unsupported key type.'), }; } /** * This method modifies the PEM to get 64 char lines and fix bug with old OpenSSL versions. */ private static function sanitizePEM(string &$pem): void { $number = preg_match_all('#(-.*-)#', $pem, $matches, PREG_PATTERN_ORDER); if ($number !== 2) { throw new InvalidArgumentException('Unable to load the key'); } $ciphertext = preg_replace('#-.*-|\r|\n| #', '', $pem); $pem = $matches[0][0] . "\n"; $pem .= chunk_split($ciphertext ?? '', 64, "\n"); $pem .= $matches[0][1] . "\n"; } /** * @param string[] $matches */ private static function decodePem(string $pem, array $matches, ?string $password = null): string { if ($password === null) { throw new InvalidArgumentException('Password required for encrypted keys.'); } $iv = pack('H*', trim($matches[2])); $iv_sub = mb_substr($iv, 0, 8, '8bit'); $symkey = pack('H*', md5($password . $iv_sub)); $symkey .= pack('H*', md5($symkey . $password . $iv_sub)); $key = preg_replace('#^(?:Proc-Type|DEK-Info): .*#m', '', $pem); $ciphertext = base64_decode(preg_replace('#-.*-|\r|\n#', '', $key ?? '') ?? '', true); if (! is_string($ciphertext)) { throw new InvalidArgumentException('Unable to encode the data.'); } $decoded = openssl_decrypt($ciphertext, mb_strtolower($matches[1]), $symkey, OPENSSL_RAW_DATA, $iv); if ($decoded === false) { throw new RuntimeException('Unable to decrypt the key'); } $number = preg_match_all('#-{5}.*-{5}#', $pem, $result); if ($number !== 2) { throw new InvalidArgumentException('Unable to load the key'); } $pem = $result[0][0] . "\n"; $pem .= chunk_split(base64_encode($decoded), 64); return $pem . ($result[0][1] . "\n"); } private static function convertDerToPem(string $der_data): string { $pem = chunk_split(base64_encode($der_data), 64, "\n"); return '-----BEGIN CERTIFICATE-----' . "\n" . $pem . '-----END CERTIFICATE-----' . "\n"; } }