diff options
author | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-07 19:33:14 +0000 |
---|---|---|
committer | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-07 19:33:14 +0000 |
commit | 36d22d82aa202bb199967e9512281e9a53db42c9 (patch) | |
tree | 105e8c98ddea1c1e4784a60a5a6410fa416be2de /third_party/rust/neqo-crypto | |
parent | Initial commit. (diff) | |
download | firefox-esr-upstream.tar.xz firefox-esr-upstream.zip |
Adding upstream version 115.7.0esr.upstream/115.7.0esrupstream
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'third_party/rust/neqo-crypto')
48 files changed, 6621 insertions, 0 deletions
diff --git a/third_party/rust/neqo-crypto/.cargo-checksum.json b/third_party/rust/neqo-crypto/.cargo-checksum.json new file mode 100644 index 0000000000..35b698af13 --- /dev/null +++ b/third_party/rust/neqo-crypto/.cargo-checksum.json @@ -0,0 +1 @@ +{"files":{"Cargo.toml":"287a43ae2a0eb706add986604264bc942ce6405bd065f94d06bc23e6f502cfa5","TODO":"ac0f1c2ebcca03f5b3c0cc56c5aedbb030a4b511e438bc07a57361c789f91e9f","bindings/bindings.toml":"cb3e29ac6d1fd7083ffab81494afe1b9a2d9e41c60774438fd9681974c87c252","bindings/mozpkix.hpp":"77072c8bb0f6eb6bfe8cbadc111dcd92e0c79936d13f2e501aae1e5d289a6675","bindings/nspr_err.h":"2d5205d017b536c2d838bcf9bc4ec79f96dd50e7bb9b73892328781f1ee6629d","bindings/nspr_error.h":"e41c03c77b8c22046f8618832c9569fbcc7b26d8b9bbc35eea7168f35e346889","bindings/nspr_io.h":"085b289849ef0e77f88512a27b4d9bdc28252bd4d39c6a17303204e46ef45f72","bindings/nspr_time.h":"2e637fd338a5cf0fd3fb0070a47f474a34c2a7f4447f31b6875f5a9928d0a261","bindings/nss_ciphers.h":"95ec6344a607558b3c5ba8510f463b6295f3a2fb3f538a01410531045a5f62d1","bindings/nss_init.h":"ef49045063782fb612aff459172cc6a89340f15005808608ade5320ca9974310","bindings/nss_p11.h":"0b81e64fe6db49b2ecff94edd850be111ef99ec11220e88ceb1c67be90143a78","bindings/nss_secerr.h":"713e8368bdae5159af7893cfa517dabfe5103cede051dee9c9557c850a2defc6","bindings/nss_ssl.h":"af222fb957b989e392e762fa2125c82608a0053aff4fb97e556691646c88c335","bindings/nss_sslerr.h":"24b97f092183d8486f774cdaef5030d0249221c78343570d83a4ee5b594210ae","bindings/nss_sslopt.h":"b7807eb7abdad14db6ad7bc51048a46b065a0ea65a4508c95a12ce90e59d1eea","build.rs":"4fe138c0f7909f281ff6ba25dac4d69c1f21472f7cbbfed988f39500767fe995","src/aead.rs":"140f77ffb5016836c970c39c6c3a42db9581a14b797b9cd05386d0dd0831fe63","src/aead_fuzzing.rs":"e11ea62b4605b84400edbd5fc394f008fc26d3b67467ba7fe1597b90db4c6a12","src/agent.rs":"a0029413c37c71599971e08df8352fbfd7b7d1970a889ef85a1cabaff256ab11","src/agentio.rs":"bce4c3dfcfa433209a409ac0c0752f8c95ab37bb6239a42f99b83858e8747bd1","src/auth.rs":"ced1a18f691894984244088020ea25dc1ee678603317f0c7dfc8b8842fa750b4","src/cert.rs":"628fc2384cdeb2a32bfbecb83c99af393df97fd6a76c6e8827e8240c0d47328b","src/constants.rs":"998e77bee88197a240032c1bfbddcff417a25ba82e576a0d2fe18ee9b63cefc7","src/ech.rs":"447f6297f50914249d0c92ec61d74df6c65ce4affceecb8cafe40927e855fe1d","src/err.rs":"f2cc71de2b40d7bba8119eeaee200337db9a0126176ba06e4902d7312facdb58","src/exp.rs":"61586662407359c1ecb8ed4987bc3c702f26ba2e203a091a51b6d6363cbd510f","src/ext.rs":"361277879194dc32f741b8d1894afe5fd3fcc8eb244f7dd5914eeb959b85717d","src/hkdf.rs":"3ff432cc9d40e1dc56e9f983b54b593647c4063a5ae0f16de0a64d033ac9bd94","src/hp.rs":"606cae31d5cbbd5fd8f02e74e8e372ce6fabc7da3d0c625e905ff74e6cdde0e6","src/lib.rs":"8d1bfe33999b20eeb5f7eef70d4c634de560de5376029f81b2412bca60176c39","src/once.rs":"b9850384899a1a016e839743d3489c0d4d916e1973746ef8c89872105d7d9736","src/p11.rs":"d76a3d365fad3d7fbc8f7c0aacefc65ef737b9286672fa800d14711f50736a0e","src/prio.rs":"e5e169296c0ac69919c59fb6c1f8bd6bf079452eaa13d75da0edd41d435d3f6f","src/replay.rs":"c9bc0261fe1ae22e7212774c315a2669784e57762ca975a15250d4a33dbf3ea3","src/result.rs":"cef34dfcb907723e195b56501132e4560e250b327783cb5e41201da5b63e9b5c","src/secrets.rs":"d5d98e34b100568fecade932832379c52530c70a51ad2d37ee6b2dd008865f01","src/selfencrypt.rs":"f11ae7f6f1e6b602b6e729d71597116fb172190b57102af4f76b22fbe78c8b6a","src/ssl.rs":"90dfa84ebad961c5ecf3c2e85f9de7acf678dd3aed7e2b9f068036512f70a97d","src/time.rs":"9bacbed11b0e40402731a9803fd1531272e00e199217c1560130ede50d7020f6","tests/aead.rs":"31b5b4374cc5ca2deee6267c4d5b4858defc74e694ec85af196339a76548a17c","tests/agent.rs":"3b1d92cc1152a85b61a702e81f23fe654309eec75ba8113e731c11f02bf2cb27","tests/ext.rs":"eba9f03accdd598e38292ac88263a81b367d60d5a736a43117a3663de105ec48","tests/handshake.rs":"0fcfa8958686aacb42c56c51c6b234842fe990470d2069a67509869baaa18452","tests/hkdf.rs":"47830c1ea58a02d100522bdde6fabc02bb447ccb85affa0cdc44bc25da1be32a","tests/hp.rs":"316973740210fc1f8166920582795a41347a0aec9024fdc480e2ee83a7a5332d","tests/init.rs":"fc9e392b1efa0d8efb28952f73ffc05e5348e7b2b69207b60e375c3888a252a2","tests/selfencrypt.rs":"1125c858ec4e0a6994f34d162aa066cb003c61b324f268529ea04bcb641347cb"},"package":null}
\ No newline at end of file diff --git a/third_party/rust/neqo-crypto/Cargo.toml b/third_party/rust/neqo-crypto/Cargo.toml new file mode 100644 index 0000000000..d37b7de0c8 --- /dev/null +++ b/third_party/rust/neqo-crypto/Cargo.toml @@ -0,0 +1,49 @@ +# THIS FILE IS AUTOMATICALLY GENERATED BY CARGO +# +# When uploading crates to the registry Cargo will automatically +# "normalize" Cargo.toml files for maximal compatibility +# with all versions of Cargo and also rewrite `path` dependencies +# to registry (e.g., crates.io) dependencies. +# +# If you are reading this file be aware that the original Cargo.toml +# will likely look very different (and much more reasonable). +# See Cargo.toml.orig for the original contents. + +[package] +edition = "2018" +rust-version = "1.57.0" +name = "neqo-crypto" +version = "0.6.4" +authors = ["Martin Thomson <mt@lowentropy.net>"] +build = "build.rs" +license = "MIT/Apache-2.0" + +[dependencies.log] +version = "0.4.0" +default-features = false + +[dependencies.neqo-common] +path = "../neqo-common" + +[dev-dependencies.test-fixture] +path = "../test-fixture" + +[build-dependencies] +serde = "1.0" +serde_derive = "1.0" +toml = "0.5" + +[build-dependencies.bindgen] +version = "0.63" +features = ["runtime"] +default-features = false + +[build-dependencies.mozbuild] +version = "0.1" +optional = true + +[features] +default = ["deny-warnings"] +deny-warnings = [] +fuzzing = [] +gecko = ["mozbuild"] diff --git a/third_party/rust/neqo-crypto/TODO b/third_party/rust/neqo-crypto/TODO new file mode 100644 index 0000000000..b0552ea10f --- /dev/null +++ b/third_party/rust/neqo-crypto/TODO @@ -0,0 +1,4 @@ +early data - API in place for inspection, but depends on resumption
+handle panics more gracefully for extension handlers
+client certificates
+read/write - probably never
\ No newline at end of file diff --git a/third_party/rust/neqo-crypto/bindings/bindings.toml b/third_party/rust/neqo-crypto/bindings/bindings.toml new file mode 100644 index 0000000000..60916075c6 --- /dev/null +++ b/third_party/rust/neqo-crypto/bindings/bindings.toml @@ -0,0 +1,270 @@ +# In this file, every section corresponds to a header file. +# A corresponding binding file will be created in $OUT_DIR. + +[nss_ssl] +types = [ + "HpkeSymmetricSuite", + "PRCList", + "PRUint16", + "PRUint64", + "PRUint8", + "SECStatus", + "SSLAeadContext", + "SSLExtensionHandler", + "SSLExtensionType", + "SSLExtensionWriter", + "SSLHelloRetryRequestAction", + "SSLHelloRetryRequestCallback", + "SSLNamedGroup", + "SSLProtocolVariant", + "SSLRecordWriteCallback", + "SSLResumptionTokenCallback", + "SSLResumptionTokenInfo", + "SSLSecretCallback", + "SSLSignatureScheme", + "SSLTimeFunc", +] +functions = [ + "SSL_AlertSentCallback", + "SSL_AuthCertificateComplete", + "SSL_AuthCertificateHook", + "SSL_CipherPrefSet", + "SSL_ConfigServerCert", + "SSL_ConfigServerSessionIDCache", + "SSL_DestroyResumptionTokenInfo", + "SSL_GetChannelInfo", + "SSL_GetExperimentalAPI", + "SSL_GetImplementedCiphers", + "SSL_GetNextProto", + "SSL_GetNumImplementedCiphers", + "SSL_GetPreliminaryChannelInfo", + "SSL_GetResumptionTokenInfo", + "SSL_ForceHandshake", + "SSL_ImportFD", + "SSL_NamedGroupConfig", + "SSL_OptionSet", + "SSL_OptionGetDefault", + "SSL_PeerCertificate", + "SSL_PeerCertificateChain", + "SSL_PeerSignedCertTimestamps", + "SSL_PeerStapledOCSPResponses", + "SSL_ResetHandshake", + "SSL_SetNextProtoNego", + "SSL_SetURL", + "SSL_VersionRangeSet", +] +enums = [ + "HpkeAeadId", + "HpkeKdfId", + "SSLAuthType", + "SSLCipherAlgorithm", + "SSLCompressionMethod", + "SSLContentType", + "SSLExtensionType", + "SSLHandshakeType", + "SSLHelloRetryRequestAction", + "SSLKEAType", + "SSLMACAlgorithm", + "SSLNamedGroup", + "SSLNextProtoState", + "SSLProtocolVariant", + "SSLSecretDirection", + "SSLSignatureScheme", + "SECStatus", +] +variables = [ + "SSL_LIBRARY_VERSION_TLS_\\d_\\d", + "SSL_NumImplementedCiphers", + "ssl_preinfo_.*", +] +opaque = [ + "CERTCertificate", + "PK11SymKey", + "PLArenaPool", + "PRFileDesc", + "SECKEYPrivateKey", + "SECKEYPublicKey", + "SSLExtraServerCertData", +] + +[nss_sslopt] +variables = [ + "SSL_REQUEST_CERTIFICATE", + "SSL_REQUIRE_CERTIFICATE", + "SSL_NO_LOCKS", + "SSL_ENABLE_SESSION_TICKETS", + "SSL_ENABLE_OCSP_STAPLING", + "SSL_ENABLE_ALPN", + "SSL_ENABLE_EXTENDED_MASTER_SECRET", + "SSL_ENABLE_SIGNED_CERT_TIMESTAMPS", + "SSL_ENABLE_0RTT_DATA", + "SSL_RECORD_SIZE_LIMIT", + "SSL_ENABLE_TLS13_COMPAT_MODE", + "SSL_ENABLE_HELLO_DOWNGRADE_CHECK", + "SSL_SUPPRESS_END_OF_EARLY_DATA", +] + +[nss_ciphers] +variables = ["TLS_.*"] +exclude = [ + ".*_(?:EXPORT(?:1024)?|anon|DES|RC4)_.*", + ".*_(?:MD5|NULL_SHA)", +] + +[nss_secerr] +types = ["SECErrorCodes"] +enums = ["SECErrorCodes"] + +[nss_sslerr] +types = ["SSLErrorCodes"] +enums = ["SSLErrorCodes"] + +[nss_init] +functions = [ + "NSS_Initialize", + "NSS_IsInitialized", + "NSS_NoDB_Init", + "NSS_SetDomesticPolicy", + "NSS_Shutdown", + "NSS_VersionCheck", +] +variables = [ + "NSS_INIT_READONLY", + "SECMOD_DB", +] + +[nss_p11] +types = [ + "CERTCertList", + "CERTCertListNode", + "CK_CHACHA20_PARAMS", + "CK_ATTRIBUTE_TYPE", + "CK_FLAGS", + "CK_MECHANISM_TYPE", + "HpkeAeadId", + "HpkeKdfId", + "HpkeKemId", + "SECItem", + "SECItemArray", +] +functions = [ + "CERT_DestroyCertificate", + "CERT_DestroyCertList", + "CERT_GetCertificateDer", + "PK11_CipherOp", + "PK11_CreateContextBySymKey", + "PK11_DestroyContext", + "PK11_Encrypt", + "PK11_ExtractKeyValue", + "PK11_FindCertFromNickname", + "PK11_FindKeyByAnyCert", + "PK11_FreeSlot", + "PK11_FreeSymKey", + "PK11_GenerateKeyPairWithOpFlags", + "PK11_GenerateRandom", + "PK11_GetBlockSize", + "PK11_GetInternalSlot", + "PK11_GetKeyData", + "PK11_GetMechanism", + "PK11_HPKE_Serialize", + "PK11_ImportDataKey", + "PK11_ReadRawAttribute", + "PK11_ReferenceSymKey", + "SECITEM_FreeItem", + "SECKEY_CopyPrivateKey", + "SECKEY_CopyPublicKey", + "SECKEY_DestroyPrivateKey", + "SECKEY_DestroyPublicKey", + "SECOID_FindOIDByTag", +] +enums = [ + "HpkeAeadId", + "HpkeKdfId", + "HpkeKemId", + "PK11ObjectType", + "PK11Origin", + "SECItemType", + "SECOidTag", +] +opaque = [ + "CERTCertificate", + "PK11Context", + "PK11SlotInfo", + "PK11SymKey", + "SECKEYPrivateKey", + "SECKEYPublicKey", +] +variables = [ + "CKA_DERIVE", + "CKA_ENCRYPT", + "CKA_VALUE", + "CKF_DERIVE", + "CKM_AES_ECB", + "CKM_AES_GCM", + "CKM_CHACHA20", + "CKM_CHACHA20_POLY1305", + "CKM_EC_KEY_PAIR_GEN", + "CKM_HKDF_DERIVE", + "CKM_INVALID_MECHANISM", + "PK11_ATTR_INSENSITIVE", + "PK11_ATTR_PRIVATE", + "PK11_ATTR_PUBLIC", + "PK11_ATTR_SENSITIVE", + "PK11_ATTR_SESSION", + "SEC_ASN1_OBJECT_ID", +] + +[nspr_err] +# NSPR doesn't use an enum for errors, so we have to pull in everything in the header file. +# Specifying no types, functions, or variables does that, but then exclude some. +exclude = [ + "nspr_.*", + "PR_MAX_ERROR", + "ERROR_TABLE_BASE_nspr" +] + +[nspr_error] +functions = [ + "PR_ErrorToName", + "PR_ErrorToString", + "PR_GetError", + "PR_SetError", +] +variables = [ + "PR_LANGUAGE_I_DEFAULT", +] + +[nspr_io] +types = ["PRIOMethods"] +functions = [ + "PR_Close", + "PR_CreateIOLayerStub", + "PR_GetUniqueIdentity", +] +variables = [ + "PR_AF_INET", +] +# opaque is for the stuff we don't plan to use, but we need for function signatures. +opaque = [ + "PRFileInfo", + "PRFileInfo64", + "PRFilePrivate", + "PRIOVec", + "PRSendFileData", +] +enums = [ + "PRDescType", + "PRStatus", + "PRSeekWhence", + "PRSockOption", + "PRTransmitFileFlags", +] + +[nspr_time] +types = ["PRTime"] +functions = ["PR_Now"] + +[mozpkix] +cplusplus = true +types = ["mozilla::pkix::ErrorCode"] +enums = ["mozilla::pkix::ErrorCode"] diff --git a/third_party/rust/neqo-crypto/bindings/mozpkix.hpp b/third_party/rust/neqo-crypto/bindings/mozpkix.hpp new file mode 100644 index 0000000000..d0a6cb5861 --- /dev/null +++ b/third_party/rust/neqo-crypto/bindings/mozpkix.hpp @@ -0,0 +1 @@ +#include "mozpkix/pkixnss.h"
\ No newline at end of file diff --git a/third_party/rust/neqo-crypto/bindings/nspr_err.h b/third_party/rust/neqo-crypto/bindings/nspr_err.h new file mode 100644 index 0000000000..204e771c49 --- /dev/null +++ b/third_party/rust/neqo-crypto/bindings/nspr_err.h @@ -0,0 +1,7 @@ +// Licensed under the Apache License, Version 2.0 <LICENSE-APACHE or +// http://www.apache.org/licenses/LICENSE-2.0> or the MIT license +// <LICENSE-MIT or http://opensource.org/licenses/MIT>, at your +// option. This file may not be copied, modified, or distributed +// except according to those terms. + +#include "prerr.h" diff --git a/third_party/rust/neqo-crypto/bindings/nspr_error.h b/third_party/rust/neqo-crypto/bindings/nspr_error.h new file mode 100644 index 0000000000..8ff8ce202d --- /dev/null +++ b/third_party/rust/neqo-crypto/bindings/nspr_error.h @@ -0,0 +1,7 @@ +// Licensed under the Apache License, Version 2.0 <LICENSE-APACHE or +// http://www.apache.org/licenses/LICENSE-2.0> or the MIT license +// <LICENSE-MIT or http://opensource.org/licenses/MIT>, at your +// option. This file may not be copied, modified, or distributed +// except according to those terms. + +#include "prerror.h" diff --git a/third_party/rust/neqo-crypto/bindings/nspr_io.h b/third_party/rust/neqo-crypto/bindings/nspr_io.h new file mode 100644 index 0000000000..9997fb812e --- /dev/null +++ b/third_party/rust/neqo-crypto/bindings/nspr_io.h @@ -0,0 +1,7 @@ +// Licensed under the Apache License, Version 2.0 <LICENSE-APACHE or +// http://www.apache.org/licenses/LICENSE-2.0> or the MIT license +// <LICENSE-MIT or http://opensource.org/licenses/MIT>, at your +// option. This file may not be copied, modified, or distributed +// except according to those terms. + +#include "prio.h" diff --git a/third_party/rust/neqo-crypto/bindings/nspr_time.h b/third_party/rust/neqo-crypto/bindings/nspr_time.h new file mode 100644 index 0000000000..f5596577fa --- /dev/null +++ b/third_party/rust/neqo-crypto/bindings/nspr_time.h @@ -0,0 +1,7 @@ +// Licensed under the Apache License, Version 2.0 <LICENSE-APACHE or +// http://www.apache.org/licenses/LICENSE-2.0> or the MIT license +// <LICENSE-MIT or http://opensource.org/licenses/MIT>, at your +// option. This file may not be copied, modified, or distributed +// except according to those terms. + +#include "prtime.h" diff --git a/third_party/rust/neqo-crypto/bindings/nss_ciphers.h b/third_party/rust/neqo-crypto/bindings/nss_ciphers.h new file mode 100644 index 0000000000..f064f39c5d --- /dev/null +++ b/third_party/rust/neqo-crypto/bindings/nss_ciphers.h @@ -0,0 +1,8 @@ +// Licensed under the Apache License, Version 2.0 <LICENSE-APACHE or +// http://www.apache.org/licenses/LICENSE-2.0> or the MIT license +// <LICENSE-MIT or http://opensource.org/licenses/MIT>, at your +// option. This file may not be copied, modified, or distributed +// except according to those terms. + +#define SSL_DISABLE_DEPRECATED_CIPHER_SUITE_NAMES +#include "sslproto.h" diff --git a/third_party/rust/neqo-crypto/bindings/nss_init.h b/third_party/rust/neqo-crypto/bindings/nss_init.h new file mode 100644 index 0000000000..ae111bac2b --- /dev/null +++ b/third_party/rust/neqo-crypto/bindings/nss_init.h @@ -0,0 +1,8 @@ +// Licensed under the Apache License, Version 2.0 <LICENSE-APACHE or +// http://www.apache.org/licenses/LICENSE-2.0> or the MIT license +// <LICENSE-MIT or http://opensource.org/licenses/MIT>, at your +// option. This file may not be copied, modified, or distributed +// except according to those terms. + +#include "nss.h" +#include "ssl.h" // For NSS_SetDomesticPolicy diff --git a/third_party/rust/neqo-crypto/bindings/nss_p11.h b/third_party/rust/neqo-crypto/bindings/nss_p11.h new file mode 100644 index 0000000000..7de50eebec --- /dev/null +++ b/third_party/rust/neqo-crypto/bindings/nss_p11.h @@ -0,0 +1,9 @@ +// Licensed under the Apache License, Version 2.0 <LICENSE-APACHE or +// http://www.apache.org/licenses/LICENSE-2.0> or the MIT license +// <LICENSE-MIT or http://opensource.org/licenses/MIT>, at your +// option. This file may not be copied, modified, or distributed +// except according to those terms. + +#include "cert.h" +#include "keyhi.h" +#include "pk11pub.h" diff --git a/third_party/rust/neqo-crypto/bindings/nss_secerr.h b/third_party/rust/neqo-crypto/bindings/nss_secerr.h new file mode 100644 index 0000000000..c2b2d4020c --- /dev/null +++ b/third_party/rust/neqo-crypto/bindings/nss_secerr.h @@ -0,0 +1,7 @@ +// Licensed under the Apache License, Version 2.0 <LICENSE-APACHE or +// http://www.apache.org/licenses/LICENSE-2.0> or the MIT license +// <LICENSE-MIT or http://opensource.org/licenses/MIT>, at your +// option. This file may not be copied, modified, or distributed +// except according to those terms. + +#include "secerr.h" diff --git a/third_party/rust/neqo-crypto/bindings/nss_ssl.h b/third_party/rust/neqo-crypto/bindings/nss_ssl.h new file mode 100644 index 0000000000..1cde112cf2 --- /dev/null +++ b/third_party/rust/neqo-crypto/bindings/nss_ssl.h @@ -0,0 +1,9 @@ +// Licensed under the Apache License, Version 2.0 <LICENSE-APACHE or +// http://www.apache.org/licenses/LICENSE-2.0> or the MIT license +// <LICENSE-MIT or http://opensource.org/licenses/MIT>, at your +// option. This file may not be copied, modified, or distributed +// except according to those terms. + +#include "sslproto.h" +#include "ssl.h" +#include "sslexp.h" diff --git a/third_party/rust/neqo-crypto/bindings/nss_sslerr.h b/third_party/rust/neqo-crypto/bindings/nss_sslerr.h new file mode 100644 index 0000000000..74a836f1e8 --- /dev/null +++ b/third_party/rust/neqo-crypto/bindings/nss_sslerr.h @@ -0,0 +1,7 @@ +// Licensed under the Apache License, Version 2.0 <LICENSE-APACHE or +// http://www.apache.org/licenses/LICENSE-2.0> or the MIT license +// <LICENSE-MIT or http://opensource.org/licenses/MIT>, at your +// option. This file may not be copied, modified, or distributed +// except according to those terms. + +#include "sslerr.h" diff --git a/third_party/rust/neqo-crypto/bindings/nss_sslopt.h b/third_party/rust/neqo-crypto/bindings/nss_sslopt.h new file mode 100644 index 0000000000..a14e1e69d1 --- /dev/null +++ b/third_party/rust/neqo-crypto/bindings/nss_sslopt.h @@ -0,0 +1,7 @@ +// Licensed under the Apache License, Version 2.0 <LICENSE-APACHE or +// http://www.apache.org/licenses/LICENSE-2.0> or the MIT license +// <LICENSE-MIT or http://opensource.org/licenses/MIT>, at your +// option. This file may not be copied, modified, or distributed +// except according to those terms. + +#include "ssl.h" diff --git a/third_party/rust/neqo-crypto/build.rs b/third_party/rust/neqo-crypto/build.rs new file mode 100644 index 0000000000..7cbc111509 --- /dev/null +++ b/third_party/rust/neqo-crypto/build.rs @@ -0,0 +1,411 @@ +// Licensed under the Apache License, Version 2.0 <LICENSE-APACHE or +// http://www.apache.org/licenses/LICENSE-2.0> or the MIT license +// <LICENSE-MIT or http://opensource.org/licenses/MIT>, at your +// option. This file may not be copied, modified, or distributed +// except according to those terms. + +#![cfg_attr(feature = "deny-warnings", deny(warnings))] +#![warn(clippy::pedantic)] + +use bindgen::Builder; +use serde_derive::Deserialize; +use std::collections::HashMap; +use std::env; +use std::fs; +use std::path::{Path, PathBuf}; +use std::process::Command; + +const BINDINGS_DIR: &str = "bindings"; +const BINDINGS_CONFIG: &str = "bindings.toml"; + +// This is the format of a single section of the configuration file. +#[derive(Deserialize)] +struct Bindings { + /// types that are explicitly included + #[serde(default)] + types: Vec<String>, + /// functions that are explicitly included + #[serde(default)] + functions: Vec<String>, + /// variables (and `#define`s) that are explicitly included + #[serde(default)] + variables: Vec<String>, + /// types that should be explicitly marked as opaque + #[serde(default)] + opaque: Vec<String>, + /// enumerations that are turned into a module (without this, the enum is + /// mapped using the default, which means that the individual values are + /// formed with an underscore as <enum_type>_<enum_value_name>). + #[serde(default)] + enums: Vec<String>, + + /// Any item that is specifically excluded; if none of the types, functions, + /// or variables fields are specified, everything defined will be mapped, + /// so this can be used to limit that. + #[serde(default)] + exclude: Vec<String>, + + /// Whether the file is to be interpreted as C++ + #[serde(default)] + cplusplus: bool, +} + +fn is_debug() -> bool { + env::var("DEBUG") + .map(|d| d.parse::<bool>().unwrap_or(false)) + .unwrap_or(false) +} + +// bindgen needs access to libclang. +// On windows, this doesn't just work, you have to set LIBCLANG_PATH. +// Rather than download the 400Mb+ files, like gecko does, let's just reuse their work. +fn setup_clang() { + if env::consts::OS != "windows" { + return; + } + println!("rerun-if-env-changed=LIBCLANG_PATH"); + println!("rerun-if-env-changed=MOZBUILD_STATE_PATH"); + if env::var("LIBCLANG_PATH").is_ok() { + return; + } + let mozbuild_root = if let Ok(dir) = env::var("MOZBUILD_STATE_PATH") { + PathBuf::from(dir.trim()) + } else { + eprintln!("warning: Building without a gecko setup is not likely to work."); + eprintln!(" A working libclang is needed to build neqo."); + eprintln!(" Either LIBCLANG_PATH or MOZBUILD_STATE_PATH needs to be set."); + eprintln!(); + eprintln!(" We recommend checking out https://github.com/mozilla/gecko-dev"); + eprintln!(" Then run `./mach bootstrap` which will retrieve clang."); + eprintln!(" Make sure to export MOZBUILD_STATE_PATH when building."); + return; + }; + let libclang_dir = mozbuild_root.join("clang").join("lib"); + if libclang_dir.is_dir() { + env::set_var("LIBCLANG_PATH", libclang_dir.to_str().unwrap()); + println!("rustc-env:LIBCLANG_PATH={}", libclang_dir.to_str().unwrap()); + } else { + println!("warning: LIBCLANG_PATH isn't set; maybe run ./mach bootstrap with gecko"); + } +} + +fn nss_dir() -> PathBuf { + let dir = if let Ok(dir) = env::var("NSS_DIR") { + let path = PathBuf::from(dir.trim()); + assert!( + !path.is_relative(), + "The NSS_DIR environment variable is expected to be an absolute path." + ); + path + } else { + let out_dir = env::var("OUT_DIR").unwrap(); + let dir = Path::new(&out_dir).join("nss"); + if !dir.exists() { + Command::new("hg") + .args([ + "clone", + "https://hg.mozilla.org/projects/nss", + dir.to_str().unwrap(), + ]) + .status() + .expect("can't clone nss"); + } + let nspr_dir = Path::new(&out_dir).join("nspr"); + if !nspr_dir.exists() { + Command::new("hg") + .args([ + "clone", + "https://hg.mozilla.org/projects/nspr", + nspr_dir.to_str().unwrap(), + ]) + .status() + .expect("can't clone nspr"); + } + dir + }; + assert!(dir.is_dir(), "NSS_DIR {:?} doesn't exist", dir); + // Note that this returns a relative path because UNC + // paths on windows cause certain tools to explode. + dir +} + +fn get_bash() -> PathBuf { + // When running under MOZILLABUILD, we need to make sure not to invoke + // another instance of bash that might be sitting around (like WSL). + match env::var("MOZILLABUILD") { + Ok(d) => PathBuf::from(d).join("msys").join("bin").join("bash.exe"), + Err(_) => PathBuf::from("bash"), + } +} + +fn build_nss(dir: PathBuf) { + let mut build_nss = vec![ + String::from("./build.sh"), + String::from("-Ddisable_tests=1"), + ]; + if is_debug() { + build_nss.push(String::from("--static")); + } else { + build_nss.push(String::from("-o")); + } + if let Ok(d) = env::var("NSS_JOBS") { + build_nss.push(String::from("-j")); + build_nss.push(d); + } + let status = Command::new(get_bash()) + .args(build_nss) + .current_dir(dir) + .status() + .expect("couldn't start NSS build"); + assert!(status.success(), "NSS build failed"); +} + +fn dynamic_link() { + let libs = if env::consts::OS == "windows" { + &["nssutil3.dll", "nss3.dll", "ssl3.dll"] + } else { + &["nssutil3", "nss3", "ssl3"] + }; + dynamic_link_both(libs); +} + +fn dynamic_link_both(extra_libs: &[&str]) { + let nspr_libs = if env::consts::OS == "windows" { + &["libplds4", "libplc4", "libnspr4"] + } else { + &["plds4", "plc4", "nspr4"] + }; + for lib in nspr_libs.iter().chain(extra_libs) { + println!("cargo:rustc-link-lib=dylib={}", lib); + } +} + +fn static_link() { + let mut static_libs = vec![ + "certdb", + "certhi", + "cryptohi", + "freebl", + "nss_static", + "nssb", + "nssdev", + "nsspki", + "nssutil", + "pk11wrap", + "pkcs12", + "pkcs7", + "smime", + "softokn_static", + "ssl", + ]; + if env::consts::OS != "macos" { + static_libs.push("sqlite"); + } + for lib in static_libs { + println!("cargo:rustc-link-lib=static={}", lib); + } + + // Dynamic libs that aren't transitively included by NSS libs. + let mut other_libs = Vec::new(); + if env::consts::OS != "windows" { + other_libs.extend_from_slice(&["pthread", "dl", "c", "z"]); + } + if env::consts::OS == "macos" { + other_libs.push("sqlite3"); + } + dynamic_link_both(&other_libs); +} + +fn get_includes(nsstarget: &Path, nssdist: &Path) -> Vec<PathBuf> { + let nsprinclude = nsstarget.join("include").join("nspr"); + let nssinclude = nssdist.join("public").join("nss"); + let includes = vec![nsprinclude, nssinclude]; + for i in &includes { + println!("cargo:include={}", i.to_str().unwrap()); + } + includes +} + +fn build_bindings(base: &str, bindings: &Bindings, flags: &[String], gecko: bool) { + let suffix = if bindings.cplusplus { ".hpp" } else { ".h" }; + let header_path = PathBuf::from(BINDINGS_DIR).join(String::from(base) + suffix); + let header = header_path.to_str().unwrap(); + let out = PathBuf::from(env::var("OUT_DIR").unwrap()).join(String::from(base) + ".rs"); + + println!("cargo:rerun-if-changed={}", header); + + let mut builder = Builder::default().header(header); + builder = builder.generate_comments(false); + builder = builder.size_t_is_usize(true); + + builder = builder.clang_arg("-v"); + + if !gecko { + builder = builder.clang_arg("-DNO_NSPR_10_SUPPORT"); + if env::consts::OS == "windows" { + builder = builder.clang_arg("-DWIN"); + } else if env::consts::OS == "macos" { + builder = builder.clang_arg("-DDARWIN"); + } else if env::consts::OS == "linux" { + builder = builder.clang_arg("-DLINUX"); + } else if env::consts::OS == "android" { + builder = builder.clang_arg("-DLINUX"); + builder = builder.clang_arg("-DANDROID"); + } + if bindings.cplusplus { + builder = builder.clang_args(&["-x", "c++", "-std=c++11"]); + } + } + + builder = builder.clang_args(flags); + + // Apply the configuration. + for v in &bindings.types { + builder = builder.allowlist_type(v); + } + for v in &bindings.functions { + builder = builder.allowlist_function(v); + } + for v in &bindings.variables { + builder = builder.allowlist_var(v); + } + for v in &bindings.exclude { + builder = builder.blocklist_item(v); + } + for v in &bindings.opaque { + builder = builder.opaque_type(v); + } + for v in &bindings.enums { + builder = builder.constified_enum_module(v); + } + + let bindings = builder.generate().expect("unable to generate bindings"); + bindings + .write_to_file(out) + .expect("couldn't write bindings"); +} + +fn setup_standalone() -> Vec<String> { + setup_clang(); + + println!("cargo:rerun-if-env-changed=NSS_DIR"); + let nss = nss_dir(); + build_nss(nss.clone()); + + // $NSS_DIR/../dist/ + let nssdist = nss.parent().unwrap().join("dist"); + println!("cargo:rerun-if-env-changed=NSS_TARGET"); + let nsstarget = env::var("NSS_TARGET") + .unwrap_or_else(|_| fs::read_to_string(nssdist.join("latest")).unwrap()); + let nsstarget = nssdist.join(nsstarget.trim()); + + let includes = get_includes(&nsstarget, &nssdist); + + let nsslibdir = nsstarget.join("lib"); + println!( + "cargo:rustc-link-search=native={}", + nsslibdir.to_str().unwrap() + ); + if is_debug() { + static_link(); + } else { + dynamic_link(); + } + + let mut flags: Vec<String> = Vec::new(); + for i in includes { + flags.push(String::from("-I") + i.to_str().unwrap()); + } + + flags +} + +#[cfg(feature = "gecko")] +fn setup_for_gecko() -> Vec<String> { + use mozbuild::TOPOBJDIR; + + let fold_libs = mozbuild::config::MOZ_FOLD_LIBS; + let libs = if fold_libs { + vec!["nss3"] + } else { + vec!["nssutil3", "nss3", "ssl3", "plds4", "plc4", "nspr4"] + }; + + for lib in &libs { + println!("cargo:rustc-link-lib=dylib={}", lib); + } + + if fold_libs { + println!( + "cargo:rustc-link-search=native={}", + TOPOBJDIR.join("security").to_str().unwrap() + ); + } else { + println!( + "cargo:rustc-link-search=native={}", + TOPOBJDIR.join("dist").join("bin").to_str().unwrap() + ); + let nsslib_path = TOPOBJDIR.join("security").join("nss").join("lib"); + println!( + "cargo:rustc-link-search=native={}", + nsslib_path.join("nss").join("nss_nss3").to_str().unwrap() + ); + println!( + "cargo:rustc-link-search=native={}", + nsslib_path.join("ssl").join("ssl_ssl3").to_str().unwrap() + ); + println!( + "cargo:rustc-link-search=native={}", + TOPOBJDIR + .join("config") + .join("external") + .join("nspr") + .join("pr") + .to_str() + .unwrap() + ); + } + + let flags_path = TOPOBJDIR.join("netwerk/socket/neqo/extra-bindgen-flags"); + + println!("cargo:rerun-if-changed={}", flags_path.to_str().unwrap()); + let mut flags = fs::read_to_string(flags_path) + .expect("Failed to read extra-bindgen-flags file") + .split_whitespace() + .map(String::from) + .collect::<Vec<_>>(); + + flags.push(String::from("-include")); + flags.push( + TOPOBJDIR + .join("dist") + .join("include") + .join("mozilla-config.h") + .to_str() + .unwrap() + .to_string(), + ); + flags +} + +#[cfg(not(feature = "gecko"))] +fn setup_for_gecko() -> Vec<String> { + unreachable!() +} + +fn main() { + let flags = if cfg!(feature = "gecko") { + setup_for_gecko() + } else { + setup_standalone() + }; + + let config_file = PathBuf::from(BINDINGS_DIR).join(BINDINGS_CONFIG); + println!("cargo:rerun-if-changed={}", config_file.to_str().unwrap()); + let config = fs::read_to_string(config_file).expect("unable to read binding configuration"); + let config: HashMap<String, Bindings> = ::toml::from_str(&config).unwrap(); + + for (k, v) in &config { + build_bindings(k, v, &flags[..], cfg!(feature = "gecko")); + } +} diff --git a/third_party/rust/neqo-crypto/src/aead.rs b/third_party/rust/neqo-crypto/src/aead.rs new file mode 100644 index 0000000000..74f0ae0ce2 --- /dev/null +++ b/third_party/rust/neqo-crypto/src/aead.rs @@ -0,0 +1,161 @@ +// Licensed under the Apache License, Version 2.0 <LICENSE-APACHE or +// http://www.apache.org/licenses/LICENSE-2.0> or the MIT license +// <LICENSE-MIT or http://opensource.org/licenses/MIT>, at your +// option. This file may not be copied, modified, or distributed +// except according to those terms. + +use crate::constants::{Cipher, Version}; +use crate::err::Res; +use crate::p11::{PK11SymKey, SymKey}; +use crate::ssl; +use crate::ssl::{PRUint16, PRUint64, PRUint8, SSLAeadContext}; + +use std::convert::{TryFrom, TryInto}; +use std::fmt; +use std::ops::{Deref, DerefMut}; +use std::os::raw::{c_char, c_uint}; +use std::ptr::null_mut; + +experimental_api!(SSL_MakeAead( + version: PRUint16, + cipher: PRUint16, + secret: *mut PK11SymKey, + label_prefix: *const c_char, + label_prefix_len: c_uint, + ctx: *mut *mut SSLAeadContext, +)); +experimental_api!(SSL_AeadEncrypt( + ctx: *const SSLAeadContext, + counter: PRUint64, + aad: *const PRUint8, + aad_len: c_uint, + input: *const PRUint8, + input_len: c_uint, + output: *const PRUint8, + output_len: *mut c_uint, + max_output: c_uint +)); +experimental_api!(SSL_AeadDecrypt( + ctx: *const SSLAeadContext, + counter: PRUint64, + aad: *const PRUint8, + aad_len: c_uint, + input: *const PRUint8, + input_len: c_uint, + output: *const PRUint8, + output_len: *mut c_uint, + max_output: c_uint +)); +experimental_api!(SSL_DestroyAead(ctx: *mut SSLAeadContext)); +scoped_ptr!(AeadContext, SSLAeadContext, SSL_DestroyAead); + +pub struct Aead { + ctx: AeadContext, +} + +impl Aead { + /// Create a new AEAD based on the indicated TLS version and cipher suite. + /// + /// # Errors + /// Returns `Error` when the supporting NSS functions fail. + pub fn new(version: Version, cipher: Cipher, secret: &SymKey, prefix: &str) -> Res<Self> { + let s: *mut PK11SymKey = **secret; + unsafe { Self::from_raw(version, cipher, s, prefix) } + } + + #[must_use] + #[allow(clippy::unused_self)] + pub fn expansion(&self) -> usize { + 16 + } + + unsafe fn from_raw( + version: Version, + cipher: Cipher, + secret: *mut PK11SymKey, + prefix: &str, + ) -> Res<Self> { + let p = prefix.as_bytes(); + let mut ctx: *mut ssl::SSLAeadContext = null_mut(); + SSL_MakeAead( + version, + cipher, + secret, + p.as_ptr().cast(), + c_uint::try_from(p.len())?, + &mut ctx, + )?; + Ok(Self { + ctx: AeadContext::from_ptr(ctx)?, + }) + } + + /// Decrypt a plaintext. + /// + /// The space provided in `output` needs to be larger than `input` by + /// the value provided in `Aead::expansion`. + /// + /// # Errors + /// If the input can't be protected or any input is too large for NSS. + pub fn encrypt<'a>( + &self, + count: u64, + aad: &[u8], + input: &[u8], + output: &'a mut [u8], + ) -> Res<&'a [u8]> { + let mut l: c_uint = 0; + unsafe { + SSL_AeadEncrypt( + *self.ctx.deref(), + count, + aad.as_ptr(), + c_uint::try_from(aad.len())?, + input.as_ptr(), + c_uint::try_from(input.len())?, + output.as_mut_ptr(), + &mut l, + c_uint::try_from(output.len())?, + ) + }?; + Ok(&output[0..(l.try_into()?)]) + } + + /// Decrypt a ciphertext. + /// + /// Note that NSS insists upon having extra space available for decryption, so + /// the buffer for `output` should be the same length as `input`, even though + /// the final result will be shorter. + /// + /// # Errors + /// If the input isn't authenticated or any input is too large for NSS. + pub fn decrypt<'a>( + &self, + count: u64, + aad: &[u8], + input: &[u8], + output: &'a mut [u8], + ) -> Res<&'a [u8]> { + let mut l: c_uint = 0; + unsafe { + SSL_AeadDecrypt( + *self.ctx.deref(), + count, + aad.as_ptr(), + c_uint::try_from(aad.len())?, + input.as_ptr(), + c_uint::try_from(input.len())?, + output.as_mut_ptr(), + &mut l, + c_uint::try_from(output.len())?, + ) + }?; + Ok(&output[0..(l.try_into()?)]) + } +} + +impl fmt::Debug for Aead { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + write!(f, "[AEAD Context]") + } +} diff --git a/third_party/rust/neqo-crypto/src/aead_fuzzing.rs b/third_party/rust/neqo-crypto/src/aead_fuzzing.rs new file mode 100644 index 0000000000..2c5cc6c56e --- /dev/null +++ b/third_party/rust/neqo-crypto/src/aead_fuzzing.rs @@ -0,0 +1,70 @@ +// Licensed under the Apache License, Version 2.0 <LICENSE-APACHE or +// http://www.apache.org/licenses/LICENSE-2.0> or the MIT license +// <LICENSE-MIT or http://opensource.org/licenses/MIT>, at your +// option. This file may not be copied, modified, or distributed +// except according to those terms. + +use crate::constants::{Cipher, Version}; +use crate::err::{sec::SEC_ERROR_BAD_DATA, Error, Res}; +use crate::p11::SymKey; +use std::fmt; + +pub const FIXED_TAG_FUZZING: &[u8] = &[0x0a; 16]; +pub struct Aead {} + +#[allow(clippy::unused_self)] +impl Aead { + pub fn new(_version: Version, _cipher: Cipher, _secret: &SymKey, _prefix: &str) -> Res<Self> { + Ok(Self {}) + } + + #[must_use] + pub fn expansion(&self) -> usize { + FIXED_TAG_FUZZING.len() + } + + pub fn encrypt<'a>( + &self, + _count: u64, + _aad: &[u8], + input: &[u8], + output: &'a mut [u8], + ) -> Res<&'a [u8]> { + let l = input.len(); + output[..l].copy_from_slice(input); + output[l..l + 16].copy_from_slice(FIXED_TAG_FUZZING); + Ok(&output[..l + 16]) + } + + pub fn decrypt<'a>( + &self, + _count: u64, + _aad: &[u8], + input: &[u8], + output: &'a mut [u8], + ) -> Res<&'a [u8]> { + if input.len() < FIXED_TAG_FUZZING.len() { + return Err(Error::from(SEC_ERROR_BAD_DATA)); + } + + let len_encrypted = input.len() - FIXED_TAG_FUZZING.len(); + // Check that: + // 1) expansion is all zeros and + // 2) if the encrypted data is also supplied that at least some values + // are no zero (otherwise padding will be interpreted as a valid packet) + if &input[len_encrypted..] == FIXED_TAG_FUZZING + && (len_encrypted == 0 || input[..len_encrypted].iter().any(|x| *x != 0x0)) + { + output[..len_encrypted].copy_from_slice(&input[..len_encrypted]); + Ok(&output[..len_encrypted]) + } else { + Err(Error::from(SEC_ERROR_BAD_DATA)) + } + } +} + +impl fmt::Debug for Aead { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + write!(f, "[FUZZING AEAD]") + } +} diff --git a/third_party/rust/neqo-crypto/src/agent.rs b/third_party/rust/neqo-crypto/src/agent.rs new file mode 100644 index 0000000000..9163c4c711 --- /dev/null +++ b/third_party/rust/neqo-crypto/src/agent.rs @@ -0,0 +1,1219 @@ +// Licensed under the Apache License, Version 2.0 <LICENSE-APACHE or +// http://www.apache.org/licenses/LICENSE-2.0> or the MIT license +// <LICENSE-MIT or http://opensource.org/licenses/MIT>, at your +// option. This file may not be copied, modified, or distributed +// except according to those terms. + +pub use crate::agentio::{as_c_void, Record, RecordList}; +use crate::agentio::{AgentIo, METHODS}; +use crate::assert_initialized; +use crate::auth::AuthenticationStatus; +pub use crate::cert::CertificateInfo; +use crate::constants::{ + Alert, Cipher, Epoch, Extension, Group, SignatureScheme, Version, TLS_VERSION_1_3, +}; +use crate::ech; +use crate::err::{is_blocked, secstatus_to_res, Error, PRErrorCode, Res}; +use crate::ext::{ExtensionHandler, ExtensionTracker}; +use crate::p11::{self, PrivateKey, PublicKey}; +use crate::prio; +use crate::replay::AntiReplay; +use crate::secrets::SecretHolder; +use crate::ssl::{self, PRBool}; +use crate::time::{Time, TimeHolder}; + +use neqo_common::{hex_snip_middle, hex_with_len, qdebug, qinfo, qtrace, qwarn}; +use std::cell::RefCell; +use std::convert::TryFrom; +use std::ffi::{CStr, CString}; +use std::mem::{self, MaybeUninit}; +use std::ops::{Deref, DerefMut}; +use std::os::raw::{c_uint, c_void}; +use std::pin::Pin; +use std::ptr::{null, null_mut}; +use std::rc::Rc; +use std::time::Instant; + +/// The maximum number of tickets to remember for a given connection. +const MAX_TICKETS: usize = 4; + +#[derive(Clone, Debug, PartialEq, Eq)] +pub enum HandshakeState { + New, + InProgress, + AuthenticationPending, + /// When encrypted client hello is enabled, the server might engage a fallback. + /// This is the status that is returned. The included value is the public + /// name of the server, which should be used to validated the certificate. + EchFallbackAuthenticationPending(String), + Authenticated(PRErrorCode), + Complete(SecretAgentInfo), + Failed(Error), +} + +impl HandshakeState { + #[must_use] + pub fn is_connected(&self) -> bool { + matches!(self, Self::Complete(_)) + } + + #[must_use] + pub fn is_final(&self) -> bool { + matches!(self, Self::Complete(_) | Self::Failed(_)) + } + + #[must_use] + pub fn authentication_needed(&self) -> bool { + matches!( + self, + Self::AuthenticationPending | Self::EchFallbackAuthenticationPending(_) + ) + } +} + +fn get_alpn(fd: *mut ssl::PRFileDesc, pre: bool) -> Res<Option<String>> { + let mut alpn_state = ssl::SSLNextProtoState::SSL_NEXT_PROTO_NO_SUPPORT; + let mut chosen = vec![0_u8; 255]; + let mut chosen_len: c_uint = 0; + secstatus_to_res(unsafe { + ssl::SSL_GetNextProto( + fd, + &mut alpn_state, + chosen.as_mut_ptr(), + &mut chosen_len, + c_uint::try_from(chosen.len())?, + ) + })?; + + let alpn = match (pre, alpn_state) { + (true, ssl::SSLNextProtoState::SSL_NEXT_PROTO_EARLY_VALUE) + | ( + false, + ssl::SSLNextProtoState::SSL_NEXT_PROTO_NEGOTIATED + | ssl::SSLNextProtoState::SSL_NEXT_PROTO_SELECTED, + ) => { + chosen.truncate(usize::try_from(chosen_len)?); + Some(match String::from_utf8(chosen) { + Ok(a) => a, + Err(_) => return Err(Error::InternalError), + }) + } + _ => None, + }; + qtrace!([format!("{:p}", fd)], "got ALPN {:?}", alpn); + Ok(alpn) +} + +pub struct SecretAgentPreInfo { + info: ssl::SSLPreliminaryChannelInfo, + alpn: Option<String>, +} + +macro_rules! preinfo_arg { + ($v:ident, $m:ident, $f:ident: $t:ident $(,)?) => { + #[must_use] + pub fn $v(&self) -> Option<$t> { + match self.info.valuesSet & ssl::$m { + 0 => None, + _ => Some($t::try_from(self.info.$f).unwrap()), + } + } + }; +} + +impl SecretAgentPreInfo { + fn new(fd: *mut ssl::PRFileDesc) -> Res<Self> { + let mut info: MaybeUninit<ssl::SSLPreliminaryChannelInfo> = MaybeUninit::uninit(); + secstatus_to_res(unsafe { + ssl::SSL_GetPreliminaryChannelInfo( + fd, + info.as_mut_ptr(), + c_uint::try_from(mem::size_of::<ssl::SSLPreliminaryChannelInfo>())?, + ) + })?; + + Ok(Self { + info: unsafe { info.assume_init() }, + alpn: get_alpn(fd, true)?, + }) + } + + preinfo_arg!(version, ssl_preinfo_version, protocolVersion: Version); + preinfo_arg!(cipher_suite, ssl_preinfo_cipher_suite, cipherSuite: Cipher); + preinfo_arg!( + early_data_cipher, + ssl_preinfo_0rtt_cipher_suite, + zeroRttCipherSuite: Cipher, + ); + + #[must_use] + pub fn early_data(&self) -> bool { + self.info.canSendEarlyData != 0 + } + + /// # Panics + /// If `usize` is less than 32 bits and the value is too large. + #[must_use] + pub fn max_early_data(&self) -> usize { + usize::try_from(self.info.maxEarlyDataSize).unwrap() + } + + /// Was ECH accepted. + #[must_use] + pub fn ech_accepted(&self) -> Option<bool> { + if self.info.valuesSet & ssl::ssl_preinfo_ech == 0 { + None + } else { + Some(self.info.echAccepted != 0) + } + } + + /// Get the ECH public name that was used. This will only be available + /// (that is, not `None`) if `ech_accepted()` returns `false`. + /// In this case, certificate validation needs to use this name rather + /// than the original name to validate the certificate. If + /// that validation passes (that is, `SecretAgent::authenticated` is called + /// with `AuthenticationStatus::Ok`), then the handshake will still fail. + /// After the failed handshake, the state will be `Error::EchRetry`, + /// which contains a valid ECH configuration. + /// + /// # Errors + /// When the public name is not valid UTF-8. (Note: names should be ASCII.) + pub fn ech_public_name(&self) -> Res<Option<&str>> { + if self.info.valuesSet & ssl::ssl_preinfo_ech == 0 || self.info.echPublicName.is_null() { + Ok(None) + } else { + let n = unsafe { CStr::from_ptr(self.info.echPublicName) }; + Ok(Some(n.to_str()?)) + } + } + + #[must_use] + pub fn alpn(&self) -> Option<&String> { + self.alpn.as_ref() + } +} + +#[derive(Clone, Debug, Default, PartialEq, Eq)] +pub struct SecretAgentInfo { + version: Version, + cipher: Cipher, + group: Group, + resumed: bool, + early_data: bool, + ech_accepted: bool, + alpn: Option<String>, + signature_scheme: SignatureScheme, +} + +impl SecretAgentInfo { + fn new(fd: *mut ssl::PRFileDesc) -> Res<Self> { + let mut info: MaybeUninit<ssl::SSLChannelInfo> = MaybeUninit::uninit(); + secstatus_to_res(unsafe { + ssl::SSL_GetChannelInfo( + fd, + info.as_mut_ptr(), + c_uint::try_from(mem::size_of::<ssl::SSLChannelInfo>())?, + ) + })?; + let info = unsafe { info.assume_init() }; + Ok(Self { + version: info.protocolVersion, + cipher: info.cipherSuite, + group: Group::try_from(info.keaGroup)?, + resumed: info.resumed != 0, + early_data: info.earlyDataAccepted != 0, + ech_accepted: info.echAccepted != 0, + alpn: get_alpn(fd, false)?, + signature_scheme: SignatureScheme::try_from(info.signatureScheme)?, + }) + } + #[must_use] + pub fn version(&self) -> Version { + self.version + } + #[must_use] + pub fn cipher_suite(&self) -> Cipher { + self.cipher + } + #[must_use] + pub fn key_exchange(&self) -> Group { + self.group + } + #[must_use] + pub fn resumed(&self) -> bool { + self.resumed + } + #[must_use] + pub fn early_data_accepted(&self) -> bool { + self.early_data + } + #[must_use] + pub fn ech_accepted(&self) -> bool { + self.ech_accepted + } + #[must_use] + pub fn alpn(&self) -> Option<&String> { + self.alpn.as_ref() + } + #[must_use] + pub fn signature_scheme(&self) -> SignatureScheme { + self.signature_scheme + } +} + +/// `SecretAgent` holds the common parts of client and server. +#[derive(Debug)] +#[allow(clippy::module_name_repetitions)] +pub struct SecretAgent { + fd: *mut ssl::PRFileDesc, + secrets: SecretHolder, + raw: Option<bool>, + io: Pin<Box<AgentIo>>, + state: HandshakeState, + + /// Records whether authentication of certificates is required. + auth_required: Pin<Box<bool>>, + /// Records any fatal alert that is sent by the stack. + alert: Pin<Box<Option<Alert>>>, + /// The current time. + now: TimeHolder, + + extension_handlers: Vec<ExtensionTracker>, + + /// The encrypted client hello (ECH) configuration that is in use. + /// Empty if ECH is not enabled. + ech_config: Vec<u8>, +} + +impl SecretAgent { + fn new() -> Res<Self> { + let mut io = Box::pin(AgentIo::new()); + let fd = Self::create_fd(&mut io)?; + Ok(Self { + fd, + secrets: SecretHolder::default(), + raw: None, + io, + state: HandshakeState::New, + + auth_required: Box::pin(false), + alert: Box::pin(None), + now: TimeHolder::default(), + + extension_handlers: Vec::new(), + + ech_config: Vec::new(), + }) + } + + // Create a new SSL file descriptor. + // + // Note that we create separate bindings for PRFileDesc as both + // ssl::PRFileDesc and prio::PRFileDesc. This keeps the bindings + // minimal, but it means that the two forms need casts to translate + // between them. ssl::PRFileDesc is left as an opaque type, as the + // ssl::SSL_* APIs only need an opaque type. + fn create_fd(io: &mut Pin<Box<AgentIo>>) -> Res<*mut ssl::PRFileDesc> { + assert_initialized(); + let label = CString::new("sslwrapper")?; + let id = unsafe { prio::PR_GetUniqueIdentity(label.as_ptr()) }; + + let base_fd = unsafe { prio::PR_CreateIOLayerStub(id, METHODS) }; + if base_fd.is_null() { + return Err(Error::CreateSslSocket); + } + let fd = unsafe { + (*base_fd).secret = as_c_void(io).cast(); + ssl::SSL_ImportFD(null_mut(), base_fd.cast()) + }; + if fd.is_null() { + unsafe { prio::PR_Close(base_fd) }; + return Err(Error::CreateSslSocket); + } + Ok(fd) + } + + unsafe extern "C" fn auth_complete_hook( + arg: *mut c_void, + _fd: *mut ssl::PRFileDesc, + _check_sig: ssl::PRBool, + _is_server: ssl::PRBool, + ) -> ssl::SECStatus { + let auth_required_ptr = arg.cast::<bool>(); + *auth_required_ptr = true; + // NSS insists on getting SECWouldBlock here rather than accepting + // the usual combination of PR_WOULD_BLOCK_ERROR and SECFailure. + ssl::_SECStatus_SECWouldBlock + } + + unsafe extern "C" fn alert_sent_cb( + fd: *const ssl::PRFileDesc, + arg: *mut c_void, + alert: *const ssl::SSLAlert, + ) { + let alert = alert.as_ref().unwrap(); + if alert.level == 2 { + // Fatal alerts demand attention. + let st = arg.cast::<Option<Alert>>().as_mut().unwrap(); + if st.is_none() { + *st = Some(alert.description); + } else { + qwarn!( + [format!("{:p}", fd)], + "duplicate alert {}", + alert.description + ); + } + } + } + + // Ready this for connecting. + fn ready(&mut self, is_server: bool) -> Res<()> { + secstatus_to_res(unsafe { + ssl::SSL_AuthCertificateHook( + self.fd, + Some(Self::auth_complete_hook), + as_c_void(&mut self.auth_required), + ) + })?; + + secstatus_to_res(unsafe { + ssl::SSL_AlertSentCallback( + self.fd, + Some(Self::alert_sent_cb), + as_c_void(&mut self.alert), + ) + })?; + + self.now.bind(self.fd)?; + self.configure()?; + secstatus_to_res(unsafe { ssl::SSL_ResetHandshake(self.fd, ssl::PRBool::from(is_server)) }) + } + + /// Default configuration. + /// + /// # Errors + /// If `set_version_range` fails. + fn configure(&mut self) -> Res<()> { + self.set_version_range(TLS_VERSION_1_3, TLS_VERSION_1_3)?; + self.set_option(ssl::Opt::Locking, false)?; + self.set_option(ssl::Opt::Tickets, false)?; + self.set_option(ssl::Opt::OcspStapling, true)?; + Ok(()) + } + + /// Set the versions that are supported. + /// + /// # Errors + /// If the range of versions isn't supported. + pub fn set_version_range(&mut self, min: Version, max: Version) -> Res<()> { + let range = ssl::SSLVersionRange { min, max }; + secstatus_to_res(unsafe { ssl::SSL_VersionRangeSet(self.fd, &range) }) + } + + /// Enable a set of ciphers. Note that the order of these is not respected. + /// + /// # Errors + /// If NSS can't enable or disable ciphers. + pub fn set_ciphers(&mut self, ciphers: &[Cipher]) -> Res<()> { + if self.state != HandshakeState::New { + qwarn!([self], "Cannot enable ciphers in state {:?}", self.state); + return Err(Error::InternalError); + } + + let all_ciphers = unsafe { ssl::SSL_GetImplementedCiphers() }; + let cipher_count = usize::from(unsafe { ssl::SSL_GetNumImplementedCiphers() }); + for i in 0..cipher_count { + let p = all_ciphers.wrapping_add(i); + secstatus_to_res(unsafe { + ssl::SSL_CipherPrefSet(self.fd, i32::from(*p), ssl::PRBool::from(false)) + })?; + } + + for c in ciphers { + secstatus_to_res(unsafe { + ssl::SSL_CipherPrefSet(self.fd, i32::from(*c), ssl::PRBool::from(true)) + })?; + } + Ok(()) + } + + /// Set key exchange groups. + /// + /// # Errors + /// If the underlying API fails (which shouldn't happen). + pub fn set_groups(&mut self, groups: &[Group]) -> Res<()> { + // SSLNamedGroup is a different size to Group, so copy one by one. + let group_vec: Vec<_> = groups + .iter() + .map(|&g| ssl::SSLNamedGroup::Type::from(g)) + .collect(); + + let ptr = group_vec.as_slice().as_ptr(); + secstatus_to_res(unsafe { + ssl::SSL_NamedGroupConfig(self.fd, ptr, c_uint::try_from(group_vec.len())?) + }) + } + + /// Set TLS options. + /// + /// # Errors + /// Returns an error if the option or option value is invalid; i.e., never. + pub fn set_option(&mut self, opt: ssl::Opt, value: bool) -> Res<()> { + opt.set(self.fd, value) + } + + /// Enable 0-RTT. + /// + /// # Errors + /// See `set_option`. + pub fn enable_0rtt(&mut self) -> Res<()> { + self.set_option(ssl::Opt::EarlyData, true) + } + + /// Disable the `EndOfEarlyData` message. + /// + /// # Errors + /// See `set_option`. + pub fn disable_end_of_early_data(&mut self) -> Res<()> { + self.set_option(ssl::Opt::SuppressEndOfEarlyData, true) + } + + /// `set_alpn` sets a list of preferred protocols, starting with the most preferred. + /// Though ALPN [RFC7301] permits octet sequences, this only allows for UTF-8-encoded + /// strings. + /// + /// This asserts if no items are provided, or if any individual item is longer than + /// 255 octets in length. + /// + /// # Errors + /// This should always panic rather than return an error. + /// # Panics + /// If any of the provided `protocols` are more than 255 bytes long. + /// + /// [RFC7301]: https://datatracker.ietf.org/doc/html/rfc7301 + pub fn set_alpn(&mut self, protocols: &[impl AsRef<str>]) -> Res<()> { + // Validate and set length. + let mut encoded_len = protocols.len(); + for v in protocols { + assert!(v.as_ref().len() < 256); + assert!(!v.as_ref().is_empty()); + encoded_len += v.as_ref().len(); + } + + // Prepare to encode. + let mut encoded = Vec::with_capacity(encoded_len); + let mut add = |v: &str| { + if let Ok(s) = u8::try_from(v.len()) { + encoded.push(s); + encoded.extend_from_slice(v.as_bytes()); + } + }; + + // NSS inherited an idiosyncratic API as a result of having implemented NPN + // before ALPN. For that reason, we need to put the "best" option last. + let (first, rest) = protocols + .split_first() + .expect("at least one ALPN value needed"); + for v in rest { + add(v.as_ref()); + } + add(first.as_ref()); + assert_eq!(encoded_len, encoded.len()); + + // Now give the result to NSS. + secstatus_to_res(unsafe { + ssl::SSL_SetNextProtoNego( + self.fd, + encoded.as_slice().as_ptr(), + c_uint::try_from(encoded.len())?, + ) + }) + } + + /// Install an extension handler. + /// + /// This can be called multiple times with different values for `ext`. The handler is provided as + /// Rc<RefCell<>> so that the caller is able to hold a reference to the handler and later access any + /// state that it accumulates. + /// + /// # Errors + /// When the extension handler can't be successfully installed. + pub fn extension_handler( + &mut self, + ext: Extension, + handler: Rc<RefCell<dyn ExtensionHandler>>, + ) -> Res<()> { + let tracker = unsafe { ExtensionTracker::new(self.fd, ext, handler) }?; + self.extension_handlers.push(tracker); + Ok(()) + } + + // This function tracks whether handshake() or handshake_raw() was used + // and prevents the other from being used. + fn set_raw(&mut self, r: bool) -> Res<()> { + if self.raw.is_none() { + self.secrets.register(self.fd)?; + self.raw = Some(r); + Ok(()) + } else if self.raw.unwrap() == r { + Ok(()) + } else { + Err(Error::MixedHandshakeMethod) + } + } + + /// Get information about the connection. + /// This includes the version, ciphersuite, and ALPN. + /// + /// Calling this function returns None until the connection is complete. + #[must_use] + pub fn info(&self) -> Option<&SecretAgentInfo> { + match self.state { + HandshakeState::Complete(ref info) => Some(info), + _ => None, + } + } + + /// Get any preliminary information about the status of the connection. + /// + /// This includes whether 0-RTT was accepted and any information related to that. + /// Calling this function collects all the relevant information. + /// + /// # Errors + /// When the underlying socket functions fail. + pub fn preinfo(&self) -> Res<SecretAgentPreInfo> { + SecretAgentPreInfo::new(self.fd) + } + + /// Get the peer's certificate chain. + #[must_use] + pub fn peer_certificate(&self) -> Option<CertificateInfo> { + CertificateInfo::new(self.fd) + } + + /// Return any fatal alert that the TLS stack might have sent. + #[must_use] + pub fn alert(&self) -> Option<&Alert> { + (*self.alert).as_ref() + } + + /// Call this function to mark the peer as authenticated. + /// # Panics + /// If the handshake doesn't need to be authenticated. + pub fn authenticated(&mut self, status: AuthenticationStatus) { + assert!(self.state.authentication_needed()); + *self.auth_required = false; + self.state = HandshakeState::Authenticated(status.into()); + } + + fn capture_error<T>(&mut self, res: Res<T>) -> Res<T> { + if let Err(e) = res { + let e = ech::convert_ech_error(self.fd, e); + qwarn!([self], "error: {:?}", e); + self.state = HandshakeState::Failed(e.clone()); + Err(e) + } else { + res + } + } + + fn update_state(&mut self, res: Res<()>) -> Res<()> { + self.state = if is_blocked(&res) { + if *self.auth_required { + self.preinfo()?.ech_public_name()?.map_or( + HandshakeState::AuthenticationPending, + |public_name| { + HandshakeState::EchFallbackAuthenticationPending(public_name.to_owned()) + }, + ) + } else { + HandshakeState::InProgress + } + } else { + self.capture_error(res)?; + let info = self.capture_error(SecretAgentInfo::new(self.fd))?; + HandshakeState::Complete(info) + }; + qinfo!([self], "state -> {:?}", self.state); + Ok(()) + } + + /// Drive the TLS handshake, taking bytes from `input` and putting + /// any bytes necessary into `output`. + /// This takes the current time as `now`. + /// On success a tuple of a `HandshakeState` and usize indicate whether the handshake + /// is complete and how many bytes were written to `output`, respectively. + /// If the state is `HandshakeState::AuthenticationPending`, then ONLY call this + /// function if you want to proceed, because this will mark the certificate as OK. + /// + /// # Errors + /// When the handshake fails this returns an error. + pub fn handshake(&mut self, now: Instant, input: &[u8]) -> Res<Vec<u8>> { + self.now.set(now)?; + self.set_raw(false)?; + + let rv = { + // Within this scope, _h maintains a mutable reference to self.io. + let _h = self.io.wrap(input); + match self.state { + HandshakeState::Authenticated(ref err) => unsafe { + ssl::SSL_AuthCertificateComplete(self.fd, *err) + }, + _ => unsafe { ssl::SSL_ForceHandshake(self.fd) }, + } + }; + // Take before updating state so that we leave the output buffer empty + // even if there is an error. + let output = self.io.take_output(); + self.update_state(secstatus_to_res(rv))?; + Ok(output) + } + + /// Setup to receive records for raw handshake functions. + fn setup_raw(&mut self) -> Res<Pin<Box<RecordList>>> { + self.set_raw(true)?; + self.capture_error(RecordList::setup(self.fd)) + } + + /// Drive the TLS handshake, but get the raw content of records, not + /// protected records as bytes. This function is incompatible with + /// `handshake()`; use either this or `handshake()` exclusively. + /// + /// Ideally, this only includes records from the current epoch. + /// If you send data from multiple epochs, you might end up being sad. + /// + /// # Errors + /// When the handshake fails this returns an error. + pub fn handshake_raw(&mut self, now: Instant, input: Option<Record>) -> Res<RecordList> { + self.now.set(now)?; + let records = self.setup_raw()?; + + // Fire off any authentication we might need to complete. + if let HandshakeState::Authenticated(ref err) = self.state { + let result = + secstatus_to_res(unsafe { ssl::SSL_AuthCertificateComplete(self.fd, *err) }); + qdebug!([self], "SSL_AuthCertificateComplete: {:?}", result); + // This should return SECSuccess, so don't use update_state(). + self.capture_error(result)?; + } + + // Feed in any records. + if let Some(rec) = input { + self.capture_error(rec.write(self.fd))?; + } + + // Drive the handshake once more. + let rv = secstatus_to_res(unsafe { ssl::SSL_ForceHandshake(self.fd) }); + self.update_state(rv)?; + + Ok(*Pin::into_inner(records)) + } + + #[allow(unknown_lints, clippy::branches_sharing_code)] + pub fn close(&mut self) { + // It should be safe to close multiple times. + if self.fd.is_null() { + return; + } + if let Some(true) = self.raw { + // Need to hold the record list in scope until the close is done. + let _records = self.setup_raw().expect("Can only close"); + unsafe { prio::PR_Close(self.fd.cast()) }; + } else { + // Need to hold the IO wrapper in scope until the close is done. + let _io = self.io.wrap(&[]); + unsafe { prio::PR_Close(self.fd.cast()) }; + }; + let _output = self.io.take_output(); + self.fd = null_mut(); + } + + /// State returns the status of the handshake. + #[must_use] + pub fn state(&self) -> &HandshakeState { + &self.state + } + + /// Take a read secret. This will only return a non-`None` value once. + #[must_use] + pub fn read_secret(&mut self, epoch: Epoch) -> Option<p11::SymKey> { + self.secrets.take_read(epoch) + } + + /// Take a write secret. + #[must_use] + pub fn write_secret(&mut self, epoch: Epoch) -> Option<p11::SymKey> { + self.secrets.take_write(epoch) + } + + /// Get the active ECH configuration, which is empty if ECH is disabled. + #[must_use] + pub fn ech_config(&self) -> &[u8] { + &self.ech_config + } +} + +impl Drop for SecretAgent { + fn drop(&mut self) { + self.close(); + } +} + +impl ::std::fmt::Display for SecretAgent { + fn fmt(&self, f: &mut ::std::fmt::Formatter) -> ::std::fmt::Result { + write!(f, "Agent {:p}", self.fd) + } +} + +#[derive(Debug, PartialOrd, Ord, PartialEq, Eq, Clone)] +pub struct ResumptionToken { + token: Vec<u8>, + expiration_time: Instant, +} + +impl AsRef<[u8]> for ResumptionToken { + fn as_ref(&self) -> &[u8] { + &self.token + } +} + +impl ResumptionToken { + #[must_use] + pub fn new(token: Vec<u8>, expiration_time: Instant) -> Self { + Self { + token, + expiration_time, + } + } + + #[must_use] + pub fn expiration_time(&self) -> Instant { + self.expiration_time + } +} + +/// A TLS Client. +#[derive(Debug)] +#[allow( + renamed_and_removed_lints, + clippy::box_vec, + unknown_lints, + clippy::box_collection +)] // We need the Box. +pub struct Client { + agent: SecretAgent, + + /// The name of the server we're attempting a connection to. + server_name: String, + /// Records the resumption tokens we've received. + resumption: Pin<Box<Vec<ResumptionToken>>>, +} + +impl Client { + /// Create a new client agent. + /// + /// # Errors + /// Errors returned if the socket can't be created or configured. + pub fn new(server_name: impl Into<String>) -> Res<Self> { + let server_name = server_name.into(); + let mut agent = SecretAgent::new()?; + let url = CString::new(server_name.as_bytes())?; + secstatus_to_res(unsafe { ssl::SSL_SetURL(agent.fd, url.as_ptr()) })?; + agent.ready(false)?; + let mut client = Self { + agent, + server_name, + resumption: Box::pin(Vec::new()), + }; + client.ready()?; + Ok(client) + } + + unsafe extern "C" fn resumption_token_cb( + fd: *mut ssl::PRFileDesc, + token: *const u8, + len: c_uint, + arg: *mut c_void, + ) -> ssl::SECStatus { + let mut info: MaybeUninit<ssl::SSLResumptionTokenInfo> = MaybeUninit::uninit(); + if ssl::SSL_GetResumptionTokenInfo( + token, + len, + info.as_mut_ptr(), + c_uint::try_from(mem::size_of::<ssl::SSLResumptionTokenInfo>()).unwrap(), + ) + .is_err() + { + // Ignore the token. + return ssl::SECSuccess; + } + let expiration_time = info.assume_init().expirationTime; + if ssl::SSL_DestroyResumptionTokenInfo(info.as_mut_ptr()).is_err() { + // Ignore the token. + return ssl::SECSuccess; + } + let resumption = arg.cast::<Vec<ResumptionToken>>().as_mut().unwrap(); + let len = usize::try_from(len).unwrap(); + let mut v = Vec::with_capacity(len); + v.extend_from_slice(std::slice::from_raw_parts(token, len)); + qinfo!( + [format!("{:p}", fd)], + "Got resumption token {}", + hex_snip_middle(&v) + ); + + if resumption.len() >= MAX_TICKETS { + resumption.remove(0); + } + if let Ok(t) = Time::try_from(expiration_time) { + resumption.push(ResumptionToken::new(v, *t)); + } + ssl::SECSuccess + } + + #[must_use] + pub fn server_name(&self) -> &str { + &self.server_name + } + + fn ready(&mut self) -> Res<()> { + let fd = self.fd; + unsafe { + ssl::SSL_SetResumptionTokenCallback( + fd, + Some(Self::resumption_token_cb), + as_c_void(&mut self.resumption), + ) + } + } + + /// Take a resumption token. + #[must_use] + pub fn resumption_token(&mut self) -> Option<ResumptionToken> { + (*self.resumption).pop() + } + + /// Check if there are more resumption tokens. + #[must_use] + pub fn has_resumption_token(&self) -> bool { + !(*self.resumption).is_empty() + } + + /// Enable resumption, using a token previously provided. + /// + /// # Errors + /// Error returned when the resumption token is invalid or + /// the socket is not able to use the value. + pub fn enable_resumption(&mut self, token: impl AsRef<[u8]>) -> Res<()> { + unsafe { + ssl::SSL_SetResumptionToken( + self.agent.fd, + token.as_ref().as_ptr(), + c_uint::try_from(token.as_ref().len())?, + ) + } + } + + /// Enable encrypted client hello (ECH), using the encoded `ECHConfigList`. + /// + /// When ECH is enabled, a client needs to look for `Error::EchRetry` as a + /// failure code. If `Error::EchRetry` is received when connecting, the + /// connection attempt should be retried and the included value provided + /// to this function (instead of what is received from DNS). + /// + /// Calling this function with an empty value for `ech_config_list` enables + /// ECH greasing. When that is done, there is no need to look for `EchRetry` + /// + /// # Errors + /// Error returned when the configuration is invalid. + pub fn enable_ech(&mut self, ech_config_list: impl AsRef<[u8]>) -> Res<()> { + let config = ech_config_list.as_ref(); + qdebug!([self], "Enable ECH for a server: {}", hex_with_len(config)); + self.ech_config = Vec::from(config); + if config.is_empty() { + unsafe { ech::SSL_EnableTls13GreaseEch(self.agent.fd, PRBool::from(true)) } + } else { + unsafe { + ech::SSL_SetClientEchConfigs( + self.agent.fd, + config.as_ptr(), + c_uint::try_from(config.len())?, + ) + } + } + } +} + +impl Deref for Client { + type Target = SecretAgent; + #[must_use] + fn deref(&self) -> &SecretAgent { + &self.agent + } +} + +impl DerefMut for Client { + fn deref_mut(&mut self) -> &mut SecretAgent { + &mut self.agent + } +} + +impl ::std::fmt::Display for Client { + fn fmt(&self, f: &mut ::std::fmt::Formatter) -> ::std::fmt::Result { + write!(f, "Client {:p}", self.agent.fd) + } +} + +/// `ZeroRttCheckResult` encapsulates the options for handling a `ClientHello`. +#[derive(Clone, Debug, PartialEq, Eq)] +pub enum ZeroRttCheckResult { + /// Accept 0-RTT. + Accept, + /// Reject 0-RTT, but continue the handshake normally. + Reject, + /// Send HelloRetryRequest (probably not needed for QUIC). + HelloRetryRequest(Vec<u8>), + /// Fail the handshake. + Fail, +} + +/// A `ZeroRttChecker` is used by the agent to validate the application token (as provided by `send_ticket`) +pub trait ZeroRttChecker: std::fmt::Debug + std::marker::Unpin { + fn check(&self, token: &[u8]) -> ZeroRttCheckResult; +} + +/// Using `AllowZeroRtt` for the implementation of `ZeroRttChecker` means +/// accepting 0-RTT always. This generally isn't a great idea, so this +/// generates a strong warning when it is used. +#[derive(Debug)] +pub struct AllowZeroRtt {} +impl ZeroRttChecker for AllowZeroRtt { + fn check(&self, _token: &[u8]) -> ZeroRttCheckResult { + qwarn!("AllowZeroRtt accepting 0-RTT"); + ZeroRttCheckResult::Accept + } +} + +#[derive(Debug)] +struct ZeroRttCheckState { + checker: Pin<Box<dyn ZeroRttChecker>>, +} + +impl ZeroRttCheckState { + pub fn new(checker: Box<dyn ZeroRttChecker>) -> Self { + Self { + checker: Pin::new(checker), + } + } +} + +#[derive(Debug)] +pub struct Server { + agent: SecretAgent, + /// This holds the HRR callback context. + zero_rtt_check: Option<Pin<Box<ZeroRttCheckState>>>, +} + +impl Server { + /// Create a new server agent. + /// + /// # Errors + /// Errors returned when NSS fails. + pub fn new(certificates: &[impl AsRef<str>]) -> Res<Self> { + let mut agent = SecretAgent::new()?; + + for n in certificates { + let c = CString::new(n.as_ref())?; + let cert_ptr = unsafe { p11::PK11_FindCertFromNickname(c.as_ptr(), null_mut()) }; + let cert = if let Ok(c) = p11::Certificate::from_ptr(cert_ptr) { + c + } else { + return Err(Error::CertificateLoading); + }; + let key_ptr = unsafe { p11::PK11_FindKeyByAnyCert(*cert.deref(), null_mut()) }; + let key = if let Ok(k) = p11::PrivateKey::from_ptr(key_ptr) { + k + } else { + return Err(Error::CertificateLoading); + }; + secstatus_to_res(unsafe { + ssl::SSL_ConfigServerCert(agent.fd, *cert.deref(), *key.deref(), null(), 0) + })?; + } + + agent.ready(true)?; + Ok(Self { + agent, + zero_rtt_check: None, + }) + } + + unsafe extern "C" fn hello_retry_cb( + first_hello: PRBool, + client_token: *const u8, + client_token_len: c_uint, + retry_token: *mut u8, + retry_token_len: *mut c_uint, + retry_token_max: c_uint, + arg: *mut c_void, + ) -> ssl::SSLHelloRetryRequestAction::Type { + if first_hello == 0 { + // On the second ClientHello after HelloRetryRequest, skip checks. + return ssl::SSLHelloRetryRequestAction::ssl_hello_retry_accept; + } + + let check_state = arg.cast::<ZeroRttCheckState>().as_mut().unwrap(); + let token = if client_token.is_null() { + &[] + } else { + std::slice::from_raw_parts(client_token, usize::try_from(client_token_len).unwrap()) + }; + match check_state.checker.check(token) { + ZeroRttCheckResult::Accept => ssl::SSLHelloRetryRequestAction::ssl_hello_retry_accept, + ZeroRttCheckResult::Fail => ssl::SSLHelloRetryRequestAction::ssl_hello_retry_fail, + ZeroRttCheckResult::Reject => { + ssl::SSLHelloRetryRequestAction::ssl_hello_retry_reject_0rtt + } + ZeroRttCheckResult::HelloRetryRequest(tok) => { + // Don't bother propagating errors from this, because it should be caught in testing. + assert!(tok.len() <= usize::try_from(retry_token_max).unwrap()); + let slc = std::slice::from_raw_parts_mut(retry_token, tok.len()); + slc.copy_from_slice(&tok); + *retry_token_len = c_uint::try_from(tok.len()).unwrap(); + ssl::SSLHelloRetryRequestAction::ssl_hello_retry_request + } + } + } + + /// Enable 0-RTT. This shadows the function of the same name that can be accessed + /// via the Deref implementation on Server. + /// + /// # Errors + /// Returns an error if the underlying NSS functions fail. + pub fn enable_0rtt( + &mut self, + anti_replay: &AntiReplay, + max_early_data: u32, + checker: Box<dyn ZeroRttChecker>, + ) -> Res<()> { + let mut check_state = Box::pin(ZeroRttCheckState::new(checker)); + unsafe { + ssl::SSL_HelloRetryRequestCallback( + self.agent.fd, + Some(Self::hello_retry_cb), + as_c_void(&mut check_state), + ) + }?; + unsafe { ssl::SSL_SetMaxEarlyDataSize(self.agent.fd, max_early_data) }?; + self.zero_rtt_check = Some(check_state); + self.agent.enable_0rtt()?; + anti_replay.config_socket(self.fd)?; + Ok(()) + } + + /// Send a session ticket to the client. + /// This adds |extra| application-specific content into that ticket. + /// The records that are sent are captured and returned. + /// + /// # Errors + /// If NSS is unable to send a ticket, or if this agent is incorrectly configured. + pub fn send_ticket(&mut self, now: Instant, extra: &[u8]) -> Res<RecordList> { + self.agent.now.set(now)?; + let records = self.setup_raw()?; + + unsafe { + ssl::SSL_SendSessionTicket(self.fd, extra.as_ptr(), c_uint::try_from(extra.len())?) + }?; + + Ok(*Pin::into_inner(records)) + } + + /// Enable encrypted client hello (ECH). + /// + /// # Errors + /// Fails when NSS cannot create a key pair. + pub fn enable_ech( + &mut self, + config: u8, + public_name: &str, + sk: &PrivateKey, + pk: &PublicKey, + ) -> Res<()> { + let cfg = ech::encode_config(config, public_name, pk)?; + qdebug!([self], "Enable ECH for a server: {}", hex_with_len(&cfg)); + unsafe { + ech::SSL_SetServerEchConfigs( + self.agent.fd, + **pk, + **sk, + cfg.as_ptr(), + c_uint::try_from(cfg.len())?, + )?; + }; + self.ech_config = cfg; + Ok(()) + } +} + +impl Deref for Server { + type Target = SecretAgent; + #[must_use] + fn deref(&self) -> &SecretAgent { + &self.agent + } +} + +impl DerefMut for Server { + fn deref_mut(&mut self) -> &mut SecretAgent { + &mut self.agent + } +} + +impl ::std::fmt::Display for Server { + fn fmt(&self, f: &mut ::std::fmt::Formatter) -> ::std::fmt::Result { + write!(f, "Server {:p}", self.agent.fd) + } +} + +/// A generic container for Client or Server. +#[derive(Debug)] +pub enum Agent { + Client(crate::agent::Client), + Server(crate::agent::Server), +} + +impl Deref for Agent { + type Target = SecretAgent; + #[must_use] + fn deref(&self) -> &SecretAgent { + match self { + Self::Client(c) => c, + Self::Server(s) => s, + } + } +} + +impl DerefMut for Agent { + fn deref_mut(&mut self) -> &mut SecretAgent { + match self { + Self::Client(c) => c, + Self::Server(s) => s, + } + } +} + +impl From<Client> for Agent { + #[must_use] + fn from(c: Client) -> Self { + Self::Client(c) + } +} + +impl From<Server> for Agent { + #[must_use] + fn from(s: Server) -> Self { + Self::Server(s) + } +} diff --git a/third_party/rust/neqo-crypto/src/agentio.rs b/third_party/rust/neqo-crypto/src/agentio.rs new file mode 100644 index 0000000000..1d39b2398a --- /dev/null +++ b/third_party/rust/neqo-crypto/src/agentio.rs @@ -0,0 +1,393 @@ +// Licensed under the Apache License, Version 2.0 <LICENSE-APACHE or +// http://www.apache.org/licenses/LICENSE-2.0> or the MIT license +// <LICENSE-MIT or http://opensource.org/licenses/MIT>, at your +// option. This file may not be copied, modified, or distributed +// except according to those terms. + +use crate::constants::{ContentType, Epoch}; +use crate::err::{nspr, Error, PR_SetError, Res}; +use crate::prio; +use crate::ssl; + +use neqo_common::{hex, hex_with_len, qtrace}; +use std::cmp::min; +use std::convert::{TryFrom, TryInto}; +use std::fmt; +use std::mem; +use std::ops::Deref; +use std::os::raw::{c_uint, c_void}; +use std::pin::Pin; +use std::ptr::{null, null_mut}; +use std::vec::Vec; + +// Alias common types. +type PrFd = *mut prio::PRFileDesc; +type PrStatus = prio::PRStatus::Type; +const PR_SUCCESS: PrStatus = prio::PRStatus::PR_SUCCESS; +const PR_FAILURE: PrStatus = prio::PRStatus::PR_FAILURE; + +/// Convert a pinned, boxed object into a void pointer. +pub fn as_c_void<T: Unpin>(pin: &mut Pin<Box<T>>) -> *mut c_void { + (Pin::into_inner(pin.as_mut()) as *mut T).cast() +} + +/// A slice of the output. +#[derive(Default)] +pub struct Record { + pub epoch: Epoch, + pub ct: ContentType, + pub data: Vec<u8>, +} + +impl Record { + #[must_use] + pub fn new(epoch: Epoch, ct: ContentType, data: &[u8]) -> Self { + Self { + epoch, + ct, + data: data.to_vec(), + } + } + + // Shoves this record into the socket, returns true if blocked. + pub(crate) fn write(self, fd: *mut ssl::PRFileDesc) -> Res<()> { + qtrace!("write {:?}", self); + unsafe { + ssl::SSL_RecordLayerData( + fd, + self.epoch, + ssl::SSLContentType::Type::from(self.ct), + self.data.as_ptr(), + c_uint::try_from(self.data.len())?, + ) + } + } +} + +impl fmt::Debug for Record { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + write!( + f, + "Record {:?}:{:?} {}", + self.epoch, + self.ct, + hex_with_len(&self.data[..]) + ) + } +} + +#[derive(Debug, Default)] +pub struct RecordList { + records: Vec<Record>, +} + +impl RecordList { + fn append(&mut self, epoch: Epoch, ct: ContentType, data: &[u8]) { + self.records.push(Record::new(epoch, ct, data)); + } + + #[allow(clippy::unused_self)] + unsafe extern "C" fn ingest( + _fd: *mut ssl::PRFileDesc, + epoch: ssl::PRUint16, + ct: ssl::SSLContentType::Type, + data: *const ssl::PRUint8, + len: c_uint, + arg: *mut c_void, + ) -> ssl::SECStatus { + let records = arg.cast::<Self>().as_mut().unwrap(); + + let slice = std::slice::from_raw_parts(data, len as usize); + records.append(epoch, ContentType::try_from(ct).unwrap(), slice); + ssl::SECSuccess + } + + /// Create a new record list. + pub(crate) fn setup(fd: *mut ssl::PRFileDesc) -> Res<Pin<Box<Self>>> { + let mut records = Box::pin(Self::default()); + unsafe { + ssl::SSL_RecordLayerWriteCallback(fd, Some(Self::ingest), as_c_void(&mut records)) + }?; + Ok(records) + } +} + +impl Deref for RecordList { + type Target = Vec<Record>; + #[must_use] + fn deref(&self) -> &Vec<Record> { + &self.records + } +} + +pub struct RecordListIter(std::vec::IntoIter<Record>); + +impl Iterator for RecordListIter { + type Item = Record; + fn next(&mut self) -> Option<Self::Item> { + self.0.next() + } +} + +impl IntoIterator for RecordList { + type Item = Record; + type IntoIter = RecordListIter; + #[must_use] + fn into_iter(self) -> Self::IntoIter { + RecordListIter(self.records.into_iter()) + } +} + +pub struct AgentIoInputContext<'a> { + input: &'a mut AgentIoInput, +} + +impl<'a> Drop for AgentIoInputContext<'a> { + fn drop(&mut self) { + self.input.reset(); + } +} + +#[derive(Debug)] +struct AgentIoInput { + // input is data that is read by TLS. + input: *const u8, + // input_available is how much data is left for reading. + available: usize, +} + +impl AgentIoInput { + fn wrap<'a: 'c, 'b: 'c, 'c>(&'a mut self, input: &'b [u8]) -> AgentIoInputContext<'c> { + assert!(self.input.is_null()); + self.input = input.as_ptr(); + self.available = input.len(); + qtrace!("AgentIoInput wrap {:p}", self.input); + AgentIoInputContext { input: self } + } + + // Take the data provided as input and provide it to the TLS stack. + fn read_input(&mut self, buf: *mut u8, count: usize) -> Res<usize> { + let amount = min(self.available, count); + if amount == 0 { + unsafe { + PR_SetError(nspr::PR_WOULD_BLOCK_ERROR, 0); + } + return Err(Error::NoDataAvailable); + } + + let src = unsafe { std::slice::from_raw_parts(self.input, amount) }; + qtrace!([self], "read {}", hex(src)); + let dst = unsafe { std::slice::from_raw_parts_mut(buf, amount) }; + dst.copy_from_slice(src); + self.input = self.input.wrapping_add(amount); + self.available -= amount; + Ok(amount) + } + + fn reset(&mut self) { + qtrace!([self], "reset"); + self.input = null(); + self.available = 0; + } +} + +impl ::std::fmt::Display for AgentIoInput { + fn fmt(&self, f: &mut ::std::fmt::Formatter) -> ::std::fmt::Result { + write!(f, "AgentIoInput {:p}", self.input) + } +} + +#[derive(Debug)] +pub struct AgentIo { + // input collects the input we might provide to TLS. + input: AgentIoInput, + + // output contains data that is written by TLS. + output: Vec<u8>, +} + +impl AgentIo { + pub fn new() -> Self { + Self { + input: AgentIoInput { + input: null(), + available: 0, + }, + output: Vec::new(), + } + } + + unsafe fn borrow(fd: &mut PrFd) -> &mut Self { + #[allow(clippy::cast_ptr_alignment)] + (**fd).secret.cast::<Self>().as_mut().unwrap() + } + + pub fn wrap<'a: 'c, 'b: 'c, 'c>(&'a mut self, input: &'b [u8]) -> AgentIoInputContext<'c> { + assert_eq!(self.output.len(), 0); + self.input.wrap(input) + } + + // Stage output from TLS into the output buffer. + fn save_output(&mut self, buf: *const u8, count: usize) { + let slice = unsafe { std::slice::from_raw_parts(buf, count) }; + qtrace!([self], "save output {}", hex(slice)); + self.output.extend_from_slice(slice); + } + + pub fn take_output(&mut self) -> Vec<u8> { + qtrace!([self], "take output"); + mem::take(&mut self.output) + } +} + +impl ::std::fmt::Display for AgentIo { + fn fmt(&self, f: &mut ::std::fmt::Formatter) -> ::std::fmt::Result { + write!(f, "AgentIo") + } +} + +unsafe extern "C" fn agent_close(fd: PrFd) -> PrStatus { + (*fd).secret = null_mut(); + if let Some(dtor) = (*fd).dtor { + dtor(fd); + } + PR_SUCCESS +} + +unsafe extern "C" fn agent_read(mut fd: PrFd, buf: *mut c_void, amount: prio::PRInt32) -> PrStatus { + let io = AgentIo::borrow(&mut fd); + if let Ok(a) = usize::try_from(amount) { + match io.input.read_input(buf.cast(), a) { + Ok(_) => PR_SUCCESS, + Err(_) => PR_FAILURE, + } + } else { + PR_FAILURE + } +} + +unsafe extern "C" fn agent_recv( + mut fd: PrFd, + buf: *mut c_void, + amount: prio::PRInt32, + flags: prio::PRIntn, + _timeout: prio::PRIntervalTime, +) -> prio::PRInt32 { + let io = AgentIo::borrow(&mut fd); + if flags != 0 { + return PR_FAILURE; + } + if let Ok(a) = usize::try_from(amount) { + match io.input.read_input(buf.cast(), a) { + Ok(v) => prio::PRInt32::try_from(v).unwrap_or(PR_FAILURE), + Err(_) => PR_FAILURE, + } + } else { + PR_FAILURE + } +} + +unsafe extern "C" fn agent_write( + mut fd: PrFd, + buf: *const c_void, + amount: prio::PRInt32, +) -> PrStatus { + let io = AgentIo::borrow(&mut fd); + if let Ok(a) = usize::try_from(amount) { + io.save_output(buf.cast(), a); + amount + } else { + PR_FAILURE + } +} + +unsafe extern "C" fn agent_send( + mut fd: PrFd, + buf: *const c_void, + amount: prio::PRInt32, + flags: prio::PRIntn, + _timeout: prio::PRIntervalTime, +) -> prio::PRInt32 { + let io = AgentIo::borrow(&mut fd); + + if flags != 0 { + return PR_FAILURE; + } + if let Ok(a) = usize::try_from(amount) { + io.save_output(buf.cast(), a); + amount + } else { + PR_FAILURE + } +} + +unsafe extern "C" fn agent_available(mut fd: PrFd) -> prio::PRInt32 { + let io = AgentIo::borrow(&mut fd); + io.input.available.try_into().unwrap_or(PR_FAILURE) +} + +unsafe extern "C" fn agent_available64(mut fd: PrFd) -> prio::PRInt64 { + let io = AgentIo::borrow(&mut fd); + io.input + .available + .try_into() + .unwrap_or_else(|_| PR_FAILURE.into()) +} + +#[allow(clippy::cast_possible_truncation)] +unsafe extern "C" fn agent_getname(_fd: PrFd, addr: *mut prio::PRNetAddr) -> PrStatus { + let a = addr.as_mut().unwrap(); + // Cast is safe because prio::PR_AF_INET is 2 + a.inet.family = prio::PR_AF_INET as prio::PRUint16; + a.inet.port = 0; + a.inet.ip = 0; + PR_SUCCESS +} + +unsafe extern "C" fn agent_getsockopt(_fd: PrFd, opt: *mut prio::PRSocketOptionData) -> PrStatus { + let o = opt.as_mut().unwrap(); + if o.option == prio::PRSockOption::PR_SockOpt_Nonblocking { + o.value.non_blocking = 1; + return PR_SUCCESS; + } + PR_FAILURE +} + +pub const METHODS: &prio::PRIOMethods = &prio::PRIOMethods { + file_type: prio::PRDescType::PR_DESC_LAYERED, + close: Some(agent_close), + read: Some(agent_read), + write: Some(agent_write), + available: Some(agent_available), + available64: Some(agent_available64), + fsync: None, + seek: None, + seek64: None, + fileInfo: None, + fileInfo64: None, + writev: None, + connect: None, + accept: None, + bind: None, + listen: None, + shutdown: None, + recv: Some(agent_recv), + send: Some(agent_send), + recvfrom: None, + sendto: None, + poll: None, + acceptread: None, + transmitfile: None, + getsockname: Some(agent_getname), + getpeername: Some(agent_getname), + reserved_fn_6: None, + reserved_fn_5: None, + getsocketoption: Some(agent_getsockopt), + setsocketoption: None, + sendfile: None, + connectcontinue: None, + reserved_fn_3: None, + reserved_fn_2: None, + reserved_fn_1: None, + reserved_fn_0: None, +}; diff --git a/third_party/rust/neqo-crypto/src/auth.rs b/third_party/rust/neqo-crypto/src/auth.rs new file mode 100644 index 0000000000..2932cdf2eb --- /dev/null +++ b/third_party/rust/neqo-crypto/src/auth.rs @@ -0,0 +1,108 @@ +// Licensed under the Apache License, Version 2.0 <LICENSE-APACHE or +// http://www.apache.org/licenses/LICENSE-2.0> or the MIT license +// <LICENSE-MIT or http://opensource.org/licenses/MIT>, at your +// option. This file may not be copied, modified, or distributed +// except according to those terms. + +use crate::err::{mozpkix, sec, ssl, PRErrorCode}; + +/// The outcome of authentication. +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub enum AuthenticationStatus { + Ok, + CaInvalid, + CaNotV3, + CertAlgorithmDisabled, + CertExpired, + CertInvalidTime, + CertIsCa, + CertKeyUsage, + CertMitm, + CertNotYetValid, + CertRevoked, + CertSelfSigned, + CertSubjectInvalid, + CertUntrusted, + CertWeakKey, + IssuerEmptyName, + IssuerExpired, + IssuerNotYetValid, + IssuerUnknown, + IssuerUntrusted, + PolicyRejection, + Unknown, +} + +impl From<AuthenticationStatus> for PRErrorCode { + #[must_use] + fn from(v: AuthenticationStatus) -> Self { + match v { + AuthenticationStatus::Ok => 0, + AuthenticationStatus::CaInvalid => sec::SEC_ERROR_CA_CERT_INVALID, + AuthenticationStatus::CaNotV3 => mozpkix::MOZILLA_PKIX_ERROR_V1_CERT_USED_AS_CA, + AuthenticationStatus::CertAlgorithmDisabled => { + sec::SEC_ERROR_CERT_SIGNATURE_ALGORITHM_DISABLED + } + AuthenticationStatus::CertExpired => sec::SEC_ERROR_EXPIRED_CERTIFICATE, + AuthenticationStatus::CertInvalidTime => sec::SEC_ERROR_INVALID_TIME, + AuthenticationStatus::CertIsCa => { + mozpkix::MOZILLA_PKIX_ERROR_CA_CERT_USED_AS_END_ENTITY + } + AuthenticationStatus::CertKeyUsage => sec::SEC_ERROR_INADEQUATE_KEY_USAGE, + AuthenticationStatus::CertMitm => mozpkix::MOZILLA_PKIX_ERROR_MITM_DETECTED, + AuthenticationStatus::CertNotYetValid => { + mozpkix::MOZILLA_PKIX_ERROR_NOT_YET_VALID_CERTIFICATE + } + AuthenticationStatus::CertRevoked => sec::SEC_ERROR_REVOKED_CERTIFICATE, + AuthenticationStatus::CertSelfSigned => mozpkix::MOZILLA_PKIX_ERROR_SELF_SIGNED_CERT, + AuthenticationStatus::CertSubjectInvalid => ssl::SSL_ERROR_BAD_CERT_DOMAIN, + AuthenticationStatus::CertUntrusted => sec::SEC_ERROR_UNTRUSTED_CERT, + AuthenticationStatus::CertWeakKey => mozpkix::MOZILLA_PKIX_ERROR_INADEQUATE_KEY_SIZE, + AuthenticationStatus::IssuerEmptyName => mozpkix::MOZILLA_PKIX_ERROR_EMPTY_ISSUER_NAME, + AuthenticationStatus::IssuerExpired => sec::SEC_ERROR_EXPIRED_ISSUER_CERTIFICATE, + AuthenticationStatus::IssuerNotYetValid => { + mozpkix::MOZILLA_PKIX_ERROR_NOT_YET_VALID_ISSUER_CERTIFICATE + } + AuthenticationStatus::IssuerUnknown => sec::SEC_ERROR_UNKNOWN_ISSUER, + AuthenticationStatus::IssuerUntrusted => sec::SEC_ERROR_UNTRUSTED_ISSUER, + AuthenticationStatus::PolicyRejection => { + mozpkix::MOZILLA_PKIX_ERROR_ADDITIONAL_POLICY_CONSTRAINT_FAILED + } + AuthenticationStatus::Unknown => sec::SEC_ERROR_LIBRARY_FAILURE, + } + } +} + +// Note that this mapping should be removed after gecko eventually learns how to +// map into the enumerated type. +impl From<PRErrorCode> for AuthenticationStatus { + #[must_use] + fn from(v: PRErrorCode) -> Self { + match v { + 0 => Self::Ok, + sec::SEC_ERROR_CA_CERT_INVALID => Self::CaInvalid, + mozpkix::MOZILLA_PKIX_ERROR_V1_CERT_USED_AS_CA => Self::CaNotV3, + sec::SEC_ERROR_CERT_SIGNATURE_ALGORITHM_DISABLED => Self::CertAlgorithmDisabled, + sec::SEC_ERROR_EXPIRED_CERTIFICATE => Self::CertExpired, + sec::SEC_ERROR_INVALID_TIME => Self::CertInvalidTime, + mozpkix::MOZILLA_PKIX_ERROR_CA_CERT_USED_AS_END_ENTITY => Self::CertIsCa, + sec::SEC_ERROR_INADEQUATE_KEY_USAGE => Self::CertKeyUsage, + mozpkix::MOZILLA_PKIX_ERROR_MITM_DETECTED => Self::CertMitm, + mozpkix::MOZILLA_PKIX_ERROR_NOT_YET_VALID_CERTIFICATE => Self::CertNotYetValid, + sec::SEC_ERROR_REVOKED_CERTIFICATE => Self::CertRevoked, + mozpkix::MOZILLA_PKIX_ERROR_SELF_SIGNED_CERT => Self::CertSelfSigned, + ssl::SSL_ERROR_BAD_CERT_DOMAIN => Self::CertSubjectInvalid, + sec::SEC_ERROR_UNTRUSTED_CERT => Self::CertUntrusted, + mozpkix::MOZILLA_PKIX_ERROR_INADEQUATE_KEY_SIZE => Self::CertWeakKey, + mozpkix::MOZILLA_PKIX_ERROR_EMPTY_ISSUER_NAME => Self::IssuerEmptyName, + sec::SEC_ERROR_EXPIRED_ISSUER_CERTIFICATE => Self::IssuerExpired, + mozpkix::MOZILLA_PKIX_ERROR_NOT_YET_VALID_ISSUER_CERTIFICATE => Self::IssuerNotYetValid, + sec::SEC_ERROR_UNKNOWN_ISSUER => Self::IssuerUnknown, + sec::SEC_ERROR_UNTRUSTED_ISSUER => Self::IssuerUntrusted, + mozpkix::MOZILLA_PKIX_ERROR_ADDITIONAL_POLICY_CONSTRAINT_FAILED => { + Self::PolicyRejection + } + _ => Self::Unknown, + } + } +} diff --git a/third_party/rust/neqo-crypto/src/cert.rs b/third_party/rust/neqo-crypto/src/cert.rs new file mode 100644 index 0000000000..d78f57c1b4 --- /dev/null +++ b/third_party/rust/neqo-crypto/src/cert.rs @@ -0,0 +1,118 @@ +// Licensed under the Apache License, Version 2.0 <LICENSE-APACHE or +// http://www.apache.org/licenses/LICENSE-2.0> or the MIT license +// <LICENSE-MIT or http://opensource.org/licenses/MIT>, at your +// option. This file may not be copied, modified, or distributed +// except according to those terms. + +use crate::err::secstatus_to_res; +use crate::p11::{CERTCertListNode, CERT_GetCertificateDer, CertList, Item, SECItem, SECItemArray}; +use crate::ssl::{ + PRFileDesc, SSL_PeerCertificateChain, SSL_PeerSignedCertTimestamps, + SSL_PeerStapledOCSPResponses, +}; +use neqo_common::qerror; + +use std::convert::TryFrom; +use std::ptr::{addr_of, NonNull}; + +use std::slice; + +pub struct CertificateInfo { + certs: CertList, + cursor: *const CERTCertListNode, + /// stapled_ocsp_responses and signed_cert_timestamp are properties + /// associated with each of the certificates. Right now, NSS only + /// reports the value for the end-entity certificate (the first). + stapled_ocsp_responses: Option<Vec<Vec<u8>>>, + signed_cert_timestamp: Option<Vec<u8>>, +} + +fn peer_certificate_chain(fd: *mut PRFileDesc) -> Option<(CertList, *const CERTCertListNode)> { + let chain = unsafe { SSL_PeerCertificateChain(fd) }; + CertList::from_ptr(chain.cast()).ok().map(|certs| { + let cursor = CertificateInfo::head(&certs); + (certs, cursor) + }) +} + +// As explained in rfc6961, an OCSPResponseList can have at most +// 2^24 items. Casting its length is therefore safe even on 32 bits targets. +fn stapled_ocsp_responses(fd: *mut PRFileDesc) -> Option<Vec<Vec<u8>>> { + let ocsp_nss = unsafe { SSL_PeerStapledOCSPResponses(fd) }; + match NonNull::new(ocsp_nss as *mut SECItemArray) { + Some(ocsp_ptr) => { + let mut ocsp_helper: Vec<Vec<u8>> = Vec::new(); + let len = if let Ok(l) = isize::try_from(unsafe { ocsp_ptr.as_ref().len }) { + l + } else { + qerror!([format!("{:p}", fd)], "Received illegal OSCP length"); + return None; + }; + for idx in 0..len { + let itemp: *const SECItem = unsafe { ocsp_ptr.as_ref().items.offset(idx).cast() }; + let item = unsafe { slice::from_raw_parts((*itemp).data, (*itemp).len as usize) }; + ocsp_helper.push(item.to_owned()); + } + Some(ocsp_helper) + } + None => None, + } +} + +fn signed_cert_timestamp(fd: *mut PRFileDesc) -> Option<Vec<u8>> { + let sct_nss = unsafe { SSL_PeerSignedCertTimestamps(fd) }; + match NonNull::new(sct_nss as *mut SECItem) { + Some(sct_ptr) => { + if unsafe { sct_ptr.as_ref().len == 0 || sct_ptr.as_ref().data.is_null() } { + Some(Vec::new()) + } else { + let sct_slice = unsafe { + slice::from_raw_parts(sct_ptr.as_ref().data, sct_ptr.as_ref().len as usize) + }; + Some(sct_slice.to_owned()) + } + } + None => None, + } +} + +impl CertificateInfo { + pub(crate) fn new(fd: *mut PRFileDesc) -> Option<Self> { + peer_certificate_chain(fd).map(|(certs, cursor)| Self { + certs, + cursor, + stapled_ocsp_responses: stapled_ocsp_responses(fd), + signed_cert_timestamp: signed_cert_timestamp(fd), + }) + } + + fn head(certs: &CertList) -> *const CERTCertListNode { + // Three stars: one for the reference, one for the wrapper, one to deference the pointer. + unsafe { addr_of!((***certs).list).cast() } + } +} + +impl<'a> Iterator for &'a mut CertificateInfo { + type Item = &'a [u8]; + fn next(&mut self) -> Option<&'a [u8]> { + self.cursor = unsafe { *self.cursor }.links.next.cast(); + if self.cursor == CertificateInfo::head(&self.certs) { + return None; + } + let mut item = Item::make_empty(); + let cert = unsafe { *self.cursor }.cert; + secstatus_to_res(unsafe { CERT_GetCertificateDer(cert, &mut item) }) + .expect("getting DER from certificate should work"); + Some(unsafe { std::slice::from_raw_parts(item.data, item.len as usize) }) + } +} + +impl CertificateInfo { + pub fn stapled_ocsp_responses(&mut self) -> &Option<Vec<Vec<u8>>> { + &self.stapled_ocsp_responses + } + + pub fn signed_cert_timestamp(&mut self) -> &Option<Vec<u8>> { + &self.signed_cert_timestamp + } +} diff --git a/third_party/rust/neqo-crypto/src/constants.rs b/third_party/rust/neqo-crypto/src/constants.rs new file mode 100644 index 0000000000..21e1a5aceb --- /dev/null +++ b/third_party/rust/neqo-crypto/src/constants.rs @@ -0,0 +1,145 @@ +// Licensed under the Apache License, Version 2.0 <LICENSE-APACHE or +// http://www.apache.org/licenses/LICENSE-2.0> or the MIT license +// <LICENSE-MIT or http://opensource.org/licenses/MIT>, at your +// option. This file may not be copied, modified, or distributed +// except according to those terms. + +#![allow(dead_code)] + +use crate::ssl; + +// Ideally all of these would be enums, but size matters and we need to allow +// for values outside of those that are defined here. + +pub type Alert = u8; + +pub type Epoch = u16; +// TLS doesn't really have an "initial" concept that maps to QUIC so directly, +// but this should be clear enough. +pub const TLS_EPOCH_INITIAL: Epoch = 0_u16; +pub const TLS_EPOCH_ZERO_RTT: Epoch = 1_u16; +pub const TLS_EPOCH_HANDSHAKE: Epoch = 2_u16; +// Also, we don't use TLS epochs > 3. +pub const TLS_EPOCH_APPLICATION_DATA: Epoch = 3_u16; + +/// Rather than defining a type alias and a bunch of constants, which leads to a ton of repetition, +/// use this macro. +macro_rules! remap_enum { + { $t:ident: $s:ty { $( $n:ident = $v:path ),+ $(,)? } } => { + pub type $t = $s; + $( pub const $n: $t = $v as $t; )+ + }; + { $t:ident: $s:ty => $e:ident { $( $n:ident = $v:ident ),+ $(,)? } } => { + remap_enum!{ $t: $s { $( $n = $e::$v ),+ } } + }; + { $t:ident: $s:ty => $p:ident::$e:ident { $( $n:ident = $v:ident ),+ $(,)? } } => { + remap_enum!{ $t: $s { $( $n = $p::$e::$v ),+ } } + }; +} + +remap_enum! { + Version: u16 => ssl { + TLS_VERSION_1_2 = SSL_LIBRARY_VERSION_TLS_1_2, + TLS_VERSION_1_3 = SSL_LIBRARY_VERSION_TLS_1_3, + } +} + +mod ciphers { + include!(concat!(env!("OUT_DIR"), "/nss_ciphers.rs")); +} + +remap_enum! { + Cipher: u16 => ciphers { + TLS_AES_128_GCM_SHA256 = TLS_AES_128_GCM_SHA256, + TLS_AES_256_GCM_SHA384 = TLS_AES_256_GCM_SHA384, + TLS_CHACHA20_POLY1305_SHA256 = TLS_CHACHA20_POLY1305_SHA256, + } +} + +remap_enum! { + Group: u16 => ssl::SSLNamedGroup { + TLS_GRP_EC_SECP256R1 = ssl_grp_ec_secp256r1, + TLS_GRP_EC_SECP384R1 = ssl_grp_ec_secp384r1, + TLS_GRP_EC_SECP521R1 = ssl_grp_ec_secp521r1, + TLS_GRP_EC_X25519 = ssl_grp_ec_curve25519, + } +} + +remap_enum! { + HandshakeMessage: u8 => ssl::SSLHandshakeType { + TLS_HS_HELLO_REQUEST = ssl_hs_hello_request, + TLS_HS_CLIENT_HELLO = ssl_hs_client_hello, + TLS_HS_SERVER_HELLO = ssl_hs_server_hello, + TLS_HS_HELLO_VERIFY_REQUEST = ssl_hs_hello_verify_request, + TLS_HS_NEW_SESSION_TICKET = ssl_hs_new_session_ticket, + TLS_HS_END_OF_EARLY_DATA = ssl_hs_end_of_early_data, + TLS_HS_HELLO_RETRY_REQUEST = ssl_hs_hello_retry_request, + TLS_HS_ENCRYPTED_EXTENSIONS = ssl_hs_encrypted_extensions, + TLS_HS_CERTIFICATE = ssl_hs_certificate, + TLS_HS_SERVER_KEY_EXCHANGE = ssl_hs_server_key_exchange, + TLS_HS_CERTIFICATE_REQUEST = ssl_hs_certificate_request, + TLS_HS_SERVER_HELLO_DONE = ssl_hs_server_hello_done, + TLS_HS_CERTIFICATE_VERIFY = ssl_hs_certificate_verify, + TLS_HS_CLIENT_KEY_EXCHANGE = ssl_hs_client_key_exchange, + TLS_HS_FINISHED = ssl_hs_finished, + TLS_HS_CERT_STATUS = ssl_hs_certificate_status, + TLS_HS_KEY_UDPATE = ssl_hs_key_update, + } +} + +remap_enum! { + ContentType: u8 => ssl::SSLContentType { + TLS_CT_CHANGE_CIPHER_SPEC = ssl_ct_change_cipher_spec, + TLS_CT_ALERT = ssl_ct_alert, + TLS_CT_HANDSHAKE = ssl_ct_handshake, + TLS_CT_APPLICATION_DATA = ssl_ct_application_data, + TLS_CT_ACK = ssl_ct_ack, + } +} + +remap_enum! { + Extension: u16 => ssl::SSLExtensionType { + TLS_EXT_SERVER_NAME = ssl_server_name_xtn, + TLS_EXT_CERT_STATUS = ssl_cert_status_xtn, + TLS_EXT_GROUPS = ssl_supported_groups_xtn, + TLS_EXT_EC_POINT_FORMATS = ssl_ec_point_formats_xtn, + TLS_EXT_SIG_SCHEMES = ssl_signature_algorithms_xtn, + TLS_EXT_USE_SRTP = ssl_use_srtp_xtn, + TLS_EXT_ALPN = ssl_app_layer_protocol_xtn, + TLS_EXT_SCT = ssl_signed_cert_timestamp_xtn, + TLS_EXT_PADDING = ssl_padding_xtn, + TLS_EXT_EMS = ssl_extended_master_secret_xtn, + TLS_EXT_RECORD_SIZE = ssl_record_size_limit_xtn, + TLS_EXT_SESSION_TICKET = ssl_session_ticket_xtn, + TLS_EXT_PSK = ssl_tls13_pre_shared_key_xtn, + TLS_EXT_EARLY_DATA = ssl_tls13_early_data_xtn, + TLS_EXT_VERSIONS = ssl_tls13_supported_versions_xtn, + TLS_EXT_COOKIE = ssl_tls13_cookie_xtn, + TLS_EXT_PSK_MODES = ssl_tls13_psk_key_exchange_modes_xtn, + TLS_EXT_CA = ssl_tls13_certificate_authorities_xtn, + TLS_EXT_POST_HS_AUTH = ssl_tls13_post_handshake_auth_xtn, + TLS_EXT_CERT_SIG_SCHEMES = ssl_signature_algorithms_cert_xtn, + TLS_EXT_KEY_SHARE = ssl_tls13_key_share_xtn, + TLS_EXT_RENEGOTIATION_INFO = ssl_renegotiation_info_xtn, + } +} + +remap_enum! { + SignatureScheme: u16 => ssl::SSLSignatureScheme { + TLS_SIG_NONE = ssl_sig_none, + TLS_SIG_RSA_PKCS1_SHA256 = ssl_sig_rsa_pkcs1_sha256, + TLS_SIG_RSA_PKCS1_SHA384 = ssl_sig_rsa_pkcs1_sha384, + TLS_SIG_RSA_PKCS1_SHA512 = ssl_sig_rsa_pkcs1_sha512, + TLS_SIG_ECDSA_SECP256R1_SHA256 = ssl_sig_ecdsa_secp256r1_sha256, + TLS_SIG_ECDSA_SECP384R1_SHA384 = ssl_sig_ecdsa_secp384r1_sha384, + TLS_SIG_ECDSA_SECP512R1_SHA512 = ssl_sig_ecdsa_secp521r1_sha512, + TLS_SIG_RSA_PSS_RSAE_SHA256 = ssl_sig_rsa_pss_rsae_sha256, + TLS_SIG_RSA_PSS_RSAE_SHA384 = ssl_sig_rsa_pss_rsae_sha384, + TLS_SIG_RSA_PSS_RSAE_SHA512 = ssl_sig_rsa_pss_rsae_sha512, + TLS_SIG_ED25519 = ssl_sig_ed25519, + TLS_SIG_ED448 = ssl_sig_ed448, + TLS_SIG_RSA_PSS_PSS_SHA256 = ssl_sig_rsa_pss_pss_sha256, + TLS_SIG_RSA_PSS_PSS_SHA384 = ssl_sig_rsa_pss_pss_sha384, + TLS_SIG_RSA_PSS_PSS_SHA512 = ssl_sig_rsa_pss_pss_sha512, + } +} diff --git a/third_party/rust/neqo-crypto/src/ech.rs b/third_party/rust/neqo-crypto/src/ech.rs new file mode 100644 index 0000000000..5c774e1360 --- /dev/null +++ b/third_party/rust/neqo-crypto/src/ech.rs @@ -0,0 +1,190 @@ +// Licensed under the Apache License, Version 2.0 <LICENSE-APACHE or +// http://www.apache.org/licenses/LICENSE-2.0> or the MIT license +// <LICENSE-MIT or http://opensource.org/licenses/MIT>, at your +// option. This file may not be copied, modified, or distributed +// except according to those terms. + +use crate::err::{ssl::SSL_ERROR_ECH_RETRY_WITH_ECH, Error, Res}; +use crate::p11::{ + self, Item, PrivateKey, PublicKey, SECITEM_FreeItem, SECItem, SECKEYPrivateKey, + SECKEYPublicKey, Slot, +}; +use crate::ssl::{PRBool, PRFileDesc}; +use neqo_common::qtrace; +use std::convert::TryFrom; +use std::ffi::CString; +use std::os::raw::{c_char, c_uint}; +use std::ptr::{addr_of_mut, null_mut}; + +pub use crate::p11::{HpkeAeadId as AeadId, HpkeKdfId as KdfId, HpkeKemId as KemId}; +pub use crate::ssl::HpkeSymmetricSuite as SymmetricSuite; + +experimental_api!(SSL_EnableTls13GreaseEch( + fd: *mut PRFileDesc, + enabled: PRBool, +)); + +experimental_api!(SSL_GetEchRetryConfigs( + fd: *mut PRFileDesc, + config: *mut SECItem, +)); + +experimental_api!(SSL_SetClientEchConfigs( + fd: *mut PRFileDesc, + config_list: *const u8, + config_list_len: c_uint, +)); + +experimental_api!(SSL_SetServerEchConfigs( + fd: *mut PRFileDesc, + pk: *const SECKEYPublicKey, + sk: *const SECKEYPrivateKey, + record: *const u8, + record_len: c_uint, +)); + +experimental_api!(SSL_EncodeEchConfigId( + config_id: u8, + public_name: *const c_char, + max_name_len: c_uint, + kem_id: KemId::Type, + pk: *const SECKEYPublicKey, + hpke_suites: *const SymmetricSuite, + hpke_suite_count: c_uint, + out: *mut u8, + out_len: *mut c_uint, + max_len: c_uint, +)); + +/// Convert any result that contains an ECH error into a result with an `EchRetry`. +pub fn convert_ech_error(fd: *mut PRFileDesc, err: Error) -> Error { + if let Error::NssError { + code: SSL_ERROR_ECH_RETRY_WITH_ECH, + .. + } = &err + { + let mut item = Item::make_empty(); + if unsafe { SSL_GetEchRetryConfigs(fd, &mut item).is_err() } { + return Error::InternalError; + } + let buf = unsafe { + let slc = std::slice::from_raw_parts(item.data, usize::try_from(item.len).unwrap()); + let buf = Vec::from(slc); + SECITEM_FreeItem(&mut item, PRBool::from(false)); + buf + }; + Error::EchRetry(buf) + } else { + err + } +} + +/// Generate a key pair for encrypted client hello (ECH). +/// +/// # Errors +/// When NSS fails to generate a key pair or when the KEM is not supported. +/// # Panics +/// When underlying types aren't large enough to hold keys. So never. +pub fn generate_keys() -> Res<(PrivateKey, PublicKey)> { + let slot = Slot::internal()?; + + let oid_data = unsafe { p11::SECOID_FindOIDByTag(p11::SECOidTag::SEC_OID_CURVE25519) }; + let oid = unsafe { oid_data.as_ref() }.ok_or(Error::InternalError)?; + let oid_slc = + unsafe { std::slice::from_raw_parts(oid.oid.data, usize::try_from(oid.oid.len).unwrap()) }; + let mut params: Vec<u8> = Vec::with_capacity(oid_slc.len() + 2); + params.push(u8::try_from(p11::SEC_ASN1_OBJECT_ID).unwrap()); + params.push(u8::try_from(oid.oid.len).unwrap()); + params.extend_from_slice(oid_slc); + + let mut public_ptr: *mut SECKEYPublicKey = null_mut(); + let mut param_item = Item::wrap(¶ms); + + // If we have tracing on, try to ensure that key data can be read. + let insensitive_secret_ptr = if log::log_enabled!(log::Level::Trace) { + unsafe { + p11::PK11_GenerateKeyPairWithOpFlags( + *slot, + p11::CK_MECHANISM_TYPE::from(p11::CKM_EC_KEY_PAIR_GEN), + addr_of_mut!(param_item).cast(), + &mut public_ptr, + p11::PK11_ATTR_SESSION | p11::PK11_ATTR_INSENSITIVE | p11::PK11_ATTR_PUBLIC, + p11::CK_FLAGS::from(p11::CKF_DERIVE), + p11::CK_FLAGS::from(p11::CKF_DERIVE), + null_mut(), + ) + } + } else { + null_mut() + }; + assert_eq!(insensitive_secret_ptr.is_null(), public_ptr.is_null()); + let secret_ptr = if insensitive_secret_ptr.is_null() { + unsafe { + p11::PK11_GenerateKeyPairWithOpFlags( + *slot, + p11::CK_MECHANISM_TYPE::from(p11::CKM_EC_KEY_PAIR_GEN), + addr_of_mut!(param_item).cast(), + &mut public_ptr, + p11::PK11_ATTR_SESSION | p11::PK11_ATTR_SENSITIVE | p11::PK11_ATTR_PRIVATE, + p11::CK_FLAGS::from(p11::CKF_DERIVE), + p11::CK_FLAGS::from(p11::CKF_DERIVE), + null_mut(), + ) + } + } else { + insensitive_secret_ptr + }; + assert_eq!(secret_ptr.is_null(), public_ptr.is_null()); + let sk = PrivateKey::from_ptr(secret_ptr)?; + let pk = PublicKey::from_ptr(public_ptr)?; + qtrace!("Generated key pair: sk={:?} pk={:?}", sk, pk); + Ok((sk, pk)) +} + +/// Encode a configuration for encrypted client hello (ECH). +/// +/// # Errors +/// When NSS fails to generate a valid configuration encoding (i.e., unlikely). +pub fn encode_config(config: u8, public_name: &str, pk: &PublicKey) -> Res<Vec<u8>> { + // A sensible fixed value for the maximum length of a name. + const MAX_NAME_LEN: c_uint = 64; + // Enable a selection of suites. + // NSS supports SHA-512 as well, which could be added here. + const SUITES: &[SymmetricSuite] = &[ + SymmetricSuite { + kdfId: KdfId::HpkeKdfHkdfSha256, + aeadId: AeadId::HpkeAeadAes128Gcm, + }, + SymmetricSuite { + kdfId: KdfId::HpkeKdfHkdfSha256, + aeadId: AeadId::HpkeAeadChaCha20Poly1305, + }, + SymmetricSuite { + kdfId: KdfId::HpkeKdfHkdfSha384, + aeadId: AeadId::HpkeAeadAes128Gcm, + }, + SymmetricSuite { + kdfId: KdfId::HpkeKdfHkdfSha384, + aeadId: AeadId::HpkeAeadChaCha20Poly1305, + }, + ]; + + let name = CString::new(public_name)?; + let mut encoded = [0; 1024]; + let mut encoded_len = 0; + unsafe { + SSL_EncodeEchConfigId( + config, + name.as_ptr(), + MAX_NAME_LEN, + KemId::HpkeDhKemX25519Sha256, + **pk, + SUITES.as_ptr(), + c_uint::try_from(SUITES.len())?, + encoded.as_mut_ptr(), + &mut encoded_len, + c_uint::try_from(encoded.len())?, + )?; + } + Ok(Vec::from(&encoded[..usize::try_from(encoded_len)?])) +} diff --git a/third_party/rust/neqo-crypto/src/err.rs b/third_party/rust/neqo-crypto/src/err.rs new file mode 100644 index 0000000000..5ba7c02d4b --- /dev/null +++ b/third_party/rust/neqo-crypto/src/err.rs @@ -0,0 +1,214 @@ +// Licensed under the Apache License, Version 2.0 <LICENSE-APACHE or +// http://www.apache.org/licenses/LICENSE-2.0> or the MIT license +// <LICENSE-MIT or http://opensource.org/licenses/MIT>, at your +// option. This file may not be copied, modified, or distributed +// except according to those terms. + +#![allow(dead_code)] +#![allow(clippy::upper_case_acronyms)] + +use std::os::raw::c_char; +use std::str::Utf8Error; + +use crate::ssl::{SECStatus, SECSuccess}; + +include!(concat!(env!("OUT_DIR"), "/nspr_error.rs")); +mod codes { + #![allow(non_snake_case)] + include!(concat!(env!("OUT_DIR"), "/nss_secerr.rs")); + include!(concat!(env!("OUT_DIR"), "/nss_sslerr.rs")); + include!(concat!(env!("OUT_DIR"), "/mozpkix.rs")); +} +pub use codes::mozilla_pkix_ErrorCode as mozpkix; +pub use codes::SECErrorCodes as sec; +pub use codes::SSLErrorCodes as ssl; +pub mod nspr { + include!(concat!(env!("OUT_DIR"), "/nspr_err.rs")); +} + +pub type Res<T> = Result<T, Error>; + +#[derive(Clone, Debug, PartialEq, PartialOrd, Ord, Eq)] +pub enum Error { + AeadError, + CertificateLoading, + CipherInitFailure, + CreateSslSocket, + EchRetry(Vec<u8>), + HkdfError, + InternalError, + IntegerOverflow, + InvalidEpoch, + MixedHandshakeMethod, + NoDataAvailable, + NssError { + name: String, + code: PRErrorCode, + desc: String, + }, + OverrunError, + SelfEncryptFailure, + StringError, + TimeTravelError, + UnsupportedCipher, + UnsupportedVersion, +} + +impl Error { + pub(crate) fn last_nss_error() -> Self { + Self::from(unsafe { PR_GetError() }) + } +} + +impl std::error::Error for Error { + #[must_use] + fn cause(&self) -> Option<&dyn std::error::Error> { + None + } + #[must_use] + fn source(&self) -> Option<&(dyn std::error::Error + 'static)> { + None + } +} + +impl std::fmt::Display for Error { + fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { + write!(f, "Error: {:?}", self) + } +} + +impl From<std::num::TryFromIntError> for Error { + #[must_use] + fn from(_: std::num::TryFromIntError) -> Self { + Self::IntegerOverflow + } +} +impl From<std::ffi::NulError> for Error { + #[must_use] + fn from(_: std::ffi::NulError) -> Self { + Self::InternalError + } +} +impl From<Utf8Error> for Error { + fn from(_: Utf8Error) -> Self { + Self::StringError + } +} +impl From<PRErrorCode> for Error { + fn from(code: PRErrorCode) -> Self { + let name = wrap_str_fn(|| unsafe { PR_ErrorToName(code) }, "UNKNOWN_ERROR"); + let desc = wrap_str_fn( + || unsafe { PR_ErrorToString(code, PR_LANGUAGE_I_DEFAULT) }, + "...", + ); + Self::NssError { name, code, desc } + } +} + +use std::ffi::CStr; + +fn wrap_str_fn<F>(f: F, dflt: &str) -> String +where + F: FnOnce() -> *const c_char, +{ + unsafe { + let p = f(); + if p.is_null() { + return dflt.to_string(); + } + CStr::from_ptr(p).to_string_lossy().into_owned() + } +} + +pub fn secstatus_to_res(rv: SECStatus) -> Res<()> { + if rv == SECSuccess { + Ok(()) + } else { + Err(Error::last_nss_error()) + } +} + +pub fn is_blocked(result: &Res<()>) -> bool { + match result { + Err(Error::NssError { code, .. }) => *code == nspr::PR_WOULD_BLOCK_ERROR, + _ => false, + } +} + +#[cfg(test)] +mod tests { + use crate::err::{self, is_blocked, secstatus_to_res, Error, PRErrorCode, PR_SetError}; + use crate::ssl::{SECFailure, SECSuccess}; + use test_fixture::fixture_init; + + fn set_error_code(code: PRErrorCode) { + // This code doesn't work without initializing NSS first. + fixture_init(); + unsafe { + PR_SetError(code, 0); + } + } + + #[test] + fn error_code() { + fixture_init(); + assert_eq!(15 - 0x3000, err::ssl::SSL_ERROR_BAD_MAC_READ); + assert_eq!(166 - 0x2000, err::sec::SEC_ERROR_LIBPKIX_INTERNAL); + assert_eq!(-5998, err::nspr::PR_WOULD_BLOCK_ERROR); + } + + #[test] + fn is_ok() { + assert!(secstatus_to_res(SECSuccess).is_ok()); + } + + #[test] + fn is_err() { + set_error_code(err::ssl::SSL_ERROR_BAD_MAC_READ); + let r = secstatus_to_res(SECFailure); + assert!(r.is_err()); + match r.unwrap_err() { + Error::NssError { name, code, desc } => { + assert_eq!(name, "SSL_ERROR_BAD_MAC_READ"); + assert_eq!(code, -12273); + assert_eq!( + desc, + "SSL received a record with an incorrect Message Authentication Code." + ); + } + _ => unreachable!(), + } + } + + #[test] + fn is_err_zero_code() { + set_error_code(0); + let r = secstatus_to_res(SECFailure); + assert!(r.is_err()); + match r.unwrap_err() { + Error::NssError { name, code, .. } => { + assert_eq!(name, "UNKNOWN_ERROR"); + assert_eq!(code, 0); + // Note that we don't test |desc| here because that comes from + // strerror(0), which is platform-dependent. + } + _ => unreachable!(), + } + } + + #[test] + fn blocked() { + set_error_code(err::nspr::PR_WOULD_BLOCK_ERROR); + let r = secstatus_to_res(SECFailure); + assert!(r.is_err()); + assert!(is_blocked(&r)); + match r.unwrap_err() { + Error::NssError { name, code, desc } => { + assert_eq!(name, "PR_WOULD_BLOCK_ERROR"); + assert_eq!(code, -5998); + assert_eq!(desc, "The operation would have blocked"); + } + _ => panic!("bad error type"), + } + } +} diff --git a/third_party/rust/neqo-crypto/src/exp.rs b/third_party/rust/neqo-crypto/src/exp.rs new file mode 100644 index 0000000000..59b6cb0ba5 --- /dev/null +++ b/third_party/rust/neqo-crypto/src/exp.rs @@ -0,0 +1,23 @@ +// Licensed under the Apache License, Version 2.0 <LICENSE-APACHE or +// http://www.apache.org/licenses/LICENSE-2.0> or the MIT license +// <LICENSE-MIT or http://opensource.org/licenses/MIT>, at your +// option. This file may not be copied, modified, or distributed +// except according to those terms. + +macro_rules! experimental_api { + ( $n:ident ( $( $a:ident : $t:ty ),* $(,)? ) ) => { + #[allow(non_snake_case)] + #[allow(clippy::too_many_arguments)] + pub(crate) unsafe fn $n ( $( $a : $t ),* ) -> Result<(), crate::err::Error> { + const EXP_FUNCTION: &str = stringify!($n); + let n = ::std::ffi::CString::new(EXP_FUNCTION)?; + let f = crate::ssl::SSL_GetExperimentalAPI(n.as_ptr()); + if f.is_null() { + return Err(crate::err::Error::InternalError); + } + let f: unsafe extern "C" fn( $( $t ),* ) -> crate::ssl::SECStatus = ::std::mem::transmute(f); + let rv = f( $( $a ),* ); + crate::err::secstatus_to_res(rv) + } + }; +} diff --git a/third_party/rust/neqo-crypto/src/ext.rs b/third_party/rust/neqo-crypto/src/ext.rs new file mode 100644 index 0000000000..5c6dc1c8ff --- /dev/null +++ b/third_party/rust/neqo-crypto/src/ext.rs @@ -0,0 +1,165 @@ +// Licensed under the Apache License, Version 2.0 <LICENSE-APACHE or +// http://www.apache.org/licenses/LICENSE-2.0> or the MIT license +// <LICENSE-MIT or http://opensource.org/licenses/MIT>, at your +// option. This file may not be copied, modified, or distributed +// except according to those terms. + +use crate::agentio::as_c_void; +use crate::constants::{ + Extension, HandshakeMessage, TLS_HS_CLIENT_HELLO, TLS_HS_ENCRYPTED_EXTENSIONS, +}; +use crate::err::Res; +use crate::ssl::{ + PRBool, PRFileDesc, SECFailure, SECStatus, SECSuccess, SSLAlertDescription, + SSLExtensionHandler, SSLExtensionWriter, SSLHandshakeType, +}; + +use std::cell::RefCell; +use std::convert::TryFrom; +use std::os::raw::{c_uint, c_void}; +use std::pin::Pin; +use std::rc::Rc; + +experimental_api!(SSL_InstallExtensionHooks( + fd: *mut PRFileDesc, + extension: u16, + writer: SSLExtensionWriter, + writer_arg: *mut c_void, + handler: SSLExtensionHandler, + handler_arg: *mut c_void, +)); + +pub enum ExtensionWriterResult { + Write(usize), + Skip, +} + +pub enum ExtensionHandlerResult { + Ok, + Alert(crate::constants::Alert), +} + +pub trait ExtensionHandler { + fn write(&mut self, msg: HandshakeMessage, _d: &mut [u8]) -> ExtensionWriterResult { + match msg { + TLS_HS_CLIENT_HELLO | TLS_HS_ENCRYPTED_EXTENSIONS => ExtensionWriterResult::Write(0), + _ => ExtensionWriterResult::Skip, + } + } + + fn handle(&mut self, msg: HandshakeMessage, _d: &[u8]) -> ExtensionHandlerResult { + match msg { + TLS_HS_CLIENT_HELLO | TLS_HS_ENCRYPTED_EXTENSIONS => ExtensionHandlerResult::Ok, + _ => ExtensionHandlerResult::Alert(110), // unsupported_extension + } + } +} + +type BoxedExtensionHandler = Box<Rc<RefCell<dyn ExtensionHandler>>>; + +pub struct ExtensionTracker { + extension: Extension, + handler: Pin<Box<BoxedExtensionHandler>>, +} + +impl ExtensionTracker { + // Technically the as_mut() call here is the only unsafe bit, + // but don't call this function lightly. + unsafe fn wrap_handler_call<F, T>(arg: *mut c_void, f: F) -> T + where + F: FnOnce(&mut dyn ExtensionHandler) -> T, + { + let rc = arg.cast::<BoxedExtensionHandler>().as_mut().unwrap(); + f(&mut *rc.borrow_mut()) + } + + #[allow(clippy::cast_possible_truncation)] + unsafe extern "C" fn extension_writer( + _fd: *mut PRFileDesc, + message: SSLHandshakeType::Type, + data: *mut u8, + len: *mut c_uint, + max_len: c_uint, + arg: *mut c_void, + ) -> PRBool { + let d = std::slice::from_raw_parts_mut(data, max_len as usize); + Self::wrap_handler_call(arg, |handler| { + // Cast is safe here because the message type is always part of the enum + match handler.write(message as HandshakeMessage, d) { + ExtensionWriterResult::Write(sz) => { + *len = c_uint::try_from(sz).expect("integer overflow from extension writer"); + 1 + } + ExtensionWriterResult::Skip => 0, + } + }) + } + + unsafe extern "C" fn extension_handler( + _fd: *mut PRFileDesc, + message: SSLHandshakeType::Type, + data: *const u8, + len: c_uint, + alert: *mut SSLAlertDescription, + arg: *mut c_void, + ) -> SECStatus { + let d = std::slice::from_raw_parts(data, len as usize); + #[allow(clippy::cast_possible_truncation)] + Self::wrap_handler_call(arg, |handler| { + // Cast is safe here because the message type is always part of the enum + match handler.handle(message as HandshakeMessage, d) { + ExtensionHandlerResult::Ok => SECSuccess, + ExtensionHandlerResult::Alert(a) => { + *alert = a; + SECFailure + } + } + }) + } + + /// Use the provided handler to manage an extension. This is quite unsafe. + /// + /// # Safety + /// The holder of this `ExtensionTracker` needs to ensure that it lives at + /// least as long as the file descriptor, as NSS provides no way to remove + /// an extension handler once it is configured. + /// + /// # Errors + /// If the underlying NSS API fails to register a handler. + pub unsafe fn new( + fd: *mut PRFileDesc, + extension: Extension, + handler: Rc<RefCell<dyn ExtensionHandler>>, + ) -> Res<Self> { + // The ergonomics here aren't great for users of this API, but it's + // horrific here. The pinned outer box gives us a stable pointer to the inner + // box. This is the pointer that is passed to NSS. + // + // The inner box points to the reference-counted object. This inner box is + // what we end up with a reference to in callbacks. That extra wrapper around + // the Rc avoid any touching of reference counts in callbacks, which would + // inevitably lead to leaks as we don't control how many times the callback + // is invoked. + // + // This way, only this "outer" code deals with the reference count. + let mut tracker = Self { + extension, + handler: Box::pin(Box::new(handler)), + }; + SSL_InstallExtensionHooks( + fd, + extension, + Some(Self::extension_writer), + as_c_void(&mut tracker.handler), + Some(Self::extension_handler), + as_c_void(&mut tracker.handler), + )?; + Ok(tracker) + } +} + +impl std::fmt::Debug for ExtensionTracker { + fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { + write!(f, "ExtensionTracker: {:?}", self.extension) + } +} diff --git a/third_party/rust/neqo-crypto/src/hkdf.rs b/third_party/rust/neqo-crypto/src/hkdf.rs new file mode 100644 index 0000000000..8de9c01fc8 --- /dev/null +++ b/third_party/rust/neqo-crypto/src/hkdf.rs @@ -0,0 +1,128 @@ +// Licensed under the Apache License, Version 2.0 <LICENSE-APACHE or +// http://www.apache.org/licenses/LICENSE-2.0> or the MIT license +// <LICENSE-MIT or http://opensource.org/licenses/MIT>, at your +// option. This file may not be copied, modified, or distributed +// except according to those terms. + +use crate::constants::{ + Cipher, Version, TLS_AES_128_GCM_SHA256, TLS_AES_256_GCM_SHA384, TLS_CHACHA20_POLY1305_SHA256, + TLS_VERSION_1_3, +}; +use crate::err::{Error, Res}; +use crate::p11::{ + random, Item, PK11Origin, PK11SymKey, PK11_ImportDataKey, Slot, SymKey, CKA_DERIVE, + CKM_HKDF_DERIVE, CK_ATTRIBUTE_TYPE, CK_MECHANISM_TYPE, +}; + +use std::convert::TryFrom; +use std::os::raw::{c_char, c_uint}; +use std::ptr::null_mut; + +experimental_api!(SSL_HkdfExtract( + version: Version, + cipher: Cipher, + salt: *mut PK11SymKey, + ikm: *mut PK11SymKey, + prk: *mut *mut PK11SymKey, +)); +experimental_api!(SSL_HkdfExpandLabel( + version: Version, + cipher: Cipher, + prk: *mut PK11SymKey, + handshake_hash: *const u8, + handshake_hash_len: c_uint, + label: *const c_char, + label_len: c_uint, + secret: *mut *mut PK11SymKey, +)); + +fn key_size(version: Version, cipher: Cipher) -> Res<usize> { + if version != TLS_VERSION_1_3 { + return Err(Error::UnsupportedVersion); + } + Ok(match cipher { + TLS_AES_128_GCM_SHA256 | TLS_CHACHA20_POLY1305_SHA256 => 32, + TLS_AES_256_GCM_SHA384 => 48, + _ => return Err(Error::UnsupportedCipher), + }) +} + +/// Generate a random key of the right size for the given suite. +/// +/// # Errors +/// Only if NSS fails. +pub fn generate_key(version: Version, cipher: Cipher) -> Res<SymKey> { + import_key(version, &random(key_size(version, cipher)?)) +} + +/// Import a symmetric key for use with HKDF. +/// +/// # Errors +/// Errors returned if the key buffer is an incompatible size or the NSS functions fail. +pub fn import_key(version: Version, buf: &[u8]) -> Res<SymKey> { + if version != TLS_VERSION_1_3 { + return Err(Error::UnsupportedVersion); + } + let slot = Slot::internal()?; + let key_ptr = unsafe { + PK11_ImportDataKey( + *slot, + CK_MECHANISM_TYPE::from(CKM_HKDF_DERIVE), + PK11Origin::PK11_OriginUnwrap, + CK_ATTRIBUTE_TYPE::from(CKA_DERIVE), + &mut Item::wrap(buf), + null_mut(), + ) + }; + SymKey::from_ptr(key_ptr) +} + +/// Extract a PRK from the given salt and IKM using the algorithm defined in RFC 5869. +/// +/// # Errors +/// Errors returned if inputs are too large or the NSS functions fail. +pub fn extract( + version: Version, + cipher: Cipher, + salt: Option<&SymKey>, + ikm: &SymKey, +) -> Res<SymKey> { + let mut prk: *mut PK11SymKey = null_mut(); + let salt_ptr: *mut PK11SymKey = match salt { + Some(s) => **s, + None => null_mut(), + }; + unsafe { SSL_HkdfExtract(version, cipher, salt_ptr, **ikm, &mut prk) }?; + SymKey::from_ptr(prk) +} + +/// Expand a PRK using the HKDF-Expand-Label function defined in RFC 8446. +/// +/// # Errors +/// Errors returned if inputs are too large or the NSS functions fail. +pub fn expand_label( + version: Version, + cipher: Cipher, + prk: &SymKey, + handshake_hash: &[u8], + label: &str, +) -> Res<SymKey> { + let l = label.as_bytes(); + let mut secret: *mut PK11SymKey = null_mut(); + + // Note that this doesn't allow for passing null() for the handshake hash. + // A zero-length slice produces an identical result. + unsafe { + SSL_HkdfExpandLabel( + version, + cipher, + **prk, + handshake_hash.as_ptr(), + c_uint::try_from(handshake_hash.len())?, + l.as_ptr().cast(), + c_uint::try_from(l.len())?, + &mut secret, + ) + }?; + SymKey::from_ptr(secret) +} diff --git a/third_party/rust/neqo-crypto/src/hp.rs b/third_party/rust/neqo-crypto/src/hp.rs new file mode 100644 index 0000000000..f968943c00 --- /dev/null +++ b/third_party/rust/neqo-crypto/src/hp.rs @@ -0,0 +1,187 @@ +// Licensed under the Apache License, Version 2.0 <LICENSE-APACHE or +// http://www.apache.org/licenses/LICENSE-2.0> or the MIT license +// <LICENSE-MIT or http://opensource.org/licenses/MIT>, at your +// option. This file may not be copied, modified, or distributed +// except according to those terms. + +use crate::constants::{ + Cipher, Version, TLS_AES_128_GCM_SHA256, TLS_AES_256_GCM_SHA384, TLS_CHACHA20_POLY1305_SHA256, +}; +use crate::err::{secstatus_to_res, Error, Res}; +use crate::p11::{ + Context, Item, PK11SymKey, PK11_CipherOp, PK11_CreateContextBySymKey, PK11_Encrypt, + PK11_GetBlockSize, SymKey, CKA_ENCRYPT, CKM_AES_ECB, CKM_CHACHA20, CK_ATTRIBUTE_TYPE, + CK_CHACHA20_PARAMS, CK_MECHANISM_TYPE, +}; + +use std::cell::RefCell; +use std::convert::TryFrom; +use std::fmt::{self, Debug}; +use std::os::raw::{c_char, c_int, c_uint}; +use std::ptr::{addr_of_mut, null, null_mut}; +use std::rc::Rc; + +experimental_api!(SSL_HkdfExpandLabelWithMech( + version: Version, + cipher: Cipher, + prk: *mut PK11SymKey, + handshake_hash: *const u8, + handshake_hash_len: c_uint, + label: *const c_char, + label_len: c_uint, + mech: CK_MECHANISM_TYPE, + key_size: c_uint, + secret: *mut *mut PK11SymKey, +)); + +#[derive(Clone)] +pub enum HpKey { + /// An AES encryption context. + /// Note: as we need to clone this object, we clone the pointer and + /// track references using `Rc`. `PK11Context` can't be used with `PK11_CloneContext` + /// as that is not supported for these contexts. + Aes(Rc<RefCell<Context>>), + /// The ChaCha20 mask has to invoke a new PK11_Encrypt every time as it needs to + /// change the counter and nonce on each invocation. + Chacha(SymKey), +} + +impl Debug for HpKey { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + write!(f, "HpKey") + } +} + +impl HpKey { + const SAMPLE_SIZE: usize = 16; + + /// QUIC-specific API for extracting a header-protection key. + /// + /// # Errors + /// Errors if HKDF fails or if the label is too long to fit in a `c_uint`. + /// # Panics + /// When `cipher` is not known to this code. + #[allow(clippy::cast_sign_loss)] // Cast for PK11_GetBlockSize is safe. + pub fn extract(version: Version, cipher: Cipher, prk: &SymKey, label: &str) -> Res<Self> { + const ZERO: &[u8] = &[0; 12]; + + let l = label.as_bytes(); + let mut secret: *mut PK11SymKey = null_mut(); + + let (mech, key_size) = match cipher { + TLS_AES_128_GCM_SHA256 => (CK_MECHANISM_TYPE::from(CKM_AES_ECB), 16), + TLS_AES_256_GCM_SHA384 => (CK_MECHANISM_TYPE::from(CKM_AES_ECB), 32), + TLS_CHACHA20_POLY1305_SHA256 => (CK_MECHANISM_TYPE::from(CKM_CHACHA20), 32), + _ => unreachable!(), + }; + + // Note that this doesn't allow for passing null() for the handshake hash. + // A zero-length slice produces an identical result. + unsafe { + SSL_HkdfExpandLabelWithMech( + version, + cipher, + **prk, + null(), + 0, + l.as_ptr().cast(), + c_uint::try_from(l.len())?, + mech, + key_size, + &mut secret, + ) + }?; + let key = SymKey::from_ptr(secret).or(Err(Error::HkdfError))?; + + let res = match cipher { + TLS_AES_128_GCM_SHA256 | TLS_AES_256_GCM_SHA384 => { + let context_ptr = unsafe { + PK11_CreateContextBySymKey( + mech, + CK_ATTRIBUTE_TYPE::from(CKA_ENCRYPT), + *key, + &Item::wrap(&ZERO[..0]), // Borrow a zero-length slice of ZERO. + ) + }; + let context = Context::from_ptr(context_ptr).or(Err(Error::CipherInitFailure))?; + Self::Aes(Rc::new(RefCell::new(context))) + } + TLS_CHACHA20_POLY1305_SHA256 => Self::Chacha(key), + _ => unreachable!(), + }; + + debug_assert_eq!( + res.block_size(), + usize::try_from(unsafe { PK11_GetBlockSize(mech, null_mut()) }).unwrap() + ); + Ok(res) + } + + /// Get the sample size, which is also the output size. + #[must_use] + #[allow(clippy::unused_self)] // To maintain an API contract. + pub fn sample_size(&self) -> usize { + Self::SAMPLE_SIZE + } + + fn block_size(&self) -> usize { + match self { + Self::Aes(_) => 16, + Self::Chacha(_) => 64, + } + } + + /// Generate a header protection mask for QUIC. + /// + /// # Errors + /// An error is returned if the NSS functions fail; a sample of the + /// wrong size is the obvious cause. + /// # Panics + /// When the mechanism for our key is not supported. + pub fn mask(&self, sample: &[u8]) -> Res<Vec<u8>> { + let mut output = vec![0_u8; self.block_size()]; + + match self { + Self::Aes(context) => { + let mut output_len: c_int = 0; + secstatus_to_res(unsafe { + PK11_CipherOp( + **context.borrow_mut(), + output.as_mut_ptr(), + &mut output_len, + c_int::try_from(output.len())?, + sample[..Self::SAMPLE_SIZE].as_ptr().cast(), + c_int::try_from(Self::SAMPLE_SIZE).unwrap(), + ) + })?; + assert_eq!(usize::try_from(output_len).unwrap(), output.len()); + Ok(output) + } + + Self::Chacha(key) => { + let params: CK_CHACHA20_PARAMS = CK_CHACHA20_PARAMS { + pBlockCounter: sample.as_ptr() as *mut u8, + blockCounterBits: 32, + pNonce: sample[4..Self::SAMPLE_SIZE].as_ptr() as *mut _, + ulNonceBits: 96, + }; + let mut output_len: c_uint = 0; + let mut param_item = Item::wrap_struct(¶ms); + secstatus_to_res(unsafe { + PK11_Encrypt( + **key, + CK_MECHANISM_TYPE::from(CKM_CHACHA20), + addr_of_mut!(param_item), + output[..].as_mut_ptr(), + &mut output_len, + c_uint::try_from(output.len())?, + output[..].as_ptr(), + c_uint::try_from(output.len())?, + ) + })?; + assert_eq!(usize::try_from(output_len).unwrap(), output.len()); + Ok(output) + } + } + } +} diff --git a/third_party/rust/neqo-crypto/src/lib.rs b/third_party/rust/neqo-crypto/src/lib.rs new file mode 100644 index 0000000000..5067701738 --- /dev/null +++ b/third_party/rust/neqo-crypto/src/lib.rs @@ -0,0 +1,204 @@ +// Licensed under the Apache License, Version 2.0 <LICENSE-APACHE or +// http://www.apache.org/licenses/LICENSE-2.0> or the MIT license +// <LICENSE-MIT or http://opensource.org/licenses/MIT>, at your +// option. This file may not be copied, modified, or distributed +// except according to those terms. + +#![cfg_attr(feature = "deny-warnings", deny(warnings))] +#![warn(clippy::pedantic)] +// Bindgen auto generated code +// won't adhere to the clippy rules below +#![allow(clippy::module_name_repetitions)] +#![allow(clippy::unseparated_literal_suffix)] +#![allow(clippy::used_underscore_binding)] + +#[macro_use] +mod exp; +#[macro_use] +mod p11; + +#[cfg(not(feature = "fuzzing"))] +mod aead; + +#[cfg(feature = "fuzzing")] +mod aead_fuzzing; + +pub mod agent; +mod agentio; +mod auth; +mod cert; +pub mod constants; +mod ech; +mod err; +pub mod ext; +pub mod hkdf; +pub mod hp; +mod once; +mod prio; +mod replay; +mod secrets; +pub mod selfencrypt; +mod ssl; +mod time; + +#[cfg(not(feature = "fuzzing"))] +pub use self::aead::Aead; + +#[cfg(feature = "fuzzing")] +pub use self::aead_fuzzing::Aead; + +#[cfg(feature = "fuzzing")] +pub use self::aead_fuzzing::FIXED_TAG_FUZZING; + +pub use self::agent::{ + Agent, AllowZeroRtt, Client, HandshakeState, Record, RecordList, ResumptionToken, SecretAgent, + SecretAgentInfo, SecretAgentPreInfo, Server, ZeroRttCheckResult, ZeroRttChecker, +}; +pub use self::auth::AuthenticationStatus; +pub use self::constants::*; +pub use self::ech::{ + encode_config as encode_ech_config, generate_keys as generate_ech_keys, AeadId, KdfId, KemId, + SymmetricSuite, +}; +pub use self::err::{Error, PRErrorCode, Res}; +pub use self::ext::{ExtensionHandler, ExtensionHandlerResult, ExtensionWriterResult}; +pub use self::p11::{random, PrivateKey, PublicKey, SymKey}; +pub use self::replay::AntiReplay; +pub use self::secrets::SecretDirection; +pub use self::ssl::Opt; + +use self::once::OnceResult; + +use std::ffi::CString; +use std::path::{Path, PathBuf}; +use std::ptr::null; + +const MINIMUM_NSS_VERSION: &str = "3.74"; + +#[allow(non_upper_case_globals, clippy::redundant_static_lifetimes)] +#[allow(clippy::upper_case_acronyms)] +#[allow(unknown_lints, clippy::borrow_as_ptr)] +mod nss { + include!(concat!(env!("OUT_DIR"), "/nss_init.rs")); +} + +// Need to map the types through. +fn secstatus_to_res(code: nss::SECStatus) -> Res<()> { + crate::err::secstatus_to_res(code as crate::ssl::SECStatus) +} + +enum NssLoaded { + External, + NoDb, + Db(Box<Path>), +} + +impl Drop for NssLoaded { + fn drop(&mut self) { + if !matches!(self, Self::External) { + unsafe { + secstatus_to_res(nss::NSS_Shutdown()).expect("NSS Shutdown failed"); + } + } + } +} + +static mut INITIALIZED: OnceResult<NssLoaded> = OnceResult::new(); + +fn already_initialized() -> bool { + unsafe { nss::NSS_IsInitialized() != 0 } +} + +fn version_check() { + let min_ver = CString::new(MINIMUM_NSS_VERSION).unwrap(); + assert_ne!( + unsafe { nss::NSS_VersionCheck(min_ver.as_ptr()) }, + 0, + "Minimum NSS version of {} not supported", + MINIMUM_NSS_VERSION, + ); +} + +/// Initialize NSS. This only executes the initialization routines once, so if there is any chance that +pub fn init() { + // Set time zero. + time::init(); + unsafe { + INITIALIZED.call_once(|| { + version_check(); + if already_initialized() { + return NssLoaded::External; + } + + secstatus_to_res(nss::NSS_NoDB_Init(null())).expect("NSS_NoDB_Init failed"); + secstatus_to_res(nss::NSS_SetDomesticPolicy()).expect("NSS_SetDomesticPolicy failed"); + + NssLoaded::NoDb + }); + } +} + +/// This enables SSLTRACE by calling a simple, harmless function to trigger its +/// side effects. SSLTRACE is not enabled in NSS until a socket is made or +/// global options are accessed. Reading an option is the least impact approach. +/// This allows us to use SSLTRACE in all of our unit tests and programs. +#[cfg(debug_assertions)] +fn enable_ssl_trace() { + let opt = ssl::Opt::Locking.as_int(); + let mut v: ::std::os::raw::c_int = 0; + secstatus_to_res(unsafe { ssl::SSL_OptionGetDefault(opt, &mut v) }) + .expect("SSL_OptionGetDefault failed"); +} + +/// Initialize with a database. +/// # Panics +/// If NSS cannot be initialized. +pub fn init_db<P: Into<PathBuf>>(dir: P) { + time::init(); + unsafe { + INITIALIZED.call_once(|| { + version_check(); + if already_initialized() { + return NssLoaded::External; + } + + let path = dir.into(); + assert!(path.is_dir()); + let pathstr = path.to_str().expect("path converts to string").to_string(); + let dircstr = CString::new(pathstr).unwrap(); + let empty = CString::new("").unwrap(); + secstatus_to_res(nss::NSS_Initialize( + dircstr.as_ptr(), + empty.as_ptr(), + empty.as_ptr(), + nss::SECMOD_DB.as_ptr().cast(), + nss::NSS_INIT_READONLY, + )) + .expect("NSS_Initialize failed"); + + secstatus_to_res(nss::NSS_SetDomesticPolicy()).expect("NSS_SetDomesticPolicy failed"); + secstatus_to_res(ssl::SSL_ConfigServerSessionIDCache( + 1024, + 0, + 0, + dircstr.as_ptr(), + )) + .expect("SSL_ConfigServerSessionIDCache failed"); + + #[cfg(debug_assertions)] + enable_ssl_trace(); + + NssLoaded::Db(path.into_boxed_path()) + }); + } +} + +/// # Panics +/// If NSS isn't initialized. +pub fn assert_initialized() { + unsafe { + INITIALIZED.call_once(|| { + panic!("NSS not initialized with init or init_db"); + }); + } +} diff --git a/third_party/rust/neqo-crypto/src/once.rs b/third_party/rust/neqo-crypto/src/once.rs new file mode 100644 index 0000000000..80657cfe26 --- /dev/null +++ b/third_party/rust/neqo-crypto/src/once.rs @@ -0,0 +1,44 @@ +// Licensed under the Apache License, Version 2.0 <LICENSE-APACHE or +// http://www.apache.org/licenses/LICENSE-2.0> or the MIT license +// <LICENSE-MIT or http://opensource.org/licenses/MIT>, at your +// option. This file may not be copied, modified, or distributed +// except according to those terms. + +use std::sync::Once; + +#[allow(clippy::module_name_repetitions)] +pub struct OnceResult<T> { + once: Once, + v: Option<T>, +} + +impl<T> OnceResult<T> { + #[must_use] + pub const fn new() -> Self { + Self { + once: Once::new(), + v: None, + } + } + + pub fn call_once<F: FnOnce() -> T>(&mut self, f: F) -> &T { + let v = &mut self.v; + self.once.call_once(|| { + *v = Some(f()); + }); + self.v.as_ref().unwrap() + } +} + +#[cfg(test)] +mod test { + use super::OnceResult; + + static mut STATIC_ONCE_RESULT: OnceResult<u64> = OnceResult::new(); + + #[test] + fn static_update() { + assert_eq!(*unsafe { STATIC_ONCE_RESULT.call_once(|| 23) }, 23); + assert_eq!(*unsafe { STATIC_ONCE_RESULT.call_once(|| 24) }, 23); + } +} diff --git a/third_party/rust/neqo-crypto/src/p11.rs b/third_party/rust/neqo-crypto/src/p11.rs new file mode 100644 index 0000000000..7848cf08c8 --- /dev/null +++ b/third_party/rust/neqo-crypto/src/p11.rs @@ -0,0 +1,303 @@ +// Licensed under the Apache License, Version 2.0 <LICENSE-APACHE or +// http://www.apache.org/licenses/LICENSE-2.0> or the MIT license +// <LICENSE-MIT or http://opensource.org/licenses/MIT>, at your +// option. This file may not be copied, modified, or distributed +// except according to those terms. + +#![allow(dead_code)] +#![allow(non_upper_case_globals)] +#![allow(non_camel_case_types)] +#![allow(non_snake_case)] + +use crate::err::{secstatus_to_res, Error, Res}; + +use neqo_common::hex_with_len; + +use std::convert::TryFrom; +use std::mem; +use std::ops::{Deref, DerefMut}; +use std::os::raw::{c_int, c_uint}; +use std::ptr::null_mut; + +#[allow(clippy::upper_case_acronyms)] +#[allow(clippy::unreadable_literal)] +#[allow(unknown_lints, clippy::borrow_as_ptr)] +mod nss_p11 { + include!(concat!(env!("OUT_DIR"), "/nss_p11.rs")); +} + +pub use nss_p11::*; + +macro_rules! scoped_ptr { + ($scoped:ident, $target:ty, $dtor:path) => { + pub struct $scoped { + ptr: *mut $target, + } + + impl $scoped { + /// Create a new instance of `$scoped` from a pointer. + /// + /// # Errors + /// When passed a null pointer generates an error. + pub fn from_ptr(ptr: *mut $target) -> Result<Self, crate::err::Error> { + if ptr.is_null() { + Err(crate::err::Error::last_nss_error()) + } else { + Ok(Self { ptr }) + } + } + } + + impl Deref for $scoped { + type Target = *mut $target; + #[must_use] + fn deref(&self) -> &*mut $target { + &self.ptr + } + } + + impl DerefMut for $scoped { + fn deref_mut(&mut self) -> &mut *mut $target { + &mut self.ptr + } + } + + impl Drop for $scoped { + #[allow(unused_must_use)] + fn drop(&mut self) { + unsafe { $dtor(self.ptr) }; + } + } + }; +} + +scoped_ptr!(Certificate, CERTCertificate, CERT_DestroyCertificate); +scoped_ptr!(CertList, CERTCertList, CERT_DestroyCertList); +scoped_ptr!(PublicKey, SECKEYPublicKey, SECKEY_DestroyPublicKey); + +impl PublicKey { + /// Get the HPKE serialization of the public key. + /// + /// # Errors + /// When the key cannot be exported, which can be because the type is not supported. + /// # Panics + /// When keys are too large to fit in `c_uint/usize`. So only on programming error. + pub fn key_data(&self) -> Res<Vec<u8>> { + let mut buf = vec![0; 100]; + let mut len: c_uint = 0; + secstatus_to_res(unsafe { + PK11_HPKE_Serialize( + **self, + buf.as_mut_ptr(), + &mut len, + c_uint::try_from(buf.len()).unwrap(), + ) + })?; + buf.truncate(usize::try_from(len).unwrap()); + Ok(buf) + } +} + +impl Clone for PublicKey { + #[must_use] + fn clone(&self) -> Self { + let ptr = unsafe { SECKEY_CopyPublicKey(self.ptr) }; + assert!(!ptr.is_null()); + Self { ptr } + } +} + +impl std::fmt::Debug for PublicKey { + fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { + if let Ok(b) = self.key_data() { + write!(f, "PublicKey {}", hex_with_len(b)) + } else { + write!(f, "Opaque PublicKey") + } + } +} + +scoped_ptr!(PrivateKey, SECKEYPrivateKey, SECKEY_DestroyPrivateKey); + +impl PrivateKey { + /// Get the bits of the private key. + /// + /// # Errors + /// When the key cannot be exported, which can be because the type is not supported + /// or because the key data cannot be extracted from the PKCS#11 module. + /// # Panics + /// When the values are too large to fit. So never. + pub fn key_data(&self) -> Res<Vec<u8>> { + let mut key_item = Item::make_empty(); + secstatus_to_res(unsafe { + PK11_ReadRawAttribute( + PK11ObjectType::PK11_TypePrivKey, + (**self).cast(), + CK_ATTRIBUTE_TYPE::from(CKA_VALUE), + &mut key_item, + ) + })?; + let slc = unsafe { + std::slice::from_raw_parts(key_item.data, usize::try_from(key_item.len).unwrap()) + }; + let key = Vec::from(slc); + // The data that `key_item` refers to needs to be freed, but we can't + // use the scoped `Item` implementation. This is OK as long as nothing + // panics between `PK11_ReadRawAttribute` succeeding and here. + unsafe { + SECITEM_FreeItem(&mut key_item, PRBool::from(false)); + } + Ok(key) + } +} +unsafe impl Send for PrivateKey {} + +impl Clone for PrivateKey { + #[must_use] + fn clone(&self) -> Self { + let ptr = unsafe { SECKEY_CopyPrivateKey(self.ptr) }; + assert!(!ptr.is_null()); + Self { ptr } + } +} + +impl std::fmt::Debug for PrivateKey { + fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { + if let Ok(b) = self.key_data() { + write!(f, "PrivateKey {}", hex_with_len(b)) + } else { + write!(f, "Opaque PrivateKey") + } + } +} + +scoped_ptr!(Slot, PK11SlotInfo, PK11_FreeSlot); + +impl Slot { + pub fn internal() -> Res<Self> { + let p = unsafe { PK11_GetInternalSlot() }; + Slot::from_ptr(p) + } +} + +scoped_ptr!(SymKey, PK11SymKey, PK11_FreeSymKey); + +impl SymKey { + /// You really don't want to use this. + /// + /// # Errors + /// Internal errors in case of failures in NSS. + pub fn as_bytes(&self) -> Res<&[u8]> { + secstatus_to_res(unsafe { PK11_ExtractKeyValue(self.ptr) })?; + + let key_item = unsafe { PK11_GetKeyData(self.ptr) }; + // This is accessing a value attached to the key, so we can treat this as a borrow. + match unsafe { key_item.as_mut() } { + None => Err(Error::InternalError), + Some(key) => Ok(unsafe { std::slice::from_raw_parts(key.data, key.len as usize) }), + } + } +} + +impl Clone for SymKey { + #[must_use] + fn clone(&self) -> Self { + let ptr = unsafe { PK11_ReferenceSymKey(self.ptr) }; + assert!(!ptr.is_null()); + Self { ptr } + } +} + +impl std::fmt::Debug for SymKey { + fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { + if let Ok(b) = self.as_bytes() { + write!(f, "SymKey {}", hex_with_len(b)) + } else { + write!(f, "Opaque SymKey") + } + } +} + +unsafe fn destroy_pk11_context(ctxt: *mut PK11Context) { + PK11_DestroyContext(ctxt, PRBool::from(true)); +} +scoped_ptr!(Context, PK11Context, destroy_pk11_context); + +unsafe fn destroy_secitem(item: *mut SECItem) { + SECITEM_FreeItem(item, PRBool::from(true)); +} +scoped_ptr!(Item, SECItem, destroy_secitem); + +impl Item { + /// Create a wrapper for a slice of this object. + /// Creating this object is technically safe, but using it is extremely dangerous. + /// Minimally, it can only be passed as a `const SECItem*` argument to functions, + /// or those that treat their argument as `const`. + pub fn wrap(buf: &[u8]) -> SECItem { + SECItem { + type_: SECItemType::siBuffer, + data: buf.as_ptr() as *mut u8, + len: c_uint::try_from(buf.len()).unwrap(), + } + } + + /// Create a wrapper for a struct. + /// Creating this object is technically safe, but using it is extremely dangerous. + /// Minimally, it can only be passed as a `const SECItem*` argument to functions, + /// or those that treat their argument as `const`. + pub fn wrap_struct<T>(v: &T) -> SECItem { + SECItem { + type_: SECItemType::siBuffer, + data: (v as *const T as *mut T).cast(), + len: c_uint::try_from(mem::size_of::<T>()).unwrap(), + } + } + + /// Make an empty `SECItem` for passing as a mutable `SECItem*` argument. + pub fn make_empty() -> SECItem { + SECItem { + type_: SECItemType::siBuffer, + data: null_mut(), + len: 0, + } + } + + /// This dereferences the pointer held by the item and makes a copy of the + /// content that is referenced there. + /// + /// # Safety + /// This dereferences two pointers. It doesn't get much less safe. + pub unsafe fn into_vec(self) -> Vec<u8> { + let b = self.ptr.as_ref().unwrap(); + // Sanity check the type, as some types don't count bytes in `Item::len`. + assert_eq!(b.type_, SECItemType::siBuffer); + let slc = std::slice::from_raw_parts(b.data, usize::try_from(b.len).unwrap()); + Vec::from(slc) + } +} + +/// Generate a randomized buffer. +/// # Panics +/// When `size` is too large or NSS fails. +#[must_use] +pub fn random(size: usize) -> Vec<u8> { + let mut buf = vec![0; size]; + secstatus_to_res(unsafe { + PK11_GenerateRandom(buf.as_mut_ptr(), c_int::try_from(buf.len()).unwrap()) + }) + .unwrap(); + buf +} + +#[cfg(test)] +mod test { + use super::random; + use test_fixture::fixture_init; + + #[test] + fn randomness() { + fixture_init(); + // If this ever fails, there is either a bug, or it's time to buy a lottery ticket. + assert_ne!(random(16), random(16)); + } +} diff --git a/third_party/rust/neqo-crypto/src/prio.rs b/third_party/rust/neqo-crypto/src/prio.rs new file mode 100644 index 0000000000..527d8739c8 --- /dev/null +++ b/third_party/rust/neqo-crypto/src/prio.rs @@ -0,0 +1,25 @@ +// Licensed under the Apache License, Version 2.0 <LICENSE-APACHE or +// http://www.apache.org/licenses/LICENSE-2.0> or the MIT license +// <LICENSE-MIT or http://opensource.org/licenses/MIT>, at your +// option. This file may not be copied, modified, or distributed +// except according to those terms. + +#![allow(clippy::upper_case_acronyms)] +#![allow( + dead_code, + non_upper_case_globals, + non_snake_case, + clippy::cognitive_complexity, + clippy::empty_enum, + clippy::too_many_lines, + unknown_lints, + clippy::borrow_as_ptr +)] + +include!(concat!(env!("OUT_DIR"), "/nspr_io.rs")); + +pub enum PRFileInfo {} +pub enum PRFileInfo64 {} +pub enum PRFilePrivate {} +pub enum PRIOVec {} +pub enum PRSendFileData {} diff --git a/third_party/rust/neqo-crypto/src/replay.rs b/third_party/rust/neqo-crypto/src/replay.rs new file mode 100644 index 0000000000..7a493fbc1b --- /dev/null +++ b/third_party/rust/neqo-crypto/src/replay.rs @@ -0,0 +1,78 @@ +// Licensed under the Apache License, Version 2.0 <LICENSE-APACHE or +// http://www.apache.org/licenses/LICENSE-2.0> or the MIT license +// <LICENSE-MIT or http://opensource.org/licenses/MIT>, at your +// option. This file may not be copied, modified, or distributed +// except according to those terms. + +use crate::err::Res; +use crate::ssl::PRFileDesc; +use crate::time::{Interval, PRTime, Time}; + +use std::convert::{TryFrom, TryInto}; +use std::ops::{Deref, DerefMut}; +use std::os::raw::c_uint; +use std::ptr::null_mut; +use std::time::{Duration, Instant}; + +// This is an opaque struct in NSS. +#[allow(clippy::upper_case_acronyms)] +#[allow(clippy::empty_enum)] +pub enum SSLAntiReplayContext {} + +experimental_api!(SSL_CreateAntiReplayContext( + now: PRTime, + window: PRTime, + k: c_uint, + bits: c_uint, + ctx: *mut *mut SSLAntiReplayContext, +)); +experimental_api!(SSL_ReleaseAntiReplayContext(ctx: *mut SSLAntiReplayContext)); +experimental_api!(SSL_SetAntiReplayContext( + fd: *mut PRFileDesc, + ctx: *mut SSLAntiReplayContext, +)); + +scoped_ptr!( + AntiReplayContext, + SSLAntiReplayContext, + SSL_ReleaseAntiReplayContext +); + +/// `AntiReplay` is used by servers when processing 0-RTT handshakes. +/// It limits the exposure of servers to replay attack by rejecting 0-RTT +/// if it appears to be a replay. There is a false-positive rate that can be +/// managed by tuning the parameters used to create the context. +#[allow(clippy::module_name_repetitions)] +pub struct AntiReplay { + ctx: AntiReplayContext, +} + +impl AntiReplay { + /// Make a new anti-replay context. + /// See the documentation in NSS for advice on how to set these values. + /// + /// # Errors + /// Returns an error if `now` is in the past relative to our baseline or + /// NSS is unable to generate an anti-replay context. + pub fn new(now: Instant, window: Duration, k: usize, bits: usize) -> Res<Self> { + let mut ctx: *mut SSLAntiReplayContext = null_mut(); + unsafe { + SSL_CreateAntiReplayContext( + Time::from(now).try_into()?, + Interval::from(window).try_into()?, + c_uint::try_from(k)?, + c_uint::try_from(bits)?, + &mut ctx, + ) + }?; + + Ok(Self { + ctx: AntiReplayContext::from_ptr(ctx)?, + }) + } + + /// Configure the provided socket with this anti-replay context. + pub(crate) fn config_socket(&self, fd: *mut PRFileDesc) -> Res<()> { + unsafe { SSL_SetAntiReplayContext(fd, *self.ctx) } + } +} diff --git a/third_party/rust/neqo-crypto/src/result.rs b/third_party/rust/neqo-crypto/src/result.rs new file mode 100644 index 0000000000..e6148dd054 --- /dev/null +++ b/third_party/rust/neqo-crypto/src/result.rs @@ -0,0 +1,133 @@ +// Licensed under the Apache License, Version 2.0 <LICENSE-APACHE or +// http://www.apache.org/licenses/LICENSE-2.0> or the MIT license +// <LICENSE-MIT or http://opensource.org/licenses/MIT>, at your +// option. This file may not be copied, modified, or distributed +// except according to those terms. + +use crate::err::{ + nspr, Error, PR_ErrorToName, PR_ErrorToString, PR_GetError, Res, PR_LANGUAGE_I_DEFAULT, +}; +use crate::ssl; + +use std::ffi::CStr; + +pub fn result(rv: ssl::SECStatus) -> Res<()> { + let _ = result_helper(rv, false)?; + Ok(()) +} + +pub fn result_or_blocked(rv: ssl::SECStatus) -> Res<bool> { + result_helper(rv, true) +} + +fn wrap_str_fn<F>(f: F, dflt: &str) -> String +where + F: FnOnce() -> *const i8, +{ + unsafe { + let p = f(); + if p.is_null() { + return dflt.to_string(); + } + CStr::from_ptr(p).to_string_lossy().into_owned() + } +} + +fn result_helper(rv: ssl::SECStatus, allow_blocked: bool) -> Res<bool> { + if rv == ssl::_SECStatus_SECSuccess { + return Ok(false); + } + + let code = unsafe { PR_GetError() }; + if allow_blocked && code == nspr::PR_WOULD_BLOCK_ERROR { + return Ok(true); + } + + let name = wrap_str_fn(|| unsafe { PR_ErrorToName(code) }, "UNKNOWN_ERROR"); + let desc = wrap_str_fn( + || unsafe { PR_ErrorToString(code, PR_LANGUAGE_I_DEFAULT) }, + "...", + ); + Err(Error::NssError { name, code, desc }) +} + +#[cfg(test)] +mod tests { + use super::{result, result_or_blocked}; + use crate::err::{self, nspr, Error, PRErrorCode, PR_SetError}; + use crate::ssl; + use test_fixture::fixture_init; + + fn set_error_code(code: PRErrorCode) { + unsafe { PR_SetError(code, 0) }; + } + + #[test] + fn is_ok() { + assert!(result(ssl::SECSuccess).is_ok()); + } + + #[test] + fn is_err() { + // This code doesn't work without initializing NSS first. + fixture_init(); + + set_error_code(err::ssl::SSL_ERROR_BAD_MAC_READ); + let r = result(ssl::SECFailure); + assert!(r.is_err()); + match r.unwrap_err() { + Error::NssError { name, code, desc } => { + assert_eq!(name, "SSL_ERROR_BAD_MAC_READ"); + assert_eq!(code, -12273); + assert_eq!( + desc, + "SSL received a record with an incorrect Message Authentication Code." + ); + } + _ => unreachable!(), + } + } + + #[test] + fn is_err_zero_code() { + // This code doesn't work without initializing NSS first. + fixture_init(); + + set_error_code(0); + let r = result(ssl::SECFailure); + assert!(r.is_err()); + match r.unwrap_err() { + Error::NssError { name, code, .. } => { + assert_eq!(name, "UNKNOWN_ERROR"); + assert_eq!(code, 0); + // Note that we don't test |desc| here because that comes from + // strerror(0), which is platform-dependent. + } + _ => unreachable!(), + } + } + + #[test] + fn blocked_as_error() { + // This code doesn't work without initializing NSS first. + fixture_init(); + + set_error_code(nspr::PR_WOULD_BLOCK_ERROR); + let r = result(ssl::SECFailure); + assert!(r.is_err()); + match r.unwrap_err() { + Error::NssError { name, code, desc } => { + assert_eq!(name, "PR_WOULD_BLOCK_ERROR"); + assert_eq!(code, -5998); + assert_eq!(desc, "The operation would have blocked"); + } + _ => panic!("bad error type"), + } + } + + #[test] + fn is_blocked() { + set_error_code(nspr::PR_WOULD_BLOCK_ERROR); + assert!(result_or_blocked(ssl::SECFailure).unwrap()); + } +} diff --git a/third_party/rust/neqo-crypto/src/secrets.rs b/third_party/rust/neqo-crypto/src/secrets.rs new file mode 100644 index 0000000000..7e2739cfa5 --- /dev/null +++ b/third_party/rust/neqo-crypto/src/secrets.rs @@ -0,0 +1,127 @@ +// Licensed under the Apache License, Version 2.0 <LICENSE-APACHE or +// http://www.apache.org/licenses/LICENSE-2.0> or the MIT license +// <LICENSE-MIT or http://opensource.org/licenses/MIT>, at your +// option. This file may not be copied, modified, or distributed +// except according to those terms. + +use crate::agentio::as_c_void; +use crate::constants::Epoch; +use crate::err::Res; +use crate::p11::{PK11SymKey, PK11_ReferenceSymKey, SymKey}; +use crate::ssl::{PRFileDesc, SSLSecretCallback, SSLSecretDirection}; + +use neqo_common::qdebug; +use std::os::raw::c_void; +use std::pin::Pin; + +experimental_api!(SSL_SecretCallback( + fd: *mut PRFileDesc, + cb: SSLSecretCallback, + arg: *mut c_void, +)); + +#[derive(Clone, Copy, Debug)] +pub enum SecretDirection { + Read, + Write, +} + +impl From<SSLSecretDirection::Type> for SecretDirection { + #[must_use] + fn from(dir: SSLSecretDirection::Type) -> Self { + match dir { + SSLSecretDirection::ssl_secret_read => Self::Read, + SSLSecretDirection::ssl_secret_write => Self::Write, + _ => unreachable!(), + } + } +} + +#[derive(Debug, Default)] +#[allow(clippy::module_name_repetitions)] +pub struct DirectionalSecrets { + // We only need to maintain 3 secrets for the epochs used during the handshake. + secrets: [Option<SymKey>; 3], +} + +impl DirectionalSecrets { + fn put(&mut self, epoch: Epoch, key: SymKey) { + assert!(epoch > 0); + let i = (epoch - 1) as usize; + assert!(i < self.secrets.len()); + // assert!(self.secrets[i].is_none()); + self.secrets[i] = Some(key); + } + + pub fn take(&mut self, epoch: Epoch) -> Option<SymKey> { + assert!(epoch > 0); + let i = (epoch - 1) as usize; + assert!(i < self.secrets.len()); + self.secrets[i].take() + } +} + +#[derive(Debug, Default)] +pub struct Secrets { + r: DirectionalSecrets, + w: DirectionalSecrets, +} + +impl Secrets { + #[allow(clippy::unused_self)] + unsafe extern "C" fn secret_available( + _fd: *mut PRFileDesc, + epoch: u16, + dir: SSLSecretDirection::Type, + secret: *mut PK11SymKey, + arg: *mut c_void, + ) { + let secrets = arg.cast::<Self>().as_mut().unwrap(); + secrets.put_raw(epoch, dir, secret); + } + + fn put_raw(&mut self, epoch: Epoch, dir: SSLSecretDirection::Type, key_ptr: *mut PK11SymKey) { + let key_ptr = unsafe { PK11_ReferenceSymKey(key_ptr) }; + let key = SymKey::from_ptr(key_ptr).expect("NSS shouldn't be passing out NULL secrets"); + self.put(SecretDirection::from(dir), epoch, key); + } + + fn put(&mut self, dir: SecretDirection, epoch: Epoch, key: SymKey) { + qdebug!("{:?} secret available for {:?}: {:?}", dir, epoch, key); + let keys = match dir { + SecretDirection::Read => &mut self.r, + SecretDirection::Write => &mut self.w, + }; + keys.put(epoch, key); + } +} + +#[derive(Debug)] +pub struct SecretHolder { + secrets: Pin<Box<Secrets>>, +} + +impl SecretHolder { + /// This registers with NSS. The lifetime of this object needs to match the lifetime + /// of the connection, or bad things might happen. + pub fn register(&mut self, fd: *mut PRFileDesc) -> Res<()> { + let p = as_c_void(&mut self.secrets); + unsafe { SSL_SecretCallback(fd, Some(Secrets::secret_available), p) } + } + + pub fn take_read(&mut self, epoch: Epoch) -> Option<SymKey> { + self.secrets.r.take(epoch) + } + + pub fn take_write(&mut self, epoch: Epoch) -> Option<SymKey> { + self.secrets.w.take(epoch) + } +} + +impl Default for SecretHolder { + fn default() -> Self { + Self { + secrets: Box::pin(Secrets::default()), + } + } +} diff --git a/third_party/rust/neqo-crypto/src/selfencrypt.rs b/third_party/rust/neqo-crypto/src/selfencrypt.rs new file mode 100644 index 0000000000..bcf219a4e9 --- /dev/null +++ b/third_party/rust/neqo-crypto/src/selfencrypt.rs @@ -0,0 +1,155 @@ +// Licensed under the Apache License, Version 2.0 <LICENSE-APACHE or +// http://www.apache.org/licenses/LICENSE-2.0> or the MIT license +// <LICENSE-MIT or http://opensource.org/licenses/MIT>, at your +// option. This file may not be copied, modified, or distributed +// except according to those terms. + +use crate::constants::{Cipher, Version}; +use crate::err::{Error, Res}; +use crate::p11::{random, SymKey}; +use crate::{hkdf, Aead}; + +use neqo_common::{hex, qinfo, qtrace, Encoder}; + +use std::mem; + +#[derive(Debug)] +pub struct SelfEncrypt { + version: Version, + cipher: Cipher, + key_id: u8, + key: SymKey, + old_key: Option<SymKey>, +} + +impl SelfEncrypt { + const VERSION: u8 = 1; + const SALT_LENGTH: usize = 16; + + /// # Errors + /// Failure to generate a new HKDF key using NSS results in an error. + pub fn new(version: Version, cipher: Cipher) -> Res<Self> { + let key = hkdf::generate_key(version, cipher)?; + Ok(Self { + version, + cipher, + key_id: 0, + key, + old_key: None, + }) + } + + fn make_aead(&self, k: &SymKey, salt: &[u8]) -> Res<Aead> { + debug_assert_eq!(salt.len(), Self::SALT_LENGTH); + let salt = hkdf::import_key(self.version, salt)?; + let secret = hkdf::extract(self.version, self.cipher, Some(&salt), k)?; + Aead::new(self.version, self.cipher, &secret, "neqo self") + } + + /// Rotate keys. This causes any previous key that is being held to be replaced by the current key. + /// + /// # Errors + /// Failure to generate a new HKDF key using NSS results in an error. + pub fn rotate(&mut self) -> Res<()> { + let new_key = hkdf::generate_key(self.version, self.cipher)?; + self.old_key = Some(mem::replace(&mut self.key, new_key)); + let (kid, _) = self.key_id.overflowing_add(1); + self.key_id = kid; + qinfo!(["SelfEncrypt"], "Rotated keys to {}", self.key_id); + Ok(()) + } + + /// Seal an item using the underlying key. This produces a single buffer that contains + /// the encrypted `plaintext`, plus a version number and salt. + /// `aad` is only used as input to the AEAD, it is not included in the output; the + /// caller is responsible for carrying the AAD as appropriate. + /// + /// # Errors + /// Failure to protect using NSS AEAD APIs produces an error. + pub fn seal(&self, aad: &[u8], plaintext: &[u8]) -> Res<Vec<u8>> { + // Format is: + // struct { + // uint8 version; + // uint8 key_id; + // uint8 salt[16]; + // opaque aead_encrypted(plaintext)[length as expanded]; + // }; + // AAD covers the entire header, plus the value of the AAD parameter that is provided. + let salt = random(Self::SALT_LENGTH); + let cipher = self.make_aead(&self.key, &salt)?; + let encoded_len = 2 + salt.len() + plaintext.len() + cipher.expansion(); + + let mut enc = Encoder::with_capacity(encoded_len); + enc.encode_byte(Self::VERSION); + enc.encode_byte(self.key_id); + enc.encode(&salt); + + let mut extended_aad = enc.clone(); + extended_aad.encode(aad); + + let offset = enc.len(); + let mut output: Vec<u8> = enc.into(); + output.resize(encoded_len, 0); + cipher.encrypt(0, extended_aad.as_ref(), plaintext, &mut output[offset..])?; + qtrace!( + ["SelfEncrypt"], + "seal {} {} -> {}", + hex(aad), + hex(plaintext), + hex(&output) + ); + Ok(output) + } + + fn select_key(&self, kid: u8) -> Option<&SymKey> { + if kid == self.key_id { + Some(&self.key) + } else { + let (prev_key_id, _) = self.key_id.overflowing_sub(1); + if kid == prev_key_id { + self.old_key.as_ref() + } else { + None + } + } + } + + /// Open the protected `ciphertext`. + /// + /// # Errors + /// Returns an error when the self-encrypted object is invalid; + /// when the keys have been rotated; or when NSS fails. + #[allow(clippy::similar_names)] // aad is similar to aead + pub fn open(&self, aad: &[u8], ciphertext: &[u8]) -> Res<Vec<u8>> { + if ciphertext[0] != Self::VERSION { + return Err(Error::SelfEncryptFailure); + } + let key = if let Some(k) = self.select_key(ciphertext[1]) { + k + } else { + return Err(Error::SelfEncryptFailure); + }; + let offset = 2 + Self::SALT_LENGTH; + + let mut extended_aad = Encoder::with_capacity(offset + aad.len()); + extended_aad.encode(&ciphertext[0..offset]); + extended_aad.encode(aad); + + let aead = self.make_aead(key, &ciphertext[2..offset])?; + // NSS insists on having extra space available for decryption. + let padded_len = ciphertext.len() - offset; + let mut output = vec![0; padded_len]; + let decrypted = + aead.decrypt(0, extended_aad.as_ref(), &ciphertext[offset..], &mut output)?; + let final_len = decrypted.len(); + output.truncate(final_len); + qtrace!( + ["SelfEncrypt"], + "open {} {} -> {}", + hex(aad), + hex(ciphertext), + hex(&output) + ); + Ok(output) + } +} diff --git a/third_party/rust/neqo-crypto/src/ssl.rs b/third_party/rust/neqo-crypto/src/ssl.rs new file mode 100644 index 0000000000..b3c0c12708 --- /dev/null +++ b/third_party/rust/neqo-crypto/src/ssl.rs @@ -0,0 +1,149 @@ +// Licensed under the Apache License, Version 2.0 <LICENSE-APACHE or +// http://www.apache.org/licenses/LICENSE-2.0> or the MIT license +// <LICENSE-MIT or http://opensource.org/licenses/MIT>, at your +// option. This file may not be copied, modified, or distributed +// except according to those terms. + +#![allow( + dead_code, + non_upper_case_globals, + non_snake_case, + clippy::cognitive_complexity, + clippy::too_many_lines, + clippy::upper_case_acronyms, + unknown_lints, + clippy::borrow_as_ptr +)] + +use crate::constants::Epoch; +use crate::err::{secstatus_to_res, Res}; + +use std::os::raw::{c_uint, c_void}; + +include!(concat!(env!("OUT_DIR"), "/nss_ssl.rs")); +mod SSLOption { + include!(concat!(env!("OUT_DIR"), "/nss_sslopt.rs")); +} + +// I clearly don't understand how bindgen operates. +#[allow(clippy::empty_enum)] +pub enum PLArenaPool {} +#[allow(clippy::empty_enum)] +pub enum PRFileDesc {} + +// Remap some constants. +pub const SECSuccess: SECStatus = _SECStatus_SECSuccess; +pub const SECFailure: SECStatus = _SECStatus_SECFailure; + +#[derive(Debug, Copy, Clone)] +pub enum Opt { + Locking, + Tickets, + OcspStapling, + Alpn, + ExtendedMasterSecret, + SignedCertificateTimestamps, + EarlyData, + RecordSizeLimit, + Tls13CompatMode, + HelloDowngradeCheck, + SuppressEndOfEarlyData, +} + +impl Opt { + // Cast is safe here because SSLOptions are within the i32 range + #[allow(clippy::cast_possible_wrap)] + pub(crate) fn as_int(self) -> PRInt32 { + let i = match self { + Self::Locking => SSLOption::SSL_NO_LOCKS, + Self::Tickets => SSLOption::SSL_ENABLE_SESSION_TICKETS, + Self::OcspStapling => SSLOption::SSL_ENABLE_OCSP_STAPLING, + Self::Alpn => SSLOption::SSL_ENABLE_ALPN, + Self::ExtendedMasterSecret => SSLOption::SSL_ENABLE_EXTENDED_MASTER_SECRET, + Self::SignedCertificateTimestamps => SSLOption::SSL_ENABLE_SIGNED_CERT_TIMESTAMPS, + Self::EarlyData => SSLOption::SSL_ENABLE_0RTT_DATA, + Self::RecordSizeLimit => SSLOption::SSL_RECORD_SIZE_LIMIT, + Self::Tls13CompatMode => SSLOption::SSL_ENABLE_TLS13_COMPAT_MODE, + Self::HelloDowngradeCheck => SSLOption::SSL_ENABLE_HELLO_DOWNGRADE_CHECK, + Self::SuppressEndOfEarlyData => SSLOption::SSL_SUPPRESS_END_OF_EARLY_DATA, + }; + i as PRInt32 + } + + // Some options are backwards, like SSL_NO_LOCKS, so use this to manage that. + fn map_enabled(self, enabled: bool) -> PRIntn { + let v = match self { + Self::Locking => !enabled, + _ => enabled, + }; + PRIntn::from(v) + } + + pub(crate) fn set(self, fd: *mut PRFileDesc, value: bool) -> Res<()> { + secstatus_to_res(unsafe { SSL_OptionSet(fd, self.as_int(), self.map_enabled(value)) }) + } +} + +experimental_api!(SSL_GetCurrentEpoch( + fd: *mut PRFileDesc, + read_epoch: *mut u16, + write_epoch: *mut u16, +)); +experimental_api!(SSL_HelloRetryRequestCallback( + fd: *mut PRFileDesc, + cb: SSLHelloRetryRequestCallback, + arg: *mut c_void, +)); +experimental_api!(SSL_RecordLayerWriteCallback( + fd: *mut PRFileDesc, + cb: SSLRecordWriteCallback, + arg: *mut c_void, +)); +experimental_api!(SSL_RecordLayerData( + fd: *mut PRFileDesc, + epoch: Epoch, + ct: SSLContentType::Type, + data: *const u8, + len: c_uint, +)); +experimental_api!(SSL_SendSessionTicket( + fd: *mut PRFileDesc, + extra: *const u8, + len: c_uint, +)); +experimental_api!(SSL_SetMaxEarlyDataSize(fd: *mut PRFileDesc, size: u32)); +experimental_api!(SSL_SetResumptionToken( + fd: *mut PRFileDesc, + token: *const u8, + len: c_uint, +)); +experimental_api!(SSL_SetResumptionTokenCallback( + fd: *mut PRFileDesc, + cb: SSLResumptionTokenCallback, + arg: *mut c_void, +)); + +experimental_api!(SSL_GetResumptionTokenInfo( + token: *const u8, + token_len: c_uint, + info: *mut SSLResumptionTokenInfo, + len: c_uint, +)); + +experimental_api!(SSL_DestroyResumptionTokenInfo( + info: *mut SSLResumptionTokenInfo, +)); + +#[cfg(test)] +mod tests { + use super::{SSL_GetNumImplementedCiphers, SSL_NumImplementedCiphers}; + + #[test] + fn num_ciphers() { + assert!(unsafe { SSL_NumImplementedCiphers } > 0); + assert!(unsafe { SSL_GetNumImplementedCiphers() } > 0); + assert_eq!(unsafe { SSL_NumImplementedCiphers }, unsafe { + SSL_GetNumImplementedCiphers() + }); + } +} diff --git a/third_party/rust/neqo-crypto/src/time.rs b/third_party/rust/neqo-crypto/src/time.rs new file mode 100644 index 0000000000..d3364b7f72 --- /dev/null +++ b/third_party/rust/neqo-crypto/src/time.rs @@ -0,0 +1,252 @@ +// Licensed under the Apache License, Version 2.0 <LICENSE-APACHE or +// http://www.apache.org/licenses/LICENSE-2.0> or the MIT license +// <LICENSE-MIT or http://opensource.org/licenses/MIT>, at your +// option. This file may not be copied, modified, or distributed +// except according to those terms. + +#![allow(clippy::upper_case_acronyms)] + +use crate::agentio::as_c_void; +use crate::err::{Error, Res}; +use crate::once::OnceResult; +use crate::ssl::{PRFileDesc, SSLTimeFunc}; + +use std::boxed::Box; +use std::convert::{TryFrom, TryInto}; +use std::ops::Deref; +use std::os::raw::c_void; +use std::pin::Pin; +use std::time::{Duration, Instant}; + +include!(concat!(env!("OUT_DIR"), "/nspr_time.rs")); + +experimental_api!(SSL_SetTimeFunc( + fd: *mut PRFileDesc, + cb: SSLTimeFunc, + arg: *mut c_void, +)); + +/// This struct holds the zero time used for converting between `Instant` and `PRTime`. +#[derive(Debug)] +struct TimeZero { + instant: Instant, + prtime: PRTime, +} + +impl TimeZero { + /// This function sets a baseline from an instance of `Instant`. + /// This allows for the possibility that code that uses these APIs will create + /// instances of `Instant` before any of this code is run. If `Instant`s older than + /// `BASE_TIME` are used with these conversion functions, they will fail. + /// To avoid that, we make sure that this sets the base time using the first value + /// it sees if it is in the past. If it is not, then use `Instant::now()` instead. + pub fn baseline(t: Instant) -> Self { + let now = Instant::now(); + let prnow = unsafe { PR_Now() }; + + if now <= t { + // `t` is in the future, just use `now`. + Self { + instant: now, + prtime: prnow, + } + } else { + let elapsed = Interval::from(now.duration_since(now)); + // An error from these unwrap functions would require + // ridiculously long application running time. + let prelapsed: PRTime = elapsed.try_into().unwrap(); + Self { + instant: t, + prtime: prnow.checked_sub(prelapsed).unwrap(), + } + } + } +} + +static mut BASE_TIME: OnceResult<TimeZero> = OnceResult::new(); + +fn get_base() -> &'static TimeZero { + let f = || TimeZero { + instant: Instant::now(), + prtime: unsafe { PR_Now() }, + }; + unsafe { BASE_TIME.call_once(f) } +} + +pub(crate) fn init() { + let _ = get_base(); +} + +/// Time wraps Instant and provides conversion functions into `PRTime`. +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub struct Time { + t: Instant, +} + +impl Deref for Time { + type Target = Instant; + fn deref(&self) -> &Self::Target { + &self.t + } +} + +impl From<Instant> for Time { + /// Convert from an Instant into a Time. + fn from(t: Instant) -> Self { + // Call `TimeZero::baseline(t)` so that time zero can be set. + let f = || TimeZero::baseline(t); + let _ = unsafe { BASE_TIME.call_once(f) }; + Self { t } + } +} + +impl TryFrom<PRTime> for Time { + type Error = Error; + fn try_from(prtime: PRTime) -> Res<Self> { + let base = get_base(); + if let Some(delta) = prtime.checked_sub(base.prtime) { + let d = Duration::from_micros(delta.try_into()?); + base.instant + .checked_add(d) + .map_or(Err(Error::TimeTravelError), |t| Ok(Self { t })) + } else { + Err(Error::TimeTravelError) + } + } +} + +impl TryInto<PRTime> for Time { + type Error = Error; + fn try_into(self) -> Res<PRTime> { + let base = get_base(); + let delta = self + .t + .checked_duration_since(base.instant) + .ok_or(Error::TimeTravelError)?; + if let Ok(d) = PRTime::try_from(delta.as_micros()) { + d.checked_add(base.prtime).ok_or(Error::TimeTravelError) + } else { + Err(Error::TimeTravelError) + } + } +} + +impl From<Time> for Instant { + #[must_use] + fn from(t: Time) -> Self { + t.t + } +} + +/// Interval wraps Duration and provides conversion functions into `PRTime`. +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub struct Interval { + d: Duration, +} + +impl Deref for Interval { + type Target = Duration; + fn deref(&self) -> &Self::Target { + &self.d + } +} + +impl TryFrom<PRTime> for Interval { + type Error = Error; + fn try_from(prtime: PRTime) -> Res<Self> { + Ok(Self { + d: Duration::from_micros(u64::try_from(prtime)?), + }) + } +} + +impl From<Duration> for Interval { + fn from(d: Duration) -> Self { + Self { d } + } +} + +impl TryInto<PRTime> for Interval { + type Error = Error; + fn try_into(self) -> Res<PRTime> { + Ok(PRTime::try_from(self.d.as_micros())?) + } +} + +/// `TimeHolder` maintains a `PRTime` value in a form that is accessible to the TLS stack. +#[derive(Debug)] +pub struct TimeHolder { + t: Pin<Box<PRTime>>, +} + +impl TimeHolder { + unsafe extern "C" fn time_func(arg: *mut c_void) -> PRTime { + let p = arg as *const PRTime; + *p.as_ref().unwrap() + } + + pub fn bind(&mut self, fd: *mut PRFileDesc) -> Res<()> { + unsafe { SSL_SetTimeFunc(fd, Some(Self::time_func), as_c_void(&mut self.t)) } + } + + pub fn set(&mut self, t: Instant) -> Res<()> { + *self.t = Time::from(t).try_into()?; + Ok(()) + } +} + +impl Default for TimeHolder { + fn default() -> Self { + TimeHolder { t: Box::pin(0) } + } +} + +#[cfg(test)] +mod test { + use super::{get_base, init, Interval, PRTime, Time}; + use crate::err::Res; + use std::convert::{TryFrom, TryInto}; + use std::time::{Duration, Instant}; + + #[test] + fn convert_stable() { + init(); + let now = Time::from(Instant::now()); + let pr: PRTime = now.try_into().expect("convert to PRTime with truncation"); + let t2 = Time::try_from(pr).expect("convert to Instant"); + let pr2: PRTime = t2.try_into().expect("convert to PRTime again"); + assert_eq!(pr, pr2); + let t3 = Time::try_from(pr2).expect("convert to Instant again"); + assert_eq!(t2, t3); + } + + #[test] + fn past_time() { + init(); + let base = get_base(); + assert!(Time::try_from(base.prtime - 1).is_err()); + } + + #[test] + fn negative_time() { + init(); + assert!(Time::try_from(-1).is_err()); + } + + #[test] + fn negative_interval() { + init(); + assert!(Interval::try_from(-1).is_err()); + } + + #[test] + // We allow replace_consts here because + // std::u64::max_value() isn't available + // in all of our targets + fn overflow_interval() { + init(); + let interval = Interval::from(Duration::from_micros(u64::max_value())); + let res: Res<PRTime> = interval.try_into(); + assert!(res.is_err()); + } +} diff --git a/third_party/rust/neqo-crypto/tests/aead.rs b/third_party/rust/neqo-crypto/tests/aead.rs new file mode 100644 index 0000000000..0fef623ae6 --- /dev/null +++ b/third_party/rust/neqo-crypto/tests/aead.rs @@ -0,0 +1,116 @@ +#![cfg_attr(feature = "deny-warnings", deny(warnings))] +#![warn(clippy::pedantic)] +#![cfg(not(feature = "fuzzing"))] + +use neqo_crypto::constants::{Cipher, TLS_AES_128_GCM_SHA256, TLS_VERSION_1_3}; +use neqo_crypto::hkdf; +use neqo_crypto::Aead; +use test_fixture::fixture_init; + +const AAD: &[u8] = &[ + 0xc1, 0xff, 0x00, 0x00, 0x12, 0x05, 0xf0, 0x67, 0xa5, 0x50, 0x2a, 0x42, 0x62, 0xb5, 0x00, 0x40, + 0x74, 0x00, 0x01, +]; +const PLAINTEXT: &[u8] = &[ + 0x0d, 0x00, 0x00, 0x00, 0x00, 0x18, 0x41, 0x0a, 0x02, 0x00, 0x00, 0x56, 0x03, 0x03, 0xee, 0xfc, + 0xe7, 0xf7, 0xb3, 0x7b, 0xa1, 0xd1, 0x63, 0x2e, 0x96, 0x67, 0x78, 0x25, 0xdd, 0xf7, 0x39, 0x88, + 0xcf, 0xc7, 0x98, 0x25, 0xdf, 0x56, 0x6d, 0xc5, 0x43, 0x0b, 0x9a, 0x04, 0x5a, 0x12, 0x00, 0x13, + 0x01, 0x00, 0x00, 0x2e, 0x00, 0x33, 0x00, 0x24, 0x00, 0x1d, 0x00, 0x20, 0x9d, 0x3c, 0x94, 0x0d, + 0x89, 0x69, 0x0b, 0x84, 0xd0, 0x8a, 0x60, 0x99, 0x3c, 0x14, 0x4e, 0xca, 0x68, 0x4d, 0x10, 0x81, + 0x28, 0x7c, 0x83, 0x4d, 0x53, 0x11, 0xbc, 0xf3, 0x2b, 0xb9, 0xda, 0x1a, 0x00, 0x2b, 0x00, 0x02, + 0x03, 0x04, +]; + +fn make_aead(cipher: Cipher) -> Aead { + fixture_init(); + + let secret = hkdf::import_key( + TLS_VERSION_1_3, + &[ + 0x47, 0xb2, 0xea, 0xea, 0x6c, 0x26, 0x6e, 0x32, 0xc0, 0x69, 0x7a, 0x9e, 0x2a, 0x89, + 0x8b, 0xdf, 0x5c, 0x4f, 0xb3, 0xe5, 0xac, 0x34, 0xf0, 0xe5, 0x49, 0xbf, 0x2c, 0x58, + 0x58, 0x1a, 0x38, 0x11, + ], + ) + .expect("make a secret"); + Aead::new( + TLS_VERSION_1_3, + cipher, + &secret, + "quic ", // QUICv1 label prefix; note the trailing space here. + ) + .expect("can make an AEAD") +} + +#[test] +fn aead_encrypt_decrypt() { + const TOGGLE: u8 = 77; + let aead = make_aead(TLS_AES_128_GCM_SHA256); + let ciphertext_buf = &mut [0; 1024]; // Can't use PLAINTEXT.len() here. + let ciphertext = aead + .encrypt(1, AAD, PLAINTEXT, ciphertext_buf) + .expect("encrypt should work"); + let expected_ciphertext: &[u8] = &[ + 0x5f, 0x01, 0xc4, 0xc2, 0xa2, 0x30, 0x3d, 0x29, 0x7e, 0x3c, 0x51, 0x9b, 0xf6, 0xb2, 0x23, + 0x86, 0xe3, 0xd0, 0xbd, 0x6d, 0xfc, 0x66, 0x12, 0x16, 0x77, 0x29, 0x80, 0x31, 0x04, 0x1b, + 0xb9, 0xa7, 0x9c, 0x9f, 0x0f, 0x9d, 0x4c, 0x58, 0x77, 0x27, 0x0a, 0x66, 0x0f, 0x5d, 0xa3, + 0x62, 0x07, 0xd9, 0x8b, 0x73, 0x83, 0x9b, 0x2f, 0xdf, 0x2e, 0xf8, 0xe7, 0xdf, 0x5a, 0x51, + 0xb1, 0x7b, 0x8c, 0x68, 0xd8, 0x64, 0xfd, 0x3e, 0x70, 0x8c, 0x6c, 0x1b, 0x71, 0xa9, 0x8a, + 0x33, 0x18, 0x15, 0x59, 0x9e, 0xf5, 0x01, 0x4e, 0xa3, 0x8c, 0x44, 0xbd, 0xfd, 0x38, 0x7c, + 0x03, 0xb5, 0x27, 0x5c, 0x35, 0xe0, 0x09, 0xb6, 0x23, 0x8f, 0x83, 0x14, 0x20, 0x04, 0x7c, + 0x72, 0x71, 0x28, 0x1c, 0xcb, 0x54, 0xdf, 0x78, 0x84, + ]; + assert_eq!(ciphertext, expected_ciphertext); + + let plaintext_buf = &mut [0; 1024]; // Can't use PLAINTEXT.len() here. + let plaintext = aead + .decrypt(1, AAD, ciphertext, plaintext_buf) + .expect("decrypt should also work"); + assert_eq!(plaintext, PLAINTEXT); + + // Decryption failures... + // Different counter. + let res = aead.decrypt(2, AAD, ciphertext, plaintext_buf); + assert!(res.is_err()); + + // Front-truncate ciphertext. + let res = aead.decrypt(1, AAD, &ciphertext[1..], plaintext_buf); + assert!(res.is_err()); + + // End-truncate ciphertext. + let ciphertext_last = ciphertext.len() - 1; + let res = aead.decrypt(1, AAD, &ciphertext[..ciphertext_last], plaintext_buf); + assert!(res.is_err()); + + // Mess with the buffer. + let mut scratch = Vec::new(); + scratch.extend_from_slice(ciphertext); + + // Toggle first octet. + scratch[0] ^= TOGGLE; + let res = aead.decrypt(1, AAD, &scratch[..], plaintext_buf); + assert!(res.is_err()); + + // Toggle the auth tag. + scratch[0] ^= TOGGLE; + scratch[ciphertext_last] ^= TOGGLE; + let res = aead.decrypt(1, AAD, &scratch[..], plaintext_buf); + assert!(res.is_err()); + + // Mess with the AAD. + scratch.clear(); + scratch.extend_from_slice(AAD); + + // Front-truncate. + let res = aead.decrypt(1, &scratch[1..], ciphertext, plaintext_buf); + assert!(res.is_err()); + + // End-truncate. + let aad_last = AAD.len() - 1; + let res = aead.decrypt(1, &scratch[..aad_last], ciphertext, plaintext_buf); + assert!(res.is_err()); + + scratch[0] ^= TOGGLE; + let res = aead.decrypt(1, &scratch[..], ciphertext, plaintext_buf); + assert!(res.is_err()); +} diff --git a/third_party/rust/neqo-crypto/tests/agent.rs b/third_party/rust/neqo-crypto/tests/agent.rs new file mode 100644 index 0000000000..1a8ec449e4 --- /dev/null +++ b/third_party/rust/neqo-crypto/tests/agent.rs @@ -0,0 +1,474 @@ +#![cfg_attr(feature = "deny-warnings", deny(warnings))] +#![warn(clippy::pedantic)] + +use neqo_crypto::{ + generate_ech_keys, AuthenticationStatus, Client, Error, HandshakeState, SecretAgentPreInfo, + Server, ZeroRttCheckResult, ZeroRttChecker, TLS_AES_128_GCM_SHA256, + TLS_CHACHA20_POLY1305_SHA256, TLS_GRP_EC_SECP256R1, TLS_VERSION_1_3, +}; + +use std::boxed::Box; + +mod handshake; +use crate::handshake::{ + connect, connect_fail, forward_records, resumption_setup, PermissiveZeroRttChecker, Resumption, + ZERO_RTT_TOKEN_DATA, +}; +use test_fixture::{fixture_init, now}; + +#[test] +fn make_client() { + fixture_init(); + let _c = Client::new("server").expect("should create client"); +} + +#[test] +fn make_server() { + fixture_init(); + let _s = Server::new(&["key"]).expect("should create server"); +} + +#[test] +fn basic() { + fixture_init(); + let mut client = Client::new("server.example").expect("should create client"); + println!("client {:p}", &client); + let mut server = Server::new(&["key"]).expect("should create server"); + println!("server {:p}", &server); + + let bytes = client.handshake(now(), &[]).expect("send CH"); + assert!(!bytes.is_empty()); + assert_eq!(*client.state(), HandshakeState::InProgress); + + let bytes = server + .handshake(now(), &bytes[..]) + .expect("read CH, send SH"); + assert!(!bytes.is_empty()); + assert_eq!(*server.state(), HandshakeState::InProgress); + + let bytes = client.handshake(now(), &bytes[..]).expect("send CF"); + assert!(bytes.is_empty()); + assert_eq!(*client.state(), HandshakeState::AuthenticationPending); + + client.authenticated(AuthenticationStatus::Ok); + assert_eq!(*client.state(), HandshakeState::Authenticated(0)); + + // Calling handshake() again indicates that we're happy with the cert. + let bytes = client.handshake(now(), &[]).expect("send CF"); + assert!(!bytes.is_empty()); + assert!(client.state().is_connected()); + + let client_info = client.info().expect("got info"); + assert_eq!(TLS_VERSION_1_3, client_info.version()); + assert_eq!(TLS_AES_128_GCM_SHA256, client_info.cipher_suite()); + + let bytes = server.handshake(now(), &bytes[..]).expect("finish"); + assert!(bytes.is_empty()); + assert!(server.state().is_connected()); + + let server_info = server.info().expect("got info"); + assert_eq!(TLS_VERSION_1_3, server_info.version()); + assert_eq!(TLS_AES_128_GCM_SHA256, server_info.cipher_suite()); +} + +fn check_client_preinfo(client_preinfo: &SecretAgentPreInfo) { + assert_eq!(client_preinfo.version(), None); + assert_eq!(client_preinfo.cipher_suite(), None); + assert!(!client_preinfo.early_data()); + assert_eq!(client_preinfo.early_data_cipher(), None); + assert_eq!(client_preinfo.max_early_data(), 0); + assert_eq!(client_preinfo.alpn(), None); +} + +fn check_server_preinfo(server_preinfo: &SecretAgentPreInfo) { + assert_eq!(server_preinfo.version(), Some(TLS_VERSION_1_3)); + assert_eq!(server_preinfo.cipher_suite(), Some(TLS_AES_128_GCM_SHA256)); + assert!(!server_preinfo.early_data()); + assert_eq!(server_preinfo.early_data_cipher(), None); + assert_eq!(server_preinfo.max_early_data(), 0); + assert_eq!(server_preinfo.alpn(), None); +} + +#[test] +fn raw() { + fixture_init(); + let mut client = Client::new("server.example").expect("should create client"); + println!("client {:?}", client); + let mut server = Server::new(&["key"]).expect("should create server"); + println!("server {:?}", server); + + let client_records = client.handshake_raw(now(), None).expect("send CH"); + assert!(!client_records.is_empty()); + assert_eq!(*client.state(), HandshakeState::InProgress); + + check_client_preinfo(&client.preinfo().expect("get preinfo")); + + let server_records = + forward_records(now(), &mut server, client_records).expect("read CH, send SH"); + assert!(!server_records.is_empty()); + assert_eq!(*server.state(), HandshakeState::InProgress); + + check_server_preinfo(&server.preinfo().expect("get preinfo")); + + let client_records = forward_records(now(), &mut client, server_records).expect("send CF"); + assert!(client_records.is_empty()); + assert_eq!(*client.state(), HandshakeState::AuthenticationPending); + + client.authenticated(AuthenticationStatus::Ok); + assert_eq!(*client.state(), HandshakeState::Authenticated(0)); + + // Calling handshake() again indicates that we're happy with the cert. + let client_records = client.handshake_raw(now(), None).expect("send CF"); + assert!(!client_records.is_empty()); + assert!(client.state().is_connected()); + + let server_records = forward_records(now(), &mut server, client_records).expect("finish"); + assert!(server_records.is_empty()); + assert!(server.state().is_connected()); + + // The client should have one certificate for the server. + let mut certs = client.peer_certificate().unwrap(); + assert_eq!(1, certs.count()); + + // The server shouldn't have a client certificate. + assert!(server.peer_certificate().is_none()); +} + +#[test] +fn chacha_client() { + fixture_init(); + let mut client = Client::new("server.example").expect("should create client"); + let mut server = Server::new(&["key"]).expect("should create server"); + client + .set_ciphers(&[TLS_CHACHA20_POLY1305_SHA256]) + .expect("ciphers set"); + + connect(&mut client, &mut server); + + assert_eq!( + client.info().unwrap().cipher_suite(), + TLS_CHACHA20_POLY1305_SHA256 + ); + assert_eq!( + server.info().unwrap().cipher_suite(), + TLS_CHACHA20_POLY1305_SHA256 + ); +} + +#[test] +fn p256_server() { + fixture_init(); + let mut client = Client::new("server.example").expect("should create client"); + let mut server = Server::new(&["key"]).expect("should create server"); + server + .set_groups(&[TLS_GRP_EC_SECP256R1]) + .expect("groups set"); + + connect(&mut client, &mut server); + + assert_eq!(client.info().unwrap().key_exchange(), TLS_GRP_EC_SECP256R1); + assert_eq!(server.info().unwrap().key_exchange(), TLS_GRP_EC_SECP256R1); +} + +#[test] +fn alpn() { + fixture_init(); + let mut client = Client::new("server.example").expect("should create client"); + client.set_alpn(&["alpn"]).expect("should set ALPN"); + let mut server = Server::new(&["key"]).expect("should create server"); + server.set_alpn(&["alpn"]).expect("should set ALPN"); + + connect(&mut client, &mut server); + + let expected = Some(String::from("alpn")); + assert_eq!(expected.as_ref(), client.info().unwrap().alpn()); + assert_eq!(expected.as_ref(), server.info().unwrap().alpn()); +} + +#[test] +fn alpn_multi() { + fixture_init(); + let mut client = Client::new("server.example").expect("should create client"); + client + .set_alpn(&["dummy", "alpn"]) + .expect("should set ALPN"); + let mut server = Server::new(&["key"]).expect("should create server"); + server + .set_alpn(&["alpn", "other"]) + .expect("should set ALPN"); + + connect(&mut client, &mut server); + + let expected = Some(String::from("alpn")); + assert_eq!(expected.as_ref(), client.info().unwrap().alpn()); + assert_eq!(expected.as_ref(), server.info().unwrap().alpn()); +} + +#[test] +fn alpn_server_pref() { + fixture_init(); + let mut client = Client::new("server.example").expect("should create client"); + client + .set_alpn(&["dummy", "alpn"]) + .expect("should set ALPN"); + let mut server = Server::new(&["key"]).expect("should create server"); + server + .set_alpn(&["alpn", "dummy"]) + .expect("should set ALPN"); + + connect(&mut client, &mut server); + + let expected = Some(String::from("alpn")); + assert_eq!(expected.as_ref(), client.info().unwrap().alpn()); + assert_eq!(expected.as_ref(), server.info().unwrap().alpn()); +} + +#[test] +fn alpn_no_protocol() { + fixture_init(); + let mut client = Client::new("server.example").expect("should create client"); + client.set_alpn(&["a"]).expect("should set ALPN"); + let mut server = Server::new(&["key"]).expect("should create server"); + server.set_alpn(&["b"]).expect("should set ALPN"); + + connect_fail(&mut client, &mut server); + + // TODO(mt) check the error code +} + +#[test] +fn alpn_client_only() { + fixture_init(); + let mut client = Client::new("server.example").expect("should create client"); + client.set_alpn(&["alpn"]).expect("should set ALPN"); + let mut server = Server::new(&["key"]).expect("should create server"); + + connect(&mut client, &mut server); + + assert_eq!(None, client.info().unwrap().alpn()); + assert_eq!(None, server.info().unwrap().alpn()); +} + +#[test] +fn alpn_server_only() { + fixture_init(); + let mut client = Client::new("server.example").expect("should create client"); + let mut server = Server::new(&["key"]).expect("should create server"); + server.set_alpn(&["alpn"]).expect("should set ALPN"); + + connect(&mut client, &mut server); + + assert_eq!(None, client.info().unwrap().alpn()); + assert_eq!(None, server.info().unwrap().alpn()); +} + +#[test] +fn resume() { + let (_, token) = resumption_setup(Resumption::WithoutZeroRtt); + + let mut client = Client::new("server.example").expect("should create second client"); + let mut server = Server::new(&["key"]).expect("should create second server"); + + client + .enable_resumption(token) + .expect("should accept token"); + connect(&mut client, &mut server); + + assert!(client.info().unwrap().resumed()); + assert!(server.info().unwrap().resumed()); +} + +#[test] +fn zero_rtt() { + let (anti_replay, token) = resumption_setup(Resumption::WithZeroRtt); + + // Finally, 0-RTT should succeed. + let mut client = Client::new("server.example").expect("should create client"); + let mut server = Server::new(&["key"]).expect("should create server"); + client + .enable_resumption(token) + .expect("should accept token"); + client.enable_0rtt().expect("should enable 0-RTT"); + server + .enable_0rtt( + anti_replay.as_ref().unwrap(), + 0xffff_ffff, + Box::<PermissiveZeroRttChecker>::default(), + ) + .expect("should enable 0-RTT"); + + connect(&mut client, &mut server); + assert!(client.info().unwrap().early_data_accepted()); + assert!(server.info().unwrap().early_data_accepted()); +} + +#[test] +fn zero_rtt_no_eoed() { + let (anti_replay, token) = resumption_setup(Resumption::WithZeroRtt); + + // Finally, 0-RTT should succeed. + let mut client = Client::new("server.example").expect("should create client"); + let mut server = Server::new(&["key"]).expect("should create server"); + client + .enable_resumption(token) + .expect("should accept token"); + client.enable_0rtt().expect("should enable 0-RTT"); + client + .disable_end_of_early_data() + .expect("should disable EOED"); + server + .enable_0rtt( + anti_replay.as_ref().unwrap(), + 0xffff_ffff, + Box::<PermissiveZeroRttChecker>::default(), + ) + .expect("should enable 0-RTT"); + server + .disable_end_of_early_data() + .expect("should disable EOED"); + + connect(&mut client, &mut server); + assert!(client.info().unwrap().early_data_accepted()); + assert!(server.info().unwrap().early_data_accepted()); +} + +#[derive(Debug)] +struct RejectZeroRtt {} +impl ZeroRttChecker for RejectZeroRtt { + fn check(&self, token: &[u8]) -> ZeroRttCheckResult { + assert_eq!(ZERO_RTT_TOKEN_DATA, token); + ZeroRttCheckResult::Reject + } +} + +#[test] +fn reject_zero_rtt() { + let (anti_replay, token) = resumption_setup(Resumption::WithZeroRtt); + + // Finally, 0-RTT should succeed. + let mut client = Client::new("server.example").expect("should create client"); + let mut server = Server::new(&["key"]).expect("should create server"); + client + .enable_resumption(token) + .expect("should accept token"); + client.enable_0rtt().expect("should enable 0-RTT"); + server + .enable_0rtt( + anti_replay.as_ref().unwrap(), + 0xffff_ffff, + Box::new(RejectZeroRtt {}), + ) + .expect("should enable 0-RTT"); + + connect(&mut client, &mut server); + assert!(!client.info().unwrap().early_data_accepted()); + assert!(!server.info().unwrap().early_data_accepted()); +} + +#[test] +fn close() { + fixture_init(); + let mut client = Client::new("server.example").expect("should create client"); + let mut server = Server::new(&["key"]).expect("should create server"); + connect(&mut client, &mut server); + client.close(); + server.close(); +} + +#[test] +fn close_client_twice() { + fixture_init(); + let mut client = Client::new("server.example").expect("should create client"); + let mut server = Server::new(&["key"]).expect("should create server"); + connect(&mut client, &mut server); + client.close(); + client.close(); // Should be a noop. +} + +#[test] +fn ech() { + fixture_init(); + let mut server = Server::new(&["key"]).expect("should create server"); + let (sk, pk) = generate_ech_keys().expect("ECH keys"); + server + .enable_ech(88, "public.example", &sk, &pk) + .expect("should enable server ECH"); + + let mut client = Client::new("server.example").expect("should create client"); + client + .enable_ech(server.ech_config()) + .expect("should enable client ECH"); + + connect(&mut client, &mut server); + assert!(client.info().unwrap().ech_accepted()); + assert!(server.info().unwrap().ech_accepted()); + assert!(client.preinfo().unwrap().ech_accepted().unwrap()); + assert!(server.preinfo().unwrap().ech_accepted().unwrap()); +} + +#[test] +fn ech_retry() { + const PUBLIC_NAME: &str = "public.example"; + const PRIVATE_NAME: &str = "private.example"; + const CONFIG_ID: u8 = 7; + + fixture_init(); + let mut server = Server::new(&["key"]).unwrap(); + let (sk, pk) = generate_ech_keys().unwrap(); + server.enable_ech(CONFIG_ID, PUBLIC_NAME, &sk, &pk).unwrap(); + + let mut client = Client::new(PRIVATE_NAME).unwrap(); + let mut cfg = Vec::from(server.ech_config()); + // Ensure that the version and config_id is correct. + assert_eq!(cfg[2], 0xfe); + assert_eq!(cfg[3], 0x0d); + assert_eq!(cfg[6], CONFIG_ID); + // Change the config_id so that the server doesn't recognize this. + cfg[6] ^= 0x94; + client.enable_ech(&cfg).unwrap(); + + // Long version of connect() so that we can check the state. + let records = client.handshake_raw(now(), None).unwrap(); // ClientHello + let records = forward_records(now(), &mut server, records).unwrap(); // ServerHello... + let records = forward_records(now(), &mut client, records).unwrap(); // (empty) + assert!(records.is_empty()); + + // The client should now be expecting authentication. + assert_eq!( + *client.state(), + HandshakeState::EchFallbackAuthenticationPending(String::from(PUBLIC_NAME)) + ); + client.authenticated(AuthenticationStatus::Ok); + let updated_config = if let Err(Error::EchRetry(c)) = client.handshake_raw(now(), None) { + c + } else { + panic!( + "Handshake should fail with EchRetry, state is instead {:?}", + client.state() + ); + }; + assert_eq!( + client + .preinfo() + .unwrap() + .ech_public_name() + .unwrap() + .unwrap(), + PUBLIC_NAME + ); + // We don't forward alerts, so we can't tell the server about them. + // An ech_required alert should be set though. + assert_eq!(client.alert(), Some(&121)); + + let mut server = Server::new(&["key"]).unwrap(); + server.enable_ech(CONFIG_ID, PUBLIC_NAME, &sk, &pk).unwrap(); + let mut client = Client::new(PRIVATE_NAME).unwrap(); + client.enable_ech(&updated_config).unwrap(); + + connect(&mut client, &mut server); + + assert!(client.info().unwrap().ech_accepted()); + assert!(server.info().unwrap().ech_accepted()); + assert!(client.preinfo().unwrap().ech_accepted().unwrap()); + assert!(server.preinfo().unwrap().ech_accepted().unwrap()); +} diff --git a/third_party/rust/neqo-crypto/tests/ext.rs b/third_party/rust/neqo-crypto/tests/ext.rs new file mode 100644 index 0000000000..b814c190a4 --- /dev/null +++ b/third_party/rust/neqo-crypto/tests/ext.rs @@ -0,0 +1,97 @@ +#![cfg_attr(feature = "deny-warnings", deny(warnings))] +#![warn(clippy::pedantic)] + +use neqo_crypto::constants::{HandshakeMessage, TLS_HS_CLIENT_HELLO, TLS_HS_ENCRYPTED_EXTENSIONS}; +use neqo_crypto::ext::{ExtensionHandler, ExtensionHandlerResult, ExtensionWriterResult}; +use neqo_crypto::{Client, Server}; +use std::cell::RefCell; +use std::rc::Rc; +use test_fixture::fixture_init; + +mod handshake; +use crate::handshake::connect; + +struct NoopExtensionHandler; +impl ExtensionHandler for NoopExtensionHandler {} + +// This test just handshakes. It doesn't really do anything about capturing the +#[test] +fn noop_extension_handler() { + fixture_init(); + let mut client = Client::new("server.example").expect("should create client"); + let mut server = Server::new(&["key"]).expect("should create server"); + + client + .extension_handler(0xffff, Rc::new(RefCell::new(NoopExtensionHandler))) + .expect("installed"); + server + .extension_handler(0xffff, Rc::new(RefCell::new(NoopExtensionHandler))) + .expect("installed"); + + connect(&mut client, &mut server); +} + +#[derive(Debug, Default)] +struct SimpleExtensionHandler { + written: bool, + handled: bool, +} + +impl SimpleExtensionHandler { + #[allow(dead_code)] + pub fn negotiated(&self) -> bool { + self.written && self.handled + } +} + +impl ExtensionHandler for SimpleExtensionHandler { + fn write(&mut self, msg: HandshakeMessage, d: &mut [u8]) -> ExtensionWriterResult { + match msg { + TLS_HS_CLIENT_HELLO | TLS_HS_ENCRYPTED_EXTENSIONS => { + self.written = true; + d[0] = 77; + ExtensionWriterResult::Write(1) + } + _ => ExtensionWriterResult::Skip, + } + } + + fn handle(&mut self, msg: HandshakeMessage, d: &[u8]) -> ExtensionHandlerResult { + match msg { + TLS_HS_CLIENT_HELLO | TLS_HS_ENCRYPTED_EXTENSIONS => { + self.handled = true; + if d.len() != 1 { + ExtensionHandlerResult::Alert(50) // decode_error + } else if d[0] == 77 { + ExtensionHandlerResult::Ok + } else { + ExtensionHandlerResult::Alert(47) // illegal_parameter + } + } + _ => ExtensionHandlerResult::Alert(110), // unsupported_extension + } + } +} + +#[test] +fn simple_extension() { + fixture_init(); + let mut client = Client::new("server.example").expect("should create client"); + let mut server = Server::new(&["key"]).expect("should create server"); + + let client_handler = Rc::new(RefCell::new(SimpleExtensionHandler::default())); + let ch = Rc::clone(&client_handler); + client + .extension_handler(0xffff, ch) + .expect("client handler installed"); + let server_handler = Rc::new(RefCell::new(SimpleExtensionHandler::default())); + let sh = Rc::clone(&server_handler); + server + .extension_handler(0xffff, sh) + .expect("server handler installed"); + + connect(&mut client, &mut server); + + assert!(client_handler.borrow().negotiated()); + assert!(server_handler.borrow().negotiated()); +} diff --git a/third_party/rust/neqo-crypto/tests/handshake.rs b/third_party/rust/neqo-crypto/tests/handshake.rs new file mode 100644 index 0000000000..48b6ba2823 --- /dev/null +++ b/third_party/rust/neqo-crypto/tests/handshake.rs @@ -0,0 +1,157 @@ +#![allow(dead_code)] + +use neqo_common::qinfo; +use neqo_crypto::{ + AntiReplay, AuthenticationStatus, Client, HandshakeState, RecordList, Res, ResumptionToken, + SecretAgent, Server, ZeroRttCheckResult, ZeroRttChecker, +}; +use std::mem; +use std::time::Instant; +use test_fixture::{anti_replay, fixture_init, now}; + +/// Consume records until the handshake state changes. +pub fn forward_records( + now: Instant, + agent: &mut SecretAgent, + records_in: RecordList, +) -> Res<RecordList> { + let mut expected_state = match agent.state() { + HandshakeState::New => HandshakeState::New, + _ => HandshakeState::InProgress, + }; + let mut records_out = RecordList::default(); + for record in records_in { + assert_eq!(records_out.len(), 0); + assert_eq!(*agent.state(), expected_state); + + records_out = agent.handshake_raw(now, Some(record))?; + expected_state = HandshakeState::InProgress; + } + Ok(records_out) +} + +fn handshake(now: Instant, client: &mut SecretAgent, server: &mut SecretAgent) { + let mut a = client; + let mut b = server; + let mut records = a.handshake_raw(now, None).unwrap(); + let is_done = |agent: &mut SecretAgent| agent.state().is_final(); + while !is_done(b) { + records = if let Ok(r) = forward_records(now, b, records) { + r + } else { + // TODO(mt) take the alert generated by the failed handshake + // and allow it to be sent to the peer. + return; + }; + + if *b.state() == HandshakeState::AuthenticationPending { + b.authenticated(AuthenticationStatus::Ok); + records = if let Ok(r) = b.handshake_raw(now, None) { + r + } else { + // TODO(mt) - as above. + return; + } + } + mem::swap(&mut a, &mut b); + } +} + +pub fn connect_at(now: Instant, client: &mut SecretAgent, server: &mut SecretAgent) { + handshake(now, client, server); + qinfo!("client: {:?}", client.state()); + qinfo!("server: {:?}", server.state()); + assert!(client.state().is_connected()); + assert!(server.state().is_connected()); +} + +pub fn connect(client: &mut SecretAgent, server: &mut SecretAgent) { + connect_at(now(), client, server); +} + +pub fn connect_fail(client: &mut SecretAgent, server: &mut SecretAgent) { + handshake(now(), client, server); + assert!(!client.state().is_connected()); + assert!(!server.state().is_connected()); +} + +#[derive(Clone, Copy, Debug)] +pub enum Resumption { + WithoutZeroRtt, + WithZeroRtt, +} + +pub const ZERO_RTT_TOKEN_DATA: &[u8] = b"zero-rtt-token"; + +#[derive(Debug)] +pub struct PermissiveZeroRttChecker { + resuming: bool, +} + +impl Default for PermissiveZeroRttChecker { + fn default() -> Self { + Self { resuming: true } + } +} + +impl ZeroRttChecker for PermissiveZeroRttChecker { + fn check(&self, token: &[u8]) -> ZeroRttCheckResult { + if self.resuming { + assert_eq!(ZERO_RTT_TOKEN_DATA, token); + } else { + assert!(token.is_empty()); + } + ZeroRttCheckResult::Accept + } +} + +fn zero_rtt_setup( + mode: Resumption, + client: &mut Client, + server: &mut Server, +) -> Option<AntiReplay> { + if let Resumption::WithZeroRtt = mode { + client.enable_0rtt().expect("should enable 0-RTT on client"); + + let anti_replay = anti_replay(); + server + .enable_0rtt( + &anti_replay, + 0xffff_ffff, + Box::new(PermissiveZeroRttChecker { resuming: false }), + ) + .expect("should enable 0-RTT on server"); + Some(anti_replay) + } else { + None + } +} + +pub fn resumption_setup(mode: Resumption) -> (Option<AntiReplay>, ResumptionToken) { + fixture_init(); + + let mut client = Client::new("server.example").expect("should create client"); + let mut server = Server::new(&["key"]).expect("should create server"); + let anti_replay = zero_rtt_setup(mode, &mut client, &mut server); + + connect(&mut client, &mut server); + + assert!(!client.info().unwrap().resumed()); + assert!(!server.info().unwrap().resumed()); + assert!(!client.info().unwrap().early_data_accepted()); + assert!(!server.info().unwrap().early_data_accepted()); + + let server_records = server + .send_ticket(now(), ZERO_RTT_TOKEN_DATA) + .expect("ticket sent"); + assert_eq!(server_records.len(), 1); + let client_records = client + .handshake_raw(now(), server_records.into_iter().next()) + .expect("records ingested"); + assert_eq!(client_records.len(), 0); + + // `client` is about to go out of scope, + // but we only need to keep the resumption token, so clone it. + let token = client.resumption_token().expect("token is present"); + (anti_replay, token) +} diff --git a/third_party/rust/neqo-crypto/tests/hkdf.rs b/third_party/rust/neqo-crypto/tests/hkdf.rs new file mode 100644 index 0000000000..10a66f10b7 --- /dev/null +++ b/third_party/rust/neqo-crypto/tests/hkdf.rs @@ -0,0 +1,153 @@ +#![cfg_attr(feature = "deny-warnings", deny(warnings))] +#![warn(clippy::pedantic)] + +use neqo_crypto::constants::{ + Cipher, TLS_AES_128_GCM_SHA256, TLS_AES_256_GCM_SHA384, TLS_CHACHA20_POLY1305_SHA256, + TLS_VERSION_1_3, +}; +use neqo_crypto::{hkdf, SymKey}; +use test_fixture::fixture_init; + +const SALT: &[u8] = &[ + 0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0a, 0x0b, 0x0c, 0x0d, 0x0e, 0x0f, + 0x10, 0x11, 0x12, 0x13, 0x14, 0x15, 0x16, 0x17, 0x18, 0x19, 0x1a, 0x1b, 0x1c, 0x1d, 0x1e, 0x1f, + 0x20, 0x21, 0x22, 0x23, 0x24, 0x25, 0x26, 0x27, 0x28, 0x29, 0x2a, 0x2b, 0x2c, 0x2d, 0x2e, 0x2f, +]; + +const IKM: &[u8] = &[ + 0x01, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0a, 0x0b, 0x0c, 0x0d, 0x0e, 0x0f, + 0x10, 0x11, 0x12, 0x13, 0x14, 0x15, 0x16, 0x17, 0x18, 0x19, 0x1a, 0x1b, 0x1c, 0x1d, 0x1e, 0x1f, + 0x20, 0x21, 0x22, 0x23, 0x24, 0x25, 0x26, 0x27, 0x28, 0x29, 0x2a, 0x2b, 0x2c, 0x2d, 0x2e, 0x2f, +]; + +const SESSION_HASH: &[u8] = &[ + 0xf0, 0xf1, 0xf2, 0xf3, 0xf4, 0xf5, 0xf6, 0xf7, 0xf8, 0xf9, 0xfa, 0xfb, 0xfc, 0xfd, 0xfe, 0xff, + 0xe0, 0xe1, 0xe2, 0xe3, 0xe4, 0xe5, 0xe6, 0xe7, 0xe8, 0xe9, 0xea, 0xeb, 0xec, 0xed, 0xee, 0xef, + 0xd0, 0xd1, 0xd2, 0xd3, 0xd4, 0xd5, 0xd6, 0xd7, 0xd8, 0xd9, 0xda, 0xdb, 0xdc, 0xdd, 0xde, 0xdf, + 0xe0, 0xe1, 0xe2, 0xe3, 0xe4, 0xe5, 0xe6, 0xe7, 0xe8, 0xe9, 0xea, 0xeb, 0xec, 0xed, 0xee, 0xef, +]; + +fn cipher_hash_len(cipher: Cipher) -> usize { + match cipher { + TLS_AES_128_GCM_SHA256 | TLS_CHACHA20_POLY1305_SHA256 => 32, + TLS_AES_256_GCM_SHA384 => 48, + _ => unreachable!(), + } +} + +fn import_keys(cipher: Cipher) -> (SymKey, SymKey) { + let l = cipher_hash_len(cipher); + ( + hkdf::import_key(TLS_VERSION_1_3, &SALT[0..l]).expect("import salt"), + hkdf::import_key(TLS_VERSION_1_3, &IKM[0..l]).expect("import IKM"), + ) +} + +fn extract(cipher: Cipher, expected: &[u8]) { + fixture_init(); + let (salt, ikm) = import_keys(cipher); + let prk = hkdf::extract(TLS_VERSION_1_3, cipher, Some(&salt), &ikm) + .expect("HKDF Extract should work"); + let raw_prk = prk.as_bytes().expect("key should have bytes"); + assert_eq!(raw_prk, expected); +} + +#[test] +fn extract_sha256() { + const EXPECTED: &[u8] = &[ + 0xa5, 0x68, 0x02, 0x5a, 0x95, 0xc9, 0x7f, 0x55, 0x38, 0xbc, 0xf7, 0x97, 0xcc, 0x0f, 0xd5, + 0xf6, 0xa8, 0x8d, 0x15, 0xbc, 0x0e, 0x85, 0x74, 0x70, 0x3c, 0xa3, 0x65, 0xbd, 0x76, 0xcf, + 0x9f, 0xd3, + ]; + extract(TLS_AES_128_GCM_SHA256, EXPECTED); + extract(TLS_CHACHA20_POLY1305_SHA256, EXPECTED); +} + +#[test] +fn extract_sha384() { + extract( + TLS_AES_256_GCM_SHA384, + &[ + 0x01, 0x93, 0xc0, 0x07, 0x3f, 0x6a, 0x83, 0x0e, 0x2e, 0x4f, 0xb2, 0x58, 0xe4, 0x00, + 0x08, 0x5c, 0x68, 0x9c, 0x37, 0x32, 0x00, 0x37, 0xff, 0xc3, 0x1c, 0x5b, 0x98, 0x0b, + 0x02, 0x92, 0x3f, 0xfd, 0x73, 0x5a, 0x6f, 0x2a, 0x95, 0xa3, 0xee, 0xf6, 0xd6, 0x8e, + 0x6f, 0x86, 0xea, 0x63, 0xf8, 0x33, + ], + ); +} + +fn derive_secret(cipher: Cipher, expected: &[u8]) { + fixture_init(); + + // Here we only use the salt as the PRK. + let (prk, _) = import_keys(cipher); + let secret = hkdf::expand_label(TLS_VERSION_1_3, cipher, &prk, &[], "master secret") + .expect("HKDF-Expand-Label should work"); + let raw_secret = secret.as_bytes().expect("key should have bytes"); + assert_eq!(raw_secret, expected); +} + +#[test] +fn derive_secret_sha256() { + const EXPECTED: &[u8] = &[ + 0xb7, 0x08, 0x00, 0xe3, 0x8e, 0x48, 0x68, 0x91, 0xb1, 0x0f, 0x5e, 0x6f, 0x22, 0x53, 0x6b, + 0x84, 0x69, 0x75, 0xaa, 0xa3, 0x2a, 0xe7, 0xde, 0xaa, 0xc3, 0xd1, 0xb4, 0x05, 0x22, 0x5c, + 0x68, 0xf5, + ]; + derive_secret(TLS_AES_128_GCM_SHA256, EXPECTED); + derive_secret(TLS_CHACHA20_POLY1305_SHA256, EXPECTED); +} + +#[test] +fn derive_secret_sha384() { + derive_secret( + TLS_AES_256_GCM_SHA384, + &[ + 0x13, 0xd3, 0x36, 0x9f, 0x3c, 0x78, 0xa0, 0x32, 0x40, 0xee, 0x16, 0xe9, 0x11, 0x12, + 0x66, 0xc7, 0x51, 0xad, 0xd8, 0x3c, 0xa1, 0xa3, 0x97, 0x74, 0xd7, 0x45, 0xff, 0xa7, + 0x88, 0x9e, 0x52, 0x17, 0x2e, 0xaa, 0x3a, 0xd2, 0x35, 0xd8, 0xd5, 0x35, 0xfd, 0x65, + 0x70, 0x9f, 0xa9, 0xf9, 0xfa, 0x23, + ], + ); +} + +fn expand_label(cipher: Cipher, expected: &[u8]) { + fixture_init(); + + let l = cipher_hash_len(cipher); + let (prk, _) = import_keys(cipher); + let secret = hkdf::expand_label( + TLS_VERSION_1_3, + cipher, + &prk, + &SESSION_HASH[0..l], + "master secret", + ) + .expect("HKDF-Expand-Label should work"); + let raw_secret = secret.as_bytes().expect("key should have bytes"); + assert_eq!(raw_secret, expected); +} + +#[test] +fn expand_label_sha256() { + const EXPECTED: &[u8] = &[ + 0x3e, 0x4e, 0x6e, 0xd0, 0xbc, 0xc4, 0xf4, 0xff, 0xf0, 0xf5, 0x69, 0xd0, 0x6c, 0x1e, 0x0e, + 0x10, 0x32, 0xaa, 0xd7, 0xa3, 0xef, 0xf6, 0xa8, 0x65, 0x8e, 0xbe, 0xee, 0xc7, 0x1f, 0x01, + 0x6d, 0x3c, + ]; + expand_label(TLS_AES_128_GCM_SHA256, EXPECTED); + expand_label(TLS_CHACHA20_POLY1305_SHA256, EXPECTED); +} + +#[test] +fn expand_label_sha384() { + expand_label( + TLS_AES_256_GCM_SHA384, + &[ + 0x41, 0xea, 0x77, 0x09, 0x8c, 0x90, 0x04, 0x10, 0xec, 0xbc, 0x37, 0xd8, 0x5b, 0x54, + 0xcd, 0x7b, 0x08, 0x15, 0x13, 0x20, 0xed, 0x1e, 0x3f, 0x54, 0x74, 0xf7, 0x8b, 0x06, + 0x38, 0x28, 0x06, 0x37, 0x75, 0x23, 0xa2, 0xb7, 0x34, 0xb1, 0x72, 0x2e, 0x59, 0x6d, + 0x5a, 0x31, 0xf5, 0x53, 0xab, 0x99, + ], + ); +} diff --git a/third_party/rust/neqo-crypto/tests/hp.rs b/third_party/rust/neqo-crypto/tests/hp.rs new file mode 100644 index 0000000000..c4ffa3f788 --- /dev/null +++ b/third_party/rust/neqo-crypto/tests/hp.rs @@ -0,0 +1,78 @@ +#![cfg_attr(feature = "deny-warnings", deny(warnings))] +#![warn(clippy::pedantic)] + +use neqo_crypto::constants::{ + Cipher, TLS_AES_128_GCM_SHA256, TLS_AES_256_GCM_SHA384, TLS_CHACHA20_POLY1305_SHA256, + TLS_VERSION_1_3, +}; +use neqo_crypto::hkdf; +use neqo_crypto::hp::HpKey; +use std::mem; +use test_fixture::fixture_init; + +fn make_hp(cipher: Cipher) -> HpKey { + fixture_init(); + let ikm = hkdf::import_key(TLS_VERSION_1_3, &[0; 16]).expect("import IKM"); + let prk = hkdf::extract(TLS_VERSION_1_3, cipher, None, &ikm).expect("extract works"); + HpKey::extract(TLS_VERSION_1_3, cipher, &prk, "hp").expect("extract label works") +} + +fn hp_test(cipher: Cipher, expected: &[u8]) { + let hp = make_hp(cipher); + let mask = hp.mask(&[0; 16]).expect("should produce a mask"); + assert_eq!(mask, expected, "first invocation should be correct"); + + let hp2 = hp.clone(); + let mask = hp2.mask(&[0; 16]).expect("clone produces mask"); + assert_eq!(mask, expected, "clone should produce the same mask"); + + let mask = hp.mask(&[0; 16]).expect("should produce a mask again"); + assert_eq!(mask, expected, "second invocation should be the same"); +} + +#[test] +fn aes128() { + const EXPECTED: &[u8] = &[ + 0x04, 0x7b, 0xda, 0x65, 0xc3, 0x41, 0xcf, 0xbc, 0x5d, 0xe1, 0x75, 0x2b, 0x9d, 0x7d, 0xc3, + 0x14, + ]; + + hp_test(TLS_AES_128_GCM_SHA256, EXPECTED); +} + +#[test] +fn aes256() { + const EXPECTED: &[u8] = &[ + 0xb5, 0xea, 0xa2, 0x1c, 0x25, 0x77, 0x48, 0x18, 0xbf, 0x25, 0xea, 0xfa, 0xbd, 0x8d, 0x80, + 0x2b, + ]; + + hp_test(TLS_AES_256_GCM_SHA384, EXPECTED); +} + +#[test] +fn chacha20_ctr() { + const EXPECTED: &[u8] = &[ + 0x34, 0x11, 0xb3, 0x53, 0x02, 0x0b, 0x16, 0xda, 0x0a, 0x85, 0x5a, 0x52, 0x0d, 0x06, 0x07, + 0x1f, 0x4a, 0xb1, 0xaf, 0xf7, 0x83, 0xa8, 0xf0, 0x29, 0xc3, 0x19, 0xef, 0x57, 0x48, 0xe7, + 0x8e, 0x3e, 0x11, 0x91, 0xe1, 0xd5, 0x92, 0x8f, 0x61, 0x6d, 0x3f, 0x3d, 0x76, 0xb5, 0x29, + 0xf1, 0x62, 0x2f, 0x1e, 0xad, 0xdd, 0x23, 0x59, 0x45, 0xac, 0xd2, 0x19, 0x8a, 0xb4, 0x1f, + 0x2f, 0x52, 0x46, 0x89, + ]; + + hp_test(TLS_CHACHA20_POLY1305_SHA256, EXPECTED); +} + +#[test] +#[should_panic] +fn aes_short() { + let hp = make_hp(TLS_AES_128_GCM_SHA256); + mem::drop(hp.mask(&[0; 15])); +} + +#[test] +#[should_panic] +fn chacha20_short() { + let hp = make_hp(TLS_CHACHA20_POLY1305_SHA256); + mem::drop(hp.mask(&[0; 15])); +} diff --git a/third_party/rust/neqo-crypto/tests/init.rs b/third_party/rust/neqo-crypto/tests/init.rs new file mode 100644 index 0000000000..21291ceebb --- /dev/null +++ b/third_party/rust/neqo-crypto/tests/init.rs @@ -0,0 +1,44 @@ +#![cfg_attr(feature = "deny-warnings", deny(warnings))] +#![warn(clippy::pedantic)] + +// This uses external interfaces to neqo_crypto rather than being a module +// inside of lib.rs. Because all other code uses the test_fixture module, +// they will be calling into the public version of init_db(). Calling into +// the version exposed to an inner module in lib.rs would result in calling +// a different version of init_db. That causes explosions as they get +// different versions of the Once instance they use and they initialize NSS +// twice, probably likely in parallel. That doesn't work out well. +use neqo_crypto::{assert_initialized, init_db}; + +// Pull in the NSS internals so that we can ask NSS if it thinks that +// it is properly initialized. +#[allow( + dead_code, + non_upper_case_globals, + clippy::redundant_static_lifetimes, + clippy::unseparated_literal_suffix, + clippy::upper_case_acronyms +)] +mod nss { + include!(concat!(env!("OUT_DIR"), "/nss_init.rs")); +} + +#[cfg(nss_nodb)] +#[test] +fn init_nodb() { + init(); + assert_initialized(); + unsafe { + assert!(nss::NSS_IsInitialized() != 0); + } +} + +#[cfg(not(nss_nodb))] +#[test] +fn init_withdb() { + init_db(::test_fixture::NSS_DB_PATH); + assert_initialized(); + unsafe { + assert!(nss::NSS_IsInitialized() != 0); + } +} diff --git a/third_party/rust/neqo-crypto/tests/selfencrypt.rs b/third_party/rust/neqo-crypto/tests/selfencrypt.rs new file mode 100644 index 0000000000..5828f09392 --- /dev/null +++ b/third_party/rust/neqo-crypto/tests/selfencrypt.rs @@ -0,0 +1,92 @@ +#![cfg_attr(feature = "deny-warnings", deny(warnings))] +#![warn(clippy::pedantic)] +#![cfg(not(feature = "fuzzing"))] + +use neqo_crypto::constants::{TLS_AES_128_GCM_SHA256, TLS_VERSION_1_3}; +use neqo_crypto::{init, selfencrypt::SelfEncrypt, Error}; + +#[test] +fn se_create() { + init(); + SelfEncrypt::new(TLS_VERSION_1_3, TLS_AES_128_GCM_SHA256).expect("constructor works"); +} + +const PLAINTEXT: &[u8] = b"PLAINTEXT"; +const AAD: &[u8] = b"AAD"; + +fn sealed() -> (SelfEncrypt, Vec<u8>) { + init(); + let se = SelfEncrypt::new(TLS_VERSION_1_3, TLS_AES_128_GCM_SHA256).unwrap(); + let sealed = se.seal(AAD, PLAINTEXT).expect("sealing works"); + (se, sealed) +} + +#[test] +fn seal_open() { + let (se, sealed) = sealed(); + let opened = se.open(AAD, &sealed).expect("opening works"); + assert_eq!(&opened[..], PLAINTEXT); +} + +#[test] +fn seal_rotate_open() { + let (mut se, sealed) = sealed(); + se.rotate().expect("rotate should be infallible"); + let opened = se.open(AAD, &sealed).expect("opening works"); + assert_eq!(&opened[..], PLAINTEXT); +} + +#[test] +fn seal_rotate_twice_open() { + let (mut se, sealed) = sealed(); + se.rotate().expect("rotate should be infallible"); + se.rotate().expect("rotate should be infallible"); + let res = se.open(AAD, &sealed); + assert_eq!(res.unwrap_err(), Error::SelfEncryptFailure); +} + +#[test] +fn damage_version() { + let (se, mut sealed) = sealed(); + sealed[0] ^= 0x80; + let res = se.open(AAD, &sealed); + assert_eq!(res.unwrap_err(), Error::SelfEncryptFailure); +} + +fn assert_bad_data<T>(res: Result<T, Error>) { + if let Err(Error::NssError { name, .. }) = res { + assert_eq!(name, "SEC_ERROR_BAD_DATA"); + } +} + +#[test] +fn damage_salt() { + let (se, mut sealed) = sealed(); + sealed[4] ^= 0x10; + let res = se.open(AAD, &sealed); + assert_bad_data(res); +} + +#[test] +fn damage_ciphertext() { + let (se, mut sealed) = sealed(); + sealed[20] ^= 0x2f; + let res = se.open(AAD, &sealed); + assert_bad_data(res); +} + +#[test] +fn damage_auth_tag() { + let (se, mut sealed) = sealed(); + let idx = sealed.len() - 1; + sealed[idx] ^= 0x3; + let res = se.open(AAD, &sealed); + assert_bad_data(res); +} + +#[test] +fn truncate() { + let (se, sealed) = sealed(); + let res = se.open(AAD, &sealed[0..(sealed.len() - 1)]); + assert_bad_data(res); +} |