Enable Discovery debug logging for production troubleshooting
- Add DISCOVERY_LOG_LEVEL=debug - Add DISCOVERY_SHOW_PROGRESS=true - Temporary changes for debugging InitializerProcessor fixes on production
This commit is contained in:
259
src/Framework/Cryptography/PublicKey.php
Normal file
259
src/Framework/Cryptography/PublicKey.php
Normal file
@@ -0,0 +1,259 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Framework\Cryptography;
|
||||
|
||||
use InvalidArgumentException;
|
||||
|
||||
/**
|
||||
* Public Key Value Object
|
||||
*
|
||||
* Represents a cryptographic public key with metadata.
|
||||
*/
|
||||
final readonly class PublicKey
|
||||
{
|
||||
public function __construct(
|
||||
private string $keyMaterial,
|
||||
private string $algorithm,
|
||||
private int $keySize,
|
||||
private ?string $curve = null
|
||||
) {
|
||||
if (empty($keyMaterial)) {
|
||||
throw new InvalidArgumentException('Key material cannot be empty');
|
||||
}
|
||||
|
||||
if (empty($algorithm)) {
|
||||
throw new InvalidArgumentException('Algorithm cannot be empty');
|
||||
}
|
||||
|
||||
$supportedAlgorithms = ['rsa', 'ecdsa'];
|
||||
if (! in_array($algorithm, $supportedAlgorithms, true)) {
|
||||
throw new InvalidArgumentException('Unsupported algorithm');
|
||||
}
|
||||
|
||||
if ($keySize < 256) {
|
||||
throw new InvalidArgumentException('Key size must be at least 256 bits');
|
||||
}
|
||||
|
||||
if ($algorithm === 'ecdsa' && $curve === null) {
|
||||
throw new InvalidArgumentException('ECDSA keys must specify a curve');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the key material (PEM format)
|
||||
*/
|
||||
public function getKeyMaterial(): string
|
||||
{
|
||||
return $this->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)
|
||||
*/
|
||||
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
|
||||
*/
|
||||
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
|
||||
*/
|
||||
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), '+/', '-_'), '=');
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user