diff --git a/.github/workflows/test.yaml b/.github/workflows/test.yaml index 995db271..13532247 100644 --- a/.github/workflows/test.yaml +++ b/.github/workflows/test.yaml @@ -99,6 +99,24 @@ jobs: - name: Integration tests run: make PROFILE=debug CERT_COMPRESSION=true integration + # TODO(@cpu): MacOS and Windows FIPS test coverage + fips: + name: FIPS + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + with: + persist-credentials: false + - uses: actions/checkout@v4 + with: + persist-credentials: false + - name: Install nightly rust toolchain + uses: dtolnay/rust-toolchain@nightly + - name: Unit tests + run: make FIPS=true test + - name: Integration tests + run: make FIPS=true integration + test-windows-cmake-debug: name: Windows CMake, Debug configuration runs-on: windows-latest diff --git a/CMakeLists.txt b/CMakeLists.txt index f460752e..79b12b90 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -10,6 +10,8 @@ endif () set(CERT_COMPRESSION "false" CACHE STRING "Whether to enable brotli and zlib certificate compression support") +set(FIPS "false" CACHE STRING "Whether to enable aws-lc-rs and FIPS support") + set(CARGO_FEATURES --no-default-features) if (CRYPTO_PROVIDER STREQUAL "aws-lc-rs") list(APPEND CARGO_FEATURES --features=aws-lc-rs) @@ -21,6 +23,11 @@ if (CERT_COMPRESSION STREQUAL "true") list(APPEND CARGO_FEATURES --features=cert_compression) endif () +# See https://docs.rs/rustls/latest/rustls/manual/_06_fips/index.html +if (FIPS STREQUAL "true") + list(APPEND CARGO_FEATURES --features=fips) +endif () + add_subdirectory(tests) include(ExternalProject) diff --git a/Cargo.lock b/Cargo.lock index cfe58966..8d707e28 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -32,12 +32,28 @@ version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f1fdabc7756949593fe60f30ec81974b613357de856987752631dea1e3394c80" +[[package]] +name = "aws-lc-fips-sys" +version = "0.12.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bf12b67bc9c5168f68655aadb2a12081689a58f1d9b1484705e4d1810ed6e4ac" +dependencies = [ + "bindgen", + "cc", + "cmake", + "dunce", + "fs_extra", + "libc", + "paste", +] + [[package]] name = "aws-lc-rs" version = "1.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2f95446d919226d587817a7d21379e6eb099b97b45110a7f272a444ca5c54070" dependencies = [ + "aws-lc-fips-sys", "aws-lc-sys", "mirai-annotations", "paste", diff --git a/Cargo.toml b/Cargo.toml index 1d986774..edfccc51 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -24,6 +24,7 @@ capi = [] ring = ["rustls/ring", "webpki/ring"] aws-lc-rs = ["rustls/aws-lc-rs", "webpki/aws_lc_rs"] cert_compression = ["rustls/brotli", "rustls/zlib"] +fips = ["aws-lc-rs", "rustls/fips"] [dependencies] # Keep in sync with RUSTLS_CRATE_VERSION in build.rs diff --git a/Makefile b/Makefile index 0eacf1d7..87831554 100644 --- a/Makefile +++ b/Makefile @@ -11,6 +11,7 @@ CFLAGS := -Werror -Wall -Wextra -Wpedantic -g -I src/ PROFILE := release CRYPTO_PROVIDER := aws-lc-rs COMPRESSION := false +FIPS := false DESTDIR=/usr/local ifeq ($(PROFILE), debug) @@ -41,6 +42,11 @@ ifeq ($(COMPRESSION), true) LDFLAGS += -lm endif +# See https://docs.rs/rustls/latest/rustls/manual/_06_fips/index.html +ifeq ($(FIPS), true) + CARGOFLAGS += --features fips +endif + default: target/$(PROFILE)/librustls_ffi.a all: default test integration connect-test diff --git a/Makefile.pkg-config b/Makefile.pkg-config index 8baf7298..08720b88 100644 --- a/Makefile.pkg-config +++ b/Makefile.pkg-config @@ -15,6 +15,7 @@ CFLAGS := -Werror -Wall -Wextra -Wpedantic -g -I src/ PROFILE := release CRYPTO_PROVIDER := aws-lc-rs CERT_COMPRESSION := false +FIPS := false PREFIX=/usr/local ifeq ($(PROFILE), debug) @@ -39,6 +40,11 @@ ifeq ($(CERT_COMPRESSION), true) CARGOFLAGS += --features cert_compression endif +# See https://docs.rs/rustls/latest/rustls/manual/_06_fips/index.html +ifeq ($(FIPS), true) + CARGOFLAGS += --features fips +endif + all: target/client target/server integration: all diff --git a/cbindgen.toml b/cbindgen.toml index b2bf782f..7c2a5966 100644 --- a/cbindgen.toml +++ b/cbindgen.toml @@ -14,7 +14,8 @@ include = ["rustls_tls_version"] "feature = read_buf" = "DEFINE_READ_BUF" "feature = aws-lc-rs" = "DEFINE_AWS_LC_RS" "feature = ring" = "DEFINE_RING" +"feature = fips" = "DEFINE_FIPS" [parse.expand] crates = ["rustls-ffi"] -features = ["read_buf", "aws-lc-rs", "ring"] +features = ["read_buf", "aws-lc-rs", "ring", "fips"] diff --git a/src/client.rs b/src/client.rs index 363354b8..66b85568 100644 --- a/src/client.rs +++ b/src/client.rs @@ -582,6 +582,19 @@ impl rustls_client_config_builder { } impl rustls_client_config { + /// Returns true if a `rustls_connection` created from the `rustls_client_config` will + /// operate in FIPS mode. + /// + /// This is different from `rustls_crypto_provider_fips` which is concerned + /// only with cryptography, whereas this also covers TLS-level configuration that NIST + /// recommends, as well as ECH HPKE suites if applicable. + #[no_mangle] + pub extern "C" fn rustls_client_config_fips(config: *const rustls_client_config) -> bool { + ffi_panic_boundary! { + try_ref_from_ptr!(config).fips() + } + } + /// "Free" a `rustls_client_config` previously returned from /// `rustls_client_config_builder_build`. /// diff --git a/src/connection.rs b/src/connection.rs index f1dc4fb4..61808814 100644 --- a/src/connection.rs +++ b/src/connection.rs @@ -613,6 +613,23 @@ impl rustls_connection { } } + /// Returns true if the `rustls_connection` was made with a `rustls_client_config` + /// or `rustls_server_config` that is FIPS compatible. + /// + /// This is different from `rustls_crypto_provider_fips` which is concerned + /// only with cryptography, whereas this also covers TLS-level configuration that NIST + /// recommends, as well as ECH HPKE suites if applicable. + #[no_mangle] + pub extern "C" fn rustls_connection_fips(conn: *const rustls_connection) -> bool { + ffi_panic_boundary! { + let conn = try_ref_from_ptr!(conn); + match &conn.conn { + rustls::Connection::Client(c) => c.fips(), + rustls::Connection::Server(c) => c.fips(), + } + } + } + /// Free a rustls_connection. Calling with NULL is fine. /// Must not be called twice with the same value. #[no_mangle] diff --git a/src/crypto_provider.rs b/src/crypto_provider.rs index ae7a6232..e5cc92b8 100644 --- a/src/crypto_provider.rs +++ b/src/crypto_provider.rs @@ -248,6 +248,26 @@ pub extern "C" fn rustls_aws_lc_rs_crypto_provider() -> *const rustls_crypto_pro } } +/// Return a `rustls_crypto_provider` that uses FIPS140-3 approved cryptography. +/// +/// Using this function expresses in your code that you require FIPS-approved cryptography, +/// and will not compile if you make a mistake with cargo features. +/// +/// See the upstream [rustls FIPS documentation][FIPS] for more information. +/// +/// The caller owns the returned `rustls_crypto_provider` and must free it using +/// `rustls_crypto_provider_free`. +/// +/// [FIPS]: https://docs.rs/rustls/latest/rustls/manual/_06_fips/index.html +#[no_mangle] +#[cfg(feature = "fips")] +pub extern "C" fn rustls_default_fips_provider() -> *const rustls_crypto_provider { + ffi_panic_boundary! { + Arc::into_raw(Arc::new(rustls::crypto::default_fips_provider())) + as *const rustls_crypto_provider + } +} + /// Retrieve a pointer to the process default `rustls_crypto_provider`. /// /// This may return `NULL` if no process default provider has been set using @@ -364,6 +384,19 @@ pub extern "C" fn rustls_crypto_provider_random( } } +/// Returns true if the `rustls_crypto_provider` is operating in FIPS mode. +/// +/// This covers only the cryptographic parts of FIPS approval. There are also +/// TLS protocol-level recommendations made by NIST. You should prefer to call +/// `rustls_client_config_fips` or `rustls_server_config_fips` which take these +/// into account. +#[no_mangle] +pub extern "C" fn rustls_crypto_provider_fips(provider: *const rustls_crypto_provider) -> bool { + ffi_panic_boundary! { + try_ref_from_ptr!(provider).fips() + } +} + /// Frees the `rustls_crypto_provider`. /// /// Calling with `NULL` is fine. diff --git a/src/rustls.h b/src/rustls.h index f23fc013..26c18912 100644 --- a/src/rustls.h +++ b/src/rustls.h @@ -1758,6 +1758,16 @@ rustls_result rustls_client_config_builder_build(struct rustls_client_config_bui */ void rustls_client_config_builder_free(struct rustls_client_config_builder *config); +/** + * Returns true if a `rustls_connection` created from the `rustls_client_config` will + * operate in FIPS mode. + * + * This is different from `rustls_crypto_provider_fips` which is concerned + * only with cryptography, whereas this also covers TLS-level configuration that NIST + * recommends, as well as ECH HPKE suites if applicable. + */ +bool rustls_client_config_fips(const struct rustls_client_config *config); + /** * "Free" a `rustls_client_config` previously returned from * `rustls_client_config_builder_build`. @@ -2047,6 +2057,16 @@ rustls_result rustls_connection_read_2(struct rustls_connection *conn, size_t *out_n); #endif +/** + * Returns true if the `rustls_connection` was made with a `rustls_client_config` + * or `rustls_server_config` that is FIPS compatible. + * + * This is different from `rustls_crypto_provider_fips` which is concerned + * only with cryptography, whereas this also covers TLS-level configuration that NIST + * recommends, as well as ECH HPKE suites if applicable. + */ +bool rustls_connection_fips(const struct rustls_connection *conn); + /** * Free a rustls_connection. Calling with NULL is fine. * Must not be called twice with the same value. @@ -2172,6 +2192,23 @@ const struct rustls_crypto_provider *rustls_ring_crypto_provider(void); const struct rustls_crypto_provider *rustls_aws_lc_rs_crypto_provider(void); #endif +#if defined(DEFINE_FIPS) +/** + * Return a `rustls_crypto_provider` that uses FIPS140-3 approved cryptography. + * + * Using this function expresses in your code that you require FIPS-approved cryptography, + * and will not compile if you make a mistake with cargo features. + * + * See the upstream [rustls FIPS documentation][FIPS] for more information. + * + * The caller owns the returned `rustls_crypto_provider` and must free it using + * `rustls_crypto_provider_free`. + * + * [FIPS]: https://docs.rs/rustls/latest/rustls/manual/_06_fips/index.html + */ +const struct rustls_crypto_provider *rustls_default_fips_provider(void); +#endif + /** * Retrieve a pointer to the process default `rustls_crypto_provider`. * @@ -2233,6 +2270,16 @@ rustls_result rustls_crypto_provider_random(const struct rustls_crypto_provider uint8_t *buff, size_t len); +/** + * Returns true if the `rustls_crypto_provider` is operating in FIPS mode. + * + * This covers only the cryptographic parts of FIPS approval. There are also + * TLS protocol-level recommendations made by NIST. You should prefer to call + * `rustls_client_config_fips` or `rustls_server_config_fips` which take these + * into account. + */ +bool rustls_crypto_provider_fips(const struct rustls_crypto_provider *provider); + /** * Frees the `rustls_crypto_provider`. * @@ -2497,6 +2544,16 @@ rustls_result rustls_server_config_builder_set_certified_keys(struct rustls_serv rustls_result rustls_server_config_builder_build(struct rustls_server_config_builder *builder, const struct rustls_server_config **config_out); +/** + * Returns true if a `rustls_connection` created from the `rustls_server_config` will + * operate in FIPS mode. + * + * This is different from `rustls_crypto_provider_fips` which is concerned + * only with cryptography, whereas this also covers TLS-level configuration that NIST + * recommends, as well as ECH HPKE suites if applicable. + */ +bool rustls_server_config_fips(const struct rustls_server_config *config); + /** * "Free" a rustls_server_config previously returned from * rustls_server_config_builder_build. diff --git a/src/server.rs b/src/server.rs index cf5d001f..f1650f12 100644 --- a/src/server.rs +++ b/src/server.rs @@ -377,6 +377,19 @@ impl rustls_server_config_builder { } impl rustls_server_config { + /// Returns true if a `rustls_connection` created from the `rustls_server_config` will + /// operate in FIPS mode. + /// + /// This is different from `rustls_crypto_provider_fips` which is concerned + /// only with cryptography, whereas this also covers TLS-level configuration that NIST + /// recommends, as well as ECH HPKE suites if applicable. + #[no_mangle] + pub extern "C" fn rustls_server_config_fips(config: *const rustls_server_config) -> bool { + ffi_panic_boundary! { + try_ref_from_ptr!(config).fips() + } + } + /// "Free" a rustls_server_config previously returned from /// rustls_server_config_builder_build. /// diff --git a/tests/client_server.rs b/tests/client_server.rs index 1f3d2a30..9f5e03ca 100644 --- a/tests/client_server.rs +++ b/tests/client_server.rs @@ -110,11 +110,17 @@ fn client_server_integration() { ], }; + // CHACHA20 is not FIPS approved :) + #[cfg(not(feature = "fips"))] + let custom_ciphersuite = "TLS13_CHACHA20_POLY1305_SHA256"; + #[cfg(feature = "fips")] + let custom_ciphersuite = "TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256"; + let custom_ciphersuites = TestCase { name: "client/server with limited ciphersuites", server_opts: ServerOptions { valgrind: valgrind.clone(), - env: vec![("RUSTLS_CIPHERSUITE", "TLS13_CHACHA20_POLY1305_SHA256")], + env: vec![("RUSTLS_CIPHERSUITE", custom_ciphersuite)], }, client_tests: vec![ ClientTest { @@ -122,7 +128,7 @@ fn client_server_integration() { valgrind: valgrind.clone(), env: vec![ ("NO_CHECK_CERTIFICATE", "1"), - ("RUSTLS_CIPHERSUITE", "TLS13_CHACHA20_POLY1305_SHA256"), + ("RUSTLS_CIPHERSUITE", custom_ciphersuite), ], expect_error: false, },