web-auth / cose-lib

Cose Key and Algorithms support
MIT License
17 stars 6 forks source link

COSEToPEMConverter that’s compatible with PHP 5.6 #87

Closed kedimomo closed 3 months ago

kedimomo commented 3 months ago

Description

I’ve been looking for a COSEToPEMConverter that’s compatible with PHP 5.6, because the minimum requirement for web-auth/cose-lib is PHP 7.0. It’s been a desperate search, taking up 2 weeks of my time. If anyone else finds this library and realizes their version is also PHP 5.6, you can try it out, giving you a glimmer of hope. This is just the most basic support, I’ve only tested it with Windows Hello, I can’t afford to buy a physical key because it’s too expensive. So, I haven’t tried ES256, but RSA works fine.

Example

class COSEToPEMConverter_v2 {

    public function extractCoseKeyAndAlg($attestationObject) {

    $attestationObject_decode = base64url_decode($attestationObject);
        $decoder = new CBOREncoder();
        $decoded = $decoder->decode($attestationObject_decode);

        // Extract attStmt from decoded structure
        $attStmt = $decoded['attStmt'];
        $alg = $attStmt['alg'];

        // Extract authData from decoded structure
        $authData = $decoded['authData']->get_byte_string();
        $coseKey = $this->extractCoseKeyFromAuthData($authData);

        return ['coseKey'=>$coseKey, 'alg'=> $alg];
    }

    private function extractCoseKeyFromAuthData($authData) {
        $offset = 37; // RP ID Hash (32) + Flags (1) + Sign Count (4)
        $aaguid = substr($authData, $offset, 16);
        $offset += 16;
        $credentialIdLength = unpack('n', substr($authData, $offset, 2))[1];
        $offset += 2;
        $credentialId = substr($authData, $offset, $credentialIdLength);
        $offset += $credentialIdLength;

        // The remaining data is the COSE Key
        $coseKey = substr($authData, $offset);

        return $coseKey;
    }

    public function createPemFromCoseKey($coseKey, $alg) {
        $decoder = new CBOREncoder();
        $coseData = $decoder->decode($coseKey);

        // Debug: Print the COSE data
        //var_dump($coseData);

        if ($alg == -257) { // RS256 algorithm
            $n = $coseData[-1]->get_byte_string();
            $e = $coseData[-2]->get_byte_string();

            // Debug: Print the key parameters
           // var_dump($n, $e);

            $asn1 = $this->buildRsaPublicKey($n, $e);

            $pem = "-----BEGIN PUBLIC KEY-----\n";
            $pem .= chunk_split(base64_encode($asn1), 64, "\n");
            $pem .= "-----END PUBLIC KEY-----\n";

            return $pem;
        } elseif ($alg == -7) { // ES256 (ECDSA) algorithm
            $x = $coseData[-2]->get_byte_string();
            $y = $coseData[-3]->get_byte_string();

            // Debug: Print the key parameters
            //var_dump($x, $y);

            $asn1 = $this->buildEcdsaPublicKey($x, $y);

            $pem = "-----BEGIN PUBLIC KEY-----\n";
            $pem .= chunk_split(base64_encode($asn1), 64, "\n");
            $pem .= "-----END PUBLIC KEY-----\n";

            return $pem;
        }  else {
            throw new Exception('Unsupported key type or algorithm');
        }
    }

    private function buildRsaPublicKey($n, $e) {
        // Ensure the length of n and e are correctly encoded in ASN.1 format
        $n = ltrim($n, "\x00"); // Remove leading null bytes
        $e = ltrim($e, "\x00"); // Remove leading null bytes

        // ASN.1 structure
        $asn1_n = "\x02" . $this->lengthBytes(strlen($n)) . $n;
        $asn1_e = "\x02" . $this->lengthBytes(strlen($e)) . $e;
        $asn1_seq = "\x30" . $this->lengthBytes(strlen($asn1_n) + strlen($asn1_e)) . $asn1_n . $asn1_e;

        $asn1_bitstring = "\x03" . $this->lengthBytes(strlen($asn1_seq) + 1) . "\x00" . $asn1_seq;
        $asn1_algo = "\x30\x0D\x06\x09\x2A\x86\x48\x86\xF7\x0D\x01\x01\x01\x05\x00";
        $asn1_pubkey = "\x30" . $this->lengthBytes(strlen($asn1_algo) + strlen($asn1_bitstring)) . $asn1_algo . $asn1_bitstring;

        return $asn1_pubkey;
    }

