<?php
/**
 * Antara — COSE_Key (RFC 8152) → PEM SubjectPublicKeyInfo.
 *
 * Supports:
 *   - ES256 (alg -7, kty 2 EC2, crv 1 P-256)   → id-ecPublicKey + prime256v1
 *   - RS256 (alg -257, kty 3 RSA)              → rsaEncryption
 *
 * The PEM is consumable directly by openssl_verify().
 */

final class AntaraCose
{
    public const ALG_ES256 = -7;
    public const ALG_RS256 = -257;

    public const KTY_OKP = 1;
    public const KTY_EC2 = 2;
    public const KTY_RSA = 3;

    /** Returns ['pem' => string, 'alg' => int]. */
    public static function coseKeyToPem(array $cose): array
    {
        $kty = $cose[1] ?? null;
        $alg = $cose[3] ?? null;

        if ($kty === self::KTY_EC2 && $alg === self::ALG_ES256) {
            $crv = $cose[-1] ?? null;
            $x   = $cose[-2] ?? null;
            $y   = $cose[-3] ?? null;
            if ($crv !== 1) throw new RuntimeException('COSE: unsupported EC curve');
            if (!is_string($x) || strlen($x) !== 32) throw new RuntimeException('COSE: bad EC x');
            if (!is_string($y) || strlen($y) !== 32) throw new RuntimeException('COSE: bad EC y');
            return ['pem' => self::ecP256SpkiPem($x, $y), 'alg' => self::ALG_ES256];
        }

        if ($kty === self::KTY_RSA && $alg === self::ALG_RS256) {
            $n = $cose[-1] ?? null;
            $e = $cose[-2] ?? null;
            if (!is_string($n) || !is_string($e)) throw new RuntimeException('COSE: bad RSA n/e');
            return ['pem' => self::rsaSpkiPem($n, $e), 'alg' => self::ALG_RS256];
        }

        throw new RuntimeException('COSE: unsupported (kty=' . var_export($kty, true) . ', alg=' . var_export($alg, true) . ')');
    }

    /** Returns the openssl algo constant for a COSE alg. */
    public static function opensslAlgo(int $coseAlg): int
    {
        if ($coseAlg === self::ALG_ES256 || $coseAlg === self::ALG_RS256) {
            return OPENSSL_ALGO_SHA256;
        }
        throw new RuntimeException('COSE: unsupported alg ' . $coseAlg);
    }

    /* --------------- ASN.1 DER builders --------------- */

    private static function derLen(int $len): string
    {
        if ($len < 0x80) return chr($len);
        $bytes = '';
        while ($len > 0) {
            $bytes = chr($len & 0xff) . $bytes;
            $len >>= 8;
        }
        return chr(0x80 | strlen($bytes)) . $bytes;
    }

    private static function derSeq(string $body): string  { return "\x30" . self::derLen(strlen($body)) . $body; }
    private static function derBitStr(string $body): string { return "\x03" . self::derLen(strlen($body) + 1) . "\x00" . $body; }
    private static function derOctet(string $b): string   { return "\x04" . self::derLen(strlen($b)) . $b; }
    private static function derOidRaw(string $oidBytes): string { return "\x06" . self::derLen(strlen($oidBytes)) . $oidBytes; }
    private static function derNull(): string             { return "\x05\x00"; }

    /** Encodes a positive integer (big-endian, no leading zero except sign byte). */
    private static function derInt(string $magnitude): string
    {
        // strip leading zeros
        $i = 0;
        while ($i < strlen($magnitude) - 1 && $magnitude[$i] === "\x00") $i++;
        $magnitude = substr($magnitude, $i);
        // If high bit is set, prepend 0x00 to keep it positive.
        if ((ord($magnitude[0]) & 0x80) !== 0) $magnitude = "\x00" . $magnitude;
        return "\x02" . self::derLen(strlen($magnitude)) . $magnitude;
    }

    private static function pemWrap(string $der, string $label): string
    {
        $b64 = chunk_split(base64_encode($der), 64, "\n");
        return "-----BEGIN {$label}-----\n{$b64}-----END {$label}-----\n";
    }

    /* --------------- Specific builders --------------- */

    /** OID 1.2.840.10045.2.1 = id-ecPublicKey. */
    private static function oidEcPublicKey(): string  { return "\x2A\x86\x48\xCE\x3D\x02\x01"; }
    /** OID 1.2.840.10045.3.1.7 = prime256v1 (P-256). */
    private static function oidP256(): string         { return "\x2A\x86\x48\xCE\x3D\x03\x01\x07"; }
    /** OID 1.2.840.113549.1.1.1 = rsaEncryption. */
    private static function oidRsaEncryption(): string { return "\x2A\x86\x48\x86\xF7\x0D\x01\x01\x01"; }

    private static function ecP256SpkiPem(string $x, string $y): string
    {
        $alg = self::derSeq(self::derOidRaw(self::oidEcPublicKey()) . self::derOidRaw(self::oidP256()));
        $point = "\x04" . $x . $y; // uncompressed point
        $spki = self::derSeq($alg . self::derBitStr($point));
        return self::pemWrap($spki, 'PUBLIC KEY');
    }

    private static function rsaSpkiPem(string $nMag, string $eMag): string
    {
        $rsaPub = self::derSeq(self::derInt($nMag) . self::derInt($eMag));
        $alg = self::derSeq(self::derOidRaw(self::oidRsaEncryption()) . self::derNull());
        $spki = self::derSeq($alg . self::derBitStr($rsaPub));
        return self::pemWrap($spki, 'PUBLIC KEY');
    }
}
