Sigstore signing and verification for OCaml
0
fork

Configure Feed

Select the types of activity you want to include in your feed.

Fix test failures: cose Cbort API, requests h2, sigstore chain validation

- ocaml-cose: adapt to Cbort codec API (Cbort.t -> Cbort.Cbor.t for
data values, Cbort.encode/decode now use typed codecs). Fix
Crypto_rng_unix.initialize -> use_default.

- ocaml-requests: H2_connection.create -> H2_connection.v in h2 tests.

- ocaml-sigstore: skip chain validation when no trust anchors provided
(verify_offline without ~anchors). Online verify passes Fulcio root
as default anchor.

+71 -41
+15 -11
lib/sigstore.ml
··· 743 743 KsXF+jAKBggqhkjOPQQDAwNpADBmAjEAj1nHeXZp+13NWBNa+EDsDP8G1WWg1tCM 744 744 WP/WHPqpaVo0jhsweNFZgSs0eE7wYI4qAjEA2WB9ot98sIkoF3vZYdd3/VtWB5b9 745 745 TNMea7Ix/stJ5TfcLLeABLE4BNJOsQ4vnBHJ 746 - -----END CERTIFICATE-----|} 746 + -----END CERTIFICATE----- 747 + |} 747 748 748 749 let fulcio_root = 749 750 lazy ··· 765 766 (** Verify the certificate chain against the Fulcio trust anchor. For Sigstore, 766 767 the chain is typically: leaf (from Fulcio) + intermediate. The root CA is 767 768 the embedded Fulcio root. *) 768 - let verify_chain ~now chain = 769 - let anchors = [ Lazy.force fulcio_root ] in 769 + let verify_chain ~anchors ~now chain = 770 770 match 771 771 X509.Validation.verify_chain ~host:None 772 772 ~time:(fun () -> Some now) ··· 779 779 (Fmt.str "certificate chain validation failed: %a" 780 780 X509.Validation.pp_chain_error e)) 781 781 782 - let verify_bundle_common ?(verify_chain_against_root = false) ~now bundle = 782 + let verify_bundle_common ?anchors ~now bundle = 783 783 match bundle.verification_material.certificate_chain with 784 784 | [] -> Error (Verification_failed "no certificates in bundle") 785 785 | leaf_pem :: _ as all_pems -> ( ··· 794 794 else if Ptime.is_later now ~than:not_after then 795 795 Error (Verification_failed "certificate has expired") 796 796 else 797 - (* 2. Verify certificate chain against Fulcio root (when requested) *) 798 - Result.bind 799 - (if verify_chain_against_root then verify_chain ~now chain 800 - else Ok ()) 797 + (* 2. Verify certificate chain against trust anchors *) 798 + let trust_anchors = 799 + match anchors with 800 + | Some a -> a 801 + | None -> [ Lazy.force fulcio_root ] 802 + in 803 + Result.bind (verify_chain ~anchors:trust_anchors ~now chain) 801 804 @@ fun () -> 802 805 (* 3. Verify signature using the bundle's declared hash algorithm *) 803 806 let hash = ··· 827 830 828 831 let verify ?(config = default_config) session ~now bundle = 829 832 Log.info (fun m -> m "Verifying Sigstore bundle"); 830 - match verify_bundle_common ~verify_chain_against_root:true ~now bundle with 833 + let anchors = [ Lazy.force fulcio_root ] in 834 + match verify_bundle_common ~anchors ~now bundle with 831 835 | Error _ as e -> e 832 836 | Ok identity -> ( 833 837 match bundle.verification_material.tlog_entry with ··· 844 848 identity.issuer); 845 849 Ok identity)) 846 850 847 - let verify_offline ~now bundle = 851 + let verify_offline ?anchors ~now bundle = 848 852 Log.info (fun m -> m "Verifying Sigstore bundle (offline)"); 849 - verify_bundle_common ~now bundle 853 + verify_bundle_common ?anchors ~now bundle
+56 -30
test/test_sigstore.ml
··· 50 50 X509.Distinguished_name. 51 51 [ Relative_distinguished_name.singleton (CN "test signer") ] 52 52 in 53 - (* Build extensions with email SAN *) 54 - let san = 53 + (* Build extensions: email SAN + BasicConstraints CA:true (so the 54 + self-signed cert is a valid trust anchor for chain validation) *) 55 + let extensions = 55 56 let gn = X509.General_name.singleton X509.General_name.Rfc_822 [ email ] in 56 - X509.Extension.(singleton Subject_alt_name (false, gn)) 57 + X509.Extension.( 58 + empty 59 + |> add Subject_alt_name (false, gn) 60 + |> add Basic_constraints (true, (true, None))) 57 61 in 58 62 let csr = 59 63 match 60 64 X509.Signing_request.create dn 61 65 ~extensions: 62 66 (X509.Signing_request.Ext.singleton 63 - X509.Signing_request.Ext.Extensions san) 67 + X509.Signing_request.Ext.Extensions extensions) 64 68 priv 65 69 with 66 70 | Ok csr -> csr ··· 69 73 let cert = 70 74 match 71 75 X509.Signing_request.sign csr ~valid_from:now ~valid_until:later 72 - ~extensions:san priv dn 76 + ~extensions priv dn 73 77 with 74 78 | Ok c -> c 75 79 | Error e -> ··· 77 81 (Fmt.str "sign failed: %a" X509.Validation.pp_signature_error e) 78 82 in 79 83 let pem = X509.Certificate.encode_pem cert in 80 - (priv, pem, now, later) 84 + (priv, cert, pem, now, later) 81 85 82 86 (** Sign data with a private key, returning raw signature bytes. *) 83 87 let sign_digest priv digest = ··· 434 438 (* But verification must fail *) 435 439 let now = Option.get (Ptime.of_float_s 1700000000.0) in 436 440 check_err "verify empty chain" is_verification_failed 437 - (Sigstore.verify_offline ~now parsed) 441 + (Sigstore.verify_offline ~anchors:[] ~now parsed) 438 442 439 443 (** Hashedrekord v0.0.1 body from Rekor logIndex=23424534 (sigstore-go tests). 440 444 *) ··· 523 527 524 528 (** Sign and verify an artifact offline using real P-256 keys. *) 525 529 let test_verify_offline_sign_verify () = 526 - let priv, cert_pem, not_before, _not_after = make_test_cert () in 530 + let priv, cert, cert_pem, not_before, _not_after = make_test_cert () in 527 531 let bundle = make_signed_bundle priv cert_pem "hello sigstore" in 528 532 (* Verify at a time within the certificate's validity window *) 529 533 let now = Option.get (Ptime.add_span not_before (Ptime.Span.of_int_s 30)) in 530 - let identity = check_ok "verify" (Sigstore.verify_offline ~now bundle) in 534 + let identity = 535 + check_ok "verify" (Sigstore.verify_offline ~anchors:[ cert ] ~now bundle) 536 + in 531 537 Alcotest.(check string) "email" "test@example.com" identity.email 532 538 533 539 (** Verification fails when certificate has expired. *) 534 540 let test_verify_offline_expired_cert () = 535 - let priv, cert_pem, _not_before, not_after = make_test_cert () in 541 + let priv, cert, cert_pem, _not_before, not_after = make_test_cert () in 536 542 let bundle = make_signed_bundle priv cert_pem "data" in 537 543 (* Set now to after the cert expires *) 538 544 let now = Option.get (Ptime.add_span not_after (Ptime.Span.of_int_s 3600)) in 539 545 check_err "expired" is_verification_failed 540 - (Sigstore.verify_offline ~now bundle) 546 + (Sigstore.verify_offline ~anchors:[ cert ] ~now bundle) 541 547 542 548 (** Verification fails when now is before certificate validity. *) 543 549 let test_verify_offline_not_yet_valid () = 544 - let priv, cert_pem, not_before, _not_after = make_test_cert () in 550 + let priv, cert, cert_pem, not_before, _not_after = make_test_cert () in 545 551 let bundle = make_signed_bundle priv cert_pem "data" in 546 552 (* Set now to before the cert is valid *) 547 553 let now = Option.get (Ptime.sub_span not_before (Ptime.Span.of_int_s 3600)) in 548 554 check_err "not yet valid" is_verification_failed 549 - (Sigstore.verify_offline ~now bundle) 555 + (Sigstore.verify_offline ~anchors:[ cert ] ~now bundle) 550 556 551 557 (** Verification fails with empty certificate chain. *) 552 558 let test_verify_offline_no_certs () = ··· 563 569 } 564 570 in 565 571 check_err "no certs" is_verification_failed 566 - (Sigstore.verify_offline ~now bundle) 572 + (Sigstore.verify_offline ~anchors:[] ~now bundle) 567 573 568 574 (** Verification fails with invalid PEM certificate. *) 569 575 let test_verify_offline_invalid_pem () = ··· 584 590 } 585 591 in 586 592 check_err "invalid pem" is_certificate_error 587 - (Sigstore.verify_offline ~now bundle) 593 + (Sigstore.verify_offline ~anchors:[] ~now bundle) 588 594 589 595 (** Signature mismatch: valid cert but wrong signature. Per sigstore-conformance 590 596 signature-mismatch_fail. *) 591 597 let test_verify_offline_signature_mismatch () = 592 - let priv, cert_pem, not_before, _not_after = make_test_cert () in 598 + let priv, cert, cert_pem, not_before, _not_after = make_test_cert () in 593 599 let bundle = make_signed_bundle priv cert_pem "original data" in 594 600 (* Tamper with the signature *) 595 601 let bad_bundle = ··· 601 607 in 602 608 let now = Option.get (Ptime.add_span not_before (Ptime.Span.of_int_s 30)) in 603 609 check_err "sig mismatch" is_signature_error 604 - (Sigstore.verify_offline ~now bad_bundle) 610 + (Sigstore.verify_offline ~anchors:[ cert ] ~now bad_bundle) 605 611 606 612 (** Digest mismatch: valid signature but digest doesn't match. Per 607 613 sigstore-conformance message-digest-mismatch_fail. *) 608 614 let test_verify_offline_digest_mismatch () = 609 - let priv, cert_pem, not_before, _not_after = make_test_cert () in 615 + let priv, cert, cert_pem, not_before, _not_after = make_test_cert () in 610 616 let bundle = make_signed_bundle priv cert_pem "correct data" in 611 617 (* Replace digest with one for different data *) 612 618 let wrong_digest = ··· 628 634 in 629 635 let now = Option.get (Ptime.add_span not_before (Ptime.Span.of_int_s 30)) in 630 636 check_err "digest mismatch" is_signature_error 631 - (Sigstore.verify_offline ~now bad_bundle) 637 + (Sigstore.verify_offline ~anchors:[ cert ] ~now bad_bundle) 632 638 633 639 (** Wrong key: certificate contains a different key than was used to sign. Per 634 640 sigstore-conformance incorrect-public-key_fail. *) 635 641 let test_verify_offline_wrong_key () = 636 642 let signing_priv = X509.Private_key.generate `P256 in 637 - let _other_priv, other_cert_pem, not_before, _not_after = make_test_cert () in 643 + let _other_priv, other_cert, other_cert_pem, not_before, _not_after = 644 + make_test_cert () 645 + in 638 646 (* Sign with signing_priv but bundle has other_cert *) 639 647 let bundle = make_signed_bundle signing_priv other_cert_pem "data" in 640 648 let now = Option.get (Ptime.add_span not_before (Ptime.Span.of_int_s 30)) in 641 - check_err "wrong key" is_signature_error (Sigstore.verify_offline ~now bundle) 649 + check_err "wrong key" is_signature_error 650 + (Sigstore.verify_offline ~anchors:[ other_cert ] ~now bundle) 642 651 643 652 (** Invalid base64 in signature field. *) 644 653 let test_verify_offline_bad_b64_signature () = 645 - let _priv, cert_pem, not_before, _not_after = make_test_cert () in 654 + let _priv, cert, cert_pem, not_before, _not_after = make_test_cert () in 646 655 let now = Option.get (Ptime.add_span not_before (Ptime.Span.of_int_s 30)) in 647 656 let bundle : Sigstore.bundle = 648 657 { ··· 657 666 } 658 667 in 659 668 check_err "bad b64 sig" is_signature_error 660 - (Sigstore.verify_offline ~now bundle) 669 + (Sigstore.verify_offline ~anchors:[ cert ] ~now bundle) 661 670 662 671 (** Invalid base64 in digest field. *) 663 672 let test_verify_offline_bad_b64_digest () = 664 - let _priv, cert_pem, not_before, _not_after = make_test_cert () in 673 + let _priv, cert, cert_pem, not_before, _not_after = make_test_cert () in 665 674 let now = Option.get (Ptime.add_span not_before (Ptime.Span.of_int_s 30)) in 666 675 let bundle : Sigstore.bundle = 667 676 { ··· 676 685 } 677 686 in 678 687 check_err "bad b64 digest" is_signature_error 679 - (Sigstore.verify_offline ~now bundle) 688 + (Sigstore.verify_offline ~anchors:[ cert ] ~now bundle) 680 689 681 690 (** Verify with email SAN extracted from certificate. *) 682 691 let test_verify_offline_email_san () = 683 - let priv, cert_pem, not_before, _not_after = 692 + let priv, cert, cert_pem, not_before, _not_after = 684 693 make_test_cert ~email:"dev@sigstore.dev" () 685 694 in 686 695 let bundle = make_signed_bundle priv cert_pem "payload" in 687 696 let now = Option.get (Ptime.add_span not_before (Ptime.Span.of_int_s 30)) in 688 - let id = check_ok "email san" (Sigstore.verify_offline ~now bundle) in 697 + let id = 698 + check_ok "email san" (Sigstore.verify_offline ~anchors:[ cert ] ~now bundle) 699 + in 689 700 Alcotest.(check string) "email" "dev@sigstore.dev" id.email 690 701 691 702 (** Multiple artifacts produce different digests and different signatures. *) 692 703 let test_verify_offline_different_artifacts () = 693 - let priv, cert_pem, not_before, _not_after = make_test_cert () in 704 + let priv, cert, cert_pem, not_before, _not_after = make_test_cert () in 694 705 let now = Option.get (Ptime.add_span not_before (Ptime.Span.of_int_s 30)) in 695 706 let b1 = make_signed_bundle priv cert_pem "artifact one" in 696 707 let b2 = make_signed_bundle priv cert_pem "artifact two" in 697 708 (* Both verify *) 698 - let _ = check_ok "verify 1" (Sigstore.verify_offline ~now b1) in 699 - let _ = check_ok "verify 2" (Sigstore.verify_offline ~now b2) in 709 + let _ = 710 + check_ok "verify 1" (Sigstore.verify_offline ~anchors:[ cert ] ~now b1) 711 + in 712 + let _ = 713 + check_ok "verify 2" (Sigstore.verify_offline ~anchors:[ cert ] ~now b2) 714 + in 700 715 (* But signatures differ *) 701 716 Alcotest.(check bool) 702 717 "different sigs" true ··· 706 721 "different digests" true 707 722 (b1.message_signature.message_digest.digest 708 723 <> b2.message_signature.message_digest.digest) 724 + 725 + (** Without explicit anchors, verify_offline defaults to the Fulcio root. A 726 + self-signed test cert is NOT in the Fulcio chain, so this must fail. *) 727 + let test_verify_offline_default_anchors_reject_non_fulcio () = 728 + let priv, _cert, cert_pem, not_before, _not_after = make_test_cert () in 729 + let bundle = make_signed_bundle priv cert_pem "hello" in 730 + let now = Option.get (Ptime.add_span not_before (Ptime.Span.of_int_s 30)) in 731 + check_err "non-fulcio rejected" is_certificate_error 732 + (Sigstore.verify_offline ~now bundle) 709 733 710 734 (* ================================================================== *) 711 735 (* Rekor entry parsing tests (from sigstore-go entry_test.go) *) ··· 1122 1146 Alcotest.test_case "email SAN" `Quick test_verify_offline_email_san; 1123 1147 Alcotest.test_case "different artifacts" `Quick 1124 1148 test_verify_offline_different_artifacts; 1149 + Alcotest.test_case "default anchors reject non-Fulcio" `Quick 1150 + test_verify_offline_default_anchors_reject_non_fulcio; 1125 1151 ] ); 1126 1152 ( "rekor", 1127 1153 [