This project implements a dual-proxy system that uses the oak_session library
to create a transparent, end-to-end encrypted, and attested tunnel between a
client and a server machine. This allows standard, unmodified applications (like
a web browser or curl) to communicate with an unmodified backend server (like
a standard HTTP server) over a secure channel without requiring any changes to
the applications themselves.
The oak_session library provides a powerful implementation of end-to-end
encrypted and attested communication sessions. However, integrating it directly
requires both the client and server applications to be specifically adapted to
use the oak_session protocol for their transport layer.
This project addresses the challenge of leveraging oak_session's security
guarantees in scenarios where modifying the client or server applications is not
feasible or desirable. By deploying a pair of proxies, we can intercept
plaintext traffic on both ends, tunnel it through a secure oak_session, and
deliver it to the destination, making the entire security layer transparent to
the applications.
The system consists of two main components: a client-side forward proxy and a
server-side reverse proxy. The oak_session is established exclusively between
these two proxies.
+----------------+ +---------------------+ +---------------------+ +-----------------+
| | | | | | | |
| Client App |----->| Client Proxy |=====>| Server Proxy |----->| Server App |
| (e.g. cURL) | Plain| (Forward Proxy) | Oak Session over | (Reverse Proxy) | Plain| (e.g. HTTP Srv) |
| | TCP | | WebSocket | TCP | |
+----------------+ +---------------------+ +---------------------+ +-----------------+
^ | |
| | |
+------------------------------------------+--------------------------------------+
Plain TCP Response (Decrypted by Client Proxy)
- Client Proxy (
//oak_proxy/client): A forward proxy that runs on the client's machine. It listens for incoming plaintext TCP connections from local applications. For each connection, it establishes anoak_sessionwith the Server Proxy, encrypts the traffic, and forwards it. - Server Proxy (
//oak_proxy/server): A reverse proxy that runs on the server's machine. It listens for incoming encrypted connections from the Client Proxy. It decrypts the traffic and forwards the plaintext to the final backend application server. - Shared Library (
//oak_proxy/lib): A shared Rust library containing the common logic for proxying, used by both the client and server binaries.
- Connection Start: The client application is configured to connect to the
Client Proxy's listening address (e.g.,
localhost:9090). - Session Establishment: When the Client Proxy receives a connection, it
initiates an
oak_sessionhandshake with the Server Proxy. The current implementation uses an unattested session for simplicity. The handshake messages are exchanged over the WebSocket connection. - Data Forwarding (Client to Server):
- The Client Proxy reads raw plaintext bytes from the application's TCP stream.
- It passes these bytes to its
ClientSessioninstance, which encrypts them. - The
ClientSessionproduces an encrypted Protobuf message. - The Client Proxy sends this message as a binary frame over the WebSocket to the Server Proxy.
- The Server Proxy receives the binary WebSocket frame.
- It passes the encrypted message to its
ServerSession, which decrypts it. - The resulting plaintext is written to the TCP stream connected to the backend server.
- Data Forwarding (Server to Client): The process is reversed for the response.
To ensure a standardized and robust transport layer, the communication between the client and server proxies uses the WebSocket protocol.
- All communication, including the
oak_sessionhandshake and subsequent data transfer, occurs over a WebSocket connection. - Protobuf messages are serialized and sent as binary frames
(
MessageType::Binary) within the WebSocket protocol.
This approach eliminates the need for a custom framing mechanism, as WebSockets
provide a message-based transport that naturally delineates each Protobuf
message. The WebSocket handling logic is located in the
//oak_proxy/lib/src/websocket.rs module.
- Trust Boundary: The
oak_sessionsecures the channel between the two proxies. The traffic between the local application and its corresponding proxy is plaintext. This is considered secure as long as the proxy and the application are running on the same trusted machine (e.g., on thelocalhostnetwork). - Attestation: When attestation is enabled, the Client Proxy and Server Proxy will cryptographically verify each other's identity and execution environment. The trust does not extend to the unmodified client and server applications themselves, but it guarantees that the tunnel is established between legitimate, attested proxy instances.
Ensure you have Bazel installed and the project dependencies are set up
correctly by running just crates repin from the root of the oak repository.
The proxies are configured using TOML files. Create two files: client.toml and
server.toml. See the Configuration section for details on
the available options.
Example server.toml (Unattested):
listen_address = "127.0.0.1:8081"
backend_address = "127.0.0.1:8080"Example client.toml (Unattested):
listen_address = "127.0.0.1:9090"
server_proxy_url = "ws://127.0.0.1:8081"Build both the client and server binaries using Bazel:
bazel build //oak_proxy/client //oak_proxy/serverThe server proxy listens for encrypted traffic from the client proxy and forwards decrypted traffic to your backend application.
Start a simple backend server to test with. For example, use netcat to listen
on port 8080 and print any received data to the console:
nc -l -p 8080Now, run the server proxy in a separate terminal, pointing it to your configuration file:
bazel run //oak_proxy/server -- --config server.toml --listen-address "127.0.0.1:8081"You should see the output: [Server] Listening on 127.0.0.1:8081.
The client proxy listens for plaintext traffic from your application and forwards it over the secure tunnel to the server proxy.
Run the client proxy in a third terminal:
bazel run //oak_proxy/client -- --config client.toml --listen-address "127.0.0.1:9090" --server-proxy-url "ws://127.0.0.1:8081"You should see the output: [Client] Listening on 127.0.0.1:9090.
Now, use any TCP-based application to send traffic to the client proxy's listen
port (127.0.0.1:9090). The traffic will be transparently encrypted, sent to
the server proxy, decrypted, and forwarded to the netcat backend.
Using curl:
curl -x http://127.0.0.1:9090 http://example.comYou will see the raw HTTP request printed in the terminal where netcat is
running. This demonstrates that the data has been successfully proxied and
decrypted.
The behavior of the client and server proxies is controlled by a TOML
configuration file passed via the --config command-line argument.
listen_address: TheSocketAddr(e.g.,"127.0.0.1:9090") where the proxy should listen for incoming connections.attestation_generators: (Optional) A list of generators to use for generating attestation evidence to send to the peer.attestation_verifiers: (Optional) A list of verifiers to use for validating the peer's attestation evidence.
server_proxy_url: The WebSocketUrlof the server proxy.
backend_address: TheSocketAddrof the final backend application where plaintext traffic should be forwarded.backend_command: (Optional) A command to execute a backend process that the server proxy will manage. The backend command should be configured with flags to listen inbackend_address. It has the following structure:cmd: The command to execute.args: A list of arguments for the command.restart_policy: Defines the behavior when the process exits. Can beterminate(default, exits the proxy),always(restarts the process), ornever(does nothing).
The presence of the attestation_generators and attestation_verifiers
sections determines the attestation mode:
- Unattested (Default): If both are omitted, the session is unattested.
- Unidirectional Attestation: If one proxy has a
generatorand the other has averifier, the session is unidirectionally attested. - Bidirectional Attestation: If both proxies have
generatorandverifiersections, the session is mutually attested.
This example shows how to configure both proxies to generate and verify Confidential Space attestations.
server.toml
listen_address = "127.0.0.1:8081"
backend_address = "127.0.0.1:8080"
# Generate our own Confidential Space attestation.
[[attestation_generators]]
type = "confidential_space"
# Verify the client's Confidential Space attestation.
[[attestation_verifiers]]
type = "confidential_space"
root_certificate_pem_path = "/path/to/gcp_root.pem"client.toml
listen_address = "127.0.0.1:9090"
server_proxy_url = "ws://127.0.0.1:8081"
# Generate our own Confidential Space attestation.
[[attestation_generators]]
type = "confidential_space"
# Verify the server's Confidential Space attestation.
[[attestation_verifiers]]
type = "confidential_space"
root_certificate_pem_path = "/path/to/gcp_root.pem"The proxy is designed to be extensible with new attestation flows. This is
achieved by adding new variants to the GeneratorConfig and VerifierConfig
enums in //oak_proxy/lib/src/config/mod.rs.
Let's say you want to add a new attestation mechanism called "MyCustomAttestation".
-
Create a New Module:
- Create a new file for your attestation logic, for example
//oak_proxy/lib/src/config/my_custom_attestation.rs. - In this new file, define
MyCustomAttestationGeneratorParamsandMyCustomAttestationVerifierParamsstructs to hold any parameters needed for your attestation (e.g., file paths, endpoint URLs). Make sure they deriveserde::Deserialize.
- Create a new file for your attestation logic, for example
-
Integrate with the Config Module:
-
In
//oak_proxy/lib/src/config/mod.rs, declare your new module withpub mod my_custom_attestation;. -
Import your new param structs.
-
Add new variants to the
GeneratorConfigandVerifierConfigenums, referencing your new structs:// in //oak_proxy/lib/src/config/mod.rs // Import your params use self::my_custom_attestation::{MyCustomAttestationGeneratorParams, MyCustomAttestationVerifierParams}; #[derive(Deserialize)] #[serde(tag = "type", rename_all = "snake_case")] pub enum GeneratorConfig { ConfidentialSpace(ConfidentialSpaceGeneratorParams), MyCustomAttestation(MyCustomAttestationGeneratorParams), // Your new variant } #[derive(Deserialize)] #[serde(tag = "type", rename_all = "snake_case")] pub enum VerifierConfig { ConfidentialSpace(ConfidentialSpaceVerifierParams), MyCustomAttestation(MyCustomAttestationVerifierParams), // Your new variant }
-
-
Implement the
applyMethod:-
In your new module (
my_custom_attestation.rs), implement theapplymethod for your param structs. This method takes aSessionConfigBuilderand should add the appropriateAttester,Endorser,Binder, orVerifierto it. -
For the generator:
// in //oak_proxy/lib/src/config/my_custom_attestation.rs impl MyCustomAttestationGeneratorParams { pub fn apply(&self, builder: SessionConfigBuilder) -> anyhow::Result<SessionConfigBuilder> { // 1. Create instances of your custom attester, endorser, and binder. // 2. Add them to the builder. // Ok(builder.add_self_attester(...).add_self_endorser(...).add_session_binder(...)) } }
-
For the verifier:
// in //oak_proxy/lib/src/config/my_custom_attestation.rs impl MyCustomAttestationVerifierParams { pub fn apply(&self, builder: SessionConfigBuilder) -> anyhow::Result<SessionConfigBuilder> { // 1. Create an instance of your custom verifier. // 2. Add it to the builder. // Ok(builder.add_peer_verifier_with_key_extractor(...)) } }
-
-
Update the
matchStatements:- In
//oak_proxy/lib/src/config/mod.rs, add a branch to thematchstatement in theapplymethods forGeneratorConfigandVerifierConfigto call your new implementation.
- In
-
Use in Configuration:
-
You can now use your new type in the TOML configuration files:
[[attestation_generators]] type = "my_custom_attestation" # ... any other params for your struct
-
The proxy includes an integration test that simulates the manual steps described in the "Usage" section. It programmatically creates configurations, starts the proxies, and verifies that data can be sent and received correctly.
To run the test:
bazel test //oak_proxy:oak_proxy_integration_testThis implementation is a functional proof-of-concept. Future enhancements could include:
- Configuration Caching: Avoid re-creating the
SessionConfigand its associated generators and verifiers for each new connection to improve performance. - Robustness: Improve error handling and connection management for production use cases.
- Packaging: Create distributable packages for easier deployment of the proxies on client and server machines.