herald-si / verificac19-sdk-php

PHP Digital Green Certificate SDK
Apache License 2.0
13 stars 6 forks source link

Help needed - New Zealand COVID Pass #3

Closed sennz closed 2 years ago

sennz commented 2 years ago

Hi Thanks for the SDK, I try to use your code to NZ verification, but i am getting "Not a valid certificate. Not a CoseSign1 type" error. Details are below https://nzcp.covid19.health.nz/#example-resolving-an-issuers-identifier-to-their-public-keys I have decoded the string to hex, next step fails "Not a valid certificate. Not a CoseSign1 type" What am I doing wrong? Thanks

DevPGS commented 2 years ago

Hi! sorry but the sdk only works with European Digital Certificate specification. The NZ cert starts with a different string scheme (NZCP:/ instead of HC1:), uses a different base (base32 instead of base45) and has a different payload structure. You can check your CoseSign1 error with the CBOR library (here an example https://github.com/Spomky-Labs/cbor-php/issues/32).

DevPGS commented 2 years ago

i did some check with the NZ specs. the problem was that the NZ certificate is not compressed, if you use that code it works and extract the data (I tested the first qrcode on the detail page):

composer require christian-riesen/base32
composer require spomky-labs/cbor-php
<?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 Base32\Base32;

require_once 'vendor/autoload.php';

$data = 'NZCP:/1/2KCEVIQEIVVWK6JNGEASNICZAEP2KALYDZSGSZB2O5SWEOTOPJRXALTDN53GSZBRHEXGQZLBNR2GQLTOPICRUYMBTIFAIGTUKBAAUYTWMOSGQQDDN5XHIZLYOSBHQJTIOR2HA4Z2F4XXO53XFZ3TGLTPOJTS6MRQGE4C6Y3SMVSGK3TUNFQWY4ZPOYYXQKTIOR2HA4Z2F4XW46TDOAXGG33WNFSDCOJONBSWC3DUNAXG46RPMNXW45DFPB2HGL3WGFTXMZLSONUW63TFGEXDALRQMR2HS4DFQJ2FMZLSNFTGSYLCNRSUG4TFMRSW45DJMFWG6UDVMJWGSY2DN53GSZCQMFZXG4LDOJSWIZLOORUWC3CTOVRGUZLDOSRWSZ3JOZSW4TTBNVSWISTBMNVWUZTBNVUWY6KOMFWWKZ2TOBQXE4TPO5RWI33CNIYTSNRQFUYDILJRGYDVAYFE6VGU4MCDGK7DHLLYWHVPUS2YIDJOA6Y524TD3AZRM263WTY2BE4DPKIF27WKF3UDNNVSVWRDYIYVJ65IRJJJ6Z25M2DO4YZLBHWFQGVQR5ZLIWEQJOZTS3IQ7JTNCFDX';

$decoded = Base32::decode(mb_substr($data, 8)); // We remove the NZCP: prefix and the version /1/

$stream = new StringStream($decoded);

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

function dump($title, $list)
{
    echo "<h1>$title</h1><pre>" . print_r($list, true) . "</pre>";
}
?>
sennz commented 2 years ago

Thank you for the quick update.

gdhnz commented 2 years ago

I know this is closed but @sennz, have you worked out how to verify the signature?

sennz commented 2 years ago

I know this is closed but @sennz, have you worked out how to verify the signature?

Not yet, I couldn't verify the digital signature with the public key.

gdhnz commented 2 years ago

I know this is closed but @sennz, have you worked out how to verify the signature?

Not yet, I couldn't verify the digital signature with the public key.

All the methods I've seen require a PEM but the libraries I found only support converting RSA public keys from JWK to PEM.

sennz commented 2 years ago

I know this is closed but @sennz, have you worked out how to verify the signature?

Not yet, I couldn't verify the digital signature with the public key.

All the methods I've seen require a PEM but the libraries I found only support converting RSA public keys from JWK to PEM.

Yes, I found a library, but I didn't have time to check https://web-token.spomky-labs.com/