EU Digital COVID Certificate [DECODING]

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)

EU Digital COVID Certificate QR code

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.

EU Digital COVID Certificate creation pipeline

Links
Code: github.com/ehn-dcc-development
Documents: ec.europa.eu/health/ehealth/covid-19_en

17 thoughts on “EU Digital COVID Certificate [DECODING]

    1. admin
      admin says:

      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.

      Reply
  1. Avatar
    Iulian Daniel says:

    And would you be so kind to define the steps to encode it back, so from Json to Base45 format ??

    Reply
    1. Avatar
      Iulian Daniel says:

      I am an idiot, sorry 😀 , yeah, i already asked something there 😉

      Reply
  2. Avatar
    Emmel says:

    hello dragan which java program did you use where can i download it ?

    Reply
    1. admin
      admin says:

      Java is a language, not a program. You should download an IDE, I would recommend IntelliJ IDEA, Nachbar.

      Reply
      1. Avatar
        Emmel says:

        i have the program now but i did not provide your instructions i have never worked with the program do you speak german?

        Reply
        1. admin
          admin says:

          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.

          Reply
  3. Avatar
    Emmel says:

    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.

    Reply
    1. admin
      admin says:

      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.

      Reply
  4. Avatar
    Crackaner says:

    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?

    Reply
    1. admin
      admin says:

      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?

      Reply
  5. Avatar
    Emmel says:

    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 ?

    Reply
      1. Avatar
        Emmel says:

        ich habe das Runtergeladen aber oben steht jetzt Oracle JDK kein Java.
        Haben sie Discord ?

        Reply

Leave a Reply

Your email address will not be published. Required fields are marked *