    private function buildEcdsaPublicKey($x, $y) {
        // Ensure the length of x and y are correctly encoded in ASN.1 format
        $x = ltrim($x, "\x00"); // Remove leading null bytes
        $y = ltrim($y, "\x00"); // Remove leading null bytes

        $ecPublicKey = "\x04" . $x . $y;

        // ASN.1 structure
        $asn1_bitstring = "\x03" . $this->lengthBytes(strlen($ecPublicKey) + 1) . "\x00" . $ecPublicKey;
        $asn1_algo = "\x30\x13\x06\x07\x2A\x86\x48\xCE\x3D\x02\x01\x06\x08\x2A\x86\x48\xCE\x3D\x03\x01\x07";
        $asn1_pubkey = "\x30" . $this->lengthBytes(strlen($asn1_algo) + strlen($asn1_bitstring)) . $asn1_algo . $asn1_bitstring;

        return $asn1_pubkey;
    }

    private function lengthBytes($length) {
        if ($length < 128) {
            return chr($length);
        } elseif ($length < 256) {
            return "\x81" . chr($length);
        } else {
            return "\x82" . pack('n', $length);
        }
    }

}
function base64url_encode($data) {
    return rtrim(strtr(base64_encode($data), '+/', '-_'), '=');
}

function base64url_decode($data) {
    return base64_decode(strtr($data, '-_', '+/'));
}

使用方法:

         // Decode the CBOR object
    // composer require 2tvenom/cborencode   安装命令
    $converter = new COSEToPEMConverter_v2();
    $CoseKeyAndAlg = $converter->extractCoseKeyAndAlg($attestationObject);
    $pemKey = $converter->createPemFromCoseKey($CoseKeyAndAlg['coseKey'], $CoseKeyAndAlg['alg']);
    // 创建一个 OpenSSL 公钥资源
    $publicKey = openssl_pkey_get_public($pemKey);

备注 :

"The object passed from the front end to the back end depends on the encoding you use. The navigator.credentials.create function returns a credential, where attestationObject is base64url(credential.response.attestationObject).

Because I'm using the following function to encode:

function base64url(buffer) {
    return btoa(String.fromCharCode.apply(null, new Uint8Array(buffer)))
        .replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/, '');
}

Wishing good luck to those who come after, the fallen 5.6.

Spomky commented 3 months ago

Hi,

PHP <8.1 reached EOL and are not supported anymore. As per the security policy, the version 3.3.x of this library is still maintained for security fix only. We do not recommened the use of those outdated versions of PHP. Tools, such as Rector, exist and can help upgrading your applications to newer PHP versions. This is the way to go instead of updating each dependencies.

kedimomo commented 2 months ago

Hi,

PHP <8.1 reached EOL and are not supported anymore. As per the security policy, the version 3.3.x of this library is still maintained for security fix only. We do not recommened the use of those outdated versions of PHP. Tools, such as Rector, exist and can help upgrading your applications to newer PHP versions. This is the way to go instead of updating each dependencies.

Thank you, author. Since a virtual host with PHP 5.6 only costs around 100 per year, while a virtual host with PHP 8 costs over 600, I am reluctant to spend the extra money and have to use the old version. This is just a reference for those using the old version. It’s normal for new frameworks not to support it. It’s not necessary, but it’s just to give those who find your framework and happen to be using PHP 5 a hope that they can solve it themselves. Sometimes, due to economic considerations, it’s not possible to use the latest version.