Let’s make a COVID Certificate [ENCODING]

The EU Digital COVID Certificate is just a QR code, is it really that hard to make it? My good old friend Homer Simpson doesn’t want to get the vaccine, so they don’t let him enter the bar, could we make him one? Let’s find out.

In a previous post I described in detail how to decode the EU Digital COVID Certificate, now we go the other way, to generate such a QR code. Here is a brief overview taken from that previous post. If you want to try this code, you should definitely visit the previous post and copy all the maven dependencies, because I will use them here as well.

Data → JSON

This is how I generated the JSON, again with ObjectMapper.

List<VaccinationPayload> vaccinations = new ArrayList<>();
VaccinationPayload vaccine = new VaccinationPayload();
...
vaccine.setVaccine("1119349007"); // COVID-19 mRNA vaccine
vaccine.setAuthorizationHolder("ORG-100030215"); // Biontech
vaccine.setDoseNumber(2); // Dose number: 2
vaccine.setDoseTotalNumber(2); // Expected doses: 2
...
vaccinations.add(vaccine);

PersonPayload person = new PersonPayload();
person.setFamilyName("Simpson");
person.setGivenName("Homer");
...
CertificatePayload certificate = new CertificatePayload();
certificate.setSubject(person);
certificate.setVaccinations(vaccinations);
certificate.setDateOfBirthString("1956-05-12");
...
ObjectMapper mapper  = new ObjectMapper();
mapper.setVisibility(mapper.getSerializationConfig()
	.getDefaultVisibilityChecker()
	.withFieldVisibility(Visibility.ANY)
	.withGetterVisibility(Visibility.NONE)
	.withSetterVisibility(Visibility.NONE)
	.withCreatorVisibility(Visibility.NONE));

String json = mapper.writeValueAsString(certificate);

Of course, this JSON can be generated in various ways, even manually, the most important thing is that in the end it looks like this.

{"v":[{"ci":"URN:UVCI:01:DE:187/37512422923","co":"DE","dn":2,"dt":"2021-09-24","is":"Robert Koch-Institut","ma":"ORG-100030215","mp":"EU/1/20/1528","sd":2,"tg":"840539006","vp":"1119349007"}],"dob":"1956-05-12","nam":{"fn":"Simpson","gn":"Homer","fnt":"SIMPSON","gnt":"HOMER"},"ver":"1.0.0"}

JSON → CBOR

You may have seen the country code DE, which means that Homer is now German, this is for a specific reason and will be revealed at the end. Now let’s make these CBOR bytes.

long issuedAtSec = LocalDateTime.now().atZone(ZoneId.systemDefault()).toInstant().toEpochMilli() / 1000L;
long expirationSec = LocalDateTime.now().plusYears(1).atZone(ZoneId.systemDefault()).toInstant().toEpochMilli() / 1000L;

CBORObject map = CBORObject.NewMap();
map.set(CBORObject.FromObject(1), CBORObject.FromObject("DE"));
map.set(CBORObject.FromObject(6), CBORObject.FromObject(issuedAtSec));
map.set(CBORObject.FromObject(4), CBORObject.FromObject(expirationSec));
CBORObject hcertVersion = CBORObject.NewMap();
CBORObject hcert = CBORObject.FromJSONString(json);
hcertVersion.set(CBORObject.FromObject(1), hcert);
map.set(CBORObject.FromObject(-260), hcertVersion);

byte[] cbor = map.EncodeToBytes();

This is what it looks like when encoded in CBOR.

A401624445041A632EFCA9061A614DC929390103A101A4617681AA626369781E55524E3A555643493A30313A44453A3138372F333735313234323239323362636F62444562646E026264746A323032312D30392D323462697374526F62657274204B6F63682D496E737469747574626D616D4F52472D313030303330323135626D706C45552F312F32302F3135323862736402627467693834303533393030366276706A3131313933343930303763646F626A313935362D30352D3132636E616DA462666E6753696D70736F6E62676E65486F6D657263666E746753494D50534F4E63676E7465484F4D45526376657265312E302E30

CBOR → COSE

Now comes the complicated part, we have to sign these bytes with a key issued by the German certification authority in charge of COVID certificates. So, let’s hack them…
Just kidding, we’ll make a new key and sign it with him.

OneKey privateKey = OneKey.generateKey(AlgorithmID.ECDSA_256);
byte[] kid = UUID.randomUUID().toString().getBytes(StandardCharsets.UTF_8);

Sign1Message msg = new Sign1Message();
msg.addAttribute(HeaderKeys.Algorithm, privateKey.get(KeyKeys.Algorithm), Attribute.PROTECTED);
msg.addAttribute(HeaderKeys.KID, CBORObject.FromObject(kid), Attribute.PROTECTED);
msg.SetContent(cbor);
msg.sign(privateKey);

byte[] cose = msg.EncodeToBytes();

This was the most complicated part, but now we have COSE bytes.

