From 6b937ae3a7a2dfac55d25a18bd6d5a084c24e3d5 Mon Sep 17 00:00:00 2001 From: "Dr. David von Oheimb" Date: Wed, 10 Mar 2021 17:21:37 +0100 Subject: [PATCH] TS ESS: Invert the search logic of ts_check_signing_certs() to correctly cover cert ID list Fixes #14190 Reviewed-by: Tomas Mraz (Merged from https://github.com/openssl/openssl/pull/14503) --- CHANGES.md | 8 ++ crypto/ess/ess_lib.c | 39 +++++++ crypto/ts/ts_rsp_verify.c | 54 ++++------ include/crypto/ess.h | 2 + test/recipes/80-test_tsa.t | 96 +++++++++++++----- test/recipes/80-test_tsa_data/all-zero.tsq | Bin 0 -> 59 bytes test/recipes/80-test_tsa_data/comodo-aaa.pem | 25 +++++ .../80-test_tsa_data/sectigo-all-zero.tsr | Bin 0 -> 4981 bytes .../80-test_tsa_data/sectigo-signer.pem | 40 ++++++++ .../sectigo-time-stamping-ca.pem | 39 +++++++ .../80-test_tsa_data/user-trust-ca-aaa.pem | 32 ++++++ .../80-test_tsa_data/user-trust-ca.pem | 34 +++++++ 12 files changed, 304 insertions(+), 65 deletions(-) create mode 100644 test/recipes/80-test_tsa_data/all-zero.tsq create mode 100644 test/recipes/80-test_tsa_data/comodo-aaa.pem create mode 100644 test/recipes/80-test_tsa_data/sectigo-all-zero.tsr create mode 100644 test/recipes/80-test_tsa_data/sectigo-signer.pem create mode 100644 test/recipes/80-test_tsa_data/sectigo-time-stamping-ca.pem create mode 100644 test/recipes/80-test_tsa_data/user-trust-ca-aaa.pem create mode 100644 test/recipes/80-test_tsa_data/user-trust-ca.pem diff --git a/CHANGES.md b/CHANGES.md index e51e61a96b1..ad6b7edd29e 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -58,6 +58,14 @@ OpenSSL 3.0 *Richard Levitte* + * Improved adherence to Enhanced Security Services (ESS, RFC 2634 and RFC 5035) + for the TSP implementation. + Correct the semantics of checking the validation chain in case ESSCertID{,v2} + contains more than one certificate identifier: This means that all + certificates referenced there MUST be part of the validation chain. + + *David von Oheimb* + * The implementation of the EVP ciphers CAST5-ECB, CAST5-CBC, CAST5-OFB, CAST5-CFB, BF-ECB, BF-CBC, BF-OFB, BF-CFB, IDEA-ECB, IDEC-CBC, IDEA-OFB, IDEA-CFB, SEED-ECB, SEED-CBC, SEED-OFB, SEED-CFB, RC2-ECB, RC2-CBC, diff --git a/crypto/ess/ess_lib.c b/crypto/ess/ess_lib.c index a5cf5d8aa79..7dda6adc98b 100644 --- a/crypto/ess/ess_lib.c +++ b/crypto/ess/ess_lib.c @@ -359,3 +359,42 @@ int ossl_ess_find_cert_v2(const STACK_OF(ESS_CERT_ID_V2) *cert_ids, return -1; } + +/* Returns < 0 if certificate is not found, certificate index otherwise. */ +int ossl_ess_find_cid(const STACK_OF(X509) *certs, + ESS_CERT_ID *cid, ESS_CERT_ID_V2 *cid_v2) +{ + unsigned char cert_digest[EVP_MAX_MD_SIZE]; + unsigned int len, cid_hash_len; + int i; + const ESS_ISSUER_SERIAL *is; + + if (certs == NULL || (cid == NULL && cid_v2 == NULL)) + return -1; + + /* Look for cert with cid in the certs. */ + for (i = 0; i < sk_X509_num(certs); ++i) { + const X509 *cert = sk_X509_value(certs, i); + const EVP_MD *md; + + /* TODO(3.0): fetch sha algorithm from providers */ + if (cid != NULL) + md = EVP_sha1(); + else + md = cid_v2->hash_alg == NULL ? EVP_sha256() : + EVP_get_digestbyobj(cid_v2->hash_alg->algorithm); + cid_hash_len = cid != NULL ? cid->hash->length : cid_v2->hash->length; + if (!X509_digest(cert, md, cert_digest, &len) + || cid_hash_len != len) + return -1; + + if (memcmp(cid != NULL ? cid->hash->data : cid_v2->hash->data, + cert_digest, len) == 0) { + is = cid != NULL ? cid->issuer_serial : cid_v2->issuer_serial; + if (is == NULL || ess_issuer_serial_cmp(is, cert) == 0) + return i; + } + } + + return -1; +} diff --git a/crypto/ts/ts_rsp_verify.c b/crypto/ts/ts_rsp_verify.c index b45ca28b5d6..6884360869a 100644 --- a/crypto/ts/ts_rsp_verify.c +++ b/crypto/ts/ts_rsp_verify.c @@ -206,52 +206,32 @@ static int ts_check_signing_certs(PKCS7_SIGNER_INFO *si, STACK_OF(X509) *chain) { ESS_SIGNING_CERT *ss = ossl_ess_signing_cert_get(si); - STACK_OF(ESS_CERT_ID) *cert_ids = NULL; ESS_SIGNING_CERT_V2 *ssv2 = ossl_ess_signing_cert_v2_get(si); - STACK_OF(ESS_CERT_ID_V2) *cert_ids_v2 = NULL; - X509 *cert; - int i = 0; + int i, j; int ret = 0; + /* + * Check if first ESSCertIDs matches signer cert + * and each further ESSCertIDs matches any cert in the chain. + */ if (ss != NULL) { - cert_ids = ss->cert_ids; - cert = sk_X509_value(chain, 0); - if (ossl_ess_find_cert(cert_ids, cert) != 0) - goto err; - - /* - * Check the other certificates of the chain if there are more than one - * certificate ids in cert_ids. - */ - if (sk_ESS_CERT_ID_num(cert_ids) > 1) { - for (i = 1; i < sk_X509_num(chain); ++i) { - cert = sk_X509_value(chain, i); - if (ossl_ess_find_cert(cert_ids, cert) < 0) - goto err; - } + for (i = 0; i < sk_ESS_CERT_ID_num(ss->cert_ids); i++) { + j = ossl_ess_find_cid(chain, sk_ESS_CERT_ID_value(ss->cert_ids, i), + NULL); + if (j < 0 || (i == 0 && j != 0)) + goto err; } + ret = 1; } else if (ssv2 != NULL) { - cert_ids_v2 = ssv2->cert_ids; - cert = sk_X509_value(chain, 0); - if (ossl_ess_find_cert_v2(cert_ids_v2, cert) != 0) - goto err; - - /* - * Check the other certificates of the chain if there are more than one - * certificate ids in cert_ids. - */ - if (sk_ESS_CERT_ID_V2_num(cert_ids_v2) > 1) { - for (i = 1; i < sk_X509_num(chain); ++i) { - cert = sk_X509_value(chain, i); - if (ossl_ess_find_cert_v2(cert_ids_v2, cert) < 0) - goto err; - } + for (i = 0; i < sk_ESS_CERT_ID_V2_num(ssv2->cert_ids); i++) { + j = ossl_ess_find_cid(chain, NULL, + sk_ESS_CERT_ID_V2_value(ssv2->cert_ids, i)); + if (j < 0 || (i == 0 && j != 0)) + goto err; } - } else { - goto err; + ret = 1; } - ret = 1; err: if (!ret) ERR_raise(ERR_LIB_TS, TS_R_ESS_SIGNING_CERTIFICATE_ERROR); diff --git a/include/crypto/ess.h b/include/crypto/ess.h index 5abd229869c..099e3de9a5a 100644 --- a/include/crypto/ess.h +++ b/include/crypto/ess.h @@ -32,6 +32,8 @@ ESS_SIGNING_CERT_V2 *ossl_ess_signing_cert_v2_new_init(const EVP_MD *hash_alg, int ossl_ess_find_cert_v2(const STACK_OF(ESS_CERT_ID_V2) *cert_ids, const X509 *cert); int ossl_ess_find_cert(const STACK_OF(ESS_CERT_ID) *cert_ids, X509 *cert); +int ossl_ess_find_cid(const STACK_OF(X509) *certs, + ESS_CERT_ID *cid, ESS_CERT_ID_V2 *cid_v2); /*- * IssuerSerial ::= SEQUENCE { diff --git a/test/recipes/80-test_tsa.t b/test/recipes/80-test_tsa.t index 3cb8399c870..6fa005aebc4 100644 --- a/test/recipes/80-test_tsa.t +++ b/test/recipes/80-test_tsa.t @@ -48,40 +48,42 @@ sub create_tsa_cert { "-extfile", $openssl_conf, "-extensions", $EXT]))); } -sub create_time_stamp_response { +sub create_resp { + my $config = shift; + my $chain = shift; my $queryfile = shift; my $outputfile = shift; - my $datafile = shift; - ok(run(app([@REPLY, "-section", "$datafile", - "-queryfile", "$queryfile", "-out", "$outputfile"]))); + ok(run(app([@REPLY, "-section", $config, "-queryfile", $queryfile, + "-chain", $chain, # this overrides "certs" entry in config + "-out", $outputfile]))); } -sub verify_time_stamp_response { +sub verify_ok { + my $datafile = shift; my $queryfile = shift; my $inputfile = shift; - my $datafile = shift; + my $untrustedfile = shift; - ok(run(app([@VERIFY, "-queryfile", "$queryfile", - "-in", "$inputfile", "-CAfile", "tsaca.pem", - "-untrusted", "tsa_cert1.pem"]))); - ok(run(app([@VERIFY, "-data", "$datafile", - "-in", "$inputfile", "-CAfile", "tsaca.pem", - "-untrusted", "tsa_cert1.pem"]))); + ok(run(app([@VERIFY, "-queryfile", $queryfile, "-in", $inputfile, + "-CAfile", "tsaca.pem", "-untrusted", $untrustedfile]))); + ok(run(app([@VERIFY, "-data", $datafile, "-in", $inputfile, + "-CAfile", "tsaca.pem", "-untrusted", $untrustedfile]))); } -sub verify_time_stamp_response_fail { +sub verify_fail { my $queryfile = shift; my $inputfile = shift; + my $untrustedfile = shift; # is needed for resp2, but not for resp1 + my $cafile = shift; - ok(!run(app([@VERIFY, "-queryfile", "$queryfile", - "-in", "$inputfile", "-CAfile", "tsaca.pem", - "-untrusted", "tsa_cert1.pem"]))); + ok(!run(app([@VERIFY, "-queryfile", $queryfile, "-in", $inputfile, + "-untrusted", $untrustedfile, "-CAfile", $cafile]))); } # main functions -plan tests => 20; +plan tests => 27; note "setting up TSA test directory"; indir "tsa" => sub @@ -123,14 +125,19 @@ indir "tsa" => sub 'printing req1.req'); subtest 'generating valid response for req1.req' => sub { - create_time_stamp_response("req1.tsq", "resp1.tsr", "tsa_config1") + create_resp("tsa_config1", "tsaca.pem", "req1.tsq", "resp1.tsr") + }; + + subtest 'generating response with wrong 2nd certid for req1.req' => sub { + create_resp("tsa_config1", "tsa_cert1.pem", "req1.tsq", + "resp1_invalid.tsr") }; ok(run(app([@REPLY, "-in", "resp1.tsr", "-text"])), 'printing response'); subtest 'verifying valid response' => sub { - verify_time_stamp_response("req1.tsq", "resp1.tsr", $testtsa) + verify_ok($testtsa, "req1.tsq", "resp1.tsr", "tsa_cert1.pem") }; skip "failed", 11 @@ -156,7 +163,7 @@ indir "tsa" => sub skip "failed", 8 unless subtest 'generating valid response for req2.req' => sub { - create_time_stamp_response("req2.tsq", "resp2.tsr", "tsa_config1") + create_resp("tsa_config1", "tsaca.pem", "req2.tsq", "resp2.tsr") }; skip "failed", 7 @@ -180,16 +187,32 @@ indir "tsa" => sub ok(run(app([@REPLY, "-in", "resp2.tsr", "-text"])), 'printing response'); - subtest 'verifying valid response' => sub { - verify_time_stamp_response("req2.tsq", "resp2.tsr", $testtsa) + subtest 'verifying valid resp1, wrong untrusted is not used' => sub { + verify_ok($testtsa, "req1.tsq", "resp1.tsr", "tsa_cert2.pem") + }; + + subtest 'verifying invalid resp1 with wrong 2nd certid' => sub { + verify_fail($testtsa, "req1.tsq", "resp1_invalid.tsr", "tsa_cert2.pem") }; - subtest 'verifying response against wrong request, it should fail' => sub { - verify_time_stamp_response_fail("req1.tsq", "resp2.tsr") + subtest 'verifying valid resp2, correct untrusted being used' => sub { + verify_ok($testtsa, "req2.tsq", "resp2.tsr", "tsa_cert1.pem") }; - subtest 'verifying response against wrong request, it should fail' => sub { - verify_time_stamp_response_fail("req2.tsq", "resp1.tsr") + subtest 'verifying resp2 against wrong req1 should fail' => sub { + verify_fail("req1.tsq", "resp2.tsr", "tsa_cert1.pem", "tsaca.pem") + }; + + subtest 'verifying resp1 against wrong req2 should fail' => sub { + verify_fail("req2.tsq", "resp1.tsr", "tsa_cert1.pem", "tsaca.pem") + }; + + subtest 'verifying resp1 using wrong untrusted should fail' => sub { + verify_fail("req2.tsq", "resp2.tsr", "tsa_cert2.pem", "tsaca.pem") + }; + + subtest 'verifying resp1 using wrong root should fail' => sub { + verify_fail("req1.tsq", "resp1.tsr", "tsa_cert1.pem", "tsa_cert1.pem") }; skip "failure", 2 @@ -200,8 +223,25 @@ indir "tsa" => sub ok(run(app([@QUERY, "-in", "req3.tsq", "-text"])), 'printing req3.req'); - subtest 'verifying response against wrong request, it should fail' => sub { - verify_time_stamp_response_fail("req3.tsq", "resp1.tsr") + subtest 'verifying resp1 against wrong req3 should fail' => sub { + verify_fail("req3.tsq", "resp1.tsr", "tsa_cert1.pem", "tsaca.pem") }; } + + # verifying response with two ESSCertIDs, referring to leaf cert + # "sectigo-signer.pem" and intermediate cert "sectigo-time-stamping-ca.pem" + # 1. validation chain contains these certs and root "user-trust-ca.pem" + ok(run(app([@VERIFY, "-no_check_time", + "-queryfile", data_file("all-zero.tsq"), + "-in", data_file("sectigo-all-zero.tsr"), + "-CAfile", data_file("user-trust-ca.pem")])), + "validation with two ESSCertIDs and 3-element chain"); + # 2. validation chain contains these certs, a cross-cert, and different root + ok(run(app([@VERIFY, "-no_check_time", + "-queryfile", data_file("all-zero.tsq"), + "-in", data_file("sectigo-all-zero.tsr"), + "-untrusted", data_file("user-trust-ca-aaa.pem"), + "-CAfile", data_file("comodo-aaa.pem")])), + "validation with two ESSCertIDs and 4-element chain"); + }, create => 1, cleanup => 1 diff --git a/test/recipes/80-test_tsa_data/all-zero.tsq b/test/recipes/80-test_tsa_data/all-zero.tsq new file mode 100644 index 0000000000000000000000000000000000000000..60d9574e61334444a4c3518fafc6a8c415667fe6 GIT binary patch literal 59 lc-k|tWMX7AFf`z0<4kDtU`%CZVPa%uU{PQo02mqn0|1UF1LFVy literal 0 Hc-jL100001 diff --git a/test/recipes/80-test_tsa_data/comodo-aaa.pem b/test/recipes/80-test_tsa_data/comodo-aaa.pem new file mode 100644 index 00000000000..33c71ba9db7 --- /dev/null +++ b/test/recipes/80-test_tsa_data/comodo-aaa.pem @@ -0,0 +1,25 @@ +-----BEGIN CERTIFICATE----- +MIIEMjCCAxqgAwIBAgIBATANBgkqhkiG9w0BAQUFADB7MQswCQYDVQQGEwJHQjEb +MBkGA1UECAwSR3JlYXRlciBNYW5jaGVzdGVyMRAwDgYDVQQHDAdTYWxmb3JkMRow +GAYDVQQKDBFDb21vZG8gQ0EgTGltaXRlZDEhMB8GA1UEAwwYQUFBIENlcnRpZmlj +YXRlIFNlcnZpY2VzMB4XDTA0MDEwMTAwMDAwMFoXDTI4MTIzMTIzNTk1OVowezEL +MAkGA1UEBhMCR0IxGzAZBgNVBAgMEkdyZWF0ZXIgTWFuY2hlc3RlcjEQMA4GA1UE +BwwHU2FsZm9yZDEaMBgGA1UECgwRQ29tb2RvIENBIExpbWl0ZWQxITAfBgNVBAMM +GEFBQSBDZXJ0aWZpY2F0ZSBTZXJ2aWNlczCCASIwDQYJKoZIhvcNAQEBBQADggEP +ADCCAQoCggEBAL5AnfRu4ep2hxxNRUSOvkbIgwadwSr+GB+O5AL686tdUIoWMQua +BtDFcCLNSS1UY8y2bmhGC1Pqy0wkwLxyTurxFa70VJoSCsN6sjNg4tqJVfMiWPPe +3M/vg4aijJRPn2jymJBGhCfHdr/jzDUsi14HZGWCwEiwqJH5YZ92IFCokcdmtet4 +YgNW8IoaE+oxox6gmf049vYnMlhvB/VruPsUK6+3qszWY19zjNoFmag4qMsXeDZR +rOme9Hg6jc8P2ULimAyrL58OAd7vn5lJ8S3frHRNG5i1R8XlKdH5kBjHYpy+g8cm +ez6KJcfA3Z3mNWgQIJ2P2N7Sw4ScDV7oL8kCAwEAAaOBwDCBvTAdBgNVHQ4EFgQU +oBEKIz6W8Qfs4q8p74Klf9AwpLQwDgYDVR0PAQH/BAQDAgEGMA8GA1UdEwEB/wQF +MAMBAf8wewYDVR0fBHQwcjA4oDagNIYyaHR0cDovL2NybC5jb21vZG9jYS5jb20v +QUFBQ2VydGlmaWNhdGVTZXJ2aWNlcy5jcmwwNqA0oDKGMGh0dHA6Ly9jcmwuY29t +b2RvLm5ldC9BQUFDZXJ0aWZpY2F0ZVNlcnZpY2VzLmNybDANBgkqhkiG9w0BAQUF +AAOCAQEACFb8AvCb6P+k+tZ7xkSAzk/ExfYAWMymtrwUSWgEdujm7l3sAg9g1o1Q +GE8mTgHj5rCl7r+8dFRBv/38ErjHT1r0iWAFf2C3BUrz9vHCv8S5dIa2LX1rzNLz +Rt0vxuBqw8M0Ayx9lt1awg6nCpnBBYurDC/zXDrPbDdVCYfeU0BsWO/8tqtlbgT2 +G9w84FoVxp7Z8VlIMCFlA2zs6SFz7JsDoeA3raAVGI/6ugLOpyypEBMs1OUIJqsi +l2D4kF501KKaU73yqWjgom7C12yxow+ev+to51byrvLjKzg6CYG1a4XXvi3tPxq3 +smPi9WIsgtRqAEFQ8TmDn5XpNpaYbg== +-----END CERTIFICATE----- diff --git a/test/recipes/80-test_tsa_data/sectigo-all-zero.tsr b/test/recipes/80-test_tsa_data/sectigo-all-zero.tsr new file mode 100644 index 0000000000000000000000000000000000000000..a65f35fceb2ed33a6f3f22e4e10447f09d8038e1 GIT binary patch literal 4981 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