1
1
//! Sign commit data.
2
2
3
- use ssh_key:: { HashAlg , LineEnding , PrivateKey } ;
4
3
use std:: path:: PathBuf ;
5
4
6
5
/// Error type for [`SignBuilder`], used to create [`Sign`]'s
@@ -72,11 +71,11 @@ pub trait Sign {
72
71
73
72
/// only available in `#[cfg(test)]` helping to diagnose issues
74
73
#[ cfg( test) ]
75
- fn program ( & self ) -> & String ;
74
+ fn program ( & self ) -> String ;
76
75
77
76
/// only available in `#[cfg(test)]` helping to diagnose issues
78
77
#[ cfg( test) ]
79
- fn signing_key ( & self ) -> & String ;
78
+ fn signing_key ( & self ) -> String ;
80
79
}
81
80
82
81
/// A builder to facilitate the creation of a signing method ([`Sign`]) by examining the git configuration.
@@ -156,35 +155,52 @@ impl SignBuilder {
156
155
String :: from ( "x509" ) ,
157
156
) ) ,
158
157
"ssh" => {
159
- let ssh_signer = config
158
+ let program = config
159
+ . get_string ( "gpg.ssh.program" )
160
+ . unwrap_or_else ( |_| "ssh-keygen" . to_string ( ) ) ;
161
+
162
+ let signing_key = config
160
163
. get_string ( "user.signingKey" )
161
- . ok ( )
162
- . and_then ( |key_path| {
163
- key_path. strip_prefix ( '~' ) . map_or_else (
164
- || Some ( PathBuf :: from ( & key_path) ) ,
165
- |ssh_key_path| {
166
- dirs:: home_dir ( ) . map ( |home| {
167
- home. join (
168
- ssh_key_path
169
- . strip_prefix ( '/' )
170
- . unwrap_or ( ssh_key_path) ,
171
- )
172
- } )
173
- } ,
164
+ . map_err ( |err| {
165
+ SignBuilderError :: SSHSigningKey (
166
+ err. to_string ( ) ,
174
167
)
175
168
} )
176
- . ok_or_else ( | | {
177
- SignBuilderError :: SSHSigningKey ( String :: from (
178
- "ssh key setting absent" ,
179
- ) )
180
- } )
181
- . and_then ( SSHSign :: new ) ? ;
182
- let signer : Box < dyn Sign > = Box :: new ( ssh_signer ) ;
183
- Ok ( signer )
169
+ . and_then ( |signing_key | {
170
+ Self :: signing_key_into_path ( & signing_key )
171
+ } ) ? ;
172
+
173
+ Ok ( Box :: new ( SSHSign {
174
+ program ,
175
+ signing_key ,
176
+ } ) )
184
177
}
185
178
_ => Err ( SignBuilderError :: InvalidFormat ( format) ) ,
186
179
}
187
180
}
181
+
182
+ fn signing_key_into_path (
183
+ signing_key : & str ,
184
+ ) -> Result < PathBuf , SignBuilderError > {
185
+ let key_path = PathBuf :: from ( signing_key) ;
186
+ if signing_key. starts_with ( "ssh-" ) {
187
+ use std:: io:: Write ;
188
+ use tempfile:: NamedTempFile ;
189
+ let mut temp_file =
190
+ NamedTempFile :: new ( ) . map_err ( |err| {
191
+ SignBuilderError :: SSHSigningKey ( err. to_string ( ) )
192
+ } ) ?;
193
+ writeln ! ( temp_file, "{signing_key}" ) . map_err ( |err| {
194
+ SignBuilderError :: SSHSigningKey ( err. to_string ( ) )
195
+ } ) ?;
196
+ let temp_file = temp_file. keep ( ) . map_err ( |err| {
197
+ SignBuilderError :: SSHSigningKey ( err. to_string ( ) )
198
+ } ) ?;
199
+ Ok ( temp_file. 1 )
200
+ } else {
201
+ Ok ( key_path)
202
+ }
203
+ }
188
204
}
189
205
190
206
/// Sign commit data using `OpenPGP`
@@ -193,16 +209,6 @@ pub struct GPGSign {
193
209
signing_key : String ,
194
210
}
195
211
196
- impl GPGSign {
197
- /// Create new [`GPGSign`] using given program and signing key.
198
- pub fn new ( program : & str , signing_key : & str ) -> Self {
199
- Self {
200
- program : program. to_string ( ) ,
201
- signing_key : signing_key. to_string ( ) ,
202
- }
203
- }
204
- }
205
-
206
212
impl Sign for GPGSign {
207
213
fn sign (
208
214
& self ,
@@ -261,79 +267,99 @@ impl Sign for GPGSign {
261
267
}
262
268
263
269
#[ cfg( test) ]
264
- fn program ( & self ) -> & String {
265
- & self . program
270
+ fn program ( & self ) -> String {
271
+ self . program . clone ( )
266
272
}
267
273
268
274
#[ cfg( test) ]
269
- fn signing_key ( & self ) -> & String {
270
- & self . signing_key
275
+ fn signing_key ( & self ) -> String {
276
+ self . signing_key . clone ( )
271
277
}
272
278
}
273
279
274
- /// Sign commit data using `SSHDiskKeySign `
280
+ /// Sign commit data using `SSHSign `
275
281
pub struct SSHSign {
276
- #[ cfg( test) ]
277
282
program : String ,
278
- #[ cfg( test) ]
279
- key_path : String ,
280
- secret_key : PrivateKey ,
281
- }
282
-
283
- impl SSHSign {
284
- /// Create new [`SSHDiskKeySign`] for sign.
285
- pub fn new ( mut key : PathBuf ) -> Result < Self , SignBuilderError > {
286
- key. set_extension ( "" ) ;
287
- if key. is_file ( ) {
288
- #[ cfg( test) ]
289
- let key_path = format ! ( "{}" , & key. display( ) ) ;
290
- std:: fs:: read ( key)
291
- . ok ( )
292
- . and_then ( |bytes| {
293
- PrivateKey :: from_openssh ( bytes) . ok ( )
294
- } )
295
- . map ( |secret_key| Self {
296
- #[ cfg( test) ]
297
- program : "ssh" . to_string ( ) ,
298
- #[ cfg( test) ]
299
- key_path,
300
- secret_key,
301
- } )
302
- . ok_or_else ( || {
303
- SignBuilderError :: SSHSigningKey ( String :: from (
304
- "Fail to read the private key for sign." ,
305
- ) )
306
- } )
307
- } else {
308
- Err ( SignBuilderError :: SSHSigningKey (
309
- String :: from ( "Currently, we only support a pair of ssh key in disk." ) ,
310
- ) )
311
- }
312
- }
283
+ signing_key : PathBuf ,
313
284
}
314
285
315
286
impl Sign for SSHSign {
316
287
fn sign (
317
288
& self ,
318
289
commit : & [ u8 ] ,
319
290
) -> Result < ( String , Option < String > ) , SignError > {
320
- let sig = self
321
- . secret_key
322
- . sign ( "git" , HashAlg :: Sha256 , commit)
323
- . map_err ( |err| SignError :: Spawn ( err. to_string ( ) ) ) ?
324
- . to_pem ( LineEnding :: LF )
325
- . map_err ( |err| SignError :: Spawn ( err. to_string ( ) ) ) ?;
326
- Ok ( ( sig, None ) )
291
+ use std:: io:: Write ;
292
+ use std:: process:: { Command , Stdio } ;
293
+
294
+ let mut cmd = Command :: new ( & self . program ) ;
295
+ cmd. stdin ( Stdio :: piped ( ) )
296
+ . stdout ( Stdio :: piped ( ) )
297
+ . stderr ( Stdio :: piped ( ) )
298
+ . arg ( "-Y" )
299
+ . arg ( "sign" )
300
+ . arg ( "-n" )
301
+ . arg ( "git" )
302
+ . arg ( "-f" )
303
+ . arg ( & self . signing_key ) ;
304
+
305
+ if & self . program == "ssh-keygen" {
306
+ cmd. arg ( "-P" ) . arg ( "\" \" " ) ;
307
+ }
308
+
309
+ log:: trace!( "signing command: {cmd:?}" ) ;
310
+
311
+ let mut child = cmd
312
+ . spawn ( )
313
+ . map_err ( |e| SignError :: Spawn ( e. to_string ( ) ) ) ?;
314
+
315
+ let mut stdin = child. stdin . take ( ) . ok_or ( SignError :: Stdin ) ?;
316
+
317
+ stdin
318
+ . write_all ( commit)
319
+ . map_err ( |e| SignError :: WriteBuffer ( e. to_string ( ) ) ) ?;
320
+ drop ( stdin) ;
321
+
322
+ //hllo
323
+
324
+ let output = child
325
+ . wait_with_output ( )
326
+ . map_err ( |e| SignError :: Output ( e. to_string ( ) ) ) ?;
327
+
328
+ let tmp_path = std:: env:: temp_dir ( ) ;
329
+ if self . signing_key . starts_with ( tmp_path) {
330
+ // Not handling error, as its not that bad. OS maintenance tasks will take care of it at a later point.
331
+ let _ = std:: fs:: remove_file ( PathBuf :: from (
332
+ & self . signing_key ,
333
+ ) ) ;
334
+ }
335
+
336
+ if !output. status . success ( ) {
337
+ let error_msg = std:: str:: from_utf8 ( & output. stderr )
338
+ . unwrap_or ( "[error could not be read from stderr]" ) ;
339
+ if error_msg. contains ( "passphrase" ) {
340
+ return Err ( SignError :: Shellout ( String :: from ( "Currently, we only support unencrypted pairs of ssh keys in disk or ssh-agents" ) ) ) ;
341
+ }
342
+ return Err ( SignError :: Shellout ( format ! (
343
+ "failed to sign data, program '{}' exited non-zero: {}" ,
344
+ & self . program,
345
+ error_msg
346
+ ) ) ) ;
347
+ }
348
+
349
+ let signed_commit = std:: str:: from_utf8 ( & output. stdout )
350
+ . map_err ( |e| SignError :: Shellout ( e. to_string ( ) ) ) ?;
351
+
352
+ Ok ( ( signed_commit. to_string ( ) , None ) )
327
353
}
328
354
329
355
#[ cfg( test) ]
330
- fn program ( & self ) -> & String {
331
- & self . program
356
+ fn program ( & self ) -> String {
357
+ self . program . clone ( )
332
358
}
333
359
334
360
#[ cfg( test) ]
335
- fn signing_key ( & self ) -> & String {
336
- & self . key_path
361
+ fn signing_key ( & self ) -> String {
362
+ format ! ( "{}" , self . signing_key . display ( ) )
337
363
}
338
364
}
339
365
@@ -424,18 +450,74 @@ mod tests {
424
450
#[ test]
425
451
fn test_ssh_program_configs ( ) -> Result < ( ) > {
426
452
let ( _tmp_dir, repo) = repo_init_empty ( ) ?;
453
+ let temp_file = tempfile:: NamedTempFile :: new ( )
454
+ . expect ( "failed to create temp file" ) ;
455
+
456
+ {
457
+ let mut config = repo. config ( ) ?;
458
+ config. set_str ( "gpg.format" , "ssh" ) ?;
459
+ config. set_str (
460
+ "user.signingKey" ,
461
+ temp_file. path ( ) . to_str ( ) . unwrap ( ) ,
462
+ ) ?;
463
+ }
464
+
465
+ let sign =
466
+ SignBuilder :: from_gitconfig ( & repo, & repo. config ( ) ?) ?;
467
+
468
+ assert_eq ! ( "ssh-keygen" , sign. program( ) ) ;
469
+ assert_eq ! (
470
+ temp_file. path( ) . to_str( ) . unwrap( ) ,
471
+ sign. signing_key( )
472
+ ) ;
473
+
474
+ drop ( temp_file) ;
475
+ Ok ( ( ) )
476
+ }
477
+
478
+ #[ test]
479
+ fn test_ssh_keyliteral_config ( ) -> Result < ( ) > {
480
+ let ( _tmp_dir, repo) = repo_init_empty ( ) ?;
427
481
428
482
{
429
483
let mut config = repo. config ( ) ?;
430
- config. set_str ( "gpg.program" , "ssh" ) ?;
431
- config. set_str ( "user.signingKey" , "/tmp/key.pub" ) ?;
484
+ config. set_str ( "gpg.format" , "ssh" ) ?;
485
+ config. set_str ( "user.signingKey" , "ssh-ed25519 test" ) ?;
486
+ }
487
+
488
+ let sign =
489
+ SignBuilder :: from_gitconfig ( & repo, & repo. config ( ) ?) ?;
490
+
491
+ assert_eq ! ( "ssh-keygen" , sign. program( ) ) ;
492
+ assert ! ( PathBuf :: from( sign. signing_key( ) ) . is_file( ) ) ;
493
+
494
+ Ok ( ( ) )
495
+ }
496
+
497
+ #[ test]
498
+ fn test_ssh_external_bin_config ( ) -> Result < ( ) > {
499
+ let ( _tmp_dir, repo) = repo_init_empty ( ) ?;
500
+ let temp_file = tempfile:: NamedTempFile :: new ( )
501
+ . expect ( "failed to create temp file" ) ;
502
+
503
+ {
504
+ let mut config = repo. config ( ) ?;
505
+ config. set_str ( "gpg.format" , "ssh" ) ?;
506
+ config. set_str ( "gpg.ssh.program" , "/opt/ssh/signer" ) ?;
507
+ config. set_str (
508
+ "user.signingKey" ,
509
+ temp_file. path ( ) . to_str ( ) . unwrap ( ) ,
510
+ ) ?;
432
511
}
433
512
434
513
let sign =
435
514
SignBuilder :: from_gitconfig ( & repo, & repo. config ( ) ?) ?;
436
515
437
- assert_eq ! ( "ssh" , sign. program( ) ) ;
438
- assert_eq ! ( "/tmp/key.pub" , sign. signing_key( ) ) ;
516
+ assert_eq ! ( "/opt/ssh/signer" , sign. program( ) ) ;
517
+ assert_eq ! (
518
+ temp_file. path( ) . to_str( ) . unwrap( ) ,
519
+ sign. signing_key( )
520
+ ) ;
439
521
440
522
Ok ( ( ) )
441
523
}
0 commit comments