Skip to content

Commit bd892bc

Browse files
Add policy parsing
1 parent dc0f25f commit bd892bc

3 files changed

Lines changed: 365 additions & 0 deletions

File tree

Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ A Rust library for using Sigsum transparency logs.
1414
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
1515

1616
[dependencies]
17+
base16ct = "0.3.0"
1718
base64ct = { version = "1.8.0", features = ["alloc"] }
1819
ed25519-dalek = "2.2.0"
1920
sha2 = "0.10.8"

src/policy/mod.rs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,10 @@ use std::collections::HashMap;
22

33
use crate::crypto::{Hash, PublicKey};
44

5+
mod parsing;
6+
7+
pub use parsing::ParsePolicyError;
8+
59
/// A Sigsum policy.
610
///
711
/// The Sigsum policy dictates if a signed tree head is considered valid (and by extension, if a

src/policy/parsing.rs

Lines changed: 360 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,360 @@
1+
use std::iter::Enumerate;
2+
use std::str::Lines;
3+
4+
use super::{Policy, PolicyBuilder};
5+
use crate::crypto::PublicKey;
6+
7+
#[derive(Debug, thiserror::Error)]
8+
#[error("line {lineno}: {reason}")]
9+
pub struct ParsePolicyError {
10+
lineno: usize,
11+
reason: String,
12+
}
13+
14+
macro_rules! bail {
15+
($lineno:expr, $($farg:tt)*) => {
16+
return Err(ParsePolicyError{lineno: $lineno, reason: format!($($farg)*)})
17+
};
18+
}
19+
20+
type Result<T> = std::result::Result<T, ParsePolicyError>;
21+
22+
impl Policy {
23+
pub fn parse(data: &str) -> Result<Policy> {
24+
parse_policy(data)
25+
}
26+
}
27+
28+
fn parse_policy(data: &str) -> Result<Policy> {
29+
let mut builder = PolicyBuilder::new();
30+
let lines = PolicyLines::new(data);
31+
for (lineno, tokens) in lines {
32+
match tokens[..] {
33+
["log", ..] => {
34+
let nargs = tokens.len() - 1;
35+
if !(1..=2).contains(&nargs) {
36+
bail!(
37+
lineno,
38+
"invalid log rule: expected 1 or 2 arguments, got {nargs}"
39+
);
40+
}
41+
let pubkey = parse_key(tokens[1], lineno)?;
42+
let url = tokens.get(2).map(|s| (*s).into());
43+
if let Err(err) = builder.add_log(pubkey, url) {
44+
bail!(lineno, "invalid log rule: {err}")
45+
}
46+
}
47+
["witness", ..] => {
48+
let nargs = tokens.len() - 1;
49+
if !(2..=3).contains(&nargs) {
50+
bail!(
51+
lineno,
52+
"invalid witness rule: expected 2 or 3 arguments, got {nargs}"
53+
);
54+
}
55+
let name = tokens[1];
56+
let pubkey = parse_key(tokens[2], lineno)?;
57+
let url = tokens.get(3).map(|s| (*s).into());
58+
if let Err(err) = builder.add_witness(name.into(), pubkey, url) {
59+
bail!(lineno, "invalid witness rule: {err}")
60+
}
61+
}
62+
["group", ..] => {
63+
let nargs = tokens.len() - 1;
64+
if nargs < 3 {
65+
bail!(
66+
lineno,
67+
"invalid group rule: expected at least 3 arguments, got {nargs}"
68+
);
69+
}
70+
let name = tokens[1];
71+
let k = tokens[2];
72+
let members: Vec<_> = tokens[3..].iter().map(|n| (*n).into()).collect();
73+
let Some(k) = parse_k(k, members.len()) else {
74+
bail!(lineno, "invalid group rule: cannot parse {k} as an integer");
75+
};
76+
if let Err(err) = builder.add_group(name.into(), k, members) {
77+
bail!(lineno, "invalid group rule: {err}");
78+
}
79+
}
80+
["quorum", name] => {
81+
if name != "none" {
82+
if let Err(err) = builder.set_quorum(name.into()) {
83+
bail!(lineno, "invalid quorum rule: {err}");
84+
}
85+
}
86+
}
87+
["quorum", ..] => bail!(lineno, "invalid quorum rule: expected exactly one argument"),
88+
_ => todo!(),
89+
}
90+
}
91+
Ok(builder.build())
92+
}
93+
94+
fn parse_k(k: &str, n: usize) -> Option<usize> {
95+
if k == "any" {
96+
Some(1)
97+
} else if k == "all" {
98+
Some(n)
99+
} else {
100+
k.parse().ok()
101+
}
102+
}
103+
104+
fn parse_key(hex: &str, lineno: usize) -> Result<PublicKey> {
105+
if base16ct::decoded_len(hex.as_bytes()) != Ok(32) {
106+
return Err(ParsePolicyError {
107+
lineno,
108+
reason: String::from("invalid public key: wrong length"),
109+
});
110+
}
111+
let mut buf = [0; 32];
112+
match base16ct::mixed::decode(hex, &mut buf) {
113+
Err(_) => Err(ParsePolicyError {
114+
lineno,
115+
reason: "invalid public key: invalid hex encoding".to_string(),
116+
}),
117+
Ok(_) => Ok(buf.into()),
118+
}
119+
}
120+
121+
// PolicyLines is essentially a lexer for parsing policy files.
122+
//
123+
// It implements a custom iterator that handles most low-level details of parsing policy files,
124+
// splitting, numbering, filtering and tokenizing lines.
125+
struct PolicyLines<'a>(Enumerate<Lines<'a>>);
126+
127+
impl<'a> PolicyLines<'a> {
128+
fn new(input: &'a str) -> Self {
129+
Self(input.lines().enumerate())
130+
}
131+
}
132+
133+
impl<'a> Iterator for PolicyLines<'a> {
134+
// Return a pair line number + tokens
135+
type Item = (usize, Vec<&'a str>);
136+
137+
// next() is the only required method
138+
fn next(&mut self) -> Option<Self::Item> {
139+
loop {
140+
let (i, line) = self.0.next()?;
141+
let line = line.trim_ascii();
142+
if line.is_empty() || line.starts_with('#') {
143+
continue;
144+
}
145+
let tokens = line.split_ascii_whitespace().collect();
146+
return Some((i + 1, tokens));
147+
}
148+
}
149+
}
150+
151+
#[cfg(test)]
152+
mod tests {
153+
use hex_literal::hex;
154+
155+
use super::*;
156+
157+
#[test]
158+
fn policy_lines_empty() {
159+
let pls: Vec<(usize, Vec<&str>)> = PolicyLines::new("").collect();
160+
assert_eq!(pls, vec![]);
161+
}
162+
163+
#[test]
164+
fn policy_lines() {
165+
let pls: Vec<(usize, Vec<&str>)> = PolicyLines::new(
166+
"# This is a comment\n\
167+
foo bar\n\
168+
\n\
169+
# The next line uses tabs\n\
170+
foo\tbar\n\
171+
\n\
172+
# This one mixes whitespaces\n\
173+
foo \tbar\t baz \t biz\n\
174+
\n\
175+
# Heading/trailing spaces\n\
176+
\t\n\
177+
\t \n\
178+
\t# Indented comment\n\
179+
foo\t \n\
180+
\n\
181+
# Unicode space\n\
182+
foo\u{2003}bar\n\
183+
\u{2003}\n\
184+
",
185+
)
186+
.collect();
187+
assert_eq!(
188+
pls,
189+
vec![
190+
(2, vec!["foo", "bar"]),
191+
(5, vec!["foo", "bar"]),
192+
(8, vec!["foo", "bar", "baz", "biz"]),
193+
(14, vec!["foo"]),
194+
(17, vec!["foo\u{2003}bar"]),
195+
(18, vec!["\u{2003}"]),
196+
]
197+
);
198+
}
199+
200+
#[test]
201+
fn parse_policy_empty() {
202+
let expected = PolicyBuilder::new().build();
203+
let actual = parse_policy("").unwrap();
204+
assert_eq!(expected, actual);
205+
}
206+
207+
#[test]
208+
fn parse_logs() {
209+
let expected = {
210+
let mut b = PolicyBuilder::new();
211+
b.add_log(
212+
hex!("fd07e34679d68f7042f0d2d3d21e956abdd0b56b1abc3d659b2b51f8e40a113e").into(),
213+
None,
214+
)
215+
.unwrap();
216+
b.add_log(
217+
hex!("348129817da7b3cbf1f87e0d82b65bc300869e4b65ba40f0f0c23588c5279d2b").into(),
218+
Some("http://example.com".into()),
219+
)
220+
.unwrap();
221+
b.build()
222+
};
223+
let actual =
224+
parse_policy("\
225+
log fd07e34679d68f7042f0d2d3d21e956abdd0b56b1abc3d659b2b51f8e40a113e\n\
226+
log 348129817da7b3cbf1f87e0d82b65bc300869e4b65ba40f0f0c23588c5279d2b http://example.com\n\
227+
quorum none
228+
").unwrap();
229+
assert_eq!(expected, actual);
230+
}
231+
232+
#[test]
233+
fn parse_witnesses() {
234+
let expected = {
235+
let mut b = PolicyBuilder::new();
236+
b.add_witness(
237+
"WIT-1".into(),
238+
hex!("acf9f514e85ba44ebf00faf1bc36a16c42f2915c7b2728b489223fdcf3f2b6e3").into(),
239+
None,
240+
)
241+
.unwrap();
242+
b.add_witness(
243+
"WIT-2".into(),
244+
hex!("df329030f76b3616f1c50f3f8ae7ce6cf3fa92905ab7ce47bbc8be71226b65c9").into(),
245+
Some("http://example.com/witness".into()),
246+
)
247+
.unwrap();
248+
b.build()
249+
};
250+
let actual = parse_policy("\
251+
witness WIT-1 acf9f514e85ba44ebf00faf1bc36a16c42f2915c7b2728b489223fdcf3f2b6e3\n\
252+
witness WIT-2 df329030f76b3616f1c50f3f8ae7ce6cf3fa92905ab7ce47bbc8be71226b65c9 http://example.com/witness\n\
253+
quorum none\n\
254+
").unwrap();
255+
assert_eq!(expected, actual);
256+
}
257+
258+
#[test]
259+
fn parse_group() {
260+
let expected = {
261+
let mut b = PolicyBuilder::new();
262+
b.add_witness(
263+
"WIT-1".into(),
264+
hex!("acf9f514e85ba44ebf00faf1bc36a16c42f2915c7b2728b489223fdcf3f2b6e3").into(),
265+
None,
266+
)
267+
.unwrap();
268+
b.add_witness(
269+
"WIT-2".into(),
270+
hex!("df329030f76b3616f1c50f3f8ae7ce6cf3fa92905ab7ce47bbc8be71226b65c9").into(),
271+
Some("http://example.com/witness".into()),
272+
)
273+
.unwrap();
274+
b.add_group("GRP".into(), 1, vec!["WIT-1".into(), "WIT-2".into()])
275+
.unwrap();
276+
b.set_quorum("GRP".into()).unwrap();
277+
b.build()
278+
};
279+
let actual = parse_policy("\
280+
witness WIT-1 acf9f514e85ba44ebf00faf1bc36a16c42f2915c7b2728b489223fdcf3f2b6e3\n\
281+
witness WIT-2 df329030f76b3616f1c50f3f8ae7ce6cf3fa92905ab7ce47bbc8be71226b65c9 http://example.com/witness\n\
282+
group GRP 1 WIT-1 WIT-2\n\
283+
quorum GRP\n\
284+
").unwrap();
285+
assert_eq!(expected, actual);
286+
}
287+
288+
#[test]
289+
fn parse_policy_invalid_public_key() {
290+
insta::assert_snapshot!(
291+
parse_policy("log fd07e34679d68f7042f0d2d3d21e956abdd0b56b1abc3d659b2b51f8e40a11").unwrap_err(),
292+
@"line 1: invalid public key: wrong length",
293+
);
294+
insta::assert_snapshot!(
295+
parse_policy("log xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx").unwrap_err(),
296+
@"line 1: invalid public key: invalid hex encoding",
297+
);
298+
}
299+
300+
#[test]
301+
fn parse_policy_invalid_log_rule() {
302+
insta::assert_snapshot!(
303+
parse_policy("log").unwrap_err(),
304+
@"line 1: invalid log rule: expected 1 or 2 arguments, got 0",
305+
);
306+
insta::assert_snapshot!(
307+
parse_policy("log fd07e34679d68f7042f0d2d3d21e956abdd0b56b1abc3d659b2b51f8e40a113e https://log.io/ xxx").unwrap_err(),
308+
@"line 1: invalid log rule: expected 1 or 2 arguments, got 3",
309+
);
310+
}
311+
312+
#[test]
313+
fn parse_policy_invalid_witness_rule() {
314+
insta::assert_snapshot!(
315+
parse_policy("witness foo").unwrap_err(),
316+
@"line 1: invalid witness rule: expected 2 or 3 arguments, got 1",
317+
);
318+
insta::assert_snapshot!(
319+
parse_policy("witness foo fd07e34679d68f7042f0d2d3d21e956abdd0b56b1abc3d659b2b51f8e40a113e https://log.io/ xxx").unwrap_err(),
320+
@"line 1: invalid witness rule: expected 2 or 3 arguments, got 4",
321+
);
322+
}
323+
324+
#[test]
325+
fn parse_policy_invalid_group_rule() {
326+
insta::assert_snapshot!(
327+
parse_policy("group foo any").unwrap_err(),
328+
@"line 1: invalid group rule: expected at least 3 arguments, got 2",
329+
);
330+
}
331+
332+
#[test]
333+
fn parse_policy_invalid_quorum_rule() {
334+
insta::assert_snapshot!(
335+
parse_policy("quorum").unwrap_err(),
336+
@"line 1: invalid quorum rule: expected exactly one argument",
337+
);
338+
insta::assert_snapshot!(
339+
parse_policy("quorum foo bar").unwrap_err(),
340+
@"line 1: invalid quorum rule: expected exactly one argument",
341+
);
342+
insta::assert_snapshot!(
343+
parse_policy("quorum foo").unwrap_err(),
344+
@"line 1: invalid quorum rule: foo: no sutch witness",
345+
);
346+
}
347+
348+
#[test]
349+
fn parse_policy_multiple_quorum() {
350+
insta::assert_snapshot!(
351+
parse_policy("\
352+
witness WIT-1 acf9f514e85ba44ebf00faf1bc36a16c42f2915c7b2728b489223fdcf3f2b6e3\n\
353+
witness WIT-2 df329030f76b3616f1c50f3f8ae7ce6cf3fa92905ab7ce47bbc8be71226b65c9\n\
354+
quorum WIT-1\n\
355+
quorum WIT-2\n\
356+
").unwrap_err(),
357+
@"line 4: invalid quorum rule: quorum already set",
358+
);
359+
}
360+
}

0 commit comments

Comments
 (0)