1- use base64:: { engine:: general_purpose, Engine as _} ;
1+ use base64:: { alphabet , engine , engine:: general_purpose, Engine as _} ;
22use clap:: { Args , Parser , Subcommand } ;
33use glome:: PrivateKey ;
44use 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 ) ]
3247struct 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+
109166fn 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