Home | Blog | Green Pass Analysis

What's Inside the EU Green Pass QR Code?

Here in Austria, you need to be either vaccinated against, tested negative for, or have recovered from COVID-19 to go to the pub (or pretty much everywhere) right now. If you don't want to carry around your paperwork proving one of those, you can alternatively show your Green Pass QR code to be let in.

As a privacy concious person, I wanted to know what details I give away when I let my server scan my code.

A Green Pass code, generated from fictitious data

This is what a Green Pass looks like. Quite large for a QR code, but given that it is supposed to store more than a hyperlink, that's to be expected. Let's decode it.

t@thi3nkpad ~ % zbarimg --raw greenpass.png
HC1:NCFOXN%TS3DH3ZSUZK+.V0ETD%65NL-AH-R6IOO6+IUKRG*I.I5BROCWAAT4V22F/8X*G3M9JUPY0BX/KR96R/S09T./0LWTKD33236J3TA3M*4VV2 73-E3GG396B-43O058YIB73A*G3W19UEBY5:PI0EGSP4*2DN43U*0CEBQ/GXQFY73CIBC:G 7376BXBJBAJ UNFMJCRN0H3PQN*E33H3OA70M3FMJIJN523.K5QZ4A+2XEN QT QTHC31M3+E32R44$28A9H0D3ZCL4JMYAZ+S-A5$XKX6T2YC 35H/ITX8GL2-LH/CJTK96L6SR9MU9RFGJA6Q3QR$P2OIC0JVLA8J3ET3:H3A+2+33U SAAUOT3TPTO4UBZIC0JKQTL*QDKBO.AI9BVYTOCFOPS4IJCOT0$89NT2V457U8+9W2KQ-7LF9-DF07U$B97JJ1D7WKP/HLIJLRKF1MFHJP7NVDEBU1J*Z222E.GJF67Z JA6B.38O4BH*HB0EGLE2%V -3O+J3.PI2G:M1SSP2Y3D38-G9C+Q3OT/.J1CDLKOYUV5C3W1A:75S4LB:6REPKM3ZYO4+QDNDLT2*ESLIH

Well, that's ... something. What we're looking at there is a Base45-encoded, compressed, signed binary data structure. The group who designed the EU green pass have published the QR Code specification, that goes into detail about the which and why of the technologies chosen, but in short: it's heavily optimized for size and reliability.

Looking at the hexdump

Once we peel this onion, we're left with the bytes that make up the signed document. Highlighted blue, is the metadata (Key ID and signature algorithm used), green is the signature, and yellow is the main CBOR data structure. The non-ASCII part at the beginning is the QR code's issuer, and expiry and generation dates as a 32 bit UNIX timestamp, the rest of the values are just about readable as-is.

