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

Leave a Reply

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