From bfe7e4844dbcd0cc9e286e2f6c9cfeb62de8b046 Mon Sep 17 00:00:00 2001 From: Rune Flobakk Date: Tue, 28 Sep 2021 22:30:36 +0200 Subject: [PATCH 1/5] Allow overriding validation of server certificate --- .../signature/client/Certificates.java | 16 +---- .../signature/client/ClientConfiguration.java | 60 +++++++++++++------ .../PostenEnterpriseCertificateStrategy.java | 56 ----------------- .../http/SignatureApiTrustStrategy.java | 46 ++++++++++++++ .../internal/security/TrustStoreLoader.java | 5 +- .../security/CertificateChainValidation.java | 34 +++++++++++ .../OrganizationNumberValidation.java | 53 ++++++++++++++++ .../signature/client/TestCertificates.java | 21 ++++--- .../http/SignatureApiTrustStrategyTest.java | 41 +++++++++++++ .../OrganizationNumberValidationTest.java | 31 ++++++++++ 10 files changed, 268 insertions(+), 95 deletions(-) delete mode 100644 src/main/java/no/digipost/signature/client/core/internal/http/PostenEnterpriseCertificateStrategy.java create mode 100644 src/main/java/no/digipost/signature/client/core/internal/http/SignatureApiTrustStrategy.java create mode 100644 src/main/java/no/digipost/signature/client/security/CertificateChainValidation.java create mode 100644 src/main/java/no/digipost/signature/client/security/OrganizationNumberValidation.java create mode 100644 src/test/java/no/digipost/signature/client/core/internal/http/SignatureApiTrustStrategyTest.java create mode 100644 src/test/java/no/digipost/signature/client/security/OrganizationNumberValidationTest.java diff --git a/src/main/java/no/digipost/signature/client/Certificates.java b/src/main/java/no/digipost/signature/client/Certificates.java index dba6998a..a59b9cf0 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; @@ -25,18 +24,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:/no/digipost/signature/client/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 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(Consumernot + * 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..7578bdff 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 @@ -5,6 +5,7 @@ import javax.net.ssl.SSLContext; import javax.net.ssl.TrustManagerFactory; + import java.io.File; import java.io.FileInputStream; import java.io.IOException; @@ -76,7 +77,9 @@ private static class ClassPathFileLoader implements ResourceLoader { @Override public void forEachFile(ForFile forEachFile) throws IOException { URL contentsUrl = Certificates.class.getResource(certificatePath); - + if (contentsUrl == null) { + throw new ConfigurationException(certificatePath + " was not found"); + } try (InputStream inputStream = contentsUrl.openStream()){ forEachFile.call(new File(contentsUrl.getFile()).getName(), inputStream); } 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..5b98f4f9 --- /dev/null +++ b/src/main/java/no/digipost/signature/client/security/OrganizationNumberValidation.java @@ -0,0 +1,53 @@ +package no.digipost.signature.client.security; + +import java.security.cert.X509Certificate; +import java.util.List; +import java.util.stream.Stream; + +import static java.util.stream.Collectors.toList; + +/** + * 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 { + + /** + * Used by some obscure cases to embed Norwegian "organisasjonsnummer" in certificates. + */ + private static final String COMMON_NAME = "CN="; + + /** + * Most common way to embed Norwegian "organisasjonsnummer" in certificates. + */ + private static final String SERIALNUMBER = "SERIALNUMBER="; + + + private final String trustedOrganizationNumber; + private final List acceptedSubstrings; + + public OrganizationNumberValidation(String trustedOrganizationNumber) { + this.trustedOrganizationNumber = trustedOrganizationNumber; + this.acceptedSubstrings = Stream.of(COMMON_NAME + trustedOrganizationNumber, SERIALNUMBER + trustedOrganizationNumber) + .map(String::toLowerCase).collect(toList()); + } + + @Override + public Result validate(X509Certificate[] certChain) { + String subjectDN = certChain[0].getSubjectDN().getName(); + String lowerCaseSubjectDN = subjectDN.toLowerCase(); + for (String acceptedSubstring : this.acceptedSubstrings) { + if (lowerCaseSubjectDN.contains(acceptedSubstring)) { + return Result.TRUSTED; + } + } + return 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/security/OrganizationNumberValidationTest.java b/src/test/java/no/digipost/signature/client/security/OrganizationNumberValidationTest.java new file mode 100644 index 00000000..542d98f1 --- /dev/null +++ b/src/test/java/no/digipost/signature/client/security/OrganizationNumberValidationTest.java @@ -0,0 +1,31 @@ +package no.digipost.signature.client.security; + +import no.digipost.signature.client.TestCertificates; +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.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_Posten_Norge_SEID1_enterprise_certificate() { + X509Certificate cert = TestCertificates.getOrganizationCertificateKeyStore().getCertificate(); + assertThat(cert.getSubjectX500Principal() + "is trusted", + cert, where(c -> new OrganizationNumberValidation("988015814").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))); + } + + +} From 9f97d6ed2f8789dca2d53e29091bc5f92f716537 Mon Sep 17 00:00:00 2001 From: Rune Flobakk Date: Thu, 30 Sep 2021 13:15:12 +0200 Subject: [PATCH 2/5] Extract orgnr using certificate-validator lib This small implementation is shaded into the signature-api-client-java jar, to avoid version conflicts. As this functionality does not require BouncyCastle, it is excluded from the certificate-validator dependency. --- .gitignore | 2 + NOTICE | 1 + pom.xml | 52 +++++++++++++++++++ .../OrganizationNumberValidation.java | 33 +++--------- 4 files changed, 62 insertions(+), 26 deletions(-) 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..43dadba6 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 @@ -269,6 +280,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 +405,16 @@ + + maven-shade-plugin + + + + shade + + + + diff --git a/src/main/java/no/digipost/signature/client/security/OrganizationNumberValidation.java b/src/main/java/no/digipost/signature/client/security/OrganizationNumberValidation.java index 5b98f4f9..723edc93 100644 --- a/src/main/java/no/digipost/signature/client/security/OrganizationNumberValidation.java +++ b/src/main/java/no/digipost/signature/client/security/OrganizationNumberValidation.java @@ -1,10 +1,8 @@ package no.digipost.signature.client.security; -import java.security.cert.X509Certificate; -import java.util.List; -import java.util.stream.Stream; +import no.digipost.security.X509; -import static java.util.stream.Collectors.toList; +import java.security.cert.X509Certificate; /** * Validates that the first certificate in a given certificate chain @@ -13,36 +11,19 @@ */ public class OrganizationNumberValidation implements CertificateChainValidation { - /** - * Used by some obscure cases to embed Norwegian "organisasjonsnummer" in certificates. - */ - private static final String COMMON_NAME = "CN="; - - /** - * Most common way to embed Norwegian "organisasjonsnummer" in certificates. - */ - private static final String SERIALNUMBER = "SERIALNUMBER="; - - private final String trustedOrganizationNumber; - private final List acceptedSubstrings; public OrganizationNumberValidation(String trustedOrganizationNumber) { this.trustedOrganizationNumber = trustedOrganizationNumber; - this.acceptedSubstrings = Stream.of(COMMON_NAME + trustedOrganizationNumber, SERIALNUMBER + trustedOrganizationNumber) - .map(String::toLowerCase).collect(toList()); } @Override public Result validate(X509Certificate[] certChain) { - String subjectDN = certChain[0].getSubjectDN().getName(); - String lowerCaseSubjectDN = subjectDN.toLowerCase(); - for (String acceptedSubstring : this.acceptedSubstrings) { - if (lowerCaseSubjectDN.contains(acceptedSubstring)) { - return Result.TRUSTED; - } - } - return Result.UNTRUSTED; + return X509 + .findOrganisasjonsnummer(certChain[0]) + .filter(trustedOrganizationNumber::equals) + .map(trusted -> Result.TRUSTED) + .orElse(Result.UNTRUSTED); } @Override From 05fb86643b211c423cd76159abb0ca2cc30ae3bc Mon Sep 17 00:00:00 2001 From: Rune Flobakk Date: Thu, 30 Sep 2021 14:52:35 +0200 Subject: [PATCH 3/5] Load certs bundled from certificate-validator By using classpath reference. --- pom.xml | 5 --- src/main/certificates/prod/BPClass3CA3.cer | Bin 1231 -> 0 bytes src/main/certificates/prod/BPClass3RootCA.cer | Bin 1373 -> 0 bytes src/main/certificates/prod/commfides_ca.cer | Bin 1283 -> 0 bytes .../certificates/prod/commfides_root_ca.cer | Bin 1087 -> 0 bytes .../test/Buypass_Class_3_Test4_CA_3.cer | Bin 1251 -> 0 bytes .../test/Buypass_Class_3_Test4_Root_CA.cer | Bin 1385 -> 0 bytes .../certificates/test/commfides_test_ca.cer | Bin 1622 -> 0 bytes .../test/commfides_test_root_ca.cer | Bin 1470 -> 0 bytes .../test/digipost_test_root_ca.pem | 33 ------------------ .../signature/client/Certificates.java | 4 +-- 11 files changed, 2 insertions(+), 40 deletions(-) delete mode 100644 src/main/certificates/prod/BPClass3CA3.cer delete mode 100644 src/main/certificates/prod/BPClass3RootCA.cer delete mode 100644 src/main/certificates/prod/commfides_ca.cer delete mode 100644 src/main/certificates/prod/commfides_root_ca.cer delete mode 100644 src/main/certificates/test/Buypass_Class_3_Test4_CA_3.cer delete mode 100644 src/main/certificates/test/Buypass_Class_3_Test4_Root_CA.cer delete mode 100644 src/main/certificates/test/commfides_test_ca.cer delete mode 100644 src/main/certificates/test/commfides_test_root_ca.cer delete mode 100644 src/main/certificates/test/digipost_test_root_ca.pem diff --git a/pom.xml b/pom.xml index 43dadba6..43f36356 100644 --- a/pom.xml +++ b/pom.xml @@ -216,11 +216,6 @@ src/main/resources true - - src/main/certificates - false - no/digipost/signature/client/certificates - diff --git a/src/main/certificates/prod/BPClass3CA3.cer b/src/main/certificates/prod/BPClass3CA3.cer deleted file mode 100644 index a29ff185a304f63938dc4cddff0b7253588ccb22..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1231 zcmXqLVmWQl#I$(Zl`k>PQbl%MdX2KDdd)KS8Wy9nRP2=)%Kkgf)3n{f8-DU|K5A3KYy>b zWW!fuKC4Y48TuKlDu18xPmHL3m&vfI^J++}=>AvMAD<_DK6pI)bB1|O{P4uSpP9eYSLc+koi0I+_`Xx@nuqK>#nRr)y)Z43PPJtU3~8{ zMR)JLwf>U-Y+W95KT>Tlig~r8GovGdZ6U9MUH1l|yAu^Vw9bU`oj7bc_vh;aUt^cJ zf6dHzQSs&d-QJ>i2UbpHVrFDuT->&fZMr+i~g zj(pz#3g(c2)koXKrp*tiV(a5vv5xTtr{sQ>16xlcxNrS5?=k} zweP3n@*TVhcKclKd%riba%D-dj!m7pbQ$-AU7;5|YhH){V_3Y*cth*{z<8#)l9kb1 zT;9|E#kL$5>f`7q{@)Oyz;i)Gy{KK)`*9)o>s3n?&a0jXnl3^$eA-M4BGFVvn@)QypPfHX6z%K zd7A{IXL8?=Y?pTQeCt(zZoTcc&50kMu?pw#C#-NVE|oO3RX(%nW#*;;fq+#DYp)A@ zGh%4tJd^f*jv9mQYopJHCBCsah8oN_d(*i-YPDEx0|BN`Hri!$i|B@_@RIH48J!@8(aq5AuK64lRcB;)1>%*4n9L*4Ezka4LI4DLs{5_nf&|>Weuc3 z94;Oar_#!T#NuKF$6#Gc3u8kwV`C$8Lj?mlkTf%oI853(2c*bYAt*n;M8Vn7Ku(<3 z(7@2Z$jrdP$imbjN}SgOnM-y1nwXT3y~N1Mz}&>d&tTBR#KqLa#K^Gp7S~j775ue>tg;F2s6Ci_|>?|`&#G^)?V+-4PQ5Fn8k=32`u)_=Kg5H zep}q~`W=nx?631b>mBqL*An=A=G2SVf3sKX&6=;l>a@=X;-<|xeB}M%n(S_?%KFWo=WQQ#&R<~Q-MVwm z6qU;U+U%9C8T{H8KHic!ow?(bXHxEo3&}}$6Zj)m^)KARx-@OkdFirEEh!s)EQ1ah z?E28-&9q_Lb|z*<2FArs1`Y=Nz?3U1%*gnkh1Gx=NEyh21o&9QSVY`+oc;ea|J{#G zZ}k26eyVQhv&lYezz34%2dQTP<{dWVR0hmAz*NS_z@VV)Xv#9-kV8_!`$wie*X`Fl zGMTVrVSd>!F|j2c<~lDY9QtJQgKPec#;3cZZ>j1mSsq%bV)X3<_oqEsKPIo)#C|A# z{_Qg-x>oJW%75r7aK`X@!|vTDZY<(U34gWd>!Fo~qL&o1-uzKw)HvU^qd{)v8NsDC zpTsu1&;4_$LGj4x3+@Gu3r{sma_HoKiY=ehB_e#qOlH!Dr)=hTmOYy=+oC@vTiUJ7 z*(u(Rkv%vhHnsM9SMkDGAyM+0&LZlETKTtjR7+V;*)E%~eCwR&nwjBIvnN+M8%Q%> zPdeeSY}KEXCtlYl1~aZu=*XKB@3QlBLZiu>{L8hsJmOz(nXpH9r408PnX8L36dG8r z{n^;BzuU`@*Q-tD^&>M)wXe5s9Qjn5c)o63S+ZA^L+a%AA94*nZBN^-_4EA+)=79F zmMFEz&bj)mWkN{S*H4{lj2X=yi;Q2KWSO{0&_vMg)dgosdFIIzc>V|4hJSI)dw6=m zzSk#{&*q+a^22OZ#@x4iW2YCm@NVNXz4MSKcK3q7DW~hiUJL3kp5%JMr|{;UlQV%%ZK)2w-M7YFQ+6NTy+5_$Qd{MJPiv`Wo)7mb-kkX4>mzlqN%P0S zs9SH}JyVZ+;d)`NOyG)))h>@ObZ0&OVZHl~@AZ^Vf%~4VS{?P$@EzNp;{C!&T=S3b GoDKkgi$p^J diff --git a/src/main/certificates/prod/commfides_ca.cer b/src/main/certificates/prod/commfides_ca.cer deleted file mode 100644 index 7451ab3e4854d73bbab506dac1d73207046cdc72..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1283 zcmXqLV)<{-#Qb~#GZP~d6Nki~DOUc)x%&-x**LY@JlekVGBR?rG8i<@GgLBAU}Fwt zVHTEj4)9Y5%Fi!xc2o%Va5OSCQ*h2nEG||sHgqts1F7K_Hh0d?%}vWpNi9|gDJm^4 zQE<&G%Ph*z%T3KIQP4=%R4_6yG*mz+^2;wuPgQUXHqp6lxxtq$WYBd1>yl& zkOy4zN>YmoiZY8+;hyku3=SSpPc$(qAqPAoD+6;ABR_*d6C)Q>6C)$TmUVw$Fv$M8 zS$ot=WzX%cNAG*TP`J5v=Jl2h-Zl=h{eq@navrWY&XMrw-0bW#C#N0B>d)y=XV@Fr zpnjw+%16q!gqd_Ucxzn%bw3ilZR9^%NsP5Li9kXqD;x6+ee<}?l4=&jKHsHd--_Itz%<=t~ z{%OTNU7-j6lip>XKVRN2ouYd;tK24Lfv&`!XV1hN_1l|eJ_zdD?sxn8CBpFiUxs4_ zYZ#fB85tNCH!)fQL&Vg;nvFx7jggg=osp46-$2(u8^$+aY?IF@DJihh*UwKbE-=(f zh9yQ23oK?J3rq^Kd@N!tB9qQG$jBYsS!uI%Mf0Q+F&(`g=XM(KgQSHS8UM4e8ZZMX z19^~uGK++PScAy4w)h7gSY>NG<06Hc9cAnW-- z9$^9I@HXV+1rLV>jqXiQ{U}!_DRed0>`@}~ZAPb$*Np!= zrfXJtFFCDs!X;X)@~Zk`i<568IdW~AdHQi=Q4T#Q5#}ek*%H)YxZJ%|E#%_TBaaSS7`F*ej+2eQ z=Q^phamfR-`)6)7ylq=l>2WLO@=QAqr%Q(}iY(q6>z)y-u}GxoM9<{g=BppvSahz| f72%`C1zIX&F~CKc7B 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 b66117540e3eda313662b401d2a802a493e2d7b3..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1087 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 MV73tr^1n6>0GS+7Hvj+t 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 8cacb78b2852d19a2b16b1d4d8331e4be2050c53..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1622 zcmXqLVhb{8VzpYp%*4pV#1XN{_+;-X&TIo-HcqWJkGAi;jEvl@3iUzAyunxarzoT`wYms4q|Z=efNtOimXP?VXQSX8OtoLW?pnUt>F^Mv0{A_4zU=32wENp`C706Ytc_pbu1x1;~sk%Ttgq&CySs9p{82Ldd zhKs3*k&)rjswnx|yaoFo?|ZuJ*R{W=x}4uXx&P93!N#MX7hHcZS=^a>LonwQ&ld+i za_{BuI5DltuW!5CtUa^0t(j0#8rwcYt}^`nQ8W6&kNJ0YT4{Xww3NGP zO~cGE&HlDmA(t;3$V^(Wep>vp({HpHJNF$+^l{oJFyU^ineX)EJDH>&7t;?ZOT`PfrFTn|9_*i=q?_NbxU%WeYzDTStD7CSM>Se* zQ*zBOn}6Gk>0BvaW6!K3`K;k@V_M^06|87fd=aFVzsxuM9FNOImFh$Kn=YkO^i2yA#%l_@eCV>HX9==D?1}2OXGfn#ytkk zFcAZ$HuH>2lcxlb>-s05cGMkEG59$W7q=s+F9wEKA-qFnY%Mbv24EMF!#^1qv(z27CrQY+MN~ zPK>hc0>D&c(AWWzk!NXaFsL)IU0}VylH9<`!PHJlFy$EVfn355@)ZlP&}2hS{lKCU znEDwRHoQp=MG%__)aCTJCRS0nn4l#5wa002}7PfZI&&^HCOi3+P2q`Kp zE>UpJE6Xg(&&y5CE75S)1glmsGB7kyKq&OfFG^2Ua11uoHqeBaq6D%DWH8WtxBMc7 z#L|+C{G!aN)D(r%;#7tFyqroyeFI&HVl|NBfTGOY#G*41fsvt+sj-Pk zlsK;uhznE;=8lFDFpQcQm5`GTBP#=Q6C*z;{ctfgF)}i2zwspNCaC6ltDr7%`uC(*+bbLw4%kjjtU+ly=%d7Q;nf zm5Y3wcP2Xar$0Zir~8mj&W%mq(r@R_JTdjf>yG!2@=n*z)cl^G=q7*k`%Z@`%O>)F zY1=KR@%Zr6pml8eL5yF#r)aX@b60TjZ#};Hx(UZEmdkT*PXE5pcs4_k_hu$$Mh3>k zO^ims5YaPmVB^qcV`ODzXJlkCF)%VPfbk6&+f*`2N(!v>_4AX93k>y2Qj1IUl3`g2 z!~#ng$O4n6EFX&)i-_C=-mhB8Da*3tJp-d>oL^U?=u%|B50VyUWc<&uURh)AV`o7N+u>D223Z?ang%yHD+A{jIy*#*C>yPF8Q> z{U4(BjVs_9cWQ92Q=#)d@5M`uEPl1mb#qvns&ZUp%RPNtiGJ}K?zgV~d_N%&fhN7lfkL-{d)PrJrYm0~c@Xot2m3p8db4 kzhJ??1$WmjP2^bO Date: Fri, 1 Oct 2021 00:43:01 +0200 Subject: [PATCH 4/5] Trust Buypass SEID2 enterprise certificates Modifies the alias-generating for certificate to include the name of the containing folder, because the file names for test and production CA certs are the same, and to be able to reliably test that the trusts are containing the expected certifiates. The aliases are not used by the client. Add test that we recognize orgnr in SEID2 certs --- .../signature/client/Certificates.java | 14 +++ .../internal/security/TrustStoreLoader.java | 113 ++++++++++-------- .../security/TrustStoreLoaderTest.java | 95 +++++++++++---- .../OrganizationNumberValidationTest.java | 22 +++- ...t4-autentiseringssertifikat-vid-europa.cer | Bin 0 -> 1593 bytes 5 files changed, 170 insertions(+), 74 deletions(-) create mode 100644 src/test/resources/test4-autentiseringssertifikat-vid-europa.cer diff --git a/src/main/java/no/digipost/signature/client/Certificates.java b/src/main/java/no/digipost/signature/client/Certificates.java index 71344dc3..9dc89879 100644 --- a/src/main/java/no/digipost/signature/client/Certificates.java +++ b/src/main/java/no/digipost/signature/client/Certificates.java @@ -10,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.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" ); 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 7578bdff..dadf56f3 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,16 +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; @@ -18,6 +19,12 @@ import java.security.cert.CertificateException; import java.security.cert.CertificateFactory; import java.security.cert.X509Certificate; +import java.util.UUID; +import java.util.stream.Stream; + +import static java.nio.file.Files.isDirectory; +import static java.util.stream.Collectors.joining; +import static java.util.stream.StreamSupport.stream; public class TrustStoreLoader { @@ -31,93 +38,103 @@ 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); - if (contentsUrl == null) { - throw new ConfigurationException(certificatePath + " was not found"); + 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(Paths.get(resourceUrl.getPath())), 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); + } + + @FunctionalInterface + private interface ForFile { + void call(String fileName, InputStream contents) throws IOException, GeneralSecurityException; } - private abstract static class ForFile { - abstract void call(String fileName, InputStream contents); + private static String generateAlias(Path location) { + return stream(location.normalize().spliterator(), false) + .reduce((e1, e2) -> e1.getFileName().resolve(e2)) + .map(nameBase -> stream(nameBase.spliterator(), false).map(Path::toString).collect(joining(":"))) + .orElseGet(() -> UUID.randomUUID().toString()); } } 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..c34c6311 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,114 @@ 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.Test; import java.security.KeyStore; import java.security.KeyStoreException; +import java.util.List; +import static java.util.Collections.list; import static no.digipost.signature.client.Certificates.PRODUCTION; import static no.digipost.signature.client.Certificates.TEST; import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.containsInAnyOrder; import static org.hamcrest.Matchers.is; -import static org.junit.jupiter.api.Assertions.assertFalse; import static org.junit.jupiter.api.Assertions.assertTrue; -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(keyStore.size(), is(1)); + assertThat(trustStore, containsExactlyTheAliases("certificatetest:commfides_test_ca.cer")); } + 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 index 542d98f1..33994179 100644 --- a/src/test/java/no/digipost/signature/client/security/OrganizationNumberValidationTest.java +++ b/src/test/java/no/digipost/signature/client/security/OrganizationNumberValidationTest.java @@ -3,6 +3,10 @@ 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; @@ -14,10 +18,20 @@ public class OrganizationNumberValidationTest { @Test - void trusts_Posten_Norge_SEID1_enterprise_certificate() { - X509Certificate cert = TestCertificates.getOrganizationCertificateKeyStore().getCertificate(); - assertThat(cert.getSubjectX500Principal() + "is trusted", - cert, where(c -> new OrganizationNumberValidation("988015814").validate(new X509Certificate[]{c}), is(TRUSTED))); + 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 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 0000000000000000000000000000000000000000..bde76975068da7f6ad9ae132e8aa266ad99e96a3 GIT binary patch literal 1593 zcmZuxc|4SP7@zl@F^(BmgHV`|R2skcb&Mi1gJC6NkR$6p)-`KTSkhFJbB8XGGTM@( zh)V8u*|v1(QaQ2`ManvMtEDlsGh6Ma?bDyn^Zh>G=Xrk5^ZWn@Zw5FxEj|{8V3+{q zH7)G#snF@GYXOn4naI1e7I+98r#zl)?&Pv&ps|2mlm`6H8q6$w`lqKqh2n7(3CFOt&R7P_hS; zOiN-%#f7oiKzpe?iU2BVfta8GrTU6ZQ;CTpX6pg)1wQ|;XDLwqkX3#`44CeC+uXi7ZEE9U zXB9TDHe#{m+zLX3i(Z?x-DNWWTE1DIng2sCshg*|3zNM|uDafsUg=rj-Ke%)SgAWK zW#qZ1oo7cBO-xj)VcU4K6)mI=4fXYPtm&H5+g{>yM}jMVc~xc&1TbSp=a^&7z3L(% zDXVeVs68|<+UdFf_3X)u7~MErbm88YDRiSuGU;WHj;97!zIW*Y_m<%b!y@_W_N7SGUCZK6+3tt1E>_4ni1JwsmGF28RGYCPd) z=h}5GezddJ-O_(;;ij|LoTcTNZTdsp?dy6y!!f*lIm#M{T4_s%tmAs{t! z{$A^qJB_(OOF~eTN=Enwpqf8F6Fi|ifNmGf&^BCAQ29Sg{Al(^vDuD zAcvO;+)INrAuJ4J@OT`CmkL3^RuZ6t+W;(pIp36zazSKbB5Q-OaY#Z8B~h~3l;96X z5E4WQiH|j=xzS@x=s^tBgGuwCh*=;FFS8a8Nl8gV5H23Xf_0y40C87*RErN`vx3=Q z69iWO(c#aMCw`6lW%95Du<}zhgvDt9$*FuSh5(D-RT_uMeeDFnpC5zxE5LGK2!p_N zn3=_2gHtYWXYU6p^x|5SMe3Vs&;{s$;Pyy(dP~W7KhQIt^{AX|lhR_B(3^Hv<#t(z zUY)?7)(za}ey`{}@AnF4hW+xi&_wan$= zm3bHMz>Z|SUCkFQy&WHHJR++q|0r6J@9wK)*x#;mBxH2Wb|uRghC@E;JjO4$xZjZL zy708JbYxh$s?X~+*^V7|w9{YpvGsJB=Blpdsh50|J1 z$QRQdeB&XId)JF~NiO4#9 zu;gs_JxSl?LMGvB@YLDER_PJhacKkIH>x3xQ>ntnw(y4GyPIzn?^1-S?GftrKkrZp xnU2|qH Date: Tue, 28 Jun 2022 14:56:38 +0200 Subject: [PATCH 5/5] Fix unsafe implementation to generate cert aliases Was based on an assumption that a path string obtained from a classpath URL can always be converted to a Path instance, which is not the case, esp. on Windows. Doing e.g. Paths.get("file:/C:/path/WEB-INF/lib/lib.jar!/file/in/jar") will make sun.nio.fs.WindowsPathParser throw an InvalidPathException. --- .../internal/security/TrustStoreLoader.java | 29 +++++++--- .../security/TrustStoreLoaderTest.java | 58 +++++++++++++++++++ 2 files changed, 78 insertions(+), 9 deletions(-) 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 dadf56f3..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 @@ -19,12 +19,10 @@ import java.security.cert.CertificateException; import java.security.cert.CertificateFactory; import java.security.cert.X509Certificate; -import java.util.UUID; +import java.util.concurrent.atomic.AtomicInteger; import java.util.stream.Stream; import static java.nio.file.Files.isDirectory; -import static java.util.stream.Collectors.joining; -import static java.util.stream.StreamSupport.stream; public class TrustStoreLoader { @@ -86,7 +84,7 @@ public void forEachFile(ForFile forEachFile) { } try (InputStream inputStream = resourceUrl.openStream()) { - forEachFile.call(generateAlias(Paths.get(resourceUrl.getPath())), inputStream); + forEachFile.call(generateAlias(resourceName), inputStream); } catch (Exception e) { throw new ConfigurationException("Unable to load certificate from classpath: " + resourceName, e); } @@ -130,12 +128,25 @@ private interface ForFile { void call(String fileName, InputStream contents) throws IOException, GeneralSecurityException; } - private static String generateAlias(Path location) { - return stream(location.normalize().spliterator(), false) - .reduce((e1, e2) -> e1.getFileName().resolve(e2)) - .map(nameBase -> stream(nameBase.spliterator(), false).map(Path::toString).collect(joining(":"))) - .orElseGet(() -> UUID.randomUUID().toString()); + 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/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 c34c6311..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 @@ -6,19 +6,33 @@ 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.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; class TrustStoreLoaderTest { @@ -78,6 +92,50 @@ void loads_certificates_from_file_location() throws KeyStoreException { 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()))); + } + + + } + private static Matcher containsExactlyTheAliases(String ... certAliases) { return new TypeSafeDiagnosingMatcher() {