1
1
//! Sign commit data.
2
2
3
+ use git2:: Config ;
3
4
use ssh_key:: { HashAlg , LineEnding , PrivateKey } ;
4
- use std:: path:: PathBuf ;
5
+ use std:: { fmt:: Display , path:: PathBuf } ;
6
+ use tempfile:: NamedTempFile ;
5
7
6
8
/// Error type for [`SignBuilder`], used to create [`Sign`]'s
7
9
#[ derive( thiserror:: Error , Debug ) ]
@@ -156,37 +158,81 @@ impl SignBuilder {
156
158
String :: from ( "x509" ) ,
157
159
) ) ,
158
160
"ssh" => {
159
- let ssh_signer = config
160
- . 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
- } ,
174
- )
175
- } )
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)
161
+ let program = SSHProgram :: new ( config) ;
162
+ program. into_signer ( config)
184
163
}
185
164
_ => Err ( SignBuilderError :: InvalidFormat ( format) ) ,
186
165
}
187
166
}
188
167
}
189
168
169
+ enum SSHProgram {
170
+ Default ,
171
+ SystemBin ( PathBuf ) ,
172
+ }
173
+
174
+ impl SSHProgram {
175
+ pub fn new ( config : & git2:: Config ) -> Self {
176
+ match config. get_string ( "gpg.ssh.program" ) {
177
+ Err ( _) => Self :: Default ,
178
+ Ok ( ssh_program) => {
179
+ if ssh_program. is_empty ( ) {
180
+ return Self :: Default ;
181
+ }
182
+ Self :: SystemBin ( PathBuf :: from ( ssh_program) )
183
+ }
184
+ }
185
+ }
186
+
187
+ fn into_signer (
188
+ self ,
189
+ config : & git2:: Config ,
190
+ ) -> Result < Box < dyn Sign > , SignBuilderError > {
191
+ match self {
192
+ SSHProgram :: Default => {
193
+ let ssh_signer = ConfigAccess ( config)
194
+ . signing_key ( )
195
+ . and_then ( SSHSign :: new) ?;
196
+ Ok ( Box :: new ( ssh_signer) )
197
+ }
198
+ SSHProgram :: SystemBin ( exec_path) => {
199
+ let key = ConfigAccess ( config) . signing_key ( ) ?;
200
+ Ok ( Box :: new ( ExternalBinSSHSign :: new ( exec_path, key) ) )
201
+ }
202
+ }
203
+ }
204
+ }
205
+
206
+ /// wrapper struct for convenience methods over [Config]
207
+ struct ConfigAccess < ' a > ( & ' a Config ) ;
208
+
209
+ impl < ' a > ConfigAccess < ' a > {
210
+ pub fn signing_key ( & self ) -> Result < PathBuf , SignBuilderError > {
211
+ self . 0
212
+ . get_string ( "user.signingKey" )
213
+ . ok ( )
214
+ . and_then ( |key_path| {
215
+ key_path. strip_prefix ( '~' ) . map_or_else (
216
+ || Some ( PathBuf :: from ( & key_path) ) ,
217
+ |ssh_key_path| {
218
+ dirs:: home_dir ( ) . map ( |home| {
219
+ home. join (
220
+ ssh_key_path
221
+ . strip_prefix ( '/' )
222
+ . unwrap_or ( ssh_key_path) ,
223
+ )
224
+ } )
225
+ } ,
226
+ )
227
+ } )
228
+ . ok_or_else ( || {
229
+ SignBuilderError :: SSHSigningKey ( String :: from (
230
+ "ssh key setting absent" ,
231
+ ) )
232
+ } )
233
+ }
234
+ }
235
+
190
236
/// Sign commit data using `OpenPGP`
191
237
pub struct GPGSign {
192
238
program : String ,
@@ -280,6 +326,144 @@ pub struct SSHSign {
280
326
secret_key : PrivateKey ,
281
327
}
282
328
329
+ enum KeyPathOrLiteral {
330
+ Literal ( PathBuf ) ,
331
+ KeyPath ( PathBuf ) ,
332
+ }
333
+
334
+ impl KeyPathOrLiteral {
335
+ fn new ( buf : PathBuf ) -> Self {
336
+ match buf. is_file ( ) {
337
+ true => KeyPathOrLiteral :: KeyPath ( buf) ,
338
+ false => KeyPathOrLiteral :: Literal ( buf) ,
339
+ }
340
+ }
341
+ }
342
+
343
+ impl Display for KeyPathOrLiteral {
344
+ fn fmt (
345
+ & self ,
346
+ f : & mut std:: fmt:: Formatter < ' _ > ,
347
+ ) -> std:: fmt:: Result {
348
+ let buf = match self {
349
+ Self :: Literal ( x) => x,
350
+ Self :: KeyPath ( x) => x,
351
+ } ;
352
+ f. write_fmt ( format_args ! ( "{}" , buf. display( ) ) )
353
+ }
354
+ }
355
+
356
+ /// Struct which allows for signing via an external binary
357
+ pub struct ExternalBinSSHSign {
358
+ program_path : PathBuf ,
359
+ key_path : KeyPathOrLiteral ,
360
+ #[ cfg( test) ]
361
+ program : String ,
362
+ #[ cfg( test) ]
363
+ signing_key : String ,
364
+ }
365
+
366
+ impl ExternalBinSSHSign {
367
+ /// constructs a new instance of the external ssh signer
368
+ pub fn new ( program_path : PathBuf , key_path : PathBuf ) -> Self {
369
+ #[ cfg( test) ]
370
+ let program: String = program_path
371
+ . file_name ( )
372
+ . unwrap_or_default ( )
373
+ . to_string_lossy ( )
374
+ . into_owned ( ) ;
375
+
376
+ let key_path = KeyPathOrLiteral :: new ( key_path) ;
377
+
378
+ #[ cfg( test) ]
379
+ let signing_key = key_path. to_string ( ) ;
380
+
381
+ ExternalBinSSHSign {
382
+ program_path,
383
+ key_path,
384
+ #[ cfg( test) ]
385
+ program,
386
+ #[ cfg( test) ]
387
+ signing_key,
388
+ }
389
+ }
390
+ }
391
+
392
+ impl Sign for ExternalBinSSHSign {
393
+ fn sign (
394
+ & self ,
395
+ commit : & [ u8 ] ,
396
+ ) -> Result < ( String , Option < String > ) , SignError > {
397
+ use std:: io:: Write ;
398
+ use std:: process:: { Command , Stdio } ;
399
+
400
+ if cfg ! ( target_os = "windows" ) {
401
+ return Err ( SignError :: Spawn ( "External binary signing is only supported on Unix based systems" . into ( ) ) ) ;
402
+ }
403
+
404
+ let mut file = NamedTempFile :: new ( )
405
+ . map_err ( |e| SignError :: Spawn ( e. to_string ( ) ) ) ?;
406
+
407
+ let key = match & self . key_path {
408
+ KeyPathOrLiteral :: Literal ( x) => {
409
+ write ! ( file, "{}" , x. display( ) ) . map_err ( |e| {
410
+ SignError :: WriteBuffer ( e. to_string ( ) )
411
+ } ) ?;
412
+ file. path ( )
413
+ }
414
+ KeyPathOrLiteral :: KeyPath ( x) => x. as_path ( ) ,
415
+ } ;
416
+
417
+ let mut c = Command :: new ( & self . program_path ) ;
418
+ c. stdin ( Stdio :: piped ( ) )
419
+ . stdout ( Stdio :: piped ( ) )
420
+ . stderr ( Stdio :: piped ( ) )
421
+ . arg ( "-Y" )
422
+ . arg ( "sign" )
423
+ . arg ( "-n" )
424
+ . arg ( "git" )
425
+ . arg ( "-f" )
426
+ . arg ( key) ;
427
+
428
+ let mut child =
429
+ c. spawn ( ) . map_err ( |e| SignError :: Spawn ( e. to_string ( ) ) ) ?;
430
+
431
+ let mut stdin = child. stdin . take ( ) . ok_or ( SignError :: Stdin ) ?;
432
+
433
+ stdin
434
+ . write_all ( commit)
435
+ . map_err ( |e| SignError :: WriteBuffer ( e. to_string ( ) ) ) ?;
436
+ drop ( stdin) ;
437
+
438
+ let output = child
439
+ . wait_with_output ( )
440
+ . map_err ( |e| SignError :: Output ( e. to_string ( ) ) ) ?;
441
+
442
+ if !output. status . success ( ) {
443
+ return Err ( SignError :: Shellout ( format ! (
444
+ "failed to sign data, program '{}' exited non-zero: {}" ,
445
+ & self . program_path. display( ) ,
446
+ std:: str :: from_utf8( & output. stderr) . unwrap_or( "[error could not be read from stderr]" )
447
+ ) ) ) ;
448
+ }
449
+
450
+ let signed_commit = std:: str:: from_utf8 ( & output. stdout )
451
+ . map_err ( |e| SignError :: Shellout ( e. to_string ( ) ) ) ?;
452
+
453
+ Ok ( ( signed_commit. to_string ( ) , None ) )
454
+ }
455
+
456
+ #[ cfg( test) ]
457
+ fn program ( & self ) -> & String {
458
+ & self . program
459
+ }
460
+
461
+ #[ cfg( test) ]
462
+ fn signing_key ( & self ) -> & String {
463
+ & self . signing_key
464
+ }
465
+ }
466
+
283
467
impl SSHSign {
284
468
/// Create new [`SSHDiskKeySign`] for sign.
285
469
pub fn new ( mut key : PathBuf ) -> Result < Self , SignBuilderError > {
@@ -306,7 +490,7 @@ impl SSHSign {
306
490
} )
307
491
} else {
308
492
Err ( SignBuilderError :: SSHSigningKey (
309
- String :: from ( "Currently, we only support a pair of ssh key in disk." ) ,
493
+ format ! ( "Currently, we only support a pair of ssh key in disk. Found {:?}" , key ) ,
310
494
) )
311
495
}
312
496
}
0 commit comments