From e85c3d250f4fd7741ba3bb3e212e600915eedaa1 Mon Sep 17 00:00:00 2001 From: Dana Keeler Date: Tue, 29 Jul 2025 20:16:42 +0000 Subject: [PATCH] Bug 1930690 - certificate transparency: support static-ct-api ("tiled") logs a=diannaS Differential Revision: https://phabricator.services.mozilla.com/D258972 --- security/certverifier/CertVerifier.cpp | 3 +- security/ct/CTKnownLogs.h | 164 ++++++++++++------ security/ct/CTLogVerifier.cpp | 3 +- security/ct/CTLogVerifier.h | 5 +- security/ct/CTPolicyEnforcer.cpp | 27 ++- security/ct/CTSerialization.cpp | 5 + security/ct/CTVerifyResult.cpp | 3 +- security/ct/CTVerifyResult.h | 3 +- security/ct/MultiLogCTVerifier.cpp | 3 +- security/ct/SignedCertificateTimestamp.cpp | 63 +++++++ security/ct/SignedCertificateTimestamp.h | 6 + security/ct/tests/gtest/CTLogVerifierTest.cpp | 5 +- .../ct/tests/gtest/CTObjectsExtractorTest.cpp | 3 +- .../ct/tests/gtest/CTPolicyEnforcerTest.cpp | 92 +++++++++- .../ct/tests/gtest/CTSerializationTest.cpp | 84 +++++++++ security/ct/tests/gtest/CTTestUtils.cpp | 70 ++++++++ security/ct/tests/gtest/CTTestUtils.h | 7 +- .../ct/tests/gtest/MultiLogCTVerifierTest.cpp | 6 +- security/manager/ssl/tests/unit/test_ct.js | 6 + .../test_ct/ct-tiled-valid.example.com.pem | 34 ++++ .../ct-tiled-valid.example.com.pem.certspec | 4 + .../test_ct/ct-unknown-log.example.com.pem | 45 +++-- .../ct-unknown-log.example.com.pem.certspec | 2 +- security/manager/tools/log_list.json | 4 +- security/manager/tools/pycert.py | 30 ++-- security/manager/tools/pyct.py | 52 ++++-- .../scripts/getCTKnownLogs.py | 131 +++++++++----- 27 files changed, 691 insertions(+), 169 deletions(-) create mode 100644 security/manager/ssl/tests/unit/test_ct/ct-tiled-valid.example.com.pem create mode 100644 security/manager/ssl/tests/unit/test_ct/ct-tiled-valid.example.com.pem.certspec diff --git a/security/certverifier/CertVerifier.cpp b/security/certverifier/CertVerifier.cpp index 59f72958d5c7..ca330770fbbd 100644 --- a/security/certverifier/CertVerifier.cpp +++ b/security/certverifier/CertVerifier.cpp @@ -219,7 +219,8 @@ void CertVerifier::LoadKnownCTLogs() { const CTLogOperatorInfo& logOperator = kCTLogOperatorList[log.operatorIndex]; - CTLogVerifier logVerifier(logOperator.id, log.state, log.timestamp); + CTLogVerifier logVerifier(logOperator.id, log.state, log.format, + log.timestamp); rv = logVerifier.Init(publicKey); if (rv != Success) { MOZ_ASSERT_UNREACHABLE("Failed initializing a known CT Log"); diff --git a/security/ct/CTKnownLogs.h b/security/ct/CTKnownLogs.h index 33d3c716eac9..fd3b8caf1ea4 100644 --- a/security/ct/CTKnownLogs.h +++ b/security/ct/CTKnownLogs.h @@ -14,7 +14,7 @@ #include -static const PRTime kCTExpirationTime = INT64_C(1759749898000000); +static const PRTime kCTExpirationTime = INT64_C(1759796803000000); namespace mozilla::ct { @@ -23,10 +23,16 @@ enum class CTLogState { Retired, }; +enum class CTLogFormat { + RFC6962, + Tiled, +}; + struct CTLogInfo { // See bug 1338873 about making these fields const. const char* name; CTLogState state; + CTLogFormat format; uint64_t timestamp; // Index within kCTLogOperatorList. size_t operatorIndex; @@ -41,7 +47,7 @@ struct CTLogOperatorInfo { }; const CTLogInfo kCTLogList[] = { - {"Google 'Argon2025h1' log", CTLogState::Admissible, + {"Google 'Argon2025h1' log", CTLogState::Admissible, CTLogFormat::RFC6962, 1701000000000, // 2023-11-26T12:00:00Z 0, // operated by Google "\x30\x59\x30\x13\x06\x07\x2a\x86\x48\xce\x3d\x02\x01\x06\x08\x2a\x86\x48" @@ -51,7 +57,7 @@ const CTLogInfo kCTLogList[] = { "\xfb\xae\xbe\xc8\x23\x52\x20\x2b\xaa\x44\x05\xfe\x54\xf9\xd5\xf1\x1d\x45" "\x9a", 91}, - {"Google 'Argon2025h2' log", CTLogState::Admissible, + {"Google 'Argon2025h2' log", CTLogState::Admissible, CTLogFormat::RFC6962, 1701000000000, // 2023-11-26T12:00:00Z 0, // operated by Google "\x30\x59\x30\x13\x06\x07\x2a\x86\x48\xce\x3d\x02\x01\x06\x08\x2a\x86\x48" @@ -61,7 +67,7 @@ const CTLogInfo kCTLogList[] = { "\x56\x4e\x98\xe8\xaa\x26\x29\x36\x1e\x28\x60\x6f\xeb\x15\x6e\xf7\x7c\xd0" "\xba", 91}, - {"Google 'Argon2026h1' log", CTLogState::Admissible, + {"Google 'Argon2026h1' log", CTLogState::Admissible, CTLogFormat::RFC6962, 1727734767000, // 2024-09-30T22:19:27Z 0, // operated by Google "\x30\x59\x30\x13\x06\x07\x2a\x86\x48\xce\x3d\x02\x01\x06\x08\x2a\x86\x48" @@ -71,7 +77,7 @@ const CTLogInfo kCTLogList[] = { "\x06\x4f\x64\x58\x75\x14\x5d\x56\x52\xe6\x6a\x2b\x14\x4c\xec\x81\xd1\xea" "\x3e", 91}, - {"Google 'Argon2026h2' log", CTLogState::Admissible, + {"Google 'Argon2026h2' log", CTLogState::Admissible, CTLogFormat::RFC6962, 1727734767000, // 2024-09-30T22:19:27Z 0, // operated by Google "\x30\x59\x30\x13\x06\x07\x2a\x86\x48\xce\x3d\x02\x01\x06\x08\x2a\x86\x48" @@ -81,7 +87,7 @@ const CTLogInfo kCTLogList[] = { "\xf5\x35\x50\x9c\xa1\xd3\x49\x4d\x13\xd5\x3b\x6a\x0e\xea\x45\x9d\x24\x13" "\x22", 91}, - {"Google 'Xenon2025h1' log", CTLogState::Admissible, + {"Google 'Xenon2025h1' log", CTLogState::Admissible, CTLogFormat::RFC6962, 1701000000000, // 2023-11-26T12:00:00Z 0, // operated by Google "\x30\x59\x30\x13\x06\x07\x2a\x86\x48\xce\x3d\x02\x01\x06\x08\x2a\x86\x48" @@ -91,7 +97,7 @@ const CTLogInfo kCTLogList[] = { "\x12\xe6\x54\x78\x50\xdc\xff\x6d\xfd\x1c\xa7\xb6\x3a\x1f\xf9\x26\xa9\x1b" "\xbd", 91}, - {"Google 'Xenon2025h2' log", CTLogState::Admissible, + {"Google 'Xenon2025h2' log", CTLogState::Admissible, CTLogFormat::RFC6962, 1701000000000, // 2023-11-26T12:00:00Z 0, // operated by Google "\x30\x59\x30\x13\x06\x07\x2a\x86\x48\xce\x3d\x02\x01\x06\x08\x2a\x86\x48" @@ -101,7 +107,7 @@ const CTLogInfo kCTLogList[] = { "\xa0\x3e\x9d\x94\x1c\xb2\xb7\x4a\xf2\x51\xec\x40\xed\x62\x47\xa4\x03\x49" "\x86", 91}, - {"Google 'Xenon2026h1' log", CTLogState::Admissible, + {"Google 'Xenon2026h1' log", CTLogState::Admissible, CTLogFormat::RFC6962, 1727734767000, // 2024-09-30T22:19:27Z 0, // operated by Google "\x30\x59\x30\x13\x06\x07\x2a\x86\x48\xce\x3d\x02\x01\x06\x08\x2a\x86\x48" @@ -111,7 +117,7 @@ const CTLogInfo kCTLogList[] = { "\xeb\x03\x0a\x30\xcc\x63\x6c\xd9\x3c\xbe\xf5\x7b\x94\xba\x94\xd3\xbf\x88" "\x4c", 91}, - {"Google 'Xenon2026h2' log", CTLogState::Admissible, + {"Google 'Xenon2026h2' log", CTLogState::Admissible, CTLogFormat::RFC6962, 1727734767000, // 2024-09-30T22:19:27Z 0, // operated by Google "\x30\x59\x30\x13\x06\x07\x2a\x86\x48\xce\x3d\x02\x01\x06\x08\x2a\x86\x48" @@ -121,7 +127,7 @@ const CTLogInfo kCTLogList[] = { "\x8a\x72\x30\x65\x86\x43\x53\xdc\x11\x44\x18\x49\x98\x25\x68\xa7\x3c\x05" "\xbf", 91}, - {"Cloudflare 'Nimbus2025'", CTLogState::Admissible, + {"Cloudflare 'Nimbus2025'", CTLogState::Admissible, CTLogFormat::RFC6962, 1702969200000, // 2023-12-19T07:00:00Z 1, // operated by Cloudflare "\x30\x59\x30\x13\x06\x07\x2a\x86\x48\xce\x3d\x02\x01\x06\x08\x2a\x86\x48" @@ -131,7 +137,7 @@ const CTLogInfo kCTLogList[] = { "\x81\x5b\x4a\x14\x41\xec\xaf\xa9\x5d\x0e\xab\x12\x19\x71\xcd\x43\xef\xbb" "\x97", 91}, - {"Cloudflare 'Nimbus2026'", CTLogState::Admissible, + {"Cloudflare 'Nimbus2026'", CTLogState::Admissible, CTLogFormat::RFC6962, 1731088800000, // 2024-11-08T18:00:00Z 1, // operated by Cloudflare "\x30\x59\x30\x13\x06\x07\x2a\x86\x48\xce\x3d\x02\x01\x06\x08\x2a\x86\x48" @@ -141,7 +147,7 @@ const CTLogInfo kCTLogList[] = { "\xbf\x91\x64\x46\x6e\x0e\x27\x13\xea\xbb\x6f\x46\x27\x58\x86\xef\x40\x21" "\xa3", 91}, - {"DigiCert Yeti2025 Log", CTLogState::Retired, + {"DigiCert Yeti2025 Log", CTLogState::Retired, CTLogFormat::RFC6962, 1753315200000, // 2025-07-24T00:00:00Z 2, // operated by DigiCert "\x30\x59\x30\x13\x06\x07\x2a\x86\x48\xce\x3d\x02\x01\x06\x08\x2a\x86\x48" @@ -151,7 +157,7 @@ const CTLogInfo kCTLogList[] = { "\x95\x24\x7c\xd8\x91\x98\x48\x3b\xf0\xf0\xdf\x21\xf1\xb0\x81\x5a\x59\x25" "\x43", 91}, - {"DigiCert Nessie2025 Log", CTLogState::Retired, + {"DigiCert Nessie2025 Log", CTLogState::Retired, CTLogFormat::RFC6962, 1744758000000, // 2025-04-15T23:00:00Z 2, // operated by DigiCert "\x30\x59\x30\x13\x06\x07\x2a\x86\x48\xce\x3d\x02\x01\x06\x08\x2a\x86\x48" @@ -161,7 +167,7 @@ const CTLogInfo kCTLogList[] = { "\x75\x80\xb7\x53\xa7\x85\xd5\xbc\xab\x47\x06\x55\xdb\xb5\xdf\x88\xa1\x6f" "\x38", 91}, - {"DigiCert 'Wyvern2025h1' Log", CTLogState::Retired, + {"DigiCert 'Wyvern2025h1' Log", CTLogState::Retired, CTLogFormat::RFC6962, 1744670000000, // 2025-04-14T22:33:20Z 2, // operated by DigiCert "\x30\x59\x30\x13\x06\x07\x2a\x86\x48\xce\x3d\x02\x01\x06\x08\x2a\x86\x48" @@ -171,7 +177,7 @@ const CTLogInfo kCTLogList[] = { "\x0d\x96\x58\x44\x9d\x3b\x8a\x80\xc5\xc8\xbe\xe1\x89\x46\x6b\x48\x4c\xd6" "\x09", 91}, - {"DigiCert 'Wyvern2025h2' Log", CTLogState::Admissible, + {"DigiCert 'Wyvern2025h2' Log", CTLogState::Admissible, CTLogFormat::RFC6962, 1724900983000, // 2024-08-29T03:09:43Z 2, // operated by DigiCert "\x30\x59\x30\x13\x06\x07\x2a\x86\x48\xce\x3d\x02\x01\x06\x08\x2a\x86\x48" @@ -181,7 +187,7 @@ const CTLogInfo kCTLogList[] = { "\x1c\x63\xde\x95\xe2\x81\x69\x97\x8d\x1e\xa8\xb7\x66\x51\x25\x75\x4d\x78" "\x2e", 91}, - {"DigiCert 'Wyvern2026h1'", CTLogState::Admissible, + {"DigiCert 'Wyvern2026h1'", CTLogState::Admissible, CTLogFormat::RFC6962, 1731024000000, // 2024-11-08T00:00:00Z 2, // operated by DigiCert "\x30\x59\x30\x13\x06\x07\x2a\x86\x48\xce\x3d\x02\x01\x06\x08\x2a\x86\x48" @@ -191,7 +197,7 @@ const CTLogInfo kCTLogList[] = { "\xd5\xd1\x83\xf8\x7a\xdf\x1e\x07\xbc\x15\xcd\xc0\x4a\xcd\x2a\x31\x71\x07" "\x55", 91}, - {"DigiCert 'Wyvern2026h2'", CTLogState::Admissible, + {"DigiCert 'Wyvern2026h2'", CTLogState::Admissible, CTLogFormat::RFC6962, 1731024000000, // 2024-11-08T00:00:00Z 2, // operated by DigiCert "\x30\x59\x30\x13\x06\x07\x2a\x86\x48\xce\x3d\x02\x01\x06\x08\x2a\x86\x48" @@ -201,7 +207,7 @@ const CTLogInfo kCTLogList[] = { "\xde\x9b\x8c\x13\x92\xb7\xad\x3d\x0f\xa1\x9c\x8f\x48\xce\x74\x27\x18\x23" "\x99", 91}, - {"DigiCert 'Sphinx2025h1' Log", CTLogState::Retired, + {"DigiCert 'Sphinx2025h1' Log", CTLogState::Retired, CTLogFormat::RFC6962, 1744670000000, // 2025-04-14T22:33:20Z 2, // operated by DigiCert "\x30\x59\x30\x13\x06\x07\x2a\x86\x48\xce\x3d\x02\x01\x06\x08\x2a\x86\x48" @@ -211,7 +217,7 @@ const CTLogInfo kCTLogList[] = { "\x45\x31\x17\xd3\x8d\xf2\xe7\xce\x18\x11\x58\x98\x2c\x60\x6f\x58\x20\x36" "\x6e", 91}, - {"DigiCert 'Sphinx2025h2' Log", CTLogState::Admissible, + {"DigiCert 'Sphinx2025h2' Log", CTLogState::Admissible, CTLogFormat::RFC6962, 1724900983000, // 2024-08-29T03:09:43Z 2, // operated by DigiCert "\x30\x59\x30\x13\x06\x07\x2a\x86\x48\xce\x3d\x02\x01\x06\x08\x2a\x86\x48" @@ -221,7 +227,7 @@ const CTLogInfo kCTLogList[] = { "\x1a\x27\x54\x85\x5d\xc1\x7b\x24\xa8\x34\xe3\xcd\x88\xce\xd4\x50\x1b\xbe" "\x69", 91}, - {"DigiCert 'Sphinx2026h1'", CTLogState::Admissible, + {"DigiCert 'Sphinx2026h1'", CTLogState::Admissible, CTLogFormat::RFC6962, 1731024000000, // 2024-11-08T00:00:00Z 2, // operated by DigiCert "\x30\x59\x30\x13\x06\x07\x2a\x86\x48\xce\x3d\x02\x01\x06\x08\x2a\x86\x48" @@ -231,7 +237,7 @@ const CTLogInfo kCTLogList[] = { "\xbc\xc3\x9e\x05\x02\x9a\x08\x01\xb5\x49\x23\x35\xc4\xd3\x50\x2b\x51\xe9" "\xf4", 91}, - {"DigiCert 'Sphinx2026h2'", CTLogState::Admissible, + {"DigiCert 'Sphinx2026h2'", CTLogState::Admissible, CTLogFormat::RFC6962, 1731024000000, // 2024-11-08T00:00:00Z 2, // operated by DigiCert "\x30\x59\x30\x13\x06\x07\x2a\x86\x48\xce\x3d\x02\x01\x06\x08\x2a\x86\x48" @@ -241,7 +247,7 @@ const CTLogInfo kCTLogList[] = { "\x38\x8c\xa4\x38\x2e\xac\x95\x0c\xeb\xed\x4f\x64\xbc\x45\x42\xf7\x06\x7a" "\xcd", 91}, - {"Sectigo 'Sabre2025h1'", CTLogState::Admissible, + {"Sectigo 'Sabre2025h1'", CTLogState::Admissible, CTLogFormat::RFC6962, 1701000000000, // 2023-11-26T12:00:00Z 3, // operated by Sectigo "\x30\x59\x30\x13\x06\x07\x2a\x86\x48\xce\x3d\x02\x01\x06\x08\x2a\x86\x48" @@ -251,7 +257,7 @@ const CTLogInfo kCTLogList[] = { "\x41\xae\x70\xb3\x31\xa2\xe3\xfa\x3d\x5f\x2c\x5d\x04\xcd\xb4\x9d\x55\xab" "\x41", 91}, - {"Sectigo 'Sabre2025h2'", CTLogState::Admissible, + {"Sectigo 'Sabre2025h2'", CTLogState::Admissible, CTLogFormat::RFC6962, 1701000000000, // 2023-11-26T12:00:00Z 3, // operated by Sectigo "\x30\x59\x30\x13\x06\x07\x2a\x86\x48\xce\x3d\x02\x01\x06\x08\x2a\x86\x48" @@ -261,7 +267,7 @@ const CTLogInfo kCTLogList[] = { "\xaf\x89\x8b\xf5\x58\xd8\xba\xeb\x7b\x83\x52\xe9\xf4\xe0\xa5\xcd\xcd\x92" "\xcc", 91}, - {"Sectigo 'Mammoth2025h1'", CTLogState::Admissible, + {"Sectigo 'Mammoth2025h1'", CTLogState::Admissible, CTLogFormat::RFC6962, 1701000000000, // 2023-11-26T12:00:00Z 3, // operated by Sectigo "\x30\x59\x30\x13\x06\x07\x2a\x86\x48\xce\x3d\x02\x01\x06\x08\x2a\x86\x48" @@ -271,7 +277,7 @@ const CTLogInfo kCTLogList[] = { "\x9b\x4e\x72\x62\x4b\x3c\x0c\x32\xdd\x86\xfb\xeb\x3e\x66\xcd\x77\x58\x5b" "\xe5", 91}, - {"Sectigo 'Mammoth2025h2'", CTLogState::Admissible, + {"Sectigo 'Mammoth2025h2'", CTLogState::Admissible, CTLogFormat::RFC6962, 1701000000000, // 2023-11-26T12:00:00Z 3, // operated by Sectigo "\x30\x59\x30\x13\x06\x07\x2a\x86\x48\xce\x3d\x02\x01\x06\x08\x2a\x86\x48" @@ -281,7 +287,7 @@ const CTLogInfo kCTLogList[] = { "\xb1\x15\x67\x66\xa0\x7c\x0b\x5b\x62\x7f\x6c\x9a\x6a\x30\x9b\x68\x02\x16" "\x6f", 91}, - {"Sectigo 'Mammoth2026h1'", CTLogState::Admissible, + {"Sectigo 'Mammoth2026h1'", CTLogState::Admissible, CTLogFormat::RFC6962, 1728925200000, // 2024-10-14T17:00:00Z 3, // operated by Sectigo "\x30\x59\x30\x13\x06\x07\x2a\x86\x48\xce\x3d\x02\x01\x06\x08\x2a\x86\x48" @@ -291,7 +297,7 @@ const CTLogInfo kCTLogList[] = { "\x9b\x50\x28\xe2\x9e\x58\xa5\xa5\xfa\xf9\xe3\xfa\x15\x25\xe3\x14\x13\x32" "\xc4", 91}, - {"Sectigo 'Mammoth2026h2'", CTLogState::Admissible, + {"Sectigo 'Mammoth2026h2'", CTLogState::Admissible, CTLogFormat::RFC6962, 1728925200000, // 2024-10-14T17:00:00Z 3, // operated by Sectigo "\x30\x59\x30\x13\x06\x07\x2a\x86\x48\xce\x3d\x02\x01\x06\x08\x2a\x86\x48" @@ -301,7 +307,7 @@ const CTLogInfo kCTLogList[] = { "\xff\xc2\x97\x71\xe5\x7e\x27\xf5\x72\xb1\x8f\x24\x27\x57\x0a\x0d\x74\xc0" "\xb6", 91}, - {"Sectigo 'Sabre2026h1'", CTLogState::Admissible, + {"Sectigo 'Sabre2026h1'", CTLogState::Admissible, CTLogFormat::RFC6962, 1728925200000, // 2024-10-14T17:00:00Z 3, // operated by Sectigo "\x30\x59\x30\x13\x06\x07\x2a\x86\x48\xce\x3d\x02\x01\x06\x08\x2a\x86\x48" @@ -311,7 +317,7 @@ const CTLogInfo kCTLogList[] = { "\x46\x67\x51\xf0\xde\xb6\xc9\x9e\xaa\xe2\x80\x6d\xce\x25\x81\x34\xd7\x6a" "\x60", 91}, - {"Sectigo 'Sabre2026h2'", CTLogState::Admissible, + {"Sectigo 'Sabre2026h2'", CTLogState::Admissible, CTLogFormat::RFC6962, 1728925200000, // 2024-10-14T17:00:00Z 3, // operated by Sectigo "\x30\x59\x30\x13\x06\x07\x2a\x86\x48\xce\x3d\x02\x01\x06\x08\x2a\x86\x48" @@ -321,7 +327,7 @@ const CTLogInfo kCTLogList[] = { "\x65\x35\x63\xf0\x49\xbe\x72\xd1\xaa\x9d\xaf\x7d\x08\xc4\xb4\x8d\x59\x3d" "\x73", 91}, - {"Sectigo 'Elephant2025h2'", CTLogState::Admissible, + {"Sectigo 'Elephant2025h2'", CTLogState::Admissible, CTLogFormat::RFC6962, 1747100000000, // 2025-05-13T01:33:20Z 3, // operated by Sectigo "\x30\x59\x30\x13\x06\x07\x2a\x86\x48\xce\x3d\x02\x01\x06\x08\x2a\x86\x48" @@ -331,7 +337,7 @@ const CTLogInfo kCTLogList[] = { "\xc3\xc4\x42\xc1\x5b\x0a\x85\x16\xce\xa8\xc1\x0e\xc5\x6e\x10\xda\x9e\x0a" "\x42", 91}, - {"Sectigo 'Elephant2026h1'", CTLogState::Admissible, + {"Sectigo 'Elephant2026h1'", CTLogState::Admissible, CTLogFormat::RFC6962, 1747100000000, // 2025-05-13T01:33:20Z 3, // operated by Sectigo "\x30\x59\x30\x13\x06\x07\x2a\x86\x48\xce\x3d\x02\x01\x06\x08\x2a\x86\x48" @@ -341,7 +347,7 @@ const CTLogInfo kCTLogList[] = { "\xc9\x65\x87\xe0\xd4\x7f\x0c\x22\x5a\xd9\xb0\x2e\x98\x7a\xd7\x25\xd0\x1c" "\x69", 91}, - {"Sectigo 'Elephant2026h2'", CTLogState::Admissible, + {"Sectigo 'Elephant2026h2'", CTLogState::Admissible, CTLogFormat::RFC6962, 1747100000000, // 2025-05-13T01:33:20Z 3, // operated by Sectigo "\x30\x59\x30\x13\x06\x07\x2a\x86\x48\xce\x3d\x02\x01\x06\x08\x2a\x86\x48" @@ -351,7 +357,7 @@ const CTLogInfo kCTLogList[] = { "\x09\xfc\xdd\x0f\xbc\x5c\x56\x39\x90\x62\x96\xed\x35\x48\x71\x44\xc4\x6d" "\x98", 91}, - {"Sectigo 'Elephant2027h1'", CTLogState::Admissible, + {"Sectigo 'Elephant2027h1'", CTLogState::Admissible, CTLogFormat::RFC6962, 1747100000000, // 2025-05-13T01:33:20Z 3, // operated by Sectigo "\x30\x59\x30\x13\x06\x07\x2a\x86\x48\xce\x3d\x02\x01\x06\x08\x2a\x86\x48" @@ -361,7 +367,7 @@ const CTLogInfo kCTLogList[] = { "\xa0\x6a\xda\x7f\xb0\x64\xcc\xa6\x5f\xec\xf0\xbc\x81\x80\x12\x73\x0d\xb0" "\xa0", 91}, - {"Sectigo 'Elephant2027h2'", CTLogState::Admissible, + {"Sectigo 'Elephant2027h2'", CTLogState::Admissible, CTLogFormat::RFC6962, 1747100000000, // 2025-05-13T01:33:20Z 3, // operated by Sectigo "\x30\x59\x30\x13\x06\x07\x2a\x86\x48\xce\x3d\x02\x01\x06\x08\x2a\x86\x48" @@ -371,7 +377,7 @@ const CTLogInfo kCTLogList[] = { "\x1e\xdc\x8a\xec\x20\x61\x7e\x52\x25\x32\x4e\xd3\xd9\x0a\xe7\xe3\x0f\xed" "\xf2", 91}, - {"Sectigo 'Tiger2025h2'", CTLogState::Admissible, + {"Sectigo 'Tiger2025h2'", CTLogState::Admissible, CTLogFormat::RFC6962, 1752066000000, // 2025-07-09T13:00:00Z 3, // operated by Sectigo "\x30\x59\x30\x13\x06\x07\x2a\x86\x48\xce\x3d\x02\x01\x06\x08\x2a\x86\x48" @@ -381,7 +387,7 @@ const CTLogInfo kCTLogList[] = { "\xd8\xa1\x24\x59\x2f\xb8\x4f\xbf\xdb\x60\xe5\xef\xe1\xd0\xcd\xcf\x3a\xc4" "\xc6", 91}, - {"Sectigo 'Tiger2026h1'", CTLogState::Admissible, + {"Sectigo 'Tiger2026h1'", CTLogState::Admissible, CTLogFormat::RFC6962, 1752066000000, // 2025-07-09T13:00:00Z 3, // operated by Sectigo "\x30\x59\x30\x13\x06\x07\x2a\x86\x48\xce\x3d\x02\x01\x06\x08\x2a\x86\x48" @@ -391,7 +397,7 @@ const CTLogInfo kCTLogList[] = { "\xef\xf5\xd0\x63\x03\x85\xb7\xd1\x5e\x6f\xc4\xf0\x41\x6f\xa6\xa9\x73\x79" "\xc6", 91}, - {"Sectigo 'Tiger2026h2'", CTLogState::Admissible, + {"Sectigo 'Tiger2026h2'", CTLogState::Admissible, CTLogFormat::RFC6962, 1752066000000, // 2025-07-09T13:00:00Z 3, // operated by Sectigo "\x30\x59\x30\x13\x06\x07\x2a\x86\x48\xce\x3d\x02\x01\x06\x08\x2a\x86\x48" @@ -401,7 +407,7 @@ const CTLogInfo kCTLogList[] = { "\x08\x44\xb5\x10\xbd\x41\xfb\x60\x44\x0b\xe3\xf9\x6d\x47\xf0\x17\x8e\x25" "\x9c", 91}, - {"Sectigo 'Tiger2027h1'", CTLogState::Admissible, + {"Sectigo 'Tiger2027h1'", CTLogState::Admissible, CTLogFormat::RFC6962, 1752066000000, // 2025-07-09T13:00:00Z 3, // operated by Sectigo "\x30\x59\x30\x13\x06\x07\x2a\x86\x48\xce\x3d\x02\x01\x06\x08\x2a\x86\x48" @@ -411,7 +417,7 @@ const CTLogInfo kCTLogList[] = { "\xdc\xd8\x27\x5c\x35\xf0\x9b\x22\x5c\x17\x04\x58\x81\x53\x81\x32\x16\x98" "\x84", 91}, - {"Sectigo 'Tiger2027h2'", CTLogState::Admissible, + {"Sectigo 'Tiger2027h2'", CTLogState::Admissible, CTLogFormat::RFC6962, 1752066000000, // 2025-07-09T13:00:00Z 3, // operated by Sectigo "\x30\x59\x30\x13\x06\x07\x2a\x86\x48\xce\x3d\x02\x01\x06\x08\x2a\x86\x48" @@ -421,7 +427,7 @@ const CTLogInfo kCTLogList[] = { "\xa3\x4f\x6b\xa3\x37\xdd\xaa\x18\xde\x8a\x12\x25\xdb\x9c\xbd\x03\x72\x61" "\xc9", 91}, - {"Let's Encrypt 'Oak2025h1'", CTLogState::Admissible, + {"Let's Encrypt 'Oak2025h1'", CTLogState::Admissible, CTLogFormat::RFC6962, 1701000000000, // 2023-11-26T12:00:00Z 4, // operated by Let's Encrypt "\x30\x59\x30\x13\x06\x07\x2a\x86\x48\xce\x3d\x02\x01\x06\x08\x2a\x86\x48" @@ -431,7 +437,7 @@ const CTLogInfo kCTLogList[] = { "\xf9\x3c\x58\x54\x5b\x37\x10\xb1\xab\xd8\x83\xfb\x84\xf1\x95\x3f\x2e\x2f" "\x1c", 91}, - {"Let's Encrypt 'Oak2025h2'", CTLogState::Admissible, + {"Let's Encrypt 'Oak2025h2'", CTLogState::Admissible, CTLogFormat::RFC6962, 1701000000000, // 2023-11-26T12:00:00Z 4, // operated by Let's Encrypt "\x30\x59\x30\x13\x06\x07\x2a\x86\x48\xce\x3d\x02\x01\x06\x08\x2a\x86\x48" @@ -441,7 +447,7 @@ const CTLogInfo kCTLogList[] = { "\xc9\xd7\x3d\xbb\xc1\xf7\x71\x86\x69\xf4\xb3\x5f\x90\x09\xaa\xae\xbd\x8d" "\xa9", 91}, - {"Let's Encrypt 'Oak2026h1'", CTLogState::Admissible, + {"Let's Encrypt 'Oak2026h1'", CTLogState::Admissible, CTLogFormat::RFC6962, 1730678400000, // 2024-11-04T00:00:00Z 4, // operated by Let's Encrypt "\x30\x59\x30\x13\x06\x07\x2a\x86\x48\xce\x3d\x02\x01\x06\x08\x2a\x86\x48" @@ -451,7 +457,7 @@ const CTLogInfo kCTLogList[] = { "\x48\x90\x23\x40\xde\x7a\x4d\x89\x32\xfb\xd7\x0a\xeb\x5e\x8c\xa2\xf1\xf6" "\x49", 91}, - {"Let's Encrypt 'Oak2026h2'", CTLogState::Admissible, + {"Let's Encrypt 'Oak2026h2'", CTLogState::Admissible, CTLogFormat::RFC6962, 1730678400000, // 2024-11-04T00:00:00Z 4, // operated by Let's Encrypt "\x30\x59\x30\x13\x06\x07\x2a\x86\x48\xce\x3d\x02\x01\x06\x08\x2a\x86\x48" @@ -461,7 +467,7 @@ const CTLogInfo kCTLogList[] = { "\xf4\xfc\x5c\xa9\x8c\x5f\xfb\x0d\x60\xe4\x2c\x0f\x16\xec\x2a\xb2\x6d\xeb" "\x15", 91}, - {"TrustAsia Log2025a", CTLogState::Admissible, + {"TrustAsia Log2025a", CTLogState::Admissible, CTLogFormat::RFC6962, 1701000000000, // 2023-11-26T12:00:00Z 5, // operated by TrustAsia "\x30\x59\x30\x13\x06\x07\x2a\x86\x48\xce\x3d\x02\x01\x06\x08\x2a\x86\x48" @@ -471,7 +477,7 @@ const CTLogInfo kCTLogList[] = { "\x90\x58\xba\x22\xd5\xf9\xf5\x69\x54\xb7\xa8\x94\x4e\x32\x09\xae\x26\x11" "\x4d", 91}, - {"TrustAsia Log2025b", CTLogState::Admissible, + {"TrustAsia Log2025b", CTLogState::Admissible, CTLogFormat::RFC6962, 1701000000000, // 2023-11-26T12:00:00Z 5, // operated by TrustAsia "\x30\x59\x30\x13\x06\x07\x2a\x86\x48\xce\x3d\x02\x01\x06\x08\x2a\x86\x48" @@ -481,7 +487,7 @@ const CTLogInfo kCTLogList[] = { "\xbd\x2f\xa9\xcf\xe8\x7b\x5e\xe1\x4b\x60\xe5\x38\x43\x60\x97\xc1\x5b\x2f" "\x65", 91}, - {"TrustAsia 'log2026a'", CTLogState::Admissible, + {"TrustAsia 'log2026a'", CTLogState::Admissible, CTLogFormat::RFC6962, 1726790400000, // 2024-09-20T00:00:00Z 5, // operated by TrustAsia "\x30\x59\x30\x13\x06\x07\x2a\x86\x48\xce\x3d\x02\x01\x06\x08\x2a\x86\x48" @@ -491,7 +497,7 @@ const CTLogInfo kCTLogList[] = { "\x89\x17\xe8\x5b\x2e\xc5\xac\x00\x05\xc9\x76\x37\x45\x97\x03\x15\xff\x60" "\x59", 91}, - {"TrustAsia 'log2026b'", CTLogState::Admissible, + {"TrustAsia 'log2026b'", CTLogState::Admissible, CTLogFormat::RFC6962, 1726790400000, // 2024-09-20T00:00:00Z 5, // operated by TrustAsia "\x30\x59\x30\x13\x06\x07\x2a\x86\x48\xce\x3d\x02\x01\x06\x08\x2a\x86\x48" @@ -501,7 +507,7 @@ const CTLogInfo kCTLogList[] = { "\x9d\x7d\x05\x53\xc7\x9e\x94\xea\x9b\x57\x46\xbf\x4f\xa4\x7e\xfb\xdf\xfa" "\x85", 91}, - {"Bogus placeholder log to unbreak misbehaving CT libraries", CTLogState::Retired, + {"Bogus placeholder log to unbreak misbehaving CT libraries", CTLogState::Retired, CTLogFormat::RFC6962, 1750489200000, // 2025-06-21T07:00:00Z 6, // operated by Geomys "\x30\x59\x30\x13\x06\x07\x2a\x86\x48\xce\x3d\x02\x01\x06\x08\x2a\x86\x48" @@ -511,8 +517,38 @@ const CTLogInfo kCTLogList[] = { "\x75\xe3\x66\x75\xa9\x59\x70\x2d\xe2\x5a\x8b\xc0\x7c\x0a\x6f\x5d\x2d\xf7" "\x37", 91}, + {"Geomys 'Tuscolo2025h2'", CTLogState::Admissible, CTLogFormat::Tiled, + 1749782400000, // 2025-06-13T02:40:00Z + 6, // operated by Geomys + "\x30\x59\x30\x13\x06\x07\x2a\x86\x48\xce\x3d\x02\x01\x06\x08\x2a\x86\x48" + "\xce\x3d\x03\x01\x07\x03\x42\x00\x04\x2b\xd7\x78\x18\x6b\x73\x6e\x4c\x30" + "\xb1\x8a\x44\xb6\xf9\xd4\xf4\xa2\xa0\x66\xbe\xbb\x32\xc1\xa5\x07\xb9\xa7" + "\x17\xc0\xd6\xf5\xb1\xe7\x9e\x01\xf3\x3f\x35\xcf\x6d\xda\x3b\x9f\xe1\x72" + "\x0b\x04\x83\x62\xa1\x07\x6d\xa9\x75\x67\x40\x82\x57\x26\x43\xeb\x04\x3f" + "\xa3", + 91}, + {"Geomys 'Tuscolo2026h1'", CTLogState::Admissible, CTLogFormat::Tiled, + 1749782400000, // 2025-06-13T02:40:00Z + 6, // operated by Geomys + "\x30\x59\x30\x13\x06\x07\x2a\x86\x48\xce\x3d\x02\x01\x06\x08\x2a\x86\x48" + "\xce\x3d\x03\x01\x07\x03\x42\x00\x04\x7e\x5c\x73\x32\x0d\x80\x8e\x38\x3b" + "\x87\x5f\x99\x22\xf4\x3d\x2d\x5e\xb2\x16\xf7\x63\xea\xe4\x62\xf5\x86\xef" + "\xb5\x19\xd2\x0a\x04\xb6\x49\xa9\xa4\x1f\x9e\x96\x70\xf4\x5a\x05\x34\x58" + "\x01\x13\xc3\x22\xbe\x49\xc0\xba\xa0\x24\x05\x40\xfc\xdb\xc3\xc5\xd2\x57" + "\x3b", + 91}, + {"Geomys 'Tuscolo2026h2'", CTLogState::Admissible, CTLogFormat::Tiled, + 1749782400000, // 2025-06-13T02:40:00Z + 6, // operated by Geomys + "\x30\x59\x30\x13\x06\x07\x2a\x86\x48\xce\x3d\x02\x01\x06\x08\x2a\x86\x48" + "\xce\x3d\x03\x01\x07\x03\x42\x00\x04\x68\x0e\x8f\xd2\x2e\xc9\x4e\xc7\x7d" + "\x5d\xfc\xc5\xd7\xfe\xfa\x6a\xf4\x56\x03\x75\xd7\x23\x83\x52\xb6\xc1\x4e" + "\xfd\xa1\x6d\x06\x9e\x92\x63\xba\x25\x0c\x43\x22\x3d\x21\x52\xc4\x6c\xc5" + "\x42\x32\x80\xd6\xbf\x6f\x80\x6f\xe8\x15\x0c\x2e\xae\x55\x8c\xcb\xd1\x50" + "\x37", + 91}, #ifdef DEBUG - {"Mozilla Test RSA Log 1", CTLogState::Admissible, + {"Mozilla Test RSA Log 1", CTLogState::Admissible, CTLogFormat::RFC6962, 1721666666000, // 2024-07-22T16:44:26Z 7, // operated by Mozilla Test Org 1 "\x30\x82\x01\x22\x30\x0d\x06\x09\x2a\x86\x48\x86\xf7\x0d\x01\x01\x01\x05" @@ -535,7 +571,7 @@ const CTLogInfo kCTLogList[] = { 294}, #endif // DEBUG #ifdef DEBUG - {"Mozilla Test EC Log", CTLogState::Admissible, + {"Mozilla Test EC Log", CTLogState::Admissible, CTLogFormat::RFC6962, 1721666666000, // 2024-07-22T16:44:26Z 7, // operated by Mozilla Test Org 1 "\x30\x59\x30\x13\x06\x07\x2a\x86\x48\xce\x3d\x02\x01\x06\x08\x2a\x86\x48" @@ -547,7 +583,7 @@ const CTLogInfo kCTLogList[] = { 91}, #endif // DEBUG #ifdef DEBUG - {"Mozilla Test RSA Log 2", CTLogState::Admissible, + {"Mozilla Test RSA Log 2", CTLogState::Admissible, CTLogFormat::RFC6962, 1721666666000, // 2024-07-22T16:44:26Z 8, // operated by Mozilla Test Org 2 "\x30\x82\x01\x22\x30\x0d\x06\x09\x2a\x86\x48\x86\xf7\x0d\x01\x01\x01\x05" @@ -569,6 +605,29 @@ const CTLogInfo kCTLogList[] = { "\xbb\x02\x03\x01\x00\x01", 294}, #endif // DEBUG +#ifdef DEBUG + {"Mozilla Test RSA Log 4", CTLogState::Admissible, CTLogFormat::Tiled, + 1750853366000, // 2025-06-25T12:09:26Z + 9, // operated by Mozilla Test Org 3 + "\x30\x82\x01\x22\x30\x0d\x06\x09\x2a\x86\x48\x86\xf7\x0d\x01\x01\x01\x05" + "\x00\x03\x82\x01\x0f\x00\x30\x82\x01\x0a\x02\x82\x01\x01\x00\xb5\x49\x89" + "\x5c\x9d\x00\x10\x8d\x11\xa1\xf9\x9f\x87\xa9\xe3\xd1\xa5\xdb\x5d\xfa\xec" + "\xf1\x88\xda\x57\xbf\x64\x13\x68\x8f\x2c\xe4\x72\x2c\xff\x10\x90\x38\xc1" + "\x74\x02\xc9\x3a\x2a\x47\x3d\xbd\x28\x6a\xed\x3f\xdc\xd3\x63\xcf\x5a\x29" + "\x14\x77\x01\xbd\xd8\x18\xd7\x61\x5b\xf9\x35\x6b\xd5\xd3\xc8\x33\x6a\xaa" + "\x8c\x09\x71\x36\x8a\x06\xc3\xcd\x44\x61\xb9\x3e\x51\x42\x4e\x17\x44\xbb" + "\x2e\xaa\xd4\x6a\xab\x38\xce\x19\x68\x21\x96\x1f\x87\x71\x4a\x16\x63\x69" + "\x3f\x09\x76\x1c\xdf\x4d\x6b\xa1\x25\xea\xce\xc7\xbe\x27\x0d\x38\x8f\x78" + "\x9f\x6c\xdf\x78\xae\x31\x44\xed\x28\xc4\x5e\x79\x29\x38\x63\xa7\xa2\x2a" + "\x48\x98\x0a\x36\xa4\x0e\x72\xd5\x79\xc9\xb9\x25\xdf\xf8\xc7\x93\x36\x2f" + "\xfd\x68\x97\xa7\xc1\x75\x4c\x5e\x97\xc9\x67\xc3\xea\xdd\x1a\xae\x8a\xa2" + "\xcc\xce\x34\x8a\x01\x69\xb8\x0e\x28\xa2\xd7\x0c\x1a\x96\x0c\x6f\x33\x5f" + "\x2d\xa0\x9b\x9b\x64\x3f\x5a\xbf\xba\x49\xe8\xaa\xa9\x81\xe9\x60\xe2\x7d" + "\x87\x48\x0b\xdd\x55\xdd\x94\x17\xfa\x18\x50\x9f\xbb\x55\x4c\xcf\x81\xa4" + "\x39\x7e\x8b\xa8\x12\x8a\x34\xbd\xf2\x78\x65\xc1\x89\xe5\x73\x4f\xb2\x29" + "\x05\x02\x03\x01\x00\x01", + 294}, +#endif // DEBUG }; const CTLogOperatorInfo kCTLogOperatorList[] = { @@ -585,6 +644,9 @@ const CTLogOperatorInfo kCTLogOperatorList[] = { #ifdef DEBUG {"Mozilla Test Org 2", 8}, #endif // DEBUG +#ifdef DEBUG + {"Mozilla Test Org 3", 9}, +#endif // DEBUG }; } // namespace mozilla::ct diff --git a/security/ct/CTLogVerifier.cpp b/security/ct/CTLogVerifier.cpp index 4ecedea7b8c5..d5e665aacac6 100644 --- a/security/ct/CTLogVerifier.cpp +++ b/security/ct/CTLogVerifier.cpp @@ -114,10 +114,11 @@ class SignatureParamsTrustDomain final : public TrustDomain { }; CTLogVerifier::CTLogVerifier(CTLogOperatorId operatorId, CTLogState state, - uint64_t timestamp) + CTLogFormat format, uint64_t timestamp) : mSignatureAlgorithm(DigitallySigned::SignatureAlgorithm::Anonymous), mOperatorId(operatorId), mState(state), + mFormat(format), mTimestamp(timestamp) {} pkix::Result CTLogVerifier::Init(Input subjectPublicKeyInfo) { diff --git a/security/ct/CTLogVerifier.h b/security/ct/CTLogVerifier.h index a76cc96ede04..a6e3adb9012c 100644 --- a/security/ct/CTLogVerifier.h +++ b/security/ct/CTLogVerifier.h @@ -30,9 +30,10 @@ class CTLogVerifier { public: // |operatorId| The numeric ID of the log operator. // |logState| "Qualified", "Usable", "ReadOnly", or "Retired". + // |logFormat| "RFC6962" or "Tiled" // |timestamp| timestamp associated with logState. CTLogVerifier(CTLogOperatorId operatorId, CTLogState logState, - uint64_t timestamp); + CTLogFormat logFormat, uint64_t timestamp); // Initializes the verifier with the given subjectPublicKeyInfo. // |subjectPublicKeyInfo| is a DER-encoded SubjectPublicKeyInfo. @@ -46,6 +47,7 @@ class CTLogVerifier { CTLogOperatorId operatorId() const { return mOperatorId; } CTLogState state() const { return mState; } + CTLogFormat format() const { return mFormat; } uint64_t timestamp() const { return mTimestamp; } // Verifies that |sct| contains a valid signature for |entry|. @@ -74,6 +76,7 @@ class CTLogVerifier { DigitallySigned::SignatureAlgorithm mSignatureAlgorithm; CTLogOperatorId mOperatorId; CTLogState mState; + CTLogFormat mFormat; uint64_t mTimestamp; }; diff --git a/security/ct/CTPolicyEnforcer.cpp b/security/ct/CTPolicyEnforcer.cpp index 440d3e7aaa1c..d1b5d7406ca9 100644 --- a/security/ct/CTPolicyEnforcer.cpp +++ b/security/ct/CTPolicyEnforcer.cpp @@ -75,11 +75,13 @@ bool LogWasQualifiedForSct(const VerifiedSCT& verifiedSct, // lifetime of the certificate. If the certificate lifetime is less than or // equal to 180 days, N is 2. Otherwise, N is 3. // Among these SCTs, at least two must be issued from distinct log operators. +// Additionally, at least one must be issued from an RFC6962 log. CTPolicyCompliance EmbeddedSCTsCompliant(const VerifiedSCTList& verifiedScts, uint64_t certIssuanceTime, Duration certLifetime) { size_t admissibleCount = 0; size_t admissibleOrRetiredCount = 0; + size_t rfc6962Count = 0; std::set logOperators; std::set logIds; for (const auto& verifiedSct : verifiedScts) { @@ -90,6 +92,14 @@ CTPolicyCompliance EmbeddedSCTsCompliant(const VerifiedSCTList& verifiedScts, !LogWasQualifiedForSct(verifiedSct, certIssuanceTime)) { continue; } + // SCTs from tiled logs "MUST" have a valid leaf index extension. + if (verifiedSct.logFormat == CTLogFormat::Tiled && + verifiedSct.sct.leafIndex.isNothing()) { + continue; + } + if (verifiedSct.logFormat == CTLogFormat::RFC6962) { + rfc6962Count++; + } // Note that a single SCT can count for both the "from a log that was // admissible" case and the "from a log that was admissible or retired" // case. @@ -104,7 +114,8 @@ CTPolicyCompliance EmbeddedSCTsCompliant(const VerifiedSCTList& verifiedScts, } size_t requiredEmbeddedScts = GetRequiredEmbeddedSctsCount(certLifetime); - if (admissibleCount < 1 || admissibleOrRetiredCount < requiredEmbeddedScts) { + if (admissibleCount < 1 || admissibleOrRetiredCount < requiredEmbeddedScts || + rfc6962Count < 1) { return CTPolicyCompliance::NotEnoughScts; } if (logIds.size() < requiredEmbeddedScts || logOperators.size() < 2) { @@ -117,10 +128,12 @@ CTPolicyCompliance EmbeddedSCTsCompliant(const VerifiedSCTList& verifiedScts, // or OCSP response): // There must be at least two SCTs from logs that were Admissible (i.e. // Qualified, Usable, or ReadOnly) at the time of the check. Among these SCTs, -// at least two must be issued from distinct log operators. +// at least two must be issued from distinct log operators. Additionally, at +// least one must be issued from an RFC6962 log. CTPolicyCompliance NonEmbeddedSCTsCompliant( const VerifiedSCTList& verifiedScts) { size_t admissibleCount = 0; + size_t rfc6962Count = 0; std::set logOperators; std::set logIds; for (const auto& verifiedSct : verifiedScts) { @@ -130,12 +143,20 @@ CTPolicyCompliance NonEmbeddedSCTsCompliant( if (verifiedSct.logState != CTLogState::Admissible) { continue; } + // SCTs from tiled logs "MUST" have a valid leaf index extension. + if (verifiedSct.logFormat == CTLogFormat::Tiled && + verifiedSct.sct.leafIndex.isNothing()) { + continue; + } admissibleCount++; + if (verifiedSct.logFormat == CTLogFormat::RFC6962) { + rfc6962Count++; + } logIds.insert(verifiedSct.sct.logId); logOperators.insert(verifiedSct.logOperatorId); } - if (admissibleCount < 2) { + if (admissibleCount < 2 || rfc6962Count < 1) { return CTPolicyCompliance::NotEnoughScts; } if (logIds.size() < 2 || logOperators.size() < 2) { diff --git a/security/ct/CTSerialization.cpp b/security/ct/CTSerialization.cpp index ed6cbb891a43..d5a9faf34000 100644 --- a/security/ct/CTSerialization.cpp +++ b/security/ct/CTSerialization.cpp @@ -360,6 +360,11 @@ Result DecodeSignedCertificateTimestamp(Reader& reader, InputToBuffer(extensions, result.extensions); result.timestamp = timestamp; + rv = result.DecodeExtensions(); + if (rv != Success) { + return rv; + } + output = std::move(result); return Success; } diff --git a/security/ct/CTVerifyResult.cpp b/security/ct/CTVerifyResult.cpp index 0236207436d2..07c88967b065 100644 --- a/security/ct/CTVerifyResult.cpp +++ b/security/ct/CTVerifyResult.cpp @@ -13,11 +13,12 @@ namespace ct { VerifiedSCT::VerifiedSCT(SignedCertificateTimestamp&& sct, SCTOrigin origin, CTLogOperatorId logOperatorId, CTLogState logState, - uint64_t logTimestamp) + CTLogFormat logFormat, uint64_t logTimestamp) : sct(std::move(sct)), origin(origin), logOperatorId(logOperatorId), logState(logState), + logFormat(logFormat), logTimestamp(logTimestamp) {} void CTVerifyResult::Reset() { diff --git a/security/ct/CTVerifyResult.h b/security/ct/CTVerifyResult.h index 4134a560f301..658340d83c38 100644 --- a/security/ct/CTVerifyResult.h +++ b/security/ct/CTVerifyResult.h @@ -28,13 +28,14 @@ enum class SCTOrigin { struct VerifiedSCT { VerifiedSCT(SignedCertificateTimestamp&& sct, SCTOrigin origin, CTLogOperatorId logOperatorId, CTLogState logState, - uint64_t logTimestamp); + CTLogFormat logFormat, uint64_t logTimestamp); // The original SCT. SignedCertificateTimestamp sct; SCTOrigin origin; CTLogOperatorId logOperatorId; CTLogState logState; + CTLogFormat logFormat; uint64_t logTimestamp; }; diff --git a/security/ct/MultiLogCTVerifier.cpp b/security/ct/MultiLogCTVerifier.cpp index bed6f32a006b..2496c532a98c 100644 --- a/security/ct/MultiLogCTVerifier.cpp +++ b/security/ct/MultiLogCTVerifier.cpp @@ -178,7 +178,8 @@ pkix::Result MultiLogCTVerifier::VerifySingleSCT( } VerifiedSCT verifiedSct(std::move(sct), origin, matchingLog->operatorId(), - matchingLog->state(), matchingLog->timestamp()); + matchingLog->state(), matchingLog->format(), + matchingLog->timestamp()); result.verifiedScts.push_back(std::move(verifiedSct)); return Success; } diff --git a/security/ct/SignedCertificateTimestamp.cpp b/security/ct/SignedCertificateTimestamp.cpp index 573006d599ed..8580fc33816f 100644 --- a/security/ct/SignedCertificateTimestamp.cpp +++ b/security/ct/SignedCertificateTimestamp.cpp @@ -6,9 +6,72 @@ #include "SignedCertificateTimestamp.h" +#include "CTUtils.h" + namespace mozilla { namespace ct { +pkix::Result SignedCertificateTimestamp::DecodeExtensions() { + if (extensions.empty()) { + return pkix::Success; + } + + // `extensions` is a sequence of Extension: + // struct { + // ExtensionType extension_type; + // opaque extension_data<0..2^16-1>; + // } Extension; + const size_t kExtensionDataLengthBytes = 2; + // Currently, the only supported extension type is `leaf_index`. Others are + // ignored. + // enum { + // leaf_index(0), (255) + // } ExtensionType; + const size_t kExtensionTypeLength = 1; + const uint8_t kExtensionTypeLeafIndex = 0; + + pkix::Input input; + pkix::Result rv = input.Init(extensions.data(), extensions.size()); + if (rv != pkix::Success) { + return rv; + } + pkix::Reader reader(input); + while (!reader.AtEnd()) { + uint8_t extensionType; + rv = ReadUint(reader, extensionType); + if (rv != pkix::Success) { + return rv; + } + pkix::Input extensionData; + rv = ReadVariableBytes(reader, extensionData); + if (rv != pkix::Success) { + return rv; + } + if (extensionType == kExtensionTypeLeafIndex) { + // Duplicate extensions are not allowed. + if (leafIndex.isSome()) { + return pkix::Result::ERROR_EXTENSION_VALUE_INVALID; + } + // A leaf index is a big-endian, unsigned 40-bit value. In other words, + // it is 5 8-bit bytes, like so: + // uint8 uint40[5]; + // uint40 LeafIndex; + const size_t kLeafIndexLength = 5; + uint64_t leafIndexValue; + pkix::Reader leafIndexReader(extensionData); + rv = ReadUint(leafIndexReader, leafIndexValue); + if (rv != pkix::Success) { + return rv; + } + if (!leafIndexReader.AtEnd()) { + return pkix::Result::ERROR_EXTENSION_VALUE_INVALID; + } + leafIndex.emplace(leafIndexValue); + } + } + return pkix::Success; +} + void LogEntry::Reset() { type = LogEntry::Type::X509; leafCertificate.clear(); diff --git a/security/ct/SignedCertificateTimestamp.h b/security/ct/SignedCertificateTimestamp.h index 2803439295ea..cda0df0c0626 100644 --- a/security/ct/SignedCertificateTimestamp.h +++ b/security/ct/SignedCertificateTimestamp.h @@ -8,6 +8,7 @@ #define SignedCertificateTimestamp_h #include "Buffer.h" +#include "mozilla/Maybe.h" #include "mozpkix/Input.h" #include "mozpkix/Result.h" @@ -65,12 +66,17 @@ struct SignedCertificateTimestamp { V1 = 0, }; + pkix::Result DecodeExtensions(); + Version version; Buffer logId; // "timestamp" is the current time in milliseconds, measured since the epoch, // ignoring leap seconds. See RFC 6962, Section 3.2. uint64_t timestamp; Buffer extensions; + // Maybe the index of the entry in the log, if specified by a LeafIndex + // extension in `extensions`. + Maybe leafIndex; DigitallySigned signature; }; diff --git a/security/ct/tests/gtest/CTLogVerifierTest.cpp b/security/ct/tests/gtest/CTLogVerifierTest.cpp index 8bc0d8c69075..cfa8c564ab03 100644 --- a/security/ct/tests/gtest/CTLogVerifierTest.cpp +++ b/security/ct/tests/gtest/CTLogVerifierTest.cpp @@ -33,7 +33,8 @@ class CTLogVerifierTest : public ::testing::Test { void TearDown() override { signature_cache_free(mSignatureCache); } protected: - CTLogVerifier mLog = CTLogVerifier(-1, CTLogState::Admissible, 0); + CTLogVerifier mLog = + CTLogVerifier(-1, CTLogState::Admissible, CTLogFormat::RFC6962, 0); // For some reason, the templating makes it impossible to use UniquePtr here. SignatureCache* mSignatureCache; }; @@ -115,7 +116,7 @@ TEST_F(CTLogVerifierTest, ExcessDataInPublicKey) { std::string extra = "extra"; key.insert(key.end(), extra.begin(), extra.end()); - CTLogVerifier log(-1, CTLogState::Admissible, 0); + CTLogVerifier log(-1, CTLogState::Admissible, CTLogFormat::RFC6962, 0); EXPECT_NE(Success, log.Init(InputForBuffer(key))); } diff --git a/security/ct/tests/gtest/CTObjectsExtractorTest.cpp b/security/ct/tests/gtest/CTObjectsExtractorTest.cpp index cc6b850be270..d144bec16be8 100644 --- a/security/ct/tests/gtest/CTObjectsExtractorTest.cpp +++ b/security/ct/tests/gtest/CTObjectsExtractorTest.cpp @@ -39,7 +39,8 @@ class CTObjectsExtractorTest : public ::testing::Test { Buffer mEmbeddedCert; Buffer mCaCert; Buffer mCaCertSPKI; - CTLogVerifier mLog = CTLogVerifier(-1, CTLogState::Admissible, 0); + CTLogVerifier mLog = + CTLogVerifier(-1, CTLogState::Admissible, CTLogFormat::RFC6962, 0); }; TEST_F(CTObjectsExtractorTest, ExtractPrecert) { diff --git a/security/ct/tests/gtest/CTPolicyEnforcerTest.cpp b/security/ct/tests/gtest/CTPolicyEnforcerTest.cpp index c6cb165700c4..435485959713 100644 --- a/security/ct/tests/gtest/CTPolicyEnforcerTest.cpp +++ b/security/ct/tests/gtest/CTPolicyEnforcerTest.cpp @@ -35,25 +35,30 @@ class CTPolicyEnforcerTest : public ::testing::Test { void AddSct(VerifiedSCTList& verifiedScts, size_t logNo, CTLogOperatorId operatorId, SCTOrigin origin, uint64_t timestamp, - CTLogState logState = CTLogState::Admissible) { + CTLogState logState = CTLogState::Admissible, + CTLogFormat logFormat = CTLogFormat::RFC6962, + Maybe leafIndex = Nothing()) { SignedCertificateTimestamp sct; sct.version = SignedCertificateTimestamp::Version::V1; sct.timestamp = timestamp; + sct.leafIndex = leafIndex; Buffer logId; GetLogId(logId, logNo); sct.logId = std::move(logId); VerifiedSCT verifiedSct(std::move(sct), origin, operatorId, logState, - LOG_TIMESTAMP); + logFormat, LOG_TIMESTAMP); verifiedScts.push_back(std::move(verifiedSct)); } void AddMultipleScts(VerifiedSCTList& verifiedScts, size_t logsCount, uint8_t operatorsCount, SCTOrigin origin, uint64_t timestamp, - CTLogState logState = CTLogState::Admissible) { + CTLogState logState = CTLogState::Admissible, + CTLogFormat logFormat = CTLogFormat::RFC6962) { for (size_t logNo = 0; logNo < logsCount; logNo++) { CTLogOperatorId operatorId = logNo % operatorsCount; - AddSct(verifiedScts, logNo, operatorId, origin, timestamp, logState); + AddSct(verifiedScts, logNo, operatorId, origin, timestamp, logState, + logFormat); } } @@ -292,5 +297,84 @@ TEST_F(CTPolicyEnforcerTest, } } +TEST_F(CTPolicyEnforcerTest, ConformsToCTPolicyWithAtLeastOneRFC6962Log) { + VerifiedSCTList scts; + + AddSct(scts, LOG_1, OPERATOR_1, ORIGIN_TLS, TIMESTAMP_1, + CTLogState::Admissible, CTLogFormat::Tiled, Some(23)); + AddSct(scts, LOG_2, OPERATOR_2, ORIGIN_TLS, TIMESTAMP_1, + CTLogState::Admissible, CTLogFormat::RFC6962); + + CheckCompliance(scts, DEFAULT_LIFETIME, CTPolicyCompliance::Compliant); +} + +TEST_F(CTPolicyEnforcerTest, + ConformsToCTPolicyWithAtLeastOneRFC6962LogEmbedded) { + VerifiedSCTList scts; + + // 3 embedded SCTs required for DEFAULT_LIFETIME. + AddSct(scts, LOG_1, OPERATOR_1, ORIGIN_EMBEDDED, TIMESTAMP_1, + CTLogState::Admissible, CTLogFormat::Tiled, Some(23)); + AddSct(scts, LOG_2, OPERATOR_1, ORIGIN_EMBEDDED, TIMESTAMP_1, + CTLogState::Admissible, CTLogFormat::Tiled, Some(23)); + AddSct(scts, LOG_3, OPERATOR_2, ORIGIN_EMBEDDED, TIMESTAMP_1, + CTLogState::Admissible, CTLogFormat::RFC6962); + + CheckCompliance(scts, DEFAULT_LIFETIME, CTPolicyCompliance::Compliant); +} + +TEST_F(CTPolicyEnforcerTest, DoesNotConformToCTPolicyWithNoRFC6962Logs) { + VerifiedSCTList scts; + + AddSct(scts, LOG_1, OPERATOR_1, ORIGIN_TLS, TIMESTAMP_1, + CTLogState::Admissible, CTLogFormat::Tiled, Some(23)); + AddSct(scts, LOG_2, OPERATOR_2, ORIGIN_TLS, TIMESTAMP_1, + CTLogState::Admissible, CTLogFormat::Tiled, Some(23)); + + CheckCompliance(scts, DEFAULT_LIFETIME, CTPolicyCompliance::NotEnoughScts); +} + +TEST_F(CTPolicyEnforcerTest, + DoesNotConformToCTPolicyWithNoRFC6962LogsEmbedded) { + VerifiedSCTList scts; + + // 3 embedded SCTs required for DEFAULT_LIFETIME. + AddSct(scts, LOG_1, OPERATOR_1, ORIGIN_EMBEDDED, TIMESTAMP_1, + CTLogState::Admissible, CTLogFormat::Tiled, Some(23)); + AddSct(scts, LOG_2, OPERATOR_1, ORIGIN_EMBEDDED, TIMESTAMP_1, + CTLogState::Admissible, CTLogFormat::Tiled, Some(23)); + AddSct(scts, LOG_3, OPERATOR_2, ORIGIN_EMBEDDED, TIMESTAMP_1, + CTLogState::Admissible, CTLogFormat::Tiled, Some(23)); + + CheckCompliance(scts, DEFAULT_LIFETIME, CTPolicyCompliance::NotEnoughScts); +} + +TEST_F(CTPolicyEnforcerTest, + DoesNotConformToCTPolicyWithSCTFromTiledLogWithNoLeafIndex) { + VerifiedSCTList scts; + + AddSct(scts, LOG_1, OPERATOR_1, ORIGIN_TLS, TIMESTAMP_1, + CTLogState::Admissible, CTLogFormat::Tiled, Nothing()); + AddSct(scts, LOG_2, OPERATOR_2, ORIGIN_TLS, TIMESTAMP_1, + CTLogState::Admissible, CTLogFormat::RFC6962); + + CheckCompliance(scts, DEFAULT_LIFETIME, CTPolicyCompliance::NotEnoughScts); +} + +TEST_F(CTPolicyEnforcerTest, + DoesNotConformToCTPolicyWithSCTFromTiledLogWithNoLeafIndexEmbedded) { + VerifiedSCTList scts; + + // 3 embedded SCTs required for DEFAULT_LIFETIME. + AddSct(scts, LOG_1, OPERATOR_1, ORIGIN_EMBEDDED, TIMESTAMP_1, + CTLogState::Admissible, CTLogFormat::Tiled, Some(23)); + AddSct(scts, LOG_2, OPERATOR_1, ORIGIN_EMBEDDED, TIMESTAMP_1, + CTLogState::Admissible, CTLogFormat::Tiled, Nothing()); + AddSct(scts, LOG_3, OPERATOR_2, ORIGIN_EMBEDDED, TIMESTAMP_1, + CTLogState::Admissible, CTLogFormat::RFC6962); + + CheckCompliance(scts, DEFAULT_LIFETIME, CTPolicyCompliance::NotEnoughScts); +} + } // namespace ct } // namespace mozilla diff --git a/security/ct/tests/gtest/CTSerializationTest.cpp b/security/ct/tests/gtest/CTSerializationTest.cpp index 983759cf7102..4a71df1e8849 100644 --- a/security/ct/tests/gtest/CTSerializationTest.cpp +++ b/security/ct/tests/gtest/CTSerializationTest.cpp @@ -192,6 +192,90 @@ TEST_F(CTSerializationTest, DecodesSignedCertificateTimestamp) { const size_t expectedSignatureLength = 71; EXPECT_EQ(expectedSignatureLength, sct.signature.signatureData.size()); EXPECT_TRUE(sct.extensions.empty()); + EXPECT_TRUE(sct.leafIndex.isNothing()); +} + +TEST_F(CTSerializationTest, + DecodesSignedCertificateTimestampWithLeafIndexExtension) { + Buffer encodedSctBuffer = + GetTestSignedCertificateTimestampWithLeafIndexExtension(); + Input encodedSctInput = InputForBuffer(encodedSctBuffer); + Reader encodedSctReader(encodedSctInput); + + SignedCertificateTimestamp sct; + ASSERT_EQ(Success, DecodeSignedCertificateTimestamp(encodedSctReader, sct)); + EXPECT_EQ(SignedCertificateTimestamp::Version::V1, sct.version); + EXPECT_EQ(GetTestPublicKeyId(), sct.logId); + const uint64_t expectedTime = 1365181456089; + EXPECT_EQ(expectedTime, sct.timestamp); + const size_t expectedSignatureLength = 71; + EXPECT_EQ(expectedSignatureLength, sct.signature.signatureData.size()); + EXPECT_FALSE(sct.extensions.empty()); + ASSERT_TRUE(sct.leafIndex.isSome()); + EXPECT_EQ(sct.leafIndex.value(), 52U); +} + +TEST_F(CTSerializationTest, + FailsDecodingSignedCertificateTimestampWithTwoLeafIndexExtensions) { + Buffer encodedSctBuffer = + GetTestSignedCertificateTimestampWithTwoLeafIndexExtensions(); + Input encodedSctInput = InputForBuffer(encodedSctBuffer); + Reader encodedSctReader(encodedSctInput); + + SignedCertificateTimestamp sct; + ASSERT_EQ(Result::ERROR_EXTENSION_VALUE_INVALID, + DecodeSignedCertificateTimestamp(encodedSctReader, sct)); +} + +TEST_F(CTSerializationTest, + DecodesSignedCertificateTimestampWithUnknownExtension) { + Buffer encodedSctBuffer = + GetTestSignedCertificateTimestampWithUnknownExtension(); + Input encodedSctInput = InputForBuffer(encodedSctBuffer); + Reader encodedSctReader(encodedSctInput); + + SignedCertificateTimestamp sct; + ASSERT_EQ(Success, DecodeSignedCertificateTimestamp(encodedSctReader, sct)); + EXPECT_EQ(SignedCertificateTimestamp::Version::V1, sct.version); + EXPECT_EQ(GetTestPublicKeyId(), sct.logId); + const uint64_t expectedTime = 1365181456089; + EXPECT_EQ(expectedTime, sct.timestamp); + const size_t expectedSignatureLength = 71; + EXPECT_EQ(expectedSignatureLength, sct.signature.signatureData.size()); + EXPECT_FALSE(sct.extensions.empty()); + EXPECT_TRUE(sct.leafIndex.isNothing()); +} + +TEST_F(CTSerializationTest, + DecodesSignedCertificateTimestampWithUnknownAndLeafIndexExtensions) { + Buffer encodedSctBuffer = + GetTestSignedCertificateTimestampWithUnknownAndLeafIndexExtensions(); + Input encodedSctInput = InputForBuffer(encodedSctBuffer); + Reader encodedSctReader(encodedSctInput); + + SignedCertificateTimestamp sct; + ASSERT_EQ(Success, DecodeSignedCertificateTimestamp(encodedSctReader, sct)); + EXPECT_EQ(SignedCertificateTimestamp::Version::V1, sct.version); + EXPECT_EQ(GetTestPublicKeyId(), sct.logId); + const uint64_t expectedTime = 1365181456089; + EXPECT_EQ(expectedTime, sct.timestamp); + const size_t expectedSignatureLength = 71; + EXPECT_EQ(expectedSignatureLength, sct.signature.signatureData.size()); + EXPECT_FALSE(sct.extensions.empty()); + ASSERT_TRUE(sct.leafIndex.isSome()); + EXPECT_EQ(sct.leafIndex.value(), 81U); +} + +TEST_F(CTSerializationTest, + FailsDecodingSignedCertificateTimestampWithTooShortExtension) { + Buffer encodedSctBuffer = + GetTestSignedCertificateTimestampWithTooShortExtension(); + Input encodedSctInput = InputForBuffer(encodedSctBuffer); + Reader encodedSctReader(encodedSctInput); + + SignedCertificateTimestamp sct; + ASSERT_EQ(Result::ERROR_BAD_DER, + DecodeSignedCertificateTimestamp(encodedSctReader, sct)); } TEST_F(CTSerializationTest, FailsDecodingInvalidSignedCertificateTimestamp) { diff --git a/security/ct/tests/gtest/CTTestUtils.cpp b/security/ct/tests/gtest/CTTestUtils.cpp index 05f588b77fde..6a25307ec395 100644 --- a/security/ct/tests/gtest/CTTestUtils.cpp +++ b/security/ct/tests/gtest/CTTestUtils.cpp @@ -87,6 +87,55 @@ const char kTestSignedCertificateTimestamp[] = "08dfbfe9ef536cf7f2022100beb29c72d7d06d61d06bdb38a069469aa86fe12e18bb7cc456" "89a2c0187ef5a5"; +// The signatures on the following "SCT"s are not actually valid. The intent is +// to test the parsing of extensions. +const char kTestSignedCertificateTimestampWithLeafIndexExtension[] = + "00df1c2ec11500945247a96168325ddc5c7959e8f7c6d388fc002e0bbd3f74d7640000013d" + "db27ded9" + "0008" // 8 bytes of extensions + "0000050000000034" // leaf_index of 52 + "0403004730450220606e10ae5c2d5a1b0aed49dc4937f48de71a4e9784e9c2" + "08dfbfe9ef536cf7f2022100beb29c72d7d06d61d06bdb38a069469aa86fe12e18bb7cc456" + "89a2c0187ef5a5"; + +const char kTestSignedCertificateTimestampWithTwoLeafIndexExtensions[] = + "00df1c2ec11500945247a96168325ddc5c7959e8f7c6d388fc002e0bbd3f74d7640000013d" + "db27ded9" + "0010" // 16 bytes of extensions + "0000050000000034" // leaf_index of 52 + "0000050000000051" // leaf_index of 81 + "0403004730450220606e10ae5c2d5a1b0aed49dc4937f48de71a4e9784e9c2" + "08dfbfe9ef536cf7f2022100beb29c72d7d06d61d06bdb38a069469aa86fe12e18bb7cc456" + "89a2c0187ef5a5"; + +const char kTestSignedCertificateTimestampWithUnknownExtension[] = + "00df1c2ec11500945247a96168325ddc5c7959e8f7c6d388fc002e0bbd3f74d7640000013d" + "db27ded9" + "0008" // 8 bytes of extensions + "0100050000000034" // an (unknown) extension with id 1 + "0403004730450220606e10ae5c2d5a1b0aed49dc4937f48de71a4e9784e9c2" + "08dfbfe9ef536cf7f2022100beb29c72d7d06d61d06bdb38a069469aa86fe12e18bb7cc456" + "89a2c0187ef5a5"; + +const char kTestSignedCertificateTimestampWithUnknownAndLeafIndexExtensions[] = + "00df1c2ec11500945247a96168325ddc5c7959e8f7c6d388fc002e0bbd3f74d7640000013d" + "db27ded9" + "0010" // 16 bytes of extensions + "0100050000000034" // an (unknown) extension with id 1 + "0000050000000051" // leaf_index of 81 + "0403004730450220606e10ae5c2d5a1b0aed49dc4937f48de71a4e9784e9c2" + "08dfbfe9ef536cf7f2022100beb29c72d7d06d61d06bdb38a069469aa86fe12e18bb7cc456" + "89a2c0187ef5a5"; + +const char kTestSignedCertificateTimestampWithTooShortExtension[] = + "00df1c2ec11500945247a96168325ddc5c7959e8f7c6d388fc002e0bbd3f74d7640000013d" + "db27ded9" + "0008" // 8 bytes of extensions + "000005000034" // 3 bytes of extension data when there should be 5 + "0403004730450220606e10ae5c2d5a1b0aed49dc4937f48de71a4e9784e9c2" + "08dfbfe9ef536cf7f2022100beb29c72d7d06d61d06bdb38a069469aa86fe12e18bb7cc456" + "89a2c0187ef5a5"; + // ct-server-key-public.pem const char kEcP256PublicKey[] = "3059301306072a8648ce3d020106082a8648ce3d0301070342000499783cb14533c0161a5a" @@ -527,6 +576,27 @@ Buffer GetTestSignedCertificateTimestamp() { return HexToBytes(kTestSignedCertificateTimestamp); } +Buffer GetTestSignedCertificateTimestampWithLeafIndexExtension() { + return HexToBytes(kTestSignedCertificateTimestampWithLeafIndexExtension); +} + +Buffer GetTestSignedCertificateTimestampWithTwoLeafIndexExtensions() { + return HexToBytes(kTestSignedCertificateTimestampWithTwoLeafIndexExtensions); +} + +Buffer GetTestSignedCertificateTimestampWithUnknownExtension() { + return HexToBytes(kTestSignedCertificateTimestampWithUnknownExtension); +} + +Buffer GetTestSignedCertificateTimestampWithUnknownAndLeafIndexExtensions() { + return HexToBytes( + kTestSignedCertificateTimestampWithUnknownAndLeafIndexExtensions); +} + +Buffer GetTestSignedCertificateTimestampWithTooShortExtension() { + return HexToBytes(kTestSignedCertificateTimestampWithTooShortExtension); +} + Buffer GetTestInclusionProof() { return HexToBytes(kTestInclusionProof); } Buffer GetTestInclusionProofUnexpectedData() { diff --git a/security/ct/tests/gtest/CTTestUtils.h b/security/ct/tests/gtest/CTTestUtils.h index 3679d4411ef7..13fb446e0993 100644 --- a/security/ct/tests/gtest/CTTestUtils.h +++ b/security/ct/tests/gtest/CTTestUtils.h @@ -39,8 +39,13 @@ Buffer GetTestDigitallySigned(); // Returns the source data of the test DigitallySigned. Buffer GetTestDigitallySignedData(); -// Returns the binary representation of a test serialized SCT. +// Returns the binary representation of various test SCTs. Buffer GetTestSignedCertificateTimestamp(); +Buffer GetTestSignedCertificateTimestampWithLeafIndexExtension(); +Buffer GetTestSignedCertificateTimestampWithTwoLeafIndexExtensions(); +Buffer GetTestSignedCertificateTimestampWithUnknownExtension(); +Buffer GetTestSignedCertificateTimestampWithUnknownAndLeafIndexExtensions(); +Buffer GetTestSignedCertificateTimestampWithTooShortExtension(); // Returns the binary representation of a test serialized InclusionProof. Buffer GetTestInclusionProof(); diff --git a/security/ct/tests/gtest/MultiLogCTVerifierTest.cpp b/security/ct/tests/gtest/MultiLogCTVerifierTest.cpp index 133ee7e37258..5e410480965c 100644 --- a/security/ct/tests/gtest/MultiLogCTVerifierTest.cpp +++ b/security/ct/tests/gtest/MultiLogCTVerifierTest.cpp @@ -30,7 +30,8 @@ class MultiLogCTVerifierTest : public ::testing::Test { abort(); } - CTLogVerifier log(mLogOperatorID, CTLogState::Admissible, 0); + CTLogVerifier log(mLogOperatorID, CTLogState::Admissible, + CTLogFormat::RFC6962, 0); ; ASSERT_EQ(Success, log.Init(InputForBuffer(GetTestPublicKey()))); mVerifier.AddLog(std::move(log)); @@ -222,7 +223,8 @@ TEST_F(MultiLogCTVerifierTest, IdentifiesSCTFromUnknownLog) { TEST_F(MultiLogCTVerifierTest, IdentifiesSCTFromDisqualifiedLog) { MultiLogCTVerifier verifier; const uint64_t retiredTime = 12345u; - CTLogVerifier log(mLogOperatorID, CTLogState::Retired, retiredTime); + CTLogVerifier log(mLogOperatorID, CTLogState::Retired, CTLogFormat::RFC6962, + retiredTime); ASSERT_EQ(Success, log.Init(InputForBuffer(GetTestPublicKey()))); verifier.AddLog(std::move(log)); diff --git a/security/manager/ssl/tests/unit/test_ct.js b/security/manager/ssl/tests/unit/test_ct.js index 937a296a0012..aae7f8557383 100644 --- a/security/manager/ssl/tests/unit/test_ct.js +++ b/security/manager/ssl/tests/unit/test_ct.js @@ -51,6 +51,12 @@ function add_tests_in_mode(mode) { Ci.nsITransportSecurityInfo.CERTIFICATE_TRANSPARENCY_POLICY_COMPLIANT, true ); + // This certificate has an embedded SCT from a tiled log. + add_ct_test( + "ct-tiled-valid.example.com", + Ci.nsITransportSecurityInfo.CERTIFICATE_TRANSPARENCY_POLICY_COMPLIANT, + true + ); // This certificate has only 2 embedded SCTs, and so is not policy-compliant. add_ct_test( "ct-insufficient-scts.example.com", diff --git a/security/manager/ssl/tests/unit/test_ct/ct-tiled-valid.example.com.pem b/security/manager/ssl/tests/unit/test_ct/ct-tiled-valid.example.com.pem new file mode 100644 index 000000000000..8b338dcdba23 --- /dev/null +++ b/security/manager/ssl/tests/unit/test_ct/ct-tiled-valid.example.com.pem @@ -0,0 +1,34 @@ +-----BEGIN CERTIFICATE----- +MIIF5TCCBM2gAwIBAgIUa+BUsfUwwqRLVgVlQikCG31eMmkwDQYJKoZIhvcNAQEL +BQAwEjEQMA4GA1UEAwwHVGVzdCBDQTAiGA8yMDIzMTEyODAwMDAwMFoYDzIwMjYw +MjA1MDAwMDAwWjAlMSMwIQYDVQQDDBpjdC10aWxlZC12YWxpZC5leGFtcGxlLmNv +bTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBALqIUahEjhbWQf1utogG +NhA9PBPZ6uQ1SrTs9WhXbCR7wcclqODYH72xnAabbhqG8mvir1p1a2pkcQh6pVqn +RYf3HNUknAJ+zUP8HmnQOCApk6sgw0nk27lMwmtsDu0Vgg/xfq1pGrHTAjqLKkHu +p3DgDw2N/WYLK7AkkqR9uYhheZCxV5A90jvF4LhIH6g304hD7ycW2FW3ZlqqfgKQ +Lzp7EIAGJMwcbJetlmFbt+KWEsB1MaMMkd20yvf8rR0l0wnvuRcOp2jhs3svIm9p +47SKlWEd7ibWJZ2rkQhONsscJAQsvxaLL+Xxj5kXMbiz/kkj+nJRxDHVA6zaGAo1 +7Y0CAwEAAaOCAxowggMWMBgGA1UdEQQRMA+CDSouZXhhbXBsZS5jb20wggL4Bgor +BgEEAdZ5AgQCBIIC6ASCAuQC4gEvAFQiJZjzPTZIBULa7ODmuU3hXA7ujFkUykjX +XjxIToA/AAABUfp73AAAAAQBAQCJabucDWdKUJvzuE80Np7eZUCW2ZbLR3l/jX+N +o9R0I7QhVmvugafYV3Hy2yjAJUXIBCAFq/MKlSfbFGfg3aR7h1Y31zdaF7YMb3kF +1yvv7dmoxmF+ELqjmKALZBoLWq66uvOKrSfXfWo256tlc30FPAHQjisFxkw7LeJO +eZbuIIVBHMnyX/sVeDaIlXysiFVDrXs0c0Bv0vn+E7kiPScZfEAzW4tmN5ursFIy +PFR2CnzcpsQTr62nl1th2qteaQ3bb/kARlJk2cmFdWFhDs7NR4JVJ5cedknYyH5X +cximwIlAArlHSL/Angeuo51gqHwOdGPfdTfr/BfOLL3mpilFAHYAKrgwRDO5FN7S +8x5CB/JRwXo3oJJoUtkIAgb4Xlc5FioAAAFR+nvcAAAABAMARzBFAiBcdVGfExFQ +zV2K3iCjvAYwkf+yc3VfMWTs/ctCgApw5gIhAJZLBL70dv/nqol8uqJmab2wTrSz +tGQVeFOuTEbraKzRATcA6Mz6YX3GS9LYtKJsKw/1dnHx5n3gb4uVYfJVLXuUA5oA +AAFR+nvcAAAIAAAFhCHKllMEAQEAgLcoyQ3a159aRMB5gZkQ3vZQu7V9c+geuNPa +HqRfubtFYQU9PFuyaGf0WoxFuN38f0f7vhvCZ8mDnC3yj8DnoEx3gMfwtSRTWQHk +k0KSC61wlz3OI6Lf/B9uN8cw9cLSodBoyB6lDUGSlq3wz03+YXHHcCwospoZ8nwv +iiPo0N11M1IJYThYrpv270YIVjlTpln4TjmUKsIaB+JQgXWrzqjKmBxX/CkHQ6ul +Z/0rjVzBv8R1OjETk8MIlyB51N7Vb018Pek7AKo8M5QiwC2YHxhd4a55QfbTCMGC +//VaWYIvw1zMNocF6UKDPgZtqpzG04BHp502eDm4TYMfVpGISzANBgkqhkiG9w0B +AQsFAAOCAQEAR0KdN8iwoqCH+F8HL+vmlDXc2p/gGoRAHDS07bRi/cMlWFiWPfgp +iWVbXzuPDqZFHU3vYcmrZhi/MFlnnU/G5cRxPFRcJ8BPzNlnLb86PYYDSXTpiLsD +tdnwdMK7f1axuXjaNxlCznoQYjWh4hre9lzUjkPkO8l9kjtOxypSUnXQu96zLhnl +GrLe26P6NkUNagupy3za7USCi4Gej6nxFMH8Cou7mXdjPZMTWzf1DvgETOCFAwX7 +lrPnJgl5GQ2u8C9H0FE1iI05krYaeNHGVb2Cq+4g5/ECEYPoTq9a75l7SeSngQEd +jSTJxocud+zZajmigOfMIvcYUKVD3ieiLg== +-----END CERTIFICATE----- diff --git a/security/manager/ssl/tests/unit/test_ct/ct-tiled-valid.example.com.pem.certspec b/security/manager/ssl/tests/unit/test_ct/ct-tiled-valid.example.com.pem.certspec new file mode 100644 index 000000000000..ddd8918bce23 --- /dev/null +++ b/security/manager/ssl/tests/unit/test_ct/ct-tiled-valid.example.com.pem.certspec @@ -0,0 +1,4 @@ +issuer:Test CA +subject:ct-tiled-valid.example.com +extension:subjectAlternativeName:*.example.com +extension:embeddedSCTList:default:20160101,secp256r1:20160101,ev:20160101:567502607955 diff --git a/security/manager/ssl/tests/unit/test_ct/ct-unknown-log.example.com.pem b/security/manager/ssl/tests/unit/test_ct/ct-unknown-log.example.com.pem index 657859cd2e09..6013c91349dd 100644 --- a/security/manager/ssl/tests/unit/test_ct/ct-unknown-log.example.com.pem +++ b/security/manager/ssl/tests/unit/test_ct/ct-unknown-log.example.com.pem @@ -1,5 +1,5 @@ -----BEGIN CERTIFICATE----- -MIIF3DCCBMSgAwIBAgIUS2SODmE9dXhGEYS8JfCogtARiyEwDQYJKoZIhvcNAQEL +MIIFRTCCBC2gAwIBAgIUG4/k+n+iibx5ZgMTEJsAirIw46YwDQYJKoZIhvcNAQEL BQAwEjEQMA4GA1UEAwwHVGVzdCBDQTAiGA8yMDIzMTEyODAwMDAwMFoYDzIwMjYw MjA1MDAwMDAwWjAlMSMwIQYDVQQDDBpjdC11bmtub3duLWxvZy5leGFtcGxlLmNv bTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBALqIUahEjhbWQf1utogG @@ -8,27 +8,24 @@ RYf3HNUknAJ+zUP8HmnQOCApk6sgw0nk27lMwmtsDu0Vgg/xfq1pGrHTAjqLKkHu p3DgDw2N/WYLK7AkkqR9uYhheZCxV5A90jvF4LhIH6g304hD7ycW2FW3ZlqqfgKQ Lzp7EIAGJMwcbJetlmFbt+KWEsB1MaMMkd20yvf8rR0l0wnvuRcOp2jhs3svIm9p 47SKlWEd7ibWJZ2rkQhONsscJAQsvxaLL+Xxj5kXMbiz/kkj+nJRxDHVA6zaGAo1 -7Y0CAwEAAaOCAxEwggMNMBgGA1UdEQQRMA+CDSouZXhhbXBsZS5jb20wggLvBgor -BgEEAdZ5AgQCBIIC3wSCAtsC2QEvAFQiJZjzPTZIBULa7ODmuU3hXA7ujFkUykjX -XjxIToA/AAABUfp73AAAAAQBAQCgHd8Scrtd8aIdXafr8o2gFUYzajTu2N0sbfC4 -yb8C8KTCT2jcJJUoDvZ7KP2uPbzBwRvZvzMho885x+empY6u7esgTsq32PkW+rX5 -NR0xWIcyWvgPIwk6IR/Y8WGI7ZYyEBNoqjxDKa+MpPUTMFn3W7WpidwCH7Fa7J49 -hWxPLQ1G4RWoj5GjG0e/tFTzvQUWAbYROfQbbLQnJPXzEjt3C+3T6zCT/yo/HSiW -lC/ylgGc3lBgqnXqtmrGVvTez4ydnSM6DEKUCVtMyBCfVw3HSGzbrtMGvUUcsEI0 -LcATaRJG/WdWjRLdacf2eO9bG8ngpLJQImK5p/9UOKM1TJW8AHUAKrgwRDO5FN7S -8x5CB/JRwXo3oJJoUtkIAgb4Xlc5FioAAAFR+nvcAAAABAMARjBEAiBcdVGfExFQ -zV2K3iCjvAYwkf+yc3VfMWTs/ctCgApw5gIgWAl+GxbSxwqeZmp4wx+zK38l6zW3 -2KrjPq4QHn+9Fb4BLwDozPphfcZL0ti0omwrD/V2cfHmfeBvi5Vh8lUte5QDmgAA -AVH6e9wAAAAEAQEARvV0PDIvS8wZzGKzNBPdsAP/XPfF/ZzEA6z63dEKpIyiPgo1 -vgoKc8EPyZfMirlvXU4pRSkuXFqmD7xKVjbfk6FdGlDeHm35qcQhg0wmmjGhJ+lJ -jVZB7Hqs6qCLxAR01HQy1sMkLT8iku+jvey+rdzF+hJLKZhACB+R0GR3gPnqN+mW -0HSCUwvYso0FIAkO8Xursq3jpjykaZVkAx2ETk3lnBwuMEw01Opn5497wyddA6WI -knVhzORNwH9NeH4vjo3vD49B57WbGN95G+DH7RHlrEwgyczOkS+BBwnt9u64pBxS -FgJj4PRggejuuQ/Eihu9GL5pbMPfUps4QTnFaDANBgkqhkiG9w0BAQsFAAOCAQEA -araaEEpMCOVu0Wi1DcyHdW1fdYPU+dX9GStO666LlgfJm9w3hcLCxVmn8Y4WUYbw -fmPghlsr0MNLg+6pyxmthDSKkE4yieckNkTUYybKZaEvFpaZQamHMtWzuPS9Uvab -XbxgC6/Z50nN3cPd6tLK7eyq2ZXzQf94vfn/EcJjkRGVdubPTw6URgu27FEJAxX1 -XAJpdu5HwOj2+4aCNw6drMg+b/Dig+EpOTSBhalofyJxi3Lv/Kedzc9wlxqvmUvm -iMxWJob8wwaFRoL4YNBxxZEYI+Hebw8kePHhtwZ7h9q/BsgFajsWn5ay3PXmVvD2 -Em16teHaNfB2r3e6sjuZBw== +7Y0CAwEAAaOCAnowggJ2MBgGA1UdEQQRMA+CDSouZXhhbXBsZS5jb20wggJYBgor +BgEEAdZ5AgQCBIICSASCAkQCQgEvAFQiJZjzPTZIBULa7ODmuU3hXA7ujFkUykjX +XjxIToA/AAABUfp73AAAAAQBAQCoorL4v69VDwyEYimAete5jKzdDsKvcwpHOdKm +0fRqi5wdqSIlqtjUyg1ThMC9+XY1INML8dcW12u50qjaqHUmb/2EcVi0qgrtTafI +yahbYk163rEmiW56AUt79egtYw05BfFb7JuJ51Hf5A4eCRWn2DA8u34/A7G1bse+ +i2CKmRucCKti2P1kffZsX/nTj4e/Fa2Ch883T0upGRhuenLxPELeVap/PHrh5TJe +HxVC5+KwYeArMjiztQgpbw3k/7GoVeimSaZUbd5qmk27JJE1j2KUdqcjrLGYebZ8 +ueRyUe1fomiI4kTitSo1ONLfP50Myegzbi+dEQm6gyVCRHJaAHYAKrgwRDO5FN7S +8x5CB/JRwXo3oJJoUtkIAgb4Xlc5FioAAAFR+nvcAAAABAMARzBFAiBcdVGfExFQ +zV2K3iCjvAYwkf+yc3VfMWTs/ctCgApw5gIhAKUxU/HmabPud/RzHLU7msT2kc62 +iGdFqyuH0YqCReJuAJcAzXkXTRhLFnhJd6/HDruMins0z+2fuxg+i6a4e9MJANIA +AAFR+nvcAAAABAMAaDBmAjEA7QYnPqFoOlS02BpDdIRIljzmPr6BFwPs1z1y8KJU +BlnU7EVG6FbnXmVVt5Op9wDzAjEAuZvQO+69a4EqQct2T3/XNZr38qeLUdxdzSoh +yzj3fF54El39EGCOvdHaQB8IqpRMMA0GCSqGSIb3DQEBCwUAA4IBAQBxeaa3fzfc +MnYQi0mVcy2pMOkiyNa2Q0coKi+9hKRPIohG9Dddl2rHdnSBtjUBtt15cEJrfZPE +Q9nkGCUC6nRUH0id4cGJaDsTCMxik0P3gOMLtUpP2k1xux+sn0VRQ+iJdiIKkTh+ +QJ53O6Apaf4lUUwPu5F/WYX685u8ruudphcf6gUeP87ldgyH3zQ4s9UQoER900pj +vmFJM/ZTOfBmSgLXevZYRE57ACi9fMjkGD/IXnj3sctl7EtM9jovygo4Hk4yjSpw +Cg7fMKIIzO2PU8WhrNlQgE+KKaDDNEZuD9p5MBh0tU+L8TUVU0Wtt2xTrb7lZXB1 +WDb3zi3AJNht -----END CERTIFICATE----- diff --git a/security/manager/ssl/tests/unit/test_ct/ct-unknown-log.example.com.pem.certspec b/security/manager/ssl/tests/unit/test_ct/ct-unknown-log.example.com.pem.certspec index 968aa10594a3..d0143fa0af73 100644 --- a/security/manager/ssl/tests/unit/test_ct/ct-unknown-log.example.com.pem.certspec +++ b/security/manager/ssl/tests/unit/test_ct/ct-unknown-log.example.com.pem.certspec @@ -1,4 +1,4 @@ issuer:Test CA subject:ct-unknown-log.example.com extension:subjectAlternativeName:*.example.com -extension:embeddedSCTList:default:20160101,secp256r1:20160101,ev:20160101 +extension:embeddedSCTList:default:20160101,secp256r1:20160101,secp384r1:20160101 diff --git a/security/manager/tools/log_list.json b/security/manager/tools/log_list.json index 4c3c1167ec9d..d9ef3505b327 100644 --- a/security/manager/tools/log_list.json +++ b/security/manager/tools/log_list.json @@ -1,6 +1,6 @@ { - "version": "59.18", - "log_list_timestamp": "2025-07-27T12:54:32Z", + "version": "59.19", + "log_list_timestamp": "2025-07-28T12:53:51Z", "operators": [ { "name": "Google", diff --git a/security/manager/tools/pycert.py b/security/manager/tools/pycert.py index 44a982e0df9d..70b49b96d235 100755 --- a/security/manager/tools/pycert.py +++ b/security/manager/tools/pycert.py @@ -36,7 +36,7 @@ certificatePolicies:[,...] nameConstraints:{permitted,excluded}:[,...] nsCertType:sslServer TLSFeature:[,...] -embeddedSCTList:[:,...] +embeddedSCTList:[:[:],...] delegationUsage: Where: @@ -109,12 +109,12 @@ class UnknownBaseError(Error): """Base class for handling unexpected input in this module.""" def __init__(self, value): - super(UnknownBaseError, self).__init__() + super().__init__() self.value = value self.category = "input" def __str__(self): - return 'Unknown %s type "%s"' % (self.category, repr(self.value)) + return f'Unknown {self.category} type "{repr(self.value)}"' class UnknownAlgorithmTypeError(UnknownBaseError): @@ -210,18 +210,18 @@ class InvalidSCTSpecification(Error): """Helper exception type to handle invalid SCT specifications.""" def __init__(self, value): - super(InvalidSCTSpecification, self).__init__() + super().__init__() self.value = value def __str__(self): - return repr('invalid SCT specification "{}"' % self.value) + return f'invalid SCT specification "{self.value}"' class InvalidSerialNumber(Error): """Exception type to handle invalid serial numbers.""" def __init__(self, value): - super(InvalidSerialNumber, self).__init__() + super().__init__() self.value = value def __str__(self): @@ -254,7 +254,7 @@ def stringToDN(string, tag=None): optional implicit tag in cases where the Name needs to be tagged differently.""" if string and "/" not in string: - string = "/CN=%s" % string + string = f"/CN={string}" rdns = rfc2459.RDNSequence() pattern = "/(C|ST|L|O|OU|CN|emailAddress)=" split = re.split(pattern, string) @@ -621,10 +621,11 @@ class Certificate: def addCertificatePolicies(self, policyOIDs, critical): policies = rfc2459.CertificatePolicies() for pos, policyOID in enumerate(policyOIDs.split(",")): - if policyOID == "any": - policyOID = "2.5.29.32.0" + policyOIDMapped = policyOID + if policyOIDMapped == "any": + policyOIDMapped = "2.5.29.32.0" policy = rfc2459.PolicyInformation() - policyIdentifier = rfc2459.CertPolicyId(policyOID) + policyIdentifier = rfc2459.CertPolicyId(policyOIDMapped) policy["policyIdentifier"] = policyIdentifier policies.setComponentByPosition(pos, policy) self.addExtension(rfc2459.id_ce_certificatePolicies, policies, critical) @@ -696,15 +697,20 @@ class Certificate: (scts, critical) = self.savedEmbeddedSCTListData encodedSCTs = [] for sctSpec in scts.split(","): - match = re.search(r"(\w+):(\d{8})", sctSpec) + match = re.search(r"(\w+):(\d{8}):?(\d+)?", sctSpec) if not match: raise InvalidSCTSpecification(sctSpec) keySpec = match.group(1) + leafIndex = match.group(3) + if leafIndex: + leafIndex = int(leafIndex) key = pykey.keyFromSpecification(keySpec) time = datetime.datetime.strptime(match.group(2), "%Y%m%d") tbsCertificate = self.getTBSCertificate() tbsDER = encoder.encode(tbsCertificate) - sct = pyct.SCT(key, time, pyct.PrecertEntry(tbsDER, self.issuerKey)) + sct = pyct.SCT( + key, time, pyct.PrecertEntry(tbsDER, self.issuerKey), leafIndex + ) signed = sct.signAndEncode() lengthPrefix = pack("!H", len(signed)) encodedSCTs.append(lengthPrefix + signed) diff --git a/security/manager/tools/pyct.py b/security/manager/tools/pyct.py index 5c0462a9f5a7..b8a851ce8f54 100644 --- a/security/manager/tools/pyct.py +++ b/security/manager/tools/pyct.py @@ -16,6 +16,7 @@ to the output object. The specification is as follows: timestamp: [key:] [tamper] +[leafIndex:] certificate: @@ -48,7 +49,7 @@ class InvalidKeyError(Exception): self.key = key def __str__(self): - return 'Invalid key: "%s"' % str(self.key) + return f'Invalid key: "{str(self.key)}"' class UnknownSignedEntryType(Exception): @@ -58,7 +59,7 @@ class UnknownSignedEntryType(Exception): self.signedEntry = signedEntry def __str__(self): - return 'Unknown SignedEntry type: "%s"' % str(self.signedEntry) + return f'Unknown SignedEntry type: "{str(self.signedEntry)}"' class SignedEntry: @@ -84,11 +85,12 @@ class X509Entry(SignedEntry): class SCT: """SCT represents a Signed Certificate Timestamp.""" - def __init__(self, key, date, signedEntry): + def __init__(self, key, date, signedEntry, leafIndex=None): self.key = key self.timestamp = calendar.timegm(date.timetuple()) * 1000 self.signedEntry = signedEntry self.tamper = False + self.leafIndex = leafIndex def signAndEncode(self): """Returns a signed and encoded representation of the @@ -100,8 +102,7 @@ class SCT: # entry_type (two bytes (one 0 byte followed by one 0 byte for # X509Entry or one 1 byte for PrecertEntry) # signed_entry (bytes of X509Entry or PrecertEntry) - # extensions (2-byte-length-prefixed, currently empty (so two 0 - # bytes)) + # extensions (2-byte-length-prefixed) # A X509Entry is: # certificate (3-byte-length-prefixed data) # A PrecertEntry is: @@ -125,7 +126,18 @@ class SCT: ) else: raise UnknownSignedEntryType(self.signedEntry) - data = b"\0\0" + timestamp + b"\0" + entry_with_type + b"\0\0" + extensions = [] + if self.leafIndex: + # An extension consists of 1 byte to identify the extension type, 2 + # big-endian bytes for the length of the extension data, and then + # the extension data. + # The type of leaf_index is 0, and its data consists of 5 bytes. + extensions = [b"\0\0\5" + self.leafIndex.to_bytes(5, byteorder="big")] + extensionsLength = sum(map(len, extensions)) + extensionsEncoded = extensionsLength.to_bytes(2, byteorder="big") + b"".join( + extensions + ) + data = b"\0\0" + timestamp + b"\0" + entry_with_type + extensionsEncoded if isinstance(self.key, pykey.ECCKey): signatureByte = b"\3" elif isinstance(self.key, pykey.RSAKey): @@ -143,8 +155,7 @@ class SCT: # id (32 bytes of SHA-256 hash of the signing key, as # DER-encoded SPKI) # timestamp (8 bytes, milliseconds since the epoch) - # extensions (2-byte-length-prefixed data, currently - # empty) + # extensions (2-byte-length-prefixed data) # hash (one 4 byte representing sha256) # signature (one byte - 1 for RSA and 3 for ECDSA) # signature (2-byte-length-prefixed data) @@ -156,7 +167,8 @@ class SCT: b"\0" + key_id + timestamp - + b"\0\0\4" + + extensionsEncoded + + b"\4" + signatureByte + signature_len_prefix + signature @@ -168,26 +180,30 @@ class SCT: certificateSpecification = StringIO() readingCertificateSpecification = False tamper = False + leafIndex = None for line in specStream.readlines(): - line = line.strip() + lineStripped = line.strip() if readingCertificateSpecification: - print(line, file=certificateSpecification) - elif line == "certificate:": + print(lineStripped, file=certificateSpecification) + elif lineStripped == "certificate:": readingCertificateSpecification = True - elif line.startswith("key:"): - key = pykey.keyFromSpecification(line[len("key:") :]) - elif line.startswith("timestamp:"): + elif lineStripped.startswith("key:"): + key = pykey.keyFromSpecification(lineStripped[len("key:") :]) + elif lineStripped.startswith("timestamp:"): timestamp = datetime.datetime.strptime( - line[len("timestamp:") :], "%Y%m%d" + lineStripped[len("timestamp:") :], "%Y%m%d" ) - elif line == "tamper": + elif lineStripped == "tamper": tamper = True + elif lineStripped.startswith("leafIndex:"): + leafIndex = int(lineStripped[len("leafIndex:") :]) else: - raise pycert.UnknownParameterTypeError(line) + raise pycert.UnknownParameterTypeError(lineStripped) certificateSpecification.seek(0) certificate = pycert.Certificate(certificateSpecification).toDER() sct = SCT(key, timestamp, X509Entry(certificate)) sct.tamper = tamper + sct.leafIndex = leafIndex return sct diff --git a/taskcluster/docker/periodic-updates/scripts/getCTKnownLogs.py b/taskcluster/docker/periodic-updates/scripts/getCTKnownLogs.py index 0c6fa1222bfc..a2402051aa29 100755 --- a/taskcluster/docker/periodic-updates/scripts/getCTKnownLogs.py +++ b/taskcluster/docker/periodic-updates/scripts/getCTKnownLogs.py @@ -58,10 +58,16 @@ enum class CTLogState { Retired, }; +enum class CTLogFormat { + RFC6962, + Tiled, +}; + struct CTLogInfo { // See bug 1338873 about making these fields const. const char* name; CTLogState state; + CTLogFormat format; uint64_t timestamp; // Index within kCTLogOperatorList. size_t operatorIndex; @@ -122,7 +128,7 @@ def get_operator_index(json_data, target_name): LOG_INFO_TEMPLATE = """\ - {$description, $state, + {$description, $state, $log_format, $timestamp, // $timestamp_comment $operator_index,$spaces // $operator_comment $indented_log_key, @@ -140,56 +146,71 @@ def map_state(state): states to be included are 'qualified', 'usable', 'readonly', or 'retired'. Valid states that are not to be included are 'pending' or 'rejected'. """ - if state == "qualified" or state == "usable" or state == "readonly": + if state in {"qualified", "usable", "readonly"}: return "CTLogState::Admissible" elif state == "retired": return "CTLogState::Retired" - elif state == "pending" or state == "rejected": + elif state in {"pending", "rejected"}: return None else: - raise UnhandledLogStateException("unhandled log state '%s'" % state) + raise UnhandledLogStateException(f"unhandled log state '{state}'") + + +def get_initializer_for_log(log, operator, json_data, log_format): + log_key = base64.b64decode(log["key"]) + operator_name = operator["name"] + operator_index = get_operator_index(json_data, operator_name) + state = list(log["state"].keys())[0] + timestamp_comment = log["state"][state]["timestamp"] + timestamp = get_timestamp(timestamp_comment) + state = map_state(state) + if state is None: + return None + is_test_log = "test_only" in operator and operator["test_only"] + prefix = "" + suffix = "," + if is_test_log: + prefix = "#ifdef DEBUG\n" + suffix = ",\n#endif // DEBUG" + num_spaces = len(str(timestamp)) - len(str(operator_index)) + spaces = " " * num_spaces + tmpl = Template(LOG_INFO_TEMPLATE) + toappend = tmpl.substitute( + # Use json.dumps for C-escaping strings. + # Not perfect but close enough. + description=json.dumps(log["description"]), + operator_index=operator_index, + operator_comment=f"operated by {operator_name}".replace("/", "|"), + state=state, + log_format=log_format, + timestamp=timestamp, + spaces=spaces, + timestamp_comment=timestamp_comment, + # Maximum line width is 80. + indented_log_key="\n".join([f' "{l}"' for l in get_hex_lines(log_key, 74)]), + log_key_len=len(log_key), + ) + return prefix + toappend + suffix def get_log_info_structs(json_data): """Return array of CTLogInfo initializers for the known logs.""" - tmpl = Template(LOG_INFO_TEMPLATE) initializers = [] for operator in json_data["operators"]: - operator_name = operator["name"] - for log in operator["logs"]: - log_key = base64.b64decode(log["key"]) - operator_index = get_operator_index(json_data, operator_name) - state = list(log["state"].keys())[0] - timestamp_comment = log["state"][state]["timestamp"] - timestamp = get_timestamp(timestamp_comment) - state = map_state(state) - if state is None: - continue - is_test_log = "test_only" in operator and operator["test_only"] - prefix = "" - suffix = "," - if is_test_log: - prefix = "#ifdef DEBUG\n" - suffix = ",\n#endif // DEBUG" - num_spaces = len(str(timestamp)) - len(str(operator_index)) - spaces = " " * num_spaces - toappend = tmpl.substitute( - # Use json.dumps for C-escaping strings. - # Not perfect but close enough. - description=json.dumps(log["description"]), - operator_index=operator_index, - operator_comment=f"operated by {operator_name}".replace("/", "|"), - state=state, - timestamp=timestamp, - spaces=spaces, - timestamp_comment=timestamp_comment, - # Maximum line width is 80. - indented_log_key="\n".join( - [f' "{l}"' for l in get_hex_lines(log_key, 74)] - ), - log_key_len=len(log_key), - ) - initializers.append(prefix + toappend + suffix) + if "logs" in operator: + for log in operator["logs"]: + initializer = get_initializer_for_log( + log, operator, json_data, "CTLogFormat::RFC6962" + ) + if initializer: + initializers.append(initializer) + if "tiled_logs" in operator: + for log in operator["tiled_logs"]: + initializer = get_initializer_for_log( + log, operator, json_data, "CTLogFormat::Tiled" + ) + if initializer: + initializers.append(initializer) return initializers @@ -301,8 +322,34 @@ def patch_in_test_logs(json_data): } ], } + mozilla_test_operator_3 = { + "name": "Mozilla Test Org 3", + "id": max_id + 3, + "test_only": True, + "tiled_logs": [ + { + "description": "Mozilla Test RSA Log 4", + # `openssl x509 -noout -pubkey -in ` + "key": """ + MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAtUmJXJ0AEI0Rofmfh6nj + 0aXbXfrs8YjaV79kE2iPLORyLP8QkDjBdALJOipHPb0oau0/3NNjz1opFHcBvdgY + 12Fb+TVr1dPIM2qqjAlxNooGw81EYbk+UUJOF0S7LqrUaqs4zhloIZYfh3FKFmNp + Pwl2HN9Na6El6s7HvicNOI94n2zfeK4xRO0oxF55KThjp6IqSJgKNqQOctV5ybkl + 3/jHkzYv/WiXp8F1TF6XyWfD6t0aroqizM40igFpuA4ootcMGpYMbzNfLaCbm2Q/ + Wr+6SeiqqYHpYOJ9h0gL3VXdlBf6GFCfu1VMz4GkOX6LqBKKNL3yeGXBieVzT7Ip + BQIDAQAB + """, + "state": { + "qualified": { + "timestamp": "2025-06-25T12:09:26Z", + }, + }, + } + ], + } json_data["operators"].append(mozilla_test_operator_1) json_data["operators"].append(mozilla_test_operator_2) + json_data["operators"].append(mozilla_test_operator_3) def get_content_at(url): @@ -348,7 +395,7 @@ def run(args): hash_alg = rsa.verify(json_text, signature, key) if hash_alg != "SHA-256": raise UnsupportedSignatureHashAlgorithmException( - "unsupported hash algorithm '%s'" % hash_alg + f"unsupported hash algorithm '{hash_alg}'" ) print("Writing output: ", args.json_file_out) with open(args.json_file_out, "wb") as json_file_out: @@ -373,7 +420,7 @@ def parse_arguments_and_run(): "Downloads the JSON file from the known source " "of truth by default, but can also operate on a " "previously-downloaded file. See https://certificate.transparency.dev/google/", - epilog="Example: ./mach python %s" % os.path.basename(sys.argv[0]), + epilog=f"Example: ./mach python {os.path.basename(sys.argv[0])}", ) arg_parser.add_argument(