Skip to content

Commit c6a8160

Browse files
committed
test(evidencectl): serve the handoff tests' Transit key from TestTransit
The production handoff tests carried their own Transit signing proxy for the same key read and prehashed sign call that platform-crypto's test_support::TestTransit serves, so the copies could drift apart. The fixture now reads the private JWK that target staging generated and starts TestTransit for transit mount key evidence-signing version 1 on the target's socket. The private proxy, its PEM helper and their imports are removed. registry-platform-crypto's test-support feature is enabled for evidencectl's dev-dependencies only. TestTransit is stricter than the removed proxy: it refuses a request without the X-Vault-Request header, as a real Transit proxy does. Signed-off-by: Jeremi Joslin <jeremi@joslin.fr>
1 parent 59cc8bc commit c6a8160

2 files changed

Lines changed: 28 additions & 271 deletions

File tree

‎crates/registry-evidencectl/Cargo.toml‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -75,4 +75,5 @@ required-features = ["schema"]
7575
registry-adapter-engine.workspace = true
7676
registry-evidence.workspace = true
7777
registry-evidence-authoring.workspace = true
78+
registry-platform-crypto = { workspace = true, features = ["test-support"] }
7879
tower.workspace = true

‎crates/registry-evidencectl/tests/production_handoff.rs‎

Lines changed: 27 additions & 271 deletions
Original file line numberDiff line numberDiff line change
@@ -15,29 +15,19 @@ use std::{
1515
fs::{self, File, OpenOptions},
1616
io::{Read as _, Write as _},
1717
net::{Ipv4Addr, TcpListener, TcpStream},
18-
os::unix::{
19-
fs::{OpenOptionsExt as _, PermissionsExt as _},
20-
net::{UnixListener, UnixStream},
21-
},
18+
os::unix::fs::{OpenOptionsExt as _, PermissionsExt as _},
2219
path::{Path, PathBuf},
2320
process::{Child, Command, Output, Stdio},
24-
sync::{
25-
atomic::{AtomicBool, Ordering},
26-
Arc, OnceLock,
27-
},
21+
sync::OnceLock,
2822
thread,
2923
time::{Duration, Instant},
3024
};
3125

32-
use base64::{
33-
engine::general_purpose::{STANDARD, URL_SAFE_NO_PAD},
34-
Engine as _,
35-
};
26+
use base64::{engine::general_purpose::URL_SAFE_NO_PAD, Engine as _};
3627
use chrono::Utc;
37-
use p256::ecdsa::{
38-
signature::{hazmat::PrehashSigner as _, Signer as _},
39-
Signature, SigningKey,
40-
};
28+
use p256::ecdsa::{signature::Signer as _, Signature, SigningKey};
29+
use registry_platform_crypto::document_signature::test_support::TestTransit;
30+
use registry_platform_crypto::PrivateJwk;
4131
use serde_json::{json, Value};
4232

4333
const TOKEN_AUDIENCE: &str = "registry-evidence-production-test";
@@ -756,23 +746,6 @@ impl Drop for DevStopGuard {
756746
}
757747
}
758748

759-
struct TransitServer {
760-
stop: Arc<AtomicBool>,
761-
socket: PathBuf,
762-
worker: Option<thread::JoinHandle<()>>,
763-
}
764-
765-
impl Drop for TransitServer {
766-
fn drop(&mut self) {
767-
self.stop.store(true, Ordering::Release);
768-
let _ = UnixStream::connect(&self.socket);
769-
if let Some(worker) = self.worker.take() {
770-
worker.join().expect("synthetic Transit server");
771-
}
772-
let _ = fs::remove_file(&self.socket);
773-
}
774-
}
775-
776749
struct SourceProxy {
777750
docker: PathBuf,
778751
container_id: String,
@@ -984,41 +957,27 @@ impl Fixture {
984957
proxy
985958
}
986959

987-
fn start_transit(&self) -> TransitServer {
988-
let socket = self.root.join("transit-proxy.sock");
989-
let listener = UnixListener::bind(&socket).expect("bind synthetic Transit socket");
990-
listener
991-
.set_nonblocking(true)
992-
.expect("synthetic Transit nonblocking listener");
993-
let stop = Arc::new(AtomicBool::new(false));
994-
let worker_stop = Arc::clone(&stop);
995-
let root = self.root.clone();
996-
let worker = thread::spawn(move || {
997-
while !worker_stop.load(Ordering::Acquire) {
998-
match listener.accept() {
999-
Ok((mut stream, _)) => {
1000-
if worker_stop.load(Ordering::Acquire) {
1001-
break;
1002-
}
1003-
// Darwin inherits O_NONBLOCK from the listener; wait for
1004-
// each complete request within the bounded read timeout.
1005-
stream
1006-
.set_nonblocking(false)
1007-
.expect("synthetic Transit blocking accepted stream");
1008-
serve_transit_request(&root, &mut stream);
1009-
}
1010-
Err(error) if error.kind() == std::io::ErrorKind::WouldBlock => {
1011-
thread::sleep(Duration::from_millis(5));
1012-
}
1013-
Err(error) => panic!("synthetic Transit accept failed: {error}"),
1014-
}
1015-
}
1016-
});
1017-
TransitServer {
1018-
stop,
1019-
socket,
1020-
worker: Some(worker),
1021-
}
960+
/// Serve the Evidence signing key the staged target binds, version 1 of
961+
/// `evidence-signing` on the `transit` mount, at the target's Transit
962+
/// socket. The key is the one target staging generated, so stage the
963+
/// target first.
964+
fn start_transit(&self) -> TestTransit {
965+
let key: PrivateJwk = serde_json::from_slice(
966+
&fs::read(
967+
self.root
968+
.join("transit-evidence-key/signing-p256-private-jwk"),
969+
)
970+
.expect("Evidence Transit fixture private JWK"),
971+
)
972+
.expect("Evidence Transit fixture private JWK parses");
973+
TestTransit::start(
974+
&self.root.join("transit-proxy.sock"),
975+
"transit",
976+
"evidence-signing",
977+
1,
978+
&key,
979+
)
980+
.expect("start the test Transit proxy")
1022981
}
1023982

1024983
fn stage_authoring_project(&self) {
@@ -1888,209 +1847,6 @@ impl Drop for Fixture {
18881847
}
18891848
}
18901849

1891-
fn serve_transit_request(root: &Path, stream: &mut UnixStream) {
1892-
stream
1893-
.set_read_timeout(Some(Duration::from_secs(2)))
1894-
.expect("synthetic Transit read timeout");
1895-
let response = read_transit_request(stream).and_then(|(method, path, body)| {
1896-
if method == "GET" {
1897-
let key_name = path
1898-
.strip_prefix("/v1/transit/keys/")
1899-
.ok_or("unsupported Transit metadata path")?;
1900-
transit_metadata(root, key_name)
1901-
} else if method == "POST" {
1902-
let key_name = path
1903-
.strip_prefix("/v1/transit/sign/")
1904-
.and_then(|path| path.strip_suffix("/sha2-256"))
1905-
.ok_or("unsupported Transit signing path")?;
1906-
transit_signature(root, key_name, &body)
1907-
} else {
1908-
Err("unsupported Transit method")
1909-
}
1910-
});
1911-
let (status, document) = match response {
1912-
Ok(document) => (200, document),
1913-
Err(message) => (400, json!({"errors": [message]})),
1914-
};
1915-
let bytes = serde_json::to_vec(&document).expect("synthetic Transit response JSON");
1916-
let reason = if status == 200 { "OK" } else { "Bad Request" };
1917-
write!(
1918-
stream,
1919-
"HTTP/1.1 {status} {reason}\r\nContent-Type: application/json\r\nContent-Length: {}\r\nConnection: close\r\n\r\n",
1920-
bytes.len()
1921-
)
1922-
.expect("synthetic Transit response headers");
1923-
stream
1924-
.write_all(&bytes)
1925-
.expect("synthetic Transit response body");
1926-
}
1927-
1928-
fn read_transit_request(
1929-
stream: &mut UnixStream,
1930-
) -> Result<(String, String, Vec<u8>), &'static str> {
1931-
let mut request = Vec::new();
1932-
let header_end = loop {
1933-
let mut chunk = [0_u8; 4096];
1934-
let read = stream
1935-
.read(&mut chunk)
1936-
.map_err(|_| "Transit request could not be read")?;
1937-
if read == 0 {
1938-
return Err("Transit request ended before its headers");
1939-
}
1940-
request.extend_from_slice(&chunk[..read]);
1941-
if request.len() > 128 * 1024 {
1942-
return Err("Transit request exceeded the fixture bound");
1943-
}
1944-
if let Some(position) = request.windows(4).position(|bytes| bytes == b"\r\n\r\n") {
1945-
break position + 4;
1946-
}
1947-
};
1948-
let headers = std::str::from_utf8(&request[..header_end])
1949-
.map_err(|_| "Transit request headers were not UTF-8")?;
1950-
let mut lines = headers.lines();
1951-
let mut request_line = lines
1952-
.next()
1953-
.ok_or("Transit request line was missing")?
1954-
.split_whitespace();
1955-
let method = request_line
1956-
.next()
1957-
.ok_or("Transit request method was missing")?
1958-
.to_owned();
1959-
let path = request_line
1960-
.next()
1961-
.ok_or("Transit request path was missing")?
1962-
.to_owned();
1963-
let content_length = lines
1964-
.filter_map(|line| line.split_once(':'))
1965-
.find(|(name, _)| name.eq_ignore_ascii_case("content-length"))
1966-
.map(|(_, value)| value.trim().parse::<usize>())
1967-
.transpose()
1968-
.map_err(|_| "Transit content length was invalid")?
1969-
.unwrap_or(0);
1970-
if content_length > 64 * 1024 {
1971-
return Err("Transit request body exceeded the fixture bound");
1972-
}
1973-
while request.len() < header_end + content_length {
1974-
let mut chunk = [0_u8; 4096];
1975-
let read = stream
1976-
.read(&mut chunk)
1977-
.map_err(|_| "Transit request body could not be read")?;
1978-
if read == 0 {
1979-
return Err("Transit request ended before its body");
1980-
}
1981-
request.extend_from_slice(&chunk[..read]);
1982-
}
1983-
Ok((
1984-
method,
1985-
path,
1986-
request[header_end..header_end + content_length].to_vec(),
1987-
))
1988-
}
1989-
1990-
fn transit_metadata(root: &Path, key_name: &str) -> Result<Value, &'static str> {
1991-
let private = transit_private_jwk(root, key_name)?;
1992-
let public_key = transit_public_pem(&private)?;
1993-
Ok(json!({
1994-
"data": {
1995-
"type": "ecdsa-p256",
1996-
"derived": false,
1997-
"exportable": false,
1998-
"allow_plaintext_backup": false,
1999-
"supports_signing": true,
2000-
"latest_version": 1,
2001-
"min_encryption_version": 1,
2002-
"keys": {"1": {"public_key": public_key}}
2003-
}
2004-
}))
2005-
}
2006-
2007-
fn transit_signature(root: &Path, key_name: &str, body: &[u8]) -> Result<Value, &'static str> {
2008-
let private = transit_private_jwk(root, key_name)?;
2009-
let request: Value =
2010-
serde_json::from_slice(body).map_err(|_| "Transit signing JSON invalid")?;
2011-
if request["key_version"] != "1"
2012-
|| request["marshaling_algorithm"] != "jws"
2013-
|| request["prehashed"] != true
2014-
{
2015-
return Err("Transit signing contract invalid");
2016-
}
2017-
let digest = request["input"]
2018-
.as_str()
2019-
.ok_or("Transit signing input missing")
2020-
.and_then(|input| {
2021-
STANDARD
2022-
.decode(input)
2023-
.map_err(|_| "Transit signing input invalid")
2024-
})?;
2025-
if digest.len() != 32 {
2026-
return Err("Transit signing digest invalid");
2027-
}
2028-
let secret = private["d"]
2029-
.as_str()
2030-
.ok_or("Transit private JWK scalar missing")
2031-
.and_then(|value| {
2032-
URL_SAFE_NO_PAD
2033-
.decode(value)
2034-
.map_err(|_| "Transit private JWK scalar invalid")
2035-
})?;
2036-
let key = SigningKey::from_slice(&secret).map_err(|_| "Transit private JWK invalid")?;
2037-
let signature: Signature = key
2038-
.sign_prehash(&digest)
2039-
.map_err(|_| "Transit signing failed")?;
2040-
Ok(json!({
2041-
"data": {
2042-
"signature": format!("vault:v1:{}", URL_SAFE_NO_PAD.encode(signature.to_bytes()))
2043-
}
2044-
}))
2045-
}
2046-
2047-
fn transit_private_jwk(root: &Path, key_name: &str) -> Result<Value, &'static str> {
2048-
let path = match key_name {
2049-
"evidence-signing" => root.join("transit-evidence-key/signing-p256-private-jwk"),
2050-
_ => return Err("unknown Transit key"),
2051-
};
2052-
let bytes = fs::read(path).map_err(|_| "Transit fixture key unavailable")?;
2053-
serde_json::from_slice(&bytes).map_err(|_| "Transit fixture key invalid")
2054-
}
2055-
2056-
fn transit_public_pem(private: &Value) -> Result<String, &'static str> {
2057-
let x = private["x"]
2058-
.as_str()
2059-
.ok_or("Transit public JWK x missing")
2060-
.and_then(|value| {
2061-
URL_SAFE_NO_PAD
2062-
.decode(value)
2063-
.map_err(|_| "Transit public JWK x invalid")
2064-
})?;
2065-
let y = private["y"]
2066-
.as_str()
2067-
.ok_or("Transit public JWK y missing")
2068-
.and_then(|value| {
2069-
URL_SAFE_NO_PAD
2070-
.decode(value)
2071-
.map_err(|_| "Transit public JWK y invalid")
2072-
})?;
2073-
if x.len() != 32 || y.len() != 32 {
2074-
return Err("Transit public JWK coordinate invalid");
2075-
}
2076-
let mut spki = vec![
2077-
0x30, 0x59, 0x30, 0x13, 0x06, 0x07, 0x2a, 0x86, 0x48, 0xce, 0x3d, 0x02, 0x01, 0x06, 0x08,
2078-
0x2a, 0x86, 0x48, 0xce, 0x3d, 0x03, 0x01, 0x07, 0x03, 0x42, 0x00, 0x04,
2079-
];
2080-
spki.extend_from_slice(&x);
2081-
spki.extend_from_slice(&y);
2082-
let encoded = STANDARD.encode(spki);
2083-
let body = encoded
2084-
.as_bytes()
2085-
.chunks(64)
2086-
.map(|line| std::str::from_utf8(line).expect("base64 is ASCII"))
2087-
.collect::<Vec<_>>()
2088-
.join("\n");
2089-
Ok(format!(
2090-
"-----BEGIN PUBLIC KEY-----\n{body}\n-----END PUBLIC KEY-----\n"
2091-
))
2092-
}
2093-
20941850
fn read_json_document(path: &Path) -> Value {
20951851
serde_norway::from_slice(
20961852
&fs::read(path).unwrap_or_else(|error| panic!("read {}: {error}", path.display())),

0 commit comments

Comments
 (0)