|
3 | 3 | #include "config.h"
|
4 | 4 | #include "run-command.h"
|
5 | 5 | #include "strbuf.h"
|
| 6 | +#include "dir.h" |
6 | 7 | #include "gpg-interface.h"
|
7 | 8 | #include "sigchain.h"
|
8 | 9 | #include "tempfile.h"
|
9 | 10 | #include "alias.h"
|
10 | 11 |
|
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; |
13 | 14 | static enum signature_trust_level configured_min_trust_level = TRUST_UNDEFINED;
|
14 | 15 |
|
15 | 16 | struct gpg_format {
|
@@ -55,6 +56,10 @@ static int verify_gpg_signed_buffer(struct signature_check *sigc,
|
55 | 56 | struct gpg_format *fmt, const char *payload,
|
56 | 57 | size_t payload_size, const char *signature,
|
57 | 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); |
58 | 63 | static int sign_buffer_gpg(struct strbuf *buffer, struct strbuf *signature,
|
59 | 64 | const char *signing_key);
|
60 | 65 | static int sign_buffer_ssh(struct strbuf *buffer, struct strbuf *signature,
|
@@ -90,7 +95,7 @@ static struct gpg_format gpg_format[] = {
|
90 | 95 | .program = "ssh-keygen",
|
91 | 96 | .verify_args = ssh_verify_args,
|
92 | 97 | .sigs = ssh_sigs,
|
93 |
| - .verify_signed_buffer = NULL, /* TODO */ |
| 98 | + .verify_signed_buffer = verify_ssh_signed_buffer, |
94 | 99 | .sign_buffer = sign_buffer_ssh,
|
95 | 100 | .get_default_key = get_default_ssh_signing_key,
|
96 | 101 | .get_key_id = get_ssh_key_id,
|
@@ -357,6 +362,200 @@ static int verify_gpg_signed_buffer(struct signature_check *sigc,
|
357 | 362 | return ret;
|
358 | 363 | }
|
359 | 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 | + |
360 | 559 | int check_signature(const char *payload, size_t plen, const char *signature,
|
361 | 560 | size_t slen, struct signature_check *sigc)
|
362 | 561 | {
|
@@ -473,6 +672,18 @@ int git_gpg_config(const char *var, const char *value, void *cb)
|
473 | 672 | return git_config_string(&ssh_default_key_command, var, value);
|
474 | 673 | }
|
475 | 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 | + |
476 | 687 | if (!strcmp(var, "gpg.program") || !strcmp(var, "gpg.openpgp.program"))
|
477 | 688 | fmtname = "openpgp";
|
478 | 689 |
|
|
0 commit comments