Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ nested-tls = { git = "https://github.com/flashbots/attested-tls", branch = "peg/
attestation = { git = "https://github.com/flashbots/attested-tls", branch = "peg/nitro" }
pccs = { git = "https://github.com/flashbots/attested-tls", branch = "peg/nitro" }
tokio = { version = "1.50.0", features = ["full"] }
tokio-vsock = "0.7.2"
tokio-rustls = { version = "0.26.4", default-features = false, features = ["aws_lc_rs"] }
x509-parser = { version = "0.18.0", features = ["verify"] }
x509-parser-016 = { package = "x509-parser", version = "0.16", features = ["verify"] }
Expand Down
57 changes: 54 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -78,14 +78,22 @@ Proxy-client to proxy-server connections use TLS 1.3. The server can expose two
- `--inner-listen-addr` exposes the inner attested TLS listener.
- `--outer-listen-addr` exposes an optional outer nested-TLS listener that wraps the inner session with a regular PKI TLS session.

At least one of these listeners must be configured. If TLS certificate and key files are provided, they apply only to the outer listener, and `--outer-listen-addr` is required.
The same listeners can be exposed over AWS Nitro VSOCK instead of TCP:

- `--inner-vsock-port` exposes the inner listener over VSOCK.
- `--outer-vsock-port` exposes the outer listener over VSOCK.
- `--inner-vsock-cid` and `--outer-vsock-cid` default to `VMADDR_CID_ANY`.

At least one of these listeners must be configured. If TLS certificate and key files are provided, they apply only to the outer listener, and an outer TCP or VSOCK listener is required.

When the server runs without an outer listener, the inner attested certificate still needs a DNS identity. In that case, use `--inner-certificate-name` to control the certificate name embedded into the inner attested certificate. If an outer certificate is present, the server derives that identity from the outer certificate instead.

On the client side:

- default mode connects to the server's outer listener and verifies the outer PKI certificate before entering the inner attested TLS session
- `--inner-session-only` connects directly to the inner attested TLS listener
- `--listen-transport vsock --listen-vsock-port <port>` makes the local client ingress listener use VSOCK instead of TCP
- `--target-transport vsock --target-vsock-cid <cid> --target-vsock-port <port>` makes the client connect to the proxy server over VSOCK; the positional target remains the TLS server name

In both modes, attestation is taken from the peer certificate on the inner TLS session, then enforced against the configured measurement policy.

Expand Down Expand Up @@ -187,15 +195,58 @@ cargo run -- client \
localhost:7001
```

In inner-only mode the client does not accept `--tls-ca-certificate`, `--tls-private-key-path`, or `--tls-certificate-path`.
In inner-only mode the client does not accept `--tls-ca-certificate`. `--tls-certificate-path` and `--tls-private-key-path` may be supplied when the server requires client authentication; the certificate identity is used for the generated inner attested client certificate.

### Nitro VSOCK Examples

Expose a server inner listener over VSOCK:

```bash
cargo run -- server \
--inner-vsock-port 7001 \
--inner-certificate-name localhost \
--server-attestation-type none \
--allowed-remote-attestation-type none \
127.0.0.1:8000
```

Connect a client to that VSOCK inner listener:

```bash
cargo run -- client \
--listen-addr 127.0.0.1:6000 \
--target-transport vsock \
--target-vsock-cid <server-cid> \
--target-vsock-port 7001 \
--inner-session-only \
--client-attestation-type none \
--allowed-remote-attestation-type none \
localhost
```

Run the proxy client itself with VSOCK ingress, useful when the client runs inside a Nitro enclave and receives requests from the parent instance:

```bash
cargo run -- client \
--listen-transport vsock \
--listen-vsock-port 6000 \
--target-transport vsock \
--target-vsock-cid <server-cid> \
--target-vsock-port 7001 \
--inner-session-only \
--tls-private-key-path client.key \
--tls-certificate-path client.crt \
--allowed-remote-attestation-type none \
localhost
```

## CLI Differences from `cvm-reverse-proxy`

This aims to have a similar command line interface to `cvm-reverse-proxy`, but there are some differences:

- The measurements file path is specified with `--measurements-file` rather than `--server-measurements` or `--client-measurements`.
- If no measurements file is specified, `--allowed-remote-attestation-type` must be given.
- The server splits listener configuration into `--inner-listen-addr` and optional `--outer-listen-addr`.
- The server splits listener configuration into inner and outer listeners, each using either TCP `--*-listen-addr` or VSOCK `--*-vsock-port`.
- `--log-dcap-quote` logs remote DCAP quotes into `quotes/`.

## Docker
Expand Down
188 changes: 165 additions & 23 deletions flake.nix
Original file line number Diff line number Diff line change
Expand Up @@ -8,51 +8,141 @@
system = "x86_64-linux";
pkgs = import nixpkgs { inherit system; };

# Both workspace members share a single Cargo.lock, so their dependency
# hashes are identical. Keeping this in one place means a lockfile bump
# only requires updating hashes here.
#
# Note: mock-tdx-0.0.1 appears twice in the lockfile (peg/nitro transitive
# dep and main-branch dev-dep). The peg/nitro rev is shared with
# attestation-0.0.1 (same SHA → same hash). The main-branch entry is
# stripped by cleanedLockFile below, so no hash entry is needed for it.
sharedOutputHashes = {
"attestation-0.0.1" = "sha256-4wa8gP9xQCZZL4JUnb1fNfpwxcahec5SgYZamdqX2h8=";
"attested-tls-0.0.1" = "sha256-4wa8gP9xQCZZL4JUnb1fNfpwxcahec5SgYZamdqX2h8=";
"cc-eventlog-0.5.11" = "sha256-q6Vrlx4N7Ce2EQTQH+0HCSEzFZmY8PzDHxrO8L3kMsQ=";
"cc-eventlog-0.5.8" = "sha256-KEauakj53LrhKTc0yYp5SM8ec0cFNm4YVuHCJYiPQjw=";
"dcap-qvl-0.3.12" = "sha256-rLTp5wIhXRAcBtJb7lfd1TAg7yPRnwa0cBa1YT4LwKU=";
"dstack-attest-0.5.11" = "sha256-q6Vrlx4N7Ce2EQTQH+0HCSEzFZmY8PzDHxrO8L3kMsQ=";
"dstack-types-0.5.11" = "sha256-q6Vrlx4N7Ce2EQTQH+0HCSEzFZmY8PzDHxrO8L3kMsQ=";
"nested-tls-0.0.1" = "sha256-4wa8gP9xQCZZL4JUnb1fNfpwxcahec5SgYZamdqX2h8=";
"pccs-0.0.1" = "sha256-4wa8gP9xQCZZL4JUnb1fNfpwxcahec5SgYZamdqX2h8=";
"ra-tls-0.5.11" = "sha256-q6Vrlx4N7Ce2EQTQH+0HCSEzFZmY8PzDHxrO8L3kMsQ=";
"size-parser-0.5.11" = "sha256-q6Vrlx4N7Ce2EQTQH+0HCSEzFZmY8PzDHxrO8L3kMsQ=";
"tdx-attest-0.5.11" = "sha256-q6Vrlx4N7Ce2EQTQH+0HCSEzFZmY8PzDHxrO8L3kMsQ=";
"tdx-attest-0.5.8" = "sha256-KEauakj53LrhKTc0yYp5SM8ec0cFNm4YVuHCJYiPQjw=";
};

# nixpkgs importCargoLock creates one symlink per package keyed by
# "<name>-<version>". When two git crates share the same name+version
# (here: mock-tdx-0.0.1 from peg/nitro and from main), the second ln
# follows the first symlink into a read-only store path and fails.
#
# The main-branch entry is a dev-dep of attested-tls-proxy only; since
# doCheck = false it is never compiled. Strip it from the lockfile at
# evaluation time so importCargoLock only ever sees the peg/nitro
# transitive dep (already covered by the attestation-0.0.1 hash above).
cleanedLockFile = builtins.toFile "Cargo.lock" (
builtins.replaceStrings
[
# [[package]] block for mock-tdx (main branch).
# The leading \n eats the blank separator line before the block;
# the trailing blank line before peg/nitro mock-tdx is preserved.
"\n[[package]]\nname = \"mock-tdx\"\nversion = \"0.0.1\"\nsource = \"git+https://github.com/flashbots/attested-tls?branch=main#eaa10f0528c8c561273717913596de65cff807b3\"\ndependencies = [\n \"axum\",\n \"dcap-qvl\",\n \"hex\",\n \"p256\",\n \"parity-scale-codec\",\n \"rcgen 0.14.7\",\n \"serde\",\n \"serde-saphyr\",\n \"serde_bytes\",\n \"serde_json\",\n \"sha2\",\n \"time\",\n \"tokio\",\n \"urlencoding\",\n \"x509-parser 0.18.1\",\n \"yasna 0.5.2\",\n]\n"
# Dep reference in the attested-tls-proxy package entry
# (Cargo.lock dep references omit the #rev suffix)
" \"mock-tdx 0.0.1 (git+https://github.com/flashbots/attested-tls?branch=main)\",\n"
]
[ "" "" ]
(builtins.readFile ./Cargo.lock)
);

# Vendor directory built from the cleaned lockfile (no mock-tdx main branch).
sharedCargoDeps = pkgs.rustPlatform.importCargoLock {
lockFile = cleanedLockFile;
outputHashes = sharedOutputHashes;
};

# Patch the unpacked source to match the vendor dir:
# cargo reads the source's Cargo.lock at build time and requires it to
# be consistent with what is vendored.
sharedPostUnpack = ''
cp ${cleanedLockFile} "$sourceRoot/Cargo.lock"
chmod u+w "$sourceRoot/Cargo.lock"
sed -i '/^mock-tdx/d' "$sourceRoot/Cargo.toml"
'';

sharedBuildInputs = [ pkgs.openssl pkgs.tpm2-tss ];
sharedNativeBuildInputs = [ pkgs.pkg-config ];

server = pkgs.rustPlatform.buildRustPackage {
pname = "attestation-provider-server";
version = "1.1.1";
src = ./.;

cargoLock = {
lockFile = ./Cargo.lock;
outputHashes = {
"attestation-0.0.1" = "sha256-1I9iQcFNt02fHs8Q18LK2+f8U0TzhfdFz7JvV0mKJUw=";
"attested-tls-0.0.1" = "sha256-1I9iQcFNt02fHs8Q18LK2+f8U0TzhfdFz7JvV0mKJUw=";
"cc-eventlog-0.5.11" = "sha256-q6Vrlx4N7Ce2EQTQH+0HCSEzFZmY8PzDHxrO8L3kMsQ=";
"cc-eventlog-0.5.8" = "sha256-KEauakj53LrhKTc0yYp5SM8ec0cFNm4YVuHCJYiPQjw=";
"dcap-qvl-0.3.12" = "sha256-rLTp5wIhXRAcBtJb7lfd1TAg7yPRnwa0cBa1YT4LwKU=";
"dstack-attest-0.5.11" = "sha256-q6Vrlx4N7Ce2EQTQH+0HCSEzFZmY8PzDHxrO8L3kMsQ=";
"dstack-types-0.5.11" = "sha256-q6Vrlx4N7Ce2EQTQH+0HCSEzFZmY8PzDHxrO8L3kMsQ=";
"nested-tls-0.0.1" = "sha256-1I9iQcFNt02fHs8Q18LK2+f8U0TzhfdFz7JvV0mKJUw=";
"pccs-0.0.1" = "sha256-1I9iQcFNt02fHs8Q18LK2+f8U0TzhfdFz7JvV0mKJUw=";
"ra-tls-0.5.11" = "sha256-q6Vrlx4N7Ce2EQTQH+0HCSEzFZmY8PzDHxrO8L3kMsQ=";
"size-parser-0.5.11" = "sha256-q6Vrlx4N7Ce2EQTQH+0HCSEzFZmY8PzDHxrO8L3kMsQ=";
"tdx-attest-0.5.11" = "sha256-q6Vrlx4N7Ce2EQTQH+0HCSEzFZmY8PzDHxrO8L3kMsQ=";
"tdx-attest-0.5.8" = "sha256-KEauakj53LrhKTc0yYp5SM8ec0cFNm4YVuHCJYiPQjw=";
};
};
cargoDeps = sharedCargoDeps;
postUnpack = sharedPostUnpack;
cargoBuildFlags = [ "-p" "attestation-provider-server" ];
cargoHash = "sha256-rLTp5wIhXRAcBtJb7lfd1TAg7yPRnwa0cBa1YT4LwKU=";

nativeBuildInputs = [ pkgs.pkg-config ];
buildInputs = [ pkgs.openssl pkgs.tpm2-tss ];
nativeBuildInputs = sharedNativeBuildInputs;
buildInputs = sharedBuildInputs;

doCheck = false;
};

proxy = pkgs.rustPlatform.buildRustPackage {
pname = "attested-tls-proxy";
version = "1.1.1";
src = ./.;

cargoDeps = sharedCargoDeps;
postUnpack = sharedPostUnpack;
cargoBuildFlags = [ "-p" "attested-tls-proxy" ];

nativeBuildInputs = sharedNativeBuildInputs;
buildInputs = sharedBuildInputs;

doCheck = false;
};

imageRoot = pkgs.buildEnv {
serverImageRoot = pkgs.buildEnv {
name = "attestation-provider-server-image-root";
paths = [ server pkgs.cacert ];
pathsToLink = [ "/bin" "/etc/ssl/certs" ];
};

proxyImageRoot = pkgs.buildEnv {
name = "attested-tls-proxy-image-root";
paths = [ proxy pkgs.cacert ];
pathsToLink = [ "/bin" "/etc/ssl/certs" ];
};

# A single text file at /srv/hello.txt for testing the file server image.
# writeTextDir "srv/hello.txt" produces $out/srv/hello.txt, which buildEnv
# links into /srv/hello.txt inside the image.
testContent = pkgs.writeTextDir "srv/hello.txt"
"Hello from attested-file-server!\n";

# Nitro enclaves don't bring up the loopback interface by default.
# The file server binds axum on 127.0.0.1 and the proxy connects back to
# it over loopback, so lo must be up before the binary starts.
fileServerEntrypoint = pkgs.writeShellScriptBin "attested-file-server-start" ''
${pkgs.iproute2}/bin/ip link set lo up
exec ${proxy}/bin/attested-tls-proxy "$@"
'';

fileServerImageRoot = pkgs.buildEnv {
name = "attested-file-server-image-root";
paths = [ fileServerEntrypoint pkgs.cacert testContent ];
pathsToLink = [ "/bin" "/etc/ssl/certs" "/srv" ];
};
in
{
packages.${system} = {
attestation-provider-server = server;
attestation-provider-server-image = pkgs.dockerTools.buildLayeredImage {
name = "attestation-provider-server";
tag = "latest";
contents = [ imageRoot ];
contents = [ serverImageRoot ];
config = {
Cmd = [
"/bin/attestation-provider-server"
Expand All @@ -66,6 +156,58 @@
];
};
};

attested-tls-proxy = proxy;
attested-tls-proxy-server-image = pkgs.dockerTools.buildLayeredImage {
name = "attested-tls-proxy-server";
tag = "latest";
contents = [ proxyImageRoot ];
config = {
# Global flags must precede the subcommand.
# --allowed-remote-attestation-type satisfies the mandatory CLI
# requirement; it only takes effect when --client-auth is passed.
# target_addr is a required positional arg supplied via Cmd so it
# can be overridden at runtime:
# docker run attested-tls-proxy-server 127.0.0.1:8080
Entrypoint = [
"/bin/attested-tls-proxy"
"--allowed-remote-attestation-type"
"aws-nitro"
"server"
"--server-attestation-type"
"aws-nitro"
"--inner-vsock-port"
"8001"
];
Cmd = [ "127.0.0.1:3000" ];
};
};

attested-file-server-image = pkgs.dockerTools.buildLayeredImage {
name = "attested-file-server";
tag = "latest";
contents = [ fileServerImageRoot ];
config = {
# attested-file-server starts an internal HTTP server on a random
# loopback port, then wraps it with an attested TLS listener.
# path_to_serve (/srv) is the positional arg after the subcommand.
# --inner-listen-addr is TCP-only (no vsock on this subcommand).
# Retrieve the test file with:
# attested-tls-proxy ... attested-get <host>:8002 --url-path /hello.txt
Entrypoint = [
"/bin/attested-file-server-start"
"--allowed-remote-attestation-type"
"aws-nitro"
"attested-file-server"
"/srv"
"--server-attestation-type"
"aws-nitro"
"--inner-vsock-port"
"8002"
];
};
};

default = self.packages.${system}.attestation-provider-server-image;
};

Expand Down
12 changes: 6 additions & 6 deletions src/file_server.rs
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
//! Static HTTP file server provided by an attested TLS proxy server
use crate::{
AttestationGenerator, AttestationVerifier, OuterTlsConfig, OuterTlsMode, ProxyError,
ProxyServer, TlsCertAndKey,
ProxyListenAddr, ProxyServer, TlsCertAndKey,
};
use std::{net::SocketAddr, path::PathBuf};
use tokio::net::ToSocketAddrs;
Expand All @@ -13,10 +13,10 @@ pub struct AttestedFileServerConfig<A> {
pub path_to_serve: PathBuf,
/// TLS certificate and key for the optional outer listener
pub outer_cert_and_key: Option<TlsCertAndKey>,
/// Bind address for the optional outer nested-TLS listener
pub outer_listen_addr: Option<A>,
/// Bind address for the optional inner attested-TLS listener
pub inner_listen_addr: Option<A>,
/// Bind address (TCP or vsock) for the optional outer nested-TLS listener
pub outer_listen_addr: Option<ProxyListenAddr<A>>,
/// Bind address (TCP or vsock) for the optional inner attested-TLS listener
pub inner_listen_addr: Option<ProxyListenAddr<A>>,
/// Certificate name to embed in the inner attested certificate
pub inner_certificate_name: Option<String>,
/// Attestation generator used by the proxy server
Expand Down Expand Up @@ -55,7 +55,7 @@ where
(None, None) => None,
};

let server = ProxyServer::new(
let server = ProxyServer::new_with_listeners(
outer_session,
inner_listen_addr,
inner_certificate_name,
Expand Down
4 changes: 2 additions & 2 deletions src/http_version.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,8 @@ pub const ALPN_H2: &[u8] = b"h2";
pub const ALPN_HTTP11: &[u8] = b"http/1.1";

type ProxyClientTlsStream =
tokio_rustls::client::TlsStream<tokio_rustls::client::TlsStream<tokio::net::TcpStream>>;
type ProxyClientInnerOnlyTlsStream = tokio_rustls::client::TlsStream<tokio::net::TcpStream>;
tokio_rustls::client::TlsStream<tokio_rustls::client::TlsStream<crate::TransportStream>>;
type ProxyClientInnerOnlyTlsStream = tokio_rustls::client::TlsStream<crate::TransportStream>;

/// Supported HTTP versions
#[derive(Debug)]
Expand Down
Loading
Loading