|
19 | 19 | import com.couchbase.analytics.client.java.internal.ThreadSafe; |
20 | 20 | import okhttp3.Credentials; |
21 | 21 | import okhttp3.tls.HandshakeCertificates; |
| 22 | +import okhttp3.tls.HeldCertificate; |
| 23 | +import org.jspecify.annotations.Nullable; |
22 | 24 |
|
| 25 | +import java.io.InputStream; |
| 26 | +import java.nio.file.Files; |
| 27 | +import java.nio.file.Path; |
| 28 | +import java.security.GeneralSecurityException; |
| 29 | +import java.security.KeyPair; |
| 30 | +import java.security.KeyStore; |
| 31 | +import java.security.PrivateKey; |
| 32 | +import java.security.cert.Certificate; |
| 33 | +import java.security.cert.X509Certificate; |
| 34 | +import java.util.ArrayList; |
| 35 | +import java.util.Enumeration; |
| 36 | +import java.util.List; |
23 | 37 | import java.util.function.Supplier; |
24 | 38 |
|
| 39 | +import static com.couchbase.analytics.client.java.internal.utils.lang.CbCollections.listCopyOf; |
25 | 40 | import static java.nio.charset.StandardCharsets.UTF_8; |
26 | 41 | import static java.util.Objects.requireNonNull; |
27 | 42 |
|
|
30 | 45 | * <pre> |
31 | 46 | * Credential.of(username, password) |
32 | 47 | * </pre> |
33 | | - * <p> |
34 | | - * For advanced use cases involving dynamic credentials, see |
35 | | - * {@link Credential#ofDynamic(Supplier)}. |
| 48 | + * Alternatively, to use a client certificate: |
| 49 | + * <pre> |
| 50 | + * Credential.fromKeyStore( |
| 51 | + * Paths.get("/path/to/client-cert.p12"), |
| 52 | + * "password" |
| 53 | + * ) |
| 54 | + * </pre> |
| 55 | + * |
| 56 | + * @see Cluster#credential(Credential) |
36 | 57 | */ |
37 | 58 | @ThreadSafe |
38 | 59 | public abstract class Credential { |
39 | 60 |
|
| 61 | + private static class UsernameAndPassword extends Credential { |
| 62 | + private final String authHeaderValue; |
| 63 | + |
| 64 | + UsernameAndPassword(String username, String password) { |
| 65 | + this.authHeaderValue = Credentials.basic(username, password, UTF_8); |
| 66 | + } |
| 67 | + |
| 68 | + @Override |
| 69 | + String httpAuthorizationHeaderValue() { |
| 70 | + return authHeaderValue; |
| 71 | + } |
| 72 | + |
| 73 | + @Override |
| 74 | + void addHeldCertificate(HandshakeCertificates.Builder builder) { |
| 75 | + } |
| 76 | + } |
| 77 | + |
| 78 | + private static class ClientCertificate extends Credential { |
| 79 | + private final HeldCertificate heldCertificate; |
| 80 | + private final List<X509Certificate> intermediates; |
| 81 | + |
| 82 | + public ClientCertificate(HeldCertificate heldCertificate, List<X509Certificate> intermediates) { |
| 83 | + this.heldCertificate = requireNonNull(heldCertificate); |
| 84 | + this.intermediates = listCopyOf(intermediates); |
| 85 | + } |
| 86 | + |
| 87 | + @Override |
| 88 | + @Nullable String httpAuthorizationHeaderValue() { |
| 89 | + return null; |
| 90 | + } |
| 91 | + |
| 92 | + @Override |
| 93 | + void addHeldCertificate(HandshakeCertificates.Builder builder) { |
| 94 | + builder.heldCertificate(heldCertificate, intermediates.toArray(new X509Certificate[0])); |
| 95 | + } |
| 96 | + } |
| 97 | + |
| 98 | + private static class Dynamic extends Credential { |
| 99 | + private final Supplier<Credential> supplier; |
| 100 | + |
| 101 | + public Dynamic(Supplier<Credential> supplier) { |
| 102 | + this.supplier = requireNonNull(supplier); |
| 103 | + } |
| 104 | + |
| 105 | + @Override |
| 106 | + @Nullable String httpAuthorizationHeaderValue() { |
| 107 | + return supplier.get().httpAuthorizationHeaderValue(); |
| 108 | + } |
| 109 | + |
| 110 | + @Override |
| 111 | + void addHeldCertificate(HandshakeCertificates.Builder builder) { |
| 112 | + supplier.get().addHeldCertificate(builder); |
| 113 | + } |
| 114 | + } |
| 115 | + |
40 | 116 | /** |
41 | 117 | * Returns a new instance that holds the given username and password. |
42 | 118 | */ |
43 | 119 | public static Credential of(String username, String password) { |
44 | | - String authHeaderValue = Credentials.basic(username, password, UTF_8); |
| 120 | + return new UsernameAndPassword(username, password); |
| 121 | + } |
| 122 | + |
| 123 | + /** |
| 124 | + * Returns a new instance that holds a client certificate loaded from the specified PKCS#12 key store file. |
| 125 | + * <p> |
| 126 | + * The key store must have a single entry which must contain a private key and certificate chain. |
| 127 | + * The same password must be used for integrity and encryption. |
| 128 | + * <p> |
| 129 | + * <b>TIP:</b> |
| 130 | + * One way to create a suitable PKCS#12 file from a PEM-encoded private key and certificate |
| 131 | + * is to concatenate the key and certificate into a file named "client-cert.pem", then run this command: |
| 132 | + * <pre> |
| 133 | + * openssl pkcs12 -export -in client-cert.pem -out client-cert.p12 -passout pass:password |
| 134 | + * </pre> |
| 135 | + * This creates a PKCS#12 file named "client-cert.p12" protected by the password "password". |
| 136 | + * |
| 137 | + * @param password for verifying key store integrity and decrypting the private key |
| 138 | + */ |
| 139 | + public static Credential fromKeyStore(Path pkcs12Path, @Nullable String password) { |
| 140 | + KeyStore keyStore = loadKeyStore(pkcs12Path, password); |
| 141 | + return fromKeyStore(keyStore, password); |
| 142 | + } |
45 | 143 |
|
46 | | - return new Credential() { |
47 | | - @Override |
48 | | - String httpAuthorizationHeaderValue() { |
49 | | - return authHeaderValue; |
| 144 | + /** |
| 145 | + * Returns a new instance that holds a client certificate loaded from the specified key store. |
| 146 | + * <p> |
| 147 | + * The key store must have a single entry which must contain a private key and certificate chain. |
| 148 | + * |
| 149 | + * @param password for decrypting the private key |
| 150 | + */ |
| 151 | + public static Credential fromKeyStore(KeyStore keyStore, @Nullable String password) { |
| 152 | + try { |
| 153 | + List<String> aliases = toList(keyStore.aliases()); |
| 154 | + if (aliases.size() != 1) { |
| 155 | + throw new IllegalArgumentException("Expected the key store to contain exactly one entry, but got aliases: " + aliases); |
50 | 156 | } |
| 157 | + String alias = aliases.get(0); |
| 158 | + |
| 159 | + PrivateKey privateKey = (PrivateKey) keyStore.getKey(alias, password == null ? null : password.toCharArray()); |
| 160 | + Certificate[] chain = keyStore.getCertificateChain(alias); |
| 161 | + X509Certificate userCert = (X509Certificate) chain[0]; |
| 162 | + |
| 163 | + HeldCertificate heldCertificate = new HeldCertificate( |
| 164 | + new KeyPair(userCert.getPublicKey(), privateKey), |
| 165 | + userCert |
| 166 | + ); |
51 | 167 |
|
52 | | - @Override |
53 | | - void addHeldCertificate(HandshakeCertificates.Builder builder) { |
54 | | - // noop |
| 168 | + List<X509Certificate> intermediates = new ArrayList<>(); |
| 169 | + for (int i = 1; i < chain.length; i++) { // skip zero-th because that's the user's certificate |
| 170 | + intermediates.add((X509Certificate) chain[i]); |
55 | 171 | } |
56 | | - }; |
| 172 | + |
| 173 | + return new ClientCertificate(heldCertificate, intermediates); |
| 174 | + |
| 175 | + } catch (ClassCastException | GeneralSecurityException e) { |
| 176 | + throw new RuntimeException("Failed to read client certificate from key store.", e); |
| 177 | + } |
| 178 | + } |
| 179 | + |
| 180 | + private static KeyStore loadKeyStore(Path keyStorePath, @Nullable String password) { |
| 181 | + try (InputStream keyStoreInputStream = Files.newInputStream(keyStorePath)) { |
| 182 | + final KeyStore store = KeyStore.getInstance(KeyStore.getDefaultType()); |
| 183 | + store.load( |
| 184 | + keyStoreInputStream, |
| 185 | + password != null ? password.toCharArray() : null |
| 186 | + ); |
| 187 | + return store; |
| 188 | + } catch (Exception ex) { |
| 189 | + throw new RuntimeException("Failed to read key store.", ex); |
| 190 | + } |
| 191 | + } |
| 192 | + |
| 193 | + private static <T> List<T> toList(Enumeration<T> e) { |
| 194 | + List<T> result = new ArrayList<>(); |
| 195 | + while (e.hasMoreElements()) { |
| 196 | + result.add(e.nextElement()); |
| 197 | + } |
| 198 | + return result; |
57 | 199 | } |
58 | 200 |
|
59 | 201 | /** |
60 | 202 | * Returns a new instance of a dynamic credential that invokes the given supplier |
61 | 203 | * every time a credential is required. |
62 | 204 | * <p> |
63 | 205 | * This enables updating a credential without having to restart your application. |
| 206 | + * |
| 207 | + * @deprecated This method is not compatible with client certificate credentials. |
| 208 | + * Instead, please update the credential by calling {@link Cluster#credential(Credential)}. |
64 | 209 | */ |
| 210 | + @Deprecated |
65 | 211 | public static Credential ofDynamic(Supplier<Credential> supplier) { |
66 | | - requireNonNull(supplier); |
67 | | - |
68 | | - return new Credential() { |
69 | | - @Override |
70 | | - String httpAuthorizationHeaderValue() { |
71 | | - return supplier.get().httpAuthorizationHeaderValue(); |
72 | | - } |
73 | | - |
74 | | - @Override |
75 | | - void addHeldCertificate(HandshakeCertificates.Builder builder) { |
76 | | - supplier.get().addHeldCertificate(builder); |
77 | | - } |
78 | | - }; |
| 212 | + return new Dynamic(supplier); |
79 | 213 | } |
80 | 214 |
|
81 | | - abstract String httpAuthorizationHeaderValue(); |
| 215 | + abstract @Nullable String httpAuthorizationHeaderValue(); |
82 | 216 |
|
83 | 217 | abstract void addHeldCertificate(HandshakeCertificates.Builder builder); |
84 | 218 |
|
85 | 219 | /** |
86 | 220 | * @see #of |
87 | | - * @see #ofDynamic |
| 221 | + * @see #fromKeyStore |
88 | 222 | */ |
89 | 223 | private Credential() { |
90 | 224 | } |
|
0 commit comments