Covid Zertifikate validieren for Fun and .. Sicherheit?
Permalink for this talk:
http://domm.plix.at/talks/2022_gpw_qrona
Corona
You might have noticed this small virus in the last two years
The main reason you're watching me on a screen...
After the second wave, when vaccinations where beginning to be available, the European Union started to think about a way to implement digital vaccination certificates
In May 2021 the European Parliament and the Council agreed on the EU Digital COVID Certificate
Which surprisingly doesn't suck!
EU Digital COVID Certificate
"The EU Digital COVID Certificate contains a QR code with a digital signature to protect it against falsification."
"When the certificate is checked, the QR code is scanned and the signature verified."
"Each issuing body (e.g. a hospital, a test centre, a health authority) has its own digital signature key. All of these are stored in a secure database in each country."
"The European Commission has built a gateway through which all certificate signatures can be verified across the EU. The personal data of the certificate holder does not pass through the gateway, as this is not necessary to verify the digital signature."
decentral validation
no DB-lookup, no tracking
implemented using existing open standards
lots of documentation
EU++
Decoding COVID certificates for fun and .. security?
A few months ago (between some waves...) I wanted to do a birthday party (which in the end did not happen for other reasons)
For that party, I wanted people to adhere to "2G+":
(recovered or fully vaccinated) and PCR-tested
But: The various "Green Pass" Apps provided by state actors where ugly and/or did not work on my laptop (only on mobile phones)
So I set out to write my own
But I rembered a rather long chat on #Austria.pm started by Maroš:
2021-11-02 09:57:39 maros hat wer erfahrungen mit cbor/cose/cwt? versuche grad so ein digitales covid cert in perl zu validieren und scheitere (mangels cpan cwt library) mit meiner naiven cwt interpretation am überprüfen der signatur
And (being lazy) asked:
2022-02-18 15:20:17 domm maros: Hast du den Corona-Zertifikat-QR-Scanner/Validator jemals fertig bekommen? 2022-02-18 15:39:43 maros yup. ist bei gh auch "produktiv" im einsatz
(See the following talk by Maroš himself on "Werda - der Geizhals Anwesenheitsmonitor")
Maroš was so kind to share his code with me, which I used to build my own Corona QR Code Scanner & Verifyer
Decode & Validate
QR-Code
QR-Code is just a fancy way to represent a piece of text
~$ zbarimg testcert_gurgel.png
~$ zbarimg testcert_gurgel.png QR-Code:HC1:NCFOXN%TS3DHDWKJ/8 1K10K.0ICID:D4 W2%CMRY4/7OMOO**IJ04Z9HPJPC%OQHIZC4.OI:OIC*I80P2W4V Z0G.8O%0VON0-CI 0 +AE3PCY0JCAEV4R83/70K%4E+4 $4LHF2B5:0IG94/Y4WW25-74GD$B9ZHHQDFGNN4:H1D7.+O8OTW$ ...
~$ zbarimg testcert_gurgel.png QR-Code:HC1:NCFOXN%TS3DHDWKJ/8 1K10K.0ICID:D4 W2%CMRY4/7OMOO**IJ04Z9HPJPC%OQHIZC4.OI:OIC*I80P2W4V Z0G.8O%0VON0-CI 0 +AE3PCY0JCAEV4R83/70K%4E+4 $4LHF2B5:0IG94/Y4WW25-74GD$B9ZHHQDFGNN4:H1D7.+O8OTW$ ...
Added by zbarimg
, please ignore
~$ zbarimg testcert_gurgel.png QR-Code:HC1:NCFOXN%TS3DHDWKJ/8 1K10K.0ICID:D4 W2%CMRY4/7OMOO**IJ04Z9HPJPC%OQHIZC4.OI:OIC*I80P2W4V Z0G.8O%0VON0-CI 0 +AE3PCY0JCAEV4R83/70K%4E+4 $4LHF2B5:0IG94/Y4WW25-74GD$B9ZHHQDFGNN4:H1D7.+O8OTW$ ...
Some sort of QR-Code header, also irrelevant
NCFOXN%TS3DHDWKJ/8 1K10K.0ICID:D4 W2%CMRY4/7OMOO**IJ04Z9HPJPC%OQHIZC4.OI:OIC*I80P2W4V Z0G.8O%0VON0-CI 0 +AE3PCY0JCAEV4R83/70K%4E+4 $4LHF2B5:0IG94/Y4WW25-74GD$B9ZHHQDFGNN4: ...
Nice, but that's not telling me lot...
So let's pass this string on to Maroš' script
Disclaimer: I removed handling of some corner cases and error handling from Maroš code for this talk!
use QRCode::Base45 qw(decode_base45); my $cert = $ARGV[0]; $cert = decode_base45($cert);
use QRCode::Base45 qw(decode_base45); my $cert = $ARGV[0]; $cert = decode_base45($cert);
use QRCode::Base45 qw(decode_base45); my $cert = $ARGV[0]; $cert = decode_base45($cert);
xڻ�⻈ţ�Fꞅ�5��j "��H%��>�&�d��1�1Ē�y
Some binary data...
that might be compressed, so we uncompress it
use Compress::Zlib qw(uncompress); $cert = uncompress($cert) if ord( bytes::substr $cert, 0, 1 ) == 120;
use Compress::Zlib qw(uncompress); $cert = uncompress($cert) if ord( bytes::substr $cert, 0, 1 ) == 120;
use Compress::Zlib qw(uncompress); $cert = uncompress($cert) if ord( bytes::substr $cert, 0, 1 ) == 120;
M�H��e��I|A&�Y �▒bG��▒b?^�bAT9��at��bsct2022-03inistry of H
Still binary, but now we can start to make out human readable text
M�H��e��I|A&�Y �▒bG��▒b?^�bAT9��at��bsct2022-03inistry of H
It's a CBOR!
CBOR
"The Concise Binary Object Representation (CBOR) is a data format whose design goals include the possibility of extremely small code size, fairly small message size, and extensibility without the need for version negotiation."
https://www.rfc-editor.org/rfc/rfc8949.html
JSON Web Tokens (JWT) on steroids
A very complex way to encode some data structure
use CBOR::XS; my $cbor = CBOR::XS->new; my $decoded = $cbor->decode($cert);
use CBOR::XS; my $cbor = CBOR::XS->new; my $decoded = $cbor->decode($cert);
use CBOR::XS; my $cbor = CBOR::XS->new; my $decoded = $cbor->decode($cert);
$decoded
now looks like this:
bless([ 18, [ '�H��e��I|A&', {}, '�▒bG��▒b?^�bAT9��at��bsct2022-03inistry of Health, A...', 'G��^fI�G�����G��J����M4���(�yZ&���\'��5�q...' ] ]), 'CBOR::XS::Tagged'
bless([ 18, [ '�H��e��I|A&', {}, '�▒bG��▒b?^�bAT9��at��bsct2022-03inistry of Health, A...', 'G��^fI�G�����G��J����M4���(�yZ&���\'��5�q...' ] ]), 'CBOR::XS::Tagged'
my ($pheader_cbor, $uheader, $payload_cbor, $signature) = $decoded->value->@*; my $payload = $cbor->decode($payload_cbor);
my ($pheader_cbor, $uheader, $payload_cbor, $signature) = $decoded->value->@*; my $payload = $cbor->decode($payload_cbor);
my ($pheader_cbor, $uheader, $payload_cbor, $signature) = $decoded->value->@*; my $payload = $cbor->decode($payload_cbor);
my ($pheader_cbor, $uheader, $payload_cbor, $signature) = $decoded->value->@*; my $payload = $cbor->decode($payload_cbor);
{ '-260' => { '1' => { 'dob' => '1976-13-32', 'nam' => { 'fn' => 'Klausner', 'fnt' => 'KLAUSNER', 'gn' => 'Thomas', 'gnt' => 'THOMAS' }, 't' => [ { 'ci' => 'URN:UVCI:01:AT:BD9DWLTL4451CCIF6JJ700CKB#/', 'co' => 'AT', 'is' => 'Ministry of Health, Austria', 'nm' => 'NAAT', 'sc' => '2022-03-26T06:23:00Z', 'tc' => 'Lifebrain - Alles gurgelt', 'tg' => '840539006', 'tr' => '260415000', 'tt' => 'LP6464-4' } ], 'ver' => '1.3.0' } }, '1' => 'AT', '4' => 1648880580, '6' => 1648320162 }
Now we're getting at some usable data
in some weird data structure
{ '-260' => { '1' => { 'nam' => { 'fn' => 'Klausner', 'gn' => 'Thomas', },
COMMISSION IMPLEMENTING DECISION (EU) 2021/1073
ANNEX I 3.2.1. CWT Structure Overview
https://eur-lex.europa.eu/legal-content/EN/TXT/HTML/?uri=CELEX:32021D1073&from=EN
A spec that's also a law :-)
Again thanks to Maroš for digging through all those specs...
my ($pheader_cbor, $uheader, $payload_cbor, $signature) = $decoded->value->@*; my $payload = $cbor->decode($payload_cbor); my $data = $payload->{-260}{1};
$data
contains some obvious data like dob
, nam
and ver
And, depending on the type of the certificate
r .. recovered t .. test v .. vaccination
In this example, we're looking at a test:
't' => [ { 'ci' => 'URN:UVCI:01:AT:BD9DWLTL4451CCIF6JJ700CKB#/', 'co' => 'AT', 'is' => 'Ministry of Health, Austria', 'nm' => 'NAAT', 'sc' => '2022-03-26T06:23:00Z', 'tc' => 'Lifebrain - Alles gurgelt', 'tg' => '840539006', 'tr' => '260415000', 'tt' => 'LP6464-4' } ],
't' => [ { 'ci' => 'URN:UVCI:01:AT:BD9DWLTL4451CCIF6JJ700CKB#/', 'co' => 'AT', 'is' => 'Ministry of Health, Austria', 'nm' => 'NAAT', 'sc' => '2022-03-26T06:23:00Z', 'tc' => 'Lifebrain - Alles gurgelt', 'tg' => '840539006', 'tr' => '260415000', 'tt' => 'LP6464-4' } ],
't' => [ { 'ci' => 'URN:UVCI:01:AT:BD9DWLTL4451CCIF6JJ700CKB#/', 'co' => 'AT', 'is' => 'Ministry of Health, Austria', 'nm' => 'NAAT', 'sc' => '2022-03-26T06:23:00Z', 'tc' => 'Lifebrain - Alles gurgelt', 'tg' => '840539006', 'tr' => '260415000', 'tt' => 'LP6464-4' } ],
't' => [ { 'ci' => 'URN:UVCI:01:AT:BD9DWLTL4451CCIF6JJ700CKB#/', 'co' => 'AT', 'is' => 'Ministry of Health, Austria', 'nm' => 'NAAT', 'sc' => '2022-03-26T06:23:00Z', 'tc' => 'Lifebrain - Alles gurgelt', 'tg' => '840539006', 'tr' => '260415000', 'tt' => 'LP6464-4' } ],
't' => [ { 'ci' => 'URN:UVCI:01:AT:BD9DWLTL4451CCIF6JJ700CKB#/', 'co' => 'AT', 'is' => 'Ministry of Health, Austria', 'nm' => 'NAAT', 'sc' => '2022-03-26T06:23:00Z', 'tc' => 'Lifebrain - Alles gurgelt', 'tg' => '840539006', 'tr' => '260415000', # test result 'tt' => 'LP6464-4' } ],
't' => [ { 'ci' => 'URN:UVCI:01:AT:BD9DWLTL4451CCIF6JJ700CKB#/', 'co' => 'AT', 'is' => 'Ministry of Health, Austria', 'nm' => 'NAAT', 'sc' => '2022-03-26T06:23:00Z', 'tc' => 'Lifebrain - Alles gurgelt', 'tg' => '840539006', COVID-19 'tr' => '260415000', 'tt' => 'LP6464-4' } ],
Future proof!
use List::Util qw(first); use DateTime; my ($valid_days,$valid_from); foreach my $type (qw(v r t)) { # vaccinated recovered tested next unless defined $data->{$type}; my $detail = first { $_->{tg} eq '840539006' } $data->{$type}->@*; if (defined $detail) { ... } }
use List::Util qw(first); use DateTime; my ($valid_days,$valid_from); foreach my $type (qw(v r t)) { # vaccinated recovered tested next unless defined $data->{$type}; my $detail = first { $_->{tg} eq '840539006' } $data->{$type}->@*; if (defined $detail) { ... } }
go through all the possible types
use List::Util qw(first); use DateTime; my ($valid_days,$valid_from); foreach my $type (qw(v r t)) { # vaccinated recovered tested next unless defined $data->{$type}; my $detail = first { $_->{tg} eq '840539006' } $data->{$type}->@*; if (defined $detail) { ... } }
find the first one where tg eq 840539006
(i.e. target is COVID-19)
if ($type eq 't') { # tt: The type of test # nm: Test name (PCR only) # ma: Test device identifier (Antigen only) # tc: Name of the actor that conducted the test # sc: The date and time when the test sample was collected $valid_from = parse_date($detail->{sc}); if ($detail->{tr} ne '260415000') { fail('Positive test certificate! Go home!'); } if ($detail->{tt} eq 'LP6464-4') { # PCR $valid_days = 2; } elsif ($detail->{tt} eq 'LP217198-3') { # Antigen $valid_days = 1; } }
If it's a test
if ($type eq 't') { # tt: The type of test # nm: Test name (PCR only) # ma: Test device identifier (Antigen only) # tc: Name of the actor that conducted the test # sc: The date and time when the test sample was collected $valid_from = parse_date($detail->{sc}); if ($detail->{tr} ne '260415000') { fail('Positive test certificate! Go home!'); } if ($detail->{tt} eq 'LP6464-4') { # PCR $valid_days = 2; } elsif ($detail->{tt} eq 'LP217198-3') { # Antigen $valid_days = 1; } }
Get the valid_from
date from sc
("sample collected")
if ($type eq 't') { # tt: The type of test # nm: Test name (PCR only) # ma: Test device identifier (Antigen only) # tc: Name of the actor that conducted the test # sc: The date and time when the test sample was collected $valid_from = parse_date($detail->{sc}); if ($detail->{tr} ne '260415000') { fail('Positive test certificate! Go home!'); } if ($detail->{tt} eq 'LP6464-4') { # PCR $valid_days = 2; } elsif ($detail->{tt} eq 'LP217198-3') { # Antigen $valid_days = 1; } }
If the test result (tr
) is positiv (obviously encoded as "260415000"), fail.
if ($type eq 't') { # tt: The type of test # nm: Test name (PCR only) # ma: Test device identifier (Antigen only) # tc: Name of the actor that conducted the test # sc: The date and time when the test sample was collected $valid_from = parse_date($detail->{sc}); if ($detail->{tr} ne '260415000') { fail('Positive test certificate! Go home!'); } if ($detail->{tt} eq 'LP6464-4') { # PCR $valid_days = 2; } elsif ($detail->{tt} eq 'LP217198-3') { # Antigen $valid_days = 1; } }
Calc expiry date based on type of test and some hardcoded values (which should be adapted to the current rules)
} elsif ($type eq 'v') { # vp = Type of the vaccine or prophylaxis used. # mp = Medicinal product used for this specific dose of vaccination. # v = Marketing authorisation holder or manufacturer # dn = Sequence number of the dose given during this vaccination event. # sd = The overall number of doses in the series # dt = Date of vaccination $valid_days = $detail->{dn} >= 3 ? 270:180; $valid_from = parse_date($detail->{dt}); # Jansen if ($detail->{mp} eq 'EU/1/20/1525') { $valid_from = $valid_from->add( days => 22 ); } # Skip invalid - single Jansen jab isn't valid anymore if ($detail->{dn} != $detail->{sd} || $detail->{dn} == 1) { $valid_from = undef; } }
If it's a vaccination
} elsif ($type eq 'v') { # vp = Type of the vaccine or prophylaxis used. # mp = Medicinal product used for this specific dose of vaccination. # v = Marketing authorisation holder or manufacturer # dn = Sequence number of the dose given during this vaccination event. # sd = The overall number of doses in the series # dt = Date of vaccination $valid_days = $detail->{dn} >= 3 ? 270:180; $valid_from = parse_date($detail->{dt}); # Jansen if ($detail->{mp} eq 'EU/1/20/1525') { $valid_from = $valid_from->add( days => 22 ); } # Skip invalid - single Jansen jab isn't valid anymore if ($detail->{dn} != $detail->{sd} || $detail->{dn} == 1) { $valid_from = undef; } }
Again get valid_from
and valid_until
from the data, considering the different "medical products"
} elsif ($type eq 'r') { # fr: The date when a sample for the NAAT test producing a positive result was collected # df: The first date on which the certificate is considered to be valid # du: The last date on which the certificate is considered to be valid, assigned by the certificate issuer. $valid_from = parse_date($detail->{df}); $valid_days = 180; }
And if it's a recovery, do some even simpler date math.
# Check expiry fail('Certificate is not yet valid') if ! $valid_from || $valid_from->epoch > time; fail('Not a valid COVID-19 certificate') if !$valid_days; fail('Certificate is epxired') if $valid_from->add( days => $valid_days )->epoch < time;
Now we can define some fail cases...
or declare the certificate valid
return "VALID, have fun!";
um, but we have a problem here
(yes, returning a string is stupid (and not in the original code), but that's not the problem)
We now only decoded the certificate, but have not validated it!
If you reverse these steps, you could easily generate a fake certificate
COSE
Like JOSE for JWTs, there is also COSE for CBORs
CBOR Object Signing and Encryption
Concise Binary Object Representation Object Signing and Encryption
my ($pheader_cbor, $uheader, $payload_cbor, $signature) = $decoded->value->@*;
my ($pheader_cbor, $uheader, $payload_cbor, $signature) = $decoded->value->@*;
my ($pheader_cbor, $uheader, $payload_cbor, $signature) = $decoded->value->@*;
my ($pheader_cbor, $uheader, $payload_cbor, $signature) = $decoded->value->@*; my $pheader = $cbor->decode($pheader_cbor);
We get the header and decode it
{ '1' => -7, '4' => '��e��I|A' }
$pheader->{1}
defines the signing & digest algorithm
$pheader->{4}
defines the key id
my %ALGS = ( '-7' => { class => 'Crypt::PK::ECC', digest => 'SHA256' }, '-35' => { class => 'Crypt::PK::ECC', digest => 'SHA384' }, '-36' => { class => 'Crypt::PK::ECC', digest => 'SHA512' }, '-37' => { class => 'Crypt::PK::RSA', digest => 'SHA256' }, '-38' => { class => 'Crypt::PK::RSA', digest => 'SHA384' }, '-39' => { class => 'Crypt::PK::RSA', digest => 'SHA512' }, ); my $alg = $ALGS{$pheader->{1}};
my %ALGS = ( '-7' => { class => 'Crypt::PK::ECC', digest => 'SHA256' }, '-35' => { class => 'Crypt::PK::ECC', digest => 'SHA384' }, '-36' => { class => 'Crypt::PK::ECC', digest => 'SHA512' }, '-37' => { class => 'Crypt::PK::RSA', digest => 'SHA256' }, '-38' => { class => 'Crypt::PK::RSA', digest => 'SHA384' }, '-39' => { class => 'Crypt::PK::RSA', digest => 'SHA512' }, ); my $alg = $ALGS{$pheader->{1}};
Could be RSA or ECC
my %ALGS = ( '-7' => { class => 'Crypt::PK::ECC', digest => 'SHA256' }, '-35' => { class => 'Crypt::PK::ECC', digest => 'SHA384' }, '-36' => { class => 'Crypt::PK::ECC', digest => 'SHA512' }, '-37' => { class => 'Crypt::PK::RSA', digest => 'SHA256' }, '-38' => { class => 'Crypt::PK::RSA', digest => 'SHA384' }, '-39' => { class => 'Crypt::PK::RSA', digest => 'SHA512' }, ); my $alg = $ALGS{$pheader->{1}};
Could be various digests
my %ALGS = ( '-7' => { class => 'Crypt::PK::ECC', digest => 'SHA256' }, '-35' => { class => 'Crypt::PK::ECC', digest => 'SHA384' }, '-36' => { class => 'Crypt::PK::ECC', digest => 'SHA512' }, '-37' => { class => 'Crypt::PK::RSA', digest => 'SHA256' }, '-38' => { class => 'Crypt::PK::RSA', digest => 'SHA384' }, '-39' => { class => 'Crypt::PK::RSA', digest => 'SHA512' }, ); my $alg = $ALGS{$pheader->{1}};
Find the one used in this cert
my %ALGS = ( '-7' => { class => 'Crypt::PK::ECC', digest => 'SHA256' }, '-35' => { class => 'Crypt::PK::ECC', digest => 'SHA384' }, '-36' => { class => 'Crypt::PK::ECC', digest => 'SHA512' }, '-37' => { class => 'Crypt::PK::RSA', digest => 'SHA256' }, '-38' => { class => 'Crypt::PK::RSA', digest => 'SHA384' }, '-39' => { class => 'Crypt::PK::RSA', digest => 'SHA512' }, ); my $alg = $ALGS{$pheader->{1}};
As $pheader->{1}
contains -7
, we're dealing with some elliptic curves keys using SHA256
for hashing.
That was the easy part..
Now we need the public key of the signer
You can get them here via a nice API https://dgcg.covidbevis.se/tp/trust-list
that returns a signed JWT
So to decode and verify that, we need their public key:
~$ curl https://dgcg.covidbevis.se/tp/cert -o signer.crt
~$ cat signer.crt -----BEGIN CERTIFICATE----- MIIB6jCCAY+gAwIBAgIUZVUDZ9xmLgGkjoqXhMizIaqko4AwCgYIKoZIzj0EAwIw TTELMAkGA1UEBhMCU0UxHzAdBgNVBAoMFlN3ZWRpc2ggZUhlYWx0aCBBZ2VuY3kx ... b3JAZWhhbHNvbXluZGlnaGV0ZW4uc2UwCgYIKoZIzj0EAwIDSQAwRgIhAL02Iy2x if3oTSivwKg+fZDwquoTgTMJnCUJzyltYExjAiEAsT3U7icfdH3FtrZ/NZ2LlC5r sSSXWtDUTXhmclnJFnU= -----END CERTIFICATE-----
~$ curl https://dgcg.covidbevis.se/tp/trust-list eyJ4NWMiOlsiTUlJQjZqQ0NBWStnQXdJQkFnSVVaVlVEWjl4bUxnR2tqb3FYaE1pe klhcWtvNEF3Q2dZSUtvWkl6ajBFQXdJd1RURUxNQWtHQTFVRUJoTUNVMFV4SHpBZE JnTlZCQW9NRmxOM1pXUnBjMmdnWlVobFlXeDBhQ0JCWjJWdVkza3hIVEFiQmdOVkJ ..
~$ curl https://dgcg.covidbevis.se/tp/trust-list | perl -MCrypt::JWT=decode_jwt -MCpanel::JSON::XS -n -E 'say encode_json( decode_jwt( token => $_, key => Crypt::PK::ECC->new("signer.crt")))' > trust_list.json
Some old-school Perl commandline power
~$ curl https://dgcg.covidbevis.se/tp/trust-list | perl -MCrypt::JWT=decode_jwt -MCpanel::JSON::XS -n -E 'say encode_json( decode_jwt( token => $_, key => Crypt::PK::ECC->new("signer.crt")))' > trust_list.json
Load the modules we need to decode the JWT and produce a nice result
~$ curl https://dgcg.covidbevis.se/tp/trust-list | perl -MCrypt::JWT=decode_jwt -MCpanel::JSON::XS -n -E 'say encode_json( decode_jwt( token => $_, key => Crypt::PK::ECC->new("signer.crt")))' > trust_list.json
Get the content we got from the API ($_
, our token) and the signature we downloaded earlier, and use that to decode_jwt
~$ curl https://dgcg.covidbevis.se/tp/trust-list | perl -MCrypt::JWT=decode_jwt -MCpanel::JSON::XS -n -E 'say encode_json( decode_jwt( token => $_, key => Crypt::PK::ECC->new("signer.crt")))' > trust_list.json
Encode the data as json and write it into a file, trust_list.json
~$ json_pretty < trust_list.json | head -n 50 { "aud" : null, "dsc_trust_list" : { "AD" : { "eku" : { "mqWkXpNR0Rk=" : [ "1.3.6.1.4.1.1847.2021.1.1", "1.3.6.1.4.1.1847.2021.1.2", "1.3.6.1.4.1.1847.2021.1.3" ] }, "keys" : [ { "crv" : "P-256",
Remember $pheader->{4}
, which contains the key id?
{ '1' => -7, '4' => '��e��I|A' }
We need to base64
encode that
use MIME::Base64 qw(encode_base64); my $key_id = encode_base64($pheader->{4});
use MIME::Base64 qw(encode_base64); my $key_id = encode_base64($pheader->{4});
which yields
gNhlvKFJfEE=
Which we can now (hopefully) find in the trust-list
"AT" : { "eku" : {}, "keys" : [ { "crv" : "P-256", "kid" : "Is2JtrOJhik=", "kty" : "EC", "x" : "YE24qIKmdcfRWUh2TqklkfZ6nyNBpX4VHeLMxfFl8rk", "x5c" : [ "MIIB7z..9AGw==" ], "x5t#S256" : "Is2JtrOJhinpnQsaO73CXL3yZEx1jbytAn55PJ52JfU", "y" : "EPGZLtG3Jx-TmV3JJErfrSrPhRmfbSidVbTQ5nnZS-s" }, { "crv" : "P-256", "kid" : "gNhlvKFJfEE=", "kty" : "EC", "x" : "RFDliGDvd_2OKXevNNyURLSR-0qv7Ajz8C6yTLXJ1QY", "x5c" : [ "MIIB8TC..ASkBs7UJ1Jc" ], "x5t#S256" : "gNhlvKFJfEGPpoJbsNE6dP5rqaKblm6KqLtSRSluq-M", "y" : "yQqj60DlauAl4sioorffH1i6LU3pTKaMOM9ZtPWLOls" } ] },
"AT" : { "eku" : {}, "keys" : [ { "crv" : "P-256", "kid" : "Is2JtrOJhik=", "kty" : "EC", "x" : "YE24qIKmdcfRWUh2TqklkfZ6nyNBpX4VHeLMxfFl8rk", "x5c" : [ "MIIB7z..9AGw==" ], "x5t#S256" : "Is2JtrOJhinpnQsaO73CXL3yZEx1jbytAn55PJ52JfU", "y" : "EPGZLtG3Jx-TmV3JJErfrSrPhRmfbSidVbTQ5nnZS-s" }, { "crv" : "P-256", "kid" : "gNhlvKFJfEE=", "kty" : "EC", "x" : "RFDliGDvd_2OKXevNNyURLSR-0qv7Ajz8C6yTLXJ1QY", "x5c" : [ "MIIB8TC..ASkBs7UJ1Jc" ], "x5t#S256" : "gNhlvKFJfEGPpoJbsNE6dP5rqaKblm6KqLtSRSluq-M", "y" : "yQqj60DlauAl4sioorffH1i6LU3pTKaMOM9ZtPWLOls" } ] },
x5c
contains the signature for our certificate!
use Cpanel::JSON::XS qw(decode_json encode_json); use Path::Tiny qw(path); use Crypt::PK::ECC; use Crypt::PK::RSA; my $trust_list = path('trust_list.json'); my $certs = decode_json($trust_list->slurp);
use Cpanel::JSON::XS qw(decode_json encode_json); use Path::Tiny qw(path); use Crypt::PK::ECC; use Crypt::PK::RSA; my $trust_list = path('trust_list.json'); my $certs = decode_json($trust_list->slurp);
use Cpanel::JSON::XS qw(decode_json encode_json); use Path::Tiny qw(path); use Crypt::PK::ECC; use Crypt::PK::RSA; my $trust_list = path('trust_list.json'); my $certs = decode_json($trust_list->slurp);
my $signing_cert = first { $_->{kid} eq $key_id } $certs->{dsc_trust_list}{$payload->{1}}{keys}->@*; my $x509cert = $signing_cert->{x5c}[0]; my $public_key = $alg->{class}->new(\qq[-----BEGIN CERTIFICATE----- ${x509cert} -----END CERTIFICATE-----]);
$payload->{1}
contains the country code ('AT'), so we get a list of all the certs for this country
my $signing_cert = first { $_->{kid} eq $key_id } $certs->{dsc_trust_list}{$payload->{1}}{keys}->@*; my $x509cert = $signing_cert->{x5c}[0]; my $public_key = $alg->{class}->new(\qq[-----BEGIN CERTIFICATE----- ${x509cert} -----END CERTIFICATE-----]);
And get the first one where the key-id matches
my $signing_cert = first { $_->{kid} eq $key_id } $certs->{dsc_trust_list}{$payload->{1}}{keys}->@*; my $x509cert = $signing_cert->{x5c}[0]; my $public_key = $alg->{class}->new(\qq[-----BEGIN CERTIFICATE----- ${x509cert} -----END CERTIFICATE-----]);
Get the public key (from x5c
)
my $signing_cert = first { $_->{kid} eq $key_id } $certs->{dsc_trust_list}{$payload->{1}}{keys}->@*; my $x509cert = $signing_cert->{x5c}[0]; my $public_key = $alg->{class}->new(\qq[-----BEGIN CERTIFICATE----- ${x509cert} -----END CERTIFICATE-----]);
Use the crypto algorithm (Crypt::PK::ECC
) to read the public key
my %ALGS = ( '-7' => { class => 'Crypt::PK::ECC', digest => 'SHA256' }, )
remember that?
my $check_signature = $cbor->encode([ CBOR::XS::as_text('Signature1'), $pheader_cbor, CBOR::XS::as_bytes(''), $payload_cbor, ]);
Build a COSE signature
my $check_signature = $cbor->encode([ CBOR::XS::as_text('Signature1'), $pheader_cbor, CBOR::XS::as_bytes(''), $payload_cbor, ]);
using some magic params I was not motivated enough to understand :-)
if ( $public_key->verify_message_rfc7518( $signature, # from QR-code $check_signature, # calculated by us $alg->{digest} ) { # OK! } else { fail('Could not verify signature') }
We can now verify the signature
if ( $public_key->verify_message_rfc7518( $signature, # from QR-code $check_signature, # calculated by us $alg->{digest} ) { # OK! } else { fail('Could not verify signature') }
comparing the signature we got from the QR-Code with the one we generated ourself, using the correct digest (as specified in the QR-Code)
if ( $public_key->verify_message_rfc7518( $signature, # from QR-code $check_signature, # calculated by us $alg->{digest} ) { # OK! } else { fail('Could not verify signature') }
If the signatures match, we now have proof that the payload has not been tampered with
if ( $public_key->verify_message_rfc7518( $signature, # from QR-code $check_signature, # calculated by us $alg->{digest} ) { # OK! } else { fail('Could not verify signature') }
If the don't match, we fail!
covidvalid.pl
~$ perl covidvalid.pl 'NCFOXN%TS3DHDWKJ/8 1K10K.0IC...WQSI008:PG5'
~$ perl covidvalid.pl 'NCFOXN%TS3DHDWKJ/8 1K10K.0IC...WQSI008:PG5' {"type":"success","fn":"Klausner","gn":"Thomas"}
Object::Pad-ify
file: lib/CheckCovidCert.pm use 5.034; use Object::Pad; class CheckCovidCert { }
file: lib/CheckCovidCert.pm use 5.034; use Object::Pad; class CheckCovidCert { }
define a new class
use QRCode::Base45 qw(decode_base45); use Compress::Zlib qw(uncompress); use CBOR::XS (); use MIME::Base64 qw(encode_base64); use Cpanel::JSON::XS qw(decode_json encode_json); use Path::Tiny qw(path); use List::Util qw(first); use FindBin qw($Bin); use DateTime; use Crypt::PK::ECC; use Crypt::PK::RSA;
load all the CPAN modules we need
has $cert :reader :param; has $ignore_expired :reader :param = 0;
Define some attributes
has $cert :reader :param; has $ignore_expired :reader :param = 0;
$cert
will get a read-only accessor (:reader
) and can be initiated on object creation (:param
)
has $cert :reader :param; has $ignore_expired :reader :param = 0;
$ignore_expired
will also default to 0
As $cert
has no default, but is a :param
, it is required
my $c3 = CheckCovidCert->new( cert => $the_cert )
Initiate an object with a cert
my $c3 = CheckCovidCert->new( cert => $the_cert, ignore_expired => 1 )
method decode { my $cbor = CBOR::XS->new; $cert =~ s/^HC1://; chomp($cert); ... }
A nice keyword to define a method
No need for my $self = shit
(c) Ingy
method decode { my $cbor = CBOR::XS->new; $cert =~ s/^HC1://; chomp($cert); ... }
No need for an accessor inside the class, just use $cert
.
# Success return { status => 'valid', given_name => $data->{nam}{gn}, family_name => $data->{nam}{fn}, date_of_birth => $data->{dob}, reason => $reason, more_reason => $more_reason, };
I changed the return value to include a bit more data
method fail { my $reason = shift; return { status => 'invalid', reason => $reason, }; }
Another simple method
A small wrapper script
file: check.pl #!/usr/bin/perl use 5.034; use CheckCovidCert; use Data::Dumper; my $c3 = CheckCovidCert->new( cert => $ARGV[0] ); say Dumper $c3->decode;
file: check.pl #!/usr/bin/perl use 5.034; use CheckCovidCert; use Data::Dumper; my $c3 = CheckCovidCert->new( cert => $ARGV[0] ); say Dumper $c3->decode;
Load the module
file: check.pl #!/usr/bin/perl use 5.034; use CheckCovidCert; use Data::Dumper; my $c3 = CheckCovidCert->new( cert => $ARGV[0] ); say Dumper $c3->decode;
Initiate an object, passing the first command line arg as the cert
file: check.pl #!/usr/bin/perl use 5.034; use CheckCovidCert; use Data::Dumper; my $c3 = CheckCovidCert->new( cert => $ARGV[0] ); say Dumper $c3->decode;
Output the result
~# perl check.pl 'NCFOXN%TS3DH...6SH+H3OFVAWDSFUQG'
~# perl check.pl 'NCFOXN%TS3DH...6SH+H3OFVAWDSFUQG' $VAR1 = { 'status' => 'invalid', 'reason' => 'Certificate is expired' };
Plack API
file: lib/QRona.pm package QRona; use 5.034; use Plack::Builder; use Plack::App::File; use Plack::Request; use CheckCovidCert; use Cpanel::JSON::XS qw(decode_json encode_json);
file: lib/QRona.pm package QRona; use 5.034; use Plack::Builder; use Plack::App::File; use Plack::Request; use CheckCovidCert; use Cpanel::JSON::XS qw(decode_json encode_json);
Load some basic Plack modules
file: lib/QRona.pm package QRona; use 5.034; use Plack::Builder; use Plack::App::File; use Plack::Request; use CheckCovidCert; use Cpanel::JSON::XS qw(decode_json encode_json);
and our new Cert-Checker
sub run_psgi { my $self = shift; my $api_app = sub { my $env = shift; # TODO .. proper app return [200, [], 'ok']; }; return builder { mount '/' => $api_app; }; }
Reminder: A simple Plack App is just a sub that returns an array containing Status Code, Header and Body
sub run_psgi { my $self = shift; my $api_app = sub { my $env = shift; # TODO .. proper app return [200, [], 'ok']; }; return builder { mount '/' => $api_app; }; }
Here's the sub
sub run_psgi { my $self = shift; my $api_app = sub { my $env = shift; # TODO .. proper app return [200, [], 'ok']; }; return builder { mount '/' => $api_app; }; }
Get the environment (i.e. request parameters etc)
sub run_psgi { my $self = shift; my $api_app = sub { my $env = shift; # TODO .. proper app return [200, [], 'ok']; }; return builder { mount '/' => $api_app; }; }
Return the HTTP Response
sub run_psgi { my $self = shift; my $api_app = sub { my $env = shift; # TODO .. proper app return [200, [], 'ok']; }; return builder { mount '/' => $api_app; }; }
Use Plack::Builder
to mount the app at /
file: qrona.psgi #!/usr/bin/env perl use 5.034; use QRona; QRona->run_psgi;
~$ plackup -p 5123 -r qrona.psgi
~$ plackup -p 5123 -r qrona.psgi HTTP::Server::PSGI: Accepting connections at http://0:5123/
~$ plackup -p 5123 -r qrona.psgi HTTP::Server::PSGI: Accepting connections at http://0:5123/ ~$ curl http://localhost:5123/
~$ plackup -p 5123 -r qrona.psgi HTTP::Server::PSGI: Accepting connections at http://0:5123/ ~$ curl http://localhost:5123/ ok
my $api_app = sub { my $env = shift; my $req = Plack::Request->new($env); if (my $raw = $req->content) { my $payload = decode_json($raw); my $c3 = CheckCovidCert->new( cert => $payload->{qr}, ignore_expired => $payload->{ignore_expired}, ); my $res = $c3->decode; return [200, ['Content-Type'=>'application/json'],[ encode_json($res) ]]; } else { return [ 400, [], ['bad request'] ]; } };
Generate a Plack::Request
because it has a nice API then dealing with the raw $env
my $api_app = sub { my $env = shift; my $req = Plack::Request->new($env); if (my $raw = $req->content) { my $payload = decode_json($raw); my $c3 = CheckCovidCert->new( cert => $payload->{qr}, ignore_expired => $payload->{ignore_expired}, ); my $res = $c3->decode; return [200, ['Content-Type'=>'application/json'],[ encode_json($res) ]]; } else { return [ 400, [], ['bad request'] ]; } };
Get the Request Content and decode the JSON payload
my $api_app = sub { my $env = shift; my $req = Plack::Request->new($env); if (my $raw = $req->content) { my $payload = decode_json($raw); my $c3 = CheckCovidCert->new( cert => $payload->{qr}, ignore_expired => $payload->{ignore_expired}, ); my $res = $c3->decode; return [200, ['Content-Type'=>'application/json'],[ encode_json($res) ]]; } else { return [ 400, [], ['bad request'] ]; } };
Init our CheckCovidCert object with the data from the request (which will hopefully contain the QR code in $payload->{$qr}
my $api_app = sub { my $env = shift; my $req = Plack::Request->new($env); if (my $raw = $req->content) { my $payload = decode_json($raw); my $c3 = CheckCovidCert->new( cert => $payload->{qr}, ignore_expired => $payload->{ignore_expired}, ); my $res = $c3->decode; return [200, ['Content-Type'=>'application/json'],[ encode_json($res) ]]; } else { return [ 400, [], ['bad request'] ]; } };
Now run the whole decode process
my $api_app = sub { my $env = shift; my $req = Plack::Request->new($env); if (my $raw = $req->content) { my $payload = decode_json($raw); my $c3 = CheckCovidCert->new( cert => $payload->{qr}, ignore_expired => $payload->{ignore_expired}, ); my $res = $c3->decode; return [200, ['Content-Type'=>'application/json'],[ encode_json($res) ]]; } else { return [ 400, [], ['bad request'] ]; } };
And send the result back as JSON
~$ curl 'http://localhost:5123/api/qr' -X POST -H 'Content-Type: application/json' --data-raw '{"qr":"HC1:NOFOXN%TS3DHDWK....VAWDSFUQG"}'
~$ curl 'http://localhost:5123/api/qr' -X POST -H 'Content-Type: application/json' --data-raw '{"qr":"HC1:NOFOXN%TS3DHDWK....VAWDSFUQG"}' {"reason":"Certificate is expired","status":"invalid"}
~$ curl 'http://localhost:5123/api/qr' -X POST -H 'Content-Type: application/json' --data-raw '{"qr":"HC1:YESOXN%TS3DHDWK....VAWDSFUQG"}'
~$ curl 'http://localhost:5123/api/qr' -X POST -H 'Content-Type: application/json' --data-raw '{"qr":"HC1:YESOXN%TS3DHDWK....VAWDSFUQG"}' { "date_of_birth" : "1976-13-32", "family_name" : "Klausner", "given_name" : "Thomas", "more_reason" : "J07BX03, EU/1/20/1528, 3", "reason" : "Vaccination", "status" : "valid" }
my $static_app = Plack::App::File->new( root => './dist' )->to_app; return builder { enable 'CrossOrigin' => ( origins => '*', headers => [ qw(Cache-Control Depth If-Modified-Since User-Agent X-File-Name X-File-Size X-Requested-With X-Prototype-Version Cookie Content-Type Accept) ] ); return builder { mount '/' => $static_app; mount '/api' => $api_app; }; };
another Plack app, serving static files (the frontend) from dist/
using Plack::App::File
my $static_app = Plack::App::File->new( root => './dist' )->to_app; return builder { enable 'CrossOrigin' => ( origins => '*', headers => [ qw(Cache-Control Depth If-Modified-Since User-Agent X-File-Name X-File-Size X-Requested-With X-Prototype-Version Cookie Content-Type Accept) ] ); return builder { mount '/' => $static_app; mount '/api' => $api_app; }; };
CORS, OMG
my $static_app = Plack::App::File->new( root => './dist' )->to_app; return builder { enable 'CrossOrigin' => ( origins => '*', headers => [ qw(Cache-Control Depth If-Modified-Since User-Agent X-File-Name X-File-Size X-Requested-With X-Prototype-Version Cookie Content-Type Accept) ] ); return builder { mount '/' => $static_app; mount '/api' => $api_app; }; };
Mount the static app (the frontend) at /
And the API and /api
Web UI
vue.js Frontend
~$ npm install -g @vue/cli
~$ npm install -g @vue/cli ~$ vue create qrona
~$ npm install -g @vue/cli ~$ vue create qrona ... manual cleanup and fiddline
~$ npm install -g @vue/cli ~$ vue create qrona ... manual cleanup and fiddline ~$ yarn run serve
App running at: - Local: http://localhost:8080/ - Network: http://192.168.1.6:8080/ Note that the development build is not optimized. To create a production build, run yarn build.
Single File Component
file: src/App.js <template>, <script>, <css>
<css>
... div#app { height: 100%; display: flex; flex-direction: column; align-content: flex-start; } main { display: flex; flex-wrap: wrap; justify-content: center; align-content: flex-start; flex: 1 0 auto; } ...
Just some plain old CSS
... div#app { height: 100%; display: flex; flex-direction: column; align-content: flex-start; } main { display: flex; flex-wrap: wrap; justify-content: center; align-content: flex-start; flex: 1 0 auto; } ...
The flex
stuff might be interesting, but I think this talk already has enough content...
<template>
<template> <div id="app"> <main> <div id="head"> <h1>QRona</h1> Scan and validate your Corona QR Certificate </div> <div id="camera"> <qrcode-stream :camera="camera" @decode="onDecode" /> </div> <div id="result" :class="status"> <div v-if="result"> <h1>{{ result.reason }}</h1> <h3>{{ result.given_name }} {{result.family_name}}</h3> <p>{{ result.date_of_birth }}</p> <p v-if="result.more_reason">{{ result.more_reason }}</p> <button @click="reset">Scan another code!</button> </div> <h1 v-else>Please scan your QR Code!</h1> </div> </main> <footer> QRona - a not very serious Corona Certificate Validator.<br /> Made by <a href="https://domm.plix.at">domm</a> for <a href="http://act.yapc.eu/gpw2022/talk/7791">this talk at German Perl Workshop</a>.<br /> Original validator code by Maroš. </footer> </div> </template>
<div id="head"> <h1>QRona</h1> Scan and validate your Corona QR Certificate </div>
Just some plain old HTML
<div id="camera"> <qrcode-stream :camera="camera" @decode="onDecode" /> </div>
And some weird HTML
Which isn't HTML, but (in this case) calling a vue component (think module)
This implements the QR code scanner
https://gruhn.github.io/vue-qrcode-reader/
~$ yarn add vue-qrcode-reader
More on that later
<div id="result" :class="status"> <div v-if="result"> <h1>{{ result.reason }}</h1> <h3>{{ result.given_name }} {{result.family_name}}</h3> <p>{{ result.date_of_birth }}</p> <p v-if="result.more_reason">{{ result.more_reason }}</p> <button @click="reset">Scan another code!</button> </div> <h1 v-else>Please scan your QR Code!</h1> </div>
Some more weird HTML
<div id="result" :class="status"> <div v-if="result"> <h1>{{ result.reason }}</h1> <h3>{{ result.given_name }} {{result.family_name}}</h3> <p>{{ result.date_of_birth }}</p> <p v-if="result.more_reason">{{ result.more_reason }}</p> <button @click="reset">Scan another code!</button> </div> <h1 v-else>Please scan your QR Code!</h1> </div>
v-if
, v-else
etc are very ugly but effective control structures
if result
is defined, render the child node(s)
else just render "Please scan your QR Code!"
<div id="result" :class="status"> <div v-if="result"> <h1>{{ result.reason }}</h1> <h3>{{ result.given_name }} {{result.family_name}}</h3> <p>{{ result.date_of_birth }}</p> <p v-if="result.more_reason">{{ result.more_reason }}</p> <button @click="reset">Scan another code!</button> </div> <h1 v-else>Please scan your QR Code!</h1> </div>
{{
and }}
define template strings (think [% %]
in Template::Toolkit.
Which we can use to access and render data provided by our app.
<div id="result" :class="status"> <div v-if="result"> <h1>{{ result.reason }}</h1> <h3>{{ result.given_name }} {{result.family_name}}</h3> <p>{{ result.date_of_birth }}</p> <p v-if="result.more_reason">{{ result.more_reason }}</p> <button @click="reset">Scan another code!</button> </div> <h1 v-else>Please scan your QR Code!</h1> </div>
@click
tells vue to listen on click
events on the button
And call the reset
method defined in our app when the event happens
<script>
import { QrcodeStream } from 'vue-qrcode-reader'; import { beep } from "@/components/beep/beep.js"; export default { name: 'QRona', components: { QrcodeStream, }, data () { return { camera: 'auto', result: null, showScanConfirmation: false, status: 'waiting', } }, methods: { async onDecode(content) { fetch("/api/qr", { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ qr: content }) }) .then(response => response.json()) .then(json => { this.result = json; this.status = json.status; if (json.status == 'valid') { beep(json.reason); } else { beep("error"); } this.camera = 'off'; setTimeout(this.reset, 10 * 1000); }) .catch(err => alert('Request Failed: ' + err)); }, reset() { this.result = null; this.status = 'waiting'; this.camera = 'auto'; } } }
import { QrcodeStream } from 'vue-qrcode-reader'; import { beep } from "@/components/beep/beep.js"; export default { name: 'QRona', components: { ... }, data() { ... }, methods: { ... } }
A Vue app usually consists of some other components
, data
and methods
import { QrcodeStream } from 'vue-qrcode-reader'; import { beep } from "@/components/beep/beep.js"; export default { name: 'QRona', components: { QrcodeStream, },
Import the Component and register it, so we can use it in template:
<qrcode-stream :camera="camera" @decode="onDecode" />
data () { return { camera: 'auto', result: null, showScanConfirmation: false, status: 'waiting', } },
a bit like attributes of a class
data () { return { camera: 'auto', result: null, showScanConfirmation: false, status: 'waiting', } },
we can access data
in the template:
<div id="result" :class="status">
renders as
<div id="result" class="waiting">
:class="status"
is short for v-bind:class="status"
and binds the value of the attribute class to the value of status
As soon as data.status
changes, the template will automatically refresh and render the new data!
This is main selling point for SPAs and frontend applications!
And it is fact nice!
methods: { async onDecode(content) { fetch("/api/qr", { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ qr: content }) }) .then(response => response.json()) .then(json => { this.result = json; this.status = json.status; if (json.status == 'valid') { beep(json.reason); } else { beep("error"); } this.camera = 'off'; setTimeout(this.reset, 10 * 1000); }) .catch(err => alert('Request Failed: ' + err)); },
methods: { async onDecode(content) { fetch("/api/qr", { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ qr: content }) })
this is called when the QRcode Reader parsed a new QR code:
<qrcode-stream :camera="camera" @decode="onDecode" />
qrcode-stream
emits an event of type decode, and we route this event to our method onDecode
methods: { async onDecode(content) { fetch("/api/qr", { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ qr: content }) })
Use fetch
to send a POST request to /api/qr
methods: { async onDecode(content) { fetch("/api/qr", { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ qr: content }) })
Pass the content
(the decoded QR code) in the request body
.then(response => response.json()) .then(json => { this.result = json; this.status = json.status;
fetch
returns a Promise, which we handle via then
.then(response => response.json()) .then(json => { this.result = json; this.status = json.status;
We can now copy the returned data from the request into our data
<div id="result" :class="status">
which will trigger updates in the UI.
if (json.status == 'valid') { beep(json.reason); } else { beep("error"); } this.camera = 'off'; setTimeout(this.reset, 10 * 1000); }) .catch(err => alert('Request Failed: ' + err)); },
If we got a valid response, play a short audio snippet based on the type of the certificate (test, vaccination, recovered)
if (json.status == 'valid') { beep(json.reason); } else { beep("error"); } this.camera = 'off'; setTimeout(this.reset, 10 * 1000); }) .catch(err => alert('Request Failed: ' + err)); },
Or play an error sound if the certicificate is invalid
if (json.status == 'valid') { beep(json.reason); } else { beep("error"); } this.camera = 'off'; setTimeout(this.reset, 10 * 1000); }) .catch(err => alert('Request Failed: ' + err)); },
Reset the camera, so we can scan another code
if (json.status == 'valid') { beep(json.reason); } else { beep("error"); } this.camera = 'off'; setTimeout(this.reset, 10 * 1000); }) .catch(err => alert('Request Failed: ' + err)); },
And finally set a timeout of 10 seconds to reset the app to its initial state.
Very simple, eh?
Not too much code for a nice small app...