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.
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)
.
Wishing good luck to those who come after, the fallen 5.6.