0

Reland "show QcStatements and QWAC policies in certificate viewer"

This is a reland of commit 84ec1e29f6

There are no changes, the original was incorrectly reverted due to a
bot issue.

Original change's description:
> show QcStatements and QWAC policies in certificate viewer
>
> Bug: 392934324
> Change-Id: Icaf6195c093bcb13ef80473887d373afc9824442
> Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/6341247
> Commit-Queue: Matt Mueller <mattm@chromium.org>
> Reviewed-by: Nick Harper <nharper@chromium.org>
> Cr-Commit-Position: refs/heads/main@{#1432499}

Bug: 392934324
Change-Id: I80e64121c7320ee35e251d3637e4545b695cee41
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/6357157
Reviewed-by: Nick Harper <nharper@chromium.org>
Commit-Queue: Nick Harper <nharper@chromium.org>
Auto-Submit: Matt Mueller <mattm@chromium.org>
Cr-Commit-Position: refs/heads/main@{#1432942}
This commit is contained in:
Matt Mueller
2025-03-14 12:45:10 -07:00
committed by Chromium LUCI CQ
parent 30d65af624
commit 986b636ef1
9 changed files with 234 additions and 25 deletions

@ -4434,6 +4434,9 @@ are declared in tools/grit/grit_args.gni.
<message name="IDS_CERT_X509_SCT_LIST" desc="description of extension Signed Certificate Timestamp List">
Signed Certificate Timestamp List
</message>
<message name="IDS_CERT_QC_STATEMENTS" desc="description of extension Qualified Certificate Statements">
Qualified Certificate Statements
</message>
<message name="IDS_CERT_X509_POLICY_MAPPINGS" desc="description of extension Certificate Policy Mappings">
Certificate Policy Mappings
</message>
@ -4593,6 +4596,24 @@ are declared in tools/grit/grit_args.gni.
<message name="IDS_CERT_EKU_OCSP_SIGNING" desc="description of extended key usage Signing OCSP Responses">
Signing OCSP Responses
</message>
<message name="IDS_CERT_POLICY_ETSI_QEVCP_W" desc="description of ETSI qualified website certificates that offer the Extended Validated level of assurance" translateable="false">
ETSI QEVCP-w
</message>
<message name="IDS_CERT_POLICY_ETSI_QNCP_W" desc="description of ETSI qualified website certificates that offer the Organization Validated or Individual Validated level of assurance" translateable="false">
ETSI QNCP-w
</message>
<message name="IDS_CERT_QC_ETSI_QCS_QCCOMPLIANCE" desc="description of ETSI qualified certificate statement compliance" translateable="false">
ETSI QcCompliance
</message>
<message name="IDS_CERT_QC_ETSI_QCS_QCTYPE" desc="description of ETSI qualified certificate statement certificate type" translateable="false">
ETSI QcType
</message>
<message name="IDS_CERT_QC_ETSI_QCT_WEB" desc="description of ETSI qualified certificate statement certificate type for web site authentication" translateable="false">
ETSI qct-web
</message>
<message name="IDS_CERT_EKU_MS_INDIVIDUAL_CODE_SIGNING" desc="description of extended key usage Microsoft Individual Code Signing">
Microsoft Individual Code Signing
</message>

@ -0,0 +1 @@
7e5e937003b674ac37a653e32833421af64419c4

@ -20,6 +20,7 @@
#include "crypto/sha2.h"
#include "net/base/ip_address.h"
#include "net/cert/ct_objects_extractor.h"
#include "net/cert/qwac.h"
#include "net/cert/time_conversions.h"
#include "net/cert/x509_util.h"
#include "third_party/boringssl/src/include/openssl/bn.h"
@ -478,6 +479,7 @@ constexpr auto kOidStringMap = base::MakeFixedFlatMap<bssl::der::Input, int>({
{bssl::der::Input(bssl::kUserNoticeId),
IDS_CERT_PKIX_USER_NOTICE_QUALIFIER},
{bssl::der::Input(net::ct::kEmbeddedSCTOid), IDS_CERT_X509_SCT_LIST},
{bssl::der::Input(net::kQcStatementsOid), IDS_CERT_QC_STATEMENTS},
// Extended Key Usages:
{bssl::der::Input(bssl::kAnyEKU), IDS_CERT_EKU_ANY_EKU},
@ -492,6 +494,16 @@ constexpr auto kOidStringMap = base::MakeFixedFlatMap<bssl::der::Input, int>({
{bssl::der::Input(kNetscapeServerGatedCrypto),
IDS_CERT_EKU_NETSCAPE_INTERNATIONAL_STEP_UP},
// Policies:
{bssl::der::Input(net::kQevcpwOid), IDS_CERT_POLICY_ETSI_QEVCP_W},
{bssl::der::Input(net::kQncpwOid), IDS_CERT_POLICY_ETSI_QNCP_W},
// QcStatements:
{bssl::der::Input(net::kEtsiQcsQcComplianceOid),
IDS_CERT_QC_ETSI_QCS_QCCOMPLIANCE},
{bssl::der::Input(net::kEtsiQcsQcTypeOid), IDS_CERT_QC_ETSI_QCS_QCTYPE},
{bssl::der::Input(net::kEtsiQctWebOid), IDS_CERT_QC_ETSI_QCT_WEB},
// Microsoft oids:
{bssl::der::Input(kMsCertExtCerttype), IDS_CERT_EXT_MS_CERT_TYPE},
{bssl::der::Input(kMsCertsrvCaVersion), IDS_CERT_EXT_MS_CA_VERSION},
@ -1234,6 +1246,42 @@ std::vector<uint8_t> BIGNUMBytes(const BIGNUM* bn) {
return ret;
}
std::optional<std::string> ProcessQcStatements(
bssl::der::Input extension_data) {
std::optional<std::vector<net::QcStatement>> qc_statements =
net::ParseQcStatements(extension_data);
if (!qc_statements.has_value()) {
return std::nullopt;
}
std::string rv;
for (const auto& statement : qc_statements.value()) {
rv += GetOidTextOrNumeric(statement.id);
if (!statement.info.empty()) {
rv += " = ";
// The `statement.info` is dependent on the `id`, so `info` can only be
// processed for known ids. Otherwise the raw bytes of `info` are shown.
if (statement.id == bssl::der::Input(net::kEtsiQcsQcTypeOid)) {
std::optional<std::vector<bssl::der::Input>> qc_types =
net::ParseQcTypeInfo(statement.info);
if (!qc_types.has_value()) {
return std::nullopt;
}
std::string sep;
for (const auto& qc_type_id : qc_types.value()) {
rv += sep;
rv += GetOidTextOrNumeric(qc_type_id);
sep = ", ";
}
} else {
rv += ProcessRawBytes(statement.info);
}
}
rv += "\n";
}
return rv;
}
} // namespace
X509CertificateModel::X509CertificateModel(
@ -1508,6 +1556,9 @@ std::optional<std::string> X509CertificateModel::ProcessExtensionData(
extension.oid == bssl::der::Input(kNetscapeLostPasswordURLOid)) {
return ProcessIA5String(extension.value);
}
if (extension.oid == bssl::der::Input(net::kQcStatementsOid)) {
return ProcessQcStatements(extension.value);
}
// TODO(crbug.com/41395047): SCT
// TODO(mattm): name constraints
// TODO(mattm): policy mappings

@ -11,6 +11,7 @@
#include <string_view>
#include "net/cert/qwac.h"
#include "net/cert/x509_util.h"
#include "net/test/cert_builder.h"
#include "net/test/cert_test_util.h"
@ -732,6 +733,55 @@ TEST_P(X509CertificateModel, SubjectEmptySequence) {
}
}
TEST_P(X509CertificateModel, QcStatements) {
base::FilePath certs_dir = net::GetTestCertsDirectory();
std::unique_ptr<net::CertBuilder> builder =
net::CertBuilder::FromFile(certs_dir.AppendASCII("ok_cert.pem"), nullptr);
ASSERT_TRUE(builder);
// SEQUENCE {
// SEQUENCE {
// OBJECT_IDENTIFIER { 1.3.6.1.5.5.7.11.2 }
// SEQUENCE {
// OBJECT_IDENTIFIER { 0.4.0.194121.1.2 }
// }
// }
// SEQUENCE {
// OBJECT_IDENTIFIER { 0.4.0.1862.1.1 }
// }
// SEQUENCE {
// OBJECT_IDENTIFIER { 0.4.0.1862.1.6 }
// SEQUENCE {
// OBJECT_IDENTIFIER { 0.4.0.1862.1.6.3 }
// OBJECT_IDENTIFIER { 0.4.0.1862.1.6.2 }
// }
// }
// }
constexpr uint8_t kQcStatementsValue[] = {
0x30, 0x3f, 0x30, 0x15, 0x06, 0x08, 0x2b, 0x06, 0x01, 0x05, 0x05,
0x07, 0x0b, 0x02, 0x30, 0x09, 0x06, 0x07, 0x04, 0x00, 0x8b, 0xec,
0x49, 0x01, 0x02, 0x30, 0x08, 0x06, 0x06, 0x04, 0x00, 0x8e, 0x46,
0x01, 0x01, 0x30, 0x1c, 0x06, 0x06, 0x04, 0x00, 0x8e, 0x46, 0x01,
0x06, 0x30, 0x12, 0x06, 0x07, 0x04, 0x00, 0x8e, 0x46, 0x01, 0x06,
0x03, 0x06, 0x07, 0x04, 0x00, 0x8e, 0x46, 0x01, 0x06, 0x02};
builder->SetExtension(bssl::der::Input(net::kQcStatementsOid),
std::string(base::as_string_view(kQcStatementsValue)));
x509_certificate_model::X509CertificateModel model(
bssl::UpRef(builder->GetCertBuffer()), GetParam());
ASSERT_TRUE(model.is_valid());
auto extensions = model.GetExtensions("critical", "notcrit");
auto extension_value =
FindExtension(extensions, "Qualified Certificate Statements");
ASSERT_TRUE(extension_value);
EXPECT_EQ(
"notcrit\n"
"OID.1.3.6.1.5.5.7.11.2 = 30 09 06 07 04 00 8B EC 49 01 02\n"
"ETSI QcCompliance\n"
"ETSI QcType = ETSI qct-web, OID.0.4.0.1862.1.6.2\n",
*extension_value);
}
INSTANTIATE_TEST_SUITE_P(All,
X509CertificateModel,
testing::Values(std::string(),

@ -2620,8 +2620,8 @@ if (!is_cronet_build) {
fuzztests = [
"CookieIndicesFuzzTest.FuzzParseFromHeader",
"QwacFuzzTest.FuzzHasQwacQcStatements",
"QwacFuzzTest.FuzzParseQcStatements",
"QwacFuzzTest.FuzzParseQcTypeInfo",
"X509CertificateFuzzTest.FuzzCreateFromDERCertChain",
]

@ -50,6 +50,29 @@ std::optional<std::vector<QcStatement>> ParseQcStatements(
return results;
}
std::optional<std::vector<bssl::der::Input>> ParseQcTypeInfo(
bssl::der::Input statement_info) {
// QcType::= SEQUENCE OF OBJECT IDENTIFIER (id-etsi-qct-esign |
// id-etsi-qct-eseal | id-etsi-qct-web, ...)
bssl::der::Parser info_parser(statement_info);
bssl::der::Parser qctype_parser;
if (!info_parser.ReadSequence(&qctype_parser)) {
return std::nullopt;
}
std::vector<bssl::der::Input> results;
while (qctype_parser.HasMore()) {
bssl::der::Input qctype_id;
if (!qctype_parser.ReadTag(CBS_ASN1_OBJECT, &qctype_id)) {
return std::nullopt;
}
results.push_back(qctype_id);
}
if (info_parser.HasMore()) {
return std::nullopt;
}
return results;
}
bool HasQwacQcStatements(const std::vector<QcStatement>& qc_statements) {
// ETSI TS 119 411-5 - V2.1.1 - section 6.1.2:
// the QWAC includes QCStatements as specified in clause 4.2 of ETSI EN 319
@ -84,19 +107,13 @@ bool HasQwacQcStatements(const std::vector<QcStatement>& qc_statements) {
if (statement.id == bssl::der::Input(kEtsiQcsQcComplianceOid)) {
has_qc_compliance = true;
} else if (statement.id == bssl::der::Input(kEtsiQcsQcTypeOid)) {
// QcType::= SEQUENCE OF OBJECT IDENTIFIER (id-etsi-qct-esign |
// id-etsi-qct-eseal | id-etsi-qct-web, ...)
bssl::der::Parser info_parser(statement.info);
bssl::der::Parser qctype_parser;
if (!info_parser.ReadSequence(&qctype_parser)) {
std::optional<std::vector<bssl::der::Input>> qc_types =
ParseQcTypeInfo(statement.info);
if (!qc_types.has_value()) {
return false;
}
while (qctype_parser.HasMore()) {
bssl::der::Input qctype_id;
if (!qctype_parser.ReadTag(CBS_ASN1_OBJECT, &qctype_id)) {
return false;
}
if (qctype_id == bssl::der::Input(kEtsiQctWebOid)) {
for (const auto& qc_type_id : qc_types.value()) {
if (qc_type_id == bssl::der::Input(kEtsiQctWebOid)) {
has_qctype_web = true;
}
}

@ -94,6 +94,11 @@ NET_EXPORT
std::optional<std::vector<QcStatement>> ParseQcStatements(
bssl::der::Input extension_value);
// Parses the statementInfo of a etsi-qcs-QcType statement. Returns a vector of
// the OID values, or nullopt on error.
NET_EXPORT std::optional<std::vector<bssl::der::Input>> ParseQcTypeInfo(
bssl::der::Input statement_info);
// Returns true if the given QcStatements extension (as returned by
// ParseQcStatements) indicates the certificate is a QWAC.
NET_EXPORT_PRIVATE bool HasQwacQcStatements(

@ -80,22 +80,32 @@ FUZZ_TEST(QwacFuzzTest, FuzzParseQcStatements)
base::ToVector(kQcStatementsValue),
});
void FuzzHasQwacQcStatements(
const std::vector<std::pair<std::vector<uint8_t>, std::vector<uint8_t>>>&
qc_statements_input) {
std::vector<QcStatement> qc_statements;
for (const auto& statement : qc_statements_input) {
qc_statements.emplace_back(bssl::der::Input(statement.first),
bssl::der::Input(statement.second));
void FuzzParseQcTypeInfo(std::vector<uint8_t> statement_info_value) {
const auto parsed = ParseQcTypeInfo(bssl::der::Input(statement_info_value));
if (!parsed.has_value()) {
return;
}
for (const auto& qc_type : parsed.value()) {
ASSERT_TRUE(IsSubSpan(qc_type, statement_info_value));
}
std::ignore = HasQwacQcStatements(qc_statements);
}
// TODO(crbug.com/392931068): Use initial seeds and/or seeded domains?
// TODO(crbug.com/392931068): not sure how useful this fuzzer actually is.
// Maybe refactor things (extract the QcType parsing into a separate function?)
// so that the fuzzer can be more focused?
FUZZ_TEST(QwacFuzzTest, FuzzHasQwacQcStatements);
// SEQUENCE {
// OBJECT_IDENTIFIER { 1.2.3 }
// OBJECT_IDENTIFIER { 2.1.6 }
// OBJECT_IDENTIFIER { 1.2.4 }
// }
constexpr uint8_t kQcTypeInfoMultiple[] = {0x30, 0x0c, 0x06, 0x02, 0x2a,
0x03, 0x06, 0x02, 0x51, 0x06,
0x06, 0x02, 0x2a, 0x04};
FUZZ_TEST(QwacFuzzTest, FuzzParseQcTypeInfo)
.WithSeeds({
base::ToVector(kEmptySequence),
base::ToVector(kInvalidStatementSequence),
base::ToVector(kInvalidStatementOid),
base::ToVector(kQcTypeInfoMultiple),
});
} // namespace

@ -85,6 +85,60 @@ TEST(HasQwacQcStatements, Empty) {
EXPECT_FALSE(HasQwacQcStatements({}));
}
TEST(ParseQcTypeInfo, InvalidSequence) {
EXPECT_EQ(std::nullopt, ParseQcTypeInfo(bssl::der::Input("invalid")));
}
TEST(ParseQcTypeInfo, EmptySequence) {
constexpr uint8_t kEmptySequence[] = {0x30, 0x0};
// An empty QCTypeInfo sequence doesn't really make sense, but the spec does
// not specify that the sequence must be non-empty, so we allow it.
auto r = ParseQcTypeInfo(bssl::der::Input(kEmptySequence));
ASSERT_TRUE(r.has_value());
EXPECT_THAT(r.value(), testing::IsEmpty());
}
TEST(ParseQcTypeInfo, SingleValue) {
// SEQUENCE { OBJECT_IDENTIFIER { 1.2.3 } }
constexpr uint8_t kQcTypeInfo[] = {0x30, 0x04, 0x06, 0x02, 0x2a, 0x03};
constexpr uint8_t kOid[] = {0x2a, 0x03};
auto r = ParseQcTypeInfo(bssl::der::Input(kQcTypeInfo));
ASSERT_TRUE(r.has_value());
EXPECT_THAT(r.value(), testing::ElementsAre(bssl::der::Input(kOid)));
}
TEST(ParseQcTypeInfo, MultipleValues) {
// SEQUENCE {
// OBJECT_IDENTIFIER { 1.2.3 }
// OBJECT_IDENTIFIER { 2.1.6 }
// OBJECT_IDENTIFIER { 1.2.4 }
// }
constexpr uint8_t kQcTypeInfo[] = {0x30, 0x0c, 0x06, 0x02, 0x2a, 0x03, 0x06,
0x02, 0x51, 0x06, 0x06, 0x02, 0x2a, 0x04};
constexpr uint8_t kOid1[] = {0x2a, 0x03};
constexpr uint8_t kOid2[] = {0x51, 0x06};
constexpr uint8_t kOid3[] = {0x2a, 0x04};
auto r = ParseQcTypeInfo(bssl::der::Input(kQcTypeInfo));
ASSERT_TRUE(r.has_value());
EXPECT_THAT(r.value(), testing::ElementsAre(bssl::der::Input(kOid1),
bssl::der::Input(kOid2),
bssl::der::Input(kOid3)));
}
TEST(ParseQcTypeInfo, InvalidTrailingData) {
// SEQUENCE { OBJECT_IDENTIFIER { 1.2.3 } } SEQUENCE { }
constexpr uint8_t kInvalid[] = {0x30, 0x04, 0x06, 0x02,
0x2a, 0x03, 0x30, 0x0};
EXPECT_EQ(std::nullopt, ParseQcTypeInfo(bssl::der::Input(kInvalid)));
}
TEST(ParseQcTypeInfo, InvalidStatementOid) {
// SEQUENCE { SEQUENCE { SEQUENCE { OBJECT_IDENTIFIER { 1.2.4 } } } }
constexpr uint8_t kInvalid[] = {0x30, 0x08, 0x30, 0x06, 0x30,
0x04, 0x06, 0x02, 0x2a, 0x04};
EXPECT_EQ(std::nullopt, ParseQcTypeInfo(bssl::der::Input(kInvalid)));
}
// A valid QcStatement which has a id-etsi-qcs-QcCompliance statement and a
// id-etsi-qcs-QcType statement which contains id-etsi-qct-web. Is a
// QwacQcStatement.