- Rust 100%
| Filename | Latest commit message | Latest commit date |
|---|---|---|
| src | ||
| .gitignore | ||
| Cargo.lock | ||
| Cargo.toml | ||
| README.md | ||
quinn-rpk-demo
This is a basic demo of how to set up a QUIC connection that uses IETF RFC 7250 for Raw Public Key authentication. This is useful for peer-to-peer networks where you don't need to validate against certs from a certificate authority, or where trusted self-signed certificates are distributed with each part of a system.
The demo supports Post-Quantum ML-DSA-65 keypairs and validates on both sides that the message is signed by the public key that represents the peer identity. The peer identity (Node ID) is derived by hashing the large post-quantum public key with SHA-256 to maintain a compact 32-byte identifier.
Instructions
To run the server:
cargo run server
It also supports a listen address if you want to run this not on the same computer as the client (it defaults to 127.0.0.1:4433):
cargo run server --listen 0.0.0.0:4433
To run the client:
cargo run client
It also supports a server address if you want to run this not on the same computer as the server:
cargo run client --server <SERVER_IP>
Both of these support:
--key <PATH> --cert <PATH>
After running the server then the client, you should get something like this on the server side:
Server mode
Generated and saved new ML-DSA-65 RPK to server.key and server.crt
server ‣ my id 7716db8efba7f4fba29e771b987d0cbc30ed11a7f300fa4779a8c7dfc6f78041
server listening on 127.0.0.1:4433
server ‣ client id 36109de5804374945cf59e8963b3dde254f7952821413fd5e396f3e0f540b589
And this on the client side:
Client mode
Generated and saved new ML-DSA-65 RPK to client.key and client.crt
client ‣ my id 36109de5804374945cf59e8963b3dde254f7952821413fd5e396f3e0f540b589
client ‣ server id 7716db8efba7f4fba29e771b987d0cbc30ed11a7f300fa4779a8c7dfc6f78041
client ‣ echoed: hello mutual rpk quic
Code Overview
This is a brief overview of the code structure to make reading it easier, but you should really read the code. The whole point of this repo is to demonstrate how to write the code to do this securely with rustls and quinn.
Common
The client and server implementations share a common make_rpk function that takes in options for the key path and cert path if files exist at these paths. If these arguments aren't passed in, it will generate new keypairs for the server or client depending on the caller of the function.
Key generation is handled with rcgen through KeyPair and PKCS_ML_DSA_65. Key saving is handled using the .serialize_der() fn for the private key and the PublicKeyData trait's .subject_public_key_info() fn for the public key. Key loading is handled with:
rustls::pki_types::{CertificateDer, PrivateKeyDer, PrivatePkcs8KeyDer}rustls::sign::{CertifiedKey, SigningKey}rustls::crypto::aws_lc_rs::sign::any_supported_type
Because ML-DSA-65 public keys are large (~2 KB), the actual Node ID printed to the console is a SHA-256 hash of the raw public key bytes.
Server
To handle RPK we need to implement our own ClientCertVerifier trait for our client struct and pass it to the .with_client_cert_verifier mutator fn on ServerConfig::builder(). We also use AlwaysResolvesServerRawPublicKeys which resolves the first cert sent and pass this into the .with_cert_resolver mutator fn on the config builder. The output of this can be used to build the QuicServerConfig with try_from() and the ServerConfig using with_crypto() and finally the Endpoint using server(). After these we can spawn the accept loop which connects to clients, hashes and prints the peer ID public key that is connecting, before echoing the client data back to them.
Client
Similarly to the server, for the client we need to implement a ServerCertVerifier trait for the client struct and pass it to the .with_custom_certificate_verifier() mutator fn on the ClientConfig::builder(), along with AlwaysResolvesClientRawPublicKeys which is passed to the .with_client_cert_resolver() mutator fn. This then allows us to build the endpoint through the QuicClientConfig::try_from() being passed into the ClientConfig::new() and finally Endpoint::client(). After this we can connect to server_addr with the endpoint's connect() fn, extract the peer identity of the server, hash it into our Node ID, and verify it before sending data which gets echoed back to the client.