Commit 929a0aa4 authored by eroman's avatar eroman Committed by Commit bot

Add error details to TBSCertificate parsing function and tests.

BUG=634443

Review-Url: https://codereview.chromium.org/2341943002
Cr-Commit-Position: refs/heads/master@{#419363}
parent bc94e6ac
......@@ -30,6 +30,9 @@ DEFINE_CERT_ERROR_ID(
DEFINE_CERT_ERROR_ID(kSignatureValueNotBitString,
"Couldn't read Certificate.signatureValue as BIT STRING");
DEFINE_CERT_ERROR_ID(kUnconsumedDataInsideTbsCertificateSequence,
"Unconsumed data inside TBSCertificate");
// Returns true if |input| is a SEQUENCE and nothing else.
WARN_UNUSED_RESULT bool IsSequenceTLV(const der::Input& input) {
der::Parser parser(input);
......@@ -258,7 +261,16 @@ bool ParseCertificate(const der::Input& certificate_tlv,
// }
bool ParseTbsCertificate(const der::Input& tbs_tlv,
const ParseCertificateOptions& options,
ParsedTbsCertificate* out) {
ParsedTbsCertificate* out,
CertErrors* errors) {
// The rest of this function assumes that |errors| is non-null.
if (!errors) {
CertErrors unused_errors;
return ParseTbsCertificate(tbs_tlv, options, out, &unused_errors);
}
// TODO(crbug.com/634443): Add useful error information to |errors|.
der::Parser parser(tbs_tlv);
// Certificate ::= SEQUENCE {
......@@ -374,8 +386,10 @@ bool ParseTbsCertificate(const der::Input& tbs_tlv,
// However because only v1, v2, and v3 certificates are supported by the
// parsing, there shouldn't be any subsequent data in those versions, so
// reject.
if (tbs_parser.HasMore())
if (tbs_parser.HasMore()) {
errors->AddError(kUnconsumedDataInsideTbsCertificateSequence);
return false;
}
// By definition the input was a single TBSCertificate, so there shouldn't be
// unconsumed data.
......
......@@ -102,6 +102,9 @@ NET_EXPORT bool ParseCertificate(const der::Input& certificate_tlv,
// on success and sets the results in |out|. Certain invalid inputs may
// be accepted based on the provided |options|.
//
// If |errors| was non-null then any warnings/errors that occur during parsing
// are added to it.
//
// Note that on success |out| aliases data from the input |tbs_tlv|.
// Hence the fields of the ParsedTbsCertificate are only valid as long as
// |tbs_tlv| remains valid.
......@@ -129,8 +132,8 @@ NET_EXPORT bool ParseCertificate(const der::Input& certificate_tlv,
// }
NET_EXPORT bool ParseTbsCertificate(const der::Input& tbs_tlv,
const ParseCertificateOptions& options,
ParsedTbsCertificate* out)
WARN_UNUSED_RESULT;
ParsedTbsCertificate* out,
CertErrors* errors) WARN_UNUSED_RESULT;
// Represents a "Version" from RFC 5280:
// Version ::= INTEGER { v1(0), v2(1), v3(2) }
......
......@@ -41,7 +41,7 @@ void ParseCertificateForFuzzer(const der::Input& in) {
SignatureAlgorithm::CreateFromDer(signature_algorithm_tlv));
ParsedTbsCertificate tbs;
if (!ParseTbsCertificate(tbs_certificate_tlv, {}, &tbs))
if (!ParseTbsCertificate(tbs_certificate_tlv, {}, &tbs, &errors))
return;
RDNSequence subject;
......
......@@ -122,7 +122,10 @@ TEST(ParseCertificateTest, AlgorithmNotSequence) {
// Loads tbsCertificate data and expectations from the PEM file |file_name|.
// Verifies that parsing the TBSCertificate succeeds, and each parsed field
// matches the expectations.
void EnsureParsingTbsSucceeds(const std::string& file_name,
//
// TODO(eroman): Get rid of the |expected_version| parameter -- this should be
// encoded in the test expectations file.
void RunTbsCertificateTestGivenVersion(const std::string& file_name,
CertificateVersion expected_version) {
std::string data;
std::string expected_serial_number;
......@@ -135,26 +138,39 @@ void EnsureParsingTbsSucceeds(const std::string& file_name,
std::string expected_issuer_unique_id;
std::string expected_subject_unique_id;
std::string expected_extensions;
std::string expected_errors;
// Read the certificate data and test expectations from a single PEM file.
const PemBlockMapping mappings[] = {
{"TBS CERTIFICATE", &data},
{"SIGNATURE ALGORITHM", &expected_signature_algorithm},
{"SERIAL NUMBER", &expected_serial_number},
{"ISSUER", &expected_issuer},
{"VALIDITY NOTBEFORE", &expected_validity_not_before},
{"VALIDITY NOTAFTER", &expected_validity_not_after},
{"SUBJECT", &expected_subject},
{"SPKI", &expected_spki},
{"SIGNATURE ALGORITHM", &expected_signature_algorithm, true},
{"SERIAL NUMBER", &expected_serial_number, true},
{"ISSUER", &expected_issuer, true},
{"VALIDITY NOTBEFORE", &expected_validity_not_before, true},
{"VALIDITY NOTAFTER", &expected_validity_not_after, true},
{"SUBJECT", &expected_subject, true},
{"SPKI", &expected_spki, true},
{"ISSUER UNIQUE ID", &expected_issuer_unique_id, true},
{"SUBJECT UNIQUE ID", &expected_subject_unique_id, true},
{"EXTENSIONS", &expected_extensions, true},
{"ERRORS", &expected_errors, true},
};
ASSERT_TRUE(ReadTestDataFromPemFile(GetFilePath(file_name), mappings));
std::string test_file_path = GetFilePath(file_name);
ASSERT_TRUE(ReadTestDataFromPemFile(test_file_path, mappings));
bool expected_result = !expected_spki.empty();
// Parsing the TBSCertificate should succeed.
ParsedTbsCertificate parsed;
ASSERT_TRUE(ParseTbsCertificate(der::Input(&data), {}, &parsed));
CertErrors errors;
bool actual_result =
ParseTbsCertificate(der::Input(&data), {}, &parsed, &errors);
EXPECT_EQ(expected_result, actual_result);
EXPECT_EQ(expected_errors, errors.ToDebugString()) << "Test file: "
<< test_file_path;
if (!expected_result || !actual_result)
return;
// Ensure that the ParsedTbsCertificate matches expectations.
EXPECT_EQ(expected_version, parsed.version);
......@@ -184,30 +200,18 @@ void EnsureParsingTbsSucceeds(const std::string& file_name,
EXPECT_EQ(!expected_extensions.empty(), parsed.has_extensions);
}
// Loads certificate data from the PEM file |file_name| and verifies that the
// Certificate parsing succeed, however the TBSCertificate parsing fails.
void EnsureParsingTbsFails(const std::string& file_name) {
std::string data;
const PemBlockMapping mappings[] = {
{"TBS CERTIFICATE", &data},
};
ASSERT_TRUE(ReadTestDataFromPemFile(GetFilePath(file_name), mappings));
// Parsing the TBSCertificate should fail.
ParsedTbsCertificate parsed;
ASSERT_FALSE(ParseTbsCertificate(der::Input(&data), {}, &parsed));
void RunTbsCertificateTest(const std::string& file_name) {
RunTbsCertificateTestGivenVersion(file_name, CertificateVersion::V3);
}
// Tests parsing a TBSCertificate for v3 that contains no optional fields.
TEST(ParseTbsCertificateTest, Version3NoOptionals) {
EnsureParsingTbsSucceeds("tbs_v3_no_optionals.pem", CertificateVersion::V3);
RunTbsCertificateTest("tbs_v3_no_optionals.pem");
}
// Tests parsing a TBSCertificate for v3 that contains extensions.
TEST(ParseTbsCertificateTest, Version3WithExtensions) {
EnsureParsingTbsSucceeds("tbs_v3_extensions.pem", CertificateVersion::V3);
RunTbsCertificateTest("tbs_v3_extensions.pem");
}
// Tests parsing a TBSCertificate for v3 that contains no optional fields, and
......@@ -216,115 +220,111 @@ TEST(ParseTbsCertificateTest, Version3WithExtensions) {
// CAs are not supposed to include negative serial numbers, however RFC 5280
// expects consumers to deal with it anyway).
TEST(ParseTbsCertificateTest, NegativeSerialNumber) {
EnsureParsingTbsSucceeds("tbs_negative_serial_number.pem",
CertificateVersion::V3);
RunTbsCertificateTest("tbs_negative_serial_number.pem");
}
// Tests parsing a TBSCertificate with a serial number that is 21 octets long
// (and the first byte is 0).
TEST(ParseTbCertificateTest, SerialNumber21OctetsLeading0) {
EnsureParsingTbsFails("tbs_serial_number_21_octets_leading_0.pem");
RunTbsCertificateTest("tbs_serial_number_21_octets_leading_0.pem");
}
// Tests parsing a TBSCertificate with a serial number that is 26 octets long
// (and does not contain a leading 0).
TEST(ParseTbsCertificateTest, SerialNumber26Octets) {
EnsureParsingTbsFails("tbs_serial_number_26_octets.pem");
RunTbsCertificateTest("tbs_serial_number_26_octets.pem");
}
// Tests parsing a TBSCertificate which lacks a version number (causing it to
// default to v1).
TEST(ParseTbsCertificateTest, Version1) {
EnsureParsingTbsSucceeds("tbs_v1.pem", CertificateVersion::V1);
RunTbsCertificateTestGivenVersion("tbs_v1.pem", CertificateVersion::V1);
}
// The version was set to v1 explicitly rather than omitting the version field.
TEST(ParseTbsCertificateTest, ExplicitVersion1) {
EnsureParsingTbsFails("tbs_explicit_v1.pem");
RunTbsCertificateTest("tbs_explicit_v1.pem");
}
// Extensions are not defined in version 1.
TEST(ParseTbsCertificateTest, Version1WithExtensions) {
EnsureParsingTbsFails("tbs_v1_extensions.pem");
RunTbsCertificateTest("tbs_v1_extensions.pem");
}
// Extensions are not defined in version 2.
TEST(ParseTbsCertificateTest, Version2WithExtensions) {
EnsureParsingTbsFails("tbs_v2_extensions.pem");
RunTbsCertificateTest("tbs_v2_extensions.pem");
}
// A boring version 2 certificate with none of the optional fields.
TEST(ParseTbsCertificateTest, Version2NoOptionals) {
EnsureParsingTbsSucceeds("tbs_v2_no_optionals.pem", CertificateVersion::V2);
RunTbsCertificateTestGivenVersion("tbs_v2_no_optionals.pem",
CertificateVersion::V2);
}
// A version 2 certificate with an issuer unique ID field.
TEST(ParseTbsCertificateTest, Version2IssuerUniqueId) {
EnsureParsingTbsSucceeds("tbs_v2_issuer_unique_id.pem",
RunTbsCertificateTestGivenVersion("tbs_v2_issuer_unique_id.pem",
CertificateVersion::V2);
}
// A version 2 certificate with both a issuer and subject unique ID field.
TEST(ParseTbsCertificateTest, Version2IssuerAndSubjectUniqueId) {
EnsureParsingTbsSucceeds("tbs_v2_issuer_and_subject_unique_id.pem",
RunTbsCertificateTestGivenVersion("tbs_v2_issuer_and_subject_unique_id.pem",
CertificateVersion::V2);
}
// A version 3 certificate with all of the optional fields (issuer unique id,
// subject unique id, and extensions).
TEST(ParseTbsCertificateTest, Version3AllOptionals) {
EnsureParsingTbsSucceeds("tbs_v3_all_optionals.pem", CertificateVersion::V3);
RunTbsCertificateTest("tbs_v3_all_optionals.pem");
}
// The version was set to v4, which is unrecognized.
TEST(ParseTbsCertificateTest, Version4) {
EnsureParsingTbsFails("tbs_v4.pem");
RunTbsCertificateTest("tbs_v4.pem");
}
// Tests that extraneous data after extensions in a v3 is rejected.
TEST(ParseTbsCertificateTest, Version3DataAfterExtensions) {
EnsureParsingTbsFails("tbs_v3_data_after_extensions.pem");
RunTbsCertificateTest("tbs_v3_data_after_extensions.pem");
}
// Tests using a real-world certificate (whereas the other tests are fabricated
// (and in fact invalid) data.
TEST(ParseTbsCertificateTest, Version3Real) {
EnsureParsingTbsSucceeds("tbs_v3_real.pem", CertificateVersion::V3);
RunTbsCertificateTest("tbs_v3_real.pem");
}
// Parses a TBSCertificate whose "validity" field expresses both notBefore
// and notAfter using UTCTime.
TEST(ParseTbsCertificateTest, ValidityBothUtcTime) {
EnsureParsingTbsSucceeds("tbs_validity_both_utc_time.pem",
CertificateVersion::V3);
RunTbsCertificateTest("tbs_validity_both_utc_time.pem");
}
// Parses a TBSCertificate whose "validity" field expresses both notBefore
// and notAfter using GeneralizedTime.
TEST(ParseTbsCertificateTest, ValidityBothGeneralizedTime) {
EnsureParsingTbsSucceeds("tbs_validity_both_generalized_time.pem",
CertificateVersion::V3);
RunTbsCertificateTest("tbs_validity_both_generalized_time.pem");
}
// Parses a TBSCertificate whose "validity" field expresses notBefore using
// UTCTime and notAfter using GeneralizedTime.
TEST(ParseTbsCertificateTest, ValidityUTCTimeAndGeneralizedTime) {
EnsureParsingTbsSucceeds("tbs_validity_utc_time_and_generalized_time.pem",
CertificateVersion::V3);
RunTbsCertificateTest("tbs_validity_utc_time_and_generalized_time.pem");
}
// Parses a TBSCertificate whose validity" field expresses notBefore using
// GeneralizedTime and notAfter using UTCTime. Also of interest, notBefore >
// notAfter. Parsing will succeed, however no time can satisfy this constraint.
TEST(ParseTbsCertificateTest, ValidityGeneralizedTimeAndUTCTime) {
EnsureParsingTbsSucceeds("tbs_validity_generalized_time_and_utc_time.pem",
CertificateVersion::V3);
RunTbsCertificateTest("tbs_validity_generalized_time_and_utc_time.pem");
}
// Parses a TBSCertificate whose "validity" field does not strictly follow
// the DER rules (and fails to be parsed).
TEST(ParseTbsCertificateTest, ValidityRelaxed) {
EnsureParsingTbsFails("tbs_validity_relaxed.pem");
RunTbsCertificateTest("tbs_validity_relaxed.pem");
}
// Reads a PEM file containing a block "EXTENSION". This input will be
......
......@@ -6,6 +6,7 @@
#include "base/sha1.h"
#include "crypto/sha2.h"
#include "net/cert/internal/cert_errors.h"
#include "net/cert/internal/parse_ocsp.h"
#include "net/der/encode_values.h"
......@@ -500,10 +501,13 @@ bool GetOCSPCertStatus(const OCSPResponseData& response_data,
out->status = OCSPRevocationStatus::GOOD;
ParsedTbsCertificate tbs_cert;
if (!ParseTbsCertificate(cert_tbs_certificate_tlv, {}, &tbs_cert))
// TODO(crbug.com/634443): Propagate the errors.
CertErrors errors;
if (!ParseTbsCertificate(cert_tbs_certificate_tlv, {}, &tbs_cert, &errors))
return false;
ParsedTbsCertificate issuer_tbs_cert;
if (!ParseTbsCertificate(issuer_tbs_certificate_tlv, {}, &issuer_tbs_cert))
if (!ParseTbsCertificate(issuer_tbs_certificate_tlv, {}, &issuer_tbs_cert,
&errors))
return false;
bool found = false;
......
......@@ -98,8 +98,8 @@ scoped_refptr<ParsedCertificate> ParsedCertificate::CreateInternal(
return nullptr;
}
if (!ParseTbsCertificate(result->tbs_certificate_tlv_, options,
&result->tbs_)) {
if (!ParseTbsCertificate(result->tbs_certificate_tlv_, options, &result->tbs_,
errors)) {
return nullptr;
}
......
......@@ -133,7 +133,7 @@ bool ParseCertificateSandboxed(const base::StringPiece& certificate,
ParsedTbsCertificate parsed_tbs_cert;
if (!ParseTbsCertificate(tbs_cert, ParseCertificateOptions(),
&parsed_tbs_cert))
&parsed_tbs_cert, nullptr))
return false;
if (!GetCommonName(parsed_tbs_cert.subject_tlv, subject))
......
......@@ -26,3 +26,9 @@ $ openssl asn1parse -i < [TBS CERTIFICATE]
MEWgAwIBAgIBATADBAEBMAMEAQUwHhcNMTIxMDE4MDMxMjAwWhcNMTMxMDE4MTQ1OTU5WjADBAG
DMAMEAfOjBTADBAHdBQA=
-----END TBS CERTIFICATE-----
[Error] Unconsumed data inside TBSCertificate
-----BEGIN ERRORS-----
W0Vycm9yXSBVbmNvbnN1bWVkIGRhdGEgaW5zaWRlIFRCU0NlcnRpZmljYXRlCg==
-----END ERRORS-----
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment