3
#include "config.h"
4
#include "run-command.h"
5
#include "strbuf.h"
6
+#include "dir.h"
7
#include "gpg-interface.h"
8
#include "sigchain.h"
9
#include "tempfile.h"
10
#include "alias.h"
11
12
static char *configured_signing_key;
12
-static const char *ssh_default_key_command;
13
+static const char *ssh_default_key_command, *ssh_allowed_signers, *ssh_revocation_file;
14
static enum signature_trust_level configured_min_trust_level = TRUST_UNDEFINED;
15
16
struct gpg_format {
56
struct gpg_format *fmt, const char *payload,
57
size_t payload_size, const char *signature,
58
size_t signature_size);
59
+static int verify_ssh_signed_buffer(struct signature_check *sigc,
60
+ struct gpg_format *fmt, const char *payload,
61
+ size_t payload_size, const char *signature,
62
+ size_t signature_size);
63
static int sign_buffer_gpg(struct strbuf *buffer, struct strbuf *signature,
64
const char *signing_key);
65
static int sign_buffer_ssh(struct strbuf *buffer, struct strbuf *signature,
95
.program = "ssh-keygen",
96
.verify_args = ssh_verify_args,
97
.sigs = ssh_sigs,
93
- .verify_signed_buffer = NULL, /* TODO */
98
+ .verify_signed_buffer = verify_ssh_signed_buffer,
99
.sign_buffer = sign_buffer_ssh,
100
.get_default_key = get_default_ssh_signing_key,
101
.get_key_id = get_ssh_key_id,
362
return ret;
363
}
364
365
+static void parse_ssh_output(struct signature_check *sigc)
366
+{
367
+ const char *line, *principal, *search;
368
+ char *key = NULL;
369
+
370
+ /*
371
+ * ssh-keygen output should be:
372
+ * Good "git" signature for PRINCIPAL with RSA key SHA256:FINGERPRINT
373
+ *
374
+ * or for valid but unknown keys:
375
+ * Good "git" signature with RSA key SHA256:FINGERPRINT
376
+ *
377
+ * Note that "PRINCIPAL" can contain whitespace, "RSA" and
378
+ * "SHA256" part could be a different token that names of
379
+ * the algorithms used, and "FINGERPRINT" is a hexadecimal
380
+ * string. By finding the last occurence of " with ", we can
381
+ * reliably parse out the PRINCIPAL.
382
+ */
383
+ sigc->result = 'B';
384
+ sigc->trust_level = TRUST_NEVER;
385
+
386
+ line = xmemdupz(sigc->output, strcspn(sigc->output, "\n"));
387
+
388
+ if (skip_prefix(line, "Good \"git\" signature for ", &line)) {
389
+ /* Valid signature and known principal */
390
+ sigc->result = 'G';
391
+ sigc->trust_level = TRUST_FULLY;
392
+
393
+ /* Search for the last "with" to get the full principal */
394
+ principal = line;
395
+ do {
396
+ search = strstr(line, " with ");
397
+ if (search)
398
+ line = search + 1;
399
+ } while (search != NULL);
400
+ sigc->signer = xmemdupz(principal, line - principal - 1);
401
+ } else if (skip_prefix(line, "Good \"git\" signature with ", &line)) {
402
+ /* Valid signature, but key unknown */
403
+ sigc->result = 'G';
404
+ sigc->trust_level = TRUST_UNDEFINED;
405
+ } else {
406
+ return;
407
+ }
408
+
409
+ key = strstr(line, "key");
410
+ if (key) {
411
+ sigc->fingerprint = xstrdup(strstr(line, "key") + 4);
412
+ sigc->key = xstrdup(sigc->fingerprint);
413
+ } else {
414
+ /*
415
+ * Output did not match what we expected
416
+ * Treat the signature as bad
417
+ */
418
+ sigc->result = 'B';
419
+ }
420
+}
421
+
422
+static int verify_ssh_signed_buffer(struct signature_check *sigc,
423
+ struct gpg_format *fmt, const char *payload,
424
+ size_t payload_size, const char *signature,
425
+ size_t signature_size)
426
+{
427
+ struct child_process ssh_keygen = CHILD_PROCESS_INIT;
428
+ struct tempfile *buffer_file;
429
+ int ret = -1;
430
+ const char *line;
431
+ size_t trust_size;
432
+ char *principal;
433
+ struct strbuf ssh_principals_out = STRBUF_INIT;
434
+ struct strbuf ssh_principals_err = STRBUF_INIT;
435
+ struct strbuf ssh_keygen_out = STRBUF_INIT;
436
+ struct strbuf ssh_keygen_err = STRBUF_INIT;
437
+
438
+ if (!ssh_allowed_signers) {
439
+ error(_("gpg.ssh.allowedSignersFile needs to be configured and exist for ssh signature verification"));
440
+ return -1;
441
+ }
442
+
443
+ buffer_file = mks_tempfile_t(".git_vtag_tmpXXXXXX");
444
+ if (!buffer_file)
445
+ return error_errno(_("could not create temporary file"));
446
+ if (write_in_full(buffer_file->fd, signature, signature_size) < 0 ||
447
+ close_tempfile_gently(buffer_file) < 0) {
448
+ error_errno(_("failed writing detached signature to '%s'"),
449
+ buffer_file->filename.buf);
450
+ delete_tempfile(&buffer_file);
451
+ return -1;
452
+ }
453
+
454
+ /* Find the principal from the signers */
455
+ strvec_pushl(&ssh_keygen.args, fmt->program,
456
+ "-Y", "find-principals",
457
+ "-f", ssh_allowed_signers,
458
+ "-s", buffer_file->filename.buf,
459
+ NULL);
460
+ ret = pipe_command(&ssh_keygen, NULL, 0, &ssh_principals_out, 0,
461
+ &ssh_principals_err, 0);
462
+ if (ret && strstr(ssh_principals_err.buf, "usage:")) {
463
+ error(_("ssh-keygen -Y find-principals/verify is needed for ssh signature verification (available in openssh version 8.2p1+)"));
464
+ goto out;
465
+ }
466
+ if (ret || !ssh_principals_out.len) {
467
+ /*
468
+ * We did not find a matching principal in the allowedSigners
469
+ * Check without validation
470
+ */
471
+ child_process_init(&ssh_keygen);
472
+ strvec_pushl(&ssh_keygen.args, fmt->program,
473
+ "-Y", "check-novalidate",
474
+ "-n", "git",
475
+ "-s", buffer_file->filename.buf,
476
+ NULL);
477
+ pipe_command(&ssh_keygen, payload, payload_size,
478
+ &ssh_keygen_out, 0, &ssh_keygen_err, 0);
479
+
480
+ /*
481
+ * Fail on unknown keys
482
+ * we still call check-novalidate to display the signature info
483
+ */
484
+ ret = -1;
485
+ } else {
486
+ /* Check every principal we found (one per line) */
487
+ for (line = ssh_principals_out.buf; *line;
488
+ line = strchrnul(line + 1, '\n')) {
489
+ while (*line == '\n')
490
+ line++;
491
+ if (!*line)
492
+ break;
493
+
494
+ trust_size = strcspn(line, "\n");
495
+ principal = xmemdupz(line, trust_size);
496
+
497
+ child_process_init(&ssh_keygen);
498
+ strbuf_release(&ssh_keygen_out);
499
+ strbuf_release(&ssh_keygen_err);
500
+ strvec_push(&ssh_keygen.args, fmt->program);
501
+ /*
502
+ * We found principals
503
+ * Try with each until we find a match
504
+ */
505
+ strvec_pushl(&ssh_keygen.args, "-Y", "verify",
506
+ "-n", "git",
507
+ "-f", ssh_allowed_signers,
508
+ "-I", principal,
509
+ "-s", buffer_file->filename.buf,
510
+ NULL);
511
+
512
+ if (ssh_revocation_file) {
513
+ if (file_exists(ssh_revocation_file)) {
514
+ strvec_pushl(&ssh_keygen.args, "-r",
515
+ ssh_revocation_file, NULL);
516
+ } else {
517
+ warning(_("ssh signing revocation file configured but not found: %s"),
518
+ ssh_revocation_file);
519
+ }
520
+ }
521
+
522
+ sigchain_push(SIGPIPE, SIG_IGN);
523
+ ret = pipe_command(&ssh_keygen, payload, payload_size,
524
+ &ssh_keygen_out, 0, &ssh_keygen_err, 0);
525
+ sigchain_pop(SIGPIPE);
526
+
527
+ FREE_AND_NULL(principal);
528
+
529
+ if (!ret)
530
+ ret = !starts_with(ssh_keygen_out.buf, "Good");
531
+
532
+ if (!ret)
533
+ break;
534
+ }
535
+ }
536
+
537
+ sigc->payload = xmemdupz(payload, payload_size);
538
+ strbuf_stripspace(&ssh_keygen_out, 0);
539
+ strbuf_stripspace(&ssh_keygen_err, 0);
540
+ /* Add stderr outputs to show the user actual ssh-keygen errors */
541
+ strbuf_add(&ssh_keygen_out, ssh_principals_err.buf, ssh_principals_err.len);
542
+ strbuf_add(&ssh_keygen_out, ssh_keygen_err.buf, ssh_keygen_err.len);
543
+ sigc->output = strbuf_detach(&ssh_keygen_out, NULL);
544
+ sigc->gpg_status = xstrdup(sigc->output);
545
+
546
+ parse_ssh_output(sigc);
547
+
548
+out:
549
+ if (buffer_file)
550
+ delete_tempfile(&buffer_file);
551
+ strbuf_release(&ssh_principals_out);
552
+ strbuf_release(&ssh_principals_err);
553
+ strbuf_release(&ssh_keygen_out);
554
+ strbuf_release(&ssh_keygen_err);
555
+
556
+ return ret;
557
+}
558
+
559
int check_signature(const char *payload, size_t plen, const char *signature,
560
size_t slen, struct signature_check *sigc)
561
{
672
return git_config_string(&ssh_default_key_command, var, value);
673
}
674
675
+ if (!strcmp(var, "gpg.ssh.allowedsignersfile")) {
676
+ if (!value)
677
+ return config_error_nonbool(var);
678
+ return git_config_pathname(&ssh_allowed_signers, var, value);
679
+ }
680
+
681
+ if (!strcmp(var, "gpg.ssh.revocationfile")) {
682
+ if (!value)
683
+ return config_error_nonbool(var);
684
+ return git_config_pathname(&ssh_revocation_file, var, value);
685
+ }
686
+
687
if (!strcmp(var, "gpg.program") || !strcmp(var, "gpg.openpgp.program"))
688
fmtname = "openpgp";
689