diff --git a/.gitignore b/.gitignore
index 1f6b3484..91a8ec3a 100644
--- a/.gitignore
+++ b/.gitignore
@@ -5,3 +5,5 @@ target
.project
.settings
.classpath
+
+dependency-reduced-pom.xml
diff --git a/NOTICE b/NOTICE
index 4fa4feb8..9a67370a 100644
--- a/NOTICE
+++ b/NOTICE
@@ -12,6 +12,7 @@ This software includes third party software subject to the following licenses:
Apache Commons Lang under Apache License, Version 2.0
Apache HttpClient under Apache License, Version 2.0
Apache HttpCore under Apache License, Version 2.0
+ Digipost Certificate Validator under The Apache Software License, Version 2.0
istack common utility code runtime under Eclipse Distribution License - v 1.0
Jakarta Activation under EDL 1.0
Jakarta Activation API jar under EDL 1.0
diff --git a/pom.xml b/pom.xml
index a78e4f4a..43f36356 100644
--- a/pom.xml
+++ b/pom.xml
@@ -59,6 +59,17 @@
signature-api-specification-jaxb
${signature.api.version}
+
+ no.digipost
+ certificate-validator
+ 2.3
+
+
+ org.bouncycastle
+ *
+
+
+
org.apache.httpcomponents
httpclient
@@ -205,11 +216,6 @@
src/main/resources
true
-
- src/main/certificates
- false
- no/digipost/signature/client/certificates
-
@@ -269,6 +275,37 @@
+
+ maven-shade-plugin
+
+ 3.2.1
+
+ true
+
+
+ no.digipost:certificate-validator
+
+
+
+
+ no.digipost.*
+
+ META-INF/*.MF
+
+
+
+
+
+ no.digipost.security
+ no.digipost.signature.client.core.internal.security.certificate_validator
+
+
+
+
maven-surefire-plugin
3.0.0-M5
@@ -363,6 +400,16 @@
+
+ maven-shade-plugin
+
+
+
+ shade
+
+
+
+
diff --git a/src/main/certificates/prod/BPClass3CA3.cer b/src/main/certificates/prod/BPClass3CA3.cer
deleted file mode 100644
index a29ff185..00000000
Binary files a/src/main/certificates/prod/BPClass3CA3.cer and /dev/null differ
diff --git a/src/main/certificates/prod/BPClass3RootCA.cer b/src/main/certificates/prod/BPClass3RootCA.cer
deleted file mode 100644
index 24e5adbb..00000000
Binary files a/src/main/certificates/prod/BPClass3RootCA.cer and /dev/null differ
diff --git a/src/main/certificates/prod/commfides_ca.cer b/src/main/certificates/prod/commfides_ca.cer
deleted file mode 100644
index 7451ab3e..00000000
Binary files a/src/main/certificates/prod/commfides_ca.cer and /dev/null differ
diff --git a/src/main/certificates/prod/commfides_root_ca.cer b/src/main/certificates/prod/commfides_root_ca.cer
deleted file mode 100644
index b6611754..00000000
Binary files a/src/main/certificates/prod/commfides_root_ca.cer and /dev/null differ
diff --git a/src/main/certificates/test/Buypass_Class_3_Test4_CA_3.cer b/src/main/certificates/test/Buypass_Class_3_Test4_CA_3.cer
deleted file mode 100644
index 0e02b93c..00000000
Binary files a/src/main/certificates/test/Buypass_Class_3_Test4_CA_3.cer and /dev/null differ
diff --git a/src/main/certificates/test/Buypass_Class_3_Test4_Root_CA.cer b/src/main/certificates/test/Buypass_Class_3_Test4_Root_CA.cer
deleted file mode 100644
index 751257c3..00000000
Binary files a/src/main/certificates/test/Buypass_Class_3_Test4_Root_CA.cer and /dev/null differ
diff --git a/src/main/certificates/test/commfides_test_ca.cer b/src/main/certificates/test/commfides_test_ca.cer
deleted file mode 100644
index 8cacb78b..00000000
Binary files a/src/main/certificates/test/commfides_test_ca.cer and /dev/null differ
diff --git a/src/main/certificates/test/commfides_test_root_ca.cer b/src/main/certificates/test/commfides_test_root_ca.cer
deleted file mode 100644
index 283971e5..00000000
Binary files a/src/main/certificates/test/commfides_test_root_ca.cer and /dev/null differ
diff --git a/src/main/certificates/test/digipost_test_root_ca.pem b/src/main/certificates/test/digipost_test_root_ca.pem
deleted file mode 100644
index 322e8df8..00000000
--- a/src/main/certificates/test/digipost_test_root_ca.pem
+++ /dev/null
@@ -1,33 +0,0 @@
------BEGIN CERTIFICATE-----
-MIIFtzCCA5+gAwIBAgIJAKlim8GZ3ABzMA0GCSqGSIb3DQEBCwUAMGkxCzAJBgNV
-BAYTAk5PMQ0wCwYDVQQIDARPc2xvMRgwFgYDVQQKDA9Qb3N0ZW4gTm9yZ2UgQVMx
-ETAPBgNVBAsMCERpZ2lwb3N0MR4wHAYDVQQDDBVEaWdpcG9zdCBUZXN0IFJvb3Qg
-Q0EwIBcNMTUxMTAzMjEyNTE3WhgPMjA1MDA5MTYyMTI1MTdaMGkxCzAJBgNVBAYT
-Ak5PMQ0wCwYDVQQIDARPc2xvMRgwFgYDVQQKDA9Qb3N0ZW4gTm9yZ2UgQVMxETAP
-BgNVBAsMCERpZ2lwb3N0MR4wHAYDVQQDDBVEaWdpcG9zdCBUZXN0IFJvb3QgQ0Ew
-ggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQDS9pw3Ax73oSAHAvPFseeb
-M3M1hUYlpxTV7iEary7mXHmcQf4mtztqEnYINMKoPgh2HpLauiMc2ZS6axrE/frh
-wPOUS9iK7c/QBnr0galeqyr4YmjTUdLNH8ifVbrx8J2qNsxgcakDX9cOj9YIIiYQ
-Or+J78x9uopOBYfJgHBYgaBbHguQ7Ackf0Y9S117/nqzWfVEQs+9P03YP3pOuS2N
-xY0iugeNfMO0Wfq/mShv0ZqtkpRs9bAhQ+EOaZzSRdRn0+DqmkiQtwyspDsWINU6
-hu+tuWLIRzX2XVEWa0h2oEs2xDLIr0oU7LHHruULcH6pneQjwk+K2o8F9Oh4sMjL
-mp921edh6SGYqxiQs2c4j5m0jNbewbkfcdFL1v/GbzUbp4bmYYBAP6pm9EHMZb/r
-q2+RVWHJIiGy7nqPNzBs7d3lGTgkGx7zHNgx76fe/DCrsKjQGwY4IO/hm8nh3ujo
-ITj2EBbt2npxHu/9C5Fs4Z3jts2Kw+pBeL16ODh8zh11GlT9KnzmSEGuaQbNNfag
-LDsUU7J9pqqk/wSgGh7FB6PMW+HqsBEeq43JQxlEAqIPzNbTdGQENYI9LEMr9FfV
-I5g+murFlVF9ZNHutLe3ufnGvVBx9Ay3ES65guBupW8hBtpWDsTKnOBg8nfhZev/
-tyOxWy1lYtnryqXi3WEJBQIDAQABo2AwXjAdBgNVHQ4EFgQUFLAeb496K7Xdwrp/
-37LcatO4WX0wHwYDVR0jBBgwFoAUFLAeb496K7Xdwrp/37LcatO4WX0wDwYDVR0T
-AQH/BAUwAwEB/zALBgNVHQ8EBAMCAgQwDQYJKoZIhvcNAQELBQADggIBAAaAk62u
-gTtTMD/UqvXlSQRZztVLVH5HLWFACRtm/nl55FinGygFETLSKXWa52T7rcC7zs6F
-5BGjQSLkb+/Kogg3GL3Ve2nts8HG6iaM5SDBu2PA/rVAG1W4xOmccFGJ79rWBw1o
-0IQg9k2+34A02B9LJ8/EzRW7SdKyYfhQibMIwwypTUhJBA4dvf797dQAey+wbsyS
-XhqzwGsk3EcBAXWIh1yUx7tpHXGjEFmmKGp7hKUSxiI+fXkFbcuKHqYkuaGlBcWj
-ybVe97flx+pPNnlj8CaXadsqE3F8xB4cuE7GmD1QsBSsCkDgETDxf1raJqEa9Lpf
-ks7BH8x6MjEbAiAKa6hBh7L1+6CcDGYFaVowAuhbxs7+GqX/pw3A6mgDkY1RTTZ1
-ZH3sU1tIwagccE0gNuocz/c/+iFgY3DXHQsqz5sFwu9AMppOZ2LMMwOXZYebBCl7
-BmpNmoBal7mFr7YfpRwKbieuGOa6pBvkHa+m78uiWy2h9BYpnLkwpr51qKETFCcG
-Fe2nPzqWGxSljPWeKcwFGzuB+Mq2oBpnA90PqkWhdhpf/26FjmMaPEz1bkC4n+Lc
-BXZ4V8e11X6sm4PqDcNFJRE8pg0gaq5xeGbWskh3Oe8RWIQZhtuHo3HQNcVqVG0P
-M0BKDEb+eUJjbZnjl/cVoV2gPllIWlkW/5Bh
------END CERTIFICATE-----
diff --git a/src/main/java/no/digipost/signature/client/Certificates.java b/src/main/java/no/digipost/signature/client/Certificates.java
index dba6998a..9dc89879 100644
--- a/src/main/java/no/digipost/signature/client/Certificates.java
+++ b/src/main/java/no/digipost/signature/client/Certificates.java
@@ -1,7 +1,6 @@
package no.digipost.signature.client;
import java.util.List;
-import java.util.function.Function;
import java.util.stream.Stream;
import static java.util.stream.Collectors.toList;
@@ -11,13 +10,27 @@ public enum Certificates {
TEST(
"test/Buypass_Class_3_Test4_CA_3.cer",
"test/Buypass_Class_3_Test4_Root_CA.cer",
+
+ "test/BPCl3CaG2HTBS.cer",
+ "test/BPCl3CaG2STBS.cer",
+ "test/BPCl3RootCaG2HT.cer",
+ "test/BPCl3RootCaG2ST.cer",
+
"test/commfides_test_ca.cer",
"test/commfides_test_root_ca.cer",
- "test/digipost_test_root_ca.pem"
+
+ "test/digipost_test_root_ca.cert.pem"
+
),
PRODUCTION(
"prod/BPClass3CA3.cer",
"prod/BPClass3RootCA.cer",
+
+ "prod/BPCl3CaG2HTBS.cer",
+ "prod/BPCl3CaG2STBS.cer",
+ "prod/BPCl3RootCaG2HT.cer",
+ "prod/BPCl3RootCaG2ST.cer",
+
"prod/commfides_ca.cer",
"prod/commfides_root_ca.cer"
);
@@ -25,18 +38,9 @@ public enum Certificates {
final List certificatePaths;
Certificates(String ... certificatePaths) {
- this.certificatePaths = Stream.of(certificatePaths).map(FullCertificateClassPathUri.instance).collect(toList());
+ this.certificatePaths = Stream.of(certificatePaths)
+ .map("classpath:/certificates/"::concat)
+ .collect(toList());
}
-}
-
-
-final class FullCertificateClassPathUri implements Function {
- static final FullCertificateClassPathUri instance = new FullCertificateClassPathUri();
- private static final String root = "/" + Certificates.class.getPackage().getName().replace('.', '/') + "/certificates/";
-
- @Override
- public String apply(String resourceName) {
- return "classpath:" + root + resourceName;
- }
}
diff --git a/src/main/java/no/digipost/signature/client/ClientConfiguration.java b/src/main/java/no/digipost/signature/client/ClientConfiguration.java
index c6270dbc..d0d4feb1 100644
--- a/src/main/java/no/digipost/signature/client/ClientConfiguration.java
+++ b/src/main/java/no/digipost/signature/client/ClientConfiguration.java
@@ -8,14 +8,14 @@
import no.digipost.signature.client.core.exceptions.KeyException;
import no.digipost.signature.client.core.internal.http.AddRequestHeaderFilter;
import no.digipost.signature.client.core.internal.http.HttpIntegrationConfiguration;
-import no.digipost.signature.client.core.internal.http.PostenEnterpriseCertificateStrategy;
+import no.digipost.signature.client.core.internal.http.SignatureApiTrustStrategy;
import no.digipost.signature.client.core.internal.security.ProvidesCertificateResourcePaths;
import no.digipost.signature.client.core.internal.security.TrustStoreLoader;
import no.digipost.signature.client.core.internal.xml.JaxbMessageReaderWriterProvider;
+import no.digipost.signature.client.security.CertificateChainValidation;
import no.digipost.signature.client.security.KeyStoreConfig;
+import no.digipost.signature.client.security.OrganizationNumberValidation;
import org.apache.commons.lang3.StringUtils;
-import org.apache.http.ssl.PrivateKeyDetails;
-import org.apache.http.ssl.PrivateKeyStrategy;
import org.apache.http.ssl.SSLContexts;
import org.glassfish.jersey.client.ClientConfig;
import org.glassfish.jersey.client.ClientProperties;
@@ -28,8 +28,8 @@
import javax.ws.rs.core.Configurable;
import javax.ws.rs.core.Configuration;
import javax.ws.rs.core.HttpHeaders;
+
import java.io.InputStream;
-import java.net.Socket;
import java.net.URI;
import java.nio.file.Path;
import java.security.KeyManagementException;
@@ -39,7 +39,6 @@
import java.time.Clock;
import java.util.ArrayList;
import java.util.List;
-import java.util.Map;
import java.util.Optional;
import java.util.UUID;
import java.util.function.Consumer;
@@ -89,14 +88,13 @@ public final class ClientConfiguration implements ProvidesCertificateResourcePat
private final Optional sender;
private final URI signatureServiceRoot;
private final Iterable documentBundleProcessors;
+ private final CertificateChainValidation serverCertificateValidation;
private final Clock clock;
-
-
private ClientConfiguration(
KeyStoreConfig keyStoreConfig, Configurable extends Configuration> jaxrsConfig,
Optional sender, URI serviceRoot, Iterable certificatePaths,
- Iterable documentBundleProcessors, Clock clock) {
+ Iterable documentBundleProcessors, CertificateChainValidation serverCertificateValidation, Clock clock) {
this.keyStoreConfig = keyStoreConfig;
this.jaxrsConfig = jaxrsConfig;
@@ -104,6 +102,7 @@ private ClientConfiguration(
this.signatureServiceRoot = serviceRoot;
this.certificatePaths = certificatePaths;
this.documentBundleProcessors = documentBundleProcessors;
+ this.serverCertificateValidation = serverCertificateValidation;
this.clock = clock;
}
@@ -153,14 +152,9 @@ public Configuration getJaxrsConfiguration() {
@Override
public SSLContext getSSLContext() {
try {
- return SSLContexts.custom()
- .loadKeyMaterial(keyStoreConfig.keyStore, keyStoreConfig.privatekeyPassword.toCharArray(), new PrivateKeyStrategy() {
- @Override
- public String chooseAlias(Map aliases, Socket socket) {
- return keyStoreConfig.alias;
- }
- })
- .loadTrustMaterial(TrustStoreLoader.build(this), new PostenEnterpriseCertificateStrategy())
+ return SSLContexts.custom()
+ .loadKeyMaterial(keyStoreConfig.keyStore, keyStoreConfig.privatekeyPassword.toCharArray(), (aliases, socket) -> keyStoreConfig.alias)
+ .loadTrustMaterial(TrustStoreLoader.build(this), new SignatureApiTrustStrategy(serverCertificateValidation))
.build();
} catch (NoSuchAlgorithmException | KeyManagementException | KeyStoreException | UnrecoverableKeyException e) {
if (e instanceof UnrecoverableKeyException && "Given final block not properly padded".equals(e.getMessage())) {
@@ -194,6 +188,7 @@ public static class Builder {
private URI serviceRoot = ServiceUri.PRODUCTION.uri;
private Optional globalSender = Optional.empty();
private Iterable certificatePaths = Certificates.PRODUCTION.certificatePaths;
+ private CertificateChainValidation serverCertificateTrustStrategy = new OrganizationNumberValidation("984661185"); // Posten Norge AS organization number
private Optional loggingFeature = Optional.empty();
private List documentBundleProcessors = new ArrayList<>();
private Clock clock = Clock.systemDefaultZone();
@@ -350,6 +345,35 @@ public Builder customizeJaxRs(Consumer super Configurable extends Configurat
return this;
}
+
+ /**
+ * Override which organization number which is expected from the server's certificate.
+ * By default, this is the organization number of Posten Norge AS, and should not
+ * be overridden unless you have a specific need such as doing testing against your own
+ * stubbed implementation of the Posten signering API.
+ *
+ * @param serverOrganizationNumber the organization number expected in the server's enterprise certificate
+ */
+ public Builder serverOrganizationNumber(String serverOrganizationNumber) {
+ return serverCertificateTrustStrategy(new OrganizationNumberValidation(serverOrganizationNumber));
+ }
+
+
+ /**
+ * Override the validation of the server's certificate. This method is mainly
+ * intended for tests if you need to override (or even disable) the default
+ * validation that the server identifies itself as "Posten Norge AS".
+ *
+ * Calling this method for a production deployment is probably not what you intend to do!
+ *
+ * @param certificateChainValidation the validation for the server's certificate
+ */
+ public Builder serverCertificateTrustStrategy(CertificateChainValidation certificateChainValidation) {
+ LOG.warn("Overriding server certificate TrustStrategy! This should NOT be done for any integration with Posten signering.");
+ this.serverCertificateTrustStrategy = certificateChainValidation;
+ return this;
+ }
+
/**
* Allows for overriding which {@link Clock} is used to convert between Java and XML,
* may be useful for e.g. automated tests.
@@ -368,7 +392,9 @@ public ClientConfiguration build() {
jaxrsConfig.register(JaxbMessageReaderWriterProvider.class);
jaxrsConfig.register(new AddRequestHeaderFilter(USER_AGENT, createUserAgentString()));
this.loggingFeature.ifPresent(jaxrsConfig::register);
- return new ClientConfiguration(keyStoreConfig, jaxrsConfig, globalSender, serviceRoot, certificatePaths, documentBundleProcessors, clock);
+ return new ClientConfiguration(
+ keyStoreConfig, jaxrsConfig, globalSender, serviceRoot, certificatePaths,
+ documentBundleProcessors, serverCertificateTrustStrategy, clock);
}
String createUserAgentString() {
diff --git a/src/main/java/no/digipost/signature/client/core/internal/http/PostenEnterpriseCertificateStrategy.java b/src/main/java/no/digipost/signature/client/core/internal/http/PostenEnterpriseCertificateStrategy.java
deleted file mode 100644
index 54221a29..00000000
--- a/src/main/java/no/digipost/signature/client/core/internal/http/PostenEnterpriseCertificateStrategy.java
+++ /dev/null
@@ -1,56 +0,0 @@
-package no.digipost.signature.client.core.internal.http;
-
-import no.digipost.signature.client.core.exceptions.SecurityException;
-import org.apache.http.conn.ssl.TrustStrategy;
-
-import java.security.cert.CertificateException;
-import java.security.cert.X509Certificate;
-
-public class PostenEnterpriseCertificateStrategy implements TrustStrategy {
-
- private static final String POSTEN_ORGANIZATION_NUMBER = "984661185";
-
- /**
- * Used by some obscure cases to embed Norwegian "organisasjonsnummer" in certificates.
- */
- private static final String COMMON_NAME_POSTEN = "CN=" + POSTEN_ORGANIZATION_NUMBER;
-
- /**
- * Most common way to embed Norwegian "organisasjonsnummer" in certificates.
- */
- private static final String SERIALNUMBER_POSTEN = "SERIALNUMBER=" + POSTEN_ORGANIZATION_NUMBER;
-
-
- /**
- * Verify that the server certificate is issued to Posten Norge AS.
- *
- * Note that we have to throw an Exception to make sure that invalid certificates will be denied.
- * The http client TrustStrategy can only be used to used to state that a server certificate is to be
- * trusted without consulting the standard Java certificate verification process.
- *
- * Always returns false to make sure http client will run the Java certificate verification process, which
- * will verify the certificate against the trust store, making sure that it's actually issued by a trusted CA.
- *
- * @see javax.net.ssl.X509TrustManager#checkServerTrusted(X509Certificate[], String)
- */
- @Override
- public boolean isTrusted(X509Certificate[] chain, String authType) throws CertificateException {
- String subjectDN = chain[0].getSubjectDN().getName();
-
- if (!isPostenEnterpriseCertiticate(subjectDN)) {
- throw new SecurityException("Could not find correct organization number in server certificate. Make sure the server URI is correct.\n" +
- "Actual certificate: " + subjectDN + ".\n" +
- "Expected certificate issued to organization number " + POSTEN_ORGANIZATION_NUMBER + "\n" +
- "This could indicate a misconfiguration of the client or server, or potentially a man-in-the-middle attack.");
- }
-
- return false;
- }
-
- private boolean isPostenEnterpriseCertiticate(String subjectDN) {
- String lowerCaseSubjectDN = subjectDN.toLowerCase();
- return lowerCaseSubjectDN.contains(SERIALNUMBER_POSTEN.toLowerCase()) ||
- lowerCaseSubjectDN.contains(COMMON_NAME_POSTEN.toLowerCase());
- }
-
-}
diff --git a/src/main/java/no/digipost/signature/client/core/internal/http/SignatureApiTrustStrategy.java b/src/main/java/no/digipost/signature/client/core/internal/http/SignatureApiTrustStrategy.java
new file mode 100644
index 00000000..a3af6747
--- /dev/null
+++ b/src/main/java/no/digipost/signature/client/core/internal/http/SignatureApiTrustStrategy.java
@@ -0,0 +1,46 @@
+package no.digipost.signature.client.core.internal.http;
+
+import no.digipost.signature.client.core.exceptions.SecurityException;
+import no.digipost.signature.client.security.CertificateChainValidation;
+import no.digipost.signature.client.security.CertificateChainValidation.Result;
+import org.apache.http.conn.ssl.TrustStrategy;
+
+import java.security.cert.X509Certificate;
+
+public final class SignatureApiTrustStrategy implements TrustStrategy {
+
+ private final CertificateChainValidation certificateChainValidation;
+
+ public SignatureApiTrustStrategy(CertificateChainValidation certificateChainValidation) {
+ this.certificateChainValidation = certificateChainValidation;
+ }
+
+ /**
+ * Verify that the server certificate is trusted.
+ *
+ * Note that we have to throw an Exception to make sure that invalid certificates will be denied.
+ * The http client TrustStrategy can only be used to used to state that a server certificate is to be
+ * trusted without consulting the standard Java certificate verification process.
+ *
+ * Unintuitively returns {@code false} when the {@link CertificateChainValidation} determines the chain
+ * to be {@link Result#TRUSTED} to make sure http client will run the Java certificate verification process, which
+ * will verify the certificate against the trust store, making sure that it's actually issued by a trusted CA.
+ *
+ * @see javax.net.ssl.X509TrustManager#checkServerTrusted(X509Certificate[], String)
+ */
+ @Override
+ public boolean isTrusted(X509Certificate[] chain, String authType) {
+ Result result = certificateChainValidation.validate(chain);
+ switch (result) {
+ case TRUSTED_AND_SKIP_FURTHER_VALIDATION: return true;
+ case TRUSTED: return false;
+ case UNTRUSTED: default:
+ String subjectDN = chain[0].getSubjectDN().getName();
+ throw new SecurityException(
+ "Untrusted server certificate, according to " + certificateChainValidation + ". " +
+ "Make sure the server URI is correct. Actual certificate: " + subjectDN + ". " +
+ "This could indicate a misconfiguration of the client or server, or potentially a man-in-the-middle attack.");
+ }
+ }
+
+}
diff --git a/src/main/java/no/digipost/signature/client/core/internal/security/TrustStoreLoader.java b/src/main/java/no/digipost/signature/client/core/internal/security/TrustStoreLoader.java
index b85de30c..3eb4d5a7 100644
--- a/src/main/java/no/digipost/signature/client/core/internal/security/TrustStoreLoader.java
+++ b/src/main/java/no/digipost/signature/client/core/internal/security/TrustStoreLoader.java
@@ -1,15 +1,17 @@
package no.digipost.signature.client.core.internal.security;
-import no.digipost.signature.client.Certificates;
import no.digipost.signature.client.core.exceptions.ConfigurationException;
import javax.net.ssl.SSLContext;
import javax.net.ssl.TrustManagerFactory;
-import java.io.File;
-import java.io.FileInputStream;
+
import java.io.IOException;
import java.io.InputStream;
import java.net.URL;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.nio.file.Paths;
+import java.security.GeneralSecurityException;
import java.security.KeyManagementException;
import java.security.KeyStore;
import java.security.KeyStoreException;
@@ -17,6 +19,10 @@
import java.security.cert.CertificateException;
import java.security.cert.CertificateFactory;
import java.security.cert.X509Certificate;
+import java.util.concurrent.atomic.AtomicInteger;
+import java.util.stream.Stream;
+
+import static java.nio.file.Files.isDirectory;
public class TrustStoreLoader {
@@ -30,92 +36,117 @@ public static KeyStore build(ProvidesCertificateResourcePaths hasCertificatePath
}
return trustStore;
- } catch (KeyStoreException | CertificateException | NoSuchAlgorithmException | IOException | KeyManagementException e) {
+ } catch (KeyStoreException | CertificateException | NoSuchAlgorithmException | IOException e) {
throw new ConfigurationException("Unable to load certificates into truststore", e);
}
}
- private static void loadCertificatesInto(String certificateFolder, final KeyStore trustStore) throws IOException, CertificateException, KeyStoreException, NoSuchAlgorithmException, KeyManagementException {
+ private static void loadCertificatesInto(String certificateLocation, KeyStore trustStore) {
ResourceLoader certificateLoader;
- if (certificateFolder.indexOf("classpath:") == 0) {
- certificateLoader = new ClassPathFileLoader(certificateFolder);
+ if (certificateLocation.startsWith(ClassPathResourceLoader.CLASSPATH_PATH_PREFIX)) {
+ certificateLoader = new ClassPathResourceLoader(certificateLocation);
} else {
- certificateLoader = new FileLoader(certificateFolder);
+ certificateLoader = new FileLoader(certificateLocation);
}
- certificateLoader.forEachFile(new ForFile() {
- @Override
- void call(String fileName, InputStream contents) {
- try {
- X509Certificate ca = (X509Certificate) CertificateFactory.getInstance("X.509").generateCertificate(contents);
- trustStore.setCertificateEntry(fileName, ca);
- } catch (CertificateException | KeyStoreException e) {
- throw new ConfigurationException("Unable to load certificate in " + fileName);
- }
- }
+ certificateLoader.forEachFile((fileName, contents) -> {
+ X509Certificate ca = (X509Certificate) CertificateFactory.getInstance("X.509").generateCertificate(contents);
+ trustStore.setCertificateEntry(fileName, ca);
});
+ try {
+ TrustManagerFactory tmf = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm());
+ tmf.init(trustStore);
- TrustManagerFactory tmf = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm());
- tmf.init(trustStore);
-
- SSLContext context = SSLContext.getInstance("TLS");
- context.init(null, tmf.getTrustManagers(), null);
+ SSLContext context = SSLContext.getInstance("TLS");
+ context.init(null, tmf.getTrustManagers(), null);
+ } catch (NoSuchAlgorithmException | KeyStoreException | KeyManagementException e) {
+ throw new ConfigurationException("Error initializing SSLContext for certification location " + certificateLocation, e);
+ }
}
- private static class ClassPathFileLoader implements ResourceLoader {
+
+ private static class ClassPathResourceLoader implements ResourceLoader {
static final String CLASSPATH_PATH_PREFIX = "classpath:";
- private final String certificatePath;
+ private final String resourceName;
- ClassPathFileLoader(String certificateFolder) {
- this.certificatePath = certificateFolder.substring(CLASSPATH_PATH_PREFIX.length());
+ ClassPathResourceLoader(String resourceName) {
+ this.resourceName = resourceName.replaceFirst(CLASSPATH_PATH_PREFIX, "");
}
@Override
- public void forEachFile(ForFile forEachFile) throws IOException {
- URL contentsUrl = Certificates.class.getResource(certificatePath);
+ public void forEachFile(ForFile forEachFile) {
+ URL resourceUrl = TrustStoreLoader.class.getResource(resourceName);
+ if (resourceUrl == null) {
+ throw new ConfigurationException(resourceName + " not found on classpath");
+ }
- try (InputStream inputStream = contentsUrl.openStream()){
- forEachFile.call(new File(contentsUrl.getFile()).getName(), inputStream);
+ try (InputStream inputStream = resourceUrl.openStream()) {
+ forEachFile.call(generateAlias(resourceName), inputStream);
+ } catch (Exception e) {
+ throw new ConfigurationException("Unable to load certificate from classpath: " + resourceName, e);
}
}
}
private static class FileLoader implements ResourceLoader {
- private final File path;
+ private final Path path;
FileLoader(String certificateFolder) {
- this.path = new File(certificateFolder);
+ this.path = Paths.get(certificateFolder);
}
@Override
- public void forEachFile(ForFile forEachFile) throws IOException {
- if (!this.path.isDirectory()) {
+ public void forEachFile(ForFile forEachFile) {
+ if (!isDirectory(path)) {
throw new ConfigurationException("Certificate path '" + this.path + "' is not a directory. " +
"It should point to a directory containing certificates.");
}
- File[] files = this.path.listFiles();
- if (files == null) {
- throw new ConfigurationException("Unable to read certificates from '" + path + "'. Make sure it's the correct path.");
- }
- for (File file : files) {
- try (InputStream contents = new FileInputStream(file)) {
- forEachFile.call(file.getName(), contents);
- }
+ try (Stream files = Files.list(path)) {
+ files.forEach(file -> {
+ try (InputStream contents = Files.newInputStream(file)) {
+ forEachFile.call(generateAlias(file), contents);
+ } catch (Exception e) {
+ throw new ConfigurationException("Unable to load certificate from file " + file, e);
+ }
+ });
+ } catch (IOException e) {
+ throw new ConfigurationException("Error reading certificates from " + path, e);
}
}
}
private interface ResourceLoader {
- void forEachFile(ForFile forEachFile) throws IOException;
+ void forEachFile(ForFile forEachFile);
}
- private abstract static class ForFile {
- abstract void call(String fileName, InputStream contents);
+ @FunctionalInterface
+ private interface ForFile {
+ void call(String fileName, InputStream contents) throws IOException, GeneralSecurityException;
}
+ static String generateAlias(Path location) {
+ return generateAlias(location.toString());
+ }
+
+ private static final AtomicInteger aliasSequence = new AtomicInteger();
+
+ static String generateAlias(String resourceName) {
+ if (resourceName == null || resourceName.trim().isEmpty()) {
+ return "certificate-alias-" + aliasSequence.getAndIncrement();
+ }
+ String[] splitOnSlashes = resourceName.split("/");
+ int size = splitOnSlashes.length;
+ if (size == 1) {
+ return splitOnSlashes[0];
+ } else {
+ return splitOnSlashes[size - 2] + ":" + splitOnSlashes[size - 1];
+ }
+ }
+
+
}
diff --git a/src/main/java/no/digipost/signature/client/security/CertificateChainValidation.java b/src/main/java/no/digipost/signature/client/security/CertificateChainValidation.java
new file mode 100644
index 00000000..313748bf
--- /dev/null
+++ b/src/main/java/no/digipost/signature/client/security/CertificateChainValidation.java
@@ -0,0 +1,34 @@
+package no.digipost.signature.client.security;
+
+import javax.net.ssl.SSLContext;
+
+import java.security.cert.X509Certificate;
+
+public interface CertificateChainValidation {
+
+ enum Result {
+ /**
+ * Indicates that the certificate chain is trusted by this particular
+ * validation, but is subject to further validation by the {@link SSLContext}'s
+ * configured trust manager. This should be considered as the default result
+ * from a successful validation.
+ */
+ TRUSTED,
+
+ /**
+ * The certificate is determined to be trusted, and validation by the
+ * {@link SSLContext}'s trust manager should be skipped. This result is not appropriate
+ * for any integration with Posten signering, as it will effectively skip validating
+ * the certificate to be issued by the trusted CA hierarchy.
+ */
+ TRUSTED_AND_SKIP_FURTHER_VALIDATION,
+
+ /**
+ * The certificate chain has been determined to be not trusted.
+ */
+ UNTRUSTED
+ }
+
+ Result validate(X509Certificate[] certChain);
+
+}
diff --git a/src/main/java/no/digipost/signature/client/security/OrganizationNumberValidation.java b/src/main/java/no/digipost/signature/client/security/OrganizationNumberValidation.java
new file mode 100644
index 00000000..723edc93
--- /dev/null
+++ b/src/main/java/no/digipost/signature/client/security/OrganizationNumberValidation.java
@@ -0,0 +1,34 @@
+package no.digipost.signature.client.security;
+
+import no.digipost.security.X509;
+
+import java.security.cert.X509Certificate;
+
+/**
+ * Validates that the first certificate in a given certificate chain
+ * is issued to a specific Norwegian enterprise by inspecting its
+ * organization number.
+ */
+public class OrganizationNumberValidation implements CertificateChainValidation {
+
+ private final String trustedOrganizationNumber;
+
+ public OrganizationNumberValidation(String trustedOrganizationNumber) {
+ this.trustedOrganizationNumber = trustedOrganizationNumber;
+ }
+
+ @Override
+ public Result validate(X509Certificate[] certChain) {
+ return X509
+ .findOrganisasjonsnummer(certChain[0])
+ .filter(trustedOrganizationNumber::equals)
+ .map(trusted -> Result.TRUSTED)
+ .orElse(Result.UNTRUSTED);
+ }
+
+ @Override
+ public String toString() {
+ return getClass().getSimpleName() + " trusting '" + trustedOrganizationNumber + "'";
+ }
+
+}
diff --git a/src/test/java/no/digipost/signature/client/TestCertificates.java b/src/test/java/no/digipost/signature/client/TestCertificates.java
index 95b21d33..2cdae57d 100644
--- a/src/test/java/no/digipost/signature/client/TestCertificates.java
+++ b/src/test/java/no/digipost/signature/client/TestCertificates.java
@@ -2,6 +2,9 @@
import no.digipost.signature.client.security.KeyStoreConfig;
+import java.io.IOException;
+import java.io.InputStream;
+
public class TestCertificates {
private static final String password = "yJPvczYAoirFfC9M";
@@ -29,17 +32,19 @@ public class TestCertificates {
*/
public static KeyStoreConfig getJavaKeyStore() {
- return KeyStoreConfig.fromOrganizationCertificate(
- TestCertificates.class.getResourceAsStream("/bring-expired-certificate-for-testing.p12"),
- password
- );
+ try (InputStream p12InputStream = TestCertificates.class.getResourceAsStream("/bring-expired-certificate-for-testing.p12")) {
+ return KeyStoreConfig.fromOrganizationCertificate(p12InputStream, password);
+ } catch (IOException e) {
+ throw new RuntimeException(e.getMessage(), e);
+ }
}
public static KeyStoreConfig getOrganizationCertificateKeyStore() {
- return KeyStoreConfig.fromJavaKeyStore(
- TestCertificates.class.getResourceAsStream("/bring-expired-keystore-for-testing.jks"),
- "digipost testintegrasjon for digital post", password, password
- );
+ try (InputStream jksInputStream = TestCertificates.class.getResourceAsStream("/bring-expired-keystore-for-testing.jks")) {
+ return KeyStoreConfig.fromJavaKeyStore(jksInputStream, "digipost testintegrasjon for digital post", password, password);
+ } catch (IOException e) {
+ throw new RuntimeException(e.getMessage(), e);
+ }
}
diff --git a/src/test/java/no/digipost/signature/client/core/internal/http/SignatureApiTrustStrategyTest.java b/src/test/java/no/digipost/signature/client/core/internal/http/SignatureApiTrustStrategyTest.java
new file mode 100644
index 00000000..4c3b3b75
--- /dev/null
+++ b/src/test/java/no/digipost/signature/client/core/internal/http/SignatureApiTrustStrategyTest.java
@@ -0,0 +1,41 @@
+package no.digipost.signature.client.core.internal.http;
+
+import no.digipost.signature.client.TestCertificates;
+import no.digipost.signature.client.core.exceptions.SecurityException;
+import no.digipost.signature.client.security.CertificateChainValidation.Result;
+import org.junit.jupiter.api.Test;
+
+import java.security.cert.X509Certificate;
+
+import static no.digipost.signature.client.security.CertificateChainValidation.Result.TRUSTED;
+import static no.digipost.signature.client.security.CertificateChainValidation.Result.TRUSTED_AND_SKIP_FURTHER_VALIDATION;
+import static no.digipost.signature.client.security.CertificateChainValidation.Result.UNTRUSTED;
+import static org.hamcrest.MatcherAssert.assertThat;
+import static org.hamcrest.Matchers.is;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+import static uk.co.probablyfine.matchers.Java8Matchers.where;
+
+class SignatureApiTrustStrategyTest {
+
+ private static final X509Certificate[] certChain = new X509Certificate[]{TestCertificates.getOrganizationCertificateKeyStore().getCertificate()};
+
+ @Test
+ void translates_TRUSTED_to_false_to_not_override_SSLContext_validation() {
+ assertThat(TRUSTED, where(SignatureApiTrustStrategyTest::httpClientTrustStrategyTrust, is(false)));
+ }
+
+ @Test
+ void translates_TRUSTED_AND_SKIP_FURTHER_VALIDATION_to_true() {
+ assertThat(TRUSTED_AND_SKIP_FURTHER_VALIDATION, where(SignatureApiTrustStrategyTest::httpClientTrustStrategyTrust, is(true)));
+ }
+
+ @Test
+ void translates_UNTRUSTED_to_throwing_exception() {
+ assertThrows(SecurityException.class, () -> httpClientTrustStrategyTrust(UNTRUSTED));
+ }
+
+ private static boolean httpClientTrustStrategyTrust(Result result) {
+ return new SignatureApiTrustStrategy(certChain -> result).isTrusted(certChain, "authType");
+ }
+
+}
diff --git a/src/test/java/no/digipost/signature/client/core/internal/security/TrustStoreLoaderTest.java b/src/test/java/no/digipost/signature/client/core/internal/security/TrustStoreLoaderTest.java
index 0ccb60fe..5595c3b7 100644
--- a/src/test/java/no/digipost/signature/client/core/internal/security/TrustStoreLoaderTest.java
+++ b/src/test/java/no/digipost/signature/client/core/internal/security/TrustStoreLoaderTest.java
@@ -2,63 +2,172 @@
import no.digipost.signature.client.ClientConfiguration;
import no.digipost.signature.client.TestKonfigurasjon;
+import org.hamcrest.Description;
+import org.hamcrest.Matcher;
+import org.hamcrest.TypeSafeDiagnosingMatcher;
import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Nested;
import org.junit.jupiter.api.Test;
+import java.nio.file.Paths;
import java.security.KeyStore;
import java.security.KeyStoreException;
+import java.util.List;
+import java.util.stream.IntStream;
+import java.util.stream.Stream;
+import static java.util.Collections.list;
+import static java.util.stream.Collectors.toList;
import static no.digipost.signature.client.Certificates.PRODUCTION;
import static no.digipost.signature.client.Certificates.TEST;
+import static no.digipost.signature.client.core.internal.security.TrustStoreLoader.generateAlias;
import static org.hamcrest.MatcherAssert.assertThat;
+import static org.hamcrest.Matchers.containsInAnyOrder;
+import static org.hamcrest.Matchers.everyItem;
+import static org.hamcrest.Matchers.hasSize;
import static org.hamcrest.Matchers.is;
-import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.hamcrest.Matchers.matchesRegex;
+import static org.hamcrest.Matchers.notNullValue;
+import static org.junit.jupiter.api.Assertions.assertAll;
import static org.junit.jupiter.api.Assertions.assertTrue;
+import static org.quicktheories.QuickTheory.qt;
+import static org.quicktheories.generators.SourceDSL.strings;
+import static uk.co.probablyfine.matchers.Java8Matchers.where;
-public class TrustStoreLoaderTest {
+class TrustStoreLoaderTest {
private ClientConfiguration.Builder configBuilder;
@BeforeEach
- public void setUp() {
+ void setUp() {
configBuilder = ClientConfiguration.builder(TestKonfigurasjon.CLIENT_KEYSTORE);
}
@Test
- public void loads_productions_certificates_by_default() throws KeyStoreException {
- KeyStore keyStore = TrustStoreLoader.build(configBuilder.build());
+ void loads_productions_certificates_by_default() throws KeyStoreException {
+ KeyStore trustStore = TrustStoreLoader.build(configBuilder.build());
- assertThat(keyStore.size(), is(4));
- assertTrue(keyStore.containsAlias("bpclass3rootca.cer"), "Trust store should contain BuyPass root CA");
+ assertTrue(trustStore.containsAlias("prod:bpclass3rootca.cer"), "Trust store should contain BuyPass root CA");
+ assertThat(trustStore.size(), is(8));
}
@Test
- public void loads_productions_certificates() throws KeyStoreException {
+ void loads_productions_certificates() throws KeyStoreException {
ClientConfiguration config = configBuilder.trustStore(PRODUCTION).build();
- KeyStore keyStore = TrustStoreLoader.build(config);
-
- assertThat(keyStore.size(), is(4));
- assertTrue(keyStore.containsAlias("bpclass3rootca.cer"), "Trust store should contain bp root ca");
- assertFalse(keyStore.containsAlias("buypass_class_3_test4_root_ca.cer"), "Trust store should not contain buypass test root ca");
+ KeyStore trustStore = TrustStoreLoader.build(config);
+
+ assertThat(trustStore, containsExactlyTheAliases(
+ "prod:bpclass3rootca.cer",
+ "prod:bpclass3ca3.cer",
+ "prod:bpcl3rootcag2st.cer",
+ "prod:bpcl3cag2stbs.cer",
+ "prod:bpcl3rootcag2ht.cer",
+ "prod:bpcl3cag2htbs.cer",
+ "prod:commfides_root_ca.cer",
+ "prod:commfides_ca.cer"));
}
@Test
- public void loads_test_certificates() throws KeyStoreException {
+ void loads_test_certificates() throws KeyStoreException {
ClientConfiguration config = configBuilder.trustStore(TEST).build();
- KeyStore keyStore = TrustStoreLoader.build(config);
-
- assertThat(keyStore.size(), is(5));
- assertFalse(keyStore.containsAlias("bpclass3rootca.cer"), "Trust store should not buypass root ca");
- assertTrue(keyStore.containsAlias("buypass_class_3_test4_root_ca.cer"), "Trust store should contain buypass test root ca");
+ KeyStore trustStore = TrustStoreLoader.build(config);
+
+ assertThat(trustStore, containsExactlyTheAliases(
+ "test:buypass_class_3_test4_root_ca.cer",
+ "test:buypass_class_3_test4_ca_3.cer",
+ "test:bpcl3rootcag2st.cer",
+ "test:bpcl3cag2stbs.cer",
+ "test:bpcl3rootcag2ht.cer",
+ "test:bpcl3cag2htbs.cer",
+ "test:commfides_test_root_ca.cer",
+ "test:commfides_test_ca.cer",
+ "test:digipost_test_root_ca.cert.pem"));
}
@Test
- public void loads_certificates_from_file_location() throws KeyStoreException {
+ void loads_certificates_from_file_location() throws KeyStoreException {
ClientConfiguration config = configBuilder.trustStore("./src/test/files/certificateTest").build();
- KeyStore keyStore = TrustStoreLoader.build(config);
+ KeyStore trustStore = TrustStoreLoader.build(config);
+
+ assertThat(trustStore, containsExactlyTheAliases("certificatetest:commfides_test_ca.cer"));
+ }
+
+ @Nested
+ class GenerateAlias {
+ @Test
+ void generateAliasFromUnixPath() {
+ assertThat(generateAlias(Paths.get("/blah/blah/funny/env/MyCert.cer")), is("env:MyCert.cer"));
+ }
+
+ @Test
+ void generateAliasFromWindowsPath() {
+ assertThat(generateAlias(Paths.get("C:/blah/blah/funny/env/MyCert.cer")), is("env:MyCert.cer"));
+ }
+
+ @Test
+ void generateAliasFromUnixFileInJarUrlString() {
+ assertThat(generateAlias("/blah/fun/prod/WEB-INF/lib/mylib.jar!/certificates/env/MyCert.cer"), is("env:MyCert.cer"));
+ }
+
+ @Test
+ void generateAliasFromWindowsFileInJarUrlString() {
+ assertThat(generateAlias("file:/C:/blah/fun/prod/WEB-INF/lib/mylib.jar!/certificates/env/MyCert.cer"), is("env:MyCert.cer"));
+ }
+
+ @Test
+ void generateUniqueDefaultAliasesForNullsAndEmptyStrings() {
+ List defaultAliases = Stream.of(null, "", " ", " \n ").map(s -> TrustStoreLoader.generateAlias(s)).collect(toList());
+ int aliasCount = defaultAliases.size();
+ assertAll("default aliases",
+ () -> assertThat(defaultAliases, everyItem(matchesRegex("certificate-alias-\\d+"))),
+ () -> assertAll(IntStream.range(0, aliasCount).mapToObj(defaultAliases::get).map(alias -> () -> {
+ List otherAliases = defaultAliases.stream().filter(a -> !alias.equals(a)).collect(toList());
+ assertThat("other alises than '" + alias + "'", otherAliases, hasSize(aliasCount - 1));
+ })));
+ }
+
+ @Test
+ void alwaysGeneratesAnAlias() {
+ qt()
+ .forAll(strings().allPossible().ofLengthBetween(0, 100))
+ .checkAssert(s -> assertThat(s, where(TrustStoreLoader::generateAlias, notNullValue())));
+ }
+
- assertThat(keyStore.size(), is(1));
}
+ private static Matcher containsExactlyTheAliases(String ... certAliases) {
+ return new TypeSafeDiagnosingMatcher() {
+
+ @Override
+ public void describeTo(Description description) {
+ description
+ .appendText("key store containing " + certAliases.length + " certificates with aliases: ")
+ .appendValueList("", ", ", "", certAliases);
+ }
+
+ @Override
+ protected boolean matchesSafely(KeyStore keyStore, Description mismatchDescription) {
+ try {
+ List actualAliases = list(keyStore.aliases());
+ Matcher> expectedAliases = containsInAnyOrder(certAliases);
+ if (!expectedAliases.matches(actualAliases)) {
+ expectedAliases.describeMismatch(actualAliases, mismatchDescription);
+ return false;
+ } else if (keyStore.size() != certAliases.length) {
+ mismatchDescription.appendText("contained " + keyStore.size() + " certificates");
+ }
+ return true;
+ } catch (KeyStoreException e) {
+ mismatchDescription
+ .appendText("threw exception retrieving the aliases from the key store: ")
+ .appendValue(e.getClass().getSimpleName());
+ throw new RuntimeException(e.getMessage(), e);
+ }
+ }
+
+ };
+ }
+
}
diff --git a/src/test/java/no/digipost/signature/client/security/OrganizationNumberValidationTest.java b/src/test/java/no/digipost/signature/client/security/OrganizationNumberValidationTest.java
new file mode 100644
index 00000000..33994179
--- /dev/null
+++ b/src/test/java/no/digipost/signature/client/security/OrganizationNumberValidationTest.java
@@ -0,0 +1,45 @@
+package no.digipost.signature.client.security;
+
+import no.digipost.signature.client.TestCertificates;
+import org.junit.jupiter.api.Test;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.security.cert.CertificateException;
+import java.security.cert.CertificateFactory;
+import java.security.cert.X509Certificate;
+
+import static no.digipost.signature.client.security.CertificateChainValidation.Result.TRUSTED;
+import static no.digipost.signature.client.security.CertificateChainValidation.Result.UNTRUSTED;
+import static org.hamcrest.MatcherAssert.assertThat;
+import static org.hamcrest.Matchers.is;
+import static uk.co.probablyfine.matchers.Java8Matchers.where;
+
+public class OrganizationNumberValidationTest {
+
+ @Test
+ void trusts_SEID1_enterprise_certificate() {
+ X509Certificate seid1Cert = TestCertificates.getOrganizationCertificateKeyStore().getCertificate();
+ assertThat(seid1Cert.getSubjectX500Principal() + "is trusted",
+ seid1Cert, where(c -> new OrganizationNumberValidation("988015814").validate(new X509Certificate[]{c}), is(TRUSTED)));
+ }
+
+ @Test
+ void trusts_SEID2_enterprise_certificate() throws CertificateException, IOException {
+ X509Certificate seid2Cert;
+ try (InputStream certContents = getClass().getResourceAsStream("/test4-autentiseringssertifikat-vid-europa.cer")) {
+ seid2Cert = (X509Certificate) CertificateFactory.getInstance("X.509").generateCertificate(certContents);
+ }
+ assertThat(seid2Cert.getSubjectX500Principal() + "is trusted",
+ seid2Cert, where(c -> new OrganizationNumberValidation("100101688").validate(new X509Certificate[]{c}), is(TRUSTED)));
+ }
+
+ @Test
+ void unexpected_organization_number_is_untrusted() {
+ X509Certificate cert = TestCertificates.getOrganizationCertificateKeyStore().getCertificate();
+ assertThat(cert.getSubjectX500Principal() + "is trusted",
+ cert, where(c -> new OrganizationNumberValidation("0000").validate(new X509Certificate[]{c}), is(UNTRUSTED)));
+ }
+
+
+}
diff --git a/src/test/resources/test4-autentiseringssertifikat-vid-europa.cer b/src/test/resources/test4-autentiseringssertifikat-vid-europa.cer
new file mode 100644
index 00000000..bde76975
Binary files /dev/null and b/src/test/resources/test4-autentiseringssertifikat-vid-europa.cer differ