feat(core): add QUIC TLS admission transport
This commit is contained in:
@@ -0,0 +1,806 @@
|
||||
use std::fs;
|
||||
use std::process::{Child, Command, Stdio};
|
||||
use std::sync::atomic::{AtomicUsize, Ordering};
|
||||
use std::sync::{Arc, Mutex};
|
||||
use std::thread;
|
||||
use std::time::{Duration, Instant};
|
||||
|
||||
use base64::engine::general_purpose::STANDARD;
|
||||
use base64::Engine as _;
|
||||
use rustls::pki_types::{PrivateKeyDer, PrivatePkcs8KeyDer};
|
||||
use rustls::sign::SigningKey;
|
||||
use rustls::SignatureScheme;
|
||||
use serde::Deserialize;
|
||||
use versevdi_core::error::CoreError;
|
||||
use versevdi_core::transport::{
|
||||
connect, connect_with_cancellation, AdmissionSigner, Cancellation, Signers, TlsEd25519Signer,
|
||||
};
|
||||
use versevdi_core::wire::{ConnectionManifest, NativeTunnelCredential};
|
||||
|
||||
const ORACLE_SOURCE: &str = r#"
|
||||
package main
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"crypto/ed25519"
|
||||
"crypto/rand"
|
||||
"crypto/tls"
|
||||
"crypto/x509"
|
||||
"crypto/x509/pkix"
|
||||
"encoding/base64"
|
||||
"encoding/json"
|
||||
"encoding/pem"
|
||||
"errors"
|
||||
"math/big"
|
||||
"os"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
gateway "git.sechmachine.io.vn/sechmachine/VerseVDI-Data-Plane/gateway"
|
||||
protocol "git.sechmachine.io.vn/sechmachine/VerseVDI-Protocol/gen/go/protocol"
|
||||
)
|
||||
|
||||
type admission struct {
|
||||
mu sync.Mutex
|
||||
lastTranscript []byte
|
||||
reusable bool
|
||||
failWorkOnce bool
|
||||
authority protocol.SessionAuthority
|
||||
work protocol.ProviderSessionWork
|
||||
public ed25519.PublicKey
|
||||
}
|
||||
|
||||
func (a *admission) Admit(_ context.Context, request protocol.TunnelAdmissionRequest) (protocol.SessionAuthority, error) {
|
||||
signature, err := base64.RawURLEncoding.DecodeString(request.DeviceSignature)
|
||||
transcript := request.DeviceAdmissionTranscript()
|
||||
if err != nil || !ed25519.Verify(a.public, transcript, signature) {
|
||||
return protocol.SessionAuthority{}, gateway.ErrAdmissionRejected
|
||||
}
|
||||
a.mu.Lock(); defer a.mu.Unlock()
|
||||
if !a.reusable && bytes.Equal(a.lastTranscript, transcript) { return protocol.SessionAuthority{}, gateway.ErrAdmissionRejected }
|
||||
a.lastTranscript = append(a.lastTranscript[:0], transcript...)
|
||||
return a.authority, nil
|
||||
}
|
||||
func (a *admission) ProviderWork(context.Context, protocol.SessionAuthority) (protocol.ProviderSessionWork, error) {
|
||||
a.mu.Lock(); defer a.mu.Unlock()
|
||||
if a.failWorkOnce { a.failWorkOnce = false; return protocol.ProviderSessionWork{}, context.DeadlineExceeded }
|
||||
return a.work, nil
|
||||
}
|
||||
func (a *admission) Release(context.Context, protocol.SessionAuthority) error { return nil }
|
||||
|
||||
type provider struct{}
|
||||
type session struct { state protocol.ProviderState; video chan gateway.ProviderMedia; audio chan gateway.ProviderMedia; events chan gateway.ProviderEvent }
|
||||
func (provider) Start(_ context.Context, request gateway.LaunchRequest) (gateway.ProviderSession, error) {
|
||||
return &session{state: protocol.ProviderState{Version:"1", SessionID:request.SessionID, State:gateway.ProviderStateReady, Channels:[]string{"video","audio","input","feedback"}}, video:make(chan gateway.ProviderMedia), audio:make(chan gateway.ProviderMedia), events:make(chan gateway.ProviderEvent)}, nil
|
||||
}
|
||||
func (s *session) Ready(context.Context) error { return nil }
|
||||
func (s *session) Video() <-chan gateway.ProviderMedia { return s.video }
|
||||
func (s *session) Audio() <-chan gateway.ProviderMedia { return s.audio }
|
||||
func (s *session) Events() <-chan gateway.ProviderEvent { return s.events }
|
||||
func (s *session) Input(context.Context, gateway.InputEvent) error { return nil }
|
||||
func (s *session) Feedback(context.Context, gateway.Feedback) error { return nil }
|
||||
func (s *session) ReadClipboard(context.Context) (string, error) { return "", errors.New("disabled") }
|
||||
func (s *session) WriteClipboard(context.Context, string) error { return errors.New("disabled") }
|
||||
func (s *session) Telemetry() gateway.ProviderTelemetry { return gateway.ProviderTelemetry{State:s.state.State} }
|
||||
func (s *session) ReleaseAll(context.Context) error { return nil }
|
||||
func (s *session) Terminate(context.Context) error { return nil }
|
||||
func (s *session) State() protocol.ProviderState { return s.state }
|
||||
|
||||
func makeCertificate(parent *x509.Certificate, parentKey ed25519.PrivateKey, serial int64, dns string, usage x509.ExtKeyUsage, isCA bool) ([]byte, ed25519.PrivateKey) {
|
||||
public, private, err := ed25519.GenerateKey(rand.Reader); if err != nil { panic(err) }
|
||||
template := &x509.Certificate{SerialNumber:big.NewInt(serial), Subject:pkix.Name{CommonName:dns}, DNSNames:[]string{dns}, NotBefore:time.Now().Add(-time.Hour), NotAfter:time.Now().Add(time.Hour), IsCA:isCA, BasicConstraintsValid:true, KeyUsage:x509.KeyUsageDigitalSignature}
|
||||
if isCA { template.KeyUsage |= x509.KeyUsageCertSign } else { template.ExtKeyUsage = []x509.ExtKeyUsage{usage} }
|
||||
if parent == nil { parent = template; parentKey = private }
|
||||
der, err := x509.CreateCertificate(rand.Reader, template, parent, public, parentKey); if err != nil { panic(err) }
|
||||
return der, private
|
||||
}
|
||||
func pemCert(ders ...[]byte) string { var out []byte; for _, der := range ders { out = append(out, pem.EncodeToMemory(&pem.Block{Type:"CERTIFICATE",Bytes:der})...) }; return string(out) }
|
||||
|
||||
func main() {
|
||||
readyPath, stopPath := os.Args[1], os.Args[2]
|
||||
mode := os.Getenv("VERSEVDI_RUST_ORACLE_MODE")
|
||||
caDER, caKey := makeCertificate(nil, nil, 1, "Verse Rust Oracle CA", 0, true)
|
||||
ca, err := x509.ParseCertificate(caDER); if err != nil { panic(err) }
|
||||
serverDER, serverKey := makeCertificate(ca, caKey, 2, "gateway.test", x509.ExtKeyUsageServerAuth, false)
|
||||
clientDER, clientKey := makeCertificate(ca, caKey, 3, "client.test", x509.ExtKeyUsageClientAuth, false)
|
||||
_, admissionKey := makeCertificate(ca, caKey, 6, "admission.test", x509.ExtKeyUsageClientAuth, false)
|
||||
badCADER, _ := makeCertificate(nil, nil, 4, "Wrong CA", 0, true)
|
||||
_, wrongClientKey := makeCertificate(ca, caKey, 5, "wrong-client.test", x509.ExtKeyUsageClientAuth, false)
|
||||
pool := x509.NewCertPool(); pool.AddCert(ca)
|
||||
server := &tls.Config{MinVersion:tls.VersionTLS13, MaxVersion:tls.VersionTLS13, Certificates:[]tls.Certificate{{Certificate:[][]byte{serverDER,caDER},PrivateKey:serverKey}}, ClientAuth:tls.RequireAndVerifyClientCert, ClientCAs:pool}
|
||||
expiry := time.Now().Add(5*time.Minute).UTC().Truncate(time.Second).Format(time.RFC3339)
|
||||
authority := protocol.SessionAuthority{Version:"1",SessionID:"session-1",GatewayID:"gateway-1",Audience:"versevdi-gateway",ExpiresAt:expiry,Capabilities:gateway.DefaultCapabilities(),ProviderProfile:gateway.ProviderProfileApollo,ProviderIdentity:"oracle#sha256:fixture"}
|
||||
if mode == "session" { authority.SessionID = "other-session" }
|
||||
if mode == "gateway" { authority.GatewayID = "other-gateway" }
|
||||
if mode == "audience" { authority.Audience = "other-audience" }
|
||||
if mode == "reconnect" { authority.ReconnectSequence = 1 }
|
||||
if mode == "expiry" { authority.ExpiresAt = time.Now().Add(-time.Minute).UTC().Truncate(time.Second).Format(time.RFC3339) }
|
||||
if mode == "capability" { authority.Capabilities.ClientDecode = []string{"hevc-opus"} }
|
||||
if mode == "alpn" { server.NextProtos = []string{"wrong-alpn"} }
|
||||
work := protocol.ProviderSessionWork{Version:"1",SessionID:authority.SessionID,GatewayID:authority.GatewayID,ExpiresAt:expiry,ProviderProfile:gateway.ProviderProfileApollo,ProviderIdentity:authority.ProviderIdentity,PolicyVersionID:"policy-1",StreamPolicy:protocol.ProviderStreamPolicy{ResolutionWidth:1920,ResolutionHeight:1080,Fps:60,Codec:"H264",BitrateKbps:8000,AudioEnabled:true},ApplicationID:"1",ClientID:"client",ManagementHost:"provider.invalid",ManagementPort:47990,StreamHost:"provider.invalid",StreamPort:47984,ClientCertificatePem:"certificate",ClientPrivateKeyPem:"private-key",ServerCertificatePem:"certificate",ClipboardPolicy:protocol.ClipboardPolicy{MaxTextBytes:65536,MaxUpdatesPerMinute:30}}
|
||||
replayGuard := &admission{authority:authority,work:work,public:admissionKey.Public().(ed25519.PublicKey)}
|
||||
replayRequest := protocol.TunnelAdmissionRequest{Version:"1",SessionID:"session-1",GatewayID:"gateway-1",Audience:"versevdi-gateway",Grant:"ggggggggggggggggggggggggggggggggggggggggggg",ClientNonce:"bm9uY2UtZm9yLXJlcGxheQ",Capabilities:gateway.DefaultCapabilities()}
|
||||
replayRequest.DeviceSignature = base64.RawURLEncoding.EncodeToString(ed25519.Sign(admissionKey,replayRequest.DeviceAdmissionTranscript()))
|
||||
_, firstReplayErr := replayGuard.Admit(context.Background(),replayRequest)
|
||||
_, secondReplayErr := replayGuard.Admit(context.Background(),replayRequest)
|
||||
replayRejected := firstReplayErr == nil && errors.Is(secondReplayErr,gateway.ErrAdmissionRejected)
|
||||
admissionService := &admission{authority:authority,work:work,public:admissionKey.Public().(ed25519.PublicKey),reusable:mode == "reusable",failWorkOnce:mode == "post-retryable"}
|
||||
newService := func() *gateway.Server {
|
||||
service, err := gateway.NewServer(gateway.ServerConfig{ListenAddress:"127.0.0.1:0",TLSConfig:server,GatewayID:authority.GatewayID,Admission:admissionService,Provider:provider{}}); if err != nil { panic(err) }
|
||||
return service
|
||||
}
|
||||
service := newService()
|
||||
services := []*gateway.Server{service}
|
||||
addresses := []string{service.Addr().String()}
|
||||
if mode == "retryable" {
|
||||
draining := newService()
|
||||
draining.BeginDrain()
|
||||
services = []*gateway.Server{draining, service}
|
||||
addresses = []string{draining.Addr().String(), service.Addr().String()}
|
||||
}
|
||||
if mode == "post-retryable" {
|
||||
second := newService()
|
||||
services = []*gateway.Server{service, second}
|
||||
addresses = []string{service.Addr().String(), second.Addr().String()}
|
||||
}
|
||||
ctx, cancel := context.WithCancel(context.Background()); defer cancel()
|
||||
done := make(chan error,len(services)); for _, current := range services { go func(server *gateway.Server){done <- server.Serve(ctx)}(current) }
|
||||
keyDER, err := x509.MarshalPKCS8PrivateKey(clientKey); if err != nil { panic(err) }
|
||||
admissionKeyDER, err := x509.MarshalPKCS8PrivateKey(admissionKey); if err != nil { panic(err) }
|
||||
manifest := map[string]any{"version":"1","purpose":"launch","session_id":"session-1","reconnect_sequence":0,"gateway":map[string]any{"id":"gateway-1","addresses":addresses,"public_identity":"gateway.test"},"tunnel":map[string]any{"versions":[]string{"verse-gateway-v1/1"},"features":[]string{"control.v1"}},"profile":map[string]any{"id":"standard","bounds":map[string]any{"minimum_kbps":1000,"target_kbps":5000,"maximum_kbps":10000},"display_mode":map[string]any{"resolution_width":1920,"resolution_height":1080,"fps":60}},"grant":map[string]any{"opaque_value":"ggggggggggggggggggggggggggggggggggggggggggg","expires_at":"2099-01-01T00:00:00Z","audience":"versevdi-gateway"},"correlation_id":"oracle"}
|
||||
credential := map[string]any{"client_device_id":"device","device_key_id":"key","certificate_chain_pem":pemCert(clientDER,caDER),"trust_bundle_pem":pemCert(caDER),"expires_at":"2099-01-01T00:00:00Z"}
|
||||
manifestJSON,_ := json.Marshal(manifest); credentialJSON,_ := json.Marshal(credential)
|
||||
wrongKeyDER, err := x509.MarshalPKCS8PrivateKey(wrongClientKey); if err != nil { panic(err) }
|
||||
serverKeyDER, err := x509.MarshalPKCS8PrivateKey(serverKey); if err != nil { panic(err) }
|
||||
ready,_ := json.Marshal(map[string]any{"manifest":string(manifestJSON),"credential":string(credentialJSON),"admission_key":base64.StdEncoding.EncodeToString(admissionKeyDER),"client_key":base64.StdEncoding.EncodeToString(keyDER),"wrong_client_key":base64.StdEncoding.EncodeToString(wrongKeyDER),"server_key":base64.StdEncoding.EncodeToString(serverKeyDER),"server_chain":pemCert(serverDER,caDER),"bad_trust":pemCert(badCADER),"replay_rejected":replayRejected})
|
||||
if err := os.WriteFile(readyPath,ready,0600); err != nil { panic(err) }
|
||||
for { if _, err := os.Stat(stopPath); err == nil { break }; time.Sleep(10*time.Millisecond) }
|
||||
cancel(); for _, current := range services { _ = current.Close() }; for range services { <-done }
|
||||
}
|
||||
"#;
|
||||
|
||||
#[derive(Deserialize)]
|
||||
struct Ready {
|
||||
manifest: String,
|
||||
credential: String,
|
||||
admission_key: String,
|
||||
client_key: String,
|
||||
wrong_client_key: String,
|
||||
server_key: String,
|
||||
server_chain: String,
|
||||
bad_trust: String,
|
||||
replay_rejected: bool,
|
||||
}
|
||||
|
||||
struct Oracle {
|
||||
_serial: std::sync::MutexGuard<'static, ()>,
|
||||
child: Child,
|
||||
directory: std::path::PathBuf,
|
||||
ready: Ready,
|
||||
}
|
||||
|
||||
impl Oracle {
|
||||
fn start(mode: &str) -> Self {
|
||||
static SERIAL: Mutex<()> = Mutex::new(());
|
||||
static NEXT: AtomicUsize = AtomicUsize::new(1);
|
||||
let serial = SERIAL
|
||||
.lock()
|
||||
.unwrap_or_else(std::sync::PoisonError::into_inner);
|
||||
let directory = std::env::temp_dir().join(format!(
|
||||
"versevdi-rust-gateway-oracle-{}-{}",
|
||||
std::process::id(),
|
||||
NEXT.fetch_add(1, Ordering::Relaxed)
|
||||
));
|
||||
fs::create_dir(&directory).expect("create oracle directory");
|
||||
let source = directory.join("main.go");
|
||||
let ready_path = directory.join("ready.json");
|
||||
let stop_path = directory.join("stop");
|
||||
fs::write(&source, ORACLE_SOURCE).expect("write oracle source");
|
||||
let child = Command::new("go")
|
||||
.args(["run", source.to_str().expect("source path is UTF-8")])
|
||||
.arg(&ready_path)
|
||||
.arg(&stop_path)
|
||||
.env("VERSEVDI_RUST_ORACLE_MODE", mode)
|
||||
.current_dir(env!("CARGO_MANIFEST_DIR"))
|
||||
.stdout(Stdio::piped())
|
||||
.stderr(Stdio::piped())
|
||||
.spawn()
|
||||
.expect("start Go gateway oracle");
|
||||
let deadline = Instant::now() + Duration::from_secs(20);
|
||||
while Instant::now() < deadline {
|
||||
if let Ok(bytes) = fs::read(&ready_path) {
|
||||
if let Ok(ready) = serde_json::from_slice(&bytes) {
|
||||
return Self {
|
||||
_serial: serial,
|
||||
child,
|
||||
directory,
|
||||
ready,
|
||||
};
|
||||
}
|
||||
}
|
||||
thread::sleep(Duration::from_millis(10));
|
||||
}
|
||||
let output = child.wait_with_output().expect("collect Go oracle output");
|
||||
panic!(
|
||||
"Go gateway oracle did not become ready: status={} stdout={} stderr={}",
|
||||
output.status,
|
||||
String::from_utf8_lossy(&output.stdout),
|
||||
String::from_utf8_lossy(&output.stderr)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
impl Drop for Oracle {
|
||||
fn drop(&mut self) {
|
||||
let _ = fs::write(self.directory.join("stop"), []);
|
||||
let _ = self.child.wait();
|
||||
let _ = fs::remove_dir_all(&self.directory);
|
||||
}
|
||||
}
|
||||
|
||||
fn test_key(encoded: &str) -> Arc<dyn SigningKey> {
|
||||
let der = STANDARD.decode(encoded).expect("decode test key");
|
||||
rustls::crypto::ring::default_provider()
|
||||
.key_provider
|
||||
.load_private_key(PrivateKeyDer::Pkcs8(PrivatePkcs8KeyDer::from(der)))
|
||||
.expect("load ephemeral Ed25519 test key")
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn callback_ed25519_signer_completes_tls13_quic_admission_without_private_key_input() {
|
||||
let oracle = Oracle::start("");
|
||||
let manifest = ConnectionManifest::decode(oracle.ready.manifest.as_bytes()).expect("manifest");
|
||||
let credential =
|
||||
NativeTunnelCredential::decode(oracle.ready.credential.as_bytes()).expect("credential");
|
||||
let admission_key = test_key(&oracle.ready.admission_key);
|
||||
let tls_key = test_key(&oracle.ready.client_key);
|
||||
let admission_inputs = Arc::new(Mutex::new(Vec::new()));
|
||||
let tls_inputs = Arc::new(Mutex::new(Vec::new()));
|
||||
let admission_calls = Arc::new(AtomicUsize::new(0));
|
||||
let tls_calls = Arc::new(AtomicUsize::new(0));
|
||||
|
||||
let sign = |key: Arc<dyn SigningKey>, input: &[u8]| -> Result<[u8; 64], CoreError> {
|
||||
let signer = key
|
||||
.choose_scheme(&[SignatureScheme::ED25519])
|
||||
.ok_or(CoreError::Tls)?;
|
||||
signer
|
||||
.sign(input)
|
||||
.map_err(|_| CoreError::Tls)?
|
||||
.try_into()
|
||||
.map_err(|_| CoreError::Tls)
|
||||
};
|
||||
let admission = {
|
||||
let key = Arc::clone(&admission_key);
|
||||
let inputs = Arc::clone(&admission_inputs);
|
||||
let calls = Arc::clone(&admission_calls);
|
||||
AdmissionSigner::new(move |input| {
|
||||
calls.fetch_add(1, Ordering::SeqCst);
|
||||
*inputs.lock().expect("admission inputs") = input.to_vec();
|
||||
sign(Arc::clone(&key), input)
|
||||
})
|
||||
};
|
||||
let tls = {
|
||||
let key = Arc::clone(&tls_key);
|
||||
let inputs = Arc::clone(&tls_inputs);
|
||||
let calls = Arc::clone(&tls_calls);
|
||||
TlsEd25519Signer::new(move |input| {
|
||||
calls.fetch_add(1, Ordering::SeqCst);
|
||||
*inputs.lock().expect("TLS inputs") = input.to_vec();
|
||||
sign(Arc::clone(&key), input)
|
||||
})
|
||||
};
|
||||
let runtime = tokio::runtime::Runtime::new().expect("runtime");
|
||||
let session = runtime
|
||||
.block_on(connect(
|
||||
&manifest,
|
||||
&credential,
|
||||
Signers::new(admission, tls),
|
||||
"2026-08-12T00:00:00Z",
|
||||
Duration::from_secs(10),
|
||||
))
|
||||
.expect("production Go gateway admission");
|
||||
|
||||
assert_eq!(session.authority().session_id(), "session-1");
|
||||
assert_eq!(admission_calls.load(Ordering::SeqCst), 1);
|
||||
assert!(tls_calls.load(Ordering::SeqCst) >= 1);
|
||||
assert!(admission_inputs
|
||||
.lock()
|
||||
.expect("admission transcript")
|
||||
.starts_with(b"versevdi/tunnel-admission/v1"));
|
||||
assert_ne!(
|
||||
*admission_inputs.lock().expect("admission transcript"),
|
||||
*tls_inputs.lock().expect("TLS transcript")
|
||||
);
|
||||
runtime.block_on(session.close());
|
||||
}
|
||||
|
||||
fn signer(key: Arc<dyn SigningKey>) -> impl Fn(&[u8]) -> Result<[u8; 64], CoreError> {
|
||||
move |input| {
|
||||
key.choose_scheme(&[SignatureScheme::ED25519])
|
||||
.ok_or(CoreError::Tls)?
|
||||
.sign(input)
|
||||
.map_err(|_| CoreError::Tls)?
|
||||
.try_into()
|
||||
.map_err(|_| CoreError::Tls)
|
||||
}
|
||||
}
|
||||
|
||||
fn connect_oracle(
|
||||
oracle: &Oracle,
|
||||
admission_key: &str,
|
||||
tls_key: &str,
|
||||
mutate_manifest: impl FnOnce(&mut serde_json::Value),
|
||||
mutate_credential: impl FnOnce(&mut serde_json::Value),
|
||||
) -> Result<versevdi_core::transport::TransportSession, CoreError> {
|
||||
let mut manifest: serde_json::Value =
|
||||
serde_json::from_str(&oracle.ready.manifest).expect("manifest JSON");
|
||||
let mut credential: serde_json::Value =
|
||||
serde_json::from_str(&oracle.ready.credential).expect("credential JSON");
|
||||
mutate_manifest(&mut manifest);
|
||||
mutate_credential(&mut credential);
|
||||
let manifest =
|
||||
ConnectionManifest::decode(&serde_json::to_vec(&manifest).expect("encode manifest"))?;
|
||||
let credential = NativeTunnelCredential::decode(
|
||||
&serde_json::to_vec(&credential).expect("encode credential"),
|
||||
)?;
|
||||
let runtime = tokio::runtime::Runtime::new().expect("runtime");
|
||||
runtime.block_on(connect(
|
||||
&manifest,
|
||||
&credential,
|
||||
Signers::new(
|
||||
AdmissionSigner::new(signer(test_key(admission_key))),
|
||||
TlsEd25519Signer::new(signer(test_key(tls_key))),
|
||||
),
|
||||
"2026-08-12T00:00:00Z",
|
||||
Duration::from_secs(5),
|
||||
))
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn production_gateway_rejects_tls_and_admission_identity_mismatches() {
|
||||
for case in [
|
||||
"wrong-key",
|
||||
"wrong-signature",
|
||||
"wrong-leaf",
|
||||
"wrong-sni",
|
||||
"wrong-root",
|
||||
"wrong-alpn",
|
||||
] {
|
||||
let oracle = Oracle::start("");
|
||||
let admission_key = &oracle.ready.admission_key;
|
||||
let tls_key = &oracle.ready.client_key;
|
||||
let wrong = &oracle.ready.wrong_client_key;
|
||||
let result = match case {
|
||||
"wrong-key" => connect_oracle(&oracle, admission_key, wrong, |_| {}, |_| {}),
|
||||
"wrong-signature" => connect_oracle(&oracle, wrong, tls_key, |_| {}, |_| {}),
|
||||
"wrong-leaf" => connect_oracle(
|
||||
&oracle,
|
||||
admission_key,
|
||||
&oracle.ready.server_key,
|
||||
|_| {},
|
||||
|credential| {
|
||||
credential["certificate_chain_pem"] = oracle.ready.server_chain.clone().into();
|
||||
},
|
||||
),
|
||||
"wrong-sni" => connect_oracle(
|
||||
&oracle,
|
||||
admission_key,
|
||||
tls_key,
|
||||
|manifest| manifest["gateway"]["public_identity"] = "other.test".into(),
|
||||
|_| {},
|
||||
),
|
||||
"wrong-root" => connect_oracle(
|
||||
&oracle,
|
||||
admission_key,
|
||||
tls_key,
|
||||
|_| {},
|
||||
|credential| credential["trust_bundle_pem"] = oracle.ready.bad_trust.clone().into(),
|
||||
),
|
||||
"wrong-alpn" => {
|
||||
drop(oracle);
|
||||
let oracle = Oracle::start("alpn");
|
||||
connect_oracle(
|
||||
&oracle,
|
||||
&oracle.ready.admission_key,
|
||||
&oracle.ready.client_key,
|
||||
|_| {},
|
||||
|_| {},
|
||||
)
|
||||
}
|
||||
_ => unreachable!(),
|
||||
};
|
||||
let expected = if case == "wrong-signature" {
|
||||
CoreError::AuthorityRejected
|
||||
} else {
|
||||
CoreError::Tls
|
||||
};
|
||||
assert_eq!(result.err(), Some(expected), "wrong error for {case}");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn production_gateway_and_rust_reject_authority_binding_and_capability_mismatches() {
|
||||
for mode in [
|
||||
"session",
|
||||
"gateway",
|
||||
"audience",
|
||||
"reconnect",
|
||||
"capability",
|
||||
"expiry",
|
||||
] {
|
||||
let oracle = Oracle::start(mode);
|
||||
let result = connect_oracle(
|
||||
&oracle,
|
||||
&oracle.ready.admission_key,
|
||||
&oracle.ready.client_key,
|
||||
|_| {},
|
||||
|_| {},
|
||||
);
|
||||
assert!(result.is_err(), "{mode} mismatch unexpectedly connected");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn signer_purposes_are_not_interchangeable() {
|
||||
let oracle = Oracle::start("");
|
||||
let admission_key = test_key(&oracle.ready.admission_key);
|
||||
let tls_key = test_key(&oracle.ready.client_key);
|
||||
let manifest = ConnectionManifest::decode(oracle.ready.manifest.as_bytes()).expect("manifest");
|
||||
let credential =
|
||||
NativeTunnelCredential::decode(oracle.ready.credential.as_bytes()).expect("credential");
|
||||
let admission = AdmissionSigner::new(move |message| {
|
||||
if !message.starts_with(b"versevdi/tunnel-admission/v1") {
|
||||
return Err(CoreError::AuthorityRejected);
|
||||
}
|
||||
signer(Arc::clone(&admission_key))(message)
|
||||
});
|
||||
let tls = TlsEd25519Signer::new(move |message| {
|
||||
if message.starts_with(b"versevdi/tunnel-admission/v1") {
|
||||
return Err(CoreError::Tls);
|
||||
}
|
||||
signer(Arc::clone(&tls_key))(message)
|
||||
});
|
||||
let runtime = tokio::runtime::Runtime::new().expect("runtime");
|
||||
let session = runtime
|
||||
.block_on(connect(
|
||||
&manifest,
|
||||
&credential,
|
||||
Signers::new(admission, tls),
|
||||
"2026-08-12T00:00:00Z",
|
||||
Duration::from_secs(5),
|
||||
))
|
||||
.expect("purpose-separated signers");
|
||||
runtime.block_on(session.close());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn swapping_admission_and_tls_signer_purposes_is_rejected() {
|
||||
let oracle = Oracle::start("");
|
||||
let result = connect_oracle(
|
||||
&oracle,
|
||||
&oracle.ready.client_key,
|
||||
&oracle.ready.admission_key,
|
||||
|_| {},
|
||||
|_| {},
|
||||
);
|
||||
assert!(
|
||||
result.is_err(),
|
||||
"one interchangeable key was accepted for both signer purposes"
|
||||
);
|
||||
}
|
||||
|
||||
fn recording_signers(
|
||||
admission_key: Arc<dyn SigningKey>,
|
||||
tls_key: Arc<dyn SigningKey>,
|
||||
transcripts: Arc<Mutex<Vec<Vec<u8>>>>,
|
||||
) -> Signers {
|
||||
Signers::new(
|
||||
AdmissionSigner::new(move |message| {
|
||||
transcripts
|
||||
.lock()
|
||||
.expect("admission transcripts")
|
||||
.push(message.to_vec());
|
||||
signer(Arc::clone(&admission_key))(message)
|
||||
}),
|
||||
TlsEd25519Signer::new(signer(tls_key)),
|
||||
)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn fresh_nonce_changes_transcript_and_oracle_rejects_identical_replay() {
|
||||
let transcripts = Arc::new(Mutex::new(Vec::new()));
|
||||
let runtime = tokio::runtime::Runtime::new().expect("runtime");
|
||||
for _ in 0..2 {
|
||||
let oracle = Oracle::start("");
|
||||
let manifest =
|
||||
ConnectionManifest::decode(oracle.ready.manifest.as_bytes()).expect("manifest");
|
||||
let credential =
|
||||
NativeTunnelCredential::decode(oracle.ready.credential.as_bytes()).expect("credential");
|
||||
let session = runtime
|
||||
.block_on(connect(
|
||||
&manifest,
|
||||
&credential,
|
||||
recording_signers(
|
||||
test_key(&oracle.ready.admission_key),
|
||||
test_key(&oracle.ready.client_key),
|
||||
Arc::clone(&transcripts),
|
||||
),
|
||||
"2026-08-12T00:00:00Z",
|
||||
Duration::from_secs(5),
|
||||
))
|
||||
.expect("fresh nonce admission");
|
||||
runtime.block_on(session.close());
|
||||
assert!(
|
||||
oracle.ready.replay_rejected,
|
||||
"oracle accepted identical replay"
|
||||
);
|
||||
}
|
||||
let transcripts = transcripts.lock().expect("transcripts");
|
||||
assert_eq!(transcripts.len(), 2);
|
||||
assert_ne!(transcripts[0], transcripts[1], "nonce was reused");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn overall_deadline_cancels_an_unresponsive_manifest_address() {
|
||||
let oracle = Oracle::start("");
|
||||
let socket = std::net::UdpSocket::bind("127.0.0.1:0").expect("bind blackhole");
|
||||
let mut manifest: serde_json::Value =
|
||||
serde_json::from_str(&oracle.ready.manifest).expect("manifest JSON");
|
||||
manifest["gateway"]["addresses"] =
|
||||
serde_json::json!([socket.local_addr().expect("blackhole address").to_string()]);
|
||||
let manifest =
|
||||
ConnectionManifest::decode(&serde_json::to_vec(&manifest).expect("encode manifest"))
|
||||
.expect("manifest");
|
||||
let credential =
|
||||
NativeTunnelCredential::decode(oracle.ready.credential.as_bytes()).expect("credential");
|
||||
let runtime = tokio::runtime::Runtime::new().expect("runtime");
|
||||
let started = Instant::now();
|
||||
let result = runtime.block_on(connect(
|
||||
&manifest,
|
||||
&credential,
|
||||
recording_signers(
|
||||
test_key(&oracle.ready.admission_key),
|
||||
test_key(&oracle.ready.client_key),
|
||||
Arc::new(Mutex::new(Vec::new())),
|
||||
),
|
||||
"2026-08-12T00:00:00Z",
|
||||
Duration::from_millis(150),
|
||||
));
|
||||
assert_eq!(result.err(), Some(CoreError::Cancelled));
|
||||
assert!(started.elapsed() < Duration::from_secs(2));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn overall_deadline_includes_synchronous_admission_signing() {
|
||||
let oracle = Oracle::start("");
|
||||
let manifest = ConnectionManifest::decode(oracle.ready.manifest.as_bytes()).expect("manifest");
|
||||
let credential =
|
||||
NativeTunnelCredential::decode(oracle.ready.credential.as_bytes()).expect("credential");
|
||||
let admission_key = test_key(&oracle.ready.admission_key);
|
||||
let tls_key = test_key(&oracle.ready.client_key);
|
||||
let runtime = tokio::runtime::Runtime::new().expect("runtime");
|
||||
let started = Instant::now();
|
||||
let result = runtime.block_on(connect(
|
||||
&manifest,
|
||||
&credential,
|
||||
Signers::new(
|
||||
AdmissionSigner::new(move |message| {
|
||||
thread::sleep(Duration::from_millis(100));
|
||||
signer(Arc::clone(&admission_key))(message)
|
||||
}),
|
||||
TlsEd25519Signer::new(signer(tls_key)),
|
||||
),
|
||||
"2026-08-12T00:00:00Z",
|
||||
Duration::from_millis(25),
|
||||
));
|
||||
assert_eq!(result.err(), Some(CoreError::Cancelled));
|
||||
assert!(started.elapsed() < Duration::from_secs(2));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn explicit_cancellation_interrupts_network_wait() {
|
||||
let oracle = Oracle::start("");
|
||||
let socket = std::net::UdpSocket::bind("127.0.0.1:0").expect("bind blackhole");
|
||||
let mut manifest: serde_json::Value =
|
||||
serde_json::from_str(&oracle.ready.manifest).expect("manifest JSON");
|
||||
manifest["gateway"]["addresses"] =
|
||||
serde_json::json!([socket.local_addr().expect("blackhole address").to_string()]);
|
||||
let manifest =
|
||||
ConnectionManifest::decode(&serde_json::to_vec(&manifest).expect("encode manifest"))
|
||||
.expect("manifest");
|
||||
let credential =
|
||||
NativeTunnelCredential::decode(oracle.ready.credential.as_bytes()).expect("credential");
|
||||
let cancellation = Cancellation::new();
|
||||
let trigger = cancellation.clone();
|
||||
let canceller = thread::spawn(move || {
|
||||
thread::sleep(Duration::from_millis(25));
|
||||
trigger.cancel();
|
||||
});
|
||||
let runtime = tokio::runtime::Runtime::new().expect("runtime");
|
||||
let started = Instant::now();
|
||||
let result = runtime.block_on(connect_with_cancellation(
|
||||
&manifest,
|
||||
&credential,
|
||||
recording_signers(
|
||||
test_key(&oracle.ready.admission_key),
|
||||
test_key(&oracle.ready.client_key),
|
||||
Arc::new(Mutex::new(Vec::new())),
|
||||
),
|
||||
"2026-08-12T00:00:00Z",
|
||||
Duration::from_secs(5),
|
||||
&cancellation,
|
||||
));
|
||||
canceller.join().expect("canceller");
|
||||
assert_eq!(result.err(), Some(CoreError::Cancelled));
|
||||
assert!(started.elapsed() < Duration::from_secs(2));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn blackholed_first_manifest_address_does_not_starve_live_second_address() {
|
||||
let oracle = Oracle::start("");
|
||||
let socket = std::net::UdpSocket::bind("127.0.0.1:0").expect("bind blackhole");
|
||||
let mut manifest: serde_json::Value =
|
||||
serde_json::from_str(&oracle.ready.manifest).expect("manifest JSON");
|
||||
let live = manifest["gateway"]["addresses"][0].clone();
|
||||
manifest["gateway"]["addresses"] = serde_json::json!([
|
||||
socket.local_addr().expect("blackhole address").to_string(),
|
||||
live,
|
||||
]);
|
||||
let manifest =
|
||||
ConnectionManifest::decode(&serde_json::to_vec(&manifest).expect("encode manifest"))
|
||||
.expect("manifest");
|
||||
let credential =
|
||||
NativeTunnelCredential::decode(oracle.ready.credential.as_bytes()).expect("credential");
|
||||
let runtime = tokio::runtime::Runtime::new().expect("runtime");
|
||||
let session = runtime
|
||||
.block_on(connect(
|
||||
&manifest,
|
||||
&credential,
|
||||
recording_signers(
|
||||
test_key(&oracle.ready.admission_key),
|
||||
test_key(&oracle.ready.client_key),
|
||||
Arc::new(Mutex::new(Vec::new())),
|
||||
),
|
||||
"2026-08-12T00:00:00Z",
|
||||
Duration::from_secs(5),
|
||||
))
|
||||
.expect("live second manifest address");
|
||||
runtime.block_on(session.close());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn multi_record_first_manifest_address_does_not_starve_live_second_address() {
|
||||
let oracle = Oracle::start("");
|
||||
let socket = std::net::UdpSocket::bind("127.0.0.1:0").expect("bind blackhole");
|
||||
let port = socket.local_addr().expect("blackhole address").port();
|
||||
let mut manifest: serde_json::Value =
|
||||
serde_json::from_str(&oracle.ready.manifest).expect("manifest JSON");
|
||||
let live = manifest["gateway"]["addresses"][0].clone();
|
||||
manifest["gateway"]["addresses"] = serde_json::json!([format!("localhost:{port}"), live]);
|
||||
let manifest =
|
||||
ConnectionManifest::decode(&serde_json::to_vec(&manifest).expect("encode manifest"))
|
||||
.expect("manifest");
|
||||
let credential =
|
||||
NativeTunnelCredential::decode(oracle.ready.credential.as_bytes()).expect("credential");
|
||||
let runtime = tokio::runtime::Runtime::new().expect("runtime");
|
||||
let session = runtime
|
||||
.block_on(connect(
|
||||
&manifest,
|
||||
&credential,
|
||||
recording_signers(
|
||||
test_key(&oracle.ready.admission_key),
|
||||
test_key(&oracle.ready.client_key),
|
||||
Arc::new(Mutex::new(Vec::new())),
|
||||
),
|
||||
"2026-08-12T00:00:00Z",
|
||||
Duration::from_secs(5),
|
||||
))
|
||||
.expect("live second address after multi-record first address");
|
||||
runtime.block_on(session.close());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn invalid_first_manifest_address_does_not_block_live_second_address() {
|
||||
let oracle = Oracle::start("");
|
||||
let mut manifest: serde_json::Value =
|
||||
serde_json::from_str(&oracle.ready.manifest).expect("manifest JSON");
|
||||
let live = manifest["gateway"]["addresses"][0].clone();
|
||||
manifest["gateway"]["addresses"] = serde_json::json!(["127.0.0.1:0", live]);
|
||||
let manifest =
|
||||
ConnectionManifest::decode(&serde_json::to_vec(&manifest).expect("encode manifest"))
|
||||
.expect("manifest");
|
||||
let credential =
|
||||
NativeTunnelCredential::decode(oracle.ready.credential.as_bytes()).expect("credential");
|
||||
let runtime = tokio::runtime::Runtime::new().expect("runtime");
|
||||
let session = runtime
|
||||
.block_on(connect(
|
||||
&manifest,
|
||||
&credential,
|
||||
recording_signers(
|
||||
test_key(&oracle.ready.admission_key),
|
||||
test_key(&oracle.ready.client_key),
|
||||
Arc::new(Mutex::new(Vec::new())),
|
||||
),
|
||||
"2026-08-12T00:00:00Z",
|
||||
Duration::from_secs(5),
|
||||
))
|
||||
.expect("live address after resolution failure");
|
||||
runtime.block_on(session.close());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn retryable_stable_error_advances_to_live_manifest_address() {
|
||||
let oracle = Oracle::start("retryable");
|
||||
let manifest = ConnectionManifest::decode(oracle.ready.manifest.as_bytes()).expect("manifest");
|
||||
let credential =
|
||||
NativeTunnelCredential::decode(oracle.ready.credential.as_bytes()).expect("credential");
|
||||
let runtime = tokio::runtime::Runtime::new().expect("runtime");
|
||||
let session = runtime
|
||||
.block_on(connect(
|
||||
&manifest,
|
||||
&credential,
|
||||
recording_signers(
|
||||
test_key(&oracle.ready.admission_key),
|
||||
test_key(&oracle.ready.client_key),
|
||||
Arc::new(Mutex::new(Vec::new())),
|
||||
),
|
||||
"2026-08-12T00:00:00Z",
|
||||
Duration::from_secs(5),
|
||||
))
|
||||
.expect("retryable draining response advanced to live address");
|
||||
runtime.block_on(session.close());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn post_admission_retry_uses_a_fresh_signed_nonce() {
|
||||
let oracle = Oracle::start("post-retryable");
|
||||
let manifest = ConnectionManifest::decode(oracle.ready.manifest.as_bytes()).expect("manifest");
|
||||
let credential =
|
||||
NativeTunnelCredential::decode(oracle.ready.credential.as_bytes()).expect("credential");
|
||||
let transcripts = Arc::new(Mutex::new(Vec::new()));
|
||||
let runtime = tokio::runtime::Runtime::new().expect("runtime");
|
||||
let session = runtime
|
||||
.block_on(connect(
|
||||
&manifest,
|
||||
&credential,
|
||||
recording_signers(
|
||||
test_key(&oracle.ready.admission_key),
|
||||
test_key(&oracle.ready.client_key),
|
||||
Arc::clone(&transcripts),
|
||||
),
|
||||
"2026-08-12T00:00:00Z",
|
||||
Duration::from_secs(5),
|
||||
))
|
||||
.expect("post-admission retry used a fresh request");
|
||||
runtime.block_on(session.close());
|
||||
let transcripts = transcripts.lock().expect("transcripts");
|
||||
assert_eq!(transcripts.len(), 2);
|
||||
assert_ne!(transcripts[0], transcripts[1]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn repeated_production_gateway_admissions_remain_bounded() {
|
||||
let runtime = tokio::runtime::Runtime::new().expect("runtime");
|
||||
for index in 0..10 {
|
||||
let oracle = Oracle::start("reusable");
|
||||
let manifest =
|
||||
ConnectionManifest::decode(oracle.ready.manifest.as_bytes()).expect("manifest");
|
||||
let credential =
|
||||
NativeTunnelCredential::decode(oracle.ready.credential.as_bytes()).expect("credential");
|
||||
let session = runtime
|
||||
.block_on(connect(
|
||||
&manifest,
|
||||
&credential,
|
||||
recording_signers(
|
||||
test_key(&oracle.ready.admission_key),
|
||||
test_key(&oracle.ready.client_key),
|
||||
Arc::new(Mutex::new(Vec::new())),
|
||||
),
|
||||
"2026-08-12T00:00:00Z",
|
||||
Duration::from_secs(5),
|
||||
))
|
||||
.unwrap_or_else(|error| panic!("stress admission {index}: {error:?}"));
|
||||
runtime.block_on(session.close());
|
||||
}
|
||||
}
|
||||
@@ -134,6 +134,19 @@ fn strict_rc5_dtos_reject_duplicate_trailing_unknown_and_provider_fields() {
|
||||
assert!(TunnelAdmissionRequest::decode(admission_provider.as_bytes()).is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn manifest_public_identity_requires_dns_sni_not_ip_or_uuid() {
|
||||
for invalid_identity in ["127.0.0.1", "::1", "550e8400-e29b-41d4-a716-446655440000"] {
|
||||
let manifest = String::from_utf8(valid_manifest().to_vec())
|
||||
.expect("fixture is UTF-8")
|
||||
.replace("gateway.test\"}", &format!("{invalid_identity}\"}}"));
|
||||
assert!(
|
||||
ConnectionManifest::decode(manifest.as_bytes()).is_err(),
|
||||
"non-DNS SNI accepted: {invalid_identity}"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn rc5_manifest_credential_and_authority_enforce_bounds_and_bindings() {
|
||||
let manifest = ConnectionManifest::decode(valid_manifest()).expect("valid manifest");
|
||||
@@ -164,6 +177,70 @@ fn rc5_manifest_credential_and_authority_enforce_bounds_and_bindings() {
|
||||
|
||||
let provider_authority = br#"{"version":"1","session_id":"session","gateway_id":"gateway","audience":"audience","reconnect_sequence":0,"expires_at":"2098-01-01T00:00:00Z","capabilities":{"transport":"quic-tls13","framing":"datagram-v2","media":"encoded","audio":"encoded","source_rate_control":"server","client_decode":["h264-opus"]},"provider_profile":"apollo"}"#;
|
||||
assert!(ClientSessionAuthority::decode(provider_authority).is_err());
|
||||
let provider_route = br#"{"version":"1","session_id":"session","gateway_id":"gateway","audience":"audience","reconnect_sequence":0,"expires_at":"2098-01-01T00:00:00Z","capabilities":{"transport":"quic-tls13","framing":"datagram-v2","media":"encoded","audio":"encoded","source_rate_control":"server","client_decode":["h264-opus"]},"stream_host":"provider.invalid"}"#;
|
||||
assert!(ClientSessionAuthority::decode(provider_route).is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn client_authority_directly_rejects_every_manifest_binding_mismatch() {
|
||||
let manifest = ConnectionManifest::decode(valid_manifest()).expect("valid manifest");
|
||||
let offered = CapabilityProfile::new(
|
||||
"quic-tls13",
|
||||
"datagram-v2",
|
||||
"encoded",
|
||||
"encoded",
|
||||
"server",
|
||||
vec!["h264-opus".to_owned()],
|
||||
)
|
||||
.expect("offered capabilities");
|
||||
let valid = serde_json::json!({
|
||||
"version": "1",
|
||||
"session_id": "session",
|
||||
"gateway_id": "gateway",
|
||||
"audience": "audience",
|
||||
"reconnect_sequence": 0,
|
||||
"expires_at": "2098-01-01T00:00:00Z",
|
||||
"capabilities": {
|
||||
"transport": "quic-tls13",
|
||||
"framing": "datagram-v2",
|
||||
"media": "encoded",
|
||||
"audio": "encoded",
|
||||
"source_rate_control": "server",
|
||||
"client_decode": ["h264-opus"],
|
||||
},
|
||||
});
|
||||
for case in [
|
||||
"session",
|
||||
"gateway",
|
||||
"audience",
|
||||
"reconnect",
|
||||
"expired",
|
||||
"beyond-grant",
|
||||
"capability",
|
||||
] {
|
||||
let mut value = valid.clone();
|
||||
match case {
|
||||
"session" => value["session_id"] = "other".into(),
|
||||
"gateway" => value["gateway_id"] = "other".into(),
|
||||
"audience" => value["audience"] = "other".into(),
|
||||
"reconnect" => value["reconnect_sequence"] = 1.into(),
|
||||
"expired" => value["expires_at"] = "2026-08-12T00:00:00Z".into(),
|
||||
"beyond-grant" => value["expires_at"] = "2100-01-01T00:00:00Z".into(),
|
||||
"capability" => {
|
||||
value["capabilities"]["client_decode"] =
|
||||
serde_json::json!(["h264-opus", "hevc-opus"]);
|
||||
}
|
||||
_ => unreachable!(),
|
||||
}
|
||||
let authority =
|
||||
ClientSessionAuthority::decode(&serde_json::to_vec(&value).expect("encode authority"))
|
||||
.expect("structurally valid authority");
|
||||
assert_eq!(
|
||||
authority.validate_binding(&manifest, &offered, "2026-08-12T00:00:00Z"),
|
||||
Err(versevdi_core::error::CoreError::AuthorityRejected),
|
||||
"binding mismatch accepted: {case}"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
||||
Reference in New Issue
Block a user