D284582AA2012604582462646663326264612D366337342D343065632D613533302D386430343034356533336132A058F6A401624445041A632EFCA9061A614DC929390103A101A4617681AA626369781E55524E3A555643493A30313A44453A3138372F333735313234323239323362636F62444562646E026264746A323032312D30392D323462697374526F62657274204B6F63682D496E737469747574626D616D4F52472D313030303330323135626D706C45552F312F32302F3135323862736402627467693834303533393030366276706A3131313933343930303763646F626A313935362D30352D3132636E616DA462666E6753696D70736F6E62676E65486F6D657263666E746753494D50534F4E63676E7465484F4D45526376657265312E302E305840FAA429AA9920AF8FDA1126A9AE3CD146CACBE30DEF0F96C7B0832CB17CF27510C02CACF787062F5B358A842BC88C06954A00343B155032CB725D7E95AE5E366C

Do not be surprised if you got different COSE bytes, in this process a new key is generated and it is almost impossible for these keys to be the same/to generate the same signature.

COSE → COMPRESSED

Compression and the rest are much simpler.

ByteArrayOutputStream stream = new ByteArrayOutputStream();
try (CompressorOutputStream deflateOut = new CompressorStreamFactory()
	.createCompressorOutputStream(CompressorStreamFactory.DEFLATE,
	stream)) {
		deflateOut.write(cose);
	}
byte[] zip = stream.toByteArray();

All zipped up and we get some bytes again that I won’t write down, because they don’t matter, they will look different every time this code is executed.

COMPRESSED → BASE45

Base45 encoding goes like this.

String base45 = Base45.getEncoder().encodeToString(zip);

BASE45 → PREFIX

Final touch, adding the prefix.

String hc1 = "HC1:" + base45;

And this is what the certificate looks like, for now.

HC1:6BFLX1VX7YUO 53TGEHQQF+Q7.O.R3$IS5 6G*B*SR6F71LB$J9*ZMMF1FEI:OI2R2J$ISP5KG4CKG$MG0NGMG9DZ8ULV00EFY9VSNZ50.*0YGE3WSL2G:ANEUSBEFYQQWP3RSLA93A*4WJPM.41VD.U9VI0+9B5EGLRBUZ9RMBW/DC8EU1RBN503G4MSOP66S6ZZGH3Q.W1U3O2JHI2SVO5W6T0-G/GHWE2TJED-ILIHZ3LVU0PXJJUGL55R6TD3JINH3F5J9CS9G533Y9NPIE3RR6/QD03  A%6V2QP5WTW 7/YPP:KKS0//N7V0U-IETA*RK9K0AF1VFO :QUQPWI2HXGG40FDM4A5CG9H*JTK58*LPYP522CE0/5A +F%PD5LPQUUV3B$T08J7KW7DAF+ ANZFY-SGIJ2XF2-B%4O-4DCHJM/922UU 9:329VSU3EWKUT%KZ8TN-N2LEM$7IK7:/VFOQ8MR4-V/TNRMEPPVO5S.08Y2VR03V2JCAWHKUDF8VAD *GK5VREMQVT37RJ0VUYD:/3Q07R2

PREFIX → QR Code Image 

All that remains is to make a QR code out of this.

QRCodeWriter qrCodeWriter = new QRCodeWriter();
int width = 300, height = 300; //px
BitMatrix bitMatrix = qrCodeWriter.encode(hc1,
	BarcodeFormat.QR_CODE, width, height);
// Write to file
MatrixToImageWriter.writeToPath(bitMatrix, "PNG",
	new File(elHomo.png).toPath());

And here it is.

Conclusion

The only thing left is to scan the code with the CovPas app. And… D’oh! It doesn’t work. That app was made by IBM and they are not so naive.

But there is hope, and it all lies in the fact that we made Homer a German. To uncover the secret, the Corona-Warn-App, before version 2.7, did not perform a digital signature check at all. This means that it will accept our certificate without any problems. You don’t believe me, download the Corona-Warn-App 2.6.1 and scan this previously generated QR code.

Guess who’s now staying in the bar until dawn, let’s hope no one will actually check his certificate.

