Skip to content

Commit b0b9cb9

Browse files
committed
rust: implement glome verify subcommand
1 parent 186a791 commit b0b9cb9

File tree

1 file changed

+127
-10
lines changed

1 file changed

+127
-10
lines changed

rust/src/cli/bin.rs

Lines changed: 127 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
use base64::{engine::general_purpose, Engine as _};
1+
use base64::{alphabet, engine, engine::general_purpose, Engine as _};
22
use clap::{Args, Parser, Subcommand};
33
use glome::PrivateKey;
44
use std::convert::TryInto;
@@ -28,6 +28,21 @@ struct TagArgs {
2828
counter: Option<u8>,
2929
}
3030

31+
#[derive(Args)]
32+
struct VerifyArgs {
33+
/// Path to secret key
34+
#[arg(short, long, value_name = "FILE")]
35+
key: PathBuf,
36+
/// Path to peer's public key
37+
#[arg(short, long, value_name = "FILE")]
38+
peer: PathBuf,
39+
/// Message counter index
40+
#[arg(short, long, value_name = "n")]
41+
counter: Option<u8>,
42+
/// Tag to verify
43+
tag: String,
44+
}
45+
3146
#[derive(Args)]
3247
struct LoginArgs {
3348
/// Path to secret key
@@ -45,6 +60,8 @@ enum Glome {
4560
Pubkey,
4661
/// Tag a message read from stdin
4762
Tag(TagArgs),
63+
/// Verify a message tag
64+
Verify(VerifyArgs),
4865
/// Generate a tag for a GLOME-Login challenge
4966
Login(LoginArgs),
5067
}
@@ -106,6 +123,46 @@ fn gentag(args: &TagArgs, stdin: &mut dyn io::Read, stdout: &mut dyn io::Write)
106123
Ok(stdout.write_all(encoded.as_bytes())?)
107124
}
108125

126+
fn verify(args: &VerifyArgs, stdin: &mut dyn io::Read) -> CommandResult {
127+
let ours: StaticSecret = read_key(&args.key)?.into();
128+
let theirs: PublicKey = read_pub(&args.peer)?.into();
129+
let ctr = args.counter.unwrap_or_default();
130+
let mut msg = Vec::new();
131+
stdin.read_to_end(&mut msg)?;
132+
133+
// We want to allow truncated tags, but not all truncations are valid
134+
// base64. A single encoded byte only holds 6 bits and can't be decoded
135+
// into a byte, so we need to ignore it by stripping it off.
136+
let mut tag_b64 = args.tag.clone();
137+
if tag_b64.len() % 4 == 1 {
138+
tag_b64.truncate(tag_b64.len() - 1);
139+
}
140+
141+
// Ensure that we're comparing at least one byte.
142+
if tag_b64.is_empty() {
143+
return Err("tag too short".into());
144+
}
145+
146+
// Truncation can cause trailing bits and missing padding if the truncated
147+
// length is not a multiple of 4. Make sure that the base64 engine can deal
148+
// with that.
149+
let permissive_config = engine::GeneralPurposeConfig::new()
150+
.with_decode_allow_trailing_bits(true)
151+
.with_decode_padding_mode(engine::DecodePaddingMode::Indifferent);
152+
let permissive_engine = engine::GeneralPurpose::new(&alphabet::URL_SAFE, permissive_config);
153+
154+
if !glome::verify(
155+
&ours,
156+
&theirs,
157+
ctr,
158+
&msg,
159+
&permissive_engine.decode(tag_b64)?,
160+
) {
161+
return Err("tags did not match".into());
162+
}
163+
Ok(())
164+
}
165+
109166
fn login(args: &LoginArgs, stdout: &mut dyn io::Write) -> CommandResult {
110167
let ours: StaticSecret = read_key(&args.key)?.into();
111168

@@ -160,6 +217,7 @@ fn main() -> CommandResult {
160217
Glome::Genkey => genkey(&mut io::stdout()),
161218
Glome::Pubkey => pubkey(&mut io::stdin(), &mut io::stdout()),
162219
Glome::Tag(tag_args) => gentag(tag_args, &mut io::stdin(), &mut io::stdout()),
220+
Glome::Verify(verify_args) => verify(verify_args, &mut io::stdin()),
163221
Glome::Login(login_args) => login(login_args, &mut io::stdout()),
164222
}
165223
}
@@ -272,18 +330,21 @@ mod tests {
272330
temp_file
273331
}
274332

