Skip to content

Dutch greenpass special format

jumpjack edited this page Nov 10, 2021 · 7 revisions

Dutch greenpasses from Holland are coded differently than any other EU greenpass, hence this repository is not able (yet) to decode them.

Main reason is the different BASE45 encoding of the QRcode data: if you read the QRcode using a standard QR reader,you get a string which starts by "NL2:" rather than "HC1:".

I found these algorithms to decode such string:

Python

def base45decode_nl(s: str) -> bytes:
     base45_nl_charset = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ $%*+-./:"
     s_len = len(s)
     res = 0
     for i, c in enumerate(s):
         f = base45_nl_charset.index(c)
         w = 45 ** (s_len - i - 1)
         res += f * w
     return res.to_bytes((res.bit_length() + 7) // 8, byteorder='big') 

C#

public static class DutchCoronaCheckBase45Utils
{
    const int BaseSize = 45;

    static readonly char[] Base45Digits =
    {
        '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'A', 'B', 'C',
        'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M', 'N', 'O', 'P',
        'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', 'Y', 'Z', ' ', '$', '%',
        '*', '+', '-', '.', '/', ':'
    };

    public static byte[] Decode(string inputString)
    {
        var stringLength = inputString.Length;

        BigInteger result = 0;
        foreach (var item in inputString.Select((ch, index) => new { index, ch }))
        {
            var location = Array.FindIndex(Base45Digits, d => d == item.ch);
            var value = BigInteger.Pow(BaseSize, stringLength - item.index - 1);

            result += location * value;
        }

        return result.ToBigEndianByteArray();
    }

    public static string Encode(byte[] srcBytes)
    {
        var source = srcBytes.ToBigEndianInteger();

        int log = (int)BigInteger.Log(source, BaseSize);
        var stringBuilder = new StringBuilder(log + 1);

        while (log >= 0)
        {
            var location = BigInteger.DivRem(source, BigInteger.Pow(BaseSize, log), out BigInteger remainder);
            stringBuilder.Append(Base45Digits[(int)location]);

            source = remainder;
            log--;
        }

        return stringBuilder.ToString();
    }
}

}

Source: https://github.com/StefH/Dutch-CoronaCheck-QR-Code-Decoder-and-Verifier/blob/main/src/DutchCoronaCheckUtils/DutchCoronaCheckBase45Utils.cs

PHP

function bctobytestring($dec) {
    $last = bcmod($dec, 256);
    $remain = bcdiv(bcsub($dec, $last), 256);

    if($remain == 0) {
        return chr($last);
    } else {
        $t = bctobytestring($remain).chr($last);
        return $t;
    }
}

function base45nl_decode($inp) {
    $base45_nl_charset = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ $%*+-./:";
    $num = "0";
    for ($i = 0; $i < strlen($inp); $i++) {
        $position = strpos($base45_nl_charset, $inp[$i]);
        $num = bcmul($num,45);
        $num = bcadd($num,$position);
    }
    $res = bctobytestring($num);
    return $res;
}

            $b458_data = str_replace("NL2:","",$data);
            $asn1_data = base45nl_decode($b458_data);
            $offset = 0;
//            var_dump(bin2hex($asn1_data));
            $asn1_object = new FG\ASN1\UnknownConstructedObject($asn1_data,$offset);
//            var_dump($asn1_object);
            $signature = bctobytestring($asn1_object->getChildren()[1]->getContent());

            $certdata = [];
            $certdata["type"]="NL";

            $certdata["header"]=[];
            $certdata["payload"]=[];
            $attrs = ['metaData',
                'isSpecimen',
                'isPaperProof',
                'validFrom',
                'validForHours',
                'firstNameInitial',
                'lastNameInitial',
                'birthDay',
                'birthMonth'];
            $attr_data = $asn1_object->getChildren()[6]->getContent();
//            var_dump(bctobytestring($attr_data[0]));
//            $signerdata = $asn1_header_data->getChildren();
//            $certdata["header"]["version"]=$signerdata[1]->getContent();
//            $certdata["header"]["kid"]=$signerdata[0]->getContent();
            for ($a=1; $a<count($attr_data);$a++) {
                $rawval = $attr_data[$a]->getContent();
                if ((intval($rawval) & 0x01) == 0x01) {
                    $bytes = pack("L", intval($rawval) >> 1);
                    $bitsize = (strlen(dechex(intval($rawval) >> 1))*4);
                    $bytes = substr($bytes,0,floor(($bitsize + 7)/8));
//                    var_dump(floor(($bitsize + 7)/8));
//                    var_dump($bytes);
                    $certdata["payload"][$attrs[$a]]=implode(array_map("chr", $bytes));
                } else {
                    $certdata["payload"][$attrs[$a]]=null;
                }
                $certdata["payload"][$attrs[$a]]=intval($rawval)>>1;
            }

            $certdata["signature"]=[];
            $certdata["signature"]["value"]=bin2hex($signature);
            $certdata["signature"]["blocked"]=getNLCertBlocked($signature);

            $res = $certdata;

Source: https://github.com/breenstorm/certdata/blob/5614c0e6b3911807f61591cdcf8f4472621baccd/index.php

GO

Source: https://github.com/minvws/nl-covid19-coronacheck-idemix/blob/4c625928010541d6669ccf2b39f2fd1609b36c48/holder/disclosure.go


References for dutch greenpass:


Dutch/Holland greenpass structure:

Clone this wiki locally