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.
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.
Nope, later in the COSE creation step the header(s) are added.
hola buenas, soy un poco torpe . que aplicación estáis utilizando.
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.
Yes, you are absolutely right. I don’t know how you create COSE, but in my case that tag is “encoded” when creating the Sign1Message() object.
CBOR Tags: https://www.iana.org/assignments/cbor-tags/cbor-tags.xhtml
Can you post the entire project folder, please?
That would be a really expensive request.
Which programme / command prompt did you use?
None, this is a simple Maven project written in Java.
The same code can be used in Android with another form of dependencies.
hi admin where can i use this commands
Yes, but is not a valid Signature..
Of course, my Spanish friend, otherwise this would be a crime of forging documents.
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?
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.
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 ?
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.
Then my understanding of the meaning of signing was false in my head, Thank you for clearing that for me.
What is “ci” field and how it encoded?
Unique certificate identifier (UVCI) as specified here: https://ec.europa.eu/health/sites/default/files/ehealth/docs/vaccination-proof_interoperability-guidelines_en.pdf
I’m using my real ci (Unique cert identifier) from my legit passport covid, why is still not working when creating a fake one?
Somewhere deep in these comments lies the answer to your question.
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
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
Hello admin can you plz list all the dependencies that you used … but in simple java app… and thanks a lot
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
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!
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 …
Please list the needed libraries to follow your steps.
Can we run this command in Android
Yes, only the dependencies are added differently.
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?
I have absolutely no experience with Canadian health cards.
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.
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.
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?
Forgot to add that the X country is in the European Union
Какое симпатичное сообщение
I can recommend to come on a site on which there are many articles on this question.
_ _ _ _ _ _ _ _ _ _ _ _ _ _
Некулицы Иван laravel github