t@thi3nkpad ~ % zbarimg --raw greenpass.png|cut -b5-|tr -d '\n'|base45 --decode|openssl zlib -d|xxd
00000000: d284 4da2 0448 d919 375f c1e7 b6b2 0126  ..M..H..7_.....&
00000010: a059 0133 a404 1a60 d345 6506 1a60 d0a2  .Y.3...`.Ee..`..
00000020: 6501 6241 5439 0103 a101 a461 7681 aa62  e.bAT9.....av..b
00000030: 646e 0162 6d61 6d4f 5247 2d31 3030 3033  dn.bmamORG-10003
00000040: 3032 3135 6276 706a 3131 3139 3334 3930  0215bvpj11193490
00000050: 3037 6264 746a 3230 3231 2d30 322d 3138  07bdtj2021-02-18
00000060: 6263 6f62 4154 6263 6978 3155 524e 3a55  bcobATbcix1URN:U
00000070: 5643 493a 3031 3a41 543a 3130 3830 3738  VCI:01:AT:108078
00000080: 3433 4639 3441 4545 3045 4535 3039 3346  43F94AEE0EE5093F
00000090: 4243 3235 3442 4438 3133 2342 626d 706c  BC254BD813#Bbmpl
000000a0: 4555 2f31 2f32 302f 3135 3238 6269 7378  EU/1/20/1528bisx
000000b0: 1b4d 696e 6973 7472 7920 6f66 2048 6561  .Ministry of Hea
000000c0: 6c74 682c 2041 7573 7472 6961 6273 6402  lth, Austriabsd.
000000d0: 6274 6769 3834 3035 3339 3030 3663 6e61  btgi840539006cna
000000e0: 6da4 6366 6e74 754d 5553 5445 5246 5241  m.cfntuMUSTERFRA
000000f0: 553c 474f 4553 5349 4e47 4552 6266 6e75  U<GOESSINGERbfnu
00000100: 4d75 7374 6572 6672 6175 2d47 c3b6 c39f  Musterfrau-G....
00000110: 696e 6765 7263 676e 7468 4741 4252 4945  ingercgnthGABRIE
00000120: 4c45 6267 6e68 4761 6272 6965 6c65 6376  LEbgnhGabrielecv
00000130: 6572 6531 2e32 2e31 6364 6f62 6a31 3939  ere1.2.1cdobj199
00000140: 382d 3032 2d32 3658 40f1 9401 7d5f 93fa  8-02-26X@...}_..
00000150: 8cd8 a646 631e fd8f 9fc1 159d 3b06 17c7  ...Fc.......;...
00000160: f403 a993 88db f0da 6271 2bd9 402b 6181  ........bq+.@+a.
00000170: 34f0 4197 fc98 0be5 9820 8d66 bf35 c104  4.A...... .f.5..
00000180: 7d5b 2c0a e53d 7aad d4                   }[,..=z..

Using a little bit of python, we can extract the following data from the payload, which I annotated by consulting the Green Pass schema.

{-260: {1: {'dob': '1998-02-26', date of birth
            'nam': {'fn': 'Musterfrau-Gößinger', family name
                    'fnt': 'MUSTERFRAU<GOESSINGER',
                    'gn': 'Gabriele', given name
                    'gnt': 'GABRIELE'},
            'v': [{'ci': 'URN:UVCI:01:AT:10807843F94AEE0EE5093FBC254BD813#B', certificate ID
                   'co': 'AT', country of vaccination
                   'dn': 1, doses received
                   'dt': '2021-02-18', date of vaccination
                   'is': 'Ministry of Health, Austria', cert issuer
                   'ma': 'ORG-100030215', vaccine manufacturer
                   'mp': 'EU/1/20/1528', vaccine product id
                   'sd': 2, total number of doses
                   'tg': '840539006', targeted disease (COVID-19)
                   'vp': '1119349007'}], vaccine or prophylaxis
            'ver': '1.2.1'}}, schema version
 1: 'AT', QR code issuer
 4: 1624458597, QR code expiry
 6: 1624285797} QR code generated

As you can see, 23 year-old Gabriele was vaccinated in February, once, with BioNTech/Pfizer's Comirnaty. What is not included is the date during which she is considered immune. Those are calculated from the number of shots received and the date of vaccination, as well as the circumstances (going to a restaurant vs. going to work, for example) by the scanner app. Apart from the name/manufacturer of the received vaccine, there is no superfluous data inside, so the QR code is not a privacy nightmare, as some have feared.

example of tested person's data
{-260: {1: {'dob': '1998-02-26', date of birth
            'nam': {'fn': 'Musterfrau-Gößinger', family name
                    'fnt': 'MUSTERFRAU<GOESSINGER',
                    'gn': 'Gabriele', given name
                    'gnt': 'GABRIELE'},
            't': [{'ci': 'URN:UVCI:01:AT:B5921A35D6A0D696421B3E2462178297#I', certificate ID
                   'co': 'AT', country of vaccination
                   'is': 'Ministry of Health, Austria', cert issuer
                   either:
                   'nm': 'Roche LightCycler qPCR', PCR test used
                   or:
                   'ma': '1232', antigen test used
                   'sc': '2021-02-20T12:34:56Z', time of test
                   'tc': 'Testing center Vienna 1', testing facility
                   'tg': '840539006', targeted disease (COVID-19)
                   'tr': '260415000', test result
                   'tt': 'LP6464-4'}], test type
            'ver': '1.2.1'}}, schema version
 1: 'AT', QR code issuer
 4: 1624458597, QR code expiry
 6: 1624285797} QR code generated
example of recovered person's data
{-260: {1: {'dob': '1998-02-26', date of birth
            'nam': {'fn': 'Musterfrau-Gößinger', family name
                    'fnt': 'MUSTERFRAU<GOESSINGER',
                    'gn': 'Gabriele', given name
                    'gnt': 'GABRIELE'},
            'r': [{'ci': 'URN:UVCI:01:AT:858CC18CFCF5965EF82F60E493349AA5#K', certificate ID
                   'co': 'AT', country of vaccination
                   'df': '2021-04-04', certificate valid from
                   'du': '2021-10-04', certificate valid until
                   'fr': '2021-02-20', first positive test result
                   'is': 'Ministry of Health, Austria', cert issuer
                   'tg': '840539006'}], targeted disease (COVID-19)
            'ver': '1.2.1'}}, schema version
 1: 'AT', QR code issuer
 4: 1624458597, QR code expiry
 6: 1624285797} QR code generated

To check out the data on your own green pass, check out greenpass.py. It takes the PDF from gruenerpass.gv.at or PNG of the QR code itself on the command line, and prints out all the data, annotated with the above mentioned schema.