Closed sebmeg closed 2 years ago
For Better understanding: https://pastebin.com/Ev6wx6cj
First i decode Base45, then decompress using zlib and put it in this class.
Hi @sebmeg,
Thank you for opening this issue. I was not aware of the use of the CBOR format for such case. I've just tested with the data from the blog post you mentioned and I can load the expected data.
The missing part is that the tag for COSE signature is not supported by this library (this is something I definitively want to do!). But this is not an issue at all, we can create one for the example.
Also, the data cannot be verified as I don't have the public key. Hereafter a working example with comments. Let me know if you need help, I am really curious!
<?php
use CBOR\ByteStringObject;
use CBOR\CBORObject;
use CBOR\Decoder;
use CBOR\ListObject;
use CBOR\OtherObject\OtherObjectManager;
use CBOR\StringStream;
use CBOR\Tag\TagObjectManager;
use CBOR\TagObject as Base;
use Mhauri\Base45;
require_once 'vendor/autoload.php';
$data = 'HC1:6BFNX1:HM*I0PS3TLU.NGMU5AG8JKM:SF9VN1RFBIKJ:3AXL1RR+ 8::N$OAG+RC4NKT1:P4.33GH40HD*98UIHJIDB 4N*2R7C*MCV+1AY3:YP*YVNUHC.G-NFPIR6UBRRQL9K5%L4.Q*4986NBHP95R*QFLNUDTQH-GYRN2FMGO73ZG6ZTJZC:$0$MTZUF2A81R9NEBTU2Y437XCI9DU 4S3N%JRP:HPE3$ 435QJ+UJVGYLJIMPI%2+YSUXHB42VE5M44%IJLX0SYI7BU+EGCSHG:AQ+58CEN RAXI:D53H8EA0+WAI9M8JC0D0S%8PO00DJAPE3 GZZB:X85Y8345MOLUZ3+HT0TRS76MW2O.0CGL EQ5AI.XM5 01LCWBA.RE.-SUYH+S7SBE0%B-KT+YSMFCLTQQQ6LEHG.P46UNL6DA2C$AF-SQ00A58HYO5:M8 7S$ULGC-IP49MZCSU8ST3HDRJNPV3UJADJ9BVV:7K13B4WQ+DCTEG4V8OT09797FZMQ3/A7DU0.3D148IDZ%UDR9CYF';
$base45Decoder = new Base45;
$decoded = $base45Decoder->decode(mb_substr($data, 4)); // We remove the HC1: prefix
$decompressed = zlib_decode($decoded); // We unzip the data
$stream = new StringStream($decompressed);
final class CoseSign1Tag extends Base // Specific tag for the example
{
public static function getTagId(): int
{
return 18;
}
public static function createFromLoadedData(int $additionalInformation, ?string $data, CBORObject $object): Base
{
return new self($additionalInformation, $data, $object);
}
public function getNormalizedData(bool $ignoreTags = false)
{
return $this->getValue()->getNormalizedData($ignoreTags);
}
}
$tagObjectManager = new TagObjectManager();
$tagObjectManager->add(CoseSign1Tag::class);
$cborDecoder = new Decoder($tagObjectManager, new OtherObjectManager());
$cbor = $cborDecoder->decode($stream); //We decode the data
if (!$cbor instanceof CoseSign1Tag) {
throw new InvalidArgumentException('Not a valid certificate. Not a CoseSign1 type.');
}
$list = $cbor->getValue();
if (!$list instanceof ListObject) {
throw new InvalidArgumentException('Not a valid certificate. No list.');
}
if ($list->count() !==4) {
throw new InvalidArgumentException('Not a valid certificate. The list size is not correct.');
}
$firstItem = $list->get(0); // The first item corresponds to the protected header
$headerStream = new StringStream($firstItem->getValue()); // The first item is also a CBOR encoded byte string
dump('Protected header', $cborDecoder->decode($headerStream)->getNormalizedData()); // The array [1 => "-7"] = ["alg" => "ES256"]
$secondItem = $list->get(1); // The second item corresponds to unprotected header
dump('Unprotected header', $secondItem->getNormalizedData()); // The index 4 refers to the 'kid' (key ID) parameter (see https://www.iana.org/assignments/cose/cose.xhtml)
$thirdItem = $list->get(2); // The third item corresponds to the data we want to load
if (!$thirdItem instanceof ByteStringObject) {
throw new InvalidArgumentException('Not a valid certificate. The payload is not a byte string.');
}
$infoStream = new StringStream($thirdItem->getValue()); // The third item is a CBOR encoded byte string
dump('The payload', $cborDecoder->decode($infoStream)->getNormalizedData()); // The data we are looking for
$fourthItem = $list->get(3); // The fourth item is the signature.
// It can be verified using the protected header (first item) and the data (third item)
// And the public key
if (!$fourthItem instanceof ByteStringObject) {
throw new InvalidArgumentException('Not a valid certificate. The signature is not a byte string.');
}
dump('Digital signature', $fourthItem->getNormalizedData()); // The digital signature
Hi, I tested the above code with a vaccination certificate issued by the Romanian Health Ministry and it works fine with the caveats mentioned. Thank you for the example!
I think the data required for checking the signature can be found here: https://raw.githubusercontent.com/Digitaler-Impfnachweis/covpass-ios/main/Certificates/PROD_RKI/CA/dsc.json
The "kid" referred in the file above is different for each county and is the data from key 4 in the protected header, base64 encoded.
Hi all,
Thank you @nonsintetic for the information. I tried to verify the digital signature with the appropriate key and it works fine!!!
Note that the keyset you shared is outdated and the key ID is missing from this set. I've found it in the v1.9.0 branch of the same project: https://raw.githubusercontent.com/Digitaler-Impfnachweis/covpass-ios/v1.9.0/Certificates/PROD/CA/dsc.json
Hereafter a full example where loading and data verification pass.
<?php
declare(strict_types=1);
use CBOR\ByteStringObject;
use CBOR\CBORObject;
use CBOR\Decoder;
use CBOR\ListObject;
use CBOR\OtherObject\OtherObjectManager;
use CBOR\StringStream;
use CBOR\Tag\TagObjectManager;
use CBOR\TagObject as Base;
use CBOR\TextStringObject;
use Mhauri\Base45;
require_once 'vendor/autoload.php';
$data = 'HC1:6BFNX1:HM*I0PS3TLU.NGMU5AG8JKM:SF9VN1RFBIKJ:3AXL1RR+ 8::N$OAG+RC4NKT1:P4.33GH40HD*98UIHJIDB 4N*2R7C*MCV+1AY3:YP*YVNUHC.G-NFPIR6UBRRQL9K5%L4.Q*4986NBHP95R*QFLNUDTQH-GYRN2FMGO73ZG6ZTJZC:$0$MTZUF2A81R9NEBTU2Y437XCI9DU 4S3N%JRP:HPE3$ 435QJ+UJVGYLJIMPI%2+YSUXHB42VE5M44%IJLX0SYI7BU+EGCSHG:AQ+58CEN RAXI:D53H8EA0+WAI9M8JC0D0S%8PO00DJAPE3 GZZB:X85Y8345MOLUZ3+HT0TRS76MW2O.0CGL EQ5AI.XM5 01LCWBA.RE.-SUYH+S7SBE0%B-KT+YSMFCLTQQQ6LEHG.P46UNL6DA2C$AF-SQ00A58HYO5:M8 7S$ULGC-IP49MZCSU8ST3HDRJNPV3UJADJ9BVV:7K13B4WQ+DCTEG4V8OT09797FZMQ3/A7DU0.3D148IDZ%UDR9CYF';
$base45Processor = new Base45();
$decoded = $base45Processor->decode(mb_substr($data, 4)); // We remove the HC1: prefix
$decompressed = zlib_decode($decoded); // We unzip the data
$stream = new StringStream($decompressed);
final class CoseSign1Tag extends Base // Specific tag for the example
{
public static function getTagId(): int
{
return 18;
}
public static function createFromLoadedData(int $additionalInformation, ?string $data, CBORObject $object): Base
{
return new self($additionalInformation, $data, $object);
}
public function getNormalizedData(bool $ignoreTags = false)
{
return $this->getValue()->getNormalizedData($ignoreTags);
}
}
$tagObjectManager = new TagObjectManager();
$tagObjectManager->add(CoseSign1Tag::class);
$cborDecoder = new Decoder($tagObjectManager, new OtherObjectManager());
$cbor = $cborDecoder->decode($stream); //We decode the data
if (!$cbor instanceof CoseSign1Tag) {
throw new InvalidArgumentException('Not a valid certificate. Not a CoseSign1 type.');
}
$list = $cbor->getValue();
if (!$list instanceof ListObject) {
throw new InvalidArgumentException('Not a valid certificate. No list.');
}
if (4 !== $list->count()) {
throw new InvalidArgumentException('Not a valid certificate. The list size is not correct.');
}
dump($list);
$firstItem = $list->get(0); // The first item corresponds to the protected header
$headerStream = new StringStream($firstItem->getValue()); // The first item is also a CBOR encoded byte string
$protectedHeader = $cborDecoder->decode($headerStream);
dump('Protected header', $protectedHeader->getNormalizedData()); // The array [1 => "-7"] = ["alg" => "ES256"]
$secondItem = $list->get(1); // The second item corresponds to unprotected header
$unprotectedHeader = $secondItem;
dump('Unprotected header', $unprotectedHeader->getNormalizedData()); // The index 4 refers to the 'kid' (key ID) parameter (see https://www.iana.org/assignments/cose/cose.xhtml)
$keyId = base64_encode($unprotectedHeader->getNormalizedData()[4]);
$thirdItem = $list->get(2); // The third item corresponds to the data we want to load
if (!$thirdItem instanceof ByteStringObject) {
throw new InvalidArgumentException('Not a valid certificate. The payload is not a byte string.');
}
$infoStream = new StringStream($thirdItem->getValue()); // The third item is a CBOR encoded byte string
$payload = $cborDecoder->decode($infoStream);
dump('The payload', $payload->getNormalizedData()); // The data we are looking for
$fourthItem = $list->get(3); // The fourth item is the signature.
// It can be verified using the protected header (first item) and the data (third item)
// And the public key
if (!$fourthItem instanceof ByteStringObject) {
throw new InvalidArgumentException('Not a valid certificate. The signature is not a byte string.');
}
$signature = $fourthItem;
dump('Digital signature', $signature->getNormalizedData()); // The digital signature
//We retrieve the public keys
$ch = curl_init('https://raw.githubusercontent.com/Digitaler-Impfnachweis/covpass-ios/v1.9.0/Certificates/PROD/CA/dsc.json');
$fp = fopen('dsc.json', 'wb');
curl_setopt($ch, CURLOPT_FILE, $fp);
curl_setopt($ch, CURLOPT_HEADER, 0);
curl_exec($ch);
if (curl_error($ch)) {
fwrite($fp, curl_error($ch));
}
curl_close($ch);
fclose($fp);
//We decode the JSON object we received
$certificates = json_decode(file_get_contents('dsc.json'), true, 512, JSON_THROW_ON_ERROR);
//We filter the keyset using the country and the key ID from the data
$country = $payload->getNormalizedData()[1];
$countryCertificates = array_filter($certificates['certificates'], static function (array $data) use ($country, $keyId): bool {
return $data['country'] === $country && $data['kid'] === $keyId;
});
//If no public key is found, we cannot continue
if (1 !== count($countryCertificates)) {
throw new \InvalidArgumentException('Public key not found');
}
//We convert the raw data into a PEM encoded certificate
$signingCertificate = current($countryCertificates);
$pem = chunk_split($signingCertificate['rawData'], 64, PHP_EOL);
$pem = '-----BEGIN CERTIFICATE-----'.PHP_EOL.$pem.'-----END CERTIFICATE-----'.PHP_EOL;
//Needed to convert the digital signature into an ASN.1 signature (this format is required by OpenSSL)
final class ECSignature
{
private const ASN1_SEQUENCE = '30';
private const ASN1_INTEGER = '02';
private const ASN1_MAX_SINGLE_BYTE = 128;
private const ASN1_LENGTH_2BYTES = '81';
private const ASN1_BIG_INTEGER_LIMIT = '7f';
private const ASN1_NEGATIVE_INTEGER = '00';
private const BYTE_SIZE = 2;
/**
* @throws InvalidArgumentException if the length of the signature is invalid
*/
public static function toAsn1(string $signature, int $length): string
{
$signature = bin2hex($signature);
if (self::octetLength($signature) !== $length) {
throw new InvalidArgumentException('Invalid signature length.');
}
$pointR = self::preparePositiveInteger(mb_substr($signature, 0, $length, '8bit'));
$pointS = self::preparePositiveInteger(mb_substr($signature, $length, null, '8bit'));
$lengthR = self::octetLength($pointR);
$lengthS = self::octetLength($pointS);
$totalLength = $lengthR + $lengthS + self::BYTE_SIZE + self::BYTE_SIZE;
$lengthPrefix = $totalLength > self::ASN1_MAX_SINGLE_BYTE ? self::ASN1_LENGTH_2BYTES : '';
$bin = hex2bin(
self::ASN1_SEQUENCE
.$lengthPrefix.dechex($totalLength)
.self::ASN1_INTEGER.dechex($lengthR).$pointR
.self::ASN1_INTEGER.dechex($lengthS).$pointS
);
if (!is_string($bin)) {
throw new InvalidArgumentException('Unable to parse the data');
}
return $bin;
}
private static function octetLength(string $data): int
{
return (int) (mb_strlen($data, '8bit') / self::BYTE_SIZE);
}
private static function preparePositiveInteger(string $data): string
{
if (mb_substr($data, 0, self::BYTE_SIZE, '8bit') > self::ASN1_BIG_INTEGER_LIMIT) {
return self::ASN1_NEGATIVE_INTEGER.$data;
}
while (0 === mb_strpos($data, self::ASN1_NEGATIVE_INTEGER, 0, '8bit')
&& mb_substr($data, 2, self::BYTE_SIZE, '8bit') <= self::ASN1_BIG_INTEGER_LIMIT) {
$data = mb_substr($data, 2, null, '8bit');
}
return $data;
}
}
//The object is the data that should have been signed
$structure = new ListObject();
$structure->add(new TextStringObject('Signature1'));
$structure->add($firstItem);
$structure->add(new ByteStringObject(''));
$structure->add($thirdItem);
//COnverted signature
$derSignature = ECSignature::toAsn1($signature->getNormalizedData(), 64);
//We verify the signature with the data structure and the PEM encoded key
// If valid, the result is 1
$isValid = 1 === openssl_verify((string) $structure, $derSignature, $pem, 'sha256');
if (!$isValid) {
while ($m = openssl_error_string()) {
dump($m);
}
throw new \InvalidArgumentException('The signature is NOT valid');
}
// At this point, we have the data and the signature is verified.
Lengendary! It all works!
Excellent. I'm happy to read that it also work for you. Next step for me is to work on the COSE/CWT RFC implementations to simlify all of that spaghetti code.
Hi all! :wave:
This thread really helped me putting together a library for reading and validating EU eHealth certificates: https://github.com/stw-on/covpasscheck-php
Maybe it's useful for some of you.
Thank you for your work! :)
This is a valid CH certificate but the index 4 that refers to the 'kid' (key ID) parameter is missing.
HC1:NCFK60DG0/3WUWGSLKH47GO0:S4KQDITFAUO9CK-500XK0JCV496F3JBS33S3F3MU394SY50.FK6ZK7:EDOLOPCO8F6%E3.DA%EOPC1G72A6YM86G77460A6TL6IL6G*8J*8:Q6E46VM8K:6 47FN8UPC0JCZ69FVCPD0LVC6JD846Y96E463W5.A6+EDG8F3I80/D6$CBECSUER:C2$NS346$C2%E9VC- CSUE145GB8JA5B$D% D3IA4W5646646-96:96.JCP9EJY8L/5M/5546.96SF63KC.SC4KCD3DX47B46IL6646H*6Z/ER2DD46JH8946JPCT3E5JDLA7$Q69464W51S6..DX%DZJC2/DYOA$$E5$C JC3/D9Z95LEZED1ECW.C8WE2OA3ZAGY8MPCG/DU2DRB8MTA8+9$PC5$CUZC$$5Y$5FBBC30.9V$*J7TUO*9T$MJ5FT9U:HMD$EUJD:IG/64QL452M4KJH8S7$9N:655W*:OY6M+M3GH0N4F9YN-W91UHUO8BBJH64H8N/5LN%05P0-KG87ONE58+M%N8FJJT9S+K65WRNN2D AM/H*UQ6SEBG71Q1R5H68KTL2R*FDP5DBGWEM18J45MS$5O.296HOMB9 CTTOZN8:MB95ITP045OUQ41AAB1K19W5L84%G%9H5Z57LRP$IH$K0PLLFCCNE8QG%8LRWN$1P$IA+3I:.3$JB:DUK%DQYIO5GRJF*$G*EMHV3RMLHISWW8FJIHGQ*:BV7N+CA:VVXOILXHDUJQL9LTNQ1THAB$EAGYU07V 89NNLA$NS7F8ENV:COAAJ+F-2NK+P-3
Also the Italian ones have the same issue:
HC1:6BFOXN%TS3DH0YOJ58S S-W5HDC *M0II5XHC9B5G2+$N IOP-IA%NFQGRJPC%OQHIZC4.OI1RM8ZA.A5:S9MKN4NN3F85QNCY0O%0VZ001HOC9JU0D0HT0HB2PL/IB*09B9LW4T*8+DCMH0LDK2%K:XFE70*LP$V25$0Q:J:4MO1P0%0L0HD+9E/HY+4J6TH48S%4K.GJ2PT3QY:GQ3TE2I+-CPHN6D7LLK*2HG%89UV-0LZ 2ZJJ524-LH/CJTK96L6SR9MU9DHGZ%P WUQRENS431T1XCNCF+47AY0-IFO0500TGPN8F5G.41Q2E4T8ALW.INSV$ 07UV5SR+BNQHNML7 /KD3TU 4V*CAT3ZGLQMI/XI%ZJNSBBXK2:UG%UJMI:TU+MMPZ5$/PMX19UE:-PSR3/$NU44CBE6DQ3D7B0FBOFX0DV2DGMB$YPF62I$60/F$Z2I6IFX21XNI-LM%3/DF/U6Z9FEOJVRLVW6K$UG+BKK57:1+D10%4K83F+1VWD1NE
What we can do to validate those signatures?
Thanks in advance
Hi @herald-si,
For these inputs, the key ID is not in the unprotected header, but the protected one. The Italian certificate can be verified with the code I wrote with the following modification
//Instead of
$keyId = base64_encode($unprotectedHeader->getNormalizedData()[4]);
//Write this
$keyId = base64_encode(($unprotectedHeader->getNormalizedData() + $protectedHeader->getNormalizedData())[4]);
Same for the Swiss one. The problem with that input is that the signature algorithm is -37
i.e. PS256 which is not supported in my example.
Hi @clariion,
Yes you can convert back the $cbor
variable into a valid certificate, unless you change one bit or more.
The initial post contains a useful link to that encoding process.
No it won't. The data is digitally signed and the signature verification will fail.
Hi @herald-si,
For these inputs, the key ID is not in the unprotected header, but the protected one. The Italian certificate can be verified with the code I wrote with the following modification
//Instead of $keyId = base64_encode($unprotectedHeader->getNormalizedData()[4]); //Write this $keyId = base64_encode(($unprotectedHeader->getNormalizedData() + $protectedHeader->getNormalizedData())[4]);
Same for the Swiss one. The problem with that input is that the signature algorithm is
-37
i.e. PS256 which is not supported in my example.
Hi @Spomky !
with this change everything works correctly. Thank you so much
Hi when I use thus example: 1.json: DGC with vaccination entry (1 dose)
https://github.com/eu-digital-green-certificates/dgc-testdata/tree/main/SK
https://github.com/eu-digital-green-certificates/dgc-testdata/blob/main/SK/2DCode/raw/1.json
"PREFIX": "HC1:NCFOXNEG2NBJ5*H:QO-.O9B3QZ8Y*M9WL7LG4/8+W4VGAXOE4+4J59BZ6%-OD 4YQFPF6R:5SVBWVBDKBYLDR4DF4D$ZJ*DJWP42W5J3U4OG7.R7%NC.UPTUD*Q9RK7RMEN4CD1B+K8AV2PTO*N--T0SFXZQ H9RQGX-FO2WYZQ2J95J02O8..V$T7%$D4J8$T7T$7YNGHM4PRAAUICO1DV59UE6Q1M650 LHZA0D9E2LBHHGKLO-K%FGLIA5D8MJKQJK JMDJL9GG.IA.C8KRDL4O54O4IGUJKJGI.IAHLCV5GVWNZIKXGG JMLII7EDTG91PC3DE0OARH9W/IO6AHCRTWA4EQN95N14Z+HP+POC1.AO5PIZ.VTZOSV0I+QWZJHN1ZBQR*MTNK EM5MGPI5A-M8F7AJOZNV9JHKIJYE9*FJ+UVAZ8-.A2*CEHJ5$0O/A%4SL/IG%8R.9Z6TG0MW%8N*48-930J7*4E%2L+9N2LY2Q%%2G0M172ZUJYBW897MJM5DB0J4XETW8PX+KN4K.-V:3WROR$04.7E93Q6VUE$TO%R$:3HUCQZ6D8OG2B:%A6-I8PJ8%VKYOU1Q96E01MRKUU2G730F%2H2",
I have error: Argument 1 passed to CBOR\StringStream::__construct() must be of the type string, boolean given, called in
Do you know why $decompressed = zlib_decode($decoded);
return false?
I tried the HC1 string (without "PREFIX" ) with the library and works correctly but throws a 'Public key not found'.
Which version of the code did you use?
yes, it works, try this code:
<?php
declare(strict_types=1);
use CBOR\ByteStringObject;
use CBOR\CBORObject;
use CBOR\Decoder;
use CBOR\ListObject;
use CBOR\OtherObject\OtherObjectManager;
use CBOR\StringStream;
use CBOR\Tag\TagObjectManager;
use CBOR\TagObject as Base;
use CBOR\TextStringObject;
use Mhauri\Base45;
require_once 'vendor/autoload.php';
$data = 'HC1:NCFOXNEG2NBJ5*H:QO-.O9B3QZ8Y*M9WL7LG4/8+W4VGAXOE4+4J59BZ6%-OD 4YQFPF6R:5SVBWVBDKBYLDR4DF4D$ZJ*DJWP42W5J3U4OG7.R7%NC.UPTUD*Q9RK7RMEN4CD1B+K8AV2PTO*N--T0SFXZQ H9RQGX-FO2WYZQ2J95J02O8..V$T7%$D4J8$T7T$7YNGHM4PRAAUICO1DV59UE6Q1M650 LHZA0D9E2LBHHGKLO-K%FGLIA5D8MJKQJK JMDJL9GG.IA.C8KRDL4O54O4IGUJKJGI.IAHLCV5GVWNZIKXGG JMLII7EDTG91PC3DE0OARH9W/IO6AHCRTWA4EQN95N14Z+HP+POC1.AO5PIZ.VTZOSV0I+QWZJHN1ZBQR*MTNK EM5MGPI5A-M8F7AJOZNV9JHKIJYE9*FJ+UVAZ8-.A2*CEHJ5$0O/A%4SL/IG%8R.9Z6TG0MW%8N*48-930J7*4E%2L+9N2LY2Q%%2G0M172ZUJYBW897MJM5DB0J4XETW8PX+KN4K.-V:3WROR$04.7E93Q6VUE$TO%R$:3HUCQZ6D8OG2B:%A6-I8PJ8%VKYOU1Q96E01MRKUU2G730F%2H2';
$base45Processor = new Base45();
$decoded = $base45Processor->decode(mb_substr($data, 4)); // We remove the HC1: prefix
$decompressed = zlib_decode($decoded); // We unzip the data
$stream = new StringStream($decompressed);
final class CoseSign1Tag extends Base // Specific tag for the example
{
public static function getTagId(): int
{
return 18;
}
public static function createFromLoadedData(int $additionalInformation, ?string $data, CBORObject $object): Base
{
return new self($additionalInformation, $data, $object);
}
public function getNormalizedData(bool $ignoreTags = false)
{
return $this->getValue()->getNormalizedData($ignoreTags);
}
}
$tagObjectManager = new TagObjectManager();
$tagObjectManager->add(CoseSign1Tag::class);
$cborDecoder = new Decoder($tagObjectManager, new OtherObjectManager());
$cbor = $cborDecoder->decode($stream); //We decode the data
if (!$cbor instanceof CoseSign1Tag) {
throw new InvalidArgumentException('Not a valid certificate. Not a CoseSign1 type.');
}
$list = $cbor->getValue();
if (!$list instanceof ListObject) {
throw new InvalidArgumentException('Not a valid certificate. No list.');
}
if (4 !== $list->count()) {
throw new InvalidArgumentException('Not a valid certificate. The list size is not correct.');
}
dump("list", $list);
$firstItem = $list->get(0); // The first item corresponds to the protected header
$headerStream = new StringStream($firstItem->getValue()); // The first item is also a CBOR encoded byte string
$protectedHeader = $cborDecoder->decode($headerStream);
dump('Protected header', $protectedHeader->getNormalizedData()); // The array [1 => "-7"] = ["alg" => "ES256"]
$secondItem = $list->get(1); // The second item corresponds to unprotected header
$unprotectedHeader = $secondItem;
dump('Unprotected header', $unprotectedHeader->getNormalizedData()); // The index 4 refers to the 'kid' (key ID) parameter (see https://www.iana.org/assignments/cose/cose.xhtml)
//Write this
$keyId = base64_encode(($unprotectedHeader->getNormalizedData() + $protectedHeader->getNormalizedData())[4]);
$thirdItem = $list->get(2); // The third item corresponds to the data we want to load
if (!$thirdItem instanceof ByteStringObject) {
throw new InvalidArgumentException('Not a valid certificate. The payload is not a byte string.');
}
$infoStream = new StringStream($thirdItem->getValue()); // The third item is a CBOR encoded byte string
$payload = $cborDecoder->decode($infoStream);
dump('The payload', $payload->getNormalizedData()); // The data we are looking for
$fourthItem = $list->get(3); // The fourth item is the signature.
// It can be verified using the protected header (first item) and the data (third item)
// And the public key
if (!$fourthItem instanceof ByteStringObject) {
throw new InvalidArgumentException('Not a valid certificate. The signature is not a byte string.');
}
$signature = $fourthItem;
dump('Digital signature', $signature->getNormalizedData()); // The digital signature
//We retrieve the public keys
$ch = curl_init('https://raw.githubusercontent.com/Digitaler-Impfnachweis/covpass-ios/v1.9.0/Certificates/PROD/CA/dsc.json');
$fp = fopen('dsc.json', 'wb');
curl_setopt($ch, CURLOPT_FILE, $fp);
curl_setopt($ch, CURLOPT_HEADER, 0);
curl_exec($ch);
if (curl_error($ch)) {
fwrite($fp, curl_error($ch));
}
curl_close($ch);
fclose($fp);
//We decode the JSON object we received
$certificates = json_decode(file_get_contents('dsc.json'), true, 512, JSON_THROW_ON_ERROR);
//We filter the keyset using the country and the key ID from the data
$country = $payload->getNormalizedData()[1];
$countryCertificates = array_filter($certificates['certificates'], static function (array $data) use ($country, $keyId): bool {
return $data['country'] === $country && $data['kid'] === $keyId;
});
//If no public key is found, we cannot continue
if (1 !== count($countryCertificates)) {
throw new \InvalidArgumentException('Public key not found');
}
//We convert the raw data into a PEM encoded certificate
$signingCertificate = current($countryCertificates);
$pem = chunk_split($signingCertificate['rawData'], 64, PHP_EOL);
$pem = '-----BEGIN CERTIFICATE-----'.PHP_EOL.$pem.'-----END CERTIFICATE-----'.PHP_EOL;
//Needed to convert the digital signature into an ASN.1 signature (this format is required by OpenSSL)
final class ECSignature
{
private const ASN1_SEQUENCE = '30';
private const ASN1_INTEGER = '02';
private const ASN1_MAX_SINGLE_BYTE = 128;
private const ASN1_LENGTH_2BYTES = '81';
private const ASN1_BIG_INTEGER_LIMIT = '7f';
private const ASN1_NEGATIVE_INTEGER = '00';
private const BYTE_SIZE = 2;
/**
* @throws InvalidArgumentException if the length of the signature is invalid
*/
public static function toAsn1(string $signature, int $length): string
{
$signature = bin2hex($signature);
if (self::octetLength($signature) !== $length) {
throw new InvalidArgumentException('Invalid signature length.');
}
$pointR = self::preparePositiveInteger(mb_substr($signature, 0, $length, '8bit'));
$pointS = self::preparePositiveInteger(mb_substr($signature, $length, null, '8bit'));
$lengthR = self::octetLength($pointR);
$lengthS = self::octetLength($pointS);
$totalLength = $lengthR + $lengthS + self::BYTE_SIZE + self::BYTE_SIZE;
$lengthPrefix = $totalLength > self::ASN1_MAX_SINGLE_BYTE ? self::ASN1_LENGTH_2BYTES : '';
$bin = hex2bin(
self::ASN1_SEQUENCE
.$lengthPrefix.dechex($totalLength)
.self::ASN1_INTEGER.dechex($lengthR).$pointR
.self::ASN1_INTEGER.dechex($lengthS).$pointS
);
if (!is_string($bin)) {
throw new InvalidArgumentException('Unable to parse the data');
}
return $bin;
}
private static function octetLength(string $data): int
{
return (int) (mb_strlen($data, '8bit') / self::BYTE_SIZE);
}
private static function preparePositiveInteger(string $data): string
{
if (mb_substr($data, 0, self::BYTE_SIZE, '8bit') > self::ASN1_BIG_INTEGER_LIMIT) {
return self::ASN1_NEGATIVE_INTEGER.$data;
}
while (0 === mb_strpos($data, self::ASN1_NEGATIVE_INTEGER, 0, '8bit')
&& mb_substr($data, 2, self::BYTE_SIZE, '8bit') <= self::ASN1_BIG_INTEGER_LIMIT) {
$data = mb_substr($data, 2, null, '8bit');
}
return $data;
}
}
//The object is the data that should have been signed
$structure = new ListObject();
$structure->add(new TextStringObject('Signature1'));
$structure->add($firstItem);
$structure->add(new ByteStringObject(''));
$structure->add($thirdItem);
//COnverted signature
$derSignature = ECSignature::toAsn1($signature->getNormalizedData(), 64);
//We verify the signature with the data structure and the PEM encoded key
// If valid, the result is 1
$isValid = 1 === openssl_verify((string) $structure, $derSignature, $pem, 'sha256');
if (!$isValid) {
while ($m = openssl_error_string()) {
dump("openssl",$m);
}
throw new \InvalidArgumentException('The signature is NOT valid');
}
// At this point, we have the data and the signature is verified.
function dump($title, $list)
{
echo "<h1>$title</h1><pre>" . print_r($list, true) . "</pre>";
}
?>
Thx, It work
And https://raw.githubusercontent.com/Digitaler-Impfnachweis/covpass-ios/v1.9.0/Certificates/PROD/CA/dsc.json
this is always actual certificates? or it is only for example?
what about this sources?
https://pkg.go.dev/github.com/stapelberg/coronaqr/trustlist/trustlistmirror
https://eudcc.tibordp.workers.dev/trust-list/prod
or ... _``
This is just one example. Each country has its own list with a different format: DE: https://de.dscg.ubirch.com/trustList/DSC/ IT: https://get.dgc.gov.it/v1/dgc/signercertificate/status - https://get.dgc.gov.it/v1/dgc/signercertificate/update SE: https://dgcg.covidbevis.se/tp/ NL: https://verifier-api.coronacheck.nl/v4/verifier/public_keys ..... they synchronize certificates with other countries on a daily basis. Moreover, each nation has different business rules for validating the data contained in the dgc.
Here: https://github.com/herald-si/verificac19-sdk-php there is a working example of dcg verification according to italian rules. It is based on the cbor library, the code https://github.com/Spomky-Labs/cbor-php/issues/32#issuecomment-929561510. once the signature has been validated, it extracts the data in a dedicated php object
This issue has been automatically marked as stale because it has not had recent activity. It will be closed if no further activity occurs. Thank you for your contributions.
Hello @Spomky,
since 2021 there were several updates. I tried replicating the code posted. I think because of the changes in the project structure I cannot use the code. The TagObjectManager
does not exist anymore. I tried changing it to the existing TagManager
which throws further issues with the list items and the ->getValue()
functions and connected with this the ->getNormalizedData
-function.
As I could not find the changed parts in the documentation or in the code I would like to ask for further information on significant changes in the CBOR-Code structure regarding the reading of the CBOR-data.
Kindly
This thread has been automatically locked since there has not been any recent activity after it was closed. Please open a new issue for related bugs.
Is it possible to decode the german covid impfpass? I tried it with python like here: https://hackernoon.com/how-to-decode-your-own-eu-vaccination-green-pass-with-a-few-lines-of-python-9v2c37s1
Works without any problems. With PHP i cant get it to work with this class. Maybe i`m to stupid.