333+
fn login_message(tc: &TestVector) -> Vec<u8> {
334+
let host = if tc.host_id_type.is_empty() {
335+
&tc.host_id
336+
} else {
337+
&format!("{}:{}", tc.host_id_type, tc.host_id)
338+
};
339+
// Some test messages contain slashes, but we don't want to add a dependency for URL
340+
// escaping, so we just replace the one character that occurs in the test vectors.
341+
format!("{}/{}", host, tc.action.replace("/", "%2F")).into_bytes()
342+
}
343+
275344
#[test]
276345
fn test_tag() {
277346
for tc in test_vectors() {
278-
let host = if tc.host_id_type.is_empty() {
279-
tc.host_id
280-
} else {
281-
format!("{}:{}", tc.host_id_type, tc.host_id)
282-
};
283-
// Some test messages contain slashes, but we don't want to add a dependency for URL
284-
// escaping, so we just replace the one character that occurs in the test vectors.
285-
let message = format!("{}/{}", host, tc.action.replace("/", "%2F"));
286-
let mut stdin = io::Cursor::new(message.into_bytes());
347+
let mut stdin = io::Cursor::new(login_message(&tc));
287348
let mut stdout = io::Cursor::new(Vec::new());
288349
let key_file = temp_file(&tc.bob.private);
289350
let peer_file = temp_file(tc.alice.public_cli.as_bytes());
@@ -299,6 +360,62 @@ mod tests {
299360
}
300361
}
301362

363+
#[test]
364+
fn test_verify() {
365+
for tc in test_vectors() {
366+
let key_file = temp_file(&tc.alice.private);
367+
let peer_file = temp_file(tc.bob.public_cli.as_bytes());
368+
369+
for n in 2..=tc.tag.len() {
370+
let mut stdin = io::Cursor::new(login_message(&tc));
371+
let mut tag = tc.tag.clone();
372+
tag.truncate(n);
373+
let args = VerifyArgs {
374+
key: key_file.path().to_path_buf(),
375+
peer: peer_file.path().to_path_buf(),
376+
counter: None,
377+
tag,
378+
};
379+
verify(&args, &mut stdin)
380+
.map_err(|e| format!("test case {}: tag length {}: {}", tc.name, n, e))
381+
.expect("should not fail")
382+
}
383+
384+
{
385+
let mut stdin = io::Cursor::new(login_message(&tc));
386+
let args = VerifyArgs {
387+
key: key_file.path().to_path_buf(),
388+
peer: peer_file.path().to_path_buf(),
389+
counter: None,
390+
tag: "MDEyMzQ1Njc4".to_owned(),
391+
};
392+
assert!(
393+
verify(&args, &mut stdin).is_err(),
394+
"test case {}: verify should fail for bad tag",
395+
tc.name
396+
);
397+
}
398+
399+
for n in 0..2 {
400+
let mut stdin = io::Cursor::new(login_message(&tc));
401+
let mut tag = tc.tag.clone();
402+
tag.truncate(n);
403+
let args = VerifyArgs {
404+
key: key_file.path().to_path_buf(),
405+
peer: peer_file.path().to_path_buf(),
406+
counter: None,
407+
tag,
408+
};
409+
assert!(
410+
verify(&args, &mut stdin).is_err(),
411+
"test case {}: verify should fail for tag length {}",
412+
tc.name,
413+
n
414+
);
415+
}
416+
}
417+
}
418+
302419
#[test]
303420
fn test_login() {
304421
for tc in test_vectors() {

0 commit comments

Comments
 (0)