859 lines
37 KiB
Rust
859 lines
37 KiB
Rust
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
|
|
releaseFails 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 {
|
|
if a.releaseFails { return errors.New("release failed") }
|
|
return nil
|
|
}
|
|
|
|
type provider struct { failStart bool }
|
|
type session struct { state protocol.ProviderState; video chan gateway.ProviderMedia; audio chan gateway.ProviderMedia; events chan gateway.ProviderEvent }
|
|
func (p provider) Start(_ context.Context, request gateway.LaunchRequest) (gateway.ProviderSession, error) {
|
|
if p.failStart { return nil, context.DeadlineExceeded }
|
|
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",releaseFails:mode == "cleanup-release-failure"}
|
|
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{failStart:mode == "provider-start-lost-response" || mode == "cleanup-release-failure"}}); 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" || mode == "provider-start-lost-response" || mode == "cleanup-release-failure" {
|
|
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_eq!(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 tls_signer_finite_deadline_overrun_is_cancelled_not_tls() {
|
|
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 cancellation = Cancellation::new();
|
|
let trigger = cancellation.clone();
|
|
let canceller = thread::spawn(move || {
|
|
thread::sleep(Duration::from_millis(25));
|
|
trigger.cancel();
|
|
});
|
|
let result =
|
|
tokio::runtime::Runtime::new()
|
|
.expect("runtime")
|
|
.block_on(connect_with_cancellation(
|
|
&manifest,
|
|
&credential,
|
|
Signers::new(
|
|
AdmissionSigner::new(signer(admission_key)),
|
|
TlsEd25519Signer::new(move |message| {
|
|
thread::sleep(Duration::from_millis(100));
|
|
signer(Arc::clone(&tls_key))(message)
|
|
}),
|
|
),
|
|
"2026-08-12T00:00:00Z",
|
|
Duration::from_secs(5),
|
|
&cancellation,
|
|
));
|
|
canceller.join().expect("canceller");
|
|
assert_eq!(result.err(), Some(CoreError::Cancelled));
|
|
}
|
|
|
|
#[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 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("retryable draining response advanced to live address");
|
|
runtime.block_on(session.close());
|
|
let transcripts = transcripts.lock().expect("transcripts");
|
|
assert_eq!(transcripts.len(), 2);
|
|
assert_ne!(transcripts[0], transcripts[1]);
|
|
}
|
|
|
|
#[test]
|
|
fn post_admission_failure_is_terminal_and_does_not_mutate_twice() {
|
|
for mode in [
|
|
"post-retryable",
|
|
"provider-start-lost-response",
|
|
"cleanup-release-failure",
|
|
] {
|
|
let oracle = Oracle::start(mode);
|
|
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 result = 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),
|
|
));
|
|
assert!(result.is_err(), "{mode} was retried to success");
|
|
let transcripts = transcripts.lock().expect("transcripts");
|
|
assert_eq!(
|
|
transcripts.len(),
|
|
1,
|
|
"{mode} attempted admission mutation twice"
|
|
);
|
|
}
|
|
}
|
|
|
|
#[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());
|
|
}
|
|
}
|