diff --git a/README.md b/README.md
index 8efdaa1..1fa2c07 100644
--- a/README.md
+++ b/README.md
@@ -42,18 +42,44 @@ Include as Maven Dependency in pom.xml
```
+If you do not need the DCC Gateway Connector feature you can exclude some dependencies by adding an exclusions tag:
+
+```xml
+
+
+
+ eu.europa.ec.dgc
+ dgc-lib
+ 1.0.0-SNAPSHOT
+
+
+ org.springframework.cloud
+ *
+
+
+ io.github.openfeign
+ *
+
+
+
+ ...
+
+```
+
### Authenticating to GitHub Packages
**Attention:**
-GitHub does not allow anonymous access to it's package registry. You need to authenticate in order to use the dgc-lib artefact provided by us.
-Therefore you need to authenticate to [GitHub Packages](https://docs.github.com/en/packages/working-with-a-github-packages-registry/working-with-the-apache-maven-registry)
+GitHub does not allow anonymous access to it's package registry. You need to authenticate in order to use the dgc-lib
+artefact provided by us. Therefore you need to authenticate
+to [GitHub Packages](https://docs.github.com/en/packages/working-with-a-github-packages-registry/working-with-the-apache-maven-registry)
The following steps need to be performed
- Create [PAT](https://docs.github.com/en/github/authenticating-to-github/creating-a-personal-access-token) with scopes:
- - `read:packages` for downloading packages
-- Copy/Augment `~/.m2/settings.xml` with the contents of `settings.xml` present in this repository (or in the DGC repository you are trying to build)
- - Replace `${app.packages.username}` with your github username
- - Replace `${app.packages.password}` with the generated PAT
+ - `read:packages` for downloading packages
+- Copy/Augment `~/.m2/settings.xml` with the contents of `settings.xml` present in this repository (or in the DGC
+ repository you are trying to build)
+ - Replace `${app.packages.username}` with your github username
+ - Replace `${app.packages.password}` with the generated PAT
## Development
diff --git a/pom.xml b/pom.xml
index 076e76a..bcf829c 100644
--- a/pom.xml
+++ b/pom.xml
@@ -32,6 +32,8 @@
5.7.1
1.4.2.Final
2.8.0
+ 4.4.3
+ 2.11.4
3.1.2
3.6.1.1688
@@ -74,17 +76,19 @@
org.springframework.boot
spring-boot-starter
${spring.boot.version}
+ true
org.springframework.boot
spring-boot-starter-web
${spring.boot.version}
+ true
org.springframework.boot
spring-boot-configuration-processor
- true
${spring.boot.version}
+ true
org.springframework.cloud
@@ -112,6 +116,21 @@
bcpkix-jdk15on
${bcpkix.version}
+
+ commons-io
+ commons-io
+ ${commonsio.version}
+
+
+ com.upokecenter
+ cbor
+ ${cbor.version}
+
+
+ com.fasterxml.jackson.core
+ jackson-databind
+ ${jackson.version}
+
org.junit.jupiter
@@ -125,11 +144,6 @@
test
${spring.boot.version}
-
- commons-io
- commons-io
- ${commonsio.version}
-
diff --git a/src/main/java/eu/europa/ec/dgc/gateway/connector/config/DgcGatewayConnectorConfigProperties.java b/src/main/java/eu/europa/ec/dgc/gateway/connector/config/DgcGatewayConnectorConfigProperties.java
index c6f9ab8..8a356c8 100644
--- a/src/main/java/eu/europa/ec/dgc/gateway/connector/config/DgcGatewayConnectorConfigProperties.java
+++ b/src/main/java/eu/europa/ec/dgc/gateway/connector/config/DgcGatewayConnectorConfigProperties.java
@@ -109,7 +109,7 @@ public static class Proxy {
/**
* Enable HTTP Proxy.
*/
- private boolean enabled = false;
+ private boolean enabled;
/**
* Host Address of Proxy server (without protocol).
diff --git a/src/main/java/eu/europa/ec/dgc/gateway/connector/config/DgcGatewayConnectorKeystore.java b/src/main/java/eu/europa/ec/dgc/gateway/connector/config/DgcGatewayConnectorKeystore.java
index 5816ab7..165bb63 100644
--- a/src/main/java/eu/europa/ec/dgc/gateway/connector/config/DgcGatewayConnectorKeystore.java
+++ b/src/main/java/eu/europa/ec/dgc/gateway/connector/config/DgcGatewayConnectorKeystore.java
@@ -92,7 +92,7 @@ public KeyStore trustAnchorKeyStore() throws KeyStoreException, IOException,
}
private void loadKeyStore(KeyStore keyStore, String path, char[] password)
- throws CertificateException, NoSuchAlgorithmException, IOException {
+ throws CertificateException, NoSuchAlgorithmException {
try (InputStream fileStream = new FileInputStream(ResourceUtils.getFile(path))) {
diff --git a/src/main/java/eu/europa/ec/dgc/generation/Base45Encoder.java b/src/main/java/eu/europa/ec/dgc/generation/Base45Encoder.java
new file mode 100644
index 0000000..70c7198
--- /dev/null
+++ b/src/main/java/eu/europa/ec/dgc/generation/Base45Encoder.java
@@ -0,0 +1,37 @@
+package eu.europa.ec.dgc.generation;
+
+/**
+ * Base45 Encoder.
+ */
+public class Base45Encoder {
+ private static final String ALPHABET = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ $%*+-./:";
+
+ private Base45Encoder() {
+ }
+
+ /**
+ * Encode a byte array to Base45 String.
+ *
+ * @param bytes bytes to encode
+ * @return encoded string
+ */
+ public static String encodeToString(byte[] bytes) {
+ StringBuilder result = new StringBuilder();
+ for (int i = 0; i < bytes.length; i += 2) {
+ if (bytes.length - i > 1) {
+ int x = ((bytes[i] & 0xFF) << 8) + (bytes[i + 1] & 0xFF);
+ int e = x / (45 * 45);
+ int y = x % (45 * 45);
+ int d = y / 45;
+ int c = y % 45;
+ result.append(ALPHABET.charAt(c)).append(ALPHABET.charAt(d)).append(ALPHABET.charAt(e));
+ } else {
+ int x = bytes[i] & 0xFF;
+ int d = x / 45;
+ int c = x % 45;
+ result.append(ALPHABET.charAt(c)).append(ALPHABET.charAt(d));
+ }
+ }
+ return result.toString();
+ }
+}
diff --git a/src/main/java/eu/europa/ec/dgc/generation/CopyDigest.java b/src/main/java/eu/europa/ec/dgc/generation/CopyDigest.java
new file mode 100644
index 0000000..a4c3bc8
--- /dev/null
+++ b/src/main/java/eu/europa/ec/dgc/generation/CopyDigest.java
@@ -0,0 +1,98 @@
+package eu.europa.ec.dgc.generation;
+
+import java.io.ByteArrayOutputStream;
+import org.bouncycastle.crypto.Digest;
+import org.bouncycastle.crypto.digests.SHA256Digest;
+import org.bouncycastle.util.Arrays;
+import org.springframework.stereotype.Service;
+
+@Service
+public class CopyDigest implements Digest {
+
+ private final OpenByteArrayOutputStream binaryOut = new OpenByteArrayOutputStream();
+ private final SHA256Digest sha256Digest = new SHA256Digest();
+ private boolean wasReset = false;
+
+ public String getAlgorithmName() {
+ return "NULL";
+ }
+
+ public int getDigestSize() {
+ return 32;
+ }
+
+ /**
+ * Updates the Message Digest with one byte.
+ *
+ * @param in byte to update.
+ */
+ public void update(byte in) {
+ if (wasReset) {
+ sha256Digest.update(in);
+ } else {
+ binaryOut.write(in);
+ }
+ }
+
+ /**
+ * Updates the Message Digest with a byte array.
+ *
+ * @param in byte array to insert
+ * @param offset Offset
+ * @param length length
+ */
+ public void update(byte[] in, int offset, int length) {
+ if (wasReset) {
+ sha256Digest.update(in, offset, length);
+ } else {
+ binaryOut.write(in, offset, length);
+ }
+ }
+
+ /**
+ * close the digest, producing the final digest value. The doFinal
+ * call leaves the digest reset.
+ *
+ * @param out the array the digest is to be copied into.
+ * @param offset the offset into the out array the digest is to start at.
+ * @return size of output
+ */
+ public int doFinal(byte[] out, int offset) {
+ if (wasReset) {
+ return sha256Digest.doFinal(out, offset);
+ } else {
+ int size = binaryOut.size();
+ binaryOut.copy(out, offset);
+ reset();
+ return size;
+ }
+ }
+
+ /**
+ * Resets the Message Digest.
+ */
+ @Override
+ public void reset() {
+ if (wasReset) {
+ sha256Digest.reset();
+ } else {
+ if (binaryOut.size() > 0) {
+ wasReset = true;
+ binaryOut.reset();
+ }
+ }
+ }
+
+ private static class OpenByteArrayOutputStream
+ extends ByteArrayOutputStream {
+ public synchronized void reset() {
+ super.reset();
+
+ Arrays.clear(buf);
+ }
+
+ void copy(byte[] out, int outOff) {
+ System.arraycopy(buf, 0, out, outOff, this.size());
+ }
+ }
+}
diff --git a/src/main/java/eu/europa/ec/dgc/generation/DccBuilderBase.java b/src/main/java/eu/europa/ec/dgc/generation/DccBuilderBase.java
new file mode 100644
index 0000000..d36da14
--- /dev/null
+++ b/src/main/java/eu/europa/ec/dgc/generation/DccBuilderBase.java
@@ -0,0 +1,191 @@
+package eu.europa.ec.dgc.generation;
+
+import com.fasterxml.jackson.databind.node.JsonNodeFactory;
+import com.fasterxml.jackson.databind.node.ObjectNode;
+import java.time.LocalDate;
+import java.time.LocalDateTime;
+import java.time.ZoneOffset;
+import java.time.format.DateTimeFormatter;
+import java.util.EnumSet;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+/**
+ * abstract builder for all certificate types.
+ * @param the concrete builder
+ */
+public abstract class DccBuilderBase> {
+
+ protected JsonNodeFactory jsonNodeFactory;
+ protected ObjectNode dccObject;
+ protected ObjectNode nameObject;
+
+ private static final Pattern countryPattern = Pattern.compile("[A-Z]{1,10}");
+ private static final Pattern standardNamePattern = Pattern.compile("^[A-Z<]*$");
+
+ private DateTimeFormatter dateFormat;
+ private DateTimeFormatter dayDateFormat;
+
+ private enum RequiredFieldsBase { dob, fnt, co, is, ci }
+
+ private EnumSet requiredNotSet = EnumSet.allOf(RequiredFieldsBase.class);
+
+ /**
+ * constructor.
+ */
+ public DccBuilderBase() {
+ dateFormat = DateTimeFormatter.ofPattern("yyyy-MM-dd'T'HH:mm:ssZ");
+ dayDateFormat = DateTimeFormatter.ofPattern("yyyy-MM-dd");
+
+ jsonNodeFactory = JsonNodeFactory.instance;
+ dccObject = jsonNodeFactory.objectNode();
+ nameObject = jsonNodeFactory.objectNode();
+
+ dccObject.set("version", jsonNodeFactory.textNode("1.0.0"));
+ dccObject.set("nam", nameObject);
+ }
+
+ public abstract T getThis();
+
+ public abstract ObjectNode getValueHolder();
+
+ /**
+ * family name field.
+ *
+ * @param fn family name
+ * @return builder
+ */
+ public T fn(String fn) {
+ assertNotNullMax("fn", fn, 50);
+ nameObject.set("fn", jsonNodeFactory.textNode(fn));
+ return getThis();
+ }
+
+ /**
+ * given name.
+ *
+ * @param gn given name
+ * @return builder
+ */
+ public T gn(String gn) {
+ assertNotNullMax("gn", gn, 50);
+ nameObject.set("gn", jsonNodeFactory.textNode(gn));
+ return getThis();
+ }
+
+ /**
+ * standardized family name.
+ *
+ * @param fnt standardized family name
+ * @return builder
+ */
+ public T fnt(String fnt) {
+ assertNotNullMaxPattern("fnt", fnt, 50, standardNamePattern);
+ requiredNotSet.remove(RequiredFieldsBase.fnt);
+ nameObject.set("fnt", jsonNodeFactory.textNode(fnt));
+ return getThis();
+ }
+
+ /**
+ * standarized given name.
+ *
+ * @param gnt standardized given name
+ * @return builder
+ */
+ public T gnt(String gnt) {
+ assertNotNullMaxPattern("gnt", gnt, 50, standardNamePattern);
+ nameObject.set("gnt", jsonNodeFactory.textNode(gnt));
+ return getThis();
+ }
+
+ protected void validate() {
+ if (!requiredNotSet.isEmpty()) {
+ throw new IllegalStateException("not all required fields set " + requiredNotSet);
+ }
+ }
+
+ /**
+ * buidl json string.
+ *
+ * @return json string
+ */
+ public String toJsonString() {
+ validate();
+ return dccObject.toString();
+ }
+
+ /**
+ * date of birth in iso format.
+ *
+ * @param birthday dob
+ * @return builder
+ */
+ public T dob(LocalDate birthday) {
+ dccObject.set("dob", jsonNodeFactory.textNode(toIsoDate(birthday)));
+ requiredNotSet.remove(RequiredFieldsBase.dob);
+ return getThis();
+ }
+
+ /**
+ * country of test.
+ * @param co co
+ * @return builder
+ */
+ public T country(String co) {
+ assertNotNullMaxPattern("co",co,0,countryPattern);
+ getValueHolder().set("co", jsonNodeFactory.textNode(co));
+ requiredNotSet.remove(RequiredFieldsBase.co);
+ return getThis();
+ }
+
+ /**
+ * test issuer.
+ * @param is issuer
+ * @return builder
+ */
+ public T certificateIssuer(String is) {
+ assertNotNullMax("is",is,50);
+ getValueHolder().set("is", jsonNodeFactory.textNode(is));
+ requiredNotSet.remove(RequiredFieldsBase.is);
+ return getThis();
+ }
+
+ /**
+ * certificate identifier.
+ * @param dgci certificate identifier
+ * @return builder
+ */
+ public T dgci(String dgci) {
+ assertNotNullMax("ci", dgci, 50);
+ getValueHolder().set("ci", jsonNodeFactory.textNode(dgci));
+ requiredNotSet.remove(RequiredFieldsBase.ci);
+ return getThis();
+ }
+
+ protected String toIsoO8601(LocalDateTime dateTime) {
+ return dateTime.atZone(ZoneOffset.UTC).format(dateFormat);
+ }
+
+ protected String toIsoDate(LocalDate date) {
+ return date.format(dayDateFormat);
+ }
+
+ protected void assertNotNullMax(String description, String value, int maxLenght) {
+ if (value == null) {
+ throw new IllegalArgumentException("field " + description + " must not be null");
+ }
+ if (maxLenght > 0 && value.length() > maxLenght) {
+ throw new IllegalArgumentException("field " + description + " has max length "
+ + maxLenght + " but was: " + value.length());
+ }
+ }
+
+ protected void assertNotNullMaxPattern(String description, String value, int maxLenght, Pattern pattern) {
+ assertNotNullMax(description, value, maxLenght);
+ Matcher matcher = pattern.matcher(value);
+ if (!matcher.matches()) {
+ throw new IllegalArgumentException("field: " + description + "value: "
+ + value + " do not match pattern: " + pattern);
+ }
+ }
+}
diff --git a/src/main/java/eu/europa/ec/dgc/generation/DccRecoveryBuilder.java b/src/main/java/eu/europa/ec/dgc/generation/DccRecoveryBuilder.java
new file mode 100644
index 0000000..d56a7ee
--- /dev/null
+++ b/src/main/java/eu/europa/ec/dgc/generation/DccRecoveryBuilder.java
@@ -0,0 +1,84 @@
+package eu.europa.ec.dgc.generation;
+
+import com.fasterxml.jackson.databind.node.ArrayNode;
+import com.fasterxml.jackson.databind.node.ObjectNode;
+import java.time.LocalDate;
+import java.time.LocalDateTime;
+import java.time.ZoneOffset;
+import java.time.format.DateTimeFormatter;
+import java.util.EnumSet;
+
+public class DccRecoveryBuilder extends DccBuilderBase {
+
+ private ObjectNode recoveryObject;
+
+ private enum RequiredFields { fr, df, du }
+
+ private EnumSet requiredNotSet = EnumSet.allOf(RequiredFields.class);
+
+ /**
+ * the constructor.
+ */
+ public DccRecoveryBuilder() {
+ super();
+
+ recoveryObject = jsonNodeFactory.objectNode();
+ ArrayNode vaccinationArray = jsonNodeFactory.arrayNode();
+ vaccinationArray.add(recoveryObject);
+ // disease-agent-targeted COVID-19
+ // see https://github.com/ehn-digital-green-development/ehn-dgc-schema/blob/main/valuesets/disease-agent-targeted.json
+ recoveryObject.set("tg", jsonNodeFactory.textNode("840539006"));
+ dccObject.set("r", vaccinationArray);
+ }
+
+ @Override
+ public DccRecoveryBuilder getThis() {
+ return this;
+ }
+
+ @Override
+ public ObjectNode getValueHolder() {
+ return recoveryObject;
+ }
+
+ protected void validate() {
+ super.validate();
+ if (!requiredNotSet.isEmpty()) {
+ throw new IllegalStateException("not all required fields set " + requiredNotSet);
+ }
+ }
+
+ /**
+ * first Day Positive Test.
+ * @param fr first Day Positive Test.
+ * @return builder
+ */
+ public DccRecoveryBuilder firstDayPositiveTest(LocalDate fr) {
+ recoveryObject.set("fr", jsonNodeFactory.textNode(toIsoDate(fr)));
+ requiredNotSet.remove(RequiredFields.fr);
+ return this;
+ }
+
+ /**
+ * certificate valid from.
+ * @param df valid from.
+ * @return builder
+ */
+ public DccRecoveryBuilder certificateValidFrom(LocalDate df) {
+ recoveryObject.set("df", jsonNodeFactory.textNode(toIsoDate(df)));
+ requiredNotSet.remove(RequiredFields.df);
+ return this;
+ }
+
+ /**
+ * certificate valid until.
+ * @param du valid until.
+ * @return builder
+ */
+ public DccRecoveryBuilder certificateValidUnitl(LocalDate du) {
+ recoveryObject.set("du", jsonNodeFactory.textNode(toIsoDate(du)));
+ requiredNotSet.remove(RequiredFields.du);
+ return this;
+ }
+
+}
diff --git a/src/main/java/eu/europa/ec/dgc/generation/DccTestBuilder.java b/src/main/java/eu/europa/ec/dgc/generation/DccTestBuilder.java
new file mode 100644
index 0000000..cdc1b2d
--- /dev/null
+++ b/src/main/java/eu/europa/ec/dgc/generation/DccTestBuilder.java
@@ -0,0 +1,105 @@
+package eu.europa.ec.dgc.generation;
+
+import com.fasterxml.jackson.databind.node.ArrayNode;
+import com.fasterxml.jackson.databind.node.JsonNodeFactory;
+import com.fasterxml.jackson.databind.node.ObjectNode;
+import java.time.LocalDateTime;
+import java.time.ZoneOffset;
+import java.time.format.DateTimeFormatter;
+import java.util.EnumSet;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+/**
+ * Builder for DCC Test Json.
+ */
+public class DccTestBuilder extends DccBuilderBase {
+ private ObjectNode testObject;
+
+ private enum RequiredFields { tt, sc, tr, tc }
+
+ private EnumSet requiredNotSet = EnumSet.allOf(RequiredFields.class);
+
+ /**
+ * the constructor.
+ */
+ public DccTestBuilder() {
+ super();
+
+ testObject = jsonNodeFactory.objectNode();
+ ArrayNode testArray = jsonNodeFactory.arrayNode();
+ testArray.add(testObject);
+ // disease-agent-targeted COVID-19
+ // see https://github.com/ehn-digital-green-development/ehn-dgc-schema/blob/main/valuesets/disease-agent-targeted.json
+ testObject.set("tg", jsonNodeFactory.textNode("840539006"));
+ dccObject.set("t", testArray);
+ }
+
+ @Override
+ public DccTestBuilder getThis() {
+ return this;
+ }
+
+ @Override
+ public ObjectNode getValueHolder() {
+ return testObject;
+ }
+
+ /**
+ * test result.
+ * @param covidDetected covid detected
+ * @return builder
+ */
+ public DccTestBuilder detected(boolean covidDetected) {
+ // https://github.com/ehn-digital-green-development/ehn-dgc-schema/blob/main/valuesets/test-result.json
+ testObject.set("tr", jsonNodeFactory.textNode(covidDetected ? "260373001" : "260415000"));
+ requiredNotSet.remove(RequiredFields.tr);
+ return this;
+ }
+
+ /**
+ * test type.
+ * @param isRapidTest true if rapid
+ * @return builder
+ */
+ public DccTestBuilder testTypeRapid(boolean isRapidTest) {
+ testObject.set("tt", jsonNodeFactory.textNode(isRapidTest ? "LP217198-3" : "LP6464-4"));
+ requiredNotSet.remove(RequiredFields.tt);
+ return this;
+ }
+
+
+
+ /**
+ * testing centre.
+ * @param tc testing centre
+ * @return builder
+ */
+ public DccTestBuilder testingCentre(String tc) {
+ testObject.set("tc", jsonNodeFactory.textNode(tc));
+ assertNotNullMax("tc",tc,50);
+ requiredNotSet.remove(RequiredFields.tc);
+ return this;
+ }
+
+ /**
+ * date time of sample collection.
+ * @param dateTime sc
+ * @return builder
+ */
+ public DccTestBuilder sampleCollection(LocalDateTime dateTime) {
+ testObject.set("sc", jsonNodeFactory.textNode(toIsoO8601(dateTime)));
+ requiredNotSet.remove(RequiredFields.sc);
+ return this;
+ }
+
+
+
+ protected void validate() {
+ super.validate();
+ if (!requiredNotSet.isEmpty()) {
+ throw new IllegalStateException("not all required fields set " + requiredNotSet);
+ }
+ }
+
+}
\ No newline at end of file
diff --git a/src/main/java/eu/europa/ec/dgc/generation/DccVaccinationBuilder.java b/src/main/java/eu/europa/ec/dgc/generation/DccVaccinationBuilder.java
new file mode 100644
index 0000000..91ad01f
--- /dev/null
+++ b/src/main/java/eu/europa/ec/dgc/generation/DccVaccinationBuilder.java
@@ -0,0 +1,123 @@
+package eu.europa.ec.dgc.generation;
+
+import com.fasterxml.jackson.databind.node.ArrayNode;
+import com.fasterxml.jackson.databind.node.ObjectNode;
+import java.time.LocalDateTime;
+import java.util.EnumSet;
+
+public class DccVaccinationBuilder extends DccBuilderBase {
+
+ private ObjectNode vaccinationObject;
+
+ private enum RequiredFields { vp, mp, ma, dn, sd, dt }
+
+ private EnumSet requiredNotSet = EnumSet.allOf(RequiredFields.class);
+
+ /**
+ * the constructor.
+ */
+ public DccVaccinationBuilder() {
+ super();
+
+ vaccinationObject = jsonNodeFactory.objectNode();
+ ArrayNode vaccinationArray = jsonNodeFactory.arrayNode();
+ vaccinationArray.add(vaccinationObject);
+ // disease-agent-targeted COVID-19
+ // see https://github.com/ehn-digital-green-development/ehn-dgc-schema/blob/main/valuesets/disease-agent-targeted.json
+ vaccinationObject.set("tg", jsonNodeFactory.textNode("840539006"));
+ dccObject.set("v", vaccinationArray);
+ }
+
+ @Override
+ public DccVaccinationBuilder getThis() {
+ return this;
+ }
+
+ @Override
+ public ObjectNode getValueHolder() {
+ return vaccinationObject;
+ }
+
+ protected void validate() {
+ super.validate();
+ if (!requiredNotSet.isEmpty()) {
+ throw new IllegalStateException("not all required fields set " + requiredNotSet);
+ }
+ }
+
+ /**
+ * vaccine Or Prophylaxis.
+ * @param vp vaccineOrProphylaxis
+ * @return builder
+ */
+ public DccVaccinationBuilder vaccineOrProphylaxis(String vp) {
+ /* TODO validate the vp or enum */
+ vaccinationObject.set("vp", jsonNodeFactory.textNode(vp));
+ requiredNotSet.remove(RequiredFields.vp);
+ return this;
+ }
+
+ /**
+ * medical product.
+ * @param mp medical product
+ * @return builder
+ */
+ public DccVaccinationBuilder medicalProduct(String mp) {
+ /* TODO validate the mp or enum */
+ vaccinationObject.set("mp", jsonNodeFactory.textNode(mp));
+ requiredNotSet.remove(RequiredFields.mp);
+ return this;
+ }
+
+ /**
+ * marketing Authorization.
+ * @param ma marketingAuthorization
+ * @return builder
+ */
+ public DccVaccinationBuilder marketingAuthorization(String ma) {
+ /* TODO validate the ma or enum */
+ vaccinationObject.set("ma", jsonNodeFactory.textNode(ma));
+ requiredNotSet.remove(RequiredFields.ma);
+ return this;
+ }
+
+ /**
+ * dose number.
+ * @param dn dose number
+ * @return builder
+ */
+ public DccVaccinationBuilder doseNumber(int dn) {
+ if (dn < 1 || dn > 9) {
+ throw new IllegalArgumentException("invalid range of dn (1-9)");
+ }
+ vaccinationObject.set("dn", jsonNodeFactory.numberNode(dn));
+ requiredNotSet.remove(RequiredFields.dn);
+ return this;
+ }
+
+ /**
+ * total series of doses.
+ * @param sd total series of doses
+ * @return builder
+ */
+ public DccVaccinationBuilder totalSeriesOfDoses(int sd) {
+ if (sd < 1 || sd > 9) {
+ throw new IllegalArgumentException("invalid range of dn (1-9)");
+ }
+ vaccinationObject.set("sd", jsonNodeFactory.numberNode(sd));
+ requiredNotSet.remove(RequiredFields.sd);
+ return this;
+ }
+
+ /**
+ * date time of vaccination.
+ * @param dateTime sc
+ * @return builder
+ */
+ public DccVaccinationBuilder dateOfVaccination(LocalDateTime dateTime) {
+ vaccinationObject.set("dt", jsonNodeFactory.textNode(toIsoO8601(dateTime)));
+ requiredNotSet.remove(RequiredFields.dt);
+ return this;
+ }
+
+}
diff --git a/src/main/java/eu/europa/ec/dgc/generation/DgcCryptedFinalizer.java b/src/main/java/eu/europa/ec/dgc/generation/DgcCryptedFinalizer.java
new file mode 100644
index 0000000..191ef7a
--- /dev/null
+++ b/src/main/java/eu/europa/ec/dgc/generation/DgcCryptedFinalizer.java
@@ -0,0 +1,83 @@
+package eu.europa.ec.dgc.generation;
+
+import java.security.GeneralSecurityException;
+import java.security.InvalidAlgorithmParameterException;
+import java.security.InvalidKeyException;
+import java.security.NoSuchAlgorithmException;
+import java.security.PrivateKey;
+import java.security.spec.InvalidKeySpecException;
+import javax.crypto.BadPaddingException;
+import javax.crypto.Cipher;
+import javax.crypto.IllegalBlockSizeException;
+import javax.crypto.NoSuchPaddingException;
+import javax.crypto.spec.IvParameterSpec;
+import javax.crypto.spec.SecretKeySpec;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.stereotype.Service;
+
+/**
+ * build final dcc qr code from encode dcc data and signature.
+ */
+@Service
+@Slf4j
+public class DgcCryptedFinalizer {
+
+ /**
+ * finalize dcc.
+ *
+ * @param encodedDccData dcc data
+ * @param dek encoded key
+ * @param privateKey private key
+ * @param signature dcc signature
+ * @return qr code of final dcc
+ */
+ public String finalizeDcc(byte[] encodedDccData, byte[] dek, PrivateKey privateKey, byte[] signature) {
+ DgcGenerator dgcGenerator = new DgcGenerator();
+ byte[] dgcData = new byte[0];
+ try {
+ dgcData = decryptDccData(encodedDccData, dek, privateKey);
+ } catch (GeneralSecurityException e) {
+ log.error("Failed to finalize DCC: {}", e.getMessage());
+ throw new IllegalStateException("can not decrypt dcc data");
+ }
+ byte[] dgcCose = dgcGenerator.dgcSetCoseSignature(dgcData, signature);
+ return dgcGenerator.coseToQrCode(dgcCose);
+ }
+
+ /**
+ * finalize dcc.
+ * @param encodedDccData dcc data
+ * @param dek encoded key
+ * @param privateKey private key
+ * @param partialDcc cose with signature and key
+ * @return qr code of final dcc
+ */
+ public String finalizePartialDcc(byte[] encodedDccData, byte[] dek, PrivateKey privateKey, byte[] partialDcc) {
+ DgcGenerator dgcGenerator = new DgcGenerator();
+ byte[] dgcData = new byte[0];
+ try {
+ dgcData = decryptDccData(encodedDccData, dek, privateKey);
+ } catch (GeneralSecurityException e) {
+ log.error("Failed to finalize DCC: {}", e.getMessage());
+ throw new IllegalStateException("can not decrypt dcc data");
+ }
+ byte[] dgcCose = dgcGenerator.dgcSetCosePartial(dgcData, partialDcc);
+ return dgcGenerator.coseToQrCode(dgcCose);
+ }
+
+ private byte[] decryptDccData(byte[] encodedDccData, byte[] dek, PrivateKey privateKey)
+ throws java.security.GeneralSecurityException {
+ // decrypt RSA key
+ Cipher keyCipher = Cipher.getInstance(DgcCryptedPublisher.KEY_CIPHER);
+ keyCipher.init(Cipher.DECRYPT_MODE, privateKey);
+ byte[] rsaKey = keyCipher.doFinal(dek);
+
+ byte[] iv = {0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0};
+ IvParameterSpec ivspec = new IvParameterSpec(iv);
+ Cipher cipher = Cipher.getInstance(DgcCryptedPublisher.DATA_CIPHER);
+
+ SecretKeySpec secretKeySpec = new SecretKeySpec(rsaKey, 0, rsaKey.length, "AES");
+ cipher.init(Cipher.DECRYPT_MODE, secretKeySpec, ivspec);
+ return cipher.doFinal(encodedDccData);
+ }
+}
diff --git a/src/main/java/eu/europa/ec/dgc/generation/DgcCryptedPublisher.java b/src/main/java/eu/europa/ec/dgc/generation/DgcCryptedPublisher.java
new file mode 100644
index 0000000..46e6a22
--- /dev/null
+++ b/src/main/java/eu/europa/ec/dgc/generation/DgcCryptedPublisher.java
@@ -0,0 +1,78 @@
+package eu.europa.ec.dgc.generation;
+
+import eu.europa.ec.dgc.generation.dto.DgcData;
+import eu.europa.ec.dgc.generation.dto.DgcInitData;
+import java.security.InvalidAlgorithmParameterException;
+import java.security.InvalidKeyException;
+import java.security.NoSuchAlgorithmException;
+import java.security.PublicKey;
+import javax.crypto.BadPaddingException;
+import javax.crypto.Cipher;
+import javax.crypto.IllegalBlockSizeException;
+import javax.crypto.KeyGenerator;
+import javax.crypto.NoSuchPaddingException;
+import javax.crypto.SecretKey;
+import javax.crypto.spec.IvParameterSpec;
+import org.springframework.stereotype.Service;
+
+@Service
+public class DgcCryptedPublisher {
+
+ public static final String KEY_CIPHER = "RSA/ECB/PKCS1Padding";
+ public static final String DATA_CIPHER = "AES/CBC/PKCS5Padding";
+
+ private final DgcGenerator dgcGenerator = new DgcGenerator();
+
+ /**
+ * Create dgc data.
+ *
+ * @param dgcInitData init params
+ * @param dgcPayloadJson dcc payload
+ * @param publicKey pubic key
+ * @return data
+ */
+ public DgcData createDgc(DgcInitData dgcInitData, String dgcPayloadJson, PublicKey publicKey) {
+ byte[] edgcCbor = dgcGenerator.genDgcCbor(dgcPayloadJson, dgcInitData.getIssuerCode(),
+ dgcInitData.getIssuedAt(), dgcInitData.getExpriation());
+ byte[] edgcCoseUnsigned =
+ dgcGenerator.genCoseUnsigned(edgcCbor, dgcInitData.getKeyId(), dgcInitData.getAlgId());
+ byte[] edgcHash = dgcGenerator.computeCoseSignHash(edgcCoseUnsigned);
+
+ DgcData dgcData = new DgcData();
+ dgcData.setHash(edgcHash);
+ dgcData.setDccData(edgcCoseUnsigned);
+
+ try {
+ encryptData(dgcData, edgcCoseUnsigned, publicKey);
+ } catch (NoSuchAlgorithmException | NoSuchPaddingException
+ | InvalidKeyException | InvalidAlgorithmParameterException
+ | IllegalBlockSizeException | BadPaddingException e) {
+ throw new IllegalArgumentException(e);
+ }
+
+ return dgcData;
+ }
+
+ private void encryptData(DgcData dgcData, byte[] edgcCoseUnsigned, PublicKey publicKey) throws
+ NoSuchAlgorithmException, NoSuchPaddingException, InvalidKeyException,
+ InvalidAlgorithmParameterException, IllegalBlockSizeException, BadPaddingException {
+ KeyGenerator keyGen = KeyGenerator.getInstance("AES");
+ keyGen.init(256); // for example
+ SecretKey secretKey = keyGen.generateKey();
+
+ // TODO set iv to something special
+ byte[] iv = {0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0};
+ IvParameterSpec ivspec = new IvParameterSpec(iv);
+ Cipher cipher = Cipher.getInstance(DATA_CIPHER);
+ cipher.init(Cipher.ENCRYPT_MODE, secretKey, ivspec);
+ byte[] edgcDataEncrpyted = cipher.doFinal(edgcCoseUnsigned);
+
+ dgcData.setDataEncrypted(edgcDataEncrpyted);
+
+ // encrypt RSA key
+ Cipher keyCipher = Cipher.getInstance(KEY_CIPHER);
+ keyCipher.init(Cipher.ENCRYPT_MODE, publicKey);
+ byte[] secretKeyBytes = secretKey.getEncoded();
+ dgcData.setDek(keyCipher.doFinal(secretKeyBytes));
+ }
+}
diff --git a/src/main/java/eu/europa/ec/dgc/generation/DgcGenerator.java b/src/main/java/eu/europa/ec/dgc/generation/DgcGenerator.java
new file mode 100644
index 0000000..0241322
--- /dev/null
+++ b/src/main/java/eu/europa/ec/dgc/generation/DgcGenerator.java
@@ -0,0 +1,165 @@
+package eu.europa.ec.dgc.generation;
+
+import com.upokecenter.cbor.CBORObject;
+import com.upokecenter.cbor.CBORType;
+import java.io.ByteArrayInputStream;
+import java.io.IOException;
+import java.security.MessageDigest;
+import java.security.NoSuchAlgorithmException;
+import java.util.zip.Deflater;
+import java.util.zip.DeflaterInputStream;
+import org.springframework.stereotype.Service;
+
+/**
+ * Dgc Generator util.
+ * It can generate EDGC cose structure, hash for signing,
+ * copy the signature into cose and also generate final QR Code as string.
+ */
+@Service
+public class DgcGenerator {
+ /**
+ * Generate CBOR EDGC payload for COSE.
+ * The json string need to be valid according to specification
+ * https://github.com/ehn-digital-green-development/hcert-spec
+ * no json structure validation is done here
+ *
+ * @param edgcJson edgc payload as json string
+ * @param countryCode for example DE
+ * @param issuedAt unix time in sec
+ * @param expirationSec unix time in sec
+ * @return CBOR bytes
+ */
+ public byte[] genDgcCbor(String edgcJson, String countryCode, long issuedAt, long expirationSec) {
+ CBORObject map = CBORObject.NewMap();
+ if (countryCode == null || countryCode.length() == 0) {
+ throw new IllegalArgumentException("dcc issuer is null or empty");
+ }
+ map.set(CBORObject.FromObject(1), CBORObject.FromObject(countryCode));
+ map.set(CBORObject.FromObject(6), CBORObject.FromObject(issuedAt));
+ map.set(CBORObject.FromObject(4), CBORObject.FromObject(expirationSec));
+ CBORObject hcertVersion = CBORObject.NewMap();
+ CBORObject hcert = CBORObject.FromJSONString(edgcJson);
+ hcertVersion.set(CBORObject.FromObject(1), hcert);
+ map.set(CBORObject.FromObject(-260), hcertVersion);
+ return map.EncodeToBytes();
+ }
+
+ /**
+ * Generate COSE unsigned structure with payload.
+ *
+ * @param payload it should be CBOR data
+ * @param keyId for protected data or null if not to set
+ * @param algId for protected data for example -7 (means EC) or 0 if not to set
+ * @return cose bytes
+ */
+ public byte[] genCoseUnsigned(byte[] payload, byte[] keyId, int algId) {
+ CBORObject protectedHeader = CBORObject.NewMap();
+ if (algId != 0) {
+ protectedHeader.set(CBORObject.FromObject(1), CBORObject.FromObject(algId));
+ }
+ if (keyId != null) {
+ protectedHeader.set(CBORObject.FromObject(4), CBORObject.FromObject(keyId));
+ }
+ byte[] protectedHeaderBytes = protectedHeader.EncodeToBytes();
+
+ CBORObject coseObject = CBORObject.NewArray();
+ coseObject.Add(protectedHeaderBytes);
+ CBORObject unprotectedHeader = CBORObject.NewMap();
+ coseObject.Add(unprotectedHeader);
+ coseObject.Add(CBORObject.FromObject(payload));
+ byte[] sigDummy = new byte[0];
+ coseObject.Add(CBORObject.FromObject(sigDummy));
+ return CBORObject.FromObjectAndTag(coseObject, 18).EncodeToBytes();
+ }
+
+ /**
+ * Set Signature in COSE data.
+ *
+ * @param coseData bytes
+ * @param signature cose signature
+ * @return cose bytes with signature
+ */
+ public byte[] dgcSetCoseSignature(byte[] coseData, byte[] signature) {
+ CBORObject cborObject = CBORObject.DecodeFromBytes(coseData);
+ if (cborObject.getType() == CBORType.Array && cborObject.getValues().size() == 4) {
+ cborObject.set(3, CBORObject.FromObject(signature));
+ } else {
+ throw new IllegalArgumentException("seems not to be cose");
+ }
+ return cborObject.EncodeToBytes();
+ }
+
+ /**
+ * Set signature and unprotected header from partialDcc into unsigned cose dcc.
+ *
+ * @param coseData unsigned cose dcc
+ * @param partialDcc cose with signature and unprotected header
+ * @return signed cose dcc
+ */
+ public byte[] dgcSetCosePartial(byte[] coseData, byte[] partialDcc) {
+ CBORObject partialCose = CBORObject.DecodeFromBytes(partialDcc);
+ if (partialCose.getType() != CBORType.Array || partialCose.getValues().size() < 3) {
+ throw new IllegalArgumentException("partial dcc is not cbor array");
+ }
+ CBORObject cborObject = CBORObject.DecodeFromBytes(coseData);
+ if (cborObject.getType() == CBORType.Array && cborObject.getValues().size() == 4) {
+ // set signature
+ cborObject.set(3, partialCose.get(3));
+ } else {
+ throw new IllegalArgumentException("seems not to be cose");
+ }
+ // copy unprotected header
+ CBORObject unprotectedHeader = partialCose.get(1);
+ if (unprotectedHeader.getType() != CBORType.Map) {
+ throw new IllegalArgumentException("unprotected header in partial dcc is not cbor map");
+ }
+ for (CBORObject key : unprotectedHeader.getKeys()) {
+ CBORObject value = unprotectedHeader.get(key);
+ cborObject.get(1).set(key, value);
+ }
+ return cborObject.EncodeToBytes();
+ }
+
+ /**
+ * convert cose bytes to qr code data.
+ *
+ * @param cose signed edgc data
+ * @return qr code data
+ */
+ public String coseToQrCode(byte[] cose) {
+ ByteArrayInputStream bis = new ByteArrayInputStream(cose);
+ DeflaterInputStream compessedInput = new DeflaterInputStream(bis, new Deflater(9));
+ byte[] coseCompressed;
+ try {
+ coseCompressed = compessedInput.readAllBytes();
+ } catch (IOException e) {
+ throw new IllegalArgumentException(e);
+ }
+ String coded = Base45Encoder.encodeToString(coseCompressed);
+ return "HC1:" + coded;
+ }
+
+ /**
+ * comute hash from cose data.
+ *
+ * @param coseMessage cose message
+ * @return hash bytes
+ */
+ public byte[] computeCoseSignHash(byte[] coseMessage) {
+ try {
+ CBORObject coseForSign = CBORObject.NewArray();
+ CBORObject cborCose = CBORObject.DecodeFromBytes(coseMessage);
+ if (cborCose.getType() == CBORType.Array) {
+ coseForSign.Add(CBORObject.FromObject("Signature1"));
+ coseForSign.Add(cborCose.get(0).GetByteString());
+ coseForSign.Add(new byte[0]);
+ coseForSign.Add(cborCose.get(2).GetByteString());
+ }
+ byte[] coseForSignBytes = coseForSign.EncodeToBytes();
+ MessageDigest digest = MessageDigest.getInstance("SHA-256");
+ return digest.digest(coseForSignBytes);
+ } catch (NoSuchAlgorithmException e) {
+ throw new RuntimeException(e);
+ }
+ }
+}
diff --git a/src/main/java/eu/europa/ec/dgc/generation/DgcSigner.java b/src/main/java/eu/europa/ec/dgc/generation/DgcSigner.java
new file mode 100644
index 0000000..044e5f0
--- /dev/null
+++ b/src/main/java/eu/europa/ec/dgc/generation/DgcSigner.java
@@ -0,0 +1,130 @@
+package eu.europa.ec.dgc.generation;
+
+import com.upokecenter.cbor.CBORObject;
+import java.math.BigInteger;
+import java.security.MessageDigest;
+import java.security.NoSuchAlgorithmException;
+import java.security.PrivateKey;
+import java.security.cert.Certificate;
+import java.security.cert.CertificateEncodingException;
+import java.security.interfaces.RSAPrivateCrtKey;
+import java.util.Arrays;
+import org.bouncycastle.crypto.CryptoException;
+import org.bouncycastle.crypto.Digest;
+import org.bouncycastle.crypto.digests.SHA256Digest;
+import org.bouncycastle.crypto.engines.RSABlindedEngine;
+import org.bouncycastle.crypto.params.ECDomainParameters;
+import org.bouncycastle.crypto.params.ECPrivateKeyParameters;
+import org.bouncycastle.crypto.params.RSAPrivateCrtKeyParameters;
+import org.bouncycastle.crypto.signers.ECDSASigner;
+import org.bouncycastle.crypto.signers.PSSSigner;
+import org.bouncycastle.jcajce.provider.asymmetric.util.EC5Util;
+import org.bouncycastle.jce.spec.ECParameterSpec;
+import org.springframework.stereotype.Service;
+
+/**
+ * The signer of cose message. It takes only hash as imput and does not need
+ * to know payload of cose data
+ */
+@Service
+public class DgcSigner {
+
+ /**
+ * sign hash. (encode hash) to signature.
+ *
+ * @param hashBytes computed cose hash
+ * @param privateKey can be EC or RSS
+ * @return signature
+ */
+ public byte[] signHash(byte[] hashBytes, PrivateKey privateKey) {
+ byte[] signature;
+ try {
+ if (privateKey instanceof RSAPrivateCrtKey) {
+ signature = signRsapss(hashBytes, privateKey);
+ } else {
+ signature = signEc(hashBytes, privateKey);
+ }
+ } catch (CryptoException e) {
+ throw new IllegalArgumentException("error during signing ", e);
+ }
+ return signature;
+ }
+
+ /**
+ * sign hash and build partial cose dcc containing key in unprotected header and signature.
+ * This variant can be user together with @link {@link DgcGenerator#dgcSetCosePartial}.
+ * @param hashBytes hash to sign
+ * @param privateKey private key
+ * @param keyId keyId bytes
+ * @return cose container but only with signature and unprotected header with keyId
+ */
+ public byte[] signPartialDcc(byte[] hashBytes, PrivateKey privateKey, byte[] keyId) {
+ CBORObject protectedHeader = CBORObject.NewMap();
+ byte[] protectedHeaderBytes = protectedHeader.EncodeToBytes();
+
+ CBORObject coseObject = CBORObject.NewArray();
+ coseObject.Add(protectedHeaderBytes);
+ CBORObject unprotectedHeader = CBORObject.NewMap();
+ unprotectedHeader.Add(CBORObject.FromObject(4),CBORObject.FromObject(keyId));
+ coseObject.Add(unprotectedHeader);
+ byte[] contentDummy = new byte[0];
+ coseObject.Add(CBORObject.FromObject(contentDummy));
+
+ byte[] signature = signHash(hashBytes, privateKey);
+ coseObject.Add(CBORObject.FromObject(signature));
+ return CBORObject.FromObjectAndTag(coseObject, 18).EncodeToBytes();
+ }
+
+ /**
+ * keyId needed for cose header data.
+ *
+ * @param certificate certificate
+ * @return keyId bytes
+ */
+ public byte[] keyId(Certificate certificate) {
+ try {
+ byte[] encoderCert = certificate.getEncoded();
+ byte[] hash = MessageDigest.getInstance("SHA-256").digest(encoderCert);
+ return Arrays.copyOfRange(hash, 0, 8);
+ } catch (CertificateEncodingException | NoSuchAlgorithmException e) {
+ throw new IllegalArgumentException("can not gen keyid", e);
+ }
+ }
+
+ private byte[] signRsapss(byte[] hashBytes, PrivateKey privateKey) throws CryptoException {
+ Digest contentDigest = new CopyDigest();
+ Digest mgfDigest = new SHA256Digest();
+ RSAPrivateCrtKey k = (RSAPrivateCrtKey) privateKey;
+ RSAPrivateCrtKeyParameters keyparam = new RSAPrivateCrtKeyParameters(k.getModulus(),
+ k.getPublicExponent(), k.getPrivateExponent(),
+ k.getPrimeP(), k.getPrimeQ(), k.getPrimeExponentP(), k.getPrimeExponentQ(), k.getCrtCoefficient());
+ RSABlindedEngine rsaBlindedEngine = new RSABlindedEngine();
+ rsaBlindedEngine.init(true, keyparam);
+ PSSSigner pssSigner = new PSSSigner(rsaBlindedEngine, contentDigest, mgfDigest, 32, (byte) (-68));
+ pssSigner.init(true, keyparam);
+ pssSigner.update(hashBytes, 0, hashBytes.length);
+ return pssSigner.generateSignature();
+ }
+
+ private byte[] signEc(byte[] hash, PrivateKey privateKey) {
+ java.security.interfaces.ECPrivateKey privKey = (java.security.interfaces.ECPrivateKey) privateKey;
+ ECParameterSpec s = EC5Util.convertSpec(privKey.getParams());
+ ECPrivateKeyParameters keyparam = new ECPrivateKeyParameters(
+ privKey.getS(),
+ new ECDomainParameters(s.getCurve(), s.getG(), s.getN(), s.getH(), s.getSeed()));
+ ECDSASigner ecdsaSigner = new ECDSASigner();
+ ecdsaSigner.init(true, keyparam);
+ BigInteger[] result3BI = ecdsaSigner.generateSignature(hash);
+ byte[] rvarArr = result3BI[0].toByteArray();
+ byte[] svarArr = result3BI[1].toByteArray();
+ // we need to convert it to 2*32 bytes array. This can 33 with leading 0 or shorter so padding is needed
+ byte[] sig = new byte[64];
+ System.arraycopy(rvarArr, rvarArr.length == 33 ? 1 : 0, sig,
+ Math.max(0, 32 - rvarArr.length), Math.min(32, rvarArr.length));
+ System.arraycopy(svarArr, svarArr.length == 33 ? 1 : 0, sig,
+ 32 + Math.max(0, 32 - svarArr.length), Math.min(32, svarArr.length));
+
+ return sig;
+ }
+
+}
diff --git a/src/main/java/eu/europa/ec/dgc/generation/DgciGenerator.java b/src/main/java/eu/europa/ec/dgc/generation/DgciGenerator.java
new file mode 100644
index 0000000..fc9b3c9
--- /dev/null
+++ b/src/main/java/eu/europa/ec/dgc/generation/DgciGenerator.java
@@ -0,0 +1,93 @@
+package eu.europa.ec.dgc.generation;
+
+import java.math.BigInteger;
+import java.nio.ByteBuffer;
+import java.util.UUID;
+
+public class DgciGenerator {
+
+ private static final String CODE_POINTS = "ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789/:";
+ private final String dgciPrefix;
+
+ public DgciGenerator(String dgciPrefix) {
+ checkPrefix(dgciPrefix);
+ this.dgciPrefix = dgciPrefix;
+ }
+
+ /**
+ * Check if dgci prefix contains character suitable for checksum calculation.
+ */
+ public void checkPrefix(String dgciPrefix) {
+ if (dgciPrefix != null) {
+ for (int i = 0; i < dgciPrefix.length(); i++) {
+ if (CODE_POINTS.indexOf(dgciPrefix.charAt(i)) < 0) {
+ throw new IllegalArgumentException("configured DGCI prefix '"
+ + dgciPrefix + "' contains invalid character '"
+ + dgciPrefix.charAt(i) + "' only following are supported " + CODE_POINTS);
+ }
+ }
+ }
+ }
+
+ /**
+ * Generates a new DGCI.
+ *
+ * @return DGCI as String
+ */
+ public String newDgci() {
+ StringBuilder sb = new StringBuilder();
+ sb.append(dgciPrefix).append(':');
+ sb.append(encodeDgci(UUID.randomUUID()));
+ sb.append(generateCheckCharacter(sb.toString()));
+ return sb.toString();
+ }
+
+ private String encodeDgci(UUID uuid) {
+ ByteBuffer bb = ByteBuffer.wrap(new byte[16]);
+ bb.putLong(uuid.getMostSignificantBits());
+ bb.putLong(uuid.getLeastSignificantBits());
+ BigInteger bint = new BigInteger(1, bb.array());
+ int radix = 10 + ('Z' - 'A');
+ return bint.toString(radix).toUpperCase();
+ }
+
+ // see https://en.wikipedia.org/wiki/Luhn_mod_N_algorithm
+ private char generateCheckCharacter(String input) {
+ int factor = 2;
+ int sum = 0;
+ int n = CODE_POINTS.length();
+
+ // Starting from the right and working leftwards is easier since
+ // the initial "factor" will always be "2".
+ for (int i = input.length() - 1; i >= 0; i--) {
+ int codePoint = codePointFromCharacter(input.charAt(i));
+ int addend = factor * codePoint;
+
+ // Alternate the "factor" that each "codePoint" is multiplied by
+ factor = (factor == 2) ? 1 : 2;
+
+ // Sum the digits of the "addend" as expressed in base "n"
+ addend = (addend / n) + (addend % n);
+ sum += addend;
+ }
+
+ // Calculate the number that must be added to the "sum"
+ // to make it divisible by "n".
+ int remainder = sum % n;
+ int checkCodePoint = (n - remainder) % n;
+
+ return characterFromCodePoint(checkCodePoint);
+ }
+
+ private char characterFromCodePoint(int checkCodePoint) {
+ return CODE_POINTS.charAt(checkCodePoint);
+ }
+
+ private int codePointFromCharacter(char charAt) {
+ int codePoint = CODE_POINTS.indexOf(charAt);
+ if (codePoint < 0) {
+ throw new IllegalArgumentException("unsupported character for checksum: " + charAt);
+ }
+ return codePoint;
+ }
+}
diff --git a/src/main/java/eu/europa/ec/dgc/generation/dto/DgcData.java b/src/main/java/eu/europa/ec/dgc/generation/dto/DgcData.java
new file mode 100644
index 0000000..8d37800
--- /dev/null
+++ b/src/main/java/eu/europa/ec/dgc/generation/dto/DgcData.java
@@ -0,0 +1,41 @@
+package eu.europa.ec.dgc.generation.dto;
+
+public class DgcData {
+
+ private byte[] dek;
+ private byte[] dataEncrypted;
+ private byte[] hash;
+ private byte[] dccData;
+
+ public byte[] getDek() {
+ return dek;
+ }
+
+ public void setDek(byte[] dek) {
+ this.dek = dek;
+ }
+
+ public byte[] getDataEncrypted() {
+ return dataEncrypted;
+ }
+
+ public void setDataEncrypted(byte[] dataEncrypted) {
+ this.dataEncrypted = dataEncrypted;
+ }
+
+ public byte[] getHash() {
+ return hash;
+ }
+
+ public void setHash(byte[] hash) {
+ this.hash = hash;
+ }
+
+ public byte[] getDccData() {
+ return dccData;
+ }
+
+ public void setDccData(byte[] dccData) {
+ this.dccData = dccData;
+ }
+}
diff --git a/src/main/java/eu/europa/ec/dgc/generation/dto/DgcInitData.java b/src/main/java/eu/europa/ec/dgc/generation/dto/DgcInitData.java
new file mode 100644
index 0000000..20944a7
--- /dev/null
+++ b/src/main/java/eu/europa/ec/dgc/generation/dto/DgcInitData.java
@@ -0,0 +1,50 @@
+package eu.europa.ec.dgc.generation.dto;
+
+public class DgcInitData {
+
+ private String issuerCode;
+ private long issuedAt;
+ private long expriation;
+ private int algId;
+ private byte[] keyId;
+
+ public String getIssuerCode() {
+ return issuerCode;
+ }
+
+ public void setIssuerCode(String issuerCode) {
+ this.issuerCode = issuerCode;
+ }
+
+ public long getIssuedAt() {
+ return issuedAt;
+ }
+
+ public void setIssuedAt(long issuedAt) {
+ this.issuedAt = issuedAt;
+ }
+
+ public long getExpriation() {
+ return expriation;
+ }
+
+ public void setExpriation(long expriation) {
+ this.expriation = expriation;
+ }
+
+ public int getAlgId() {
+ return algId;
+ }
+
+ public void setAlgId(int algId) {
+ this.algId = algId;
+ }
+
+ public byte[] getKeyId() {
+ return keyId;
+ }
+
+ public void setKeyId(byte[] keyId) {
+ this.keyId = keyId;
+ }
+}
diff --git a/src/main/resources/dgcg_tst_digit.p12 b/src/main/resources/dgcg_tst_digit.p12
new file mode 100644
index 0000000..4555b0e
Binary files /dev/null and b/src/main/resources/dgcg_tst_digit.p12 differ
diff --git a/src/test/java/eu/europa/ec/dgc/gateway/connector/UploadConnectorTest.java b/src/test/java/eu/europa/ec/dgc/gateway/connector/UploadConnectorTest.java
index c22f46a..53e30b9 100644
--- a/src/test/java/eu/europa/ec/dgc/gateway/connector/UploadConnectorTest.java
+++ b/src/test/java/eu/europa/ec/dgc/gateway/connector/UploadConnectorTest.java
@@ -28,7 +28,6 @@
import feign.FeignException;
import feign.Request;
import feign.RequestTemplate;
-import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.security.KeyPair;
import java.security.KeyPairGenerator;
diff --git a/src/test/java/eu/europa/ec/dgc/generation/Base45EncoderTest.java b/src/test/java/eu/europa/ec/dgc/generation/Base45EncoderTest.java
new file mode 100644
index 0000000..801f68a
--- /dev/null
+++ b/src/test/java/eu/europa/ec/dgc/generation/Base45EncoderTest.java
@@ -0,0 +1,16 @@
+package eu.europa.ec.dgc.generation;
+
+import java.nio.charset.StandardCharsets;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import org.junit.jupiter.api.Test;
+
+class Base45EncoderTest {
+
+ @Test
+ void encodeTest() {
+ assertEquals("7WE QE",Base45Encoder.encodeToString("test".getBytes(StandardCharsets.UTF_8)));
+
+ byte[] bytes = new byte[] { 0, 2, -2, 30, -12, 23, -23, -40};
+ assertEquals("200T5WR%UEPT",Base45Encoder.encodeToString(bytes));
+ }
+}
\ No newline at end of file
diff --git a/src/test/java/eu/europa/ec/dgc/generation/DccRecoveryBuilderTest.java b/src/test/java/eu/europa/ec/dgc/generation/DccRecoveryBuilderTest.java
new file mode 100644
index 0000000..f9e0927
--- /dev/null
+++ b/src/test/java/eu/europa/ec/dgc/generation/DccRecoveryBuilderTest.java
@@ -0,0 +1,24 @@
+package eu.europa.ec.dgc.generation;
+
+import java.time.LocalDate;
+import java.time.LocalDateTime;
+import org.junit.jupiter.api.Test;
+
+import static org.junit.jupiter.api.Assertions.*;
+
+class DccRecoveryBuilderTest {
+ @Test
+ void genTestJson() {
+ DccRecoveryBuilder dccRecoveryBuilder = new DccRecoveryBuilder();
+ dccRecoveryBuilder.gn("Artur").fn("Trzewik").gnt("ARTUR").fnt("TRZEWIK").dob(LocalDate.parse("1973-01-01"));
+ dccRecoveryBuilder.dgci("URN:UVCI:01:OS:B5921A35D6A0D696421B3E2462178297I")
+ .country("DE")
+ .certificateIssuer("Dr Who")
+ .firstDayPositiveTest(LocalDate.now())
+ .certificateValidFrom(LocalDate.now())
+ .certificateValidUnitl(LocalDate.now());
+ String jsonString = dccRecoveryBuilder.toJsonString();
+ assertNotNull(jsonString);
+ System.out.println(jsonString);
+ }
+}
\ No newline at end of file
diff --git a/src/test/java/eu/europa/ec/dgc/generation/DccTestBuilderTest.java b/src/test/java/eu/europa/ec/dgc/generation/DccTestBuilderTest.java
new file mode 100644
index 0000000..03c4492
--- /dev/null
+++ b/src/test/java/eu/europa/ec/dgc/generation/DccTestBuilderTest.java
@@ -0,0 +1,44 @@
+package eu.europa.ec.dgc.generation;
+
+import java.time.LocalDate;
+import java.time.LocalDateTime;
+import org.junit.jupiter.api.Assertions;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+import org.junit.jupiter.api.Test;
+
+class DccTestBuilderTest {
+ @Test
+ void requiredFieldsFormat() throws Exception {
+ DccTestBuilder dccTestBuilder = new DccTestBuilder();
+ dccTestBuilder.fn("Tester");
+ dccTestBuilder.fnt("TESTER");
+ Assertions.assertThrows(IllegalStateException.class, () -> {
+ dccTestBuilder.toJsonString();
+ });
+ }
+
+ @Test
+ void patternMatch() {
+ DccTestBuilder dccTestBuilder = new DccTestBuilder();
+ Assertions.assertThrows(IllegalArgumentException.class, () -> {
+ dccTestBuilder.fnt("tester");
+ });
+ }
+
+ @Test
+ void genTestJson() {
+ DccTestBuilder dccTestBuilder = new DccTestBuilder();
+ dccTestBuilder.gn("Artur").fn("Trzewik").gnt("ARTUR").fnt("TRZEWIK").dob(LocalDate.parse("1973-01-01"));
+ dccTestBuilder.detected(false)
+ .dgci("URN:UVCI:01:OS:B5921A35D6A0D696421B3E2462178297I")
+ .country("DE")
+ .testTypeRapid(true)
+ .testingCentre("Hochdahl")
+ .certificateIssuer("Dr Who")
+ .sampleCollection(LocalDateTime.now());
+ String jsonString = dccTestBuilder.toJsonString();
+ assertNotNull(jsonString);
+ System.out.println(jsonString);
+ }
+
+}
\ No newline at end of file
diff --git a/src/test/java/eu/europa/ec/dgc/generation/DccVaccinationBuilderTest.java b/src/test/java/eu/europa/ec/dgc/generation/DccVaccinationBuilderTest.java
new file mode 100644
index 0000000..8ecd400
--- /dev/null
+++ b/src/test/java/eu/europa/ec/dgc/generation/DccVaccinationBuilderTest.java
@@ -0,0 +1,27 @@
+package eu.europa.ec.dgc.generation;
+
+import java.time.LocalDate;
+import java.time.LocalDateTime;
+import org.junit.jupiter.api.Test;
+
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+
+class DccVaccinationBuilderTest {
+ @Test
+ void genTestJson() {
+ DccVaccinationBuilder dccVaccinationBuilder = new DccVaccinationBuilder();
+ dccVaccinationBuilder.gn("Artur").fn("Trzewik").gnt("ARTUR").fnt("TRZEWIK").dob(LocalDate.parse("1973-01-01"));
+ dccVaccinationBuilder.dgci("URN:UVCI:01:OS:B5921A35D6A0D696421B3E2462178297I")
+ .country("DE")
+ .certificateIssuer("Dr Who")
+ .doseNumber(1)
+ .totalSeriesOfDoses(2)
+ .dateOfVaccination(LocalDateTime.now())
+ .vaccineOrProphylaxis("1119349007")
+ .medicalProduct("EU/1/20/1507")
+ .marketingAuthorization("ORG-100001699");
+ String jsonString = dccVaccinationBuilder.toJsonString();
+ assertNotNull(jsonString);
+ System.out.println(jsonString);
+ }
+}
\ No newline at end of file
diff --git a/src/test/java/eu/europa/ec/dgc/generation/DgcCryptedPublisherTest.java b/src/test/java/eu/europa/ec/dgc/generation/DgcCryptedPublisherTest.java
new file mode 100644
index 0000000..5a2be65
--- /dev/null
+++ b/src/test/java/eu/europa/ec/dgc/generation/DgcCryptedPublisherTest.java
@@ -0,0 +1,135 @@
+package eu.europa.ec.dgc.generation;
+
+import eu.europa.ec.dgc.generation.dto.DgcData;
+import eu.europa.ec.dgc.generation.dto.DgcInitData;
+import java.nio.charset.StandardCharsets;
+import java.security.KeyPair;
+import java.security.KeyPairGenerator;
+import java.time.Duration;
+import java.time.ZonedDateTime;
+import java.time.temporal.ChronoUnit;
+import java.util.Base64;
+import javax.crypto.Cipher;
+import static org.junit.jupiter.api.Assertions.assertArrayEquals;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+
+class DgcCryptedPublisherTest {
+ KeyHelper keyHelper;
+
+ @BeforeEach
+ void setup() throws Exception {
+ keyHelper = new KeyHelper();
+ }
+
+ @Test
+ void getEncodedDGCData() throws Exception {
+ DgcSigner dgcSigner = new DgcSigner();
+ DgcCryptedPublisher dgcCryptedPublisher = new DgcCryptedPublisher();
+
+ String edgcJson = DgcSignerTest.genSampleJson();
+ String countryCode = "DE";
+ ZonedDateTime now = ZonedDateTime.now();
+ ZonedDateTime expiration = now.plus(Duration.of(365, ChronoUnit.DAYS));
+ long issuedAt = now.toInstant().getEpochSecond();
+ long expirationSec = expiration.toInstant().getEpochSecond();
+ byte[] keyId = dgcSigner.keyId(keyHelper.getCert());
+ // We assume that it is EC Key
+ int algId = -7;
+
+ KeyPairGenerator keyPairGen = KeyPairGenerator.getInstance("RSA");
+ keyPairGen.initialize(3072);
+ KeyPair keyPair = keyPairGen.generateKeyPair();
+
+ // Test coding of public key
+ // Base64-kodierte RSA-3072 Public Key in x.509 Format (ohne PEM Header/Footer). Immer 564 Zeichen (als Base64-Darstellung).
+ String publicKeyBase64 = Base64.getEncoder().encodeToString(keyPair.getPublic().getEncoded());
+ assertEquals(564,publicKeyBase64.length());
+
+ DgcInitData dgcInitData = new DgcInitData();
+ dgcInitData.setExpriation(expirationSec);
+ dgcInitData.setIssuedAt(issuedAt);
+ dgcInitData.setIssuerCode(countryCode);
+ dgcInitData.setKeyId(keyId);
+ dgcInitData.setAlgId(-7);
+ DgcData dgcData = dgcCryptedPublisher.createDgc(dgcInitData, edgcJson, keyPair.getPublic());
+
+ // Base64-kodierte und mit dem RSA Public Key verschlüsselter DEK. Der DEK selbst muss 32 Bytes haben (für AES-256).
+ // Der verschlüsselte DEK hat 384 Bytes und die base64-kodierte Darstellung entsprechend 512 Zeichen.
+ assertEquals(384,dgcData.getDek().length);
+ String dekBase64 = Base64.getEncoder().encodeToString(dgcData.getDek());
+ assertEquals(512,dekBase64.length());
+
+ byte[] signature = dgcSigner.signHash(dgcData.getHash(),keyHelper.getPrivateKey());
+
+ DgcCryptedFinalizer dgcCryptedFinalizer = new DgcCryptedFinalizer();
+ String edgcQRCode = dgcCryptedFinalizer.finalizeDcc(dgcData.getDataEncrypted(), dgcData.getDek(), keyPair.getPrivate(), signature);
+ System.out.println(edgcQRCode);
+ }
+
+ @Test
+ void dccWithPartialDCCSigning() throws Exception {
+ DgcSigner dgcSigner = new DgcSigner();
+ DgcCryptedPublisher dgcCryptedPublisher = new DgcCryptedPublisher();
+
+ String edgcJson = DgcSignerTest.genSampleJson();
+ String countryCode = "DE";
+ ZonedDateTime now = ZonedDateTime.now();
+ ZonedDateTime expiration = now.plus(Duration.of(365, ChronoUnit.DAYS));
+ long issuedAt = now.toInstant().getEpochSecond();
+ long expirationSec = expiration.toInstant().getEpochSecond();
+ byte[] keyId = dgcSigner.keyId(keyHelper.getCert());
+ // We assume that it is EC Key
+ int algId = -7;
+
+ KeyPairGenerator keyPairGen = KeyPairGenerator.getInstance("RSA");
+ keyPairGen.initialize(3072);
+ KeyPair keyPair = keyPairGen.generateKeyPair();
+
+ // Test coding of public key
+ // Base64-kodierte RSA-3072 Public Key in x.509 Format (ohne PEM Header/Footer). Immer 564 Zeichen (als Base64-Darstellung).
+ String publicKeyBase64 = Base64.getEncoder().encodeToString(keyPair.getPublic().getEncoded());
+ assertEquals(564,publicKeyBase64.length());
+
+ DgcInitData dgcInitData = new DgcInitData();
+ dgcInitData.setExpriation(expirationSec);
+ dgcInitData.setIssuedAt(issuedAt);
+ dgcInitData.setIssuerCode(countryCode);
+ // do not set keyId here, because is it unknown for dcc creator
+ dgcInitData.setKeyId(null);
+ dgcInitData.setAlgId(-7);
+ DgcData dgcData = dgcCryptedPublisher.createDgc(dgcInitData, edgcJson, keyPair.getPublic());
+
+ // Base64-kodierte und mit dem RSA Public Key verschlüsselter DEK. Der DEK selbst muss 32 Bytes haben (für AES-256).
+ // Der verschlüsselte DEK hat 384 Bytes und die base64-kodierte Darstellung entsprechend 512 Zeichen.
+ assertEquals(384,dgcData.getDek().length);
+ String dekBase64 = Base64.getEncoder().encodeToString(dgcData.getDek());
+ assertEquals(512,dekBase64.length());
+
+ byte[] partialDcc = dgcSigner.signPartialDcc(dgcData.getHash(),keyHelper.getPrivateKey(),keyId);
+
+ DgcCryptedFinalizer dgcCryptedFinalizer = new DgcCryptedFinalizer();
+ String edgcQRCode = dgcCryptedFinalizer.finalizePartialDcc(dgcData.getDataEncrypted(), dgcData.getDek(), keyPair.getPrivate(), partialDcc);
+ System.out.println(edgcQRCode);
+ }
+
+ @Test
+ void rsaCrypt() throws Exception {
+ KeyPairGenerator keyPairGen = KeyPairGenerator.getInstance("RSA");
+ keyPairGen.initialize(2048);
+ KeyPair keyPair = keyPairGen.generateKeyPair();
+
+ byte[] testData = "Test".getBytes(StandardCharsets.UTF_8);
+
+ Cipher cipher = Cipher.getInstance(DgcCryptedPublisher.KEY_CIPHER);
+ cipher.init(Cipher.ENCRYPT_MODE, keyPair.getPublic());
+ byte[] encrypted = cipher.doFinal(testData);
+
+ Cipher cipherDecrypt = Cipher.getInstance(DgcCryptedPublisher.KEY_CIPHER);
+ cipherDecrypt.init(Cipher.DECRYPT_MODE, keyPair.getPrivate());
+ byte[] decrypted = cipherDecrypt.doFinal(encrypted);
+
+ assertArrayEquals(testData,decrypted);
+ }
+}
\ No newline at end of file
diff --git a/src/test/java/eu/europa/ec/dgc/generation/DgcSignerTest.java b/src/test/java/eu/europa/ec/dgc/generation/DgcSignerTest.java
new file mode 100644
index 0000000..b5bb1aa
--- /dev/null
+++ b/src/test/java/eu/europa/ec/dgc/generation/DgcSignerTest.java
@@ -0,0 +1,63 @@
+package eu.europa.ec.dgc.generation;
+
+import java.io.IOException;
+import java.time.Duration;
+import java.time.LocalDate;
+import java.time.LocalDateTime;
+import java.time.ZonedDateTime;
+import java.time.temporal.ChronoUnit;
+import org.junit.jupiter.api.Assertions;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+
+class DgcSignerTest {
+ KeyHelper keyHelper;
+
+ @BeforeEach
+ void setup() throws Exception {
+ keyHelper = new KeyHelper();
+ }
+
+ public static String genSampleJson() {
+ DccTestBuilder dccTestBuilder = new DccTestBuilder();
+ dccTestBuilder.gn("Artur").fn("Trzewik").gnt("ARTUR").fnt("TRZEWIK").dob(LocalDate.parse("1973-01-01"));
+ dccTestBuilder.detected(false)
+ .dgci("URN:UVCI:01:OS:B5921A35D6A0D696421B3E2462178297I")
+ .country("DE")
+ .testTypeRapid(true)
+ .testingCentre("Hochdahl")
+ .certificateIssuer("Dr Who")
+ .sampleCollection(LocalDateTime.now());
+ return dccTestBuilder.toJsonString();
+ }
+
+ @Test
+ void genEdgc() throws IOException {
+
+ DgcGenerator dgcGenerator = new DgcGenerator();
+ DgcSigner dgcSigner = new DgcSigner();
+
+ String edgcJson = genSampleJson();
+
+ String countryCode = "DE";
+ ZonedDateTime now = ZonedDateTime.now();
+ ZonedDateTime expiration = now.plus(Duration.of(365, ChronoUnit.DAYS));
+ long issuedAt = now.toInstant().getEpochSecond();
+ long expirationSec = expiration.toInstant().getEpochSecond();
+ byte[] keyId = dgcSigner.keyId(keyHelper.getCert());
+ // We assume that it is EC Key
+ int algId = -7;
+
+ byte[] dgcCbor = dgcGenerator.genDgcCbor(edgcJson, countryCode, issuedAt, expirationSec);
+
+ byte[] coseBytes = dgcGenerator.genCoseUnsigned(dgcCbor, keyId, algId);
+ byte[] hash = dgcGenerator.computeCoseSignHash(coseBytes);
+
+ byte[] signature = dgcSigner.signHash(hash,keyHelper.getPrivateKey());
+
+ byte[] coseSigned = dgcGenerator.dgcSetCoseSignature(coseBytes,signature);
+ String edgcQR = dgcGenerator.coseToQrCode(coseSigned);
+
+ System.out.println(edgcQR);
+ }
+}
\ No newline at end of file
diff --git a/src/test/java/eu/europa/ec/dgc/generation/KeyHelper.java b/src/test/java/eu/europa/ec/dgc/generation/KeyHelper.java
new file mode 100644
index 0000000..2304d6a
--- /dev/null
+++ b/src/test/java/eu/europa/ec/dgc/generation/KeyHelper.java
@@ -0,0 +1,28 @@
+package eu.europa.ec.dgc.generation;
+
+import eu.europa.ec.dgc.testdata.CertificateTestUtils;
+import java.security.KeyPair;
+import java.security.KeyPairGenerator;
+import java.security.PrivateKey;
+import java.security.cert.Certificate;
+
+public class KeyHelper {
+ private final Certificate cert;
+ private final PrivateKey privateKey;
+
+ public Certificate getCert() {
+ return cert;
+ }
+
+ public PrivateKey getPrivateKey() {
+ return privateKey;
+ }
+
+ public KeyHelper() throws Exception {
+ KeyPairGenerator keyPairGenerator = KeyPairGenerator.getInstance("ec");
+ KeyPair keyPair = keyPairGenerator.generateKeyPair();
+
+ cert = CertificateTestUtils.generateCertificate(keyPair, "DE", "DCC Gen Lib Test");
+ privateKey = keyPair.getPrivate();
+ }
+}
diff --git a/src/test/java/eu/europa/ec/dgc/signing/SignedCertificateMessageBuilderTest.java b/src/test/java/eu/europa/ec/dgc/signing/SignedCertificateMessageBuilderTest.java
index 716127a..1a1e011 100644
--- a/src/test/java/eu/europa/ec/dgc/signing/SignedCertificateMessageBuilderTest.java
+++ b/src/test/java/eu/europa/ec/dgc/signing/SignedCertificateMessageBuilderTest.java
@@ -29,7 +29,6 @@
import java.util.Collection;
import org.bouncycastle.asn1.cms.CMSObjectIdentifiers;
import org.bouncycastle.cert.X509CertificateHolder;
-import org.bouncycastle.cms.CMSException;
import org.bouncycastle.cms.CMSProcessableByteArray;
import org.bouncycastle.cms.CMSSignedData;
import org.bouncycastle.cms.SignerInformation;