From 6ceced8f67876842ce0e4d0dcdc52c23c72463e8 Mon Sep 17 00:00:00 2001 From: peg Date: Fri, 13 Mar 2026 08:35:55 +0100 Subject: [PATCH 01/27] Basic attested cert resolver and verifier --- Cargo.lock | 636 ++++++++++++++++++++++++++++- Cargo.toml | 1 + crates/attested-tls/Cargo.toml | 17 + crates/attested-tls/src/lib.rs | 719 +++++++++++++++++++++++++++++++++ 4 files changed, 1363 insertions(+), 10 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 4c76e33..1e7a6e2 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -110,6 +110,12 @@ dependencies = [ "pin-project-lite", ] +[[package]] +name = "adler2" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa" + [[package]] name = "aho-corasick" version = "1.1.4" @@ -152,19 +158,41 @@ version = "1.0.102" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7f202df86484c868dbad7eaa557ef785d5c66295e41b460ef922eca0723b842c" +[[package]] +name = "arrayref" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "76a2e8124351fda1ef8aaaa3bbd7ebbcb486bbcd4225aca0aa0d84bb2db8fecb" + [[package]] name = "arrayvec" version = "0.7.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7c02d123df017efcdfbd739ef81735b36c5ba83ec3c59c80a9d7ecc718f92e50" +[[package]] +name = "asn1-rs" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5493c3bedbacf7fd7382c6346bbd66687d12bbaad3a89a2d2c303ee6cf20b048" +dependencies = [ + "asn1-rs-derive 0.5.1", + "asn1-rs-impl", + "displaydoc", + "nom", + "num-traits", + "rusticata-macros", + "thiserror 1.0.69", + "time", +] + [[package]] name = "asn1-rs" version = "0.7.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "56624a96882bb8c26d61312ae18cb45868e5a9992ea73c58e45c3101e56a1e60" dependencies = [ - "asn1-rs-derive", + "asn1-rs-derive 0.6.0", "asn1-rs-impl", "displaydoc", "nom", @@ -174,6 +202,18 @@ dependencies = [ "time", ] +[[package]] +name = "asn1-rs-derive" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "965c2d33e53cb6b267e148a4cb0760bc01f4904c1cd4bb4002a085bb016d1490" +dependencies = [ + "proc-macro2", + "quote", + "syn", + "synstructure", +] + [[package]] name = "asn1-rs-derive" version = "0.6.0" @@ -228,7 +268,7 @@ dependencies = [ "az-tdx-vtpm", "base64 0.22.1", "configfs-tsm", - "dcap-qvl", + "dcap-qvl 0.3.12 (git+https://github.com/flashbots/dcap-qvl.git?branch=peg%2Fazure-outdated-tcp-override)", "hex", "http 1.4.0", "num-bigint", @@ -249,12 +289,26 @@ dependencies = [ "tokio-rustls", "tracing", "tss-esapi", - "x509-parser", + "x509-parser 0.18.1", ] [[package]] name = "attested-tls" version = "0.0.1" +dependencies = [ + "anyhow", + "attestation", + "ra-tls", + "rcgen 0.14.7", + "rustls", + "serde_json", + "sha2", + "thiserror 2.0.18", + "tokio", + "tracing", + "x509-parser 0.18.1", + "yasna 0.5.2", +] [[package]] name = "autocfg" @@ -438,6 +492,29 @@ dependencies = [ "wyz", ] +[[package]] +name = "blake2" +version = "0.10.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "46502ad458c9a52b69d4d4d32775c788b7a1b85e8bc9d482d92250fc0e3f8efe" +dependencies = [ + "digest", +] + +[[package]] +name = "blake3" +version = "1.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2468ef7d57b3fb7e16b576e8377cdbde2320c60e1491e961d11da40fc4f02a2d" +dependencies = [ + "arrayref", + "arrayvec", + "cc", + "cfg-if", + "constant_time_eq", + "cpufeatures", +] + [[package]] name = "block-buffer" version = "0.10.4" @@ -447,6 +524,31 @@ dependencies = [ "generic-array", ] +[[package]] +name = "bon" +version = "3.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2d13a61f2963b88eef9c1be03df65d42f6996dfeac1054870d950fcf66686f83" +dependencies = [ + "bon-macros", + "rustversion", +] + +[[package]] +name = "bon-macros" +version = "3.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d314cc62af2b6b0c65780555abb4d02a03dd3b799cd42419044f0c38d99738c0" +dependencies = [ + "darling", + "ident_case", + "prettyplease", + "proc-macro2", + "quote", + "rustversion", + "syn", +] + [[package]] name = "borsh" version = "1.6.0" @@ -542,6 +644,23 @@ dependencies = [ "shlex", ] +[[package]] +name = "cc-eventlog" +version = "0.5.7" +source = "git+https://github.com/Dstack-TEE/dstack.git#31cfd481b178fd36b2137fc8fc1e5c728f89145d" +dependencies = [ + "anyhow", + "digest", + "ez-hash", + "fs-err", + "hex", + "parity-scale-codec", + "serde", + "serde-human-bytes", + "serde_json", + "sha2", +] + [[package]] name = "cfg-if" version = "1.0.4" @@ -637,6 +756,18 @@ version = "0.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "187437900921c8172f33316ad51a3267df588e99a2aebfa5ca1a2ed44df9e703" +[[package]] +name = "console" +version = "0.15.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "054ccb5b10f9f2cbf51eb355ca1d05c2d279ce1804688d0db74b4733a5aeafd8" +dependencies = [ + "encode_unicode", + "libc", + "once_cell", + "windows-sys 0.59.0", +] + [[package]] name = "const-oid" version = "0.9.6" @@ -663,6 +794,12 @@ dependencies = [ "unicode-xid", ] +[[package]] +name = "constant_time_eq" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d52eff69cd5e647efe296129160853a42795992097e8af39800e1060caeea9b" + [[package]] name = "convert_case" version = "0.10.0" @@ -681,6 +818,15 @@ dependencies = [ "libc", ] +[[package]] +name = "crc32fast" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9481c1c90cbf2ac953f07c8d4a58aa3945c425b7185c9154d67a65e4230da511" +dependencies = [ + "cfg-if", +] + [[package]] name = "criterion" version = "0.5.1" @@ -811,12 +957,83 @@ dependencies = [ "syn", ] +[[package]] +name = "darling" +version = "0.23.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "25ae13da2f202d56bd7f91c25fba009e7717a1e4a1cc98a76d844b65ae912e9d" +dependencies = [ + "darling_core", + "darling_macro", +] + +[[package]] +name = "darling_core" +version = "0.23.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9865a50f7c335f53564bb694ef660825eb8610e0a53d3e11bf1b0d3df31e03b0" +dependencies = [ + "ident_case", + "proc-macro2", + "quote", + "strsim", + "syn", +] + +[[package]] +name = "darling_macro" +version = "0.23.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac3984ec7bd6cfa798e62b4a642426a5be0e68f9401cfc2a01e3fa9ea2fcdb8d" +dependencies = [ + "darling_core", + "quote", + "syn", +] + [[package]] name = "data-encoding" version = "2.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d7a1e2f27636f116493b8b860f5546edb47c8d8f8ea73e1d2a20be88e28d1fea" +[[package]] +name = "dcap-qvl" +version = "0.3.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67e7842b81018f3b991dc65ec0a95ff347332de58478c4ac43459095af00cc89" +dependencies = [ + "anyhow", + "asn1_der", + "base64 0.22.1", + "borsh", + "byteorder", + "chrono", + "const-oid", + "dcap-qvl-webpki", + "der", + "derive_more 2.1.1", + "futures", + "hex", + "log", + "p256", + "parity-scale-codec", + "pem", + "reqwest", + "ring", + "rustls-pki-types", + "scale-info", + "serde", + "serde-human-bytes", + "serde_json", + "sha2", + "signature", + "tracing", + "urlencoding", + "wasm-bindgen-futures", + "x509-cert", +] + [[package]] name = "dcap-qvl" version = "0.3.12" @@ -884,13 +1101,27 @@ dependencies = [ "zeroize", ] +[[package]] +name = "der-parser" +version = "9.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5cd0a5c643689626bec213c4d8bd4d96acc8ffdb4ad4bb6bc16abf27d5f4b553" +dependencies = [ + "asn1-rs 0.6.2", + "displaydoc", + "nom", + "num-bigint", + "num-traits", + "rusticata-macros", +] + [[package]] name = "der-parser" version = "10.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "07da5016415d5a3c4dd39b11ed26f915f52fc4e0dc197d87908bc916e51bc1a6" dependencies = [ - "asn1-rs", + "asn1-rs 0.7.1", "displaydoc", "nom", "num-bigint", @@ -1005,6 +1236,43 @@ dependencies = [ "syn", ] +[[package]] +name = "dstack-attest" +version = "0.5.7" +source = "git+https://github.com/Dstack-TEE/dstack.git#31cfd481b178fd36b2137fc8fc1e5c728f89145d" +dependencies = [ + "anyhow", + "cc-eventlog", + "dcap-qvl 0.3.12 (registry+https://github.com/rust-lang/crates.io-index)", + "dstack-types", + "errify", + "ez-hash", + "fs-err", + "hex", + "hex_fmt", + "insta", + "or-panic", + "parity-scale-codec", + "serde", + "serde-human-bytes", + "serde_json", + "sha2", + "sha3", + "tdx-attest", +] + +[[package]] +name = "dstack-types" +version = "0.5.7" +source = "git+https://github.com/Dstack-TEE/dstack.git#31cfd481b178fd36b2137fc8fc1e5c728f89145d" +dependencies = [ + "parity-scale-codec", + "serde", + "serde-human-bytes", + "sha3", + "size-parser", +] + [[package]] name = "dunce" version = "1.0.5" @@ -1064,6 +1332,7 @@ dependencies = [ "ff", "generic-array", "group", + "pem-rfc7468", "pkcs8", "rand_core 0.6.4", "sec1", @@ -1071,6 +1340,12 @@ dependencies = [ "zeroize", ] +[[package]] +name = "encode_unicode" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34aa73646ffb006b8f5147f3dc182bd4bcb190227ce861fc4a4844bf8e3cb2c0" + [[package]] name = "encoding_rs" version = "0.8.35" @@ -1118,6 +1393,28 @@ version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" +[[package]] +name = "errify" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4bb818c3c01af9cdeb367f7e92e290b9a080935cdc5fb6cc0c1193ae17032849" +dependencies = [ + "anyhow", + "errify-macros", +] + +[[package]] +name = "errify-macros" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9e87afa19e6030c2cf5514b00d5a242a3ea9492a2aa618635076914f5d15e7af" +dependencies = [ + "proc-macro2", + "proc-macro2-diagnostics", + "quote", + "syn", +] + [[package]] name = "errno" version = "0.3.14" @@ -1128,6 +1425,21 @@ dependencies = [ "windows-sys 0.61.2", ] +[[package]] +name = "ez-hash" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42b3b3adc5fbbc9e21416d5b721b1bccb501a87d7b32ac89f2c7cea229d40772" +dependencies = [ + "blake2", + "blake3", + "digest", + "md-5", + "sha1", + "sha2", + "sha3", +] + [[package]] name = "fastrand" version = "2.3.0" @@ -1162,6 +1474,16 @@ version = "0.4.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b7ac824320a75a52197e8f2d787f6a38b6718bb6897a35142d749af3c0e8f4fe" +[[package]] +name = "flate2" +version = "1.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "843fba2746e448b37e26a819579957415c8cef339bf08564fe8b7ddbd959573c" +dependencies = [ + "crc32fast", + "miniz_oxide", +] + [[package]] name = "fnv" version = "1.0.7" @@ -1198,6 +1520,15 @@ dependencies = [ "percent-encoding", ] +[[package]] +name = "fs-err" +version = "3.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "73fde052dbfc920003cfd2c8e2c6e6d4cc7c1091538c3a24226cec0665ab08c0" +dependencies = [ + "autocfg", +] + [[package]] name = "fs_extra" version = "1.3.0" @@ -1404,6 +1735,12 @@ version = "0.4.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" +[[package]] +name = "hex_fmt" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b07f60793ff0a4d9cef0f18e63b5357e06209987153a64648c972c1e5aff336f" + [[package]] name = "hickory-proto" version = "0.25.2" @@ -1450,6 +1787,15 @@ dependencies = [ "tracing", ] +[[package]] +name = "hkdf" +version = "0.12.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7b5f8eb2ad728638ea2c7d47a21db23b7b58a72ed6a38256b8a1849f15fbbdf7" +dependencies = [ + "hmac", +] + [[package]] name = "hmac" version = "0.12.1" @@ -1669,6 +2015,12 @@ version = "2.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3d3067d79b975e8844ca9eb072e16b31c3c1c36928edf9c6789548c524d0d954" +[[package]] +name = "ident_case" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39" + [[package]] name = "idna" version = "1.1.0" @@ -1725,6 +2077,18 @@ dependencies = [ "serde_core", ] +[[package]] +name = "insta" +version = "1.46.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e82db8c87c7f1ccecb34ce0c24399b8a73081427f3c7c50a5d597925356115e4" +dependencies = [ + "console", + "once_cell", + "similar", + "tempfile", +] + [[package]] name = "iocuddle" version = "0.1.1" @@ -1834,6 +2198,15 @@ dependencies = [ "sha2", ] +[[package]] +name = "keccak" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb26cec98cce3a3d96cbb7bced3c4b16e3d13f27ec56dbd62cbc8f39cfb9d653" +dependencies = [ + "cpufeatures", +] + [[package]] name = "language-tags" version = "0.3.2" @@ -1925,6 +2298,16 @@ dependencies = [ "stable_deref_trait", ] +[[package]] +name = "md-5" +version = "0.10.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d89e7ee0cfbedfc4da3340218492196241d89eefb6dab27de5df917a6d2e78cf" +dependencies = [ + "cfg-if", + "digest", +] + [[package]] name = "memchr" version = "2.8.0" @@ -1952,6 +2335,16 @@ version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a" +[[package]] +name = "miniz_oxide" +version = "0.8.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fa76a2c86f704bdb222d66965fb3d63269ce38518b83cb0575fca855ebb6316" +dependencies = [ + "adler2", + "simd-adler32", +] + [[package]] name = "mio" version = "1.1.1" @@ -1992,13 +2385,26 @@ dependencies = [ "criterion", "impl-more 0.3.1", "pin-project-lite", - "rcgen", + "rcgen 0.14.7", "rustls", "tokio", "tokio-rustls", "tracing", ] +[[package]] +name = "nix" +version = "0.31.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d6d0705320c1e6ba1d912b5e37cf18071b6c2e9b7fa8215a1e8a7651966f5d3" +dependencies = [ + "bitflags 2.11.0", + "cfg-if", + "cfg_aliases", + "libc", + "memoffset", +] + [[package]] name = "nom" version = "7.1.3" @@ -2091,13 +2497,22 @@ dependencies = [ "serde", ] +[[package]] +name = "oid-registry" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a8d8034d9489cdaf79228eb9f6a3b8d7bb32ba00d6645ebd48eef4077ceb5bd9" +dependencies = [ + "asn1-rs 0.6.2", +] + [[package]] name = "oid-registry" version = "0.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "12f40cff3dde1b6087cc5d5f5d4d65712f34016a03ed60e9c08dcc392736b5b7" dependencies = [ - "asn1-rs", + "asn1-rs 0.7.1", ] [[package]] @@ -2170,6 +2585,12 @@ version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "04744f49eae99ab78e0d5c0b603ab218f515ea8cfe5a456d7629ad883a3b6e7d" +[[package]] +name = "or-panic" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "596a79faf55e869e7bc0c2162cf2f18a54d4d1112876bceae587ad954fcbd574" + [[package]] name = "p256" version = "0.13.2" @@ -2452,6 +2873,19 @@ dependencies = [ "unicode-ident", ] +[[package]] +name = "proc-macro2-diagnostics" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "af066a9c399a26e020ada66a034357a868728e72cd426f3adcd35f80d88d88c8" +dependencies = [ + "proc-macro2", + "quote", + "syn", + "version_check", + "yansi", +] + [[package]] name = "quinn" version = "0.11.9" @@ -2528,6 +2962,43 @@ version = "6.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f8dcc9c7d52a811697d2151c701e0d08956f92b0e24136cf4cf27b57a6a0d9bf" +[[package]] +name = "ra-tls" +version = "0.5.7" +source = "git+https://github.com/Dstack-TEE/dstack.git#31cfd481b178fd36b2137fc8fc1e5c728f89145d" +dependencies = [ + "anyhow", + "bon", + "cc-eventlog", + "dcap-qvl 0.3.12 (registry+https://github.com/rust-lang/crates.io-index)", + "dstack-attest", + "dstack-types", + "elliptic-curve", + "errify", + "ez-hash", + "flate2", + "fs-err", + "hex", + "hex_fmt", + "hkdf", + "or-panic", + "p256", + "parity-scale-codec", + "rand 0.8.5", + "rcgen 0.13.2", + "ring", + "rmp-serde", + "rustls-pki-types", + "serde", + "serde-human-bytes", + "serde_json", + "sha2", + "sha3", + "tracing", + "x509-parser 0.16.0", + "yasna 0.5.2", +] + [[package]] name = "radium" version = "0.7.0" @@ -2540,6 +3011,7 @@ version = "0.8.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404" dependencies = [ + "libc", "rand_chacha 0.3.1", "rand_core 0.6.4", ] @@ -2612,6 +3084,20 @@ dependencies = [ "crossbeam-utils", ] +[[package]] +name = "rcgen" +version = "0.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75e669e5202259b5314d1ea5397316ad400819437857b90861765f24c4cf80a2" +dependencies = [ + "pem", + "ring", + "rustls-pki-types", + "time", + "x509-parser 0.16.0", + "yasna 0.5.2", +] + [[package]] name = "rcgen" version = "0.14.7" @@ -2622,7 +3108,7 @@ dependencies = [ "ring", "rustls-pki-types", "time", - "x509-parser", + "x509-parser 0.18.1", "yasna 0.5.2", ] @@ -2756,6 +3242,25 @@ dependencies = [ "windows-sys 0.52.0", ] +[[package]] +name = "rmp" +version = "0.8.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ba8be72d372b2c9b35542551678538b562e7cf86c3315773cae48dfbfe7790c" +dependencies = [ + "num-traits", +] + +[[package]] +name = "rmp-serde" +version = "1.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72f81bee8c8ef9b577d1681a70ebbc962c232461e397b22c208c43c04b67a155" +dependencies = [ + "rmp", + "serde", +] + [[package]] name = "rsa" version = "0.9.10" @@ -3038,6 +3543,17 @@ dependencies = [ "uuid", ] +[[package]] +name = "sha1" +version = "0.10.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3bf829a2d51ab4a5ddf1352d8470c140cadc8301b2ae1789db023f01cedd6ba" +dependencies = [ + "cfg-if", + "cpufeatures", + "digest", +] + [[package]] name = "sha2" version = "0.10.9" @@ -3049,6 +3565,16 @@ dependencies = [ "digest", ] +[[package]] +name = "sha3" +version = "0.10.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75872d278a8f37ef87fa0ddbda7802605cb18344497949862c0d4dcb291eba60" +dependencies = [ + "digest", + "keccak", +] + [[package]] name = "shlex" version = "1.3.0" @@ -3075,6 +3601,28 @@ dependencies = [ "rand_core 0.6.4", ] +[[package]] +name = "simd-adler32" +version = "0.3.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e320a6c5ad31d271ad523dcf3ad13e2767ad8b1cb8f047f75a8aeaf8da139da2" + +[[package]] +name = "similar" +version = "2.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbbb5d9659141646ae647b42fe094daf6c6192d1620870b449d9557f748b2daa" + +[[package]] +name = "size-parser" +version = "0.5.7" +source = "git+https://github.com/Dstack-TEE/dstack.git#31cfd481b178fd36b2137fc8fc1e5c728f89145d" +dependencies = [ + "anyhow", + "serde", + "thiserror 2.0.18", +] + [[package]] name = "slab" version = "0.4.12" @@ -3135,6 +3683,12 @@ version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f" +[[package]] +name = "strsim" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" + [[package]] name = "subtle" version = "2.6.1" @@ -3190,6 +3744,25 @@ version = "0.12.16" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "61c41af27dd6d1e27b1b16b489db798443478cef1f06a660c96db617ba5de3b1" +[[package]] +name = "tdx-attest" +version = "0.5.7" +source = "git+https://github.com/Dstack-TEE/dstack.git#31cfd481b178fd36b2137fc8fc1e5c728f89145d" +dependencies = [ + "anyhow", + "cc-eventlog", + "fs-err", + "hex", + "libc", + "parity-scale-codec", + "serde", + "serde-human-bytes", + "serde_json", + "sha2", + "thiserror 2.0.18", + "vsock", +] + [[package]] name = "tdx-quote" version = "0.0.5" @@ -3624,6 +4197,16 @@ version = "0.0.18" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "051eb1abcf10076295e815102942cc58f9d5e3b4560e46e53c21e8ff6f3af7b1" +[[package]] +name = "vsock" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b82aeb12ad864eb8cd26a6c21175d0bdc66d398584ee6c93c76964c3bcfc78ff" +dependencies = [ + "libc", + "nix", +] + [[package]] name = "walkdir" version = "2.5.0" @@ -3828,6 +4411,15 @@ dependencies = [ "windows-targets 0.52.6", ] +[[package]] +name = "windows-sys" +version = "0.59.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b" +dependencies = [ + "windows-targets 0.52.6", +] + [[package]] name = "windows-sys" version = "0.61.2" @@ -4103,18 +4695,36 @@ dependencies = [ "x509-cert", ] +[[package]] +name = "x509-parser" +version = "0.16.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fcbc162f30700d6f3f82a24bf7cc62ffe7caea42c0b2cba8bf7f3ae50cf51f69" +dependencies = [ + "asn1-rs 0.6.2", + "data-encoding", + "der-parser 9.0.0", + "lazy_static", + "nom", + "oid-registry 0.7.1", + "ring", + "rusticata-macros", + "thiserror 1.0.69", + "time", +] + [[package]] name = "x509-parser" version = "0.18.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d43b0f71ce057da06bc0851b23ee24f3f86190b07203dd8f567d0b706a185202" dependencies = [ - "asn1-rs", + "asn1-rs 0.7.1", "data-encoding", - "der-parser", + "der-parser 10.0.0", "lazy_static", "nom", - "oid-registry", + "oid-registry 0.8.1", "ring", "rusticata-macros", "thiserror 2.0.18", @@ -4142,6 +4752,12 @@ dependencies = [ "x509-ocsp", ] +[[package]] +name = "yansi" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cfe53a6657fd280eaa890a3bc59152892ffa3e30101319d168b781ed6529b049" + [[package]] name = "yasna" version = "0.4.0" diff --git a/Cargo.toml b/Cargo.toml index 8067bbd..8d84292 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -20,3 +20,4 @@ unused_async = "warn" rustls = { version = "0.23.37", default-features = false, features = ["brotli"] } tokio = { version = "1.50.0", features = ["default"] } tokio-rustls = { version = "0.26.4", default-features = false } +attestation = { path = "crates/attestation" } diff --git a/crates/attested-tls/Cargo.toml b/crates/attested-tls/Cargo.toml index 989d69c..4c496b4 100644 --- a/crates/attested-tls/Cargo.toml +++ b/crates/attested-tls/Cargo.toml @@ -3,5 +3,22 @@ name = "attested-tls" version = "0.0.1" edition = "2024" +[dependencies] +tokio = { workspace = true } +rustls = { workspace = true, default-features = false, features = ["aws_lc_rs"] } +attestation = { workspace = true } +rcgen = "0.14.7" +thiserror = "2.0.17" +ra-tls = { git = "https://github.com/Dstack-TEE/dstack.git", version = "0.5.7", features = ["quote"] } +anyhow = "1.0.102" +x509-parser = "0.18.1" +serde_json = "1.0.149" +yasna = "0.5.2" +sha2 = "0.10.9" +tracing = "0.1.41" + +[dev-dependencies] +attestation = { workspace = true, features = ["mock"] } + [lints] workspace = true diff --git a/crates/attested-tls/src/lib.rs b/crates/attested-tls/src/lib.rs index 8b13789..bb04d52 100644 --- a/crates/attested-tls/src/lib.rs +++ b/crates/attested-tls/src/lib.rs @@ -1 +1,720 @@ +use std::{ + fmt, + sync::{Arc, RwLock}, + time::{Duration, SystemTime}, +}; +pub use attestation::{ + AttestationExchangeMessage, + AttestationGenerator, + AttestationType, + AttestationVerifier, +}; +use ra_tls::{ + attestation::{Attestation, AttestationQuote, VersionedAttestation}, + cert::{CaCert, CertRequest}, + rcgen::{KeyPair, PKCS_ECDSA_P256_SHA256}, +}; +use rustls::{ + DigitallySignedStruct, + DistinguishedName, + RootCertStore, + SignatureScheme, + client::{ + VerifierBuilderError, + WebPkiServerVerifier, + danger::{HandshakeSignatureValid, ServerCertVerified, ServerCertVerifier}, + }, + crypto::CryptoProvider, + pki_types::{ + CertificateDer, + PrivateKeyDer, + PrivatePkcs8KeyDer, + ServerName, + UnixTime, + pem::PemObject, + }, + server::ResolvesServerCert, + sign::{CertifiedKey, SigningKey}, +}; +use sha2::{Digest as _, Sha512}; +use thiserror::Error; +use x509_parser::oid_registry::Oid; + +/// The length of time a certificate is valid for +#[cfg(not(test))] +const CERTIFICATE_VALIDITY: Duration = Duration::from_secs(30 * 60); +#[cfg(test)] +const CERTIFICATE_VALIDITY: Duration = Duration::from_secs(4); + +/// How long before expiry to renew certificate +#[cfg(not(test))] +const CERTIFICATE_RENEWAL_LEAD_TIME: Duration = Duration::from_secs(5 * 60); +#[cfg(test)] +const CERTIFICATE_RENEWAL_LEAD_TIME: Duration = Duration::from_secs(2); + +/// How long to wait before re-trying certificate renewal on failure +#[cfg(not(test))] +const CERTIFICATE_RENEWAL_RETRY_DELAY: Duration = Duration::from_secs(30); +#[cfg(test)] +const CERTIFICATE_RENEWAL_RETRY_DELAY: Duration = Duration::from_millis(200); + +/// A TLS certificate resolver which includes an attestation as a +/// certificate extension +#[derive(Clone)] +pub struct AttestedCertificateResolver { + /// Cloneable inner state + state: Arc, +} + +impl fmt::Debug for AttestedCertificateResolver { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.debug_struct("AttestedCertificateResolver").finish_non_exhaustive() + } +} + +/// Internal state used by the resolver and its renewal loop +struct ResolverState { + /// The private TLS key in a format ready to be + /// used in handshake + key: Arc, + /// Optional CA used to sign leaf certificates - default is self-signed + ca: Option>, + /// The private TLS key in a format ready to be used by to sign + /// certificates if no CA is used + key_pair_der: Vec, + /// The current certificate with attestation + certificate: RwLock>>, + /// Attestation generator used when renewing ceritifcate + attestation_generator: AttestationGenerator, +} + +impl AttestedCertificateResolver { + /// Create a certificate resolver with a given attestation generator + /// A private cerificate authority can also be given - otherwise + /// certificates will be self signed + pub async fn new( + attestation_generator: AttestationGenerator, + ca: Option, + ) -> Result { + Self::new_with_provider(attestation_generator, ca, default_crypto_provider()?).await + } + + /// Also provide a crypto provider + pub async fn new_with_provider( + attestation_generator: AttestationGenerator, + ca: Option, + provider: Arc, + ) -> Result { + debug_assert!(CERTIFICATE_RENEWAL_LEAD_TIME < CERTIFICATE_VALIDITY); + + // Generate keypair + let key_pair = KeyPair::generate_for(&PKCS_ECDSA_P256_SHA256)?; + let key_pair_der = key_pair.serialize_der(); + let key = Self::load_signing_key(&key_pair, provider)?; + + // Generate initial attested certificate + let certificate = + Self::issue_ra_cert_chain(&key_pair, ca.as_ref(), &attestation_generator).await?; + + let state = Arc::new(ResolverState { + key, + certificate: RwLock::new(certificate), + ca: ca.map(Arc::new), + key_pair_der, + attestation_generator, + }); + + // Start a loop which will periodically renew the certificate + Self::spawn_renewal_task(Arc::downgrade(&state)); + + Ok(Self { state }) + } + + async fn issue_ra_cert_chain( + key: &KeyPair, + ca: Option<&CaCert>, + attestation_generator: &AttestationGenerator, + ) -> Result>, AttestedTlsError> { + let pubkey = key.public_key_der(); + let alt_names = vec!["foo".to_string()]; + let now = SystemTime::now(); + let not_after = now + CERTIFICATE_VALIDITY; + + let attestation = + Self::create_attestation_payload(pubkey, now, not_after, attestation_generator).await?; + + let cert_request = CertRequest::builder() + .key(key) + .subject("foo") + .alt_names(&alt_names) + .not_before(now) + .not_after(not_after) + .usage_server_auth(true) + .usage_client_auth(false) + .attestation(&attestation) + .build(); + + let leaf = match ca { + Some(ca) => ca.sign(cert_request).map_err(AttestedTlsError::RaTls)?, + None => cert_request.self_signed().map_err(AttestedTlsError::RaTls)?, + }; + + let mut chain = vec![leaf.der().to_vec().into()]; + if let Some(ca) = ca { + chain.push(CertificateDer::from_pem_slice(ca.pem_cert.as_bytes())?); + } + + Ok(chain) + } + + /// Get keypair into a format ready to be used in handshakes + fn load_signing_key( + key_pair: &KeyPair, + provider: Arc, + ) -> Result, AttestedTlsError> { + let private_key = PrivateKeyDer::Pkcs8(PrivatePkcs8KeyDer::from(key_pair.serialize_der())); + + provider.key_provider.load_private_key(private_key).map_err(AttestedTlsError::SigningKey) + } + + /// Create an attestation, and format it to be used in certificate + /// extension + async fn create_attestation_payload( + pubkey: Vec, + not_before: SystemTime, + not_after: SystemTime, + attestation_generator: &AttestationGenerator, + ) -> Result { + let report_data = create_report_data(pubkey, not_before, not_after)?; + let attestation = attestation_generator.generate_attestation(report_data).await.unwrap(); + Ok(VersionedAttestation::V0 { + attestation: Attestation { + quote: ra_tls::attestation::AttestationQuote::DstackTdx( + ra_tls::attestation::TdxQuote { + quote: serde_json::to_vec(&attestation).unwrap(), + event_log: Vec::new(), + }, + ), + runtime_events: Vec::new(), + report_data, + config: String::new(), + report: (), + }, + }) + } + + /// Start a loop which periodically renews the certificate + fn spawn_renewal_task(state: std::sync::Weak) { + tokio::spawn(async move { + let renewal_delay = CERTIFICATE_VALIDITY - CERTIFICATE_RENEWAL_LEAD_TIME; + + loop { + tokio::time::sleep(renewal_delay).await; + let Some(current) = state.upgrade() else { + tracing::warn!("Resolver has been dropped - stopping renewal loop"); + break; + }; + + let key_pair = match KeyPair::try_from(current.key_pair_der.clone()) { + Ok(key_pair) => key_pair, + Err(e) => { + tracing::error!("Failed to load keypair: {e}"); + tokio::time::sleep(CERTIFICATE_RENEWAL_RETRY_DELAY).await; + continue; + } + }; + + match Self::issue_ra_cert_chain( + &key_pair, + current.ca.as_deref(), + ¤t.attestation_generator, + ) + .await + { + Ok(certificate) => { + *current.certificate.write().expect("Certificate lock poisoned") = + certificate; + } + Err(e) => { + tracing::error!("Failed to renew attested certificate: {e}"); + tokio::time::sleep(CERTIFICATE_RENEWAL_RETRY_DELAY).await; + } + } + } + }); + } +} + +impl ResolvesServerCert for AttestedCertificateResolver { + fn resolve(&self, _: rustls::server::ClientHello<'_>) -> Option> { + let certificate = self.state.certificate.read().expect("certificate lock poisoned").clone(); + + Some(Arc::new(CertifiedKey::new(certificate, self.state.key.clone()))) + } +} + +fn default_crypto_provider() -> Result, AttestedTlsError> { + CryptoProvider::get_default().cloned().ok_or(AttestedTlsError::CryptoProviderUnavailable) +} + +/// Make input data for the attestation by hashing together public key and +/// validity period +fn create_report_data( + public_key: Vec, + not_before: SystemTime, + not_after: SystemTime, +) -> Result<[u8; 64], AttestedTlsError> { + let not_before = not_before + .duration_since(SystemTime::UNIX_EPOCH) + .map_err(AttestedTlsError::SystemTime)? + .as_secs() + .to_be_bytes(); + let not_after = not_after + .duration_since(SystemTime::UNIX_EPOCH) + .map_err(AttestedTlsError::SystemTime)? + .as_secs() + .to_be_bytes(); + + let mut hasher = Sha512::new(); + hasher.update(public_key); + hasher.update(not_before); + hasher.update(not_after); + + Ok(hasher.finalize().into()) +} + +#[derive(Debug)] +pub struct AttestedCertificateVerifier { + inner: Arc, + attestation_verifier: AttestationVerifier, +} + +impl AttestedCertificateVerifier { + pub fn new( + root_store: RootCertStore, + attestation_verifier: AttestationVerifier, + ) -> Result { + Self::new_with_provider(root_store, attestation_verifier, default_crypto_provider()?) + } + + pub fn new_with_provider( + root_store: RootCertStore, + attestation_verifier: AttestationVerifier, + provider: Arc, + ) -> Result { + let inner = WebPkiServerVerifier::builder_with_provider(root_store.into(), provider) + .build() + .map_err(AttestedTlsError::VerifierBuilder)?; + + Ok(Self { inner, attestation_verifier }) + } + + fn extract_custom_attestation_from_cert( + cert: &CertificateDer<'_>, + ) -> Result { + // First try to parse using ra_tls which assumes DCAP + if let Ok(Some(attestation)) = ra_tls::attestation::from_der(cert.as_ref()) { + if let AttestationQuote::DstackTdx(tdx_quote) = attestation.quote { + return Ok(AttestationExchangeMessage { + attestation_type: AttestationType::DcapTdx, + attestation: tdx_quote.quote, + }); + } + } + + // If that fails, extract and parse the extension + let (_, cert) = x509_parser::parse_x509_certificate(cert.as_ref()).unwrap(); + let oid = Oid::from(ra_tls::oids::PHALA_RATLS_TDX_QUOTE).unwrap(); + let ext = cert.get_extension_unique(&oid).unwrap().unwrap(); + let payload = yasna::parse_der(ext.value, |reader| reader.read_bytes()).unwrap(); + Ok(serde_json::from_slice(&payload).unwrap()) + } + + /// Given a certifcate, get the public key and validity period to check + /// against attestation input + fn expected_input_data_from_cert(cert: &CertificateDer<'_>) -> Result<[u8; 64], rustls::Error> { + let (_, cert) = x509_parser::parse_x509_certificate(cert.as_ref()).unwrap(); + let not_before: u64 = cert + .validity() + .not_before + .timestamp() + .try_into() + .map_err(|_| rustls::Error::General("invalid certificate not_before".into()))?; + let not_after: u64 = cert + .validity() + .not_after + .timestamp() + .try_into() + .map_err(|_| rustls::Error::General("invalid certificate not_after".into()))?; + create_report_data( + cert.public_key().raw.to_vec(), + SystemTime::UNIX_EPOCH + Duration::from_secs(not_before), + SystemTime::UNIX_EPOCH + Duration::from_secs(not_after), + ) + .map_err(|err| rustls::Error::General(err.to_string())) + } + + /// Given a cerificate and the current time, check if it is currently + /// valid + fn verify_cert_time_validity( + cert: &CertificateDer<'_>, + now: UnixTime, + ) -> Result<(), rustls::Error> { + let (_, cert) = x509_parser::parse_x509_certificate(cert.as_ref()).unwrap(); + let now = now.as_secs(); + let not_before: u64 = cert + .validity() + .not_before + .timestamp() + .try_into() + .map_err(|_| rustls::Error::General("invalid certificate not_before".into()))?; + let not_after: u64 = cert + .validity() + .not_after + .timestamp() + .try_into() + .map_err(|_| rustls::Error::General("invalid certificate not_after".into()))?; + + if now < not_before { + return Err(rustls::Error::InvalidCertificate( + rustls::CertificateError::NotValidYetContext { + time: UnixTime::since_unix_epoch(Duration::from_secs(now)), + not_before: UnixTime::since_unix_epoch(Duration::from_secs(not_before)), + }, + )); + } + + if now > not_after { + return Err(rustls::Error::InvalidCertificate( + rustls::CertificateError::ExpiredContext { + time: UnixTime::since_unix_epoch(Duration::from_secs(now)), + not_after: UnixTime::since_unix_epoch(Duration::from_secs(not_after)), + }, + )); + } + + Ok(()) + } +} + +impl ServerCertVerifier for AttestedCertificateVerifier { + fn verify_server_cert( + &self, + end_entity: &CertificateDer<'_>, + intermediates: &[CertificateDer<'_>], + server_name: &ServerName<'_>, + ocsp_response: &[u8], + now: UnixTime, + ) -> Result { + match self.inner.verify_server_cert( + end_entity, + intermediates, + server_name, + ocsp_response, + now, + ) { + Err(rustls::Error::InvalidCertificate(rustls::CertificateError::UnknownIssuer)) => { + // handle self-signed certs differently + Self::verify_cert_time_validity(end_entity, now)?; + } + Err(err) => return Err(err), + Ok(_) => {} + }; + let expected_input_data = + AttestedCertificateVerifier::expected_input_data_from_cert(end_entity)?; + let attestation = + AttestedCertificateVerifier::extract_custom_attestation_from_cert(end_entity)?; + + // Block when calling the verify function as it is async + tokio::task::block_in_place(|| { + tokio::runtime::Handle::current().block_on(async { + self.attestation_verifier + .verify_attestation(attestation, expected_input_data) + .await + .unwrap(); + }) + }); + Ok(ServerCertVerified::assertion()) + } + + fn verify_tls12_signature( + &self, + message: &[u8], + cert: &CertificateDer<'_>, + dss: &DigitallySignedStruct, + ) -> Result { + self.inner.verify_tls12_signature(message, cert, dss) + } + + fn verify_tls13_signature( + &self, + message: &[u8], + cert: &CertificateDer<'_>, + dss: &DigitallySignedStruct, + ) -> Result { + self.inner.verify_tls13_signature(message, cert, dss) + } + + fn supported_verify_schemes(&self) -> Vec { + self.inner.supported_verify_schemes() + } + + fn root_hint_subjects(&self) -> Option<&[DistinguishedName]> { + self.inner.root_hint_subjects() + } +} + +#[derive(Debug, Error)] +pub enum AttestedTlsError { + #[error("Failed to generate certificate key pair: {0}")] + CertificateKeyGeneration(#[source] rcgen::Error), + #[error("Failed to build certificate parameters: {0}")] + CertificateParams(#[source] rcgen::Error), + #[error("Failed to self-sign certificate: {0}")] + CertificateSigning(#[source] rcgen::Error), + #[error("Failed to load signing key into rustls: {0}")] + SigningKey(#[source] rustls::Error), + #[error("Failed to build certificate verifier: {0}")] + VerifierBuilder(#[source] VerifierBuilderError), + #[error("Cetificate generation: {0}")] + RcGen(#[from] ra_tls::rcgen::Error), + #[error("RA-TLS: {0}")] + RaTls(#[source] anyhow::Error), + #[error("Rustls: {0}")] + Rustls(#[from] rustls::Error), + #[error("Failed to parse PEM certificate: {0}")] + Pem(#[from] rustls::pki_types::pem::Error), + #[error("System time: {0}")] + SystemTime(#[source] std::time::SystemTimeError), + #[error("No rustls CryptoProvider is installed")] + CryptoProviderUnavailable, +} + +#[cfg(test)] +mod tests { + use std::{io::Cursor, sync::Arc}; + + use ra_tls::rcgen::{BasicConstraints, CertificateParams, IsCa}; + use rustls::{ + ClientConfig, + ClientConnection, + RootCertStore, + ServerConfig, + ServerConnection, + crypto::aws_lc_rs, + }; + + use super::*; + + #[tokio::test(flavor = "multi_thread")] + async fn certificate_resolver_creates_initial_certificate() { + let provider: Arc = aws_lc_rs::default_provider().into(); + let resolver = AttestedCertificateResolver::new_with_provider( + AttestationGenerator::new(AttestationType::DcapTdx, None) + .expect("mock generator construction should succeed"), + None, + provider, + ) + .await + .expect("resolver construction should succeed"); + let certificate = resolver.state.certificate.read().expect("certificate lock poisoned"); + + assert_eq!(certificate.len(), 1); + } + + #[tokio::test(flavor = "multi_thread")] + async fn server_and_client_configs_complete_a_handshake() { + let provider: Arc = aws_lc_rs::default_provider().into(); + let resolver = AttestedCertificateResolver::new_with_provider( + AttestationGenerator::new(AttestationType::DcapTdx, None) + .expect("mock generator construction should succeed"), + None, + provider.clone(), + ) + .await + .expect("resolver construction should succeed"); + let server_certificate = resolver + .state + .certificate + .read() + .expect("certificate lock poisoned") + .first() + .expect("resolver should hold a certificate") + .clone(); + + let mut roots = RootCertStore::empty(); + roots.add(server_certificate).expect("resolver certificate should be trusted"); + + let verifier = AttestedCertificateVerifier::new_with_provider( + roots, + AttestationVerifier::mock(), + provider.clone(), + ) + .expect("verifier construction should succeed"); + + let server_config = ServerConfig::builder_with_provider(provider.clone()) + .with_safe_default_protocol_versions() + .expect("server config should support default protocol versions") + .with_no_client_auth() + .with_cert_resolver(Arc::new(resolver)); + let client_config = ClientConfig::builder_with_provider(provider) + .with_safe_default_protocol_versions() + .expect("client config should support default protocol versions") + .dangerous() + .with_custom_certificate_verifier(Arc::new(verifier)) + .with_no_client_auth(); + + let mut client = ClientConnection::new( + Arc::new(client_config), + ServerName::try_from("foo").expect("server name should be valid"), + ) + .expect("client connection should be created"); + let mut server = + ServerConnection::new(Arc::new(server_config)).expect("server connection should exist"); + + while client.is_handshaking() || server.is_handshaking() { + transfer_tls_client_to_server(&mut client, &mut server); + transfer_tls_server_to_client(&mut server, &mut client); + } + + assert!(!client.is_handshaking()); + assert!(!server.is_handshaking()); + } + + #[tokio::test(flavor = "multi_thread")] + async fn ca_signed_server_and_client_configs_complete_a_handshake() { + let provider: Arc = aws_lc_rs::default_provider().into(); + let ca = test_ca(); + let ca_cert = CertificateDer::from_pem_slice(ca.pem_cert.as_bytes()) + .expect("test CA PEM should parse"); + let resolver = AttestedCertificateResolver::new_with_provider( + AttestationGenerator::new(AttestationType::DcapTdx, None) + .expect("mock generator construction should succeed"), + Some(ca), + provider.clone(), + ) + .await + .expect("resolver construction should succeed"); + let certificate_chain = + resolver.state.certificate.read().expect("certificate lock poisoned").clone(); + + assert_eq!(certificate_chain.len(), 2); + + let mut roots = RootCertStore::empty(); + roots.add(ca_cert).expect("CA certificate should be trusted"); + + let verifier = AttestedCertificateVerifier::new_with_provider( + roots, + AttestationVerifier::mock(), + provider.clone(), + ) + .expect("verifier construction should succeed"); + + let server_config = ServerConfig::builder_with_provider(provider.clone()) + .with_safe_default_protocol_versions() + .expect("server config should support default protocol versions") + .with_no_client_auth() + .with_cert_resolver(Arc::new(resolver)); + let client_config = ClientConfig::builder_with_provider(provider) + .with_safe_default_protocol_versions() + .expect("client config should support default protocol versions") + .dangerous() + .with_custom_certificate_verifier(Arc::new(verifier)) + .with_no_client_auth(); + + let mut client = ClientConnection::new( + Arc::new(client_config), + ServerName::try_from("foo").expect("server name should be valid"), + ) + .expect("client connection should be created"); + let mut server = + ServerConnection::new(Arc::new(server_config)).expect("server connection should exist"); + + while client.is_handshaking() || server.is_handshaking() { + transfer_tls_client_to_server(&mut client, &mut server); + transfer_tls_server_to_client(&mut server, &mut client); + } + + assert!(!client.is_handshaking()); + assert!(!server.is_handshaking()); + } + + #[tokio::test(flavor = "multi_thread")] + async fn certificate_is_renewed_before_expiry() { + let provider: Arc = aws_lc_rs::default_provider().into(); + let resolver = AttestedCertificateResolver::new_with_provider( + AttestationGenerator::new(AttestationType::DcapTdx, None) + .expect("mock generator construction should succeed"), + None, + provider, + ) + .await + .expect("resolver construction should succeed"); + let initial_certificate = resolver + .state + .certificate + .read() + .expect("certificate lock poisoned") + .first() + .expect("resolver should hold a certificate") + .clone(); + + tokio::time::sleep( + CERTIFICATE_VALIDITY - CERTIFICATE_RENEWAL_LEAD_TIME + Duration::from_secs(1), + ) + .await; + + let renewed_certificate = resolver + .state + .certificate + .read() + .expect("certificate lock poisoned") + .first() + .expect("resolver should hold a renewed certificate") + .clone(); + + assert_ne!(initial_certificate.as_ref(), renewed_certificate.as_ref()); + } + + fn test_ca() -> CaCert { + let key = KeyPair::generate_for(&PKCS_ECDSA_P256_SHA256) + .expect("test CA key generation should succeed"); + let mut params = CertificateParams::new(vec!["test-ca".to_string()]) + .expect("test CA params should be created"); + params.is_ca = IsCa::Ca(BasicConstraints::Unconstrained); + let cert = params.self_signed(&key).expect("test CA certificate should be self-signed"); + + CaCert::from_parts(key, cert) + } + + fn transfer_tls_client_to_server(client: &mut ClientConnection, server: &mut ServerConnection) { + let mut tls = Vec::new(); + + while client.wants_write() { + client.write_tls(&mut tls).expect("writing tls should succeed"); + } + + if tls.is_empty() { + return; + } + + server.read_tls(&mut Cursor::new(tls)).expect("reading tls should succeed"); + server.process_new_packets().expect("processing tls packets should succeed"); + } + + fn transfer_tls_server_to_client(server: &mut ServerConnection, client: &mut ClientConnection) { + let mut tls = Vec::new(); + + while server.wants_write() { + server.write_tls(&mut tls).expect("writing tls should succeed"); + } + + if tls.is_empty() { + return; + } + + client.read_tls(&mut Cursor::new(tls)).expect("reading tls should succeed"); + client.process_new_packets().expect("processing tls packets should succeed"); + } +} From 6b92a59be9bf358cdc4f6652d1bc15353e2ebcfd Mon Sep 17 00:00:00 2001 From: peg Date: Fri, 13 Mar 2026 08:52:06 +0100 Subject: [PATCH 02/27] Support client auth --- crates/attested-tls/src/lib.rs | 238 ++++++++++++++++++++++++++++----- 1 file changed, 206 insertions(+), 32 deletions(-) diff --git a/crates/attested-tls/src/lib.rs b/crates/attested-tls/src/lib.rs index bb04d52..6934551 100644 --- a/crates/attested-tls/src/lib.rs +++ b/crates/attested-tls/src/lib.rs @@ -21,6 +21,7 @@ use rustls::{ RootCertStore, SignatureScheme, client::{ + ResolvesClientCert, VerifierBuilderError, WebPkiServerVerifier, danger::{HandshakeSignatureValid, ServerCertVerified, ServerCertVerifier}, @@ -34,7 +35,11 @@ use rustls::{ UnixTime, pem::PemObject, }, - server::ResolvesServerCert, + server::{ + ResolvesServerCert, + WebPkiClientVerifier, + danger::{ClientCertVerified, ClientCertVerifier}, + }, sign::{CertifiedKey, SigningKey}, }; use sha2::{Digest as _, Sha512}; @@ -131,6 +136,8 @@ impl AttestedCertificateResolver { Ok(Self { state }) } + /// Create an attested certificate chain - either self-signed or with + /// the provided CA async fn issue_ra_cert_chain( key: &KeyPair, ca: Option<&CaCert>, @@ -151,7 +158,7 @@ impl AttestedCertificateResolver { .not_before(now) .not_after(not_after) .usage_server_auth(true) - .usage_client_auth(false) + .usage_client_auth(true) .attestation(&attestation) .build(); @@ -248,8 +255,23 @@ impl AttestedCertificateResolver { impl ResolvesServerCert for AttestedCertificateResolver { fn resolve(&self, _: rustls::server::ClientHello<'_>) -> Option> { - let certificate = self.state.certificate.read().expect("certificate lock poisoned").clone(); + self.current_certified_key() + } +} + +impl ResolvesClientCert for AttestedCertificateResolver { + fn resolve(&self, _: &[&[u8]], _: &[SignatureScheme]) -> Option> { + self.current_certified_key() + } + + fn has_certs(&self) -> bool { + !self.state.certificate.read().expect("certificate lock poisoned").is_empty() + } +} +impl AttestedCertificateResolver { + fn current_certified_key(&self) -> Option> { + let certificate = self.state.certificate.read().expect("certificate lock poisoned").clone(); Some(Arc::new(CertifiedKey::new(certificate, self.state.key.clone()))) } } @@ -286,7 +308,8 @@ fn create_report_data( #[derive(Debug)] pub struct AttestedCertificateVerifier { - inner: Arc, + server_inner: Arc, + client_inner: Arc, attestation_verifier: AttestationVerifier, } @@ -303,24 +326,29 @@ impl AttestedCertificateVerifier { attestation_verifier: AttestationVerifier, provider: Arc, ) -> Result { - let inner = WebPkiServerVerifier::builder_with_provider(root_store.into(), provider) + let root_store = Arc::new(root_store); + let server_inner = + WebPkiServerVerifier::builder_with_provider(root_store.clone(), provider.clone()) + .build() + .map_err(AttestedTlsError::VerifierBuilder)?; + let client_inner = WebPkiClientVerifier::builder_with_provider(root_store, provider) .build() .map_err(AttestedTlsError::VerifierBuilder)?; - Ok(Self { inner, attestation_verifier }) + Ok(Self { server_inner, client_inner, attestation_verifier }) } fn extract_custom_attestation_from_cert( cert: &CertificateDer<'_>, ) -> Result { // First try to parse using ra_tls which assumes DCAP - if let Ok(Some(attestation)) = ra_tls::attestation::from_der(cert.as_ref()) { - if let AttestationQuote::DstackTdx(tdx_quote) = attestation.quote { - return Ok(AttestationExchangeMessage { - attestation_type: AttestationType::DcapTdx, - attestation: tdx_quote.quote, - }); - } + if let Ok(Some(attestation)) = ra_tls::attestation::from_der(cert.as_ref()) && + let AttestationQuote::DstackTdx(tdx_quote) = attestation.quote + { + return Ok(AttestationExchangeMessage { + attestation_type: AttestationType::DcapTdx, + attestation: tdx_quote.quote, + }); } // If that fails, extract and parse the extension @@ -396,6 +424,25 @@ impl AttestedCertificateVerifier { Ok(()) } + + fn verify_attestation_binding( + &self, + end_entity: &CertificateDer<'_>, + ) -> Result<(), rustls::Error> { + let expected_input_data = Self::expected_input_data_from_cert(end_entity)?; + let attestation = Self::extract_custom_attestation_from_cert(end_entity)?; + + tokio::task::block_in_place(|| { + tokio::runtime::Handle::current().block_on(async { + self.attestation_verifier + .verify_attestation(attestation, expected_input_data) + .await + .unwrap(); + }) + }); + + Ok(()) + } } impl ServerCertVerifier for AttestedCertificateVerifier { @@ -407,7 +454,7 @@ impl ServerCertVerifier for AttestedCertificateVerifier { ocsp_response: &[u8], now: UnixTime, ) -> Result { - match self.inner.verify_server_cert( + match self.server_inner.verify_server_cert( end_entity, intermediates, server_name, @@ -421,20 +468,7 @@ impl ServerCertVerifier for AttestedCertificateVerifier { Err(err) => return Err(err), Ok(_) => {} }; - let expected_input_data = - AttestedCertificateVerifier::expected_input_data_from_cert(end_entity)?; - let attestation = - AttestedCertificateVerifier::extract_custom_attestation_from_cert(end_entity)?; - - // Block when calling the verify function as it is async - tokio::task::block_in_place(|| { - tokio::runtime::Handle::current().block_on(async { - self.attestation_verifier - .verify_attestation(attestation, expected_input_data) - .await - .unwrap(); - }) - }); + self.verify_attestation_binding(end_entity)?; Ok(ServerCertVerified::assertion()) } @@ -444,7 +478,7 @@ impl ServerCertVerifier for AttestedCertificateVerifier { cert: &CertificateDer<'_>, dss: &DigitallySignedStruct, ) -> Result { - self.inner.verify_tls12_signature(message, cert, dss) + self.server_inner.verify_tls12_signature(message, cert, dss) } fn verify_tls13_signature( @@ -453,15 +487,68 @@ impl ServerCertVerifier for AttestedCertificateVerifier { cert: &CertificateDer<'_>, dss: &DigitallySignedStruct, ) -> Result { - self.inner.verify_tls13_signature(message, cert, dss) + self.server_inner.verify_tls13_signature(message, cert, dss) } fn supported_verify_schemes(&self) -> Vec { - self.inner.supported_verify_schemes() + self.server_inner.supported_verify_schemes() } fn root_hint_subjects(&self) -> Option<&[DistinguishedName]> { - self.inner.root_hint_subjects() + self.server_inner.root_hint_subjects() + } +} + +impl ClientCertVerifier for AttestedCertificateVerifier { + fn offer_client_auth(&self) -> bool { + self.client_inner.offer_client_auth() + } + + fn client_auth_mandatory(&self) -> bool { + self.client_inner.client_auth_mandatory() + } + + fn root_hint_subjects(&self) -> &[DistinguishedName] { + self.client_inner.root_hint_subjects() + } + + fn verify_client_cert( + &self, + end_entity: &CertificateDer<'_>, + intermediates: &[CertificateDer<'_>], + now: UnixTime, + ) -> Result { + match self.client_inner.verify_client_cert(end_entity, intermediates, now) { + Err(rustls::Error::InvalidCertificate(rustls::CertificateError::UnknownIssuer)) => { + Self::verify_cert_time_validity(end_entity, now)?; + } + Err(err) => return Err(err), + Ok(_) => {} + }; + self.verify_attestation_binding(end_entity)?; + Ok(ClientCertVerified::assertion()) + } + + fn verify_tls12_signature( + &self, + message: &[u8], + cert: &CertificateDer<'_>, + dss: &DigitallySignedStruct, + ) -> Result { + self.client_inner.verify_tls12_signature(message, cert, dss) + } + + fn verify_tls13_signature( + &self, + message: &[u8], + cert: &CertificateDer<'_>, + dss: &DigitallySignedStruct, + ) -> Result { + self.client_inner.verify_tls13_signature(message, cert, dss) + } + + fn supported_verify_schemes(&self) -> Vec { + self.client_inner.supported_verify_schemes() } } @@ -677,6 +764,93 @@ mod tests { assert_ne!(initial_certificate.as_ref(), renewed_certificate.as_ref()); } + #[tokio::test(flavor = "multi_thread")] + async fn server_and_client_configs_complete_a_mutual_auth_handshake() { + let provider: Arc = aws_lc_rs::default_provider().into(); + + let server_resolver = AttestedCertificateResolver::new_with_provider( + AttestationGenerator::new(AttestationType::DcapTdx, None) + .expect("mock generator construction should succeed"), + None, + provider.clone(), + ) + .await + .expect("server resolver construction should succeed"); + let client_resolver = AttestedCertificateResolver::new_with_provider( + AttestationGenerator::new(AttestationType::DcapTdx, None) + .expect("mock generator construction should succeed"), + None, + provider.clone(), + ) + .await + .expect("client resolver construction should succeed"); + + let server_certificate = server_resolver + .state + .certificate + .read() + .expect("certificate lock poisoned") + .first() + .expect("resolver should hold a certificate") + .clone(); + let client_certificate = client_resolver + .state + .certificate + .read() + .expect("certificate lock poisoned") + .first() + .expect("resolver should hold a certificate") + .clone(); + + let mut client_roots = RootCertStore::empty(); + client_roots.add(server_certificate).expect("server certificate should be trusted"); + let mut server_roots = RootCertStore::empty(); + server_roots.add(client_certificate).expect("client certificate should be trusted"); + + let server_verifier = AttestedCertificateVerifier::new_with_provider( + server_roots, + AttestationVerifier::mock(), + provider.clone(), + ) + .expect("server verifier construction should succeed"); + let client_verifier = AttestedCertificateVerifier::new_with_provider( + client_roots, + AttestationVerifier::mock(), + provider.clone(), + ) + .expect("client verifier construction should succeed"); + + let server_config = ServerConfig::builder_with_provider(provider.clone()) + .with_safe_default_protocol_versions() + .expect("server config should support default protocol versions") + .with_client_cert_verifier(Arc::new(server_verifier)) + .with_cert_resolver(Arc::new(server_resolver)); + let client_config = ClientConfig::builder_with_provider(provider) + .with_safe_default_protocol_versions() + .expect("client config should support default protocol versions") + .dangerous() + .with_custom_certificate_verifier(Arc::new(client_verifier)) + .with_client_cert_resolver(Arc::new(client_resolver)); + + let mut client = ClientConnection::new( + Arc::new(client_config), + ServerName::try_from("foo").expect("server name should be valid"), + ) + .expect("client connection should be created"); + let mut server = + ServerConnection::new(Arc::new(server_config)).expect("server connection should exist"); + + while client.is_handshaking() || server.is_handshaking() { + transfer_tls_client_to_server(&mut client, &mut server); + transfer_tls_server_to_client(&mut server, &mut client); + } + + assert!(!client.is_handshaking()); + assert!(!server.is_handshaking()); + assert!(client.peer_certificates().is_some()); + assert!(server.peer_certificates().is_some()); + } + fn test_ca() -> CaCert { let key = KeyPair::generate_for(&PKCS_ECDSA_P256_SHA256) .expect("test CA key generation should succeed"); From 8d138d8ef96e6ee4a1eea63a4b0fc613e4d81dac Mon Sep 17 00:00:00 2001 From: peg Date: Fri, 13 Mar 2026 09:01:17 +0100 Subject: [PATCH 03/27] Allow hostnames to be provided in the constructor --- crates/attested-tls/src/lib.rs | 136 ++++++++++++++++++++++++++++++--- 1 file changed, 127 insertions(+), 9 deletions(-) diff --git a/crates/attested-tls/src/lib.rs b/crates/attested-tls/src/lib.rs index 6934551..deb421d 100644 --- a/crates/attested-tls/src/lib.rs +++ b/crates/attested-tls/src/lib.rs @@ -92,6 +92,10 @@ struct ResolverState { certificate: RwLock>>, /// Attestation generator used when renewing ceritifcate attestation_generator: AttestationGenerator, + /// Primary DNS name used as certificate subject / common name. + primary_name: String, + /// DNS subject alternative names, including the primary name. + subject_alt_names: Vec, } impl AttestedCertificateResolver { @@ -101,17 +105,29 @@ impl AttestedCertificateResolver { pub async fn new( attestation_generator: AttestationGenerator, ca: Option, + primary_name: String, + subject_alt_names: Vec, ) -> Result { - Self::new_with_provider(attestation_generator, ca, default_crypto_provider()?).await + Self::new_with_provider( + attestation_generator, + ca, + primary_name, + subject_alt_names, + default_crypto_provider()?, + ) + .await } /// Also provide a crypto provider pub async fn new_with_provider( attestation_generator: AttestationGenerator, ca: Option, + primary_name: String, + subject_alt_names: Vec, provider: Arc, ) -> Result { debug_assert!(CERTIFICATE_RENEWAL_LEAD_TIME < CERTIFICATE_VALIDITY); + let subject_alt_names = normalized_subject_alt_names(primary_name.as_str(), subject_alt_names); // Generate keypair let key_pair = KeyPair::generate_for(&PKCS_ECDSA_P256_SHA256)?; @@ -119,8 +135,14 @@ impl AttestedCertificateResolver { let key = Self::load_signing_key(&key_pair, provider)?; // Generate initial attested certificate - let certificate = - Self::issue_ra_cert_chain(&key_pair, ca.as_ref(), &attestation_generator).await?; + let certificate = Self::issue_ra_cert_chain( + &key_pair, + ca.as_ref(), + primary_name.as_str(), + &subject_alt_names, + &attestation_generator, + ) + .await?; let state = Arc::new(ResolverState { key, @@ -128,6 +150,8 @@ impl AttestedCertificateResolver { ca: ca.map(Arc::new), key_pair_der, attestation_generator, + primary_name, + subject_alt_names, }); // Start a loop which will periodically renew the certificate @@ -141,10 +165,11 @@ impl AttestedCertificateResolver { async fn issue_ra_cert_chain( key: &KeyPair, ca: Option<&CaCert>, + primary_name: &str, + subject_alt_names: &[String], attestation_generator: &AttestationGenerator, ) -> Result>, AttestedTlsError> { let pubkey = key.public_key_der(); - let alt_names = vec!["foo".to_string()]; let now = SystemTime::now(); let not_after = now + CERTIFICATE_VALIDITY; @@ -153,8 +178,8 @@ impl AttestedCertificateResolver { let cert_request = CertRequest::builder() .key(key) - .subject("foo") - .alt_names(&alt_names) + .subject(primary_name) + .alt_names(subject_alt_names) .not_before(now) .not_after(not_after) .usage_server_auth(true) @@ -235,6 +260,8 @@ impl AttestedCertificateResolver { match Self::issue_ra_cert_chain( &key_pair, current.ca.as_deref(), + current.primary_name.as_str(), + ¤t.subject_alt_names, ¤t.attestation_generator, ) .await @@ -280,6 +307,19 @@ fn default_crypto_provider() -> Result, AttestedTlsError> { CryptoProvider::get_default().cloned().ok_or(AttestedTlsError::CryptoProviderUnavailable) } +fn normalized_subject_alt_names(primary_name: &str, subject_alt_names: Vec) -> Vec { + let mut normalized = Vec::with_capacity(subject_alt_names.len() + 1); + normalized.push(primary_name.to_string()); + + for name in subject_alt_names { + if !normalized.iter().any(|existing| existing == &name) { + normalized.push(name); + } + } + + normalized +} + /// Make input data for the attestation by hashing together public key and /// validity period fn create_report_data( @@ -601,6 +641,8 @@ mod tests { AttestationGenerator::new(AttestationType::DcapTdx, None) .expect("mock generator construction should succeed"), None, + "foo".to_string(), + vec![], provider, ) .await @@ -613,10 +655,13 @@ mod tests { #[tokio::test(flavor = "multi_thread")] async fn server_and_client_configs_complete_a_handshake() { let provider: Arc = aws_lc_rs::default_provider().into(); + let server_name = "foo"; let resolver = AttestedCertificateResolver::new_with_provider( AttestationGenerator::new(AttestationType::DcapTdx, None) .expect("mock generator construction should succeed"), None, + server_name.to_string(), + vec![], provider.clone(), ) .await @@ -654,7 +699,7 @@ mod tests { let mut client = ClientConnection::new( Arc::new(client_config), - ServerName::try_from("foo").expect("server name should be valid"), + ServerName::try_from(server_name).expect("server name should be valid"), ) .expect("client connection should be created"); let mut server = @@ -672,6 +717,7 @@ mod tests { #[tokio::test(flavor = "multi_thread")] async fn ca_signed_server_and_client_configs_complete_a_handshake() { let provider: Arc = aws_lc_rs::default_provider().into(); + let server_name = "foo"; let ca = test_ca(); let ca_cert = CertificateDer::from_pem_slice(ca.pem_cert.as_bytes()) .expect("test CA PEM should parse"); @@ -679,6 +725,8 @@ mod tests { AttestationGenerator::new(AttestationType::DcapTdx, None) .expect("mock generator construction should succeed"), Some(ca), + server_name.to_string(), + vec![], provider.clone(), ) .await @@ -712,7 +760,7 @@ mod tests { let mut client = ClientConnection::new( Arc::new(client_config), - ServerName::try_from("foo").expect("server name should be valid"), + ServerName::try_from(server_name).expect("server name should be valid"), ) .expect("client connection should be created"); let mut server = @@ -734,6 +782,8 @@ mod tests { AttestationGenerator::new(AttestationType::DcapTdx, None) .expect("mock generator construction should succeed"), None, + "foo".to_string(), + vec![], provider, ) .await @@ -767,11 +817,14 @@ mod tests { #[tokio::test(flavor = "multi_thread")] async fn server_and_client_configs_complete_a_mutual_auth_handshake() { let provider: Arc = aws_lc_rs::default_provider().into(); + let server_name = "foo"; let server_resolver = AttestedCertificateResolver::new_with_provider( AttestationGenerator::new(AttestationType::DcapTdx, None) .expect("mock generator construction should succeed"), None, + server_name.to_string(), + vec![], provider.clone(), ) .await @@ -780,6 +833,8 @@ mod tests { AttestationGenerator::new(AttestationType::DcapTdx, None) .expect("mock generator construction should succeed"), None, + "client".to_string(), + vec![], provider.clone(), ) .await @@ -834,7 +889,7 @@ mod tests { let mut client = ClientConnection::new( Arc::new(client_config), - ServerName::try_from("foo").expect("server name should be valid"), + ServerName::try_from(server_name).expect("server name should be valid"), ) .expect("client connection should be created"); let mut server = @@ -851,6 +906,69 @@ mod tests { assert!(server.peer_certificates().is_some()); } + #[tokio::test(flavor = "multi_thread")] + async fn alternate_san_completes_a_handshake() { + let provider: Arc = aws_lc_rs::default_provider().into(); + let primary_name = "foo"; + let alternate_name = "bar"; + let resolver = AttestedCertificateResolver::new_with_provider( + AttestationGenerator::new(AttestationType::DcapTdx, None) + .expect("mock generator construction should succeed"), + None, + primary_name.to_string(), + vec![alternate_name.to_string(), primary_name.to_string()], + provider.clone(), + ) + .await + .expect("resolver construction should succeed"); + let server_certificate = resolver + .state + .certificate + .read() + .expect("certificate lock poisoned") + .first() + .expect("resolver should hold a certificate") + .clone(); + + let mut roots = RootCertStore::empty(); + roots.add(server_certificate).expect("resolver certificate should be trusted"); + + let verifier = AttestedCertificateVerifier::new_with_provider( + roots, + AttestationVerifier::mock(), + provider.clone(), + ) + .expect("verifier construction should succeed"); + + let server_config = ServerConfig::builder_with_provider(provider.clone()) + .with_safe_default_protocol_versions() + .expect("server config should support default protocol versions") + .with_no_client_auth() + .with_cert_resolver(Arc::new(resolver)); + let client_config = ClientConfig::builder_with_provider(provider) + .with_safe_default_protocol_versions() + .expect("client config should support default protocol versions") + .dangerous() + .with_custom_certificate_verifier(Arc::new(verifier)) + .with_no_client_auth(); + + let mut client = ClientConnection::new( + Arc::new(client_config), + ServerName::try_from(alternate_name).expect("alternate server name should be valid"), + ) + .expect("client connection should be created"); + let mut server = + ServerConnection::new(Arc::new(server_config)).expect("server connection should exist"); + + while client.is_handshaking() || server.is_handshaking() { + transfer_tls_client_to_server(&mut client, &mut server); + transfer_tls_server_to_client(&mut server, &mut client); + } + + assert!(!client.is_handshaking()); + assert!(!server.is_handshaking()); + } + fn test_ca() -> CaCert { let key = KeyPair::generate_for(&PKCS_ECDSA_P256_SHA256) .expect("test CA key generation should succeed"); From 6c8664a56e7779979657fa319c7083f094857ce4 Mon Sep 17 00:00:00 2001 From: peg Date: Fri, 13 Mar 2026 09:03:03 +0100 Subject: [PATCH 04/27] Fmt --- crates/attested-tls/src/lib.rs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/crates/attested-tls/src/lib.rs b/crates/attested-tls/src/lib.rs index deb421d..84ed1ab 100644 --- a/crates/attested-tls/src/lib.rs +++ b/crates/attested-tls/src/lib.rs @@ -127,7 +127,8 @@ impl AttestedCertificateResolver { provider: Arc, ) -> Result { debug_assert!(CERTIFICATE_RENEWAL_LEAD_TIME < CERTIFICATE_VALIDITY); - let subject_alt_names = normalized_subject_alt_names(primary_name.as_str(), subject_alt_names); + let subject_alt_names = + normalized_subject_alt_names(primary_name.as_str(), subject_alt_names); // Generate keypair let key_pair = KeyPair::generate_for(&PKCS_ECDSA_P256_SHA256)?; @@ -829,6 +830,7 @@ mod tests { ) .await .expect("server resolver construction should succeed"); + let client_resolver = AttestedCertificateResolver::new_with_provider( AttestationGenerator::new(AttestationType::DcapTdx, None) .expect("mock generator construction should succeed"), From 4cfe1315d137aa8e0f67f93a694a73dc0b1855d2 Mon Sep 17 00:00:00 2001 From: peg Date: Fri, 13 Mar 2026 10:06:46 +0100 Subject: [PATCH 05/27] Improve error handling during verification --- crates/attested-tls/src/lib.rs | 168 ++++++++++++++++++++++++++++++--- 1 file changed, 157 insertions(+), 11 deletions(-) diff --git a/crates/attested-tls/src/lib.rs b/crates/attested-tls/src/lib.rs index 84ed1ab..cad28a4 100644 --- a/crates/attested-tls/src/lib.rs +++ b/crates/attested-tls/src/lib.rs @@ -44,7 +44,7 @@ use rustls::{ }; use sha2::{Digest as _, Sha512}; use thiserror::Error; -use x509_parser::oid_registry::Oid; +use x509_parser::{certificate::X509Certificate, oid_registry::Oid}; /// The length of time a certificate is valid for #[cfg(not(test))] @@ -393,17 +393,23 @@ impl AttestedCertificateVerifier { } // If that fails, extract and parse the extension - let (_, cert) = x509_parser::parse_x509_certificate(cert.as_ref()).unwrap(); - let oid = Oid::from(ra_tls::oids::PHALA_RATLS_TDX_QUOTE).unwrap(); - let ext = cert.get_extension_unique(&oid).unwrap().unwrap(); - let payload = yasna::parse_der(ext.value, |reader| reader.read_bytes()).unwrap(); - Ok(serde_json::from_slice(&payload).unwrap()) + let cert = Self::parse_x509_certificate(cert)?; + let oid = Oid::from(ra_tls::oids::PHALA_RATLS_TDX_QUOTE) + .map_err(|err| rustls::Error::General(format!("invalid attestation OID: {err}")))?; + let ext = cert + .get_extension_unique(&oid) + .map_err(|err| Self::bad_encoding(format!("invalid attestation extension: {err}")))? + .ok_or_else(|| Self::bad_encoding("missing attestation extension"))?; + let payload = yasna::parse_der(ext.value, |reader| reader.read_bytes()) + .map_err(|err| Self::bad_encoding(format!("invalid attestation DER payload: {err}")))?; + serde_json::from_slice(&payload) + .map_err(|err| Self::bad_encoding(format!("invalid attestation JSON payload: {err}"))) } /// Given a certifcate, get the public key and validity period to check /// against attestation input fn expected_input_data_from_cert(cert: &CertificateDer<'_>) -> Result<[u8; 64], rustls::Error> { - let (_, cert) = x509_parser::parse_x509_certificate(cert.as_ref()).unwrap(); + let cert = Self::parse_x509_certificate(cert)?; let not_before: u64 = cert .validity() .not_before @@ -430,7 +436,7 @@ impl AttestedCertificateVerifier { cert: &CertificateDer<'_>, now: UnixTime, ) -> Result<(), rustls::Error> { - let (_, cert) = x509_parser::parse_x509_certificate(cert.as_ref()).unwrap(); + let cert = Self::parse_x509_certificate(cert)?; let now = now.as_secs(); let not_before: u64 = cert .validity() @@ -478,11 +484,45 @@ impl AttestedCertificateVerifier { self.attestation_verifier .verify_attestation(attestation, expected_input_data) .await - .unwrap(); + .map(|_| ()) + .map_err(|err| { + tracing::warn!( + "Rejecting certificate after attestation verification failure: {err}" + ); + rustls::Error::InvalidCertificate( + rustls::CertificateError::ApplicationVerificationFailure, + ) + }) }) - }); + }) + } - Ok(()) + /// Helper for creating encoding related verification errors + fn bad_encoding(message: impl Into) -> rustls::Error { + let message = message.into(); + tracing::debug!("Rejecting malformed certificate or attestation payload: {message}"); + rustls::Error::InvalidCertificate(rustls::CertificateError::BadEncoding) + } + + /// Helper to parse a certificate and map the error for rustls + fn parse_x509_certificate<'a>( + cert: &'a CertificateDer<'_>, + ) -> Result, rustls::Error> { + x509_parser::parse_x509_certificate(cert.as_ref()) + .map(|(_, parsed)| parsed) + .map_err(|err| Self::bad_encoding(format!("Invalid X.509 DER: {err}"))) + } +} + +impl AttestedCertificateVerifier { + #[cfg(test)] + fn verify_server_cert_direct( + &self, + end_entity: &CertificateDer<'_>, + server_name: &ServerName<'_>, + now: UnixTime, + ) -> Result { + self.verify_server_cert(end_entity, &[], server_name, &[], now) } } @@ -625,8 +665,10 @@ mod tests { use ra_tls::rcgen::{BasicConstraints, CertificateParams, IsCa}; use rustls::{ + CertificateError, ClientConfig, ClientConnection, + Error, RootCertStore, ServerConfig, ServerConnection, @@ -971,6 +1013,88 @@ mod tests { assert!(!server.is_handshaking()); } + #[tokio::test(flavor = "multi_thread")] + async fn malformed_certificate_returns_bad_encoding() { + let provider: Arc = aws_lc_rs::default_provider().into(); + let verifier = AttestedCertificateVerifier::new_with_provider( + non_empty_root_store(), + AttestationVerifier::mock(), + provider, + ) + .expect("verifier construction should succeed"); + let cert = CertificateDer::from(vec![1_u8, 2, 3, 4]); + + let result = verifier.verify_server_cert_direct( + &cert, + &ServerName::try_from("foo").expect("server name should be valid"), + UnixTime::now(), + ); + + assert_eq!(result.unwrap_err(), Error::InvalidCertificate(CertificateError::BadEncoding)); + } + + #[tokio::test(flavor = "multi_thread")] + async fn certificate_without_attestation_extension_returns_bad_encoding() { + let provider: Arc = aws_lc_rs::default_provider().into(); + let cert = plain_self_signed_certificate("foo"); + let mut roots = RootCertStore::empty(); + roots.add(cert.clone()).expect("plain certificate should be trusted"); + let verifier = AttestedCertificateVerifier::new_with_provider( + roots, + AttestationVerifier::mock(), + provider, + ) + .expect("verifier construction should succeed"); + + let result = verifier.verify_server_cert_direct( + &cert, + &ServerName::try_from("foo").expect("server name should be valid"), + UnixTime::now(), + ); + + assert_eq!(result.unwrap_err(), Error::InvalidCertificate(CertificateError::BadEncoding)); + } + + #[tokio::test(flavor = "multi_thread")] + async fn attestation_rejection_returns_application_verification_failure() { + let provider: Arc = aws_lc_rs::default_provider().into(); + let resolver = AttestedCertificateResolver::new_with_provider( + AttestationGenerator::new(AttestationType::DcapTdx, None) + .expect("mock generator construction should succeed"), + None, + "foo".to_string(), + vec![], + provider.clone(), + ) + .await + .expect("resolver construction should succeed"); + let verifier = AttestedCertificateVerifier::new_with_provider( + non_empty_root_store(), + AttestationVerifier::expect_none(), + provider, + ) + .expect("verifier construction should succeed"); + let cert = resolver + .state + .certificate + .read() + .expect("certificate lock poisoned") + .first() + .expect("resolver should hold a certificate") + .clone(); + + let result = verifier.verify_server_cert_direct( + &cert, + &ServerName::try_from("foo").expect("server name should be valid"), + UnixTime::now(), + ); + + assert_eq!( + result.unwrap_err(), + Error::InvalidCertificate(CertificateError::ApplicationVerificationFailure) + ); + } + fn test_ca() -> CaCert { let key = KeyPair::generate_for(&PKCS_ECDSA_P256_SHA256) .expect("test CA key generation should succeed"); @@ -982,6 +1106,28 @@ mod tests { CaCert::from_parts(key, cert) } + fn plain_self_signed_certificate(subject_name: &str) -> CertificateDer<'static> { + let key = KeyPair::generate_for(&PKCS_ECDSA_P256_SHA256) + .expect("test key generation should succeed"); + let params = CertificateParams::new(vec![subject_name.to_string()]) + .expect("test certificate params should be created"); + params + .self_signed(&key) + .expect("test certificate should be self-signed") + .der() + .to_vec() + .into() + } + + fn non_empty_root_store() -> RootCertStore { + let mut roots = RootCertStore::empty(); + let ca = test_ca(); + let ca_cert = CertificateDer::from_pem_slice(ca.pem_cert.as_bytes()) + .expect("test CA PEM should parse"); + roots.add(ca_cert).expect("test CA certificate should be trusted"); + roots + } + fn transfer_tls_client_to_server(client: &mut ClientConnection, server: &mut ServerConnection) { let mut tls = Vec::new(); From 1ad4dd1ddd0c5e7a8c1532feeb22b88fd6377ff3 Mon Sep 17 00:00:00 2001 From: peg Date: Fri, 13 Mar 2026 10:16:46 +0100 Subject: [PATCH 06/27] Tidy tests --- crates/attested-tls/src/lib.rs | 71 ++++++++++++++++------------------ 1 file changed, 33 insertions(+), 38 deletions(-) diff --git a/crates/attested-tls/src/lib.rs b/crates/attested-tls/src/lib.rs index cad28a4..1bc7b3d 100644 --- a/crates/attested-tls/src/lib.rs +++ b/crates/attested-tls/src/lib.rs @@ -347,6 +347,7 @@ fn create_report_data( Ok(hasher.finalize().into()) } +/// Verifies attested TLS server or client certificates during TLS handshake #[derive(Debug)] pub struct AttestedCertificateVerifier { server_inner: Arc, @@ -681,16 +682,16 @@ mod tests { async fn certificate_resolver_creates_initial_certificate() { let provider: Arc = aws_lc_rs::default_provider().into(); let resolver = AttestedCertificateResolver::new_with_provider( - AttestationGenerator::new(AttestationType::DcapTdx, None) - .expect("mock generator construction should succeed"), + AttestationGenerator::new(AttestationType::DcapTdx, None).unwrap(), None, "foo".to_string(), vec![], provider, ) .await - .expect("resolver construction should succeed"); - let certificate = resolver.state.certificate.read().expect("certificate lock poisoned"); + .unwrap(); + + let certificate = resolver.state.certificate.read().unwrap(); assert_eq!(certificate.len(), 1); } @@ -700,53 +701,47 @@ mod tests { let provider: Arc = aws_lc_rs::default_provider().into(); let server_name = "foo"; let resolver = AttestedCertificateResolver::new_with_provider( - AttestationGenerator::new(AttestationType::DcapTdx, None) - .expect("mock generator construction should succeed"), + AttestationGenerator::new(AttestationType::DcapTdx, None).unwrap(), None, server_name.to_string(), vec![], provider.clone(), ) .await - .expect("resolver construction should succeed"); - let server_certificate = resolver - .state - .certificate - .read() - .expect("certificate lock poisoned") - .first() - .expect("resolver should hold a certificate") - .clone(); + .unwrap(); + + let server_certificate = + resolver.state.certificate.read().unwrap().first().unwrap().clone(); let mut roots = RootCertStore::empty(); - roots.add(server_certificate).expect("resolver certificate should be trusted"); + roots.add(server_certificate).unwrap(); let verifier = AttestedCertificateVerifier::new_with_provider( roots, AttestationVerifier::mock(), provider.clone(), ) - .expect("verifier construction should succeed"); + .unwrap(); let server_config = ServerConfig::builder_with_provider(provider.clone()) .with_safe_default_protocol_versions() - .expect("server config should support default protocol versions") + .unwrap() .with_no_client_auth() .with_cert_resolver(Arc::new(resolver)); let client_config = ClientConfig::builder_with_provider(provider) .with_safe_default_protocol_versions() - .expect("client config should support default protocol versions") + .unwrap() .dangerous() .with_custom_certificate_verifier(Arc::new(verifier)) .with_no_client_auth(); let mut client = ClientConnection::new( Arc::new(client_config), - ServerName::try_from(server_name).expect("server name should be valid"), + ServerName::try_from(server_name).unwrap(), ) - .expect("client connection should be created"); - let mut server = - ServerConnection::new(Arc::new(server_config)).expect("server connection should exist"); + .unwrap(); + + let mut server = ServerConnection::new(Arc::new(server_config)).unwrap(); while client.is_handshaking() || server.is_handshaking() { transfer_tls_client_to_server(&mut client, &mut server); @@ -762,52 +757,52 @@ mod tests { let provider: Arc = aws_lc_rs::default_provider().into(); let server_name = "foo"; let ca = test_ca(); - let ca_cert = CertificateDer::from_pem_slice(ca.pem_cert.as_bytes()) - .expect("test CA PEM should parse"); + let ca_cert = CertificateDer::from_pem_slice(ca.pem_cert.as_bytes()).unwrap(); + let resolver = AttestedCertificateResolver::new_with_provider( - AttestationGenerator::new(AttestationType::DcapTdx, None) - .expect("mock generator construction should succeed"), + AttestationGenerator::new(AttestationType::DcapTdx, None).unwrap(), Some(ca), server_name.to_string(), vec![], provider.clone(), ) .await - .expect("resolver construction should succeed"); - let certificate_chain = - resolver.state.certificate.read().expect("certificate lock poisoned").clone(); + .unwrap(); + + let certificate_chain = resolver.state.certificate.read().unwrap().clone(); assert_eq!(certificate_chain.len(), 2); let mut roots = RootCertStore::empty(); - roots.add(ca_cert).expect("CA certificate should be trusted"); + roots.add(ca_cert).unwrap(); let verifier = AttestedCertificateVerifier::new_with_provider( roots, AttestationVerifier::mock(), provider.clone(), ) - .expect("verifier construction should succeed"); + .unwrap(); let server_config = ServerConfig::builder_with_provider(provider.clone()) .with_safe_default_protocol_versions() - .expect("server config should support default protocol versions") + .unwrap() .with_no_client_auth() .with_cert_resolver(Arc::new(resolver)); + let client_config = ClientConfig::builder_with_provider(provider) .with_safe_default_protocol_versions() - .expect("client config should support default protocol versions") + .unwrap() .dangerous() .with_custom_certificate_verifier(Arc::new(verifier)) .with_no_client_auth(); let mut client = ClientConnection::new( Arc::new(client_config), - ServerName::try_from(server_name).expect("server name should be valid"), + ServerName::try_from(server_name).unwrap(), ) - .expect("client connection should be created"); - let mut server = - ServerConnection::new(Arc::new(server_config)).expect("server connection should exist"); + .unwrap(); + + let mut server = ServerConnection::new(Arc::new(server_config)).unwrap(); while client.is_handshaking() || server.is_handshaking() { transfer_tls_client_to_server(&mut client, &mut server); From dcb7492fbbb3742df5d5963cee715e0acbb16397 Mon Sep 17 00:00:00 2001 From: peg Date: Fri, 13 Mar 2026 10:31:35 +0100 Subject: [PATCH 07/27] Improve delay handling in ceritifcate refresh loop --- crates/attested-tls/src/lib.rs | 17 ++++++++++------- 1 file changed, 10 insertions(+), 7 deletions(-) diff --git a/crates/attested-tls/src/lib.rs b/crates/attested-tls/src/lib.rs index 1bc7b3d..af07c5d 100644 --- a/crates/attested-tls/src/lib.rs +++ b/crates/attested-tls/src/lib.rs @@ -241,9 +241,10 @@ impl AttestedCertificateResolver { fn spawn_renewal_task(state: std::sync::Weak) { tokio::spawn(async move { let renewal_delay = CERTIFICATE_VALIDITY - CERTIFICATE_RENEWAL_LEAD_TIME; + let mut next_delay = renewal_delay; loop { - tokio::time::sleep(renewal_delay).await; + tokio::time::sleep(next_delay).await; let Some(current) = state.upgrade() else { tracing::warn!("Resolver has been dropped - stopping renewal loop"); break; @@ -253,12 +254,12 @@ impl AttestedCertificateResolver { Ok(key_pair) => key_pair, Err(e) => { tracing::error!("Failed to load keypair: {e}"); - tokio::time::sleep(CERTIFICATE_RENEWAL_RETRY_DELAY).await; + next_delay = CERTIFICATE_RENEWAL_RETRY_DELAY; continue; } }; - match Self::issue_ra_cert_chain( + next_delay = match Self::issue_ra_cert_chain( &key_pair, current.ca.as_deref(), current.primary_name.as_str(), @@ -270,12 +271,13 @@ impl AttestedCertificateResolver { Ok(certificate) => { *current.certificate.write().expect("Certificate lock poisoned") = certificate; + renewal_delay } Err(e) => { tracing::error!("Failed to renew attested certificate: {e}"); - tokio::time::sleep(CERTIFICATE_RENEWAL_RETRY_DELAY).await; + CERTIFICATE_RENEWAL_RETRY_DELAY } - } + }; } }); } @@ -293,13 +295,13 @@ impl ResolvesClientCert for AttestedCertificateResolver { } fn has_certs(&self) -> bool { - !self.state.certificate.read().expect("certificate lock poisoned").is_empty() + !self.state.certificate.read().expect("Certificate lock poisoned").is_empty() } } impl AttestedCertificateResolver { fn current_certified_key(&self) -> Option> { - let certificate = self.state.certificate.read().expect("certificate lock poisoned").clone(); + let certificate = self.state.certificate.read().expect("Certificate lock poisoned").clone(); Some(Arc::new(CertifiedKey::new(certificate, self.state.key.clone()))) } } @@ -308,6 +310,7 @@ fn default_crypto_provider() -> Result, AttestedTlsError> { CryptoProvider::get_default().cloned().ok_or(AttestedTlsError::CryptoProviderUnavailable) } +/// Ensures that SAN contains the primary hostname fn normalized_subject_alt_names(primary_name: &str, subject_alt_names: Vec) -> Vec { let mut normalized = Vec::with_capacity(subject_alt_names.len() + 1); normalized.push(primary_name.to_string()); From ea409744a9b484528d145baa9c00c831ae0f3ea4 Mon Sep 17 00:00:00 2001 From: peg Date: Fri, 13 Mar 2026 10:48:15 +0100 Subject: [PATCH 08/27] Add test to demonstrate using nested attested tls --- Cargo.lock | 1 + crates/attested-tls/Cargo.toml | 1 + crates/attested-tls/src/lib.rs | 106 +++++++++++++++++++++++++++++++++ 3 files changed, 108 insertions(+) diff --git a/Cargo.lock b/Cargo.lock index 1e7a6e2..f88657a 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -298,6 +298,7 @@ version = "0.0.1" dependencies = [ "anyhow", "attestation", + "nested-tls", "ra-tls", "rcgen 0.14.7", "rustls", diff --git a/crates/attested-tls/Cargo.toml b/crates/attested-tls/Cargo.toml index 4c496b4..4359038 100644 --- a/crates/attested-tls/Cargo.toml +++ b/crates/attested-tls/Cargo.toml @@ -19,6 +19,7 @@ tracing = "0.1.41" [dev-dependencies] attestation = { workspace = true, features = ["mock"] } +nested-tls = { path = "../nested-tls" } [lints] workspace = true diff --git a/crates/attested-tls/src/lib.rs b/crates/attested-tls/src/lib.rs index af07c5d..d52899a 100644 --- a/crates/attested-tls/src/lib.rs +++ b/crates/attested-tls/src/lib.rs @@ -667,6 +667,7 @@ pub enum AttestedTlsError { mod tests { use std::{io::Cursor, sync::Arc}; + use nested_tls::{client::NestingTlsConnector, server::NestingTlsAcceptor}; use ra_tls::rcgen::{BasicConstraints, CertificateParams, IsCa}; use rustls::{ CertificateError, @@ -678,6 +679,7 @@ mod tests { ServerConnection, crypto::aws_lc_rs, }; + use tokio::io::{AsyncReadExt, AsyncWriteExt, duplex}; use super::*; @@ -1093,6 +1095,45 @@ mod tests { ); } + #[tokio::test(flavor = "multi_thread")] + async fn nested_tls_uses_attested_tls_for_inner_session() { + let provider: Arc = aws_lc_rs::default_provider().into(); + let (outer_server, outer_client) = plain_tls_config_pair(provider.clone()); + let inner_server = attested_server_config("localhost", provider.clone()).await; + let inner_client = attested_client_config(provider.clone()); + + let acceptor = NestingTlsAcceptor::new(Arc::new(outer_server), Arc::new(inner_server)); + let connector = NestingTlsConnector::new(Arc::new(outer_client), Arc::new(inner_client)); + + let (client_io, server_io) = duplex(16 * 1024); + + let server = tokio::spawn(async move { + let mut stream = acceptor.accept(server_io).await.expect("nested accept should succeed"); + + let mut req = [0_u8; 5]; + stream.read_exact(&mut req).await.expect("server read should succeed"); + assert_eq!(&req, b"hello"); + + stream.write_all(b"world").await.expect("server write should succeed"); + stream.flush().await.expect("server flush should succeed"); + }); + + let domain = ServerName::try_from("localhost").expect("domain should be valid"); + let mut stream = connector + .connect(domain, client_io) + .await + .expect("nested connect should succeed"); + + stream.write_all(b"hello").await.expect("client write should succeed"); + stream.flush().await.expect("client flush should succeed"); + + let mut resp = [0_u8; 5]; + stream.read_exact(&mut resp).await.expect("client read should succeed"); + assert_eq!(&resp, b"world"); + + server.await.expect("server task should complete"); + } + fn test_ca() -> CaCert { let key = KeyPair::generate_for(&PKCS_ECDSA_P256_SHA256) .expect("test CA key generation should succeed"); @@ -1117,6 +1158,71 @@ mod tests { .into() } + fn plain_tls_config_pair(provider: Arc) -> (ServerConfig, ClientConfig) { + let subject_name = "localhost"; + let key = KeyPair::generate_for(&PKCS_ECDSA_P256_SHA256) + .expect("test key generation should succeed"); + let params = CertificateParams::new(vec![subject_name.to_string()]) + .expect("test certificate params should be created"); + let cert = params.self_signed(&key).expect("test certificate should be self-signed"); + let cert_der: CertificateDer<'static> = cert.der().clone(); + let key_der = + PrivateKeyDer::Pkcs8(PrivatePkcs8KeyDer::from(key.serialize_der())); + + let server = ServerConfig::builder_with_provider(provider.clone()) + .with_safe_default_protocol_versions() + .expect("server config should support default protocol versions") + .with_no_client_auth() + .with_single_cert(vec![cert_der.clone()], key_der) + .expect("server config should be created"); + + let mut roots = RootCertStore::empty(); + roots.add(cert_der).expect("client roots should trust server certificate"); + + let client = ClientConfig::builder_with_provider(provider) + .with_safe_default_protocol_versions() + .expect("client config should support default protocol versions") + .with_root_certificates(roots) + .with_no_client_auth(); + + (server, client) + } + + async fn attested_server_config(server_name: &str, provider: Arc) -> ServerConfig { + let resolver = AttestedCertificateResolver::new_with_provider( + AttestationGenerator::new(AttestationType::DcapTdx, None) + .expect("mock generator construction should succeed"), + None, + server_name.to_string(), + vec![], + provider.clone(), + ) + .await + .expect("resolver construction should succeed"); + + ServerConfig::builder_with_provider(provider) + .with_safe_default_protocol_versions() + .expect("server config should support default protocol versions") + .with_no_client_auth() + .with_cert_resolver(Arc::new(resolver)) + } + + fn attested_client_config(provider: Arc) -> ClientConfig { + let verifier = AttestedCertificateVerifier::new_with_provider( + non_empty_root_store(), + AttestationVerifier::mock(), + provider.clone(), + ) + .expect("verifier construction should succeed"); + + ClientConfig::builder_with_provider(provider) + .with_safe_default_protocol_versions() + .expect("client config should support default protocol versions") + .dangerous() + .with_custom_certificate_verifier(Arc::new(verifier)) + .with_no_client_auth() + } + fn non_empty_root_store() -> RootCertStore { let mut roots = RootCertStore::empty(); let ca = test_ca(); From 643ccfd4a1e13167159e8c67784e34f9072a4ec0 Mon Sep 17 00:00:00 2001 From: peg Date: Fri, 13 Mar 2026 10:59:00 +0100 Subject: [PATCH 09/27] Move nestedtls test to be integration test --- crates/attested-tls/src/lib.rs | 106 ------------------ crates/attested-tls/tests/nested_tls.rs | 140 ++++++++++++++++++++++++ 2 files changed, 140 insertions(+), 106 deletions(-) create mode 100644 crates/attested-tls/tests/nested_tls.rs diff --git a/crates/attested-tls/src/lib.rs b/crates/attested-tls/src/lib.rs index d52899a..af07c5d 100644 --- a/crates/attested-tls/src/lib.rs +++ b/crates/attested-tls/src/lib.rs @@ -667,7 +667,6 @@ pub enum AttestedTlsError { mod tests { use std::{io::Cursor, sync::Arc}; - use nested_tls::{client::NestingTlsConnector, server::NestingTlsAcceptor}; use ra_tls::rcgen::{BasicConstraints, CertificateParams, IsCa}; use rustls::{ CertificateError, @@ -679,7 +678,6 @@ mod tests { ServerConnection, crypto::aws_lc_rs, }; - use tokio::io::{AsyncReadExt, AsyncWriteExt, duplex}; use super::*; @@ -1095,45 +1093,6 @@ mod tests { ); } - #[tokio::test(flavor = "multi_thread")] - async fn nested_tls_uses_attested_tls_for_inner_session() { - let provider: Arc = aws_lc_rs::default_provider().into(); - let (outer_server, outer_client) = plain_tls_config_pair(provider.clone()); - let inner_server = attested_server_config("localhost", provider.clone()).await; - let inner_client = attested_client_config(provider.clone()); - - let acceptor = NestingTlsAcceptor::new(Arc::new(outer_server), Arc::new(inner_server)); - let connector = NestingTlsConnector::new(Arc::new(outer_client), Arc::new(inner_client)); - - let (client_io, server_io) = duplex(16 * 1024); - - let server = tokio::spawn(async move { - let mut stream = acceptor.accept(server_io).await.expect("nested accept should succeed"); - - let mut req = [0_u8; 5]; - stream.read_exact(&mut req).await.expect("server read should succeed"); - assert_eq!(&req, b"hello"); - - stream.write_all(b"world").await.expect("server write should succeed"); - stream.flush().await.expect("server flush should succeed"); - }); - - let domain = ServerName::try_from("localhost").expect("domain should be valid"); - let mut stream = connector - .connect(domain, client_io) - .await - .expect("nested connect should succeed"); - - stream.write_all(b"hello").await.expect("client write should succeed"); - stream.flush().await.expect("client flush should succeed"); - - let mut resp = [0_u8; 5]; - stream.read_exact(&mut resp).await.expect("client read should succeed"); - assert_eq!(&resp, b"world"); - - server.await.expect("server task should complete"); - } - fn test_ca() -> CaCert { let key = KeyPair::generate_for(&PKCS_ECDSA_P256_SHA256) .expect("test CA key generation should succeed"); @@ -1158,71 +1117,6 @@ mod tests { .into() } - fn plain_tls_config_pair(provider: Arc) -> (ServerConfig, ClientConfig) { - let subject_name = "localhost"; - let key = KeyPair::generate_for(&PKCS_ECDSA_P256_SHA256) - .expect("test key generation should succeed"); - let params = CertificateParams::new(vec![subject_name.to_string()]) - .expect("test certificate params should be created"); - let cert = params.self_signed(&key).expect("test certificate should be self-signed"); - let cert_der: CertificateDer<'static> = cert.der().clone(); - let key_der = - PrivateKeyDer::Pkcs8(PrivatePkcs8KeyDer::from(key.serialize_der())); - - let server = ServerConfig::builder_with_provider(provider.clone()) - .with_safe_default_protocol_versions() - .expect("server config should support default protocol versions") - .with_no_client_auth() - .with_single_cert(vec![cert_der.clone()], key_der) - .expect("server config should be created"); - - let mut roots = RootCertStore::empty(); - roots.add(cert_der).expect("client roots should trust server certificate"); - - let client = ClientConfig::builder_with_provider(provider) - .with_safe_default_protocol_versions() - .expect("client config should support default protocol versions") - .with_root_certificates(roots) - .with_no_client_auth(); - - (server, client) - } - - async fn attested_server_config(server_name: &str, provider: Arc) -> ServerConfig { - let resolver = AttestedCertificateResolver::new_with_provider( - AttestationGenerator::new(AttestationType::DcapTdx, None) - .expect("mock generator construction should succeed"), - None, - server_name.to_string(), - vec![], - provider.clone(), - ) - .await - .expect("resolver construction should succeed"); - - ServerConfig::builder_with_provider(provider) - .with_safe_default_protocol_versions() - .expect("server config should support default protocol versions") - .with_no_client_auth() - .with_cert_resolver(Arc::new(resolver)) - } - - fn attested_client_config(provider: Arc) -> ClientConfig { - let verifier = AttestedCertificateVerifier::new_with_provider( - non_empty_root_store(), - AttestationVerifier::mock(), - provider.clone(), - ) - .expect("verifier construction should succeed"); - - ClientConfig::builder_with_provider(provider) - .with_safe_default_protocol_versions() - .expect("client config should support default protocol versions") - .dangerous() - .with_custom_certificate_verifier(Arc::new(verifier)) - .with_no_client_auth() - } - fn non_empty_root_store() -> RootCertStore { let mut roots = RootCertStore::empty(); let ca = test_ca(); diff --git a/crates/attested-tls/tests/nested_tls.rs b/crates/attested-tls/tests/nested_tls.rs new file mode 100644 index 0000000..ccd4cae --- /dev/null +++ b/crates/attested-tls/tests/nested_tls.rs @@ -0,0 +1,140 @@ +use std::sync::Arc; + +use attestation::{AttestationGenerator, AttestationType, AttestationVerifier}; +use attested_tls::{AttestedCertificateResolver, AttestedCertificateVerifier}; +use nested_tls::{client::NestingTlsConnector, server::NestingTlsAcceptor}; +use ra_tls::rcgen::{KeyPair, PKCS_ECDSA_P256_SHA256}; +use rustls::{ + ClientConfig, + RootCertStore, + ServerConfig, + crypto::{CryptoProvider, aws_lc_rs}, + pki_types::{CertificateDer, PrivateKeyDer, PrivatePkcs8KeyDer, ServerName, pem::PemObject}, +}; +use tokio::io::{AsyncReadExt, AsyncWriteExt, duplex}; + +#[tokio::test(flavor = "multi_thread")] +async fn nested_tls_uses_attested_tls_for_inner_session() { + let provider: Arc = aws_lc_rs::default_provider().into(); + let (outer_server, outer_client) = plain_tls_config_pair(provider.clone()); + let inner_server = attested_server_config("localhost", provider.clone()).await; + let inner_client = attested_client_config(provider.clone()); + + let acceptor = NestingTlsAcceptor::new(Arc::new(outer_server), Arc::new(inner_server)); + let connector = NestingTlsConnector::new(Arc::new(outer_client), Arc::new(inner_client)); + + let (client_io, server_io) = duplex(16 * 1024); + + let server = tokio::spawn(async move { + let mut stream = acceptor.accept(server_io).await.expect("nested accept should succeed"); + + let mut req = [0_u8; 5]; + stream.read_exact(&mut req).await.expect("server read should succeed"); + assert_eq!(&req, b"hello"); + + stream.write_all(b"world").await.expect("server write should succeed"); + stream.flush().await.expect("server flush should succeed"); + }); + + let domain = ServerName::try_from("localhost").expect("domain should be valid"); + let mut stream = connector + .connect(domain, client_io) + .await + .expect("nested connect should succeed"); + + stream.write_all(b"hello").await.expect("client write should succeed"); + stream.flush().await.expect("client flush should succeed"); + + let mut resp = [0_u8; 5]; + stream.read_exact(&mut resp).await.expect("client read should succeed"); + assert_eq!(&resp, b"world"); + + server.await.expect("server task should complete"); +} + +fn plain_tls_config_pair(provider: Arc) -> (ServerConfig, ClientConfig) { + let subject_name = "localhost"; + let key = KeyPair::generate_for(&PKCS_ECDSA_P256_SHA256) + .expect("test key generation should succeed"); + let mut params = ra_tls::rcgen::CertificateParams::new(vec![subject_name.to_string()]) + .expect("test certificate params should be created"); + params + .subject_alt_names + .push(ra_tls::rcgen::SanType::DnsName(subject_name.try_into().expect("valid dns name"))); + let cert = params.self_signed(&key).expect("test certificate should be self-signed"); + let cert_der: CertificateDer<'static> = cert.der().clone(); + let key_der = PrivateKeyDer::Pkcs8(PrivatePkcs8KeyDer::from(key.serialize_der())); + + let server = ServerConfig::builder_with_provider(provider.clone()) + .with_safe_default_protocol_versions() + .expect("server config should support default protocol versions") + .with_no_client_auth() + .with_single_cert(vec![cert_der.clone()], key_der) + .expect("server config should be created"); + + let mut roots = RootCertStore::empty(); + roots.add(cert_der).expect("client roots should trust server certificate"); + + let client = ClientConfig::builder_with_provider(provider) + .with_safe_default_protocol_versions() + .expect("client config should support default protocol versions") + .with_root_certificates(roots) + .with_no_client_auth(); + + (server, client) +} + +async fn attested_server_config(server_name: &str, provider: Arc) -> ServerConfig { + let resolver = AttestedCertificateResolver::new_with_provider( + AttestationGenerator::new(AttestationType::DcapTdx, None) + .expect("mock generator construction should succeed"), + None, + server_name.to_string(), + vec![], + provider.clone(), + ) + .await + .expect("resolver construction should succeed"); + + ServerConfig::builder_with_provider(provider) + .with_safe_default_protocol_versions() + .expect("server config should support default protocol versions") + .with_no_client_auth() + .with_cert_resolver(Arc::new(resolver)) +} + +fn attested_client_config(provider: Arc) -> ClientConfig { + let verifier = AttestedCertificateVerifier::new_with_provider( + non_empty_root_store(), + AttestationVerifier::mock(), + provider.clone(), + ) + .expect("verifier construction should succeed"); + + ClientConfig::builder_with_provider(provider) + .with_safe_default_protocol_versions() + .expect("client config should support default protocol versions") + .dangerous() + .with_custom_certificate_verifier(Arc::new(verifier)) + .with_no_client_auth() +} + +fn non_empty_root_store() -> RootCertStore { + let mut roots = RootCertStore::empty(); + let ca = test_ca(); + let ca_cert = CertificateDer::from_pem_slice(ca.pem_cert.as_bytes()) + .expect("test CA PEM should parse"); + roots.add(ca_cert).expect("test CA certificate should be trusted"); + roots +} + +fn test_ca() -> ra_tls::cert::CaCert { + let key = KeyPair::generate_for(&PKCS_ECDSA_P256_SHA256) + .expect("test CA key generation should succeed"); + let mut params = ra_tls::rcgen::CertificateParams::new(vec!["test-ca".to_string()]) + .expect("test CA params should be created"); + params.is_ca = ra_tls::rcgen::IsCa::Ca(ra_tls::rcgen::BasicConstraints::Unconstrained); + let cert = params.self_signed(&key).expect("test CA certificate should be self-signed"); + + ra_tls::cert::CaCert::from_parts(key, cert) +} From fa5a156ed9017b652a3eaf91015266f97966497c Mon Sep 17 00:00:00 2001 From: peg Date: Fri, 13 Mar 2026 10:59:56 +0100 Subject: [PATCH 10/27] Fmt --- crates/attested-tls/tests/nested_tls.rs | 14 ++++++-------- 1 file changed, 6 insertions(+), 8 deletions(-) diff --git a/crates/attested-tls/tests/nested_tls.rs b/crates/attested-tls/tests/nested_tls.rs index ccd4cae..380f2c3 100644 --- a/crates/attested-tls/tests/nested_tls.rs +++ b/crates/attested-tls/tests/nested_tls.rs @@ -37,10 +37,8 @@ async fn nested_tls_uses_attested_tls_for_inner_session() { }); let domain = ServerName::try_from("localhost").expect("domain should be valid"); - let mut stream = connector - .connect(domain, client_io) - .await - .expect("nested connect should succeed"); + let mut stream = + connector.connect(domain, client_io).await.expect("nested connect should succeed"); stream.write_all(b"hello").await.expect("client write should succeed"); stream.flush().await.expect("client flush should succeed"); @@ -54,8 +52,8 @@ async fn nested_tls_uses_attested_tls_for_inner_session() { fn plain_tls_config_pair(provider: Arc) -> (ServerConfig, ClientConfig) { let subject_name = "localhost"; - let key = KeyPair::generate_for(&PKCS_ECDSA_P256_SHA256) - .expect("test key generation should succeed"); + let key = + KeyPair::generate_for(&PKCS_ECDSA_P256_SHA256).expect("test key generation should succeed"); let mut params = ra_tls::rcgen::CertificateParams::new(vec![subject_name.to_string()]) .expect("test certificate params should be created"); params @@ -122,8 +120,8 @@ fn attested_client_config(provider: Arc) -> ClientConfig { fn non_empty_root_store() -> RootCertStore { let mut roots = RootCertStore::empty(); let ca = test_ca(); - let ca_cert = CertificateDer::from_pem_slice(ca.pem_cert.as_bytes()) - .expect("test CA PEM should parse"); + let ca_cert = + CertificateDer::from_pem_slice(ca.pem_cert.as_bytes()).expect("test CA PEM should parse"); roots.add(ca_cert).expect("test CA certificate should be trusted"); roots } From c9207f46ef67d7ff18337ee672eb3e1f7677a748 Mon Sep 17 00:00:00 2001 From: peg Date: Fri, 13 Mar 2026 11:37:45 +0100 Subject: [PATCH 11/27] Make root store optional for verifier --- crates/attested-tls/src/lib.rs | 87 ++++++++----------------- crates/attested-tls/tests/nested_tls.rs | 24 +------ 2 files changed, 29 insertions(+), 82 deletions(-) diff --git a/crates/attested-tls/src/lib.rs b/crates/attested-tls/src/lib.rs index af07c5d..7672b28 100644 --- a/crates/attested-tls/src/lib.rs +++ b/crates/attested-tls/src/lib.rs @@ -360,18 +360,21 @@ pub struct AttestedCertificateVerifier { impl AttestedCertificateVerifier { pub fn new( - root_store: RootCertStore, + root_store: Option, attestation_verifier: AttestationVerifier, ) -> Result { Self::new_with_provider(root_store, attestation_verifier, default_crypto_provider()?) } pub fn new_with_provider( - root_store: RootCertStore, + root_store: Option, attestation_verifier: AttestationVerifier, provider: Arc, ) -> Result { - let root_store = Arc::new(root_store); + let root_store = Arc::new(match root_store { + Some(root_store) => root_store, + None => Self::synthetic_root_store()?, + }); let server_inner = WebPkiServerVerifier::builder_with_provider(root_store.clone(), provider.clone()) .build() @@ -383,6 +386,19 @@ impl AttestedCertificateVerifier { Ok(Self { server_inner, client_inner, attestation_verifier }) } + fn synthetic_root_store() -> Result { + let key_pair = KeyPair::generate_for(&PKCS_ECDSA_P256_SHA256)?; + let mut params = + ra_tls::rcgen::CertificateParams::new(vec!["attested-tls-placeholder-ca".to_string()])?; + params.is_ca = ra_tls::rcgen::IsCa::Ca(ra_tls::rcgen::BasicConstraints::Unconstrained); + let cert = params.self_signed(&key_pair)?; + + let mut root_store = RootCertStore::empty(); + root_store.add(cert.der().clone())?; + + Ok(root_store) + } + fn extract_custom_attestation_from_cert( cert: &CertificateDer<'_>, ) -> Result { @@ -713,14 +729,8 @@ mod tests { .await .unwrap(); - let server_certificate = - resolver.state.certificate.read().unwrap().first().unwrap().clone(); - - let mut roots = RootCertStore::empty(); - roots.add(server_certificate).unwrap(); - let verifier = AttestedCertificateVerifier::new_with_provider( - roots, + None, AttestationVerifier::mock(), provider.clone(), ) @@ -780,7 +790,7 @@ mod tests { roots.add(ca_cert).unwrap(); let verifier = AttestedCertificateVerifier::new_with_provider( - roots, + Some(roots), AttestationVerifier::mock(), provider.clone(), ) @@ -882,36 +892,14 @@ mod tests { .await .expect("client resolver construction should succeed"); - let server_certificate = server_resolver - .state - .certificate - .read() - .expect("certificate lock poisoned") - .first() - .expect("resolver should hold a certificate") - .clone(); - let client_certificate = client_resolver - .state - .certificate - .read() - .expect("certificate lock poisoned") - .first() - .expect("resolver should hold a certificate") - .clone(); - - let mut client_roots = RootCertStore::empty(); - client_roots.add(server_certificate).expect("server certificate should be trusted"); - let mut server_roots = RootCertStore::empty(); - server_roots.add(client_certificate).expect("client certificate should be trusted"); - let server_verifier = AttestedCertificateVerifier::new_with_provider( - server_roots, + None, AttestationVerifier::mock(), provider.clone(), ) .expect("server verifier construction should succeed"); let client_verifier = AttestedCertificateVerifier::new_with_provider( - client_roots, + None, AttestationVerifier::mock(), provider.clone(), ) @@ -963,20 +951,8 @@ mod tests { ) .await .expect("resolver construction should succeed"); - let server_certificate = resolver - .state - .certificate - .read() - .expect("certificate lock poisoned") - .first() - .expect("resolver should hold a certificate") - .clone(); - - let mut roots = RootCertStore::empty(); - roots.add(server_certificate).expect("resolver certificate should be trusted"); - let verifier = AttestedCertificateVerifier::new_with_provider( - roots, + None, AttestationVerifier::mock(), provider.clone(), ) @@ -1015,7 +991,7 @@ mod tests { async fn malformed_certificate_returns_bad_encoding() { let provider: Arc = aws_lc_rs::default_provider().into(); let verifier = AttestedCertificateVerifier::new_with_provider( - non_empty_root_store(), + None, AttestationVerifier::mock(), provider, ) @@ -1038,7 +1014,7 @@ mod tests { let mut roots = RootCertStore::empty(); roots.add(cert.clone()).expect("plain certificate should be trusted"); let verifier = AttestedCertificateVerifier::new_with_provider( - roots, + Some(roots), AttestationVerifier::mock(), provider, ) @@ -1067,7 +1043,7 @@ mod tests { .await .expect("resolver construction should succeed"); let verifier = AttestedCertificateVerifier::new_with_provider( - non_empty_root_store(), + None, AttestationVerifier::expect_none(), provider, ) @@ -1117,15 +1093,6 @@ mod tests { .into() } - fn non_empty_root_store() -> RootCertStore { - let mut roots = RootCertStore::empty(); - let ca = test_ca(); - let ca_cert = CertificateDer::from_pem_slice(ca.pem_cert.as_bytes()) - .expect("test CA PEM should parse"); - roots.add(ca_cert).expect("test CA certificate should be trusted"); - roots - } - fn transfer_tls_client_to_server(client: &mut ClientConnection, server: &mut ServerConnection) { let mut tls = Vec::new(); diff --git a/crates/attested-tls/tests/nested_tls.rs b/crates/attested-tls/tests/nested_tls.rs index 380f2c3..030c259 100644 --- a/crates/attested-tls/tests/nested_tls.rs +++ b/crates/attested-tls/tests/nested_tls.rs @@ -9,7 +9,7 @@ use rustls::{ RootCertStore, ServerConfig, crypto::{CryptoProvider, aws_lc_rs}, - pki_types::{CertificateDer, PrivateKeyDer, PrivatePkcs8KeyDer, ServerName, pem::PemObject}, + pki_types::{CertificateDer, PrivateKeyDer, PrivatePkcs8KeyDer, ServerName}, }; use tokio::io::{AsyncReadExt, AsyncWriteExt, duplex}; @@ -103,7 +103,7 @@ async fn attested_server_config(server_name: &str, provider: Arc fn attested_client_config(provider: Arc) -> ClientConfig { let verifier = AttestedCertificateVerifier::new_with_provider( - non_empty_root_store(), + None, AttestationVerifier::mock(), provider.clone(), ) @@ -116,23 +116,3 @@ fn attested_client_config(provider: Arc) -> ClientConfig { .with_custom_certificate_verifier(Arc::new(verifier)) .with_no_client_auth() } - -fn non_empty_root_store() -> RootCertStore { - let mut roots = RootCertStore::empty(); - let ca = test_ca(); - let ca_cert = - CertificateDer::from_pem_slice(ca.pem_cert.as_bytes()).expect("test CA PEM should parse"); - roots.add(ca_cert).expect("test CA certificate should be trusted"); - roots -} - -fn test_ca() -> ra_tls::cert::CaCert { - let key = KeyPair::generate_for(&PKCS_ECDSA_P256_SHA256) - .expect("test CA key generation should succeed"); - let mut params = ra_tls::rcgen::CertificateParams::new(vec!["test-ca".to_string()]) - .expect("test CA params should be created"); - params.is_ca = ra_tls::rcgen::IsCa::Ca(ra_tls::rcgen::BasicConstraints::Unconstrained); - let cert = params.self_signed(&key).expect("test CA certificate should be self-signed"); - - ra_tls::cert::CaCert::from_parts(key, cert) -} From fc6203d169dafa031b2c35ce44eed72bfe529891 Mon Sep 17 00:00:00 2001 From: peg Date: Fri, 13 Mar 2026 11:49:26 +0100 Subject: [PATCH 12/27] Tidy optional root store logic --- crates/attested-tls/src/lib.rs | 130 +++++++++++++++++++-------------- 1 file changed, 77 insertions(+), 53 deletions(-) diff --git a/crates/attested-tls/src/lib.rs b/crates/attested-tls/src/lib.rs index 7672b28..23d345d 100644 --- a/crates/attested-tls/src/lib.rs +++ b/crates/attested-tls/src/lib.rs @@ -353,8 +353,9 @@ fn create_report_data( /// Verifies attested TLS server or client certificates during TLS handshake #[derive(Debug)] pub struct AttestedCertificateVerifier { - server_inner: Arc, - client_inner: Arc, + server_inner: Option>, + client_inner: Option>, + provider: Arc, attestation_verifier: AttestationVerifier, } @@ -371,32 +372,25 @@ impl AttestedCertificateVerifier { attestation_verifier: AttestationVerifier, provider: Arc, ) -> Result { - let root_store = Arc::new(match root_store { - Some(root_store) => root_store, - None => Self::synthetic_root_store()?, - }); - let server_inner = - WebPkiServerVerifier::builder_with_provider(root_store.clone(), provider.clone()) + let (server_inner, client_inner) = match root_store { + Some(root_store) => { + let root_store = Arc::new(root_store); + let server_inner = WebPkiServerVerifier::builder_with_provider( + root_store.clone(), + provider.clone(), + ) .build() .map_err(AttestedTlsError::VerifierBuilder)?; - let client_inner = WebPkiClientVerifier::builder_with_provider(root_store, provider) - .build() - .map_err(AttestedTlsError::VerifierBuilder)?; - - Ok(Self { server_inner, client_inner, attestation_verifier }) - } + let client_inner = WebPkiClientVerifier::builder_with_provider(root_store, provider.clone()) + .build() + .map_err(AttestedTlsError::VerifierBuilder)?; - fn synthetic_root_store() -> Result { - let key_pair = KeyPair::generate_for(&PKCS_ECDSA_P256_SHA256)?; - let mut params = - ra_tls::rcgen::CertificateParams::new(vec!["attested-tls-placeholder-ca".to_string()])?; - params.is_ca = ra_tls::rcgen::IsCa::Ca(ra_tls::rcgen::BasicConstraints::Unconstrained); - let cert = params.self_signed(&key_pair)?; - - let mut root_store = RootCertStore::empty(); - root_store.add(cert.der().clone())?; + (Some(server_inner), Some(client_inner)) + } + None => (None, None), + }; - Ok(root_store) + Ok(Self { server_inner, client_inner, provider, attestation_verifier }) } fn extract_custom_attestation_from_cert( @@ -555,20 +549,24 @@ impl ServerCertVerifier for AttestedCertificateVerifier { ocsp_response: &[u8], now: UnixTime, ) -> Result { - match self.server_inner.verify_server_cert( - end_entity, - intermediates, - server_name, - ocsp_response, - now, - ) { - Err(rustls::Error::InvalidCertificate(rustls::CertificateError::UnknownIssuer)) => { - // handle self-signed certs differently - Self::verify_cert_time_validity(end_entity, now)?; + if let Some(server_inner) = &self.server_inner { + match server_inner.verify_server_cert( + end_entity, + intermediates, + server_name, + ocsp_response, + now, + ) { + Err(rustls::Error::InvalidCertificate(rustls::CertificateError::UnknownIssuer)) => { + // handle self-signed certs differently + Self::verify_cert_time_validity(end_entity, now)?; + } + Err(err) => return Err(err), + Ok(_) => {} } - Err(err) => return Err(err), - Ok(_) => {} - }; + } else { + Self::verify_cert_time_validity(end_entity, now)?; + } self.verify_attestation_binding(end_entity)?; Ok(ServerCertVerified::assertion()) } @@ -579,7 +577,12 @@ impl ServerCertVerifier for AttestedCertificateVerifier { cert: &CertificateDer<'_>, dss: &DigitallySignedStruct, ) -> Result { - self.server_inner.verify_tls12_signature(message, cert, dss) + rustls::crypto::verify_tls12_signature( + message, + cert, + dss, + &self.provider.signature_verification_algorithms, + ) } fn verify_tls13_signature( @@ -588,29 +591,36 @@ impl ServerCertVerifier for AttestedCertificateVerifier { cert: &CertificateDer<'_>, dss: &DigitallySignedStruct, ) -> Result { - self.server_inner.verify_tls13_signature(message, cert, dss) + rustls::crypto::verify_tls13_signature( + message, + cert, + dss, + &self.provider.signature_verification_algorithms, + ) } fn supported_verify_schemes(&self) -> Vec { - self.server_inner.supported_verify_schemes() + self.provider.signature_verification_algorithms.supported_schemes() } fn root_hint_subjects(&self) -> Option<&[DistinguishedName]> { - self.server_inner.root_hint_subjects() + self.server_inner.as_ref().and_then(|server_inner| server_inner.root_hint_subjects()) } } impl ClientCertVerifier for AttestedCertificateVerifier { fn offer_client_auth(&self) -> bool { - self.client_inner.offer_client_auth() + self.client_inner.as_ref().map_or(true, |client_inner| client_inner.offer_client_auth()) } fn client_auth_mandatory(&self) -> bool { - self.client_inner.client_auth_mandatory() + self.client_inner + .as_ref() + .map_or(true, |client_inner| client_inner.client_auth_mandatory()) } fn root_hint_subjects(&self) -> &[DistinguishedName] { - self.client_inner.root_hint_subjects() + self.client_inner.as_ref().map_or(&[], |client_inner| client_inner.root_hint_subjects()) } fn verify_client_cert( @@ -619,13 +629,17 @@ impl ClientCertVerifier for AttestedCertificateVerifier { intermediates: &[CertificateDer<'_>], now: UnixTime, ) -> Result { - match self.client_inner.verify_client_cert(end_entity, intermediates, now) { - Err(rustls::Error::InvalidCertificate(rustls::CertificateError::UnknownIssuer)) => { - Self::verify_cert_time_validity(end_entity, now)?; + if let Some(client_inner) = &self.client_inner { + match client_inner.verify_client_cert(end_entity, intermediates, now) { + Err(rustls::Error::InvalidCertificate(rustls::CertificateError::UnknownIssuer)) => { + Self::verify_cert_time_validity(end_entity, now)?; + } + Err(err) => return Err(err), + Ok(_) => {} } - Err(err) => return Err(err), - Ok(_) => {} - }; + } else { + Self::verify_cert_time_validity(end_entity, now)?; + } self.verify_attestation_binding(end_entity)?; Ok(ClientCertVerified::assertion()) } @@ -636,7 +650,12 @@ impl ClientCertVerifier for AttestedCertificateVerifier { cert: &CertificateDer<'_>, dss: &DigitallySignedStruct, ) -> Result { - self.client_inner.verify_tls12_signature(message, cert, dss) + rustls::crypto::verify_tls12_signature( + message, + cert, + dss, + &self.provider.signature_verification_algorithms, + ) } fn verify_tls13_signature( @@ -645,11 +664,16 @@ impl ClientCertVerifier for AttestedCertificateVerifier { cert: &CertificateDer<'_>, dss: &DigitallySignedStruct, ) -> Result { - self.client_inner.verify_tls13_signature(message, cert, dss) + rustls::crypto::verify_tls13_signature( + message, + cert, + dss, + &self.provider.signature_verification_algorithms, + ) } fn supported_verify_schemes(&self) -> Vec { - self.client_inner.supported_verify_schemes() + self.provider.signature_verification_algorithms.supported_schemes() } } From d58141eeac30756abe3ced23ebb182c136c70320 Mon Sep 17 00:00:00 2001 From: peg Date: Fri, 13 Mar 2026 12:57:06 +0100 Subject: [PATCH 13/27] Fmt --- crates/attested-tls/src/lib.rs | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/crates/attested-tls/src/lib.rs b/crates/attested-tls/src/lib.rs index 23d345d..13f01a8 100644 --- a/crates/attested-tls/src/lib.rs +++ b/crates/attested-tls/src/lib.rs @@ -381,9 +381,10 @@ impl AttestedCertificateVerifier { ) .build() .map_err(AttestedTlsError::VerifierBuilder)?; - let client_inner = WebPkiClientVerifier::builder_with_provider(root_store, provider.clone()) - .build() - .map_err(AttestedTlsError::VerifierBuilder)?; + let client_inner = + WebPkiClientVerifier::builder_with_provider(root_store, provider.clone()) + .build() + .map_err(AttestedTlsError::VerifierBuilder)?; (Some(server_inner), Some(client_inner)) } @@ -614,9 +615,7 @@ impl ClientCertVerifier for AttestedCertificateVerifier { } fn client_auth_mandatory(&self) -> bool { - self.client_inner - .as_ref() - .map_or(true, |client_inner| client_inner.client_auth_mandatory()) + self.client_inner.as_ref().map_or(true, |client_inner| client_inner.client_auth_mandatory()) } fn root_hint_subjects(&self) -> &[DistinguishedName] { From 5e7af15f3e983a734442260a88ec125163d36d7c Mon Sep 17 00:00:00 2001 From: peg Date: Mon, 16 Mar 2026 08:07:53 +0100 Subject: [PATCH 14/27] Clippy --- crates/attested-tls/src/lib.rs | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/crates/attested-tls/src/lib.rs b/crates/attested-tls/src/lib.rs index 13f01a8..ef39d2f 100644 --- a/crates/attested-tls/src/lib.rs +++ b/crates/attested-tls/src/lib.rs @@ -611,11 +611,13 @@ impl ServerCertVerifier for AttestedCertificateVerifier { impl ClientCertVerifier for AttestedCertificateVerifier { fn offer_client_auth(&self) -> bool { - self.client_inner.as_ref().map_or(true, |client_inner| client_inner.offer_client_auth()) + self.client_inner.as_ref().is_none_or(|client_inner| client_inner.offer_client_auth()) } fn client_auth_mandatory(&self) -> bool { - self.client_inner.as_ref().map_or(true, |client_inner| client_inner.client_auth_mandatory()) + self.client_inner + .as_ref() + .is_none_or(|client_inner| client_inner.client_auth_mandatory()) } fn root_hint_subjects(&self) -> &[DistinguishedName] { From c58fcfb9dc803851e65fc47cc00bf25c5906b9f9 Mon Sep 17 00:00:00 2001 From: peg Date: Mon, 16 Mar 2026 08:13:39 +0100 Subject: [PATCH 15/27] Tidy, add debug impl for ResolverState --- crates/attested-tls/src/lib.rs | 29 +++++++++++++++++++---------- 1 file changed, 19 insertions(+), 10 deletions(-) diff --git a/crates/attested-tls/src/lib.rs b/crates/attested-tls/src/lib.rs index ef39d2f..212a565 100644 --- a/crates/attested-tls/src/lib.rs +++ b/crates/attested-tls/src/lib.rs @@ -1,3 +1,4 @@ +//! An attested TLS certificate resolver and verifier use std::{ fmt, sync::{Arc, RwLock}, @@ -66,18 +67,12 @@ const CERTIFICATE_RENEWAL_RETRY_DELAY: Duration = Duration::from_millis(200); /// A TLS certificate resolver which includes an attestation as a /// certificate extension -#[derive(Clone)] +#[derive(Clone, Debug)] pub struct AttestedCertificateResolver { /// Cloneable inner state state: Arc, } -impl fmt::Debug for AttestedCertificateResolver { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - f.debug_struct("AttestedCertificateResolver").finish_non_exhaustive() - } -} - /// Internal state used by the resolver and its renewal loop struct ResolverState { /// The private TLS key in a format ready to be @@ -98,6 +93,22 @@ struct ResolverState { subject_alt_names: Vec, } +impl fmt::Debug for ResolverState { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + let certificate_chain_len = self.certificate.read().ok().map(|certs| certs.len()); + + f.debug_struct("ResolverState") + .field("key", &"") + .field("ca_present", &self.ca.is_some()) + .field("key_pair_der_len", &self.key_pair_der.len()) + .field("certificate_chain_len", &certificate_chain_len) + .field("attestation_generator", &self.attestation_generator) + .field("primary_name", &self.primary_name) + .field("subject_alt_names", &self.subject_alt_names) + .finish() + } +} + impl AttestedCertificateResolver { /// Create a certificate resolver with a given attestation generator /// A private cerificate authority can also be given - otherwise @@ -615,9 +626,7 @@ impl ClientCertVerifier for AttestedCertificateVerifier { } fn client_auth_mandatory(&self) -> bool { - self.client_inner - .as_ref() - .is_none_or(|client_inner| client_inner.client_auth_mandatory()) + self.client_inner.as_ref().is_none_or(|client_inner| client_inner.client_auth_mandatory()) } fn root_hint_subjects(&self) -> &[DistinguishedName] { From 92071edb509388bb2fab175bb67747ab30d15d84 Mon Sep 17 00:00:00 2001 From: peg Date: Mon, 16 Mar 2026 08:30:57 +0100 Subject: [PATCH 16/27] Add comments --- crates/attested-tls/src/lib.rs | 18 ++++++++++++++---- 1 file changed, 14 insertions(+), 4 deletions(-) diff --git a/crates/attested-tls/src/lib.rs b/crates/attested-tls/src/lib.rs index 212a565..73099e3 100644 --- a/crates/attested-tls/src/lib.rs +++ b/crates/attested-tls/src/lib.rs @@ -11,9 +11,10 @@ pub use attestation::{ AttestationType, AttestationVerifier, }; +pub use ra_tls::cert::CaCert; use ra_tls::{ attestation::{Attestation, AttestationQuote, VersionedAttestation}, - cert::{CaCert, CertRequest}, + cert::CertRequest, rcgen::{KeyPair, PKCS_ECDSA_P256_SHA256}, }; use rustls::{ @@ -181,6 +182,7 @@ impl AttestedCertificateResolver { subject_alt_names: &[String], attestation_generator: &AttestationGenerator, ) -> Result>, AttestedTlsError> { + tracing::debug!("Generating new remote-attested ceritifcate for {primary_name}"); let pubkey = key.public_key_der(); let now = SystemTime::now(); let not_after = now + CERTIFICATE_VALIDITY; @@ -219,7 +221,7 @@ impl AttestedCertificateResolver { ) -> Result, AttestedTlsError> { let private_key = PrivateKeyDer::Pkcs8(PrivatePkcs8KeyDer::from(key_pair.serialize_der())); - provider.key_provider.load_private_key(private_key).map_err(AttestedTlsError::SigningKey) + Ok(provider.key_provider.load_private_key(private_key)?) } /// Create an attestation, and format it to be used in certificate @@ -364,13 +366,21 @@ fn create_report_data( /// Verifies attested TLS server or client certificates during TLS handshake #[derive(Debug)] pub struct AttestedCertificateVerifier { + /// Underlying verifier when used with a private CA rather than + /// self-signed server_inner: Option>, + /// Underlying client verifier when used with a private CA rather than + /// self-signed client_inner: Option>, + /// Underlying cryptography provider provider: Arc, + /// Configured for verifying attestations attestation_verifier: AttestationVerifier, } impl AttestedCertificateVerifier { + /// Create a certificate verifier with given attestation verification + /// and optionally a private CA root of trust pub fn new( root_store: Option, attestation_verifier: AttestationVerifier, @@ -378,6 +388,7 @@ impl AttestedCertificateVerifier { Self::new_with_provider(root_store, attestation_verifier, default_crypto_provider()?) } + /// Also provide a crypto provider pub fn new_with_provider( root_store: Option, attestation_verifier: AttestationVerifier, @@ -405,6 +416,7 @@ impl AttestedCertificateVerifier { Ok(Self { server_inner, client_inner, provider, attestation_verifier }) } + /// Given a TLS certificate, return the embedded attestation fn extract_custom_attestation_from_cert( cert: &CertificateDer<'_>, ) -> Result { @@ -695,8 +707,6 @@ pub enum AttestedTlsError { CertificateParams(#[source] rcgen::Error), #[error("Failed to self-sign certificate: {0}")] CertificateSigning(#[source] rcgen::Error), - #[error("Failed to load signing key into rustls: {0}")] - SigningKey(#[source] rustls::Error), #[error("Failed to build certificate verifier: {0}")] VerifierBuilder(#[source] VerifierBuilderError), #[error("Cetificate generation: {0}")] From d6d45a10084ef57550d13628da61c812168cd8ac Mon Sep 17 00:00:00 2001 From: peg Date: Mon, 16 Mar 2026 08:47:14 +0100 Subject: [PATCH 17/27] Cache trusted cerificate input data --- crates/attestation/src/dcap.rs | 1 + crates/attested-tls/src/lib.rs | 91 ++++++++++++++++++++++++++++++++-- 2 files changed, 89 insertions(+), 3 deletions(-) diff --git a/crates/attestation/src/dcap.rs b/crates/attestation/src/dcap.rs index bf29dcd..5122c9a 100644 --- a/crates/attestation/src/dcap.rs +++ b/crates/attestation/src/dcap.rs @@ -111,6 +111,7 @@ pub async fn verify_dcap_attestation_with_given_timestamp( } #[cfg(any(test, feature = "mock"))] +#[allow(clippy::unused_async)] pub async fn verify_dcap_attestation( input: Vec, expected_input_data: [u8; 64], diff --git a/crates/attested-tls/src/lib.rs b/crates/attested-tls/src/lib.rs index 73099e3..5f19fbb 100644 --- a/crates/attested-tls/src/lib.rs +++ b/crates/attested-tls/src/lib.rs @@ -1,5 +1,6 @@ //! An attested TLS certificate resolver and verifier use std::{ + collections::HashSet, fmt, sync::{Arc, RwLock}, time::{Duration, SystemTime}, @@ -376,6 +377,8 @@ pub struct AttestedCertificateVerifier { provider: Arc, /// Configured for verifying attestations attestation_verifier: AttestationVerifier, + /// Report data of pre-trusted certificates + trusted_certificates: Arc>>, } impl AttestedCertificateVerifier { @@ -413,7 +416,13 @@ impl AttestedCertificateVerifier { None => (None, None), }; - Ok(Self { server_inner, client_inner, provider, attestation_verifier }) + Ok(Self { + server_inner, + client_inner, + provider, + attestation_verifier, + trusted_certificates: Default::default(), + }) } /// Given a TLS certificate, return the embedded attestation @@ -515,6 +524,19 @@ impl AttestedCertificateVerifier { end_entity: &CertificateDer<'_>, ) -> Result<(), rustls::Error> { let expected_input_data = Self::expected_input_data_from_cert(end_entity)?; + + // First check if we have already successfully verified the attestation + // associated with this certificate + { + let trusted_certificates = self.trusted_certificates.read().map_err(|_| { + rustls::Error::General("Trusted certificate cache lock poisoned".into()) + })?; + if trusted_certificates.contains(&expected_input_data) { + tracing::debug!("Skipping attestation verification for trusted certificate"); + return Ok(()); + } + } + let attestation = Self::extract_custom_attestation_from_cert(end_entity)?; tokio::task::block_in_place(|| { @@ -522,7 +544,6 @@ impl AttestedCertificateVerifier { self.attestation_verifier .verify_attestation(attestation, expected_input_data) .await - .map(|_| ()) .map_err(|err| { tracing::warn!( "Rejecting certificate after attestation verification failure: {err}" @@ -532,7 +553,15 @@ impl AttestedCertificateVerifier { ) }) }) - }) + })?; + + // Write trusted certificate details to cache + self.trusted_certificates + .write() + .map_err(|_| rustls::Error::General("Trusted certificate cache lock poisoned".into()))? + .insert(expected_input_data); + + Ok(()) } /// Helper for creating encoding related verification errors @@ -1113,6 +1142,62 @@ mod tests { ); } + #[tokio::test(flavor = "multi_thread")] + async fn verifier_reuses_trusted_certificate_cache() { + let provider: Arc = aws_lc_rs::default_provider().into(); + let resolver = AttestedCertificateResolver::new_with_provider( + AttestationGenerator::new(AttestationType::DcapTdx, None) + .expect("mock generator construction should succeed"), + None, + "foo".to_string(), + vec![], + provider.clone(), + ) + .await + .expect("resolver construction should succeed"); + let mut verifier = AttestedCertificateVerifier::new_with_provider( + None, + AttestationVerifier::mock(), + provider, + ) + .expect("verifier construction should succeed"); + let cert = resolver + .state + .certificate + .read() + .expect("certificate lock poisoned") + .first() + .expect("resolver should hold a certificate") + .clone(); + let expected_input_data = AttestedCertificateVerifier::expected_input_data_from_cert(&cert) + .expect("certificate report data should be computed"); + + verifier + .verify_server_cert_direct( + &cert, + &ServerName::try_from("foo").expect("server name should be valid"), + UnixTime::now(), + ) + .expect("initial verification should succeed"); + assert!( + verifier + .trusted_certificates + .read() + .expect("trusted certificate lock poisoned") + .contains(&expected_input_data) + ); + + verifier.attestation_verifier = AttestationVerifier::expect_none(); + + verifier + .verify_server_cert_direct( + &cert, + &ServerName::try_from("foo").expect("server name should be valid"), + UnixTime::now(), + ) + .expect("cached verification should skip attestation verification"); + } + fn test_ca() -> CaCert { let key = KeyPair::generate_for(&PKCS_ECDSA_P256_SHA256) .expect("test CA key generation should succeed"); From bc51460c78af4394bec037e8071279e42c5aa6fd Mon Sep 17 00:00:00 2001 From: peg Date: Mon, 16 Mar 2026 09:02:35 +0100 Subject: [PATCH 18/27] Comments, tidy tests --- crates/attested-tls/src/lib.rs | 68 ++++++++++++++----------- crates/attested-tls/tests/nested_tls.rs | 55 ++++++++++---------- 2 files changed, 67 insertions(+), 56 deletions(-) diff --git a/crates/attested-tls/src/lib.rs b/crates/attested-tls/src/lib.rs index 5f19fbb..ddee369 100644 --- a/crates/attested-tls/src/lib.rs +++ b/crates/attested-tls/src/lib.rs @@ -519,6 +519,8 @@ impl AttestedCertificateVerifier { Ok(()) } + /// Given a certificate with embedded attestation, verify the + /// attestation if it has not already been verified fn verify_attestation_binding( &self, end_entity: &CertificateDer<'_>, @@ -581,18 +583,6 @@ impl AttestedCertificateVerifier { } } -impl AttestedCertificateVerifier { - #[cfg(test)] - fn verify_server_cert_direct( - &self, - end_entity: &CertificateDer<'_>, - server_name: &ServerName<'_>, - now: UnixTime, - ) -> Result { - self.verify_server_cert(end_entity, &[], server_name, &[], now) - } -} - impl ServerCertVerifier for AttestedCertificateVerifier { fn verify_server_cert( &self, @@ -770,6 +760,23 @@ mod tests { use super::*; + /// Test helper to verify a certificate + fn verify_server_cert_direct( + verifier: &AttestedCertificateVerifier, + end_entity: &CertificateDer<'_>, + server_name: &ServerName<'_>, + now: UnixTime, + ) -> Result { + rustls::client::danger::ServerCertVerifier::verify_server_cert( + verifier, + end_entity, + &[], + server_name, + &[], + now, + ) + } + #[tokio::test(flavor = "multi_thread")] async fn certificate_resolver_creates_initial_certificate() { let provider: Arc = aws_lc_rs::default_provider().into(); @@ -1071,7 +1078,8 @@ mod tests { .expect("verifier construction should succeed"); let cert = CertificateDer::from(vec![1_u8, 2, 3, 4]); - let result = verifier.verify_server_cert_direct( + let result = verify_server_cert_direct( + &verifier, &cert, &ServerName::try_from("foo").expect("server name should be valid"), UnixTime::now(), @@ -1093,7 +1101,8 @@ mod tests { ) .expect("verifier construction should succeed"); - let result = verifier.verify_server_cert_direct( + let result = verify_server_cert_direct( + &verifier, &cert, &ServerName::try_from("foo").expect("server name should be valid"), UnixTime::now(), @@ -1130,7 +1139,8 @@ mod tests { .expect("resolver should hold a certificate") .clone(); - let result = verifier.verify_server_cert_direct( + let result = verify_server_cert_direct( + &verifier, &cert, &ServerName::try_from("foo").expect("server name should be valid"), UnixTime::now(), @@ -1172,13 +1182,13 @@ mod tests { let expected_input_data = AttestedCertificateVerifier::expected_input_data_from_cert(&cert) .expect("certificate report data should be computed"); - verifier - .verify_server_cert_direct( - &cert, - &ServerName::try_from("foo").expect("server name should be valid"), - UnixTime::now(), - ) - .expect("initial verification should succeed"); + verify_server_cert_direct( + &verifier, + &cert, + &ServerName::try_from("foo").expect("server name should be valid"), + UnixTime::now(), + ) + .expect("initial verification should succeed"); assert!( verifier .trusted_certificates @@ -1189,13 +1199,13 @@ mod tests { verifier.attestation_verifier = AttestationVerifier::expect_none(); - verifier - .verify_server_cert_direct( - &cert, - &ServerName::try_from("foo").expect("server name should be valid"), - UnixTime::now(), - ) - .expect("cached verification should skip attestation verification"); + verify_server_cert_direct( + &verifier, + &cert, + &ServerName::try_from("foo").expect("server name should be valid"), + UnixTime::now(), + ) + .expect("cached verification should skip attestation verification"); } fn test_ca() -> CaCert { diff --git a/crates/attested-tls/tests/nested_tls.rs b/crates/attested-tls/tests/nested_tls.rs index 030c259..6bc818a 100644 --- a/crates/attested-tls/tests/nested_tls.rs +++ b/crates/attested-tls/tests/nested_tls.rs @@ -1,3 +1,4 @@ +//! Provides a test demonstrating using nested-tls and attested-tls together use std::sync::Arc; use attestation::{AttestationGenerator, AttestationType, AttestationVerifier}; @@ -26,92 +27,92 @@ async fn nested_tls_uses_attested_tls_for_inner_session() { let (client_io, server_io) = duplex(16 * 1024); let server = tokio::spawn(async move { - let mut stream = acceptor.accept(server_io).await.expect("nested accept should succeed"); + let mut stream = acceptor.accept(server_io).await.unwrap(); let mut req = [0_u8; 5]; - stream.read_exact(&mut req).await.expect("server read should succeed"); + stream.read_exact(&mut req).await.unwrap(); assert_eq!(&req, b"hello"); - stream.write_all(b"world").await.expect("server write should succeed"); - stream.flush().await.expect("server flush should succeed"); + stream.write_all(b"world").await.unwrap(); + stream.flush().await.unwrap(); }); - let domain = ServerName::try_from("localhost").expect("domain should be valid"); - let mut stream = - connector.connect(domain, client_io).await.expect("nested connect should succeed"); + let domain = ServerName::try_from("localhost").unwrap(); + let mut stream = connector.connect(domain, client_io).await.unwrap(); - stream.write_all(b"hello").await.expect("client write should succeed"); - stream.flush().await.expect("client flush should succeed"); + stream.write_all(b"hello").await.unwrap(); + stream.flush().await.unwrap(); let mut resp = [0_u8; 5]; - stream.read_exact(&mut resp).await.expect("client read should succeed"); + stream.read_exact(&mut resp).await.unwrap(); assert_eq!(&resp, b"world"); - server.await.expect("server task should complete"); + server.await.unwrap(); } +/// Create vanilla TLS server and client config for outer session fn plain_tls_config_pair(provider: Arc) -> (ServerConfig, ClientConfig) { let subject_name = "localhost"; - let key = - KeyPair::generate_for(&PKCS_ECDSA_P256_SHA256).expect("test key generation should succeed"); - let mut params = ra_tls::rcgen::CertificateParams::new(vec![subject_name.to_string()]) - .expect("test certificate params should be created"); + let key = KeyPair::generate_for(&PKCS_ECDSA_P256_SHA256).unwrap(); + let mut params = ra_tls::rcgen::CertificateParams::new(vec![subject_name.to_string()]).unwrap(); params .subject_alt_names - .push(ra_tls::rcgen::SanType::DnsName(subject_name.try_into().expect("valid dns name"))); - let cert = params.self_signed(&key).expect("test certificate should be self-signed"); + .push(ra_tls::rcgen::SanType::DnsName(subject_name.try_into().unwrap())); + let cert = params.self_signed(&key).unwrap(); let cert_der: CertificateDer<'static> = cert.der().clone(); let key_der = PrivateKeyDer::Pkcs8(PrivatePkcs8KeyDer::from(key.serialize_der())); let server = ServerConfig::builder_with_provider(provider.clone()) .with_safe_default_protocol_versions() - .expect("server config should support default protocol versions") + .unwrap() .with_no_client_auth() .with_single_cert(vec![cert_der.clone()], key_der) - .expect("server config should be created"); + .unwrap(); let mut roots = RootCertStore::empty(); - roots.add(cert_der).expect("client roots should trust server certificate"); + roots.add(cert_der).unwrap(); let client = ClientConfig::builder_with_provider(provider) .with_safe_default_protocol_versions() - .expect("client config should support default protocol versions") + .unwrap() .with_root_certificates(roots) .with_no_client_auth(); (server, client) } +/// Create attested server TLS config with mock DCAP attestation and +/// self-signed certs async fn attested_server_config(server_name: &str, provider: Arc) -> ServerConfig { let resolver = AttestedCertificateResolver::new_with_provider( - AttestationGenerator::new(AttestationType::DcapTdx, None) - .expect("mock generator construction should succeed"), + AttestationGenerator::new(AttestationType::DcapTdx, None).unwrap(), None, server_name.to_string(), vec![], provider.clone(), ) .await - .expect("resolver construction should succeed"); + .unwrap(); ServerConfig::builder_with_provider(provider) .with_safe_default_protocol_versions() - .expect("server config should support default protocol versions") + .unwrap() .with_no_client_auth() .with_cert_resolver(Arc::new(resolver)) } +/// Create client TLS config with attestation verification fn attested_client_config(provider: Arc) -> ClientConfig { let verifier = AttestedCertificateVerifier::new_with_provider( None, AttestationVerifier::mock(), provider.clone(), ) - .expect("verifier construction should succeed"); + .unwrap(); ClientConfig::builder_with_provider(provider) .with_safe_default_protocol_versions() - .expect("client config should support default protocol versions") + .unwrap() .dangerous() .with_custom_certificate_verifier(Arc::new(verifier)) .with_no_client_auth() From 97bd108039f8c1eacea13ff75de5f21471ef8f0c Mon Sep 17 00:00:00 2001 From: peg Date: Mon, 16 Mar 2026 09:06:46 +0100 Subject: [PATCH 19/27] Tidy tests --- crates/attested-tls/src/lib.rs | 135 ++++++++++++++------------------- 1 file changed, 59 insertions(+), 76 deletions(-) diff --git a/crates/attested-tls/src/lib.rs b/crates/attested-tls/src/lib.rs index ddee369..0c454dd 100644 --- a/crates/attested-tls/src/lib.rs +++ b/crates/attested-tls/src/lib.rs @@ -910,22 +910,21 @@ mod tests { async fn certificate_is_renewed_before_expiry() { let provider: Arc = aws_lc_rs::default_provider().into(); let resolver = AttestedCertificateResolver::new_with_provider( - AttestationGenerator::new(AttestationType::DcapTdx, None) - .expect("mock generator construction should succeed"), + AttestationGenerator::new(AttestationType::DcapTdx, None).unwrap(), None, "foo".to_string(), vec![], provider, ) .await - .expect("resolver construction should succeed"); + .unwrap(); let initial_certificate = resolver .state .certificate .read() - .expect("certificate lock poisoned") + .unwrap() .first() - .expect("resolver should hold a certificate") + .unwrap() .clone(); tokio::time::sleep( @@ -937,9 +936,9 @@ mod tests { .state .certificate .read() - .expect("certificate lock poisoned") + .unwrap() .first() - .expect("resolver should hold a renewed certificate") + .unwrap() .clone(); assert_ne!(initial_certificate.as_ref(), renewed_certificate.as_ref()); @@ -951,59 +950,56 @@ mod tests { let server_name = "foo"; let server_resolver = AttestedCertificateResolver::new_with_provider( - AttestationGenerator::new(AttestationType::DcapTdx, None) - .expect("mock generator construction should succeed"), + AttestationGenerator::new(AttestationType::DcapTdx, None).unwrap(), None, server_name.to_string(), vec![], provider.clone(), ) .await - .expect("server resolver construction should succeed"); + .unwrap(); let client_resolver = AttestedCertificateResolver::new_with_provider( - AttestationGenerator::new(AttestationType::DcapTdx, None) - .expect("mock generator construction should succeed"), + AttestationGenerator::new(AttestationType::DcapTdx, None).unwrap(), None, "client".to_string(), vec![], provider.clone(), ) .await - .expect("client resolver construction should succeed"); + .unwrap(); let server_verifier = AttestedCertificateVerifier::new_with_provider( None, AttestationVerifier::mock(), provider.clone(), ) - .expect("server verifier construction should succeed"); + .unwrap(); let client_verifier = AttestedCertificateVerifier::new_with_provider( None, AttestationVerifier::mock(), provider.clone(), ) - .expect("client verifier construction should succeed"); + .unwrap(); let server_config = ServerConfig::builder_with_provider(provider.clone()) .with_safe_default_protocol_versions() - .expect("server config should support default protocol versions") + .unwrap() .with_client_cert_verifier(Arc::new(server_verifier)) .with_cert_resolver(Arc::new(server_resolver)); let client_config = ClientConfig::builder_with_provider(provider) .with_safe_default_protocol_versions() - .expect("client config should support default protocol versions") + .unwrap() .dangerous() .with_custom_certificate_verifier(Arc::new(client_verifier)) .with_client_cert_resolver(Arc::new(client_resolver)); let mut client = ClientConnection::new( Arc::new(client_config), - ServerName::try_from(server_name).expect("server name should be valid"), + ServerName::try_from(server_name).unwrap(), ) - .expect("client connection should be created"); - let mut server = - ServerConnection::new(Arc::new(server_config)).expect("server connection should exist"); + .unwrap(); + let mut server = ServerConnection::new(Arc::new(server_config)).unwrap(); while client.is_handshaking() || server.is_handshaking() { transfer_tls_client_to_server(&mut client, &mut server); @@ -1022,41 +1018,39 @@ mod tests { let primary_name = "foo"; let alternate_name = "bar"; let resolver = AttestedCertificateResolver::new_with_provider( - AttestationGenerator::new(AttestationType::DcapTdx, None) - .expect("mock generator construction should succeed"), + AttestationGenerator::new(AttestationType::DcapTdx, None).unwrap(), None, primary_name.to_string(), vec![alternate_name.to_string(), primary_name.to_string()], provider.clone(), ) .await - .expect("resolver construction should succeed"); + .unwrap(); let verifier = AttestedCertificateVerifier::new_with_provider( None, AttestationVerifier::mock(), provider.clone(), ) - .expect("verifier construction should succeed"); + .unwrap(); let server_config = ServerConfig::builder_with_provider(provider.clone()) .with_safe_default_protocol_versions() - .expect("server config should support default protocol versions") + .unwrap() .with_no_client_auth() .with_cert_resolver(Arc::new(resolver)); let client_config = ClientConfig::builder_with_provider(provider) .with_safe_default_protocol_versions() - .expect("client config should support default protocol versions") + .unwrap() .dangerous() .with_custom_certificate_verifier(Arc::new(verifier)) .with_no_client_auth(); let mut client = ClientConnection::new( Arc::new(client_config), - ServerName::try_from(alternate_name).expect("alternate server name should be valid"), + ServerName::try_from(alternate_name).unwrap(), ) - .expect("client connection should be created"); - let mut server = - ServerConnection::new(Arc::new(server_config)).expect("server connection should exist"); + .unwrap(); + let mut server = ServerConnection::new(Arc::new(server_config)).unwrap(); while client.is_handshaking() || server.is_handshaking() { transfer_tls_client_to_server(&mut client, &mut server); @@ -1075,13 +1069,13 @@ mod tests { AttestationVerifier::mock(), provider, ) - .expect("verifier construction should succeed"); + .unwrap(); let cert = CertificateDer::from(vec![1_u8, 2, 3, 4]); let result = verify_server_cert_direct( &verifier, &cert, - &ServerName::try_from("foo").expect("server name should be valid"), + &ServerName::try_from("foo").unwrap(), UnixTime::now(), ); @@ -1093,18 +1087,18 @@ mod tests { let provider: Arc = aws_lc_rs::default_provider().into(); let cert = plain_self_signed_certificate("foo"); let mut roots = RootCertStore::empty(); - roots.add(cert.clone()).expect("plain certificate should be trusted"); + roots.add(cert.clone()).unwrap(); let verifier = AttestedCertificateVerifier::new_with_provider( Some(roots), AttestationVerifier::mock(), provider, ) - .expect("verifier construction should succeed"); + .unwrap(); let result = verify_server_cert_direct( &verifier, &cert, - &ServerName::try_from("foo").expect("server name should be valid"), + &ServerName::try_from("foo").unwrap(), UnixTime::now(), ); @@ -1115,34 +1109,33 @@ mod tests { async fn attestation_rejection_returns_application_verification_failure() { let provider: Arc = aws_lc_rs::default_provider().into(); let resolver = AttestedCertificateResolver::new_with_provider( - AttestationGenerator::new(AttestationType::DcapTdx, None) - .expect("mock generator construction should succeed"), + AttestationGenerator::new(AttestationType::DcapTdx, None).unwrap(), None, "foo".to_string(), vec![], provider.clone(), ) .await - .expect("resolver construction should succeed"); + .unwrap(); let verifier = AttestedCertificateVerifier::new_with_provider( None, AttestationVerifier::expect_none(), provider, ) - .expect("verifier construction should succeed"); + .unwrap(); let cert = resolver .state .certificate .read() - .expect("certificate lock poisoned") + .unwrap() .first() - .expect("resolver should hold a certificate") + .unwrap() .clone(); let result = verify_server_cert_direct( &verifier, &cert, - &ServerName::try_from("foo").expect("server name should be valid"), + &ServerName::try_from("foo").unwrap(), UnixTime::now(), ); @@ -1156,44 +1149,43 @@ mod tests { async fn verifier_reuses_trusted_certificate_cache() { let provider: Arc = aws_lc_rs::default_provider().into(); let resolver = AttestedCertificateResolver::new_with_provider( - AttestationGenerator::new(AttestationType::DcapTdx, None) - .expect("mock generator construction should succeed"), + AttestationGenerator::new(AttestationType::DcapTdx, None).unwrap(), None, "foo".to_string(), vec![], provider.clone(), ) .await - .expect("resolver construction should succeed"); + .unwrap(); let mut verifier = AttestedCertificateVerifier::new_with_provider( None, AttestationVerifier::mock(), provider, ) - .expect("verifier construction should succeed"); + .unwrap(); let cert = resolver .state .certificate .read() - .expect("certificate lock poisoned") + .unwrap() .first() - .expect("resolver should hold a certificate") + .unwrap() .clone(); let expected_input_data = AttestedCertificateVerifier::expected_input_data_from_cert(&cert) - .expect("certificate report data should be computed"); + .unwrap(); verify_server_cert_direct( &verifier, &cert, - &ServerName::try_from("foo").expect("server name should be valid"), + &ServerName::try_from("foo").unwrap(), UnixTime::now(), ) - .expect("initial verification should succeed"); + .unwrap(); assert!( verifier .trusted_certificates .read() - .expect("trusted certificate lock poisoned") + .unwrap() .contains(&expected_input_data) ); @@ -1202,63 +1194,54 @@ mod tests { verify_server_cert_direct( &verifier, &cert, - &ServerName::try_from("foo").expect("server name should be valid"), + &ServerName::try_from("foo").unwrap(), UnixTime::now(), ) - .expect("cached verification should skip attestation verification"); + .unwrap(); } fn test_ca() -> CaCert { - let key = KeyPair::generate_for(&PKCS_ECDSA_P256_SHA256) - .expect("test CA key generation should succeed"); - let mut params = CertificateParams::new(vec!["test-ca".to_string()]) - .expect("test CA params should be created"); + let key = KeyPair::generate_for(&PKCS_ECDSA_P256_SHA256).unwrap(); + let mut params = CertificateParams::new(vec!["test-ca".to_string()]).unwrap(); params.is_ca = IsCa::Ca(BasicConstraints::Unconstrained); - let cert = params.self_signed(&key).expect("test CA certificate should be self-signed"); + let cert = params.self_signed(&key).unwrap(); CaCert::from_parts(key, cert) } fn plain_self_signed_certificate(subject_name: &str) -> CertificateDer<'static> { - let key = KeyPair::generate_for(&PKCS_ECDSA_P256_SHA256) - .expect("test key generation should succeed"); - let params = CertificateParams::new(vec![subject_name.to_string()]) - .expect("test certificate params should be created"); - params - .self_signed(&key) - .expect("test certificate should be self-signed") - .der() - .to_vec() - .into() + let key = KeyPair::generate_for(&PKCS_ECDSA_P256_SHA256).unwrap(); + let params = CertificateParams::new(vec![subject_name.to_string()]).unwrap(); + params.self_signed(&key).unwrap().der().to_vec().into() } fn transfer_tls_client_to_server(client: &mut ClientConnection, server: &mut ServerConnection) { let mut tls = Vec::new(); while client.wants_write() { - client.write_tls(&mut tls).expect("writing tls should succeed"); + client.write_tls(&mut tls).unwrap(); } if tls.is_empty() { return; } - server.read_tls(&mut Cursor::new(tls)).expect("reading tls should succeed"); - server.process_new_packets().expect("processing tls packets should succeed"); + server.read_tls(&mut Cursor::new(tls)).unwrap(); + server.process_new_packets().unwrap(); } fn transfer_tls_server_to_client(server: &mut ServerConnection, client: &mut ClientConnection) { let mut tls = Vec::new(); while server.wants_write() { - server.write_tls(&mut tls).expect("writing tls should succeed"); + server.write_tls(&mut tls).unwrap(); } if tls.is_empty() { return; } - client.read_tls(&mut Cursor::new(tls)).expect("reading tls should succeed"); - client.process_new_packets().expect("processing tls packets should succeed"); + client.read_tls(&mut Cursor::new(tls)).unwrap(); + client.process_new_packets().unwrap(); } } From 20c8fc694fb74dadb0f9dd33de14c2d164438345 Mon Sep 17 00:00:00 2001 From: peg Date: Mon, 16 Mar 2026 09:10:53 +0100 Subject: [PATCH 20/27] Move aws provider to only be enabled in tests --- crates/attested-tls/Cargo.toml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/crates/attested-tls/Cargo.toml b/crates/attested-tls/Cargo.toml index 4359038..baf403c 100644 --- a/crates/attested-tls/Cargo.toml +++ b/crates/attested-tls/Cargo.toml @@ -5,7 +5,7 @@ edition = "2024" [dependencies] tokio = { workspace = true } -rustls = { workspace = true, default-features = false, features = ["aws_lc_rs"] } +rustls = { workspace = true, default-features = false } attestation = { workspace = true } rcgen = "0.14.7" thiserror = "2.0.17" @@ -20,6 +20,7 @@ tracing = "0.1.41" [dev-dependencies] attestation = { workspace = true, features = ["mock"] } nested-tls = { path = "../nested-tls" } +rustls = { workspace = true, default-features = false, features = ["aws_lc_rs"] } [lints] workspace = true From bec6b88c4b746d00a55a07a318875542ed10f2c2 Mon Sep 17 00:00:00 2001 From: peg Date: Mon, 16 Mar 2026 09:13:26 +0100 Subject: [PATCH 21/27] Fmt --- crates/attested-tls/src/lib.rs | 52 +++++++--------------------------- 1 file changed, 11 insertions(+), 41 deletions(-) diff --git a/crates/attested-tls/src/lib.rs b/crates/attested-tls/src/lib.rs index 0c454dd..2b55420 100644 --- a/crates/attested-tls/src/lib.rs +++ b/crates/attested-tls/src/lib.rs @@ -918,28 +918,16 @@ mod tests { ) .await .unwrap(); - let initial_certificate = resolver - .state - .certificate - .read() - .unwrap() - .first() - .unwrap() - .clone(); + let initial_certificate = + resolver.state.certificate.read().unwrap().first().unwrap().clone(); tokio::time::sleep( CERTIFICATE_VALIDITY - CERTIFICATE_RENEWAL_LEAD_TIME + Duration::from_secs(1), ) .await; - let renewed_certificate = resolver - .state - .certificate - .read() - .unwrap() - .first() - .unwrap() - .clone(); + let renewed_certificate = + resolver.state.certificate.read().unwrap().first().unwrap().clone(); assert_ne!(initial_certificate.as_ref(), renewed_certificate.as_ref()); } @@ -1123,14 +1111,7 @@ mod tests { provider, ) .unwrap(); - let cert = resolver - .state - .certificate - .read() - .unwrap() - .first() - .unwrap() - .clone(); + let cert = resolver.state.certificate.read().unwrap().first().unwrap().clone(); let result = verify_server_cert_direct( &verifier, @@ -1163,16 +1144,9 @@ mod tests { provider, ) .unwrap(); - let cert = resolver - .state - .certificate - .read() - .unwrap() - .first() - .unwrap() - .clone(); - let expected_input_data = AttestedCertificateVerifier::expected_input_data_from_cert(&cert) - .unwrap(); + let cert = resolver.state.certificate.read().unwrap().first().unwrap().clone(); + let expected_input_data = + AttestedCertificateVerifier::expected_input_data_from_cert(&cert).unwrap(); verify_server_cert_direct( &verifier, @@ -1181,13 +1155,7 @@ mod tests { UnixTime::now(), ) .unwrap(); - assert!( - verifier - .trusted_certificates - .read() - .unwrap() - .contains(&expected_input_data) - ); + assert!(verifier.trusted_certificates.read().unwrap().contains(&expected_input_data)); verifier.attestation_verifier = AttestationVerifier::expect_none(); @@ -1200,6 +1168,7 @@ mod tests { .unwrap(); } + /// Helper to create a private cerificate authority fn test_ca() -> CaCert { let key = KeyPair::generate_for(&PKCS_ECDSA_P256_SHA256).unwrap(); let mut params = CertificateParams::new(vec!["test-ca".to_string()]).unwrap(); @@ -1209,6 +1178,7 @@ mod tests { CaCert::from_parts(key, cert) } + /// Helper to create a self signed cert with no attestation fn plain_self_signed_certificate(subject_name: &str) -> CertificateDer<'static> { let key = KeyPair::generate_for(&PKCS_ECDSA_P256_SHA256).unwrap(); let params = CertificateParams::new(vec![subject_name.to_string()]).unwrap(); From 75cc172c5d6f2585033220bac8138a66cde73e74 Mon Sep 17 00:00:00 2001 From: peg Date: Mon, 16 Mar 2026 09:24:13 +0100 Subject: [PATCH 22/27] Error handling --- crates/attested-tls/src/lib.rs | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/crates/attested-tls/src/lib.rs b/crates/attested-tls/src/lib.rs index 2b55420..0c845ec 100644 --- a/crates/attested-tls/src/lib.rs +++ b/crates/attested-tls/src/lib.rs @@ -234,12 +234,12 @@ impl AttestedCertificateResolver { attestation_generator: &AttestationGenerator, ) -> Result { let report_data = create_report_data(pubkey, not_before, not_after)?; - let attestation = attestation_generator.generate_attestation(report_data).await.unwrap(); + let attestation = attestation_generator.generate_attestation(report_data).await?; Ok(VersionedAttestation::V0 { attestation: Attestation { quote: ra_tls::attestation::AttestationQuote::DstackTdx( ra_tls::attestation::TdxQuote { - quote: serde_json::to_vec(&attestation).unwrap(), + quote: serde_json::to_vec(&attestation)?, event_log: Vec::new(), }, ), @@ -740,6 +740,10 @@ pub enum AttestedTlsError { SystemTime(#[source] std::time::SystemTimeError), #[error("No rustls CryptoProvider is installed")] CryptoProviderUnavailable, + #[error("JSON: {0}")] + Json(#[from] serde_json::Error), + #[error("Attestation: {0}")] + Attestation(#[from] attestation::AttestationError), } #[cfg(test)] From 2a5bcbb00ad8925fd43ad38da4b0029f9449d9fc Mon Sep 17 00:00:00 2001 From: peg Date: Mon, 16 Mar 2026 09:35:49 +0100 Subject: [PATCH 23/27] Add check for server hostname --- crates/attested-tls/src/lib.rs | 60 ++++++++++++++++++++++++++++++++-- 1 file changed, 58 insertions(+), 2 deletions(-) diff --git a/crates/attested-tls/src/lib.rs b/crates/attested-tls/src/lib.rs index 0c845ec..81fc137 100644 --- a/crates/attested-tls/src/lib.rs +++ b/crates/attested-tls/src/lib.rs @@ -27,6 +27,7 @@ use rustls::{ ResolvesClientCert, VerifierBuilderError, WebPkiServerVerifier, + verify_server_name, danger::{HandshakeSignatureValid, ServerCertVerified, ServerCertVerifier}, }, crypto::CryptoProvider, @@ -39,6 +40,7 @@ use rustls::{ pem::PemObject, }, server::{ + ParsedCertificate, ResolvesServerCert, WebPkiClientVerifier, danger::{ClientCertVerified, ClientCertVerifier}, @@ -484,6 +486,15 @@ impl AttestedCertificateVerifier { now: UnixTime, ) -> Result<(), rustls::Error> { let cert = Self::parse_x509_certificate(cert)?; + Self::verify_cert_time_validity_parsed(&cert, now) + } + + /// Given a parsed cerificate and the current time, check if it is + /// currently valid + fn verify_cert_time_validity_parsed( + cert: &X509Certificate<'_>, + now: UnixTime, + ) -> Result<(), rustls::Error> { let now = now.as_secs(); let not_before: u64 = cert .validity() @@ -519,6 +530,18 @@ impl AttestedCertificateVerifier { Ok(()) } + /// Verify server name and time validity for self-signed certs + fn verify_server_cert_constraints( + cert: &CertificateDer<'_>, + server_name: &ServerName<'_>, + now: UnixTime, + ) -> Result<(), rustls::Error> { + let parsed = ParsedCertificate::try_from(cert)?; + let cert = Self::parse_x509_certificate(cert)?; + Self::verify_cert_time_validity_parsed(&cert, now)?; + verify_server_name(&parsed, server_name) + } + /// Given a certificate with embedded attestation, verify the /// attestation if it has not already been verified fn verify_attestation_binding( @@ -602,13 +625,13 @@ impl ServerCertVerifier for AttestedCertificateVerifier { ) { Err(rustls::Error::InvalidCertificate(rustls::CertificateError::UnknownIssuer)) => { // handle self-signed certs differently - Self::verify_cert_time_validity(end_entity, now)?; + Self::verify_server_cert_constraints(end_entity, server_name, now)?; } Err(err) => return Err(err), Ok(_) => {} } } else { - Self::verify_cert_time_validity(end_entity, now)?; + Self::verify_server_cert_constraints(end_entity, server_name, now)?; } self.verify_attestation_binding(end_entity)?; Ok(ServerCertVerified::assertion()) @@ -1097,6 +1120,39 @@ mod tests { assert_eq!(result.unwrap_err(), Error::InvalidCertificate(CertificateError::BadEncoding)); } + #[tokio::test(flavor = "multi_thread")] + async fn self_signed_attested_certificate_with_wrong_name_is_rejected() { + let provider: Arc = aws_lc_rs::default_provider().into(); + let resolver = AttestedCertificateResolver::new_with_provider( + AttestationGenerator::new(AttestationType::DcapTdx, None).unwrap(), + None, + "foo".to_string(), + vec![], + provider.clone(), + ) + .await + .unwrap(); + let verifier = AttestedCertificateVerifier::new_with_provider( + None, + AttestationVerifier::mock(), + provider, + ) + .unwrap(); + let cert = resolver.state.certificate.read().unwrap().first().unwrap().clone(); + + let result = verify_server_cert_direct( + &verifier, + &cert, + &ServerName::try_from("bar").unwrap(), + UnixTime::now(), + ); + + assert!(matches!( + result.unwrap_err(), + Error::InvalidCertificate(CertificateError::NotValidForNameContext { .. }) + )); + } + #[tokio::test(flavor = "multi_thread")] async fn attestation_rejection_returns_application_verification_failure() { let provider: Arc = aws_lc_rs::default_provider().into(); From f86dd9a54901178295931f10c584822eaf78c750 Mon Sep 17 00:00:00 2001 From: peg Date: Mon, 16 Mar 2026 10:00:33 +0100 Subject: [PATCH 24/27] Rm old entries from cache on writing a new one --- crates/attested-tls/src/lib.rs | 50 ++++++++++++++++++++-------------- 1 file changed, 30 insertions(+), 20 deletions(-) diff --git a/crates/attested-tls/src/lib.rs b/crates/attested-tls/src/lib.rs index 81fc137..163c71d 100644 --- a/crates/attested-tls/src/lib.rs +++ b/crates/attested-tls/src/lib.rs @@ -1,6 +1,6 @@ //! An attested TLS certificate resolver and verifier use std::{ - collections::HashSet, + collections::HashMap, fmt, sync::{Arc, RwLock}, time::{Duration, SystemTime}, @@ -27,8 +27,8 @@ use rustls::{ ResolvesClientCert, VerifierBuilderError, WebPkiServerVerifier, - verify_server_name, danger::{HandshakeSignatureValid, ServerCertVerified, ServerCertVerifier}, + verify_server_name, }, crypto::CryptoProvider, pki_types::{ @@ -379,8 +379,8 @@ pub struct AttestedCertificateVerifier { provider: Arc, /// Configured for verifying attestations attestation_verifier: AttestationVerifier, - /// Report data of pre-trusted certificates - trusted_certificates: Arc>>, + /// Report data of pre-trusted certificates with cache expiry time + trusted_certificates: Arc>>, } impl AttestedCertificateVerifier { @@ -455,9 +455,9 @@ impl AttestedCertificateVerifier { .map_err(|err| Self::bad_encoding(format!("invalid attestation JSON payload: {err}"))) } - /// Given a certifcate, get the public key and validity period to check - /// against attestation input - fn expected_input_data_from_cert(cert: &CertificateDer<'_>) -> Result<[u8; 64], rustls::Error> { + /// Given a certificate, return the attestation report input data based + /// on public key and expriy, as well as the expiry time + fn cert_binding_data(cert: &CertificateDer<'_>) -> Result<([u8; 64], UnixTime), rustls::Error> { let cert = Self::parse_x509_certificate(cert)?; let not_before: u64 = cert .validity() @@ -471,12 +471,15 @@ impl AttestedCertificateVerifier { .timestamp() .try_into() .map_err(|_| rustls::Error::General("invalid certificate not_after".into()))?; - create_report_data( + let expected_input_data = create_report_data( cert.public_key().raw.to_vec(), SystemTime::UNIX_EPOCH + Duration::from_secs(not_before), SystemTime::UNIX_EPOCH + Duration::from_secs(not_after), ) - .map_err(|err| rustls::Error::General(err.to_string())) + .map_err(|err| rustls::Error::General(err.to_string()))?; + let not_after = UnixTime::since_unix_epoch(Duration::from_secs(not_after)); + + Ok((expected_input_data, not_after)) } /// Given a cerificate and the current time, check if it is currently @@ -547,8 +550,9 @@ impl AttestedCertificateVerifier { fn verify_attestation_binding( &self, end_entity: &CertificateDer<'_>, + now: UnixTime, ) -> Result<(), rustls::Error> { - let expected_input_data = Self::expected_input_data_from_cert(end_entity)?; + let (expected_input_data, expiry) = Self::cert_binding_data(end_entity)?; // First check if we have already successfully verified the attestation // associated with this certificate @@ -556,7 +560,7 @@ impl AttestedCertificateVerifier { let trusted_certificates = self.trusted_certificates.read().map_err(|_| { rustls::Error::General("Trusted certificate cache lock poisoned".into()) })?; - if trusted_certificates.contains(&expected_input_data) { + if trusted_certificates.get(&expected_input_data).is_some_and(|expiry| *expiry >= now) { tracing::debug!("Skipping attestation verification for trusted certificate"); return Ok(()); } @@ -580,11 +584,14 @@ impl AttestedCertificateVerifier { }) })?; + let mut trusted_certificates = self.trusted_certificates.write().map_err(|_| { + rustls::Error::General("Trusted certificate cache lock poisoned".into()) + })?; + + // Remove any expired entries + trusted_certificates.retain(|_, cached_expiry| *cached_expiry >= now); // Write trusted certificate details to cache - self.trusted_certificates - .write() - .map_err(|_| rustls::Error::General("Trusted certificate cache lock poisoned".into()))? - .insert(expected_input_data); + trusted_certificates.insert(expected_input_data, expiry); Ok(()) } @@ -633,7 +640,7 @@ impl ServerCertVerifier for AttestedCertificateVerifier { } else { Self::verify_server_cert_constraints(end_entity, server_name, now)?; } - self.verify_attestation_binding(end_entity)?; + self.verify_attestation_binding(end_entity, now)?; Ok(ServerCertVerified::assertion()) } @@ -704,7 +711,7 @@ impl ClientCertVerifier for AttestedCertificateVerifier { } else { Self::verify_cert_time_validity(end_entity, now)?; } - self.verify_attestation_binding(end_entity)?; + self.verify_attestation_binding(end_entity, now)?; Ok(ClientCertVerified::assertion()) } @@ -1205,8 +1212,8 @@ mod tests { ) .unwrap(); let cert = resolver.state.certificate.read().unwrap().first().unwrap().clone(); - let expected_input_data = - AttestedCertificateVerifier::expected_input_data_from_cert(&cert).unwrap(); + let (expected_input_data, not_after) = + AttestedCertificateVerifier::cert_binding_data(&cert).unwrap(); verify_server_cert_direct( &verifier, @@ -1215,7 +1222,10 @@ mod tests { UnixTime::now(), ) .unwrap(); - assert!(verifier.trusted_certificates.read().unwrap().contains(&expected_input_data)); + assert_eq!( + verifier.trusted_certificates.read().unwrap().get(&expected_input_data), + Some(¬_after) + ); verifier.attestation_verifier = AttestationVerifier::expect_none(); From 1330820bcfce6922556e59617ab3343485197ef3 Mon Sep 17 00:00:00 2001 From: peg Date: Mon, 16 Mar 2026 10:28:50 +0100 Subject: [PATCH 25/27] Include hostname in report data --- crates/attested-tls/src/lib.rs | 73 +++++++++++++++++++++++++++++++--- 1 file changed, 68 insertions(+), 5 deletions(-) diff --git a/crates/attested-tls/src/lib.rs b/crates/attested-tls/src/lib.rs index 163c71d..a63e1e2 100644 --- a/crates/attested-tls/src/lib.rs +++ b/crates/attested-tls/src/lib.rs @@ -190,8 +190,14 @@ impl AttestedCertificateResolver { let now = SystemTime::now(); let not_after = now + CERTIFICATE_VALIDITY; - let attestation = - Self::create_attestation_payload(pubkey, now, not_after, attestation_generator).await?; + let attestation = Self::create_attestation_payload( + pubkey, + now, + not_after, + primary_name, + attestation_generator, + ) + .await?; let cert_request = CertRequest::builder() .key(key) @@ -233,9 +239,11 @@ impl AttestedCertificateResolver { pubkey: Vec, not_before: SystemTime, not_after: SystemTime, + primary_name: &str, attestation_generator: &AttestationGenerator, ) -> Result { - let report_data = create_report_data(pubkey, not_before, not_after)?; + let report_data = + create_report_data(pubkey, not_before, not_after, primary_name.as_bytes())?; let attestation = attestation_generator.generate_attestation(report_data).await?; Ok(VersionedAttestation::V0 { attestation: Attestation { @@ -340,12 +348,13 @@ fn normalized_subject_alt_names(primary_name: &str, subject_alt_names: Vec, not_before: SystemTime, not_after: SystemTime, + hostname: &[u8], ) -> Result<[u8; 64], AttestedTlsError> { let not_before = not_before .duration_since(SystemTime::UNIX_EPOCH) @@ -362,6 +371,7 @@ fn create_report_data( hasher.update(public_key); hasher.update(not_before); hasher.update(not_after); + hasher.update(hostname_binding_data); Ok(hasher.finalize().into()) } @@ -471,10 +481,12 @@ impl AttestedCertificateVerifier { .timestamp() .try_into() .map_err(|_| rustls::Error::General("invalid certificate not_after".into()))?; + let hostname = Self::hostname_from_cert(&cert)?; let expected_input_data = create_report_data( cert.public_key().raw.to_vec(), SystemTime::UNIX_EPOCH + Duration::from_secs(not_before), SystemTime::UNIX_EPOCH + Duration::from_secs(not_after), + &hostname, ) .map_err(|err| rustls::Error::General(err.to_string()))?; let not_after = UnixTime::since_unix_epoch(Duration::from_secs(not_after)); @@ -611,6 +623,17 @@ impl AttestedCertificateVerifier { .map(|(_, parsed)| parsed) .map_err(|err| Self::bad_encoding(format!("Invalid X.509 DER: {err}"))) } + + /// Given a certificate get the hostname for report input data + fn hostname_from_cert(cert: &X509Certificate<'_>) -> Result, rustls::Error> { + cert.subject() + .iter_common_name() + .next() + .ok_or_else(|| Self::bad_encoding("Missing common name"))? + .as_str() + .map(|hostname| hostname.as_bytes().to_vec()) + .map_err(|err| Self::bad_encoding(format!("Invalid common name: {err}"))) + } } impl ServerCertVerifier for AttestedCertificateVerifier { @@ -1160,6 +1183,46 @@ mod tests { )); } + #[tokio::test(flavor = "multi_thread")] + async fn certificate_binding_changes_when_identity_changes() { + let provider: Arc = aws_lc_rs::default_provider().into(); + let resolver = AttestedCertificateResolver::new_with_provider( + AttestationGenerator::new(AttestationType::DcapTdx, None).unwrap(), + None, + "foo".to_string(), + vec![], + provider.clone(), + ) + .await + .unwrap(); + let original_cert = resolver.state.certificate.read().unwrap().first().unwrap().clone(); + let (original_report_data, original_not_after) = + AttestedCertificateVerifier::cert_binding_data(&original_cert).unwrap(); + let parsed_cert = + AttestedCertificateVerifier::parse_x509_certificate(&original_cert).unwrap(); + let not_before = parsed_cert.validity().not_before.timestamp() as u64; + let not_after = parsed_cert.validity().not_after.timestamp() as u64; + let key_pair = KeyPair::try_from(resolver.state.key_pair_der.clone()).unwrap(); + let replay_name = "bar".to_string(); + let replay_alt_names = vec![replay_name.clone()]; + let replayed_cert_request = CertRequest::builder() + .key(&key_pair) + .subject(&replay_name) + .alt_names(&replay_alt_names) + .not_before(SystemTime::UNIX_EPOCH + Duration::from_secs(not_before)) + .not_after(SystemTime::UNIX_EPOCH + Duration::from_secs(not_after)) + .usage_server_auth(true) + .usage_client_auth(true) + .build(); + let replayed_cert: CertificateDer<'static> = + replayed_cert_request.self_signed().unwrap().der().to_vec().into(); + let (replayed_report_data, replayed_not_after) = + AttestedCertificateVerifier::cert_binding_data(&replayed_cert).unwrap(); + + assert_eq!(original_not_after, replayed_not_after); + assert_ne!(original_report_data, replayed_report_data); + } + #[tokio::test(flavor = "multi_thread")] async fn attestation_rejection_returns_application_verification_failure() { let provider: Arc = aws_lc_rs::default_provider().into(); From 0fdb708e0a5728872f0a4a2979f008da0c2a3c00 Mon Sep 17 00:00:00 2001 From: peg Date: Mon, 16 Mar 2026 10:49:30 +0100 Subject: [PATCH 26/27] Minor fix variable name --- crates/attested-tls/src/lib.rs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/crates/attested-tls/src/lib.rs b/crates/attested-tls/src/lib.rs index a63e1e2..67444d4 100644 --- a/crates/attested-tls/src/lib.rs +++ b/crates/attested-tls/src/lib.rs @@ -371,7 +371,7 @@ fn create_report_data( hasher.update(public_key); hasher.update(not_before); hasher.update(not_after); - hasher.update(hostname_binding_data); + hasher.update(hostname); Ok(hasher.finalize().into()) } @@ -1348,3 +1348,4 @@ mod tests { client.process_new_packets().unwrap(); } } + From 5c109dba74d4f9de58b4b846f480599752dfb1f9 Mon Sep 17 00:00:00 2001 From: peg Date: Mon, 16 Mar 2026 10:51:18 +0100 Subject: [PATCH 27/27] Fmt --- crates/attested-tls/src/lib.rs | 1 - 1 file changed, 1 deletion(-) diff --git a/crates/attested-tls/src/lib.rs b/crates/attested-tls/src/lib.rs index 67444d4..ff7a5a0 100644 --- a/crates/attested-tls/src/lib.rs +++ b/crates/attested-tls/src/lib.rs @@ -1348,4 +1348,3 @@ mod tests { client.process_new_packets().unwrap(); } } -