<?php
 declare(strict_types=1); namespace SpomkyLabs\Pki\CryptoTypes\Asymmetric\EC; use InvalidArgumentException; use LogicException; use RuntimeException; use SpomkyLabs\Pki\ASN1\Type\Primitive\BitString; use SpomkyLabs\Pki\ASN1\Type\Primitive\Integer; use SpomkyLabs\Pki\ASN1\Type\Primitive\OctetString; use SpomkyLabs\Pki\CryptoEncoding\PEM; use SpomkyLabs\Pki\CryptoTypes\AlgorithmIdentifier\AlgorithmIdentifier; use SpomkyLabs\Pki\CryptoTypes\AlgorithmIdentifier\Asymmetric\ECPublicKeyAlgorithmIdentifier; use SpomkyLabs\Pki\CryptoTypes\AlgorithmIdentifier\Feature\AlgorithmIdentifierType; use SpomkyLabs\Pki\CryptoTypes\Asymmetric\PublicKey; use SpomkyLabs\Pki\CryptoTypes\Asymmetric\PublicKeyInfo; use UnexpectedValueException; use function array_key_exists; use function in_array; use function mb_strlen; use function ord; final class ECPublicKey extends PublicKey { private readonly string $ecPoint; private function __construct( string $ecPoint, private readonly ?string $namedCurve ) { if (($ecPoint === '') || ! in_array(ord($ecPoint[0]), [2, 3, 4], true)) { throw new InvalidArgumentException('Invalid ECPoint.'); } $this->ecPoint = $ecPoint; } public static function create(string $ecPoint, ?string $namedCurve = null): self { return new self($ecPoint, $namedCurve); } public static function fromCoordinates( int|string $x, int|string $y, ?string $named_curve = null, ?int $bits = null ): self { if (! isset($bits) && isset($named_curve)) { $bits = self::_curveSize($named_curve); } $mlen = null; if (isset($bits)) { $mlen = (int) ceil($bits / 8); } $x_os = ECConversion::integerToOctetString(Integer::create($x), $mlen)->string(); $y_os = ECConversion::integerToOctetString(Integer::create($y), $mlen)->string(); $ec_point = "\x4{$x_os}{$y_os}"; return self::create($ec_point, $named_curve); } public static function fromPEM(PEM $pem): self { if ($pem->type() !== PEM::TYPE_PUBLIC_KEY) { throw new UnexpectedValueException('Not a public key.'); } $pki = PublicKeyInfo::fromDER($pem->data()); $algo = $pki->algorithmIdentifier(); if ($algo->oid() !== AlgorithmIdentifier::OID_EC_PUBLIC_KEY || ! ($algo instanceof ECPublicKeyAlgorithmIdentifier)) { throw new UnexpectedValueException('Not an elliptic curve key.'); } return self::create($pki->publicKeyData()->string(), $algo->namedCurve()); } public function ECPoint(): string { return $this->ecPoint; } public function curvePoint(): array { return array_map(ECConversion::octetsToNumber(...), $this->curvePointOctets()); } public function curvePointOctets(): array { if ($this->isCompressed()) { throw new RuntimeException('EC point compression not supported.'); } $str = mb_substr($this->ecPoint, 1, null, '8bit'); $length = (int) floor(mb_strlen($str, '8bit') / 2); if ($length < 1) { throw new RuntimeException('Invalid EC point.'); } [$x, $y] = mb_str_split($str, $length, '8bit'); return [$x, $y]; } public function isCompressed(): bool { $c = ord($this->ecPoint[0]); return $c !== 4; } public function hasNamedCurve(): bool { return isset($this->namedCurve); } public function namedCurve(): string { if (! $this->hasNamedCurve()) { throw new LogicException('namedCurve not set.'); } return $this->namedCurve; } public function algorithmIdentifier(): AlgorithmIdentifierType { return ECPublicKeyAlgorithmIdentifier::create($this->namedCurve()); } public function toASN1(): OctetString { return OctetString::create($this->ecPoint); } public function toDER(): string { return $this->toASN1() ->toDER(); } public function subjectPublicKey(): BitString { return BitString::create($this->ecPoint); } private static function _curveSize(string $oid): ?int { if (! array_key_exists($oid, ECPublicKeyAlgorithmIdentifier::MAP_CURVE_TO_SIZE)) { return null; } return ECPublicKeyAlgorithmIdentifier::MAP_CURVE_TO_SIZE[$oid]; } } 