Skip to content

Commit fd2693f

Browse files
committed
Add CLI tool to signature crate to check Balrog results
1 parent 268c160 commit fd2693f

2 files changed

Lines changed: 137 additions & 2 deletions

File tree

signature/Cargo.toml

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
[package]
22
name = "signature"
33
version = "0.2.0"
4-
edition = "2021"
4+
edition = "2024"
55

66
[dependencies]
77
asn1-rs = { version = "0.7.2", features=["datetime"] }
@@ -13,7 +13,14 @@ hex = "0.4"
1313
ring = "0.17.14"
1414
x509-parser = { version = "0.18.1", features = ["verify", "validate"] }
1515

16+
[dev-dependencies]
17+
clap = { version = "4.6", features=["derive"] }
18+
ureq = "3.3.0"
19+
20+
[[example]]
21+
name = "balrog-cli"
22+
1623
[lib]
1724
name = "signature"
1825
path = "src/lib.rs"
19-
crate-type = ["staticlib"]
26+
crate-type = ["staticlib", "rlib"]

signature/examples/balrog-cli.rs

Lines changed: 128 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,128 @@
1+
use clap::Parser;
2+
use ureq;
3+
use std::{io, process};
4+
use std::io::Write;
5+
use x509_parser::time::ASN1Time;
6+
7+
use signature::{Balrog, BalrogError, parse_pem_chain};
8+
9+
const PROD_ROOT_HASH: &str = "97e8ba9cf12fb3de53cc42a4e6577ed64df493c247b414fea036818d3823560e";
10+
11+
/// Program arguments
12+
#[derive(Parser, Debug)]
13+
#[command(version, about, long_about = None)]
14+
struct Args {
15+
/// Balrog update URL
16+
url: String,
17+
18+
/// Root certificate hash
19+
#[arg(short, long, default_value_t = PROD_ROOT_HASH.to_string())]
20+
root: String,
21+
}
22+
23+
struct BalrogData {
24+
x5u: String,
25+
signature: String,
26+
payload: Vec<u8>,
27+
chain: Vec<u8>,
28+
}
29+
30+
fn fetch(args: &Args) -> Result<BalrogData, ureq::Error> {
31+
// Step 1: Fetch the Balrog update details
32+
let mut response = ureq::get(args.url.clone()).call()?;
33+
let body = response.body_mut().read_to_vec()?;
34+
let signature = match response.headers().get("Content-Signature") {
35+
Some(x) => x.to_str().unwrap(),
36+
None => return Err(ureq::Error::StatusCode(404)),
37+
};
38+
39+
// We should be able to split the content signature on semicolons to get name=value pairs.
40+
let mut x5u = String::new();
41+
let mut sigdata = String::new();
42+
for entry in signature.split(";") {
43+
let Some((name, value)) = entry.split_once("=") else {
44+
continue;
45+
};
46+
let name = name.trim();
47+
let value = value.trim();
48+
if name == "x5u" {
49+
x5u = value.to_string();
50+
}
51+
if name == "p384ecdsa" {
52+
sigdata = value.to_string();
53+
}
54+
}
55+
56+
// Step 2: Fetch the certificate chain.
57+
let chain = ureq::get(x5u.clone())
58+
.call()?
59+
.body_mut()
60+
.read_to_vec()?;
61+
62+
Ok(BalrogData{
63+
x5u: String::from(x5u),
64+
payload: body.clone(),
65+
chain: chain.clone(),
66+
signature: String::from(sigdata),
67+
})
68+
}
69+
70+
fn run(args: &Args, data: &BalrogData) -> Result<(), BalrogError> {
71+
let chain = parse_pem_chain(data.chain.as_slice())?;
72+
let balrog = Balrog::new(&chain)?;
73+
74+
// Print some details about the certificate chain.
75+
if let Some(leaf) = balrog.chain.first() {
76+
for cn in leaf.subject().iter_common_name() {
77+
let name = cn.as_str()?;
78+
eprintln!("Leaf hostname: {}", name);
79+
}
80+
81+
if let Some(ext) = leaf.subject_alternative_name()? {
82+
for san in ext.value.general_names.iter() {
83+
eprintln!("Leaf alternative name: {}", san);
84+
}
85+
}
86+
}
87+
88+
//let roothash = hex::decode(args.root.as_str()).unwrap();
89+
90+
balrog.verify(
91+
data.payload.as_slice(),
92+
data.signature.as_str(),
93+
ASN1Time::now().timestamp(),
94+
"aus.content-signature.mozilla.org",
95+
)
96+
}
97+
98+
fn main() {
99+
let args = Args::parse();
100+
101+
// Fetch the Balrog update details
102+
let data = match fetch(&args) {
103+
Ok(x) => {
104+
eprintln!("Balrog fetch successful");
105+
eprintln!("Chain: {}", x.x5u);
106+
eprintln!("Signature: {}", x.signature);
107+
eprintln!("");
108+
x
109+
},
110+
Err(e) => {
111+
eprintln!("Balrog fetch failed: {}", e);
112+
process::exit(1);
113+
}
114+
};
115+
116+
// Run the cryptographic validation
117+
let result = run(&args, &data);
118+
119+
// Write the payload data to stdout.
120+
let _ = io::stdout().write_all(data.payload.as_slice());
121+
122+
if result.is_ok() {
123+
eprintln!("Verification successful");
124+
} else {
125+
eprintln!("Verification failed: {}", result.unwrap_err());
126+
process::exit(1);
127+
}
128+
}

0 commit comments

Comments
 (0)