36 thoughts on “Let’s make a COVID Certificate [ENCODING]

  1. Avatar
    Daniel Harmann says:

    Hi,
    Excellent Article
    Small question at the JSON – CBOR step.
    You have converted the JSON data to CBOR before the COSE signing stage.
    Do you also need to convert the protected/unprotected headers to CBOR BEFORE the COSE signing stage?
    Thanks.

    Reply
    1. Avatar
      alberto says:

      hola buenas, soy un poco torpe . que aplicación estáis utilizando.

      Reply
  2. Avatar
    Daniel Harmann says:

    Hi, me again.
    Thanks for your reply to the last comment.
    At the CBOR-COSE step, I have found that decoding a QR is not possible if the cbor.tag “18” has not been encoded at the COSE stage.
    The COSE step would look like this {18: [[headers],[payload],[signature]]} and not just {[[headers],[payload],[signature]]}
    Do you have any idea how you would encode the cbor.tag of 18 into the COSE step.

    Reply
        1. admin
          admin says:

          None, this is a simple Maven project written in Java.
          The same code can be used in Android with another form of dependencies.

          Reply
          1. Avatar
            console says:

            hi admin where can i use this commands

  3. Avatar
    Randomguy says:

    If there was to be a leak of digital signatures, does it mean one could create a working covid certificate pretty much in any country using this?

    Reply
    1. admin
      admin says:

      Nope, every certificate already has a digital signature, what you need is a “leaked” key to create such signature. That shouldn’t be possible, because all compromised keys are put into CRL lists/black listed.

      Reply
      1. Avatar
        Iulian Daniel says:

        I think he meant , if a valid certificate is already at hand… Can one use that data to make other certificates which have the same signature ? And still be valid?! The process of verifying the qr code is made offline anyway, so how would the know ?

        Reply
        1. admin
          admin says:

          It’s not that simple. I will avoid using the word certificate because it confuses the QR certificate with the signer key certificate.
          Imagine you have a doctor who signs these QR codes with his keys. He has a private key, with which he signs the data and generates a signature that is also in the QR code and a public key, which he puts in the certificate verification application because it can be used to verify the validity of the signature. The application can work offline because while it is online it downloads all these public keys and has them in memory when checking certificate signatures.
          Now when you make a small change to the data and leave the same signature, an error will appear when verifying the signature, because any change in the data will generate a completely different signature when signing with a real private key.

          Reply
          1. Avatar
            Iulian Daniel says:

            Then my understanding of the meaning of signing was false in my head, Thank you for clearing that for me.

  4. Avatar
    Crackaner says:

    I’m using my real ci (Unique cert identifier) from my legit passport covid, why is still not working when creating a fake one?

    Reply
  5. Avatar
    jonofoster says:

    Hi Admin

    wonderful article, i’m just stuck on something i don’t know how to insert a key in your code i see you generate a key

    I would like to put a key instead of generating one

    for example directly a key in SHA256 or a key in string: ‘abcde%12345’ who is transformed into sha256

    how do I put that in place of

    OneKey privateKey = OneKey.generateKey(AlgorithmID.ECDSA_256);

    Thanks for your response

    Reply
    1. admin
      admin says:

      Hi,

      With that OneKey class, something like that is definitely not possible. I would recommend that you google something like “loading a private key from a file”. Because you need a private key to sign, and the file can be binary DER (where you will see how a key is made from bytes) or written as a string PEM (you will see how to convert the string to bytes and then to a key).
      And at the same time you will see the algorithms for the keys and realize that SHA256 is a hash algorithm used for digital signatures, not for keys.

      Greetings to the UK,
      Dragan

      Reply
  6. Avatar
    Mike says:

    Hello admin can you plz list all the dependencies that you used … but in simple java app… and thanks a lot

    Reply
  7. Avatar
    Stephan says:

    Can You Please post the project here . I literally import all the needed dependencies but every time I got a lot of errors even with the localdatetime thanks in advance

    Reply
  8. Avatar
    Mr. M says:

    Hi Admin,
    I would like to ask you a question. I have a original valid recovery-certificate. When I now decode the qr-code and afterwards encode the json again but with no changes of the original data I would expect the so created qr-code will work and is valid but in my case the new generated qr-code is then invalid. Do you maybe know why it is the case?
    Thanks a lot for your answer!

    Reply
    1. Avatar
      Stephan says:

      Will simply because the second time it was signed by a random key 🔐 not the original from the RKI … And how do you generate one what are the libraries you used and if it possible can you post the code here …

      Reply
  9. Avatar
    Ghostpuppet says:

    Hey nice job encoding, how similar would this be to the smart health card framework used in Canada?(shc) any way we could get those encoded?

    Reply
  10. Avatar
    Dan says:

    Hi I have read this entire page and the previous one with the dependencies, I was wondering if we could get the whole portrait at once. Not being an expert in java i find it a bit twisted and all my efforts so far has failed.

    Reply
  11. Avatar
    Sonny says:

    Hallo
    ich habe es entschlüsselt aber sobald ich vom Json eine Zahl änder zum beispiel 28.OKT 2021 zu 27.OKT 2021 wird der QR Code nicht mehr als Gültig erkannt nur in der alten App wird es als gültig erkannt.

    Ich hoffe sie verstehen Deutsch.

    Reply
  12. Avatar
    Bonito Broccolini says:

    Good day, I have a little problem – what if I am not from Germany and I am travelling – won’t the fact that I am from X country instead of Germany cause problems? Am I even allowed to get the covid-QR pass in Germany as a person from X country and use it?
    Does the DE code mean that I am German or that I just got the certificate in Germany?

    Reply
  13. Avatar
    Bonito Broccolini says:

    Forgot to add that the X country is in the European Union

    Reply

Leave a Reply

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