Intro
As you all know, the EU Digital Green Certificate is just a QR code, but what is behind all this and what is inside of it?
In this post, I’ll explain how to decode and read all the data from such QR code, using pure Java. I did not plan to use real data, so test data from github will be used.
Image → PREFIX (HC1)
This sample QR code was picked and when scanned with an application for scanning QR/Barcodes you get:
HC1:6BF+70790T9WJWG.FKY*4GO0.O1CV2 O5 N2FBBRW1*70HS8WY04AC*WIFN0AHCD8KD97TK0F90KECTHGWJC0FDC:5AIA%G7X+AQB9746HS80:54IBQF60R6$A80X6S1BTYACG6M+9XG8KIAWNA91AY%67092L4WJCT3EHS8XJC$+DXJCCWENF6OF63W5NW6WF6%JC QE/IAYJC5LEW34U3ET7DXC9 QE-ED8%E.JCBECB1A-:8$96646AL60A60S6Q$D.UDRYA 96NF6L/5QW6307KQEPD09WEQDD+Q6TW6FA7C466KCN9E%961A6DL6FA7D46JPCT3E5JDLA7$Q6E464W5TG6..DX%DZJC6/DTZ9 QE5$CB$DA/D JC1/D3Z8WED1ECW.CCWE.Y92OAGY8MY9L+9MPCG/D5 C5IA5N9$PC5$CUZCY$5Y$527B+A4KZNQG5TKOWWD9FL%I8U$F7O2IBM85CWOC%LEZU4R/BXHDAHN 11$CA5MRI:AONFN7091K9FKIGIY%VWSSSU9%01FO2*FTPQ3C3F
This strange text is called PREFIX, because it starts with HC1, meaning Health Certificate 1. Such a QR code reader can easily be implemented in Java.
<dependencies>
...
<!-- QR Libraries -->
<dependency>
<groupId>com.google.zxing</groupId>
<artifactId>core</artifactId>
<version>3.4.1</version>
</dependency>
<dependency>
<groupId>com.google.zxing</groupId>
<artifactId>javase</artifactId>
<version>3.4.1</version>
</dependency>
...
</dependencies>
File qrFile = new File("1.png");
BinaryBitmap image = new BinaryBitmap(new HybridBinarizer(
new BufferedImageLuminanceSource(ImageIO.read(
new FileInputStream(qrFile.getAbsolutePath())))));
Result result = new MultiFormatReader().decode(image);
String qrString = result.getText();
PREFIX → BASE45
Short and clear, and now removing that prefix because it’s not needed.
String base45String = qrString.substring(4);
What remains is BASE45 encoded data.
6BF+70790T9WJWG.FKY*4GO0.O1CV2 O5 N2FBBRW1*70HS8WY04AC*WIFN0AHCD8KD97TK0F90KECTHGWJC0FDC:5AIA%G7X+AQB9746HS80:54IBQF60R6$A80X6S1BTYACG6M+9XG8KIAWNA91AY%67092L4WJCT3EHS8XJC$+DXJCCWENF6OF63W5NW6WF6%JC QE/IAYJC5LEW34U3ET7DXC9 QE-ED8%E.JCBECB1A-:8$96646AL60A60S6Q$D.UDRYA 96NF6L/5QW6307KQEPD09WEQDD+Q6TW6FA7C466KCN9E%961A6DL6FA7D46JPCT3E5JDLA7$Q6E464W5TG6..DX%DZJC6/DTZ9 QE5$CB$DA/D JC1/D3Z8WED1ECW.CCWE.Y92OAGY8MY9L+9MPCG/D5 C5IA5N9$PC5$CUZCY$5Y$527B+A4KZNQG5TKOWWD9FL%I8U$F7O2IBM85CWOC%LEZU4R/BXHDAHN 11$CA5MRI:AONFN7091K9FKIGIY%VWSSSU9%01FO2*FTPQ3C3F
BASE45 → COMPRESSED
Base45 decoding can be implemented as follows.
<dependencies>
...
<!-- Base45 Library -->
<dependency>
<groupId>io.github.ehn-digital-green-development</groupId>
<artifactId>base45</artifactId>
<version>0.0.3</version>
</dependency>
...
</dependencies>
byte[] zip = Base45.getDecoder().decode(base45String);
What you get are zipped bytes, called COMPRESSED. They always start with 0x78, if it is not 78 at the beginning, it is not compressed data.
789c0163019cfed28443a10126a104480c4b15512be9140159010da401624445061a60b29429041a61f39fa9390103a101a4617681aa626369782f55524e3a555643493a303144452f495a3132333435412f3543574c553132524e4f4239525853454f5036464738235762636f62444562646e026264746a323032312d30352d323962697374526f62657274204b6f63682d496e737469747574626d616d4f52472d313030303331313834626d706c45552f312f32302f3135303762736402627467693834303533393030366276706a3131313933343930303763646f626a313936342d30382d3132636e616da462666e6a4d75737465726d616e6e62676e654572696b6163666e746a4d55535445524d414e4e63676e74654552494b416376657265312e302e305840218ebc2a2a77c1796c95a8c942987d461411b0075fd563447295250d5ead69f3b8f6083a515bd97656e87aca01529e6aa0e09144fc07e2884c93080f1419e82f1c66773a
COMPRESSED → COSE
The compressed bytes can then be decompressed/unzipped.
<dependencies>
...
<!-- Compress Library -->
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-compress</artifactId>
<version>1.20</version>
</dependency>
...
</dependencies>
CompressorInputStream compressedStream = new CompressorStreamFactory()
.createCompressorInputStream(CompressorStreamFactory.DEFLATE,
new ByteArrayInputStream(zip));
byte[] cose = toByteArray(compressedStream);
// org.apache.commons.compress.utils.IOUtils.toByteArray()
After unzipping, we get COSE bytes, in them is the whole essence of the digital signing mechanism for these certificates.
d28443a10126a104480c4b15512be9140159010da401624445061a60b29429041a61f39fa9390103a101a4617681aa626369782f55524e3a555643493a303144452f495a3132333435412f3543574c553132524e4f4239525853454f5036464738235762636f62444562646e026264746a323032312d30352d323962697374526f62657274204b6f63682d496e737469747574626d616d4f52472d313030303331313834626d706c45552f312f32302f3135303762736402627467693834303533393030366276706a3131313933343930303763646f626a313936342d30382d3132636e616da462666e6a4d75737465726d616e6e62676e654572696b6163666e746a4d55535445524d414e4e63676e74654552494b416376657265312e302e305840218ebc2a2a77c1796c95a8c942987d461411b0075fd563447295250d5ead69f3b8f6083a515bd97656e87aca01529e6aa0e09144fc07e2884c93080f1419e82f
COSE → CBOR → JSON
Now skipping one structure called CBOR and further decoding the data we get a JSON-like string. From the COSE bytes we will get that JSON string as follows.
<dependencies>
...
<!-- CBOR Library -->
<dependency>
<groupId>com.upokecenter</groupId>
<artifactId>cbor</artifactId>
<version>4.3.0</version>
</dependency>
<!-- COSE Library -->
<dependency>
<groupId>com.augustcellars.cose</groupId>
<artifactId>cose-java</artifactId>
<version>1.1.0</version>
</dependency>
...
</dependencies>
Sign1Message msg = (Sign1Message) Message.DecodeFromBytes(cose, MessageTag.Sign1);
CBORObject cborObject = CBORObject.DecodeFromBytes(msg.GetContent());
ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
cborObject.WriteJSONTo(byteArrayOutputStream);
String jsonString = byteArrayOutputStream.toString("UTF-8");
This string is called JSON, although it’s not possible to convert it directly to JSON with new JSONObject (jsonString)
.
{
"dob": "1964-08-12",
"nam": {
"fn": "Mustermann",
"fnt": "MUSTERMANN",
"gn": "Erika",
"gnt": "ERIKA"
},
"v": [{
"ci": "URN:UVCI:01DE/IZ12345A/5CWLU12RNOB9RXSEOP6FG8#W",
"co": "DE",
"dn": 2,
"dt": "2021-05-29",
"is": "Robert Koch-Institut",
"ma": "ORG-100031184",
"mp": "EU/1/20/1507",
"sd": 2,
"tg": "840539006",
"vp": "1119349007"
}],
"ver": "1.0.0"
}
Briefly to fly over the basic data, at the beginning, we have the date of birth (dob), person surname (fn) and name (gn). Followed by a list of vaccines (v). The vaccine itself has the number of doses (dn – current dose; sd – total doses), date (dt), type (vp), manufacturer (ma), country (co) and so on.
VP: 1119349007
→ SARS-CoV-2 mRNA vaccine
MA: ORG-100031184
→ Moderna Biotech Spain S.L.
More information in the Value Sets document.
For all of you who want to explore this in detail, I suggest you download the Technical Specifications for EU Digital COVID Certificates.
JSON → Object
Now only for the more experienced and those who seriously plan to deal with this. There are various ways to make an object from this string, that can be further used in the code, I will list the one that proved to be the easiest with ObjectMapper.
<dependencies>
...
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
<version>2.12.5</version>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.datatype</groupId>
<artifactId>jackson-datatype-jsr310</artifactId>
<version>2.12.5</version>
</dependency>
...
</dependencies>
ObjectMapper objectMapper = new ObjectMapper();
objectMapper.registerModule(new JavaTimeModule());
objectMapper.configure(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS, false);
DGCPayload dgc = objectMapper.readValue(jsonString, DGCPayload.class);
...
Conclusion
And that’s basically it, whoever read everything in detail, understood how this QR code is generated. It’s a little unnecessarily complicated, but that’s how they defined it. A brief overview of everything to finish up.
Links
Code: github.com/ehn-dcc-development
Documents: ec.europa.eu/health/ehealth/covid-19_en
Hi Dragan,
On decoding the signature, you get a string in hex that is 128 characters long.
Do you know if you need to “convert” this string into anything else before you can use it to encode it? Possibly from P1363 to DER as stated here: http://www.corentindupont.info/blog/posts/Programming/2021-08-13-GreenPass.html
Thanks,
Daniel
Hello Daniel,
That example is a little bit too hard-code, but that’s about how it goes. I use COSE libraries in these examples so it goes automatically, manually it gets a lot more complicated.
The signature is Base64 encoded, so first it must be decoded into bytes. After that, the EC signature is divided into two halves and packed into BER integers. The last step is to generate a sequence (BER Tree) of these two integers. Bytes created in this way represent a complete digital signature and can be used in a normal way, as these are bytes (binary encoding) we can say that it is a DER format.
And would you be so kind to define the steps to encode it back, so from Json to Base45 format ??
Have you seen this: https://dx.dragan.ba/covid-certificate-encoding/
I am an idiot, sorry 😀 , yeah, i already asked something there 😉
hello dragan which java program did you use where can i download it ?
Java is a language, not a program. You should download an IDE, I would recommend IntelliJ IDEA, Nachbar.
i have the program now but i did not provide your instructions i have never worked with the program do you speak german?
Yes, I speak German.
Now you have to learn how to write a Hello World in java, how to create a maven project with dependencies and then you can copy and run this code.
also ich habe diesen Präfix Code auf mein PC aus einem QR Code ausgelesen aber weiter weiß ich nicht.
Habe noch nie sowas gemacht mit dem Programm.
Ich verstehe, aber dieser Beitrag war nicht für jemanden gedacht, der absolut keine Erfahrung mit Java hat.
ENG: I understand, but this post was not intended for someone who has absolutely no experience with Java.
Hi!
The “ToByteArray(compressedStream)” method is giving me problems, because it needs an InputStream for parameter, not a CompressorInputStream.
Do you know how to solve this?
CompressorInputStream extends InputStream so that shouldn’t be a problem. If it’s such a stupid comiler, you can always manually cast it in (InputStream). Are you sure you are using the right IOUtils.toByteArray() method?
ich habe mir das Programm runtergeladen und Installiert (IntelliJ IDEA Community Edition 2021.3)
wenn ich ein neues Projekt mache steht oben das ich keine SDK habe wo kann ich die richtigen SDK oder JDK Runterladen ?
JDK: https://www.oracle.com/java/technologies/downloads/
ich habe das Runtergeladen aber oben steht jetzt Oracle JDK kein Java.
Haben sie Discord ?
how can i use this commandes and where ?