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