Implement plants-04 landmark relative MTC verification. Change-Id: I81733dd7b7b4f7048e53fd17d8fbdcaccbde129b Bug: 452986180 Reviewed-on: https://boringssl-review.googlesource.com/c/boringssl/+/96247 Reviewed-by: Nick Harper <nharper@chromium.org> Commit-Queue: Matt Mueller <mattm@google.com>
diff --git a/gen/sources.bzl b/gen/sources.bzl index 8b57aa8..a7ff8d4 100644 --- a/gen/sources.bzl +++ b/gen/sources.bzl
@@ -2324,6 +2324,9 @@ "pki/testdata/path_builder_unittest/mtc/leaf.pem", "pki/testdata/path_builder_unittest/mtc/mtc-ica.pem", "pki/testdata/path_builder_unittest/mtc/mtc-leaf.pem", + "pki/testdata/path_builder_unittest/mtc_plants04/leaf.pem", + "pki/testdata/path_builder_unittest/mtc_plants04/mtc-ica.pem", + "pki/testdata/path_builder_unittest/mtc_plants04/mtc-leaf.pem", "pki/testdata/path_builder_unittest/multi-root-A-by-B.pem", "pki/testdata/path_builder_unittest/multi-root-B-by-C.pem", "pki/testdata/path_builder_unittest/multi-root-B-by-F.pem",
diff --git a/gen/sources.cmake b/gen/sources.cmake index 9acba30..e04e968 100644 --- a/gen/sources.cmake +++ b/gen/sources.cmake
@@ -2382,6 +2382,9 @@ pki/testdata/path_builder_unittest/mtc/leaf.pem pki/testdata/path_builder_unittest/mtc/mtc-ica.pem pki/testdata/path_builder_unittest/mtc/mtc-leaf.pem + pki/testdata/path_builder_unittest/mtc_plants04/leaf.pem + pki/testdata/path_builder_unittest/mtc_plants04/mtc-ica.pem + pki/testdata/path_builder_unittest/mtc_plants04/mtc-leaf.pem pki/testdata/path_builder_unittest/multi-root-A-by-B.pem pki/testdata/path_builder_unittest/multi-root-B-by-C.pem pki/testdata/path_builder_unittest/multi-root-B-by-F.pem
diff --git a/gen/sources.gni b/gen/sources.gni index 8b13bb5..611aab5 100644 --- a/gen/sources.gni +++ b/gen/sources.gni
@@ -2324,6 +2324,9 @@ "pki/testdata/path_builder_unittest/mtc/leaf.pem", "pki/testdata/path_builder_unittest/mtc/mtc-ica.pem", "pki/testdata/path_builder_unittest/mtc/mtc-leaf.pem", + "pki/testdata/path_builder_unittest/mtc_plants04/leaf.pem", + "pki/testdata/path_builder_unittest/mtc_plants04/mtc-ica.pem", + "pki/testdata/path_builder_unittest/mtc_plants04/mtc-leaf.pem", "pki/testdata/path_builder_unittest/multi-root-A-by-B.pem", "pki/testdata/path_builder_unittest/multi-root-B-by-C.pem", "pki/testdata/path_builder_unittest/multi-root-B-by-F.pem",
diff --git a/gen/sources.json b/gen/sources.json index 92e375e..6309f27 100644 --- a/gen/sources.json +++ b/gen/sources.json
@@ -2304,6 +2304,9 @@ "pki/testdata/path_builder_unittest/mtc/leaf.pem", "pki/testdata/path_builder_unittest/mtc/mtc-ica.pem", "pki/testdata/path_builder_unittest/mtc/mtc-leaf.pem", + "pki/testdata/path_builder_unittest/mtc_plants04/leaf.pem", + "pki/testdata/path_builder_unittest/mtc_plants04/mtc-ica.pem", + "pki/testdata/path_builder_unittest/mtc_plants04/mtc-leaf.pem", "pki/testdata/path_builder_unittest/multi-root-A-by-B.pem", "pki/testdata/path_builder_unittest/multi-root-B-by-C.pem", "pki/testdata/path_builder_unittest/multi-root-B-by-F.pem",
diff --git a/gen/sources.mk b/gen/sources.mk index fa0b051..87b3d53 100644 --- a/gen/sources.mk +++ b/gen/sources.mk
@@ -2296,6 +2296,9 @@ pki/testdata/path_builder_unittest/mtc/leaf.pem \ pki/testdata/path_builder_unittest/mtc/mtc-ica.pem \ pki/testdata/path_builder_unittest/mtc/mtc-leaf.pem \ + pki/testdata/path_builder_unittest/mtc_plants04/leaf.pem \ + pki/testdata/path_builder_unittest/mtc_plants04/mtc-ica.pem \ + pki/testdata/path_builder_unittest/mtc_plants04/mtc-leaf.pem \ pki/testdata/path_builder_unittest/multi-root-A-by-B.pem \ pki/testdata/path_builder_unittest/multi-root-B-by-C.pem \ pki/testdata/path_builder_unittest/multi-root-B-by-F.pem \
diff --git a/pki/path_builder_unittest.cc b/pki/path_builder_unittest.cc index ee1a852..f83af58 100644 --- a/pki/path_builder_unittest.cc +++ b/pki/path_builder_unittest.cc
@@ -2452,6 +2452,133 @@ EXPECT_EQ(0xB33F, data->value); } +class PathBuilderMTCPlants04Test : public PathBuilderSimpleChainTest { + public: + PathBuilderMTCPlants04Test() = default; + + protected: + void SetUp() override { + PathBuilderSimpleChainTest::SetUp(); + + // Set up the MTCAnchor. + std::string subtree_hash; + ASSERT_TRUE(string_util::Base64Decode( + "ir6iVcgTrbuHdXB3lKGny6VKSlE1OB5Q+LrtdFH+qIE=", &subtree_hash)); + TrustedSubtree subtree; + ASSERT_EQ(subtree_hash.size(), subtree.hash.size()); + memcpy(subtree.hash.data(), subtree_hash.data(), subtree_hash.size()); + subtree.range.start = 0; + subtree.range.end = 10; + std::map<uint16_t, std::vector<TrustedSubtree>> subtrees; + subtrees[1] = {std::move(subtree)}; + static constexpr uint8_t ca_id[] = {0x81, 0xfd, 0x59, 0x01}; + mtc_anchor_ = + std::make_shared<MTCAnchor>(MakeSpan(ca_id), std::move(subtrees)); + ASSERT_EQ(mtc_anchor_->spec_version(), MTCAnchor::kPlants04); + } + + CertPathBuilder::Result RunPathBuilder( + const std::shared_ptr<const ParsedCertificate> &leaf, + TrustStoreInMemory *trust_store, CertIssuerSource *intermediates, + CertPathBuilderDelegate *delegate) { + SimplePathBuilderDelegate default_delegate( + 2048, SimplePathBuilderDelegate::DigestPolicy::kStrong); + if (!delegate) { + delegate = &default_delegate; + } + + CertPathBuilder path_builder( + leaf, trust_store, delegate, leaf->tbs().validity_not_before, + KeyPurpose::ANY_EKU, InitialExplicitPolicy::kFalse, + {der::Input(kAnyPolicyOid)}, InitialPolicyMappingInhibit::kFalse, + InitialAnyPolicyInhibit::kFalse); + if (intermediates) { + path_builder.AddCertIssuerSource(intermediates); + } + return path_builder.Run(); + } + + std::shared_ptr<MTCAnchor> mtc_anchor_; +}; + +TEST_F(PathBuilderMTCPlants04Test, CheckPathAfterVerification) { + // Set up MTC leaf and its trust anchor. + std::shared_ptr<const ParsedCertificate> mtc_leaf; + ASSERT_TRUE(ReadTestCert("mtc_plants04/mtc-leaf.pem", &mtc_leaf)); + TrustStoreInMemory in_memory; + ASSERT_TRUE(in_memory.AddMTCTrustAnchor(mtc_anchor_)); + + // Check that the path is valid with no delegate. + CertPathBuilder::Result result = + RunPathBuilder(mtc_leaf, &in_memory, nullptr, nullptr); + ASSERT_TRUE(result.HasValidPath()); + + // Check that verification fails when the delegate adds an error. + AddOtherErrorPathBuilderDelegate delegate; + result = RunPathBuilder(mtc_leaf, &in_memory, nullptr, &delegate); + ASSERT_FALSE(result.HasValidPath()); + + ASSERT_LT(result.best_result_index, result.paths.size()); + const CertPathBuilderResultPath *failed_path = + result.paths[result.best_result_index].get(); + ASSERT_TRUE(failed_path); + + // An error should have been added to other errors + const CertErrors *other_errors = failed_path->errors.GetOtherErrors(); + ASSERT_TRUE(other_errors); + EXPECT_TRUE(other_errors->ContainsError(kErrorFromDelegate)); + + // The newly defined delegate error should map to VERIFICATION_FAILURE + // since the error is not associated to a certificate. + VerifyError error = result.GetBestPathVerifyError(); + ASSERT_EQ(error.Code(), VerifyError::StatusCode::VERIFICATION_FAILURE) + << error.DiagnosticString(); +} + +TEST_F(PathBuilderMTCPlants04Test, PathLength) { + std::shared_ptr<const ParsedCertificate> leaf; + ASSERT_TRUE(ReadTestCert("mtc_plants04/leaf.pem", &leaf)); + std::shared_ptr<const ParsedCertificate> ica; + ASSERT_TRUE(ReadTestCert("mtc_plants04/mtc-ica.pem", &ica)); + + // Test that verifying leaf succeeds using ica as the trusted root. + { + TrustStoreInMemory in_memory; + in_memory.AddTrustAnchor(ica); + CertPathBuilder::Result result = + RunPathBuilder(leaf, &in_memory, nullptr, nullptr); + EXPECT_TRUE(result.HasValidPath()); + } + + // Test that verifying ica (as a leaf) succeeds using the MTC trust anchor. + { + TrustStoreInMemory in_memory; + ASSERT_TRUE(in_memory.AddMTCTrustAnchor(mtc_anchor_)); + CertPathBuilder::Result result = + RunPathBuilder(ica, &in_memory, nullptr, nullptr); + EXPECT_TRUE(result.HasValidPath()); + } + + // Test that verifying leaf fails when using the MTC trust anchor. + { + TrustStoreInMemory in_memory; + ASSERT_TRUE(in_memory.AddMTCTrustAnchor(mtc_anchor_)); + CertIssuerSourceStatic intermediates; + intermediates.AddCert(ica); + CertPathBuilder::Result result = + RunPathBuilder(leaf, &in_memory, &intermediates, nullptr); + EXPECT_FALSE(result.HasValidPath()); + VerifyError error = result.GetBestPathVerifyError(); + EXPECT_EQ(error.Code(), VerifyError::StatusCode::PATH_NOT_FOUND) + << error.DiagnosticString(); + const auto &path = *result.GetBestPathPossiblyInvalid(); + ASSERT_EQ(3u, path.certs.size()); + EXPECT_EQ(leaf, path.certs[0]); + EXPECT_EQ(ica, path.certs[1]); + EXPECT_EQ(mtc_anchor_->AsCert(), path.certs[2]); + } +} + class PathBuilderMTCTest : public PathBuilderSimpleChainTest { public: PathBuilderMTCTest() = default; @@ -2473,6 +2600,7 @@ static const uint8_t log_id[] = {0x81, 0xfd, 0x59, 0x01}; mtc_anchor_ = std::make_shared<MTCAnchor>(MakeSpan(log_id), MakeSpan(subtrees)); + ASSERT_EQ(mtc_anchor_->spec_version(), MTCAnchor::kDavidben08); } CertPathBuilder::Result RunPathBuilder(
diff --git a/pki/signature_algorithm.h b/pki/signature_algorithm.h index b518392..ba10e2c 100644 --- a/pki/signature_algorithm.h +++ b/pki/signature_algorithm.h
@@ -55,7 +55,9 @@ kRsaPssSha256, kRsaPssSha384, kRsaPssSha512, - kMtcProofDraftDavidben08, + kMtcProofDraftPlants04, + // The MTC draft versions use the same OID, so make them an alias. + kMtcProofDraftDavidben08 = kMtcProofDraftPlants04, kMldsa44, kMldsa65, kMldsa87,
diff --git a/pki/testdata/path_builder_unittest/mtc_plants04/README.md b/pki/testdata/path_builder_unittest/mtc_plants04/README.md new file mode 100644 index 0000000..3eaecee --- /dev/null +++ b/pki/testdata/path_builder_unittest/mtc_plants04/README.md
@@ -0,0 +1,35 @@ +# MTC test certs + +This directory contains the following certs: + +- `mtc-leaf.pem` + - signatureless MTC issued by an MTC CA +- `mtc-ica.pem` + - signatureless MTC issued by the same MTC CA + - its BasicConstraints has `cA=TRUE` +- `leaf.pem` + - classical ECDSA cert (SPKI) with ECDSA `signatureAlgorithm` + - issued by `mtc-ica.pem` + +## (Re)generating test certs + +Generating these certs is done in two steps. + +The first step is to generate a keypair for the ICA and use the private key to +sign the leaf cert: + +1. Run `go run generate_leaf.go` +2. Copy the certificate PEM to `leaf.pem` +3. Copy the ICA SPKI base64 to the first `PublicKey` entry in `mtc-config.json` + +The next step is to generate the MTC representation of the ICA: + +1. Run `mkdir out` +2. Run + `go run github.com/ietf-plants-wg/merkle-tree-certs.git/demo@9029a99bcfa4e91b8b8e9ba646ac386a6e1c208f -config=mtc-config.json -out=out` +3. Move `out/cert_0_0.pem` to `mtc-ica.pem` +4. Move `out/cert_1_0.pem` to `mtc-leaf.pem` +5. Copy the subtree and hash from the command output into + `PathBuilderMTCPlants04Test::SetUp`. +6. Remove other artifacts created by the merkle-tree-certs/demo tool (e.g. + `rm -r out`).
diff --git a/pki/testdata/path_builder_unittest/mtc_plants04/generate_leaf.go b/pki/testdata/path_builder_unittest/mtc_plants04/generate_leaf.go new file mode 100644 index 0000000..b63e467 --- /dev/null +++ b/pki/testdata/path_builder_unittest/mtc_plants04/generate_leaf.go
@@ -0,0 +1,73 @@ +package main + +import ( + "crypto/ecdsa" + "crypto/elliptic" + "crypto/rand" + "crypto/x509" + "crypto/x509/pkix" + "encoding/base64" + "encoding/pem" + "fmt" + "math/big" + "os" + "time" +) + +func do() error { + key, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader) + if err != nil { + return err + } + leafKey, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader) + if err != nil { + return err + } + icaTempl := &x509.Certificate{ + Subject: pkix.Name{CommonName: "ICA 1"}, + IsCA: true, + SerialNumber: big.NewInt(1), + KeyUsage: x509.KeyUsageCertSign | x509.KeyUsageDigitalSignature, + ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth}, + BasicConstraintsValid: true, + NotBefore: time.Date(2020, time.January, 1, 0, 0, 0, 0, time.UTC), + NotAfter: time.Date(2030, time.December, 31, 23, 59, 59, 0, time.UTC), + } + ica, err := x509.CreateCertificate(rand.Reader, icaTempl, icaTempl, key.Public(), key) + if err != nil { + return err + } + icaCert, err := x509.ParseCertificate(ica) + if err != nil { + return err + } + + leafTempl := &x509.Certificate{ + Subject: pkix.Name{CommonName: "Leaf 1"}, + SerialNumber: big.NewInt(1), + KeyUsage: x509.KeyUsageDigitalSignature, + ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth}, + BasicConstraintsValid: true, + NotBefore: time.Date(2020, time.January, 1, 0, 0, 0, 0, time.UTC), + NotAfter: time.Date(2030, time.December, 31, 23, 59, 59, 0, time.UTC), + DNSNames: []string{"example.com"}, + } + + cert, err := x509.CreateCertificate(rand.Reader, leafTempl, icaTempl, leafKey.Public(), key) + if err != nil { + return err + } + + pemBlock := &pem.Block{Type: "CERTIFICATE", Bytes: cert} + pem.Encode(os.Stdout, pemBlock) + ica_spki := base64.StdEncoding.EncodeToString(icaCert.RawSubjectPublicKeyInfo) + fmt.Printf("ICA SPKI: %s\n", ica_spki) + return nil +} + +func main() { + if err := do(); err != nil { + fmt.Fprintf(os.Stderr, "Error: %v", err) + os.Exit(1) + } +}
diff --git a/pki/testdata/path_builder_unittest/mtc_plants04/leaf.pem b/pki/testdata/path_builder_unittest/mtc_plants04/leaf.pem new file mode 100644 index 0000000..ebbd09f --- /dev/null +++ b/pki/testdata/path_builder_unittest/mtc_plants04/leaf.pem
@@ -0,0 +1,10 @@ +-----BEGIN CERTIFICATE----- +MIIBXDCCAQOgAwIBAgIBATAKBggqhkjOPQQDAjAQMQ4wDAYDVQQDEwVJQ0EgMTAe +Fw0yMDAxMDEwMDAwMDBaFw0zMDEyMzEyMzU5NTlaMBExDzANBgNVBAMTBkxlYWYg +MTBZMBMGByqGSM49AgEGCCqGSM49AwEHA0IABHhYrp7qxXYTNqUCG36ka6f7GCgR +fKd0j7dnYU5oDXmFFfzML2c072nZWLCN3touQ8rqWYEvzu0q5FchiXPTi22jTTBL +MA4GA1UdDwEB/wQEAwIHgDATBgNVHSUEDDAKBggrBgEFBQcDATAMBgNVHRMBAf8E +AjAAMBYGA1UdEQQPMA2CC2V4YW1wbGUuY29tMAoGCCqGSM49BAMCA0cAMEQCIFLN +Ah9IrTtUwJuH6NbthNNE3+SxbZLl0wz7PiBgqvmZAiAO/aT4xctesdxvFGC24s2+ +ewkyTiCBiq2bej/yub5bog== +-----END CERTIFICATE-----
diff --git a/pki/testdata/path_builder_unittest/mtc_plants04/mtc-config.json b/pki/testdata/path_builder_unittest/mtc_plants04/mtc-config.json new file mode 100644 index 0000000..8060d9c --- /dev/null +++ b/pki/testdata/path_builder_unittest/mtc_plants04/mtc-config.json
@@ -0,0 +1,62 @@ +{ + "Version": "plants-04", + "ID": "32473.1", + "LogNumber": 1, + "Cosigners": [ + { + "CosignerID": "32473.1", + "SignatureAlgorithm": "mldsa44", + "PrivateKey": "MDQCAQAwCwYJYIZIAWUDBAMRBCKAIAABAgMEBQYHCAkKCwwNDg8QERITFBUWFxgZGhscHR4f" + } + ], + "Entries": [ + { + "PublicKey": "MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEyXLJquJMGPkBDu854pXQbva4Gqn48F6tH5GW9NQpNJCAzCveJVZpSrriRUt6ira3Nags4ZYniXDDa7VkvaMflA==", + "Subject": { + "CommonName": "ICA 1" + }, + "NotBefore": "2020-01-01T00:00:00Z", + "NotAfter": "2030-12-31T23:59:59Z", + "ExtKeyUsage": ["ServerAuth"], + "KeyUsage": ["CertSign"], + "IsCA": true, + "Certificates": [ + { + "SubtreeStart": 0, + "SubtreeEnd": 10 + } + ] + }, + { + "PublicKey": "MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEyxiC16lts+SOL87OwHhGDzK9/dyUd7sM80jAf53VL+AV38HoHKpmA7LJoYLf4yVhcVleNcxufsAfcPueTLX0Vg==", + "NotBefore": "2020-01-01T00:00:00Z", + "NotAfter": "2030-12-31T23:59:59Z", + "DNSNames": [ + "a.example", + "*.blah.a.example" + ], + "KeyUsage": [ + "DigitalSignature" + ], + "ExtKeyUsage": [ + "ServerAuth" + ], + "IsCA": false, + "Certificates": [ + { + "SubtreeStart": 0, + "SubtreeEnd": 10 + } + ] + }, + { + "Repeat": 8, + "Subject": { + "CommonName": "Filler certificates" + }, + "PublicKey": "MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEyxiC16lts+SOL87OwHhGDzK9/dyUd7sM80jAf53VL+AV38HoHKpmA7LJoYLf4yVhcVleNcxufsAfcPueTLX0Vg==", + "NotBefore": "2020-01-01T00:00:00Z", + "NotAfter": "2030-12-31T23:59:59Z" + } + ] +}
diff --git a/pki/testdata/path_builder_unittest/mtc_plants04/mtc-ica.pem b/pki/testdata/path_builder_unittest/mtc_plants04/mtc-ica.pem new file mode 100644 index 0000000..141e3dc --- /dev/null +++ b/pki/testdata/path_builder_unittest/mtc_plants04/mtc-ica.pem
@@ -0,0 +1,11 @@ +-----BEGIN CERTIFICATE----- +MIIBqTCCAQGgAwIBAgIHAQAAAAAAADAMBgorBgEEAYLaSy8AMBkxFzAVBgorBgEE +AYLaSy8BDAczMjQ3My4xMB4XDTIwMDEwMTAwMDAwMFoXDTMwMTIzMTIzNTk1OVow +EDEOMAwGA1UEAxMFSUNBIDEwWTATBgcqhkjOPQIBBggqhkjOPQMBBwNCAATJcsmq +4kwY+QEO7znildBu9rgaqfjwXq0fkZb01Ck0kIDMK94lVmlKuuJFS3qKtrc1qCzh +lieJcMNrtWS9ox+UozswOTAOBgNVHQ8BAf8EBAMCAgQwFgYDVR0lAQH/BAwwCgYI +KwYBBQUHAwEwDwYDVR0TAQH/BAUwAwEB/zAMBgorBgEEAYLaSy8AA4GTAAAAAAAA +AAAAAAAAAAAKAIA5h2IM+KooLFZ06DQOr46/cN+w687YLbzTbiuolmvNCBM4nk7Z +VC430YX0uWLvZnWWQLtKn27NQ5TTBlhBNe2ANgd/I+JGoIgfURbLdN2pIkCalEIJ +1a+2c+oBuQqvCbYTOJ5O2VQuN9GF9Lli72Z1lkC7Sp9uzUOU0wZYQTXtgAAA +-----END CERTIFICATE-----
diff --git a/pki/testdata/path_builder_unittest/mtc_plants04/mtc-leaf.pem b/pki/testdata/path_builder_unittest/mtc_plants04/mtc-leaf.pem new file mode 100644 index 0000000..5e72225 --- /dev/null +++ b/pki/testdata/path_builder_unittest/mtc_plants04/mtc-leaf.pem
@@ -0,0 +1,12 @@ +-----BEGIN CERTIFICATE----- +MIIBwTCCARmgAwIBAgIHAQAAAAAAATAMBgorBgEEAYLaSy8AMBkxFzAVBgorBgEE +AYLaSy8BDAczMjQ3My4xMB4XDTIwMDEwMTAwMDAwMFoXDTMwMTIzMTIzNTk1OVow +ADBZMBMGByqGSM49AgEGCCqGSM49AwEHA0IABMsYgtepbbPkji/OzsB4Rg8yvf3c +lHe7DPNIwH+d1S/gFd/B6ByqZgOyyaGC3+MlYXFZXjXMbn7AH3D7nky19FajYzBh +MA4GA1UdDwEB/wQEAwIHgDAWBgNVHSUBAf8EDDAKBggrBgEFBQcDATApBgNVHREB +Af8EHzAdgglhLmV4YW1wbGWCECouYmxhaC5hLmV4YW1wbGUwDAYDVR0TAQH/BAIw +ADAMBgorBgEEAYLaSy8AA4GTAAAAAAAAAAAAAAAAAAAKAIAZtsOGmmcSEZW9/6yg +EowPe+S3P3kI+Lry2r5fHt8IGxM4nk7ZVC430YX0uWLvZnWWQLtKn27NQ5TTBlhB +Ne2ANgd/I+JGoIgfURbLdN2pIkCalEIJ1a+2c+oBuQqvCbYTOJ5O2VQuN9GF9Lli +72Z1lkC7Sp9uzUOU0wZYQTXtgAAA +-----END CERTIFICATE-----
diff --git a/pki/trust_store.cc b/pki/trust_store.cc index ac9af61..5f8fef5 100644 --- a/pki/trust_store.cc +++ b/pki/trust_store.cc
@@ -15,6 +15,7 @@ #include "trust_store.h" #include <cassert> +#include <cstdint> #include <cstring> #include <optional> @@ -189,21 +190,38 @@ MTCAnchor::MTCAnchor(bssl::Span<const uint8_t> log_id, Span<const TrustedSubtree> trusted_subtrees) - : log_id_(log_id.begin(), log_id.end()), - trusted_subtrees_(trusted_subtrees.begin(), trusted_subtrees.end()) { + : spec_version_(MTCAnchor::MtcSpecVersion::kDavidben08), + ca_id_(log_id.begin(), log_id.end()) { + // For davidben-08 anchors (which don't have multiple logs), stick the + // subtrees into the map with log_number 0. (log_number 0 is invalid in + // plants-04.) + trusted_subtrees_.emplace( + 0, std::vector<TrustedSubtree>(trusted_subtrees.begin(), + trusted_subtrees.end())); CreateSyntheticCert(log_id); } +MTCAnchor::MTCAnchor( + bssl::Span<const uint8_t> ca_id, + std::map<uint16_t, std::vector<TrustedSubtree>> trusted_subtrees) + : spec_version_(MTCAnchor::MtcSpecVersion::kPlants04), + ca_id_(ca_id.begin(), ca_id.end()), + trusted_subtrees_(std::move(trusted_subtrees)) { + CreateSyntheticCert(ca_id); +} + bool MTCAnchor::IsValid() const { if (!synthetic_cert_) { return false; } - Subtree min_subtree; - for (const auto &subtree : trusted_subtrees_) { - if (!subtree.range.IsValid() || subtree.range < min_subtree) { - return false; + for (const auto &[log_number, subtrees] : trusted_subtrees_) { + Subtree min_subtree; + for (const auto &subtree : subtrees) { + if (!subtree.range.IsValid() || subtree.range < min_subtree) { + return false; + } + min_subtree = subtree.range; } - min_subtree = subtree.range; } return true; } @@ -224,18 +242,30 @@ std::optional<TreeHashConstSpan> MTCAnchor::SubtreeHash( Subtree target_range) const { + BSSL_CHECK(spec_version_ == kDavidben08); + return SubtreeHash(0, target_range); +} + +std::optional<TreeHashConstSpan> MTCAnchor::SubtreeHash( + uint16_t log_number, + Subtree target_range) const { + auto subtrees_it = trusted_subtrees_.find(log_number); + if (subtrees_it == trusted_subtrees_.end()) { + return std::nullopt; + } + const auto& subtrees = subtrees_it->second; auto it = std::lower_bound( - trusted_subtrees_.begin(), trusted_subtrees_.end(), target_range, + subtrees.begin(), subtrees.end(), target_range, [](const TrustedSubtree &subtree, Subtree range) -> bool { return subtree.range < range; }); - if (it == trusted_subtrees_.end() || it->range != target_range) { + if (it == subtrees.end() || it->range != target_range) { return std::nullopt; } return it->hash; } -void MTCAnchor::CreateSyntheticCert(bssl::Span<const uint8_t> log_id) { +void MTCAnchor::CreateSyntheticCert(bssl::Span<const uint8_t> ca_id) { bssl::ScopedCBB cbb; CBB cert, tbs_cert, version, validity, subject_seq, subject_set, subject_log, signature; @@ -268,27 +298,27 @@ BSSL_CHECK(CBB_add_asn1(&subject_seq, &subject_set, CBS_ASN1_SET)); BSSL_CHECK(CBB_add_asn1(&subject_set, &subject_log, CBS_ASN1_SEQUENCE)); - // Section 5.2: Use OID 1.3.6.1.4.1.44363.47.1 as the attribute type for the - // log ID's name. Note that this is the early experimentation OID in the + // Section 5.1: Use OID 1.3.6.1.4.1.44363.47.1 as the attribute type for the + // CA ID's name. Note that this is the early experimentation OID in the // draft rather than the real value of `id-rdna-trustAnchorID`. - static uint8_t log_attr_oid[] = {0x2b, 0x06, 0x01, 0x04, 0x01, + static uint8_t tai_attr_oid[] = {0x2b, 0x06, 0x01, 0x04, 0x01, 0x82, 0xda, 0x4b, 0x2f, 0x01}; - BSSL_CHECK(CBB_add_asn1_element(&subject_log, CBS_ASN1_OBJECT, log_attr_oid, - sizeof(log_attr_oid))); + BSSL_CHECK(CBB_add_asn1_element(&subject_log, CBS_ASN1_OBJECT, tai_attr_oid, + sizeof(tai_attr_oid))); - // Section 5.2's note for initial experimentation also says to use UTF8String + // Section 5.1's note for initial experimentation also says to use UTF8String // to represent the attribute's value rather than RELATIVE-OID. - // Convert the relative OID `log_id` to a string. This can fail. - CBS log_id_oid(log_id); - bssl::UniquePtr<char> log_id_text(CBS_asn1_relative_oid_to_text(&log_id_oid)); - if (!log_id_text) { + // Convert the relative OID `ca_id` to a string. This can fail. + CBS ca_id_oid(ca_id); + bssl::UniquePtr<char> ca_id_text(CBS_asn1_relative_oid_to_text(&ca_id_oid)); + if (!ca_id_text) { return; } BSSL_CHECK( CBB_add_asn1_element(&subject_log, CBS_ASN1_UTF8STRING, - reinterpret_cast<const uint8_t *>(log_id_text.get()), - strlen(log_id_text.get()))); + reinterpret_cast<const uint8_t *>(ca_id_text.get()), + strlen(ca_id_text.get()))); // subjectPublicKeyInfo BSSL_CHECK(CBB_add_asn1_element(&tbs_cert, CBS_ASN1_SEQUENCE, nullptr, 0));
diff --git a/pki/trust_store.h b/pki/trust_store.h index 03cacbb..39c74d8 100644 --- a/pki/trust_store.h +++ b/pki/trust_store.h
@@ -143,31 +143,62 @@ class OPENSSL_EXPORT MTCAnchor { public: - // Create an MTCAnchor for a trusted log with `log_id` containing the DER - // encoding of the relative OID of the log's ID. The `trusted_subtrees` must - // be sorted by their subtree ranges. + enum MtcSpecVersion { + // draft-davidben-tls-merkle-tree-certs-08 + kDavidben08, + // draft-ietf-plants-merkle-tree-certs-04 + kPlants04 + }; + // Create an MTCAnchor with spec version kDavidben08 for a trusted log with + // `log_id` containing the DER encoding of the relative OID of the log's ID. + // The `trusted_subtrees` must be sorted by their subtree ranges. MTCAnchor(Span<const uint8_t> log_id, Span<const TrustedSubtree> trusted_subtrees); + // Create an MTCAnchor with spec version kPlants04 for a trusted CA with + // `ca_id` containing the DER encoding of the relative OID of the CA's ID. + // The `trusted_subtrees` must be sorted by their subtree ranges. + MTCAnchor(Span<const uint8_t> ca_id, + std::map<uint16_t, std::vector<TrustedSubtree>> trusted_subtrees); + // Returns whether this MTCAnchor represents a valid anchor. This function // exists because the c'tor inputs could be invalid. bool IsValid() const; - Span<const uint8_t> log_id() const { return log_id_; } + MtcSpecVersion spec_version() const { return spec_version_; } + Span<const uint8_t> log_id() const { + BSSL_CHECK(spec_version_ == kDavidben08); + return ca_id_; + } + Span<const uint8_t> ca_id() const { + BSSL_CHECK(spec_version_ == kPlants04); + return ca_id_; + } // TODO(nharper): Move this function to TrustAnchor. der::Input NormalizedSubject() const; // TODO(nharper): Remove this function in favor of TrustAnchor's version. CertificateTrust CertTrust() const; // TODO(nharper): Move this function to TrustAnchor. std::shared_ptr<const ParsedCertificate> AsCert() const; + + // Only valid for spec version kDavidben08. std::optional<TreeHashConstSpan> SubtreeHash(Subtree target_range) const; + // Only valid for spec version kPlants04. + std::optional<TreeHashConstSpan> SubtreeHash(uint16_t log_number, + Subtree target_range) const; private: - void CreateSyntheticCert(Span<const uint8_t> log_id); + void CreateSyntheticCert(Span<const uint8_t> ca_id); - std::vector<uint8_t> log_id_; + + MtcSpecVersion spec_version_; + // (If spec_version_ == kDavidben08, `ca_id` is actually the log id.) + std::vector<uint8_t> ca_id_; std::shared_ptr<const ParsedCertificate> synthetic_cert_; - std::vector<TrustedSubtree> trusted_subtrees_; + // If spec_version_ == kDavidben08, the 0th entry in the map will have the + // trusted subtrees. Otherwise, this maps from the log_number to the trusted + // subtrees for that log. + std::map<uint16_t, std::vector<TrustedSubtree>> trusted_subtrees_; }; // A TrustAnchor contains information about how a trust anchor is trusted and
diff --git a/pki/verify_certificate_chain.cc b/pki/verify_certificate_chain.cc index ddbc6c1..21322c5 100644 --- a/pki/verify_certificate_chain.cc +++ b/pki/verify_certificate_chain.cc
@@ -19,6 +19,7 @@ #include <openssl/base.h> #include <openssl/bytestring.h> +#include <openssl/digest.h> #include <openssl/mem.h> #include <openssl/sha2.h> #include <openssl/span.h> @@ -1125,8 +1126,8 @@ // This function implements draft-davidben-tls-merkle-tree-certs-08 section 7.2: // Verifying Certificate Signatures. -static bool VerifyMTC(const ParsedCertificate &cert, - const MTCAnchor *mtc_anchor) { +static bool VerifyMTCDraftDavidben08(const ParsedCertificate &cert, + const MTCAnchor *mtc_anchor) { // Step 1: Check that the TBSCertificate's signature field is id-alg-mtcProof // (kMtcProofDraftDavidben08) with omitted parameters. if (cert.signature_algorithm() != @@ -1263,6 +1264,203 @@ expected_subtree_hash->size()) == 0; } +// This function implements draft-ietf-plants-merkle-tree-certs-04 section 7.2: +// Verifying Certificate Signatures. +static bool VerifyMTCDraftPlants04(const ParsedCertificate &cert, + const MTCAnchor *mtc_anchor) { + // Step 1: Check that the TBSCertificate's signature field is id-alg-mtcProof + // (kMtcProofDraftPlants04) with omitted parameters. + if (cert.signature_algorithm() != + SignatureAlgorithm::kMtcProofDraftPlants04) { + // When we parse the signature algorithm, we check that the parameters are + // omitted. + return false; + } + + // Step 2: Decode the signatureValue as an MTCProof. + uint64_t start, end; + CBS extensions, inclusion_proof, signatures; + CBS mtc_proof(cert.signature_value().bytes()); + if (cert.signature_value().unused_bits() != 0 || + !CBS_get_u16_length_prefixed(&mtc_proof, &extensions) || + !CBS_get_u48(&mtc_proof, &start) || !CBS_get_u48(&mtc_proof, &end) || + !CBS_get_u16_length_prefixed(&mtc_proof, &inclusion_proof) || + !CBS_get_u16_length_prefixed(&mtc_proof, &signatures) || + CBS_len(&mtc_proof) != 0) { + return false; + } + + // Step 3: Let serial be the certificate's serial number. If serial is + // negative or greater than 2^64-1, abort this process and fail verification. + uint64_t serial; + if (!der::ParseUint64(cert.tbs().serial_number, &serial)) { + return false; + } + + // Step 4's revocation check is not performed in this function. The caller is + // responsible for performing revocation checks. + + // Step 5: Let index be the least significant 48 bits of serial and let + // log_number be serial >> 48. If log_number is zero, abort this process and + // fail verification. + uint64_t index = serial & ((1ull << 48) - 1); + uint16_t log_number = serial >> 48; + if (log_number == 0) { + return false; + } + + // Step 6: Let log_id be the log ID constructed from the CA ID in issuer and + // the log_number. + // + // TODO(crbug.com/452983502): This step is only used for standalone + // certificate verification, which isn't implemented yet. + + // Steps 7, 8, and 9 are done in a single pass as described in the procedure + // at the end of section 7.2 ("entry_hash can equivalently be computed in a + // single pass"): + // 1. Initialize a hash instance. + bssl::ScopedEVP_MD_CTX entry_hash_ctx; + if (!EVP_DigestInit(entry_hash_ctx.get(), EVP_sha256())) { + return false; + } + + // Write the octet 0x00 to the hash. This is the domain separator for leaf + // nodes. + // (Note: the procedure as defined in plants-04 is missing this step.) + static constexpr uint8_t kDomainSeparator[] = {0x00}; + if (!EVP_DigestUpdate(entry_hash_ctx.get(), kDomainSeparator, + sizeof(kDomainSeparator))) { + return false; + } + + // 2. Write the extensions field from the MTCProof to the hash. + uint8_t extensions_length[2] = { + static_cast<uint8_t>(CBS_len(&extensions) >> 8), + static_cast<uint8_t>(CBS_len(&extensions) & 0xff)}; + if (!EVP_DigestUpdate(entry_hash_ctx.get(), extensions_length, + sizeof(extensions_length)) || + !EVP_DigestUpdate(entry_hash_ctx.get(), CBS_data(&extensions), + CBS_len(&extensions))) { + return false; + } + + // 3. Write the big-endian, two-byte tbs_cert_entry value to the hash. + static constexpr uint8_t kTbsCertEntry[] = {0, 1}; + if (!EVP_DigestUpdate(entry_hash_ctx.get(), kTbsCertEntry, + sizeof(kTbsCertEntry))) { + return false; + } + + // 4. Write the TBSCertificate's `version`, `issuer`, `validity`, and + // `subject` fields to the hash. + // (Note that this is a correction to the instructions in plants-04.) + if (cert.tbs().version != CertificateVersion::V1) { + ScopedCBB version_outer; + CBB version; + if (!CBB_init(version_outer.get(), 0) || + !CBB_add_asn1(version_outer.get(), &version, + CBS_ASN1_CONTEXT_SPECIFIC | CBS_ASN1_CONSTRUCTED | 0) || + !CBB_add_asn1_uint64(&version, + static_cast<uint64_t>(cert.tbs().version)) || + !CBB_flush(version_outer.get()) || + !EVP_DigestUpdate(entry_hash_ctx.get(), CBB_data(version_outer.get()), + CBB_len(version_outer.get()))) { + return false; + } + } + if (!EVP_DigestUpdate(entry_hash_ctx.get(), cert.tbs().issuer_tlv.data(), + cert.tbs().issuer_tlv.size()) || + !EVP_DigestUpdate(entry_hash_ctx.get(), cert.tbs().validity_tlv.data(), + cert.tbs().validity_tlv.size()) || + !EVP_DigestUpdate(entry_hash_ctx.get(), cert.tbs().subject_tlv.data(), + cert.tbs().subject_tlv.size())) { + return false; + } + + // 5. Write the subjectPublicKeyInfo's algorithm field to the hash. + CBS spki(cert.tbs().spki_tlv); + CBS spki_sequence, spki_algorithm_tlv; + if (!CBS_get_asn1(&spki, &spki_sequence, CBS_ASN1_SEQUENCE) || + !CBS_get_asn1_element(&spki_sequence, &spki_algorithm_tlv, + CBS_ASN1_SEQUENCE) || + !EVP_DigestUpdate(entry_hash_ctx.get(), CBS_data(&spki_algorithm_tlv), + CBS_len(&spki_algorithm_tlv))) { + return false; + } + + // 6. Write the octet 0x04 to the hash. This is an OCTET STRING identifier. + // 7. Write the octet L to the hash, where L is the hash length. (This + // assumes L is at most 127.) + static constexpr uint8_t kSpkiHashTagAndLength[] = {CBS_ASN1_OCTETSTRING, + SHA256_DIGEST_LENGTH}; + if (!EVP_DigestUpdate(entry_hash_ctx.get(), kSpkiHashTagAndLength, + sizeof(kSpkiHashTagAndLength))) { + return false; + } + + // 8. Write H to the hash, where H is the hash of the entire + // subjectPublicKeyInfo field. + uint8_t spki_hash[SHA256_DIGEST_LENGTH]; + SHA256(cert.tbs().spki_tlv.data(), cert.tbs().spki_tlv.size(), spki_hash); + if (!EVP_DigestUpdate(entry_hash_ctx.get(), spki_hash, sizeof(spki_hash))) { + return false; + } + + // 9. Write the remainder of the TBSCertificate contents octets to the hash, + // starting just after the subjectPublicKeyInfo field. + if (!EVP_DigestUpdate(entry_hash_ctx.get(), + cert.tbs().bytes_after_spki.data(), + cert.tbs().bytes_after_spki.size())) { + return false; + } + + // 10. Finalize the hash and set entry_hash to the result. + TreeHash entry_hash; + if (!EVP_DigestFinal(entry_hash_ctx.get(), entry_hash.data(), nullptr)) { + return false; + } + + // Step 10. Let expected_subtree_hash be the result of evaluating the + // MTCProof's inclusion_proof + Subtree range{start, end}; + std::optional<TreeHash> expected_subtree_hash = + EvaluateMerkleSubtreeInclusionProof(inclusion_proof, index, entry_hash, + range); + if (!expected_subtree_hash) { + return false; + } + + // Step 11. If log_number, start, and end matches a trusted subtree (Section + // 7.4) for the CA, check that expected_subtree_hash is equal to the trusted + // subtree's hash. + if (!mtc_anchor) { + return false; + } + std::optional<TreeHashConstSpan> trusted_subtree_hash = + mtc_anchor->SubtreeHash(log_number, range); + if (trusted_subtree_hash) { + return CRYPTO_memcmp(expected_subtree_hash->data(), + trusted_subtree_hash->data(), + expected_subtree_hash->size()) == 0; + } + + // TODO(crbug.com/452983502): Step 12 would check the MTCProof's signatures + // if there's no matching trusted subtree. This implementation does not + // support that check yet. + return false; +} + +static bool VerifyMTC(const ParsedCertificate &cert, + const MTCAnchor *mtc_anchor) { + switch (mtc_anchor->spec_version()) { + case MTCAnchor::MtcSpecVersion::kDavidben08: + return VerifyMTCDraftDavidben08(cert, mtc_anchor); + case MTCAnchor::MtcSpecVersion::kPlants04: + return VerifyMTCDraftPlants04(cert, mtc_anchor); + } + return false; +} + void PathVerifier::BasicCertificateProcessing( const ParsedCertificate &cert, bool is_target_cert, bool is_target_cert_issuer, const der::GeneralizedTime &time,