use openmoat_core::{Policy, Verdict};
use zeroize::Zeroizing;
use super::{Broker, BrokerError, Carried, RULE_SECRET, Watch};
const VALUE: &str = "fake-0123456789abcdef";
fn broker(value: &str) -> Result<Broker, BrokerError> {
broker_with(value, false)
}
fn broker_with(value: &str, plain_http: bool) -> Result<Broker, BrokerError> {
let policy = Policy::parse(&format!(
"version: 1\nsecrets:\n - id: gh\n host: api.github.com\n header: Authorization\n \
source: {{ env: FAKE_TOKEN }}\n plain_http: {plain_http}\n",
))
.unwrap();
let secrets = policy
.secrets
.into_iter()
.map(|s| (s, Zeroizing::new(value.to_owned())))
.collect();
Broker::new(secrets)
}
#[test]
fn values_that_could_break_a_header_are_refused() {
assert!(matches!(broker(""), Err(BrokerError::Empty(id)) if id == "gh"));
for bad in ["a\r\nX-Evil: 1", "a\nb", "a\0b"] {
let err = broker(bad).unwrap_err();
assert!(matches!(err, BrokerError::Control(_)), "{bad:?}");
assert!(!err.to_string().contains(bad), "{err}");
}
}
#[test]
fn the_owner_may_receive_both_forms_and_no_other_host_may() {
let b = broker(VALUE).unwrap();
let placeholder = b"Authorization: Bearer moat-secret:gh:placeholder";
let value = format!("POST /x HTTP/1.1\r\n\r\ntoken={VALUE}");
assert!(b.leak("api.github.com", placeholder).is_none());
assert!(b.leak("api.github.com", value.as_bytes()).is_none());
let leak = b.leak("evil.example", placeholder).unwrap();
assert_eq!(
(leak.id, leak.owner, leak.carried),
("gh", "api.github.com", Carried::Placeholder)
);
let leak = b.leak("github.com", value.as_bytes()).unwrap();
assert_eq!(leak.carried, Carried::Value);
let decision = leak.decision("github.com");
assert_eq!(decision.verdict, Verdict::Deny);
assert_eq!(decision.rules, [RULE_SECRET]);
assert!(!decision.reasons.join(" ").contains(VALUE), "{decision:?}");
assert!(b.leak("evil.example", b"GET / HTTP/1.1\r\n\r\n").is_none());
}
#[test]
fn a_secret_split_across_chunks_is_found() {
let b = broker(VALUE).unwrap();
let stream = format!("{}{VALUE}{}", "x".repeat(5000), "y".repeat(100));
for cut in 1..VALUE.len() {
let at = 5000 + cut;
let mut watch = Watch::new(&b, "evil.example");
assert!(watch.next(&stream.as_bytes()[..at]).is_none(), "{cut}");
assert!(watch.next(&stream.as_bytes()[at..]).is_some(), "{cut}");
}
let mut owner = Watch::new(&b, "api.github.com");
assert!(owner.next(stream.as_bytes()).is_none());
}
#[test]
fn nothing_brokered_finds_nothing() {
let b = Broker::default();
assert!(
Watch::new(&b, "evil.example")
.next(VALUE.as_bytes())
.is_none()
);
}
#[test]
fn debug_names_ids_and_hosts_never_values() {
let shown = format!("{:?}", broker(VALUE).unwrap());
assert!(
shown.contains("gh") && shown.contains("api.github.com"),
"{shown}"
);
assert!(!shown.contains(VALUE), "{shown}");
}
#[test]
fn the_owner_head_gets_the_value_in_place_of_the_placeholder() {
let b = broker_with(VALUE, true).unwrap();
let head = b"GET /moat-secret:gh:placeholder HTTP/1.1\r\nHost: api.github.com\r\n\
authorization: Bearer moat-secret:gh:placeholder\r\n\
X-Other: moat-secret:gh:placeholder\r\nConnection: close\r\n\r\n";
let out = b.inject("api.github.com", head).unwrap();
assert_eq!(
String::from_utf8(out.to_vec()).unwrap(),
format!(
"GET /moat-secret:gh:placeholder HTTP/1.1\r\nHost: api.github.com\r\n\
authorization: Bearer {VALUE}\r\nX-Other: moat-secret:gh:placeholder\r\n\
Connection: close\r\n\r\n"
)
);
}
#[test]
fn a_missing_header_is_added_and_other_hosts_get_nothing() {
let b = broker_with(VALUE, true).unwrap();
let head = b"GET / HTTP/1.1\r\nHost: api.github.com\r\nConnection: close\r\n\r\n";
let out = b.inject("api.github.com", head).unwrap();
let added = format!("Connection: close\r\nAuthorization: {VALUE}\r\n\r\n");
assert!(out.ends_with(added.as_bytes()));
assert!(b.inject("github.com", head).is_none());
assert!(Broker::default().inject("api.github.com", head).is_none());
assert!(b.withheld("api.github.com").is_empty());
}
#[test]
fn without_plain_http_the_owner_head_is_left_alone() {
let b = broker(VALUE).unwrap();
let head = b"GET / HTTP/1.1\r\nAuthorization: moat-secret:gh:placeholder\r\n\r\n";
assert!(b.inject("api.github.com", head).is_none());
assert_eq!(b.withheld("api.github.com"), ["gh"]);
assert!(b.withheld("github.com").is_empty());
}
#[test]
fn a_secret_header_without_the_placeholder_is_replaced_by_the_secret() {
let b = broker_with(VALUE, true).unwrap();
let head = b"GET / HTTP/1.1\r\nAuthorization: Bearer chosen-by-client\r\n\
authorization: Bearer moat-secret:gh:placeholder\r\nX-Other: 1\r\n\r\n";
let out = String::from_utf8(b.inject("api.github.com", head).unwrap().to_vec()).unwrap();
assert_eq!(
out,
format!("GET / HTTP/1.1\r\nauthorization: Bearer {VALUE}\r\nX-Other: 1\r\n\r\n")
);
let head = b"GET / HTTP/1.1\r\nAUTHORIZATION: chosen-by-client\r\n\r\n";
let out = String::from_utf8(b.inject("api.github.com", head).unwrap().to_vec()).unwrap();
assert_eq!(
out,
format!("GET / HTTP/1.1\r\nAuthorization: {VALUE}\r\n\r\n")
);
}