Skip to content

Commit 01a3f6f

Browse files
Add policy parsing
1 parent f520429 commit 01a3f6f

3 files changed

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

0 commit comments

Comments
 (0)