From 3f172839a4bacf862ca2f152e89b73b27ef18b77 Mon Sep 17 00:00:00 2001 From: Alex Weibel Date: Thu, 1 Aug 2019 13:32:51 -0700 Subject: [PATCH] Add Java TLS Cipher API (#74) * Add TLS Cipher API * Update Dependencies to use PostQuantum Compatible Versions --- aws-common-runtime/CMakeLists.txt | 4 +- .../awssdk/crt/io/TlsCipherPreference.java | 45 ++++++++++++ .../awssdk/crt/io/TlsContextOptions.java | 64 ++++++++++++++++- src/native/tls_options.c | 45 ++++++++++++ .../awssdk/crt/test/HttpConnectionTest.java | 70 +++++++++++++------ .../crt/test/TlsContextOptionsTest.java | 46 ++++++++++++ 6 files changed, 251 insertions(+), 23 deletions(-) create mode 100644 src/main/java/software/amazon/awssdk/crt/io/TlsCipherPreference.java create mode 100644 src/test/java/software/amazon/awssdk/crt/test/TlsContextOptionsTest.java diff --git a/aws-common-runtime/CMakeLists.txt b/aws-common-runtime/CMakeLists.txt index 92d86c975..774ce16c3 100644 --- a/aws-common-runtime/CMakeLists.txt +++ b/aws-common-runtime/CMakeLists.txt @@ -30,7 +30,7 @@ if (UNIX AND NOT APPLE) endif() set(AWS_C_IO_URL "https://github.com/awslabs/aws-c-io.git") -set(AWS_C_IO_SHA "v0.4.1") +set(AWS_C_IO_SHA "v0.4.2") include(BuildAwsCIO) set(AWS_C_COMPRESSION_URL "https://github.com/awslabs/aws-c-compression.git") @@ -42,7 +42,7 @@ set(AWS_C_MQTT_SHA "v0.4.1") include(BuildAwsCMqtt) set(AWS_C_HTTP_URL "https://github.com/awslabs/aws-c-http.git") -set(AWS_C_HTTP_SHA "v0.3.2") +set(AWS_C_HTTP_SHA "v0.3.3") include(BuildAwsCHttp) add_dependencies(AwsCCompression AwsCCommon) diff --git a/src/main/java/software/amazon/awssdk/crt/io/TlsCipherPreference.java b/src/main/java/software/amazon/awssdk/crt/io/TlsCipherPreference.java new file mode 100644 index 000000000..01d3df21c --- /dev/null +++ b/src/main/java/software/amazon/awssdk/crt/io/TlsCipherPreference.java @@ -0,0 +1,45 @@ +/* + * Copyright 2010-2018 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * A copy of the License is located at + * + * http://aws.amazon.com/apache2.0 + * + * or in the "license" file accompanying this file. This file is distributed + * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either + * express or implied. See the License for the specific language governing + * permissions and limitations under the License. + */ +package software.amazon.awssdk.crt.io; + +public enum TlsCipherPreference { + /** + * Use whatever the System Default Preference is. This is usually the best option, as it will be automatically + * updated as the underlying OS or platform changes. + */ + TLS_CIPHER_SYSTEM_DEFAULT(0), + + /** + * Contains Draft Hybrid TLS Ciphers: https://tools.ietf.org/html/draft-campagna-tls-bike-sike-hybrid + * + * These ciphers perform two Key Exchanges (1 ECDHE + 1 Post-Quantum) during the TLS Handshake in order to + * combine the security of Classical ECDHE Key Exchange with the conjectured quantum-resistance of newly + * proposed key exchanges. + * + * The algorithms these new Post-Quantum ciphers are based on have been submitted to NIST's Post-Quantum Crypto + * Standardization Process, and are still under review. + * + * This Cipher Preference may stop being supported at any time. + */ + TLS_CIPHER_KMS_PQ_TLSv1_0_2019_06(1); + + private int val; + + TlsCipherPreference(int val) { + this.val = val; + } + + int getValue() { return val; } +} diff --git a/src/main/java/software/amazon/awssdk/crt/io/TlsContextOptions.java b/src/main/java/software/amazon/awssdk/crt/io/TlsContextOptions.java index b669918d4..feb35ba18 100644 --- a/src/main/java/software/amazon/awssdk/crt/io/TlsContextOptions.java +++ b/src/main/java/software/amazon/awssdk/crt/io/TlsContextOptions.java @@ -55,6 +55,9 @@ public enum TlsVersions { int getValue() { return version; } } + private TlsVersions tlsVersion = TlsVersions.TLS_VER_SYS_DEFAULTS; + private TlsCipherPreference tlsCipherPreference = TlsCipherPreference.TLS_CIPHER_SYSTEM_DEFAULT; + /** * Creates a new set of options that can be used to create a {@link TlsContext} * @throws CrtRuntimeException If the system is not able to allocate space for a native tls context options structure @@ -75,11 +78,18 @@ public void close() { } /** - * Sets the minimum acceptable TLS version that the {@link TlsContext} will allow + * Sets the minimum acceptable TLS version that the {@link TlsContext} will allow. Not compatible with + * setCipherPreference() API. + * * @param version Select from TlsVersions, a good default is TlsVersions.TLS_VER_SYS_DEFAULTS * as this will update if the OS TLS is updated */ public void setMinimumTlsVersion(TlsVersions version) { + if (this.tlsCipherPreference != TlsCipherPreference.TLS_CIPHER_SYSTEM_DEFAULT && version != TlsVersions.TLS_VER_SYS_DEFAULTS) { + throw new IllegalArgumentException("Currently only setMinimumTlsVersion() or setCipherPreference() may be used, not both."); + } + + this.tlsVersion = version; tlsContextOptionsSetMinimumTlsVersion(native_ptr(), version.getValue()); } @@ -92,6 +102,25 @@ public void setAlpnList(String alpn) { tlsContextOptionsSetAlpn(native_ptr(), alpn); } + /** + * Sets the TLS Cipher Preferences that can be negotiated and used during the TLS Connection. Not compatible with + * setMinimumTlsVersion() API. + * + * @param cipherPref The Cipher Preference to use + */ + public void setCipherPreference(TlsCipherPreference cipherPref) { + if(!isCipherPreferenceSupported(cipherPref)) { + throw new IllegalArgumentException("TlsCipherPreference is not supported on this platform: " + cipherPref.name()); + } + + if (this.tlsVersion != TlsVersions.TLS_VER_SYS_DEFAULTS && cipherPref != TlsCipherPreference.TLS_CIPHER_SYSTEM_DEFAULT) { + throw new IllegalArgumentException("Currently only setMinimumTlsVersion() or setCipherPreference() may be used, not both."); + } + + this.tlsCipherPreference = cipherPref; + tlsContextOptionsSetCipherPreference(native_ptr(), cipherPref.getValue()); + } + /** * Sets the path to the certificate that identifies this TLS host. Must be in PEM format. * @param certificatePath Path to PEM format certificate @@ -129,6 +158,15 @@ public static boolean isAlpnSupported() { return tlsContextOptionsIsAlpnAvailable(); } + /** + * Returns whether or not the current platform can be configured to a specific TlsCipherPreference. + * @param cipherPref The TlsCipherPreference to check + * @return True if the current platform does support this TlsCipherPreference, false otherwise + */ + public static boolean isCipherPreferenceSupported(TlsCipherPreference cipherPref) { + return tlsContextOptionsIsCipherPreferenceSupported(cipherPref.getValue()); + } + /** * Helper function to provide a TlsContext-local trust store * @param caPath Path to the local trust store. Can be null. @@ -173,6 +211,25 @@ public static TlsContextOptions createWithMTLSPkcs12(String pkcs12Path, String p return options; } + /******************************************************************************* + * .with() methods + ******************************************************************************/ + + public TlsContextOptions withCipherPreference(TlsCipherPreference cipherPref) { + setCipherPreference(cipherPref); + return this; + } + + public TlsContextOptions withMinimumTlsVersion(TlsVersions version) { + setMinimumTlsVersion(version); + return this; + } + + public TlsContextOptions withAlpnList(String alpnList) { + setAlpnList(alpnList); + return this; + } + /******************************************************************************* * native methods ******************************************************************************/ @@ -186,6 +243,8 @@ public static TlsContextOptions createWithMTLSPkcs12(String pkcs12Path, String p private static native void tlsContextOptionsSetAlpn(long tls, String alpn); + private static native void tlsContextOptionsSetCipherPreference(long tls, int cipherPref); + private static native void tlsContextOptionsInitMTLSFromPath(long tls, String cert_path, String key_path); private static native void tlsContextOptionsInitMTLSPkcs12FromPath(long tls, String pkcs12_path, String pkcs12_password); @@ -193,4 +252,7 @@ public static TlsContextOptions createWithMTLSPkcs12(String pkcs12Path, String p private static native void tlsContextOptionsSetVerifyPeer(long tls, boolean verify); private static native boolean tlsContextOptionsIsAlpnAvailable(); + + private static native boolean tlsContextOptionsIsCipherPreferenceSupported(int cipherPref); + }; diff --git a/src/native/tls_options.c b/src/native/tls_options.c index 32e77ffb3..d73dd31c6 100644 --- a/src/native/tls_options.c +++ b/src/native/tls_options.c @@ -157,6 +157,31 @@ void JNICALL Java_software_amazon_awssdk_crt_io_TlsContextOptions_tlsContextOpti aws_tls_ctx_options_set_alpn_list(&tls->options, (const char *)aws_string_bytes(tls->alpn_list)); } +JNIEXPORT +void JNICALL Java_software_amazon_awssdk_crt_io_TlsContextOptions_tlsContextOptionsSetCipherPreference( + JNIEnv *env, + jclass jni_class, + jlong jni_tls, + jint jni_cipher_pref) { + + (void)jni_class; + struct jni_tls_ctx_options *tls = (struct jni_tls_ctx_options *)jni_tls; + + if (!tls) { + return; + } + + if (jni_cipher_pref < 0 || AWS_IO_TLS_CIPHER_PREF_END_RANGE <= jni_cipher_pref) { + aws_jni_throw_runtime_exception( + env, + "TlsContextOptions.tlsContextOptionsSetCipherPreference: TlsCipherPreference is out of range: %d", + (int)jni_cipher_pref); + return; + } + + tls->options.cipher_pref = (enum aws_tls_cipher_pref)jni_cipher_pref; +} + JNIEXPORT void JNICALL Java_software_amazon_awssdk_crt_io_TlsContextOptions_tlsContextOptionsInitMTLSFromPath( JNIEnv *env, @@ -243,6 +268,26 @@ jboolean JNICALL Java_software_amazon_awssdk_crt_io_TlsContextOptions_tlsContext return aws_tls_is_alpn_available(); } +JNIEXPORT +jboolean JNICALL Java_software_amazon_awssdk_crt_io_TlsContextOptions_tlsContextOptionsIsCipherPreferenceSupported( + JNIEnv *env, + jclass jni_class, + jint jni_cipher_pref) { + + (void)env; + (void)jni_class; + + if (jni_cipher_pref < 0 || AWS_IO_TLS_CIPHER_PREF_END_RANGE <= jni_cipher_pref) { + aws_jni_throw_runtime_exception( + env, + "TlsContextOptions.tlsContextOptionsSetCipherPreference: TlsCipherPreference is out of range: %d", + (int)jni_cipher_pref); + return false; + } + + return aws_tls_is_cipher_pref_supported((enum aws_tls_cipher_pref)jni_cipher_pref); +} + #if UINTPTR_MAX == 0xffffffff # if defined(_MSC_VER) # pragma warning(pop) diff --git a/src/test/java/software/amazon/awssdk/crt/test/HttpConnectionTest.java b/src/test/java/software/amazon/awssdk/crt/test/HttpConnectionTest.java index 7eb7ea655..22c8094c5 100644 --- a/src/test/java/software/amazon/awssdk/crt/test/HttpConnectionTest.java +++ b/src/test/java/software/amazon/awssdk/crt/test/HttpConnectionTest.java @@ -22,49 +22,79 @@ import software.amazon.awssdk.crt.http.HttpConnection; import software.amazon.awssdk.crt.io.ClientBootstrap; import software.amazon.awssdk.crt.io.SocketOptions; +import software.amazon.awssdk.crt.io.TlsCipherPreference; import software.amazon.awssdk.crt.io.TlsContext; import java.net.URI; +import software.amazon.awssdk.crt.io.TlsContextOptions; public class HttpConnectionTest { - protected void testConnection(URI uri, boolean expectConnected, String exceptionMsg) throws CrtRuntimeException { + private class HttpConnectionTestResponse { boolean actuallyConnected = false; boolean exceptionThrown = false; + Exception exception = null; + } - ClientBootstrap bootstrap = new ClientBootstrap(1); - SocketOptions sockOpts = new SocketOptions(); - TlsContext tlsContext = new TlsContext(); - + private HttpConnectionTestResponse testConnection(URI uri, ClientBootstrap bootstrap, SocketOptions sockOpts, TlsContext tlsContext) { + HttpConnectionTestResponse resp = new HttpConnectionTestResponse(); try { HttpConnection conn = HttpConnection.createConnection(uri, bootstrap, sockOpts, tlsContext).get(); - actuallyConnected = true; + resp.actuallyConnected = true; conn.shutdown().get(); conn.close(); } catch (Exception e) { - exceptionThrown = true; - Assert.assertTrue(e.getMessage(), e.getMessage().contains(exceptionMsg)); + resp.exceptionThrown = true; + resp.exception = e; + } finally { tlsContext.close(); sockOpts.close(); bootstrap.close(); } - Assert.assertEquals("URI: " + uri.toString(), expectConnected, actuallyConnected); - Assert.assertEquals("URI: " + uri.toString(), expectConnected, !exceptionThrown); - Assert.assertEquals(0, CrtResource.getAllocatedNativeResourceCount()); + return resp; + } + + private void testConnectionWithAllCiphers(URI uri, boolean expectConnected, String exceptionMsg) throws CrtRuntimeException { + for (TlsCipherPreference pref: TlsCipherPreference.values()) { + if (!TlsContextOptions.isCipherPreferenceSupported(pref)) { + continue; + } + + TlsContextOptions tlsOpts = new TlsContextOptions().withCipherPreference(pref); + HttpConnectionTestResponse resp = testConnection(uri, new ClientBootstrap(1), new SocketOptions(), new TlsContext(tlsOpts)); + tlsOpts.close(); + + Assert.assertEquals("URI: " + uri.toString(), expectConnected, resp.actuallyConnected); + Assert.assertEquals("URI: " + uri.toString(), expectConnected, !resp.exceptionThrown); + if (resp.exception != null) { + Assert.assertTrue(resp.exception.getMessage(), resp.exception.getMessage().contains(exceptionMsg)); + } + + Assert.assertEquals(0, CrtResource.getAllocatedNativeResourceCount()); + } } @Test public void testHttpConnection() throws Exception { - testConnection(new URI("https://aws-crt-test-stuff.s3.amazonaws.com"), true, null); - testConnection(new URI("http://aws-crt-test-stuff.s3.amazonaws.com"), true, null); - testConnection(new URI("http://aws-crt-test-stuff.s3.amazonaws.com:80"), true, null); - testConnection(new URI("http://aws-crt-test-stuff.s3.amazonaws.com:443"), true, null); - testConnection(new URI("https://aws-crt-test-stuff.s3.amazonaws.com:443"), true, null); - testConnection(new URI("https://rsa2048.badssl.com/"), true, null); - testConnection(new URI("http://http.badssl.com/"), true, null); - testConnection(new URI("https://expired.badssl.com/"), false, "TLS (SSL) negotiation failed"); - testConnection(new URI("https://self-signed.badssl.com/"), false, "TLS (SSL) negotiation failed"); + // S3 + testConnectionWithAllCiphers(new URI("https://aws-crt-test-stuff.s3.amazonaws.com"), true, null); + testConnectionWithAllCiphers(new URI("http://aws-crt-test-stuff.s3.amazonaws.com"), true, null); + testConnectionWithAllCiphers(new URI("http://aws-crt-test-stuff.s3.amazonaws.com:80"), true, null); + testConnectionWithAllCiphers(new URI("http://aws-crt-test-stuff.s3.amazonaws.com:443"), true, null); + testConnectionWithAllCiphers(new URI("https://aws-crt-test-stuff.s3.amazonaws.com:443"), true, null); + + // KMS + testConnectionWithAllCiphers(new URI("https://kms.us-east-1.amazonaws.com:443"), true, null); + testConnectionWithAllCiphers(new URI("https://kms-fips.us-east-1.amazonaws.com:443"), true, null); + testConnectionWithAllCiphers(new URI("https://kms.us-west-2.amazonaws.com:443"), true, null); + testConnectionWithAllCiphers(new URI("https://kms-fips.us-west-2.amazonaws.com:443"), true, null); + + // BadSSL + testConnectionWithAllCiphers(new URI("https://rsa2048.badssl.com/"), true, null); + testConnectionWithAllCiphers(new URI("http://http.badssl.com/"), true, null); + testConnectionWithAllCiphers(new URI("https://expired.badssl.com/"), false, "TLS (SSL) negotiation failed"); + testConnectionWithAllCiphers(new URI("https://self-signed.badssl.com/"), false, "TLS (SSL) negotiation failed"); } } diff --git a/src/test/java/software/amazon/awssdk/crt/test/TlsContextOptionsTest.java b/src/test/java/software/amazon/awssdk/crt/test/TlsContextOptionsTest.java new file mode 100644 index 000000000..05b0587ad --- /dev/null +++ b/src/test/java/software/amazon/awssdk/crt/test/TlsContextOptionsTest.java @@ -0,0 +1,46 @@ +package software.amazon.awssdk.crt.test; + +import static software.amazon.awssdk.crt.io.TlsContextOptions.TlsVersions; + +import org.junit.Assert; +import org.junit.Test; +import software.amazon.awssdk.crt.CrtResource; +import software.amazon.awssdk.crt.io.TlsCipherPreference; +import software.amazon.awssdk.crt.io.TlsContextOptions; + +public class TlsContextOptionsTest { + + @Test + public void testTlsContextOptionsAPI() { + Assert.assertEquals(0, CrtResource.getAllocatedNativeResourceCount()); + + TlsContextOptions options = new TlsContextOptions(); + + for (TlsVersions tlsVersion: TlsContextOptions.TlsVersions.values()) { + options.setMinimumTlsVersion(tlsVersion); + } + + options.setMinimumTlsVersion(TlsVersions.TLS_VER_SYS_DEFAULTS); + + for (TlsCipherPreference pref: TlsCipherPreference.values()) { + if (TlsContextOptions.isCipherPreferenceSupported(pref)) { + options.setCipherPreference(pref); + } + } + + boolean exceptionThrown = false; + + try { + options.setCipherPreference(TlsCipherPreference.TLS_CIPHER_KMS_PQ_TLSv1_0_2019_06); + options.setMinimumTlsVersion(TlsVersions.TLSv1_2); + Assert.fail(); + } catch (IllegalArgumentException e) { + exceptionThrown = true; + } + + Assert.assertTrue(exceptionThrown); + + options.close(); + Assert.assertEquals(0, CrtResource.getAllocatedNativeResourceCount()); + } +}