Skip to content

Commit

Permalink
JVMCBC-1066 Support reading multiple X.509 certificates from a single…
Browse files Browse the repository at this point in the history
… PEM file

Motivation
----------
With multi-CA support coming in Couchbase Server 7.1,
it will be useful to read multiple trust certificates
from the same file.

Modifications
-------------
Modify SecurityConfig.Builder.certificate(Path) to read
all certificates in the file instead of just the first one.

Modify decodeCertificates(List<String>) so all certificates
in each string are decoded, instead of just the first
certificate in each string.

Add Bytes.readAllBytes(InputStream).

Rework the certificate reading code to use InputStream
instead of String.

Change-Id: Id43dfcf5edba0ee1c8f587d22e5b6b4f62de506f
Reviewed-on: https://review.couchbase.org/c/couchbase-jvm-clients/+/170635
Tested-by: Build Bot <build@couchbase.com>
Reviewed-by: Michael Nitschinger <michael.nitschinger@couchbase.com>
  • Loading branch information
dnault committed Feb 17, 2022
1 parent 3a14e4f commit 93f7c39
Show file tree
Hide file tree
Showing 5 changed files with 185 additions and 30 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -17,17 +17,16 @@
package com.couchbase.client.core.env;

import com.couchbase.client.core.annotation.Stability;
import com.couchbase.client.core.deps.io.netty.handler.ssl.SslContextBuilder;
import com.couchbase.client.core.error.CouchbaseException;
import com.couchbase.client.core.error.InvalidArgumentException;
import com.couchbase.client.core.io.netty.SslHandlerFactory;
import com.couchbase.client.core.util.Bytes;

import javax.net.ssl.KeyManagerFactory;
import javax.net.ssl.TrustManagerFactory;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.security.KeyStore;
Expand All @@ -43,6 +42,7 @@

import static com.couchbase.client.core.util.Validators.notNull;
import static com.couchbase.client.core.util.Validators.notNullOrEmpty;
import static java.nio.charset.StandardCharsets.UTF_8;

/**
* The {@link SecurityConfig} allows to enable transport encryption between the client and the servers.
Expand Down Expand Up @@ -103,7 +103,7 @@ public static Builder enableTls(boolean tlsEnabled) {
}

/**
* Allows to enable or disable hostname verification (enabled by default).
* Allows enabling or disabling hostname verification (enabled by default).
* <p>
* Note that disabling hostname verification will cause the TLS connection to not verify that the hostname/ip
* is actually part of the certificate and as a result not detect certain kinds of attacks. Only disable if
Expand Down Expand Up @@ -137,9 +137,9 @@ public static Builder trustCertificates(final List<X509Certificate> certificates
}

/**
* Loads a X.509 trust certificate from the given path and uses it.
* Loads X.509 certificates from the specified file into the trust store.
*
* @param certificatePath the path to load the certificate from.
* @param certificatePath the path to load the certificates from.
* @return this {@link Builder} for chaining purposes.
*/
public static Builder trustCertificate(final Path certificatePath) {
Expand Down Expand Up @@ -356,24 +356,32 @@ public Builder trustCertificates(final List<X509Certificate> certificates) {
}

/**
* Loads a X.509 trust certificate from the given path and uses it.
* Loads X.509 certificates from the file at the given path into the trust store.
* <p>
* TIP: If you have multiple certificate files in PEM format (for example,
* "cert1.pem" and "cert2.pem"), and you want to create a single PEM file
* containing all the certificates, concatenate the PEM files using this shell command:
* <pre>
* $ cat cert1.pem cert2.pem > both-certs.pem
* </pre>
* Then, when configuring the SDK, call this method with the path to `both-certs.pem`
* as the argument.
*
* @param certificatePath the path to load the certificate from.
* @param certificatePath the file to load the certificates from.
* @return this {@link Builder} for chaining purposes.
*/
public Builder trustCertificate(final Path certificatePath) {
notNull(certificatePath, "CertificatePath");

final StringBuilder contentBuilder = new StringBuilder();
try {
Files.lines(certificatePath, StandardCharsets.UTF_8).forEach(s -> contentBuilder.append(s).append("\n"));
} catch (IOException ex) {
try (InputStream is = Files.newInputStream(certificatePath)) {
return trustCertificates(decodeCertificates(Bytes.readAllBytes(is)));

} catch (IOException e) {
throw InvalidArgumentException.fromMessage(
"Could not read trust certificate from file \"" + certificatePath + "\"" ,
ex
"Could not read trust certificates from file \"" + certificatePath + "\"",
e
);
}
return trustCertificates(decodeCertificates(Collections.singletonList(contentBuilder.toString())));
}

/**
Expand Down Expand Up @@ -468,22 +476,30 @@ public Builder ciphers(final List<String> ciphers) {
public static List<X509Certificate> decodeCertificates(final List<String> certificates) {
notNull(certificates, "Certificates");

final CertificateFactory cf;
return certificates.stream()
.flatMap(it -> decodeCertificates(it.getBytes(UTF_8)).stream())
.collect(Collectors.toList());
}

private static List<X509Certificate> decodeCertificates(byte[] bytes) {
notNull(bytes, "bytes");

try {
cf = CertificateFactory.getInstance("X.509");
//noinspection unchecked
return (List<X509Certificate>) getX509CertificateFactory()
.generateCertificates(new ByteArrayInputStream(bytes));
} catch (CertificateException e) {
throw InvalidArgumentException.fromMessage("Could not instantiate X.509 CertificateFactory", e);
String inputAsString = new String(bytes, UTF_8);
throw InvalidArgumentException.fromMessage("Could not generate certificates from raw input: \"" + inputAsString + "\"", e);
}
}

return certificates.stream().map(c -> {
try {
return (X509Certificate) cf.generateCertificate(
new ByteArrayInputStream(c.getBytes(StandardCharsets.UTF_8))
);
} catch (CertificateException e) {
throw InvalidArgumentException.fromMessage("Could not generate certificate from raw input: \"" + c + "\"", e);
}
}).collect(Collectors.toList());
private static CertificateFactory getX509CertificateFactory() {
try {
return CertificateFactory.getInstance("X.509");
} catch (CertificateException e) {
throw new CouchbaseException("Could not instantiate X.509 CertificateFactory", e);
}
}

/**
Expand Down
27 changes: 25 additions & 2 deletions core-io/src/main/java/com/couchbase/client/core/util/Bytes.java
Original file line number Diff line number Diff line change
Expand Up @@ -16,14 +16,37 @@

package com.couchbase.client.core.util;

import com.couchbase.client.core.error.CouchbaseException;

import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;

import static com.couchbase.client.core.util.Validators.notNull;

/**
* Defines useful constants and methods with regards to bytes.
* Defines useful constants and methods regarding bytes.
*/
public class Bytes {

/**
* Holds an empty byte array, so we do not need to create one every time.
*/
public static final byte[] EMPTY_BYTE_ARRAY = new byte[] {};
public static final byte[] EMPTY_BYTE_ARRAY = new byte[]{};

public static byte[] readAllBytes(InputStream is) {
notNull(is, "input stream");
try {
ByteArrayOutputStream result = new ByteArrayOutputStream();
byte[] buffer = new byte[4 * 1024];
int len;
while ((len = is.read(buffer)) != -1) {
result.write(buffer, 0, len);
}
return result.toByteArray();
} catch (IOException e) {
throw new CouchbaseException("Failed to read bytes from stream.", e);
}
}
}

Original file line number Diff line number Diff line change
Expand Up @@ -20,9 +20,19 @@
import com.couchbase.client.core.io.netty.SslHandlerFactory;
import org.junit.jupiter.api.Test;

import java.net.URISyntaxException;
import java.net.URL;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.List;

import static org.junit.jupiter.api.Assertions.*;
import static com.couchbase.client.core.util.CbCollections.listOf;
import static java.util.Objects.requireNonNull;
import static java.util.stream.Collectors.toList;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.junit.jupiter.api.Assertions.assertTrue;

class SecurityConfigTest {

Expand All @@ -48,4 +58,53 @@ void listsDefaultCiphersForNativeTls() {
}
}

}
@Test
void trustOneCertificateFromFile() {
checkCertificatesFromFile(
"one-certificate.pem",
listOf(
"CN=Couchbase Server 1d6c9ec6"
)
);
}

@Test
void trustTwoCertificatesFromFile() {
checkCertificatesFromFile(
"two-certificates.pem",
listOf(
"CN=Couchbase Server 1d6c9ec6",
"CN=Couchbase Server f233ba43"
)
);
}

private void checkCertificatesFromFile(
String resourceName,
List<String> expectedSubjectDns
) {
Path path = getResourceAsPath(getClass(), resourceName);
SecurityConfig config = SecurityConfig.trustCertificate(path).build();

assertEquals(
expectedSubjectDns,
config.trustCertificates().stream()
.map(it -> it.getSubjectDN().getName())
.collect(toList())
);
}

private static Path getResourceAsPath(
Class<?> loader,
String resourceName
) {
try {
URL url = loader.getResource(resourceName);
requireNonNull(url, "missing class path resource " + resourceName);
return Paths.get(url.toURI());

} catch (URISyntaxException e) {
throw new AssertionError(e);
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
-----BEGIN CERTIFICATE-----
MIIDAjCCAeqgAwIBAgIIFpZtHpcc9cgwDQYJKoZIhvcNAQELBQAwJDEiMCAGA1UE
AxMZQ291Y2hiYXNlIFNlcnZlciAxZDZjOWVjNjAeFw0xMzAxMDEwMDAwMDBaFw00
OTEyMzEyMzU5NTlaMCQxIjAgBgNVBAMTGUNvdWNoYmFzZSBTZXJ2ZXIgMWQ2Yzll
YzYwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDagbxtpv/RTBOS3LEL
yMJI4N1QPVmPyZfMR2XhOBQzzXpEWrIIoc5hW5hcCNnPs94hMgtrIK3o14//kRfS
EjGqIbKepdruNvGgkXLeASlTc3aCh4vVdWMSrGNjIJTOqkeagA1vyKE4BU592oGC
KhmMkAX/fJS2+aHgNMar9/4xqUic6eNScQhVSF9AbTt3c/87IHNDTPavatvbFllY
X+H1J/yErUwj9SJBaqpJhU7zUmdo6v28gp4kvN4sIjd7FpFf0n2usRRdPMEOKEIK
bA/Fu575oXs4/05AeP/ZG0xzU4kMOWIBxqejZ4hUFfQps7adQHaWD3BJVryNx04T
O+KXAgMBAAGjODA2MA4GA1UdDwEB/wQEAwICpDATBgNVHSUEDDAKBggrBgEFBQcD
ATAPBgNVHRMBAf8EBTADAQH/MA0GCSqGSIb3DQEBCwUAA4IBAQC7s5rYi+1QNJcH
sYRcmSZ5Rv9u4YN1cUYZ8Gb+RdfX2IBJff+vf15iBf4N2XWnA+zCn/WCm8sgaZ1Y
HP4Dn55aBIIfhOUdJ8bJL9eK9Ew9IQ4FrT1UkmDd/CqE4/pIwHamWfcpII20XnqE
FPiymngFAkAMAaynzku/Lw9VmbaafiLBUvwx3aJzF3totNd6LdigAG5iH4Ir2fhb
gtGoZ9ZuskKRJ8pGXu95DrJ6VJJQBwveUCYqHX+hx16iyMdsYZ/EObhbkXacEaBo
xFptQ/XVtEO/zh0gqSnUD/dROeUG28zbDKdP4Q1b70XE87HKnjYDcpfwfyJwo0Xg
FT2XIEXd
-----END CERTIFICATE-----
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
-----BEGIN CERTIFICATE-----
MIIDAjCCAeqgAwIBAgIIFpZtHpcc9cgwDQYJKoZIhvcNAQELBQAwJDEiMCAGA1UE
AxMZQ291Y2hiYXNlIFNlcnZlciAxZDZjOWVjNjAeFw0xMzAxMDEwMDAwMDBaFw00
OTEyMzEyMzU5NTlaMCQxIjAgBgNVBAMTGUNvdWNoYmFzZSBTZXJ2ZXIgMWQ2Yzll
YzYwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDagbxtpv/RTBOS3LEL
yMJI4N1QPVmPyZfMR2XhOBQzzXpEWrIIoc5hW5hcCNnPs94hMgtrIK3o14//kRfS
EjGqIbKepdruNvGgkXLeASlTc3aCh4vVdWMSrGNjIJTOqkeagA1vyKE4BU592oGC
KhmMkAX/fJS2+aHgNMar9/4xqUic6eNScQhVSF9AbTt3c/87IHNDTPavatvbFllY
X+H1J/yErUwj9SJBaqpJhU7zUmdo6v28gp4kvN4sIjd7FpFf0n2usRRdPMEOKEIK
bA/Fu575oXs4/05AeP/ZG0xzU4kMOWIBxqejZ4hUFfQps7adQHaWD3BJVryNx04T
O+KXAgMBAAGjODA2MA4GA1UdDwEB/wQEAwICpDATBgNVHSUEDDAKBggrBgEFBQcD
ATAPBgNVHRMBAf8EBTADAQH/MA0GCSqGSIb3DQEBCwUAA4IBAQC7s5rYi+1QNJcH
sYRcmSZ5Rv9u4YN1cUYZ8Gb+RdfX2IBJff+vf15iBf4N2XWnA+zCn/WCm8sgaZ1Y
HP4Dn55aBIIfhOUdJ8bJL9eK9Ew9IQ4FrT1UkmDd/CqE4/pIwHamWfcpII20XnqE
FPiymngFAkAMAaynzku/Lw9VmbaafiLBUvwx3aJzF3totNd6LdigAG5iH4Ir2fhb
gtGoZ9ZuskKRJ8pGXu95DrJ6VJJQBwveUCYqHX+hx16iyMdsYZ/EObhbkXacEaBo
xFptQ/XVtEO/zh0gqSnUD/dROeUG28zbDKdP4Q1b70XE87HKnjYDcpfwfyJwo0Xg
FT2XIEXd
-----END CERTIFICATE-----
-----BEGIN CERTIFICATE-----
MIIDAjCCAeqgAwIBAgIIFsmouG9qrMQwDQYJKoZIhvcNAQELBQAwJDEiMCAGA1UE
AxMZQ291Y2hiYXNlIFNlcnZlciBmMjMzYmE0MzAeFw0xMzAxMDEwMDAwMDBaFw00
OTEyMzEyMzU5NTlaMCQxIjAgBgNVBAMTGUNvdWNoYmFzZSBTZXJ2ZXIgZjIzM2Jh
NDMwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDRdb+uGSgCfkP2QYbe
4OI45cxp67I+43s3rVeK3/EVNdmcVsost943DRN8wGGlAl3UNMY499vJ/P5rV1+N
ALvsGGsgY9QuRUeiEPXFBsyQTA6kZOcCBhlUflPnZFH//OpwAgPBbU96BJRuIM/K
1gIrmcBLB9x8WHTOYIwEqwnqE6uReEJxG5L4J7oFj8zvrWRvQD3tkQgNpMKCJGJn
PleIxzzYA8VZ4XytE5O4rB/wFlGsPW4LDMAOLuZ6slpojFAEGbaxtHKtvwupMfgi
yzRmsJrHzsWd4Gy9ualeB+7r9fktDvzaffid2SW+z1sdP/FzNhTiiKOg4JgX+CB5
Zd+/AgMBAAGjODA2MA4GA1UdDwEB/wQEAwICpDATBgNVHSUEDDAKBggrBgEFBQcD
ATAPBgNVHRMBAf8EBTADAQH/MA0GCSqGSIb3DQEBCwUAA4IBAQC+/tKhyohI3/Bg
wBV3r9VoBkJ6j1r6oh6+ncx9Hu/DkJp7IYuwMcmzgXx7bYgTbjTpbB2rxUmwaTY7
V9iJU6iW1xmdE00wGDYIqcUq+quGl9cf0aqJWMwoETPCAt7Gl35PeuOMgBZN1Bez
Akh8ieoMJrOyL6bBP5j1zRMHdF+BhP5SKIIxriaPIlQAJXEAH0Q8VWphuO1qI/9w
8ZM3rDQmlZUZJoGznATEccgH6gC6TOnGbRlIqKDSob0doPzJHHKUxWDaqAZVGsgf
gJsIJjscpbmD05t74gCVwOSCDTwoFAYAAGx+PlxqV3/xTsfEow+8L64i5j7GYPwc
k9mh/NQD
-----END CERTIFICATE-----

0 comments on commit 93f7c39

Please sign in to comment.