keyMaterial; } /** * Get the algorithm */ public function getAlgorithm(): string { return $this->algorithm; } /** * Get the key size in bits */ public function getKeySize(): int { return $this->keySize; } /** * Get the curve name (ECDSA only) */ public function getCurve(): ?string { return $this->curve; } /** * Check if this is an RSA key */ public function isRsa(): bool { return $this->algorithm === 'rsa'; } /** * Check if this is an ECDSA key */ public function isEcdsa(): bool { return $this->algorithm === 'ecdsa'; } /** * Export key to array (includes key material - safe for public keys) * @return array */ public function toArray(): array { $data = [ 'key_material' => $this->keyMaterial, 'algorithm' => $this->algorithm, 'key_size' => $this->keySize, ]; if ($this->curve !== null) { $data['curve'] = $this->curve; } return $data; } /** * Create from array * @param array $data */ public static function fromArray(array $data): self { $requiredFields = ['key_material', 'algorithm', 'key_size']; foreach ($requiredFields as $field) { if (! isset($data[$field])) { throw new InvalidArgumentException("Missing required field: {$field}"); } } return new self( keyMaterial: $data['key_material'], algorithm: $data['algorithm'], keySize: (int)$data['key_size'], curve: $data['curve'] ?? null ); } /** * Get key fingerprint (SHA-256 hash) */ public function getFingerprint(): string { return hash('sha256', $this->keyMaterial); } /** * Get short fingerprint (first 16 chars of full fingerprint) */ public function getShortFingerprint(): string { return substr($this->getFingerprint(), 0, 16); } /** * Get key in DER format (binary) */ public function getDerFormat(): string { $resource = openssl_pkey_get_public($this->keyMaterial); if ($resource === false) { throw new InvalidArgumentException('Invalid public key'); } $details = openssl_pkey_get_details($resource); if ($details === false || ! isset($details['key'])) { throw new InvalidArgumentException('Failed to get key details'); } // Convert PEM to DER $pem = $details['key']; $pem = str_replace(['-----BEGIN PUBLIC KEY-----', '-----END PUBLIC KEY-----'], '', $pem); $pem = str_replace(["\r", "\n", " "], '', $pem); $der = base64_decode($pem); if ($der === false) { throw new InvalidArgumentException('Failed to convert to DER format'); } return $der; } /** * Get key type description */ public function getDescription(): string { $description = strtoupper($this->algorithm); if ($this->algorithm === 'rsa') { $description .= " {$this->keySize}-bit"; } elseif ($this->algorithm === 'ecdsa') { $description .= " {$this->curve}"; } return $description; } /** * Check equality with another public key */ public function equals(self $other): bool { return hash_equals($this->keyMaterial, $other->keyMaterial); } /** * Validate key material */ public function isValid(): bool { $resource = openssl_pkey_get_public($this->keyMaterial); return $resource !== false; } /** * Get key as JWK (JSON Web Key) format * @return array */ public function toJwk(): array { if (! $this->isValid()) { throw new InvalidArgumentException('Invalid key material'); } $resource = openssl_pkey_get_public($this->keyMaterial); $details = openssl_pkey_get_details($resource); if ($this->algorithm === 'rsa') { return [ 'kty' => 'RSA', 'alg' => 'RS256', 'use' => 'sig', 'n' => $this->base64UrlEncode($details['rsa']['n']), 'e' => $this->base64UrlEncode($details['rsa']['e']), ]; } if ($this->algorithm === 'ecdsa') { $crv = match ($this->curve) { 'prime256v1' => 'P-256', 'secp384r1' => 'P-384', 'secp521r1' => 'P-521', default => throw new InvalidArgumentException('Unsupported curve for JWK') }; return [ 'kty' => 'EC', 'alg' => 'ES256', 'use' => 'sig', 'crv' => $crv, 'x' => $this->base64UrlEncode($details['ec']['x']), 'y' => $this->base64UrlEncode($details['ec']['y']), ]; } throw new InvalidArgumentException('Unsupported algorithm for JWK conversion'); } /** * Base64 URL encode (for JWK format) */ private function base64UrlEncode(string $data): string { return rtrim(strtr(base64_encode($data), '+/', '-_'), '='); } }