diff options
Diffstat (limited to 'security/ct')
57 files changed, 6271 insertions, 0 deletions
diff --git a/security/ct/BTTypes.h b/security/ct/BTTypes.h new file mode 100644 index 0000000000..aa16072150 --- /dev/null +++ b/security/ct/BTTypes.h @@ -0,0 +1,64 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#ifndef BTTypes_h +#define BTTypes_h + +#include <vector> + +#include "Buffer.h" + +namespace mozilla { +namespace ct { + +// Represents a Merkle inclusion proof for purposes of serialization, +// deserialization, and verification of the proof. The format for inclusion +// proofs in RFC 6962-bis is as follows: +// +// opaque LogID<2..127>; +// opaque NodeHash<32..2^8-1>; +// +// struct { +// LogID log_id; +// uint64 tree_size; +// uint64 leaf_index; +// NodeHash inclusion_path<1..2^16-1>; +// } InclusionProofDataV2; + +struct InclusionProofDataV2 { + Buffer logId; + uint64_t treeSize; + uint64_t leafIndex; + std::vector<Buffer> inclusionPath; +}; + +// Represents a Signed Tree Head as per RFC 6962-bis. All extensions are +// ignored. The signature field covers the data in the tree_head field. +// +// struct { +// LogID log_id; +// TreeHeadDataV2 tree_head; +// opaque signature<0..2^16-1>; +// } SignedTreeHeadDataV2; +// +// struct { +// uint64 timestamp; +// uint64 tree_size; +// NodeHash root_hash; +// Extension sth_extensions<0..2^16-1>; +// } TreeHeadDataV2; + +struct SignedTreeHeadDataV2 { + Buffer logId; + uint64_t timestamp; + uint64_t treeSize; + Buffer rootHash; +}; + +} // namespace ct +} // namespace mozilla + +#endif // BTTypes_h diff --git a/security/ct/BTVerifier.cpp b/security/ct/BTVerifier.cpp new file mode 100644 index 0000000000..02281fcffd --- /dev/null +++ b/security/ct/BTVerifier.cpp @@ -0,0 +1,322 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include "BTVerifier.h" + +#include <stdint.h> + +#include "CTUtils.h" +#include "SignedCertificateTimestamp.h" +#include "hasht.h" +#include "mozpkix/pkixnss.h" +#include "mozpkix/pkixutil.h" + +namespace mozilla { +namespace ct { + +using namespace mozilla::pkix; + +typedef mozilla::pkix::Result Result; + +// Common prefix lengths +static const size_t kLogIdPrefixLengthBytes = 1; +static const size_t kBTTreeSizeLength = 8; +static const size_t kNodeHashPrefixLengthBytes = 1; + +// Members of a SignedTreeHeadDataV2 struct +static const size_t kSTHTimestampLength = 8; +static const size_t kSTHExtensionsLengthBytes = 2; +static const size_t kSTHSignatureLengthBytes = 2; + +// Members of a Inclusion Proof struct +static const size_t kLeafIndexLength = 8; +static const size_t kInclusionPathLengthBytes = 2; + +static Result GetDigestAlgorithmLengthAndIdentifier( + DigestAlgorithm digestAlgorithm, + /* out */ size_t& digestAlgorithmLength, + /* out */ SECOidTag& digestAlgorithmId) { + switch (digestAlgorithm) { + case DigestAlgorithm::sha512: + digestAlgorithmLength = SHA512_LENGTH; + digestAlgorithmId = SEC_OID_SHA512; + return Success; + case DigestAlgorithm::sha256: + digestAlgorithmLength = SHA256_LENGTH; + digestAlgorithmId = SEC_OID_SHA256; + return Success; + default: + return pkix::Result::FATAL_ERROR_INVALID_ARGS; + } +} + +Result DecodeAndVerifySignedTreeHead( + Input signerSubjectPublicKeyInfo, DigestAlgorithm digestAlgorithm, + der::PublicKeyAlgorithm publicKeyAlgorithm, Input signedTreeHeadInput, + /* out */ SignedTreeHeadDataV2& signedTreeHead) { + SignedTreeHeadDataV2 result; + Reader reader(signedTreeHeadInput); + + Input logId; + Result rv = ReadVariableBytes<kLogIdPrefixLengthBytes>(reader, logId); + if (rv != Success) { + return rv; + } + InputToBuffer(logId, result.logId); + + // This is the beginning of the data covered by the signature. + Reader::Mark signedDataMark = reader.GetMark(); + + rv = ReadUint<kSTHTimestampLength>(reader, result.timestamp); + if (rv != Success) { + return rv; + } + + rv = ReadUint<kBTTreeSizeLength>(reader, result.treeSize); + if (rv != Success) { + return rv; + } + + Input hash; + rv = ReadVariableBytes<kNodeHashPrefixLengthBytes>(reader, hash); + if (rv != Success) { + return rv; + } + InputToBuffer(hash, result.rootHash); + + // We ignore any extensions, but we have to read them. + Input extensionsInput; + rv = ReadVariableBytes<kSTHExtensionsLengthBytes>(reader, extensionsInput); + if (rv != Success) { + return rv; + } + + Input signedDataInput; + rv = reader.GetInput(signedDataMark, signedDataInput); + if (rv != Success) { + return rv; + } + + Input signatureInput; + rv = ReadVariableBytes<kSTHSignatureLengthBytes>(reader, signatureInput); + if (rv != Success) { + return rv; + } + + switch (publicKeyAlgorithm) { + case der::PublicKeyAlgorithm::ECDSA: + rv = VerifyECDSASignedDataNSS(signedDataInput, digestAlgorithm, + signatureInput, signerSubjectPublicKeyInfo, + nullptr); + break; + case der::PublicKeyAlgorithm::RSA_PKCS1: + default: + return Result::FATAL_ERROR_INVALID_ARGS; + } + if (rv != Success) { + return rv; + } + + if (!reader.AtEnd()) { + return pkix::Result::ERROR_BAD_DER; + } + + signedTreeHead = std::move(result); + return Success; +} + +Result DecodeInclusionProof(Input input, InclusionProofDataV2& output) { + InclusionProofDataV2 result; + Reader reader(input); + + Input logId; + Result rv = ReadVariableBytes<kLogIdPrefixLengthBytes>(reader, logId); + if (rv != Success) { + return rv; + } + + rv = ReadUint<kBTTreeSizeLength>(reader, result.treeSize); + if (rv != Success) { + return rv; + } + + if (result.treeSize < 1) { + return pkix::Result::ERROR_BAD_DER; + } + + rv = ReadUint<kLeafIndexLength>(reader, result.leafIndex); + if (rv != Success) { + return rv; + } + + if (result.leafIndex >= result.treeSize) { + return pkix::Result::ERROR_BAD_DER; + } + + Input pathInput; + rv = ReadVariableBytes<kInclusionPathLengthBytes>(reader, pathInput); + if (rv != Success) { + return rv; + } + + if (pathInput.GetLength() < 1) { + return pkix::Result::ERROR_BAD_DER; + } + + Reader pathReader(pathInput); + std::vector<Buffer> inclusionPath; + + while (!pathReader.AtEnd()) { + Input hash; + rv = ReadVariableBytes<kNodeHashPrefixLengthBytes>(pathReader, hash); + if (rv != Success) { + return rv; + } + + Buffer hashBuffer; + InputToBuffer(hash, hashBuffer); + + inclusionPath.push_back(std::move(hashBuffer)); + } + + if (!reader.AtEnd()) { + return pkix::Result::ERROR_BAD_DER; + } + + InputToBuffer(logId, result.logId); + + result.inclusionPath = std::move(inclusionPath); + + output = std::move(result); + return Success; +} + +static Result CommonFinishDigest(UniquePK11Context& context, + size_t digestAlgorithmLength, + /* out */ Buffer& outputBuffer) { + uint32_t outLen = 0; + outputBuffer.assign(digestAlgorithmLength, 0); + if (PK11_DigestFinal(context.get(), outputBuffer.data(), &outLen, + digestAlgorithmLength) != SECSuccess) { + return Result::FATAL_ERROR_LIBRARY_FAILURE; + } + if (outLen != digestAlgorithmLength) { + return Result::FATAL_ERROR_LIBRARY_FAILURE; + } + return Success; +} + +static Result LeafHash(Input leafEntry, size_t digestAlgorithmLength, + SECOidTag digestAlgorithmId, + /* out */ Buffer& calculatedHash) { + UniquePK11Context context(PK11_CreateDigestContext(digestAlgorithmId)); + if (!context) { + return Result::FATAL_ERROR_LIBRARY_FAILURE; + } + const unsigned char zero = 0; + if (PK11_DigestOp(context.get(), &zero, 1u) != SECSuccess) { + return Result::FATAL_ERROR_LIBRARY_FAILURE; + } + SECItem leafEntryItem = UnsafeMapInputToSECItem(leafEntry); + if (PK11_DigestOp(context.get(), leafEntryItem.data, leafEntryItem.len) != + SECSuccess) { + return Result::FATAL_ERROR_LIBRARY_FAILURE; + } + return CommonFinishDigest(context, digestAlgorithmLength, calculatedHash); +} + +static Result NodeHash(const Buffer& left, const Buffer& right, + size_t digestAlgorithmLength, + SECOidTag digestAlgorithmId, + /* out */ Buffer& calculatedHash) { + UniquePK11Context context(PK11_CreateDigestContext(digestAlgorithmId)); + if (!context) { + return Result::FATAL_ERROR_LIBRARY_FAILURE; + } + const unsigned char one = 1; + if (PK11_DigestOp(context.get(), &one, 1u) != SECSuccess) { + return Result::FATAL_ERROR_LIBRARY_FAILURE; + } + if (PK11_DigestOp(context.get(), left.data(), left.size()) != SECSuccess) { + return Result::FATAL_ERROR_LIBRARY_FAILURE; + } + if (PK11_DigestOp(context.get(), right.data(), right.size()) != SECSuccess) { + return Result::FATAL_ERROR_LIBRARY_FAILURE; + } + return CommonFinishDigest(context, digestAlgorithmLength, calculatedHash); +} + +// This algorithm is specified by: +// https://tools.ietf.org/html/draft-ietf-trans-rfc6962-bis-28#section-2.1.3.2 +Result VerifyInclusionProof(const InclusionProofDataV2& proof, Input leafEntry, + Input expectedRootHash, + DigestAlgorithm digestAlgorithm) { + if (proof.treeSize == 0) { + return pkix::Result::ERROR_BAD_SIGNATURE; + } + size_t digestAlgorithmLength; + SECOidTag digestAlgorithmId; + Result rv = GetDigestAlgorithmLengthAndIdentifier( + digestAlgorithm, digestAlgorithmLength, digestAlgorithmId); + if (rv != Success) { + return rv; + } + if (proof.leafIndex >= proof.treeSize) { + return pkix::Result::ERROR_BAD_SIGNATURE; + } + if (expectedRootHash.GetLength() != digestAlgorithmLength) { + return pkix::Result::ERROR_BAD_SIGNATURE; + } + uint64_t leafIndex = proof.leafIndex; + uint64_t lastNodeIndex = proof.treeSize - 1; + Buffer calculatedHash; + rv = LeafHash(leafEntry, digestAlgorithmLength, digestAlgorithmId, + calculatedHash); + if (rv != Success) { + return rv; + } + for (const auto& hash : proof.inclusionPath) { + if (lastNodeIndex == 0) { + return pkix::Result::ERROR_BAD_SIGNATURE; + } + if (leafIndex % 2 == 1 || leafIndex == lastNodeIndex) { + rv = NodeHash(hash, calculatedHash, digestAlgorithmLength, + digestAlgorithmId, calculatedHash); + if (rv != Success) { + return rv; + } + if (leafIndex % 2 == 0) { + while (leafIndex % 2 == 0 && lastNodeIndex > 0) { + leafIndex >>= 1; + lastNodeIndex >>= 1; + } + } + } else { + rv = NodeHash(calculatedHash, hash, digestAlgorithmLength, + digestAlgorithmId, calculatedHash); + if (rv != Success) { + return rv; + } + } + leafIndex >>= 1; + lastNodeIndex >>= 1; + } + if (lastNodeIndex != 0) { + return pkix::Result::ERROR_BAD_SIGNATURE; + } + assert(calculatedHash.size() == digestAlgorithmLength); + if (calculatedHash.size() != digestAlgorithmLength) { + return pkix::Result::FATAL_ERROR_LIBRARY_FAILURE; + } + if (memcmp(calculatedHash.data(), expectedRootHash.UnsafeGetData(), + digestAlgorithmLength) != 0) { + return pkix::Result::ERROR_BAD_SIGNATURE; + } + return Success; +} + +} // namespace ct +} // namespace mozilla diff --git a/security/ct/BTVerifier.h b/security/ct/BTVerifier.h new file mode 100644 index 0000000000..543493797c --- /dev/null +++ b/security/ct/BTVerifier.h @@ -0,0 +1,46 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#ifndef BTVerifier_h +#define BTVerifier_h + +#include "BTTypes.h" +#include "mozpkix/Input.h" +#include "mozpkix/Result.h" +#include "mozpkix/pkixder.h" +#include "mozpkix/pkixtypes.h" + +namespace mozilla { +namespace ct { + +// Given an encoded subject public key info, a digest algorithm, a public key +// algorithm, and an encoded signed tree head (SignedTreeHeadV2 as defined in +// RFC 6962-bis), decodes and verifies the STH. Currently only ECDSA keys are +// supported. SHA-256 and SHA-512 are supported, but according to the +// specification only SHA-256 should be supported at this time. +pkix::Result DecodeAndVerifySignedTreeHead( + pkix::Input signerSubjectPublicKeyInfo, + pkix::DigestAlgorithm digestAlgorithm, + pkix::der::PublicKeyAlgorithm publicKeyAlg, pkix::Input signedTreeHeadInput, + SignedTreeHeadDataV2& signedTreeHead); + +// Decodes an Inclusion Proof (InclusionProofDataV2 as defined in RFC +// 6962-bis). This consumes the entirety of the input. +pkix::Result DecodeInclusionProof(pkix::Input input, + InclusionProofDataV2& output); +// Given a decoded Inclusion Proof (InclusionProofDataV2 as per RFC 6962-bis), +// the corresponding leaf data entry, and the expected root hash, returns +// Success if the proof verifies correctly. +// (https://tools.ietf.org/html/draft-ietf-trans-rfc6962-bis-28#section-2.1.3.2) +pkix::Result VerifyInclusionProof(const InclusionProofDataV2& proof, + pkix::Input leafEntry, + pkix::Input expectedRootHash, + pkix::DigestAlgorithm digestAlgorithm); + +} // namespace ct +} // namespace mozilla + +#endif // BTVerifier_h diff --git a/security/ct/Buffer.cpp b/security/ct/Buffer.cpp new file mode 100644 index 0000000000..09f39e6212 --- /dev/null +++ b/security/ct/Buffer.cpp @@ -0,0 +1,20 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include "Buffer.h" + +#include <string.h> + +namespace mozilla { + +bool operator==(const ct::Buffer& a, const ct::Buffer& b) { + return (a.empty() && b.empty()) || + (a.size() == b.size() && memcmp(a.data(), b.data(), a.size()) == 0); +} + +bool operator!=(const ct::Buffer& a, const ct::Buffer& b) { return !(a == b); } + +} // namespace mozilla diff --git a/security/ct/Buffer.h b/security/ct/Buffer.h new file mode 100644 index 0000000000..20d5a2e25a --- /dev/null +++ b/security/ct/Buffer.h @@ -0,0 +1,21 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#ifndef Buffer_h +#define Buffer_h + +#include <vector> +#include <cstdint> + +namespace mozilla { +namespace ct { + +typedef std::vector<uint8_t> Buffer; + +} +} // namespace mozilla + +#endif // Buffer_h diff --git a/security/ct/CTDiversityPolicy.cpp b/security/ct/CTDiversityPolicy.cpp new file mode 100644 index 0000000000..dbb80c1321 --- /dev/null +++ b/security/ct/CTDiversityPolicy.cpp @@ -0,0 +1,39 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include "CTDiversityPolicy.h" + +namespace mozilla { +namespace ct { + +typedef pkix::Result Result; + +void GetCTLogOperatorsFromVerifiedSCTList(const VerifiedSCTList& list, + CTLogOperatorList& operators) { + operators.clear(); + for (const VerifiedSCT& verifiedSct : list) { + CTLogOperatorId sctLogOperatorId = verifiedSct.logOperatorId; + bool alreadyAdded = false; + for (CTLogOperatorId id : operators) { + if (id == sctLogOperatorId) { + alreadyAdded = true; + break; + } + } + if (!alreadyAdded) { + operators.push_back(sctLogOperatorId); + } + } +} + +Result CTDiversityPolicy::GetDependentOperators( + const nsTArray<nsTArray<uint8_t>>& builtChain, + const CTLogOperatorList& operators, CTLogOperatorList& dependentOperators) { + return pkix::Success; +} + +} // namespace ct +} // namespace mozilla diff --git a/security/ct/CTDiversityPolicy.h b/security/ct/CTDiversityPolicy.h new file mode 100644 index 0000000000..783e5f6db1 --- /dev/null +++ b/security/ct/CTDiversityPolicy.h @@ -0,0 +1,43 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#ifndef CTDiversityPolicy_h +#define CTDiversityPolicy_h + +#include "CTLog.h" +#include "CTVerifyResult.h" +#include "certt.h" +#include "nsTArray.h" +#include "mozpkix/Result.h" + +namespace mozilla { +namespace ct { + +// Retuns the list of unique CT log operator IDs appearing in the provided +// list of verified SCTs. +void GetCTLogOperatorsFromVerifiedSCTList(const VerifiedSCTList& list, + CTLogOperatorList& operators); + +// Helper class used by CTPolicyEnforcer to check the CT log operators +// diversity requirements of the CT Policy. +// See CTPolicyEnforcer.h for more details. +class CTDiversityPolicy { + public: + // Given a certificate chain and a set of CT log operators, + // returns the subset of log operators that are dependent on the CA + // issuing the certificate (as defined by the CT Policy). + // + // NOTE: TBD, PENDING FINALIZATION OF MOZILLA CT POLICY. + pkix::Result GetDependentOperators( + const nsTArray<nsTArray<uint8_t>>& builtChain, + const CTLogOperatorList& operators, + CTLogOperatorList& dependentOperators); +}; + +} // namespace ct +} // namespace mozilla + +#endif // CTDiversityPolicy_h diff --git a/security/ct/CTKnownLogs.h b/security/ct/CTKnownLogs.h new file mode 100644 index 0000000000..d3c04b1b52 --- /dev/null +++ b/security/ct/CTKnownLogs.h @@ -0,0 +1,341 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +/* This file was automatically generated by getCTKnownLogs.py. */ + +#ifndef CTKnownLogs_h +#define CTKnownLogs_h + +#include "CTLog.h" + +#include <stddef.h> + +struct CTLogInfo { + // See bug 1338873 about making these fields const. + const char* name; + // Index within kCTLogOperatorList. + mozilla::ct::CTLogStatus status; + // 0 for qualified logs, disqualification time for disqualified logs + // (in milliseconds, measured since the epoch, ignoring leap seconds). + uint64_t disqualificationTime; + size_t operatorIndex; + const char* key; + size_t keyLength; +}; + +struct CTLogOperatorInfo { + // See bug 1338873 about making these fields const. + const char* name; + mozilla::ct::CTLogOperatorId id; +}; + +const CTLogInfo kCTLogList[] = { + {"Google 'Argon2023' log", mozilla::ct::CTLogStatus::Included, + 0, // no disqualification time + 0, // operated by Google + "\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\xd0\x90\x8f\x64\x52\x4e\x42\xac\x84" + "\xb6\x2e\x4c\xf2\x3d\x77\x00\xb3\x77\x08\x05\x47\xaa\x45\x4c\xe3\x2c\x8e" + "\x70\xa5\x82\xbb\x6c\xb2\x7b\x9c\x98\x7a\xa0\xe9\x11\x76\x28\x00\xb2\x20" + "\xb4\xcd\xd3\x98\x7b\x4d\x96\x27\xe6\xb7\xee\x22\x6a\xd1\xb0\x2e\x91\x77" + "\x78", + 91}, + {"Google 'Argon2024' log", mozilla::ct::CTLogStatus::Included, + 0, // no disqualification time + 0, // operated by Google + "\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\x1d\xb9\x6c\xa9\xcb\x69\x94\xc5\x5c" + "\xe6\xb6\xa6\x03\xbb\xd2\xb8\xdc\x54\x43\x17\x28\x99\x0c\x06\x01\x50\x1d" + "\x9d\x64\xc0\x59\x46\x2b\xdc\xc8\x03\x1d\x05\xb4\x2d\xa8\x09\xf7\x99\x41" + "\xed\x04\xfb\xe5\x57\xba\x26\x04\xf6\x11\x52\xce\x14\x65\x3b\x2f\x76\x2b" + "\xc0", + 91}, + {"Google 'Xenon2023' log", mozilla::ct::CTLogStatus::Included, + 0, // no disqualification time + 0, // operated by Google + "\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\x72\x16\x3e\x0b\xef\xef\xce\x3e\x60" + "\xdd\x95\xcb\x63\x7a\xb9\xa9\x8d\x4a\x6f\x6c\xdc\x61\x80\xa6\x45\x5e\x2f" + "\x83\xac\x94\xf3\x85\x88\xd0\xa5\x74\xd0\x7b\x8e\xff\xc5\xee\x42\xa2\xf0" + "\x2d\x93\xe3\xc2\xd0\xb2\x99\xe2\xe1\x42\xe9\xd2\xc6\x00\x27\x69\x74\xae" + "\xce", + 91}, + {"Google 'Xenon2024' log", mozilla::ct::CTLogStatus::Included, + 0, // no disqualification time + 0, // operated by Google + "\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\xb9\x60\xe0\x34\x1e\x35\xe4\x65\x00" + "\x93\x4f\x90\x09\xbd\x5a\xec\x44\xdd\x8c\x0f\xce\xed\x11\x3e\x2a\x59\x46" + "\x9a\x31\xb6\xc7\x99\xf7\xdc\xef\x3d\xcd\x8f\x86\xc2\x35\xa5\x3e\xdc\x29" + "\xba\xbb\xf2\x54\xe2\xa8\x0c\x83\x08\x51\x06\xde\x21\x6d\x36\x50\x8e\x38" + "\x4d", + 91}, + {"Google 'Icarus' log", mozilla::ct::CTLogStatus::Included, + 0, // no disqualification time + 0, // operated by Google + "\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\x4e\xd2\xbc\xbf\xb3\x08\x0a\xf7\xb9" + "\xea\xa4\xc7\x1c\x38\x61\x04\xeb\x95\xe0\x89\x54\x68\x44\xb1\x66\xbc\x82" + "\x7e\x4f\x50\x6c\x6f\x5c\xa3\xf0\xaa\x3e\xf4\xec\x80\xf0\xdb\x0a\x9a\x7a" + "\xa0\x5b\x72\x00\x7c\x25\x0e\x19\xef\xaf\xb2\x62\x8d\x74\x43\xf4\x26\xf6" + "\x14", + 91}, + {"Google 'Pilot' log", mozilla::ct::CTLogStatus::Included, + 0, // no disqualification time + 0, // operated by Google + "\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\x7d\xa8\x4b\x12\x29\x80\xa3\x3d\xad" + "\xd3\x5a\x77\xb8\xcc\xe2\x88\xb3\xa5\xfd\xf1\xd3\x0c\xcd\x18\x0c\xe8\x41" + "\x46\xe8\x81\x01\x1b\x15\xe1\x4b\xf1\x1b\x62\xdd\x36\x0a\x08\x18\xba\xed" + "\x0b\x35\x84\xd0\x9e\x40\x3c\x2d\x9e\x9b\x82\x65\xbd\x1f\x04\x10\x41\x4c" + "\xa0", + 91}, + {"Google 'Rocketeer' log", mozilla::ct::CTLogStatus::Included, + 0, // no disqualification time + 0, // operated by Google + "\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\x20\x5b\x18\xc8\x3c\xc1\x8b\xb3\x31" + "\x08\x00\xbf\xa0\x90\x57\x2b\xb7\x47\x8c\x6f\xb5\x68\xb0\x8e\x90\x78\xe9" + "\xa0\x73\xea\x4f\x28\x21\x2e\x9c\xc0\xf4\x16\x1b\xaa\xf9\xd5\xd7\xa9\x80" + "\xc3\x4e\x2f\x52\x3c\x98\x01\x25\x46\x24\x25\x28\x23\x77\x2d\x05\xc2\x40" + "\x7a", + 91}, + {"Google 'Skydiver' log", mozilla::ct::CTLogStatus::Included, + 0, // no disqualification time + 0, // operated by Google + "\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\x12\x6c\x86\x0e\xf6\x17\xb1\x12\x6c" + "\x37\x25\xd2\xad\x87\x3d\x0e\x31\xec\x21\xad\xb1\xcd\xbe\x14\x47\xb6\x71" + "\x56\x85\x7a\x9a\xb7\x3d\x89\x90\x7b\xc6\x32\x3a\xf8\xda\xce\x8b\x01\xfe" + "\x3f\xfc\x71\x91\x19\x8e\x14\x6e\x89\x7a\x5d\xb4\xab\x7e\xe1\x4e\x1e\x7c" + "\xac", + 91}, + {"Cloudflare 'Nimbus2023' Log", mozilla::ct::CTLogStatus::Included, + 0, // no disqualification time + 1, // operated by Cloudflare + "\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\x8b\xff\x2d\x92\x18\xcb\x46\x9d\x12" + "\x5e\xb9\x59\x75\x3c\xcd\x91\x37\x7a\x1e\xa9\x9c\x99\x78\x83\x27\x3d\xdf" + "\x01\xd5\x8b\x80\xe8\x63\x9a\xfe\x26\xa2\x1b\xd1\x87\x05\xee\x97\xd6\xe0" + "\x5b\x43\x83\x81\x1c\x02\xf5\x41\x80\x80\x7f\xef\xa4\x61\xcf\xbc\x84\xb5" + "\xa8", + 91}, + {"Cloudflare 'Nimbus2024' Log", mozilla::ct::CTLogStatus::Included, + 0, // no disqualification time + 1, // operated by Cloudflare + "\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\x77\xb1\x9b\x7b\x8f\xe6\x8b\x35\xfe" + "\x3a\x92\x29\x2d\xac\x8a\x8d\x51\x8a\x25\xfc\x93\xb6\xd7\xa0\x8b\x29\x37" + "\x71\x1d\x33\xca\xcc\x33\xea\x28\xb9\x1f\xe2\xac\xc3\xa9\x5d\xdd\x97\xbe" + "\xf6\x9e\x94\x25\xdd\x36\x81\xd1\xeb\x5d\x29\xc3\x2b\x44\xf1\x5b\xca\x15" + "\x48", + 91}, + {"DigiCert Yeti2023 Log", mozilla::ct::CTLogStatus::Included, + 0, // no disqualification time + 2, // operated by DigiCert + "\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\x7d\x0d\x03\xb1\xd5\x98\x8a\xdc\xf0" + "\x15\x3b\xc6\xdc\x5e\x0d\x6e\x3f\x0d\xbf\x95\xc8\x55\x8c\xd0\xa6\x4c\x96" + "\xb1\x4e\x27\xb9\x26\x25\x99\xcc\x2b\x02\x9e\xa6\xd3\xdd\x9f\xb1\xd5\xc4" + "\xc3\xac\x35\x04\x07\x87\x97\x36\xaa\xad\x28\x0d\x7f\x2b\xd9\x68\x9f\x72" + "\xd1", + 91}, + {"DigiCert Yeti2024 Log", mozilla::ct::CTLogStatus::Included, + 0, // no disqualification time + 2, // operated by DigiCert + "\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\x57\xb8\xc1\x6f\x30\xa4\x7f\x2e\xe4" + "\xf0\xd0\xd9\x60\x62\x13\x95\xe3\x7a\xe3\x4e\x53\xc3\xb3\xb8\x73\x85\xc1" + "\x18\x0d\x23\x0e\x58\x84\xd2\x78\xef\x9b\xb3\x1e\x2c\x1a\xde\xc1\x8f\x81" + "\x1b\x19\x44\x58\xb7\x00\x77\x60\x20\x1a\x72\xd8\x82\xde\xae\x9e\xb1\xc6" + "\x4b", + 91}, + {"DigiCert Yeti2025 Log", mozilla::ct::CTLogStatus::Included, + 0, // no disqualification time + 2, // operated by DigiCert + "\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\xdf\x95\x00\x5e\x10\xc1\x01\xf7\x37" + "\xe3\x10\x74\xd1\xff\xb2\xca\x90\xed\x32\x99\x5f\x0c\x39\xfe\xa1\xd1\x13" + "\x11\xac\xd1\xb3\x73\x93\x20\xc2\x13\x3c\x4c\xb5\x7a\x52\x86\x86\x3d\xe3" + "\x95\x24\x7c\xd8\x91\x98\x48\x3b\xf0\xf0\xdf\x21\xf1\xb0\x81\x5a\x59\x25" + "\x43", + 91}, + {"DigiCert Nessie2023 Log", mozilla::ct::CTLogStatus::Included, + 0, // no disqualification time + 2, // operated by DigiCert + "\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\x11\x7b\xbc\x89\x0c\x12\x09\x14\x9f" + "\xd8\x26\xc8\x4c\x6a\x54\xa4\x1b\x45\x56\xdf\x3e\x23\x42\x14\xd1\xdd\x42" + "\xdf\xa2\xdf\x7b\x5f\x9f\x6f\x07\x5a\x23\x46\x79\x16\x4b\x5f\x33\x67\xc1" + "\xa0\x8d\x5b\x5c\x17\x75\xf2\x4d\xa0\x80\xa1\x98\x1a\x07\x59\x06\x02\xca" + "\x4e", + 91}, + {"DigiCert Nessie2024 Log", mozilla::ct::CTLogStatus::Included, + 0, // no disqualification time + 2, // operated by DigiCert + "\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\x2d\xfc\xa2\x7b\x36\xbf\x56\x91\xe9" + "\xfe\x3f\xe8\x3d\xfc\xc3\xa7\xe0\x61\x52\xea\x2c\xe9\x05\xa3\x9f\x27\x17" + "\x81\x05\x70\x6b\x81\x61\x44\x8a\xf8\x3b\x10\x80\x42\xed\x03\x2f\x00\x50" + "\x21\xfc\x41\x54\x84\xa3\x54\xd5\x2e\xb2\x7a\x16\x4b\x2a\x1f\x2b\x66\x04" + "\x2b", + 91}, + {"DigiCert Nessie2025 Log", mozilla::ct::CTLogStatus::Included, + 0, // no disqualification time + 2, // operated by DigiCert + "\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\xf2\xf0\xf0\xa7\x8b\x81\x2e\x09\x39" + "\x3b\x9f\x42\xda\x38\x44\x5f\xb4\xcc\xed\x36\xbb\xd8\x43\x7f\x16\x49\x57" + "\x87\x04\x7f\xa5\x01\x34\xf7\xe8\x68\x3f\xb7\x78\x1f\x60\x66\x2d\x67\x9a" + "\x75\x80\xb7\x53\xa7\x85\xd5\xbc\xab\x47\x06\x55\xdb\xb5\xdf\x88\xa1\x6f" + "\x38", + 91}, + {"Sectigo 'Sabre' CT log", mozilla::ct::CTLogStatus::Included, + 0, // no disqualification time + 3, // operated by Sectigo + "\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\xf2\x6f\xd2\x89\x0f\x3f\xc5\xf8\x87" + "\x1e\xab\x65\xb3\xd9\xbb\x17\x23\x8c\x06\x0e\x09\x55\x96\x3d\x0a\x08\xa2" + "\xc5\x71\xb3\xd1\xa9\x2f\x28\x3e\x83\x10\xbf\x12\xd0\x44\x66\x15\xef\x54" + "\xe1\x98\x80\xd0\xce\x24\x6d\x3e\x67\x9a\xe9\x37\x23\xce\x52\x93\x86\xda" + "\x80", + 91}, + {"Sectigo 'Mammoth' CT log", mozilla::ct::CTLogStatus::Included, + 0, // no disqualification time + 3, // operated by Sectigo + "\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\xef\xe4\x7d\x74\x2e\x15\x15\xb6\xe9" + "\xbb\x23\x8b\xfb\x2c\xb5\xe1\xc7\x80\x98\x47\xfb\x40\x69\x68\xfc\x49\xad" + "\x61\x4e\x83\x47\x3c\x1a\xb7\x8d\xdf\xff\x7b\x30\xb4\xba\xff\x2f\xcb\xa0" + "\x14\xe3\xad\xd5\x85\x3f\x44\x59\x8c\x8c\x60\x8b\xd7\xb8\xb1\xbf\xae\x8c" + "\x67", + 91}, + {"Let's Encrypt 'Oak2023' log", mozilla::ct::CTLogStatus::Included, + 0, // no disqualification time + 4, // operated by Let's Encrypt + "\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\xb3\x3d\x0e\x78\xbe\xe3\xad\x5c\x44" + "\x5c\x9b\xbe\xa3\x84\x16\x41\x82\xca\xca\x89\x17\x1e\x23\xce\x38\xa5\x54" + "\x2f\x7f\xd3\x34\x51\x6a\xb9\x5c\xc3\x49\xea\xfb\x91\x9d\xe0\x8a\x3c\x73" + "\x06\x9f\x7c\x65\x38\x11\x80\xc4\x9a\x5a\x00\xa6\x67\xc3\x83\xef\x89\x85" + "\x51", + 91}, + {"Let's Encrypt 'Oak2024H1' log", mozilla::ct::CTLogStatus::Included, + 0, // no disqualification time + 4, // operated by Let's Encrypt + "\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\x56\x43\xd7\x7e\x7b\xd4\x72\xb7\xba" + "\xa9\x51\xbd\x36\x93\xb7\xe9\xb5\x92\x0f\xea\x5e\xb7\x45\xa3\x92\xfd\xc9" + "\xa5\x3c\x80\xac\x1a\x20\xef\x25\x2f\xb8\xe1\x20\xf7\xa8\x3a\x2e\x07\x8d" + "\xe6\xeb\xa4\xe2\x7d\x24\x63\x9f\x46\xbf\x94\x73\x52\x8d\x96\xae\xa9\x26" + "\xfd", + 91}, + {"Let's Encrypt 'Oak2024H2' log", mozilla::ct::CTLogStatus::Included, + 0, // no disqualification time + 4, // operated by Let's Encrypt + "\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\xd7\x73\xd6\x53\x47\xe9\xf3\xc9\xd5" + "\x7c\x16\xc2\xd6\x8f\x70\x65\xfa\xf2\x51\x36\xa9\x13\x80\x2f\xed\xf9\x94" + "\xd3\x5a\x8b\xe8\x4f\x33\xcf\xc3\xd3\x89\xd4\x5f\x5a\x66\x89\xba\x20\x1f" + "\x71\xcb\xca\xbb\x9f\x9f\xf3\x5c\x2d\x1e\xa3\x81\x59\xaf\x92\xb3\x6d\x30" + "\x68", + 91}, + {"Trust Asia Log2023", mozilla::ct::CTLogStatus::Included, + 0, // no disqualification time + 5, // operated by TrustAsia + "\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\xa4\x11\x52\xdb\x17\x41\x4e\x90\xd4" + "\x56\x51\x12\x30\x52\xf8\x9b\x03\xcf\x4c\x9f\xf8\x2e\x38\xb5\xf1\x5a\xba" + "\xfa\x38\xb9\xd2\x8f\x1a\x81\xda\x95\xcc\x33\xec\x21\x28\x66\xc6\x56\x3e" + "\x60\x36\x21\x20\xd9\xd4\xac\x5d\xfa\x5c\x19\xa1\x05\x7d\xfe\x20\x23\xfc" + "\xf5", + 91}, + {"Trust Asia Log2024-2", mozilla::ct::CTLogStatus::Included, + 0, // no disqualification time + 5, // operated by TrustAsia + "\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\xa7\x64\xe2\x79\x81\x3f\x61\xd7\xec" + "\xc6\xf8\x65\x28\x1d\xa0\xb4\x66\x33\xc3\x25\xd5\x0a\x95\x78\x9c\x8f\xfe" + "\xa4\x2a\xd8\x8f\x7e\x72\xe0\xfe\xa8\x7f\xf8\xb1\x2d\x85\xc0\x8e\x12\x74" + "\x0d\x2f\x8c\xab\xd7\x7f\x7a\x1e\xd9\x84\x33\x39\xe8\xfd\x89\x5f\x96\x48" + "\x08", + 91}, +#ifdef DEBUG + {"Mozilla Test RSA Log 1", mozilla::ct::CTLogStatus::Included, + 0, // no disqualification time + 6, // operated by Mozilla Test Org 1 + "\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\xba\x88\x51" + "\xa8\x44\x8e\x16\xd6\x41\xfd\x6e\xb6\x88\x06\x36\x10\x3d\x3c\x13\xd9\xea" + "\xe4\x35\x4a\xb4\xec\xf5\x68\x57\x6c\x24\x7b\xc1\xc7\x25\xa8\xe0\xd8\x1f" + "\xbd\xb1\x9c\x06\x9b\x6e\x1a\x86\xf2\x6b\xe2\xaf\x5a\x75\x6b\x6a\x64\x71" + "\x08\x7a\xa5\x5a\xa7\x45\x87\xf7\x1c\xd5\x24\x9c\x02\x7e\xcd\x43\xfc\x1e" + "\x69\xd0\x38\x20\x29\x93\xab\x20\xc3\x49\xe4\xdb\xb9\x4c\xc2\x6b\x6c\x0e" + "\xed\x15\x82\x0f\xf1\x7e\xad\x69\x1a\xb1\xd3\x02\x3a\x8b\x2a\x41\xee\xa7" + "\x70\xe0\x0f\x0d\x8d\xfd\x66\x0b\x2b\xb0\x24\x92\xa4\x7d\xb9\x88\x61\x79" + "\x90\xb1\x57\x90\x3d\xd2\x3b\xc5\xe0\xb8\x48\x1f\xa8\x37\xd3\x88\x43\xef" + "\x27\x16\xd8\x55\xb7\x66\x5a\xaa\x7e\x02\x90\x2f\x3a\x7b\x10\x80\x06\x24" + "\xcc\x1c\x6c\x97\xad\x96\x61\x5b\xb7\xe2\x96\x12\xc0\x75\x31\xa3\x0c\x91" + "\xdd\xb4\xca\xf7\xfc\xad\x1d\x25\xd3\x09\xef\xb9\x17\x0e\xa7\x68\xe1\xb3" + "\x7b\x2f\x22\x6f\x69\xe3\xb4\x8a\x95\x61\x1d\xee\x26\xd6\x25\x9d\xab\x91" + "\x08\x4e\x36\xcb\x1c\x24\x04\x2c\xbf\x16\x8b\x2f\xe5\xf1\x8f\x99\x17\x31" + "\xb8\xb3\xfe\x49\x23\xfa\x72\x51\xc4\x31\xd5\x03\xac\xda\x18\x0a\x35\xed" + "\x8d\x02\x03\x01\x00\x01", + 294}, +#endif // DEBUG +#ifdef DEBUG + {"Mozilla Test EC Log", mozilla::ct::CTLogStatus::Included, + 0, // no disqualification time + 6, // operated by Mozilla Test Org 1 + "\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\x4f\xbf\xbb\xbb\x61\xe0\xf8\xf9\xb1" + "\xa6\x0a\x59\xac\x87\x04\xe2\xec\x05\x0b\x42\x3e\x3c\xf7\x2e\x92\x3f\x2c" + "\x4f\x79\x4b\x45\x5c\x2a\x69\xd2\x33\x45\x6c\x36\xc4\x11\x9d\x07\x06\xe0" + "\x0e\xed\xc8\xd1\x93\x90\xd7\x99\x1b\x7b\x2d\x07\xa3\x04\xea\xa0\x4a\xa6" + "\xc0", + 91}, +#endif // DEBUG +#ifdef DEBUG + {"Mozilla Test RSA Log 2", mozilla::ct::CTLogStatus::Included, + 0, // no disqualification time + 7, // operated by Mozilla Test Org 2 + "\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\xc1\x75\xc6" + "\x52\x66\x09\x9f\x77\x08\x2a\x67\x91\xf1\xb8\x76\xc3\x7f\x5c\xe5\x38\xb0" + "\x6c\x4a\xcd\x22\xb1\xcb\xd4\x6f\xa6\x5a\xda\x2a\xdd\x41\xc8\xc2\x49\x8a" + "\xc4\xa3\xb3\xc1\xf6\x14\x87\xf4\x1b\x69\x89\x41\xbd\x80\xa5\x1c\x3c\x12" + "\x02\x44\xc5\x84\xa4\xc4\x48\x33\x05\xe5\x13\x8c\x01\x06\xcf\x08\xbe\x9a" + "\x86\x27\x60\xba\xe6\xa2\xe8\xf3\x6f\x23\xc5\xd9\x83\x13\xb9\xdf\xaf\x37" + "\x83\x45\xda\xce\x51\xd4\xd6\xdc\xd2\xa6\xcb\x3c\xc7\x06\xeb\xcd\x30\x70" + "\xec\x98\xcc\xe4\x0a\xa5\x91\xd7\x29\x5a\x7f\x71\xc5\xbe\x66\x69\x1d\x2b" + "\x2d\xfe\xc8\x49\x44\x59\x0b\xc5\xa3\xea\x49\xfd\x93\xb1\xd7\x53\x40\x5f" + "\x17\x73\x76\x99\x95\x86\x66\x25\x47\x97\xed\x42\x69\x08\x88\x08\x11\x42" + "\x20\x69\x98\x8a\x43\xfe\xe4\x8c\xe6\x87\x81\xdd\x22\xb6\xa6\x9c\xd2\x83" + "\x75\x13\x1f\x93\x2b\x12\x8c\xe2\x86\xfa\x7d\x25\x1c\x06\x2a\xd2\x7e\xf0" + "\x16\xf1\x87\xcd\xd5\x4e\x83\x2b\x35\xb8\x93\x0f\x74\xba\x90\xaa\x8b\xc7" + "\x61\x67\x24\x2a\xb1\xfd\x6d\x62\x14\x0d\x18\xc4\xc0\xb8\xc6\x8f\xc3\x74" + "\x84\x57\x32\x4a\xd7\xde\x86\xe6\x55\x2f\x1d\x1e\x19\x1d\x71\x21\x68\xd3" + "\xbb\x02\x03\x01\x00\x01", + 294}, +#endif // DEBUG +}; + +const CTLogOperatorInfo kCTLogOperatorList[] = { + {"Google", 0}, + {"Cloudflare", 1}, + {"DigiCert", 2}, + {"Sectigo", 3}, + {"Let's Encrypt", 4}, + {"TrustAsia", 5}, +#ifdef DEBUG + {"Mozilla Test Org 1", 6}, +#endif // DEBUG +#ifdef DEBUG + {"Mozilla Test Org 2", 7}, +#endif // DEBUG +}; + +#endif // CTKnownLogs_h diff --git a/security/ct/CTLog.h b/security/ct/CTLog.h new file mode 100644 index 0000000000..3d59dbd296 --- /dev/null +++ b/security/ct/CTLog.h @@ -0,0 +1,38 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#ifndef CTLog_h +#define CTLog_h + +#include <stdint.h> +#include <vector> + +namespace mozilla { +namespace ct { + +// Signed integer sufficient to store the numeric ID of CT log operators +// as assigned at https://www.certificate-transparency.org/known-logs . +// The assigned IDs are 0-based positive integers, so you can use special +// values (such as -1) to indicate a "null" or unknown log ID. +typedef int16_t CTLogOperatorId; + +typedef std::vector<CTLogOperatorId> CTLogOperatorList; + +// Current status of a CT log in regard to its inclusion in the +// Known Logs List such as https://www.certificate-transparency.org/known-logs +enum class CTLogStatus { + // Status unknown or unavailable. + Unknown, + // Included in the list of known logs. + Included, + // Previously included, but disqualified at some point of time. + Disqualified, +}; + +} // namespace ct +} // namespace mozilla + +#endif // CTLog_h diff --git a/security/ct/CTLogVerifier.cpp b/security/ct/CTLogVerifier.cpp new file mode 100644 index 0000000000..6727192622 --- /dev/null +++ b/security/ct/CTLogVerifier.cpp @@ -0,0 +1,305 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include "CTLogVerifier.h" + +#include <stdint.h> + +#include "CTSerialization.h" +#include "hasht.h" +#include "mozpkix/pkixnss.h" +#include "mozpkix/pkixutil.h" + +namespace mozilla { +namespace ct { + +using namespace mozilla::pkix; + +// A TrustDomain used to extract the SCT log signature parameters +// given its subjectPublicKeyInfo. +// Only RSASSA-PKCS1v15 with SHA-256 and ECDSA (using the NIST P-256 curve) +// with SHA-256 are allowed. +// RSA keys must be at least 2048 bits. +// See See RFC 6962, Section 2.1.4. +class SignatureParamsTrustDomain final : public TrustDomain { + public: + SignatureParamsTrustDomain() + : mSignatureAlgorithm(DigitallySigned::SignatureAlgorithm::Anonymous) {} + + Result GetCertTrust(EndEntityOrCA, const CertPolicyId&, Input, + TrustLevel&) override { + return Result::FATAL_ERROR_LIBRARY_FAILURE; + } + + Result FindIssuer(Input, IssuerChecker&, Time) override { + return Result::FATAL_ERROR_LIBRARY_FAILURE; + } + + Result CheckRevocation(EndEntityOrCA, const CertID&, Time, Duration, + const Input*, const Input*, const Input*) override { + return Result::FATAL_ERROR_LIBRARY_FAILURE; + } + + Result IsChainValid(const DERArray&, Time, const CertPolicyId&) override { + return Result::FATAL_ERROR_LIBRARY_FAILURE; + } + + Result DigestBuf(Input, DigestAlgorithm, uint8_t*, size_t) override { + return Result::FATAL_ERROR_LIBRARY_FAILURE; + } + + Result CheckSignatureDigestAlgorithm(DigestAlgorithm, EndEntityOrCA, + Time) override { + return Result::FATAL_ERROR_LIBRARY_FAILURE; + } + + Result CheckECDSACurveIsAcceptable(EndEntityOrCA, NamedCurve curve) override { + assert(mSignatureAlgorithm == + DigitallySigned::SignatureAlgorithm::Anonymous); + if (curve != NamedCurve::secp256r1) { + return Result::ERROR_UNSUPPORTED_ELLIPTIC_CURVE; + } + mSignatureAlgorithm = DigitallySigned::SignatureAlgorithm::ECDSA; + return Success; + } + + Result VerifyECDSASignedData(Input, DigestAlgorithm, Input, Input) override { + return Result::FATAL_ERROR_LIBRARY_FAILURE; + } + + Result CheckRSAPublicKeyModulusSizeInBits( + EndEntityOrCA, unsigned int modulusSizeInBits) override { + assert(mSignatureAlgorithm == + DigitallySigned::SignatureAlgorithm::Anonymous); + // Require RSA keys of at least 2048 bits. See RFC 6962, Section 2.1.4. + if (modulusSizeInBits < 2048) { + return Result::ERROR_INADEQUATE_KEY_SIZE; + } + mSignatureAlgorithm = DigitallySigned::SignatureAlgorithm::RSA; + return Success; + } + + Result VerifyRSAPKCS1SignedData(Input, DigestAlgorithm, Input, + Input) override { + return Result::FATAL_ERROR_LIBRARY_FAILURE; + } + + Result VerifyRSAPSSSignedData(Input, DigestAlgorithm, Input, Input) override { + return Result::FATAL_ERROR_LIBRARY_FAILURE; + } + + Result CheckValidityIsAcceptable(Time, Time, EndEntityOrCA, + KeyPurposeId) override { + return Result::FATAL_ERROR_LIBRARY_FAILURE; + } + + Result NetscapeStepUpMatchesServerAuth(Time, bool&) override { + return Result::FATAL_ERROR_LIBRARY_FAILURE; + } + + void NoteAuxiliaryExtension(AuxiliaryExtension, Input) override {} + + DigitallySigned::SignatureAlgorithm mSignatureAlgorithm; +}; + +CTLogVerifier::CTLogVerifier() + : mSignatureAlgorithm(DigitallySigned::SignatureAlgorithm::Anonymous), + mOperatorId(-1), + mDisqualified(false), + mDisqualificationTime(UINT64_MAX) {} + +Result CTLogVerifier::Init(Input subjectPublicKeyInfo, + CTLogOperatorId operatorId, CTLogStatus logStatus, + uint64_t disqualificationTime) { + switch (logStatus) { + case CTLogStatus::Included: + mDisqualified = false; + mDisqualificationTime = UINT64_MAX; + break; + case CTLogStatus::Disqualified: + mDisqualified = true; + mDisqualificationTime = disqualificationTime; + break; + case CTLogStatus::Unknown: + default: + assert(false); + return Result::FATAL_ERROR_INVALID_ARGS; + } + + SignatureParamsTrustDomain trustDomain; + Result rv = CheckSubjectPublicKeyInfo(subjectPublicKeyInfo, trustDomain, + EndEntityOrCA::MustBeEndEntity); + if (rv != Success) { + return rv; + } + mSignatureAlgorithm = trustDomain.mSignatureAlgorithm; + + InputToBuffer(subjectPublicKeyInfo, mSubjectPublicKeyInfo); + + if (mSignatureAlgorithm == DigitallySigned::SignatureAlgorithm::ECDSA) { + SECItem spkiSECItem = { + siBuffer, mSubjectPublicKeyInfo.data(), + static_cast<unsigned int>(mSubjectPublicKeyInfo.size())}; + UniqueCERTSubjectPublicKeyInfo spki( + SECKEY_DecodeDERSubjectPublicKeyInfo(&spkiSECItem)); + if (!spki) { + return MapPRErrorCodeToResult(PR_GetError()); + } + mPublicECKey.reset(SECKEY_ExtractPublicKey(spki.get())); + if (!mPublicECKey) { + return MapPRErrorCodeToResult(PR_GetError()); + } + UniquePK11SlotInfo slot(PK11_GetInternalSlot()); + if (!slot) { + return MapPRErrorCodeToResult(PR_GetError()); + } + CK_OBJECT_HANDLE handle = + PK11_ImportPublicKey(slot.get(), mPublicECKey.get(), false); + if (handle == CK_INVALID_HANDLE) { + return MapPRErrorCodeToResult(PR_GetError()); + } + } else { + mPublicECKey.reset(nullptr); + } + + mKeyId.resize(SHA256_LENGTH); + rv = DigestBufNSS(subjectPublicKeyInfo, DigestAlgorithm::sha256, + mKeyId.data(), mKeyId.size()); + if (rv != Success) { + return rv; + } + + mOperatorId = operatorId; + return Success; +} + +Result CTLogVerifier::Verify(const LogEntry& entry, + const SignedCertificateTimestamp& sct) { + if (mKeyId.empty() || sct.logId != mKeyId) { + return Result::FATAL_ERROR_INVALID_ARGS; + } + if (!SignatureParametersMatch(sct.signature)) { + return Result::FATAL_ERROR_INVALID_ARGS; + } + + Buffer serializedLogEntry; + Result rv = EncodeLogEntry(entry, serializedLogEntry); + if (rv != Success) { + return rv; + } + + Input logEntryInput; + rv = BufferToInput(serializedLogEntry, logEntryInput); + if (rv != Success) { + return rv; + } + + // sct.extensions may be empty. If it is, sctExtensionsInput will remain in + // its default state, which is valid but of length 0. + Input sctExtensionsInput; + if (!sct.extensions.empty()) { + rv = sctExtensionsInput.Init(sct.extensions.data(), sct.extensions.size()); + if (rv != Success) { + return rv; + } + } + + Buffer serializedData; + rv = EncodeV1SCTSignedData(sct.timestamp, logEntryInput, sctExtensionsInput, + serializedData); + if (rv != Success) { + return rv; + } + return VerifySignature(serializedData, sct.signature.signatureData); +} + +bool CTLogVerifier::SignatureParametersMatch(const DigitallySigned& signature) { + return signature.SignatureParametersMatch( + DigitallySigned::HashAlgorithm::SHA256, mSignatureAlgorithm); +} + +static Result FasterVerifyECDSASignedDataNSS(Input data, Input signature, + UniqueSECKEYPublicKey& pubkey) { + assert(pubkey); + if (!pubkey) { + return Result::FATAL_ERROR_LIBRARY_FAILURE; + } + // The signature is encoded as a DER SEQUENCE of two INTEGERs. PK11_Verify + // expects the signature as only the two integers r and s (so no encoding - + // just two series of bytes each half as long as SECKEY_SignatureLen(pubkey)). + // DSAU_DecodeDerSigToLen converts from the former format to the latter. + SECItem derSignatureSECItem(UnsafeMapInputToSECItem(signature)); + size_t signatureLen = SECKEY_SignatureLen(pubkey.get()); + if (signatureLen == 0) { + return MapPRErrorCodeToResult(PR_GetError()); + } + UniqueSECItem signatureSECItem( + DSAU_DecodeDerSigToLen(&derSignatureSECItem, signatureLen)); + if (!signatureSECItem) { + return MapPRErrorCodeToResult(PR_GetError()); + } + SECItem dataSECItem(UnsafeMapInputToSECItem(data)); + SECStatus srv = + PK11_VerifyWithMechanism(pubkey.get(), CKM_ECDSA_SHA256, nullptr, + signatureSECItem.get(), &dataSECItem, nullptr); + if (srv != SECSuccess) { + return MapPRErrorCodeToResult(PR_GetError()); + } + return Success; +} + +Result CTLogVerifier::VerifySignature(Input data, Input signature) { + Input spki; + Result rv = BufferToInput(mSubjectPublicKeyInfo, spki); + if (rv != Success) { + return rv; + } + + switch (mSignatureAlgorithm) { + case DigitallySigned::SignatureAlgorithm::RSA: + rv = VerifyRSAPKCS1SignedDataNSS(data, DigestAlgorithm::sha256, signature, + spki, nullptr); + break; + case DigitallySigned::SignatureAlgorithm::ECDSA: + rv = FasterVerifyECDSASignedDataNSS(data, signature, mPublicECKey); + break; + // We do not expect new values added to this enum any time soon, + // so just listing all the available ones seems to be the easiest way + // to suppress warning C4061 on MSVC (which expects all values of the + // enum to be explicitly handled). + case DigitallySigned::SignatureAlgorithm::Anonymous: + case DigitallySigned::SignatureAlgorithm::DSA: + default: + assert(false); + return Result::FATAL_ERROR_INVALID_ARGS; + } + if (rv != Success) { + if (IsFatalError(rv)) { + return rv; + } + // If the error is non-fatal, we assume the signature was invalid. + return Result::ERROR_BAD_SIGNATURE; + } + return Success; +} + +Result CTLogVerifier::VerifySignature(const Buffer& data, + const Buffer& signature) { + Input dataInput; + Result rv = BufferToInput(data, dataInput); + if (rv != Success) { + return rv; + } + Input signatureInput; + rv = BufferToInput(signature, signatureInput); + if (rv != Success) { + return rv; + } + return VerifySignature(dataInput, signatureInput); +} + +} // namespace ct +} // namespace mozilla diff --git a/security/ct/CTLogVerifier.h b/security/ct/CTLogVerifier.h new file mode 100644 index 0000000000..659698b4a1 --- /dev/null +++ b/security/ct/CTLogVerifier.h @@ -0,0 +1,88 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#ifndef CTLogVerifier_h +#define CTLogVerifier_h + +#include <memory> + +#include "CTLog.h" +#include "CTUtils.h" +#include "SignedCertificateTimestamp.h" +#include "mozpkix/Input.h" +#include "mozpkix/Result.h" +#include "mozpkix/pkix.h" + +namespace mozilla { +namespace ct { + +// Verifies Signed Certificate Timestamps (SCTs) provided by a specific log +// using the public key of that log. Assumes the SCT being verified +// matches the log by log key ID and signature parameters (an error is returned +// otherwise). +// The verification functions return Success if the provided SCT has passed +// verification, ERROR_BAD_SIGNATURE if failed verification, or other result +// on error. +class CTLogVerifier { + public: + CTLogVerifier(); + + // Initializes the verifier with log-specific information. Only the public + // key is used for verification, other parameters are purely informational. + // |subjectPublicKeyInfo| is a DER-encoded SubjectPublicKeyInfo. + // |operatorId| The numeric ID of the log operator as assigned at + // https://www.certificate-transparency.org/known-logs . + // |logStatus| Either "Included" or "Disqualified". + // |disqualificationTime| Disqualification timestamp (for disqualified logs). + // An error is returned if |subjectPublicKeyInfo| refers to an unsupported + // public key. + pkix::Result Init(pkix::Input subjectPublicKeyInfo, + CTLogOperatorId operatorId, CTLogStatus logStatus, + uint64_t disqualificationTime); + + // Returns the log's key ID, which is a SHA256 hash of its public key. + // See RFC 6962, Section 3.2. + const Buffer& keyId() const { return mKeyId; } + + CTLogOperatorId operatorId() const { return mOperatorId; } + bool isDisqualified() const { return mDisqualified; } + uint64_t disqualificationTime() const { return mDisqualificationTime; } + + // Verifies that |sct| contains a valid signature for |entry|. + // |sct| must be signed by the verifier's log. + pkix::Result Verify(const LogEntry& entry, + const SignedCertificateTimestamp& sct); + + // Returns true if the signature and hash algorithms in |signature| + // match those of the log. + bool SignatureParametersMatch(const DigitallySigned& signature); + + private: + // Performs the underlying verification using the log's public key. Note + // that |signature| contains the raw signature data (i.e. without any + // DigitallySigned struct encoding). + // Returns Success if passed verification, ERROR_BAD_SIGNATURE if failed + // verification, or other result on error. + pkix::Result VerifySignature(pkix::Input data, pkix::Input signature); + pkix::Result VerifySignature(const Buffer& data, const Buffer& signature); + + // mPublicECKey works around an architectural deficiency in NSS. In the case + // of EC, if we don't create, import, and cache this key, NSS will import and + // verify it every signature verification, which is slow. For RSA, this is + // unused and will be null. + UniqueSECKEYPublicKey mPublicECKey; + Buffer mSubjectPublicKeyInfo; + Buffer mKeyId; + DigitallySigned::SignatureAlgorithm mSignatureAlgorithm; + CTLogOperatorId mOperatorId; + bool mDisqualified; + uint64_t mDisqualificationTime; +}; + +} // namespace ct +} // namespace mozilla + +#endif // CTLogVerifier_h diff --git a/security/ct/CTObjectsExtractor.cpp b/security/ct/CTObjectsExtractor.cpp new file mode 100644 index 0000000000..7285bf90d5 --- /dev/null +++ b/security/ct/CTObjectsExtractor.cpp @@ -0,0 +1,378 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include "CTObjectsExtractor.h" + +#include <limits> +#include <vector> + +#include "hasht.h" +#include "mozpkix/pkixnss.h" +#include "mozpkix/pkixutil.h" + +namespace mozilla { +namespace ct { + +using namespace mozilla::pkix; + +// Holds a non-owning pointer to a byte buffer and allows writing chunks of data +// to the buffer, placing the later chunks after the earlier ones +// in a stream-like fashion. +// Note that writing to Output always succeeds. If the internal buffer +// overflows, an error flag is turned on and you won't be able to retrieve +// the final data. +class Output { + public: + Output(uint8_t* buffer, size_t length) + : begin(buffer), + end(buffer + length), + current(begin), + overflowed(false) {} + + template <size_t N> + explicit Output(uint8_t (&buffer)[N]) : Output(buffer, N) {} + + void Write(Input data) { Write(data.UnsafeGetData(), data.GetLength()); } + + void Write(uint8_t b) { Write(&b, 1); } + + bool IsOverflowed() const { return overflowed; } + + Result GetInput(/*out*/ Input& input) const { + if (overflowed || current < begin) { + return Result::FATAL_ERROR_INVALID_STATE; + } + size_t length = static_cast<size_t>(current - begin); + return input.Init(begin, length); + } + + private: + uint8_t* begin; + uint8_t* end; + uint8_t* current; + bool overflowed; + + Output(const Output&) = delete; + void operator=(const Output&) = delete; + + void Write(const uint8_t* data, size_t length) { + if (end < current) { + overflowed = true; + } + size_t available = static_cast<size_t>(end - current); + if (available < length) { + overflowed = true; + } + if (overflowed) { + return; + } + memcpy(current, data, length); + current += length; + } +}; + +// For reference: +// +// Certificate ::= SEQUENCE { +// tbsCertificate TBSCertificate, +// signatureAlgorithm AlgorithmIdentifier, +// signatureValue BIT STRING } +// +// TBSCertificate ::= SEQUENCE { +// version [0] EXPLICIT Version DEFAULT v1, +// serialNumber CertificateSerialNumber, +// signature AlgorithmIdentifier, +// issuer Name, +// validity Validity, +// subject Name, +// subjectPublicKeyInfo SubjectPublicKeyInfo, +// issuerUniqueID [1] IMPLICIT UniqueIdentifier OPTIONAL, +// -- If present, version MUST be v2 or v3 +// subjectUniqueID [2] IMPLICIT UniqueIdentifier OPTIONAL, +// -- If present, version MUST be v2 or v3 +// extensions [3] EXPLICIT Extensions OPTIONAL +// -- If present, version MUST be v3 +// } + +// python DottedOIDToCode.py id-embeddedSctList 1.3.6.1.4.1.11129.2.4.2 +// See Section 3.3 of RFC 6962. +static const uint8_t EMBEDDED_SCT_LIST_OID[] = {0x2b, 0x06, 0x01, 0x04, 0x01, + 0xd6, 0x79, 0x02, 0x04, 0x02}; +// Maximum length of DER TLV header +static const size_t MAX_TLV_HEADER_LENGTH = 4; +// DER tag of the "extensions [3]" field from TBSCertificate +static const uint8_t EXTENSIONS_CONTEXT_TAG = + der::CONTEXT_SPECIFIC | der::CONSTRUCTED | 3; + +Result CheckForInputSizeTypeOverflow(size_t length) { + if (length > std::numeric_limits<Input::size_type>::max()) { + return Result::FATAL_ERROR_INVALID_STATE; + } + return Success; +} + +// Given a leaf certificate, extracts the DER-encoded TBSCertificate component +// of the corresponding Precertificate. +// Basically, the extractor needs to remove the embedded SCTs extension +// from the certificate and return its TBSCertificate. We do it in an ad hoc +// manner by breaking the source DER into several parts and then joining +// the right parts, taking care to update the relevant TLV headers. +// See WriteOutput for more details on the parts involved. +class PrecertTBSExtractor { + public: + // |buffer| is the buffer to be used for writing the output. Since the + // required buffer size is not generally known in advance, it's best + // to use at least the size of the input certificate DER. + PrecertTBSExtractor(Input der, uint8_t* buffer, size_t bufferLength) + : mDER(der), mOutput(buffer, bufferLength) {} + + // Performs the extraction. + Result Init() { + Reader tbsReader; + Result rv = GetTBSCertificate(tbsReader); + if (rv != Success) { + return rv; + } + + rv = ExtractTLVsBeforeExtensions(tbsReader); + if (rv != Success) { + return rv; + } + + rv = ExtractOptionalExtensionsExceptSCTs(tbsReader); + if (rv != Success) { + return rv; + } + + return WriteOutput(); + } + + // Use to retrieve the result after a successful call to Init. + // The returned Input points to the buffer supplied in the constructor. + Input GetPrecertTBS() { return mPrecertTBS; } + + private: + Result GetTBSCertificate(Reader& tbsReader) { + Reader certificateReader; + Result rv = + der::ExpectTagAndGetValueAtEnd(mDER, der::SEQUENCE, certificateReader); + if (rv != Success) { + return rv; + } + return ExpectTagAndGetValue(certificateReader, der::SEQUENCE, tbsReader); + } + + Result ExtractTLVsBeforeExtensions(Reader& tbsReader) { + Reader::Mark tbsBegin = tbsReader.GetMark(); + while (!tbsReader.AtEnd()) { + if (tbsReader.Peek(EXTENSIONS_CONTEXT_TAG)) { + break; + } + uint8_t tag; + Input tagValue; + Result rv = der::ReadTagAndGetValue(tbsReader, tag, tagValue); + if (rv != Success) { + return rv; + } + } + return tbsReader.GetInput(tbsBegin, mTLVsBeforeExtensions); + } + + Result ExtractOptionalExtensionsExceptSCTs(Reader& tbsReader) { + if (!tbsReader.Peek(EXTENSIONS_CONTEXT_TAG)) { + return Success; + } + + Reader extensionsContextReader; + Result rv = der::ExpectTagAndGetValueAtEnd( + tbsReader, EXTENSIONS_CONTEXT_TAG, extensionsContextReader); + if (rv != Success) { + return rv; + } + + Reader extensionsReader; + rv = der::ExpectTagAndGetValueAtEnd(extensionsContextReader, der::SEQUENCE, + extensionsReader); + if (rv != Success) { + return rv; + } + + while (!extensionsReader.AtEnd()) { + Reader::Mark extensionTLVBegin = extensionsReader.GetMark(); + Reader extension; + rv = + der::ExpectTagAndGetValue(extensionsReader, der::SEQUENCE, extension); + if (rv != Success) { + return rv; + } + Reader extensionID; + rv = der::ExpectTagAndGetValue(extension, der::OIDTag, extensionID); + if (rv != Success) { + return rv; + } + if (!extensionID.MatchRest(EMBEDDED_SCT_LIST_OID)) { + Input extensionTLV; + rv = extensionsReader.GetInput(extensionTLVBegin, extensionTLV); + if (rv != Success) { + return rv; + } + mExtensionTLVs.push_back(std::move(extensionTLV)); + } + } + return Success; + } + + Result WriteOutput() { + // What should be written here: + // + // TBSCertificate ::= SEQUENCE (TLV with header |tbsHeader|) + // dump of |mTLVsBeforeExtensions| + // extensions [3] OPTIONAL (TLV with header |extensionsContextHeader|) + // SEQUENCE (TLV with with header |extensionsHeader|) + // dump of |mExtensionTLVs| + + Result rv; + if (!mExtensionTLVs.empty()) { + uint8_t tbsHeaderBuffer[MAX_TLV_HEADER_LENGTH]; + uint8_t extensionsContextHeaderBuffer[MAX_TLV_HEADER_LENGTH]; + uint8_t extensionsHeaderBuffer[MAX_TLV_HEADER_LENGTH]; + + Input tbsHeader; + Input extensionsContextHeader; + Input extensionsHeader; + + // Count the total size of the extensions. Note that since + // the extensions data is contained within mDER (an Input), + // their combined length won't overflow Input::size_type. + Input::size_type extensionsValueLength = 0; + for (auto& extensionTLV : mExtensionTLVs) { + extensionsValueLength += extensionTLV.GetLength(); + } + + rv = MakeTLVHeader(der::SEQUENCE, extensionsValueLength, + extensionsHeaderBuffer, extensionsHeader); + if (rv != Success) { + return rv; + } + // Since we're getting these extensions from a certificate that has + // already fit in an Input, this shouldn't overflow. + size_t extensionsContextLengthAsSizeT = + static_cast<size_t>(extensionsHeader.GetLength()) + + static_cast<size_t>(extensionsValueLength); + rv = CheckForInputSizeTypeOverflow(extensionsContextLengthAsSizeT); + if (rv != Success) { + return rv; + } + Input::size_type extensionsContextLength = + static_cast<Input::size_type>(extensionsContextLengthAsSizeT); + rv = + MakeTLVHeader(EXTENSIONS_CONTEXT_TAG, extensionsContextLength, + extensionsContextHeaderBuffer, extensionsContextHeader); + if (rv != Success) { + return rv; + } + size_t tbsLengthAsSizeT = + static_cast<size_t>(mTLVsBeforeExtensions.GetLength()) + + static_cast<size_t>(extensionsContextHeader.GetLength()) + + static_cast<size_t>(extensionsHeader.GetLength()) + + static_cast<size_t>(extensionsValueLength); + rv = CheckForInputSizeTypeOverflow(tbsLengthAsSizeT); + if (rv != Success) { + return rv; + } + Input::size_type tbsLength = + static_cast<Input::size_type>(tbsLengthAsSizeT); + rv = MakeTLVHeader(der::SEQUENCE, tbsLength, tbsHeaderBuffer, tbsHeader); + if (rv != Success) { + return rv; + } + + mOutput.Write(tbsHeader); + mOutput.Write(mTLVsBeforeExtensions); + mOutput.Write(extensionsContextHeader); + mOutput.Write(extensionsHeader); + for (auto& extensionTLV : mExtensionTLVs) { + mOutput.Write(extensionTLV); + } + } else { + uint8_t tbsHeaderBuffer[MAX_TLV_HEADER_LENGTH]; + Input tbsHeader; + rv = MakeTLVHeader(der::SEQUENCE, mTLVsBeforeExtensions.GetLength(), + tbsHeaderBuffer, tbsHeader); + if (rv != Success) { + return rv; + } + mOutput.Write(tbsHeader); + mOutput.Write(mTLVsBeforeExtensions); + } + + return mOutput.GetInput(mPrecertTBS); + } + + Result MakeTLVHeader(uint8_t tag, size_t length, + uint8_t (&buffer)[MAX_TLV_HEADER_LENGTH], + /*out*/ Input& header) { + Output output(buffer); + output.Write(tag); + if (length < 128) { + output.Write(static_cast<uint8_t>(length)); + } else if (length < 256) { + output.Write(0x81u); + output.Write(static_cast<uint8_t>(length)); + } else if (length < 65536) { + output.Write(0x82u); + output.Write(static_cast<uint8_t>(length / 256)); + output.Write(static_cast<uint8_t>(length % 256)); + } else { + return Result::FATAL_ERROR_INVALID_ARGS; + } + return output.GetInput(header); + } + + Input mDER; + Input mTLVsBeforeExtensions; + std::vector<Input> mExtensionTLVs; + Output mOutput; + Input mPrecertTBS; +}; + +Result GetPrecertLogEntry(Input leafCertificate, + Input issuerSubjectPublicKeyInfo, LogEntry& output) { + assert(leafCertificate.GetLength() > 0); + assert(issuerSubjectPublicKeyInfo.GetLength() > 0); + output.Reset(); + + Buffer precertTBSBuffer; + precertTBSBuffer.resize(leafCertificate.GetLength()); + + PrecertTBSExtractor extractor(leafCertificate, precertTBSBuffer.data(), + precertTBSBuffer.size()); + Result rv = extractor.Init(); + if (rv != Success) { + return rv; + } + Input precertTBS(extractor.GetPrecertTBS()); + assert(precertTBS.UnsafeGetData() == precertTBSBuffer.data()); + assert(precertTBS.GetLength() <= precertTBSBuffer.size()); + precertTBSBuffer.resize(precertTBS.GetLength()); + + output.type = LogEntry::Type::Precert; + output.tbsCertificate = std::move(precertTBSBuffer); + + output.issuerKeyHash.resize(SHA256_LENGTH); + return DigestBufNSS(issuerSubjectPublicKeyInfo, DigestAlgorithm::sha256, + output.issuerKeyHash.data(), output.issuerKeyHash.size()); +} + +void GetX509LogEntry(Input leafCertificate, LogEntry& output) { + assert(leafCertificate.GetLength() > 0); + output.Reset(); + output.type = LogEntry::Type::X509; + InputToBuffer(leafCertificate, output.leafCertificate); +} + +} // namespace ct +} // namespace mozilla diff --git a/security/ct/CTObjectsExtractor.h b/security/ct/CTObjectsExtractor.h new file mode 100644 index 0000000000..c759307da7 --- /dev/null +++ b/security/ct/CTObjectsExtractor.h @@ -0,0 +1,45 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#ifndef CTObjectsExtractor_h +#define CTObjectsExtractor_h + +#include "mozpkix/Input.h" +#include "mozpkix/Result.h" +#include "SignedCertificateTimestamp.h" + +namespace mozilla { +namespace ct { + +// Obtains a PrecertChain log entry for |leafCertificate|, a DER-encoded +// X.509v3 certificate that contains an X.509v3 extension with the +// OID 1.3.6.1.4.1.11129.2.4.2. +// |issuerSubjectPublicKeyInfo| is a DER-encoded SPKI of |leafCertificate|'s +// issuer. +// On success, fills |output| with the data for a PrecertChain log entry. +// If |leafCertificate| does not contain the required extension, +// an error is returned. +// The returned |output| is intended to be verified by CTLogVerifier::Verify. +// Note that |leafCertificate| is not checked for validity or well-formedness. +// You might want to validate it first using pkix::BuildCertChain or similar. +pkix::Result GetPrecertLogEntry(pkix::Input leafCertificate, + pkix::Input issuerSubjectPublicKeyInfo, + LogEntry& output); + +// Obtains an X509Chain log entry for |leafCertificate|, a DER-encoded +// X.509v3 certificate that is not expected to contain an X.509v3 extension +// with the OID 1.3.6.1.4.1.11129.2.4.2 (meaning a certificate without +// an embedded SCT). +// Fills |output| with the data for an X509Chain log entry. +// The returned |output| is intended to be verified by CTLogVerifier::Verify. +// Note that |leafCertificate| is not checked for validity or well-formedness. +// You might want to validate it first using pkix::BuildCertChain or similar. +void GetX509LogEntry(pkix::Input leafCertificate, LogEntry& output); + +} // namespace ct +} // namespace mozilla + +#endif // CTObjectsExtractor_h diff --git a/security/ct/CTPolicyEnforcer.cpp b/security/ct/CTPolicyEnforcer.cpp new file mode 100644 index 0000000000..0cacb25da6 --- /dev/null +++ b/security/ct/CTPolicyEnforcer.cpp @@ -0,0 +1,277 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include "CTPolicyEnforcer.h" + +#include <algorithm> +#include <stdint.h> +#include <vector> + +namespace mozilla { +namespace ct { + +using namespace mozilla::pkix; + +// Returns the number of embedded SCTs required to be present on a +// certificate for Qualification Case #2 (embedded SCTs). +static size_t GetRequiredEmbeddedSctsCount( + size_t certLifetimeInFullCalendarMonths) { + // "there are Embedded SCTs from AT LEAST N+1 once or currently qualified + // logs, where N is the lifetime of the certificate in years (normally + // rounding up, but rounding down when up to 3 months over), and must be + // at least 1" + return 1 + (certLifetimeInFullCalendarMonths + 9) / 12; +} + +// Whether a valid embedded SCT is present in the list. +static bool HasValidEmbeddedSct(const VerifiedSCTList& verifiedScts) { + for (const VerifiedSCT& verifiedSct : verifiedScts) { + if (verifiedSct.status == VerifiedSCT::Status::Valid && + verifiedSct.origin == VerifiedSCT::Origin::Embedded) { + return true; + } + } + return false; +} + +// Whether a valid non-embedded SCT is present in the list. +static bool HasValidNonEmbeddedSct(const VerifiedSCTList& verifiedScts) { + for (const VerifiedSCT& verifiedSct : verifiedScts) { + if (verifiedSct.status == VerifiedSCT::Status::Valid && + (verifiedSct.origin == VerifiedSCT::Origin::TLSExtension || + verifiedSct.origin == VerifiedSCT::Origin::OCSPResponse)) { + return true; + } + } + return false; +} + +// Given a list of verified SCTs, counts the number of distinct CA-independent +// log operators running the CT logs that issued the SCTs which satisfy +// the provided boolean predicate. +template <typename SelectFunc> +void CountIndependentLogOperatorsForSelectedScts( + const VerifiedSCTList& verifiedScts, + const CTLogOperatorList& dependentOperators, size_t& count, + SelectFunc selected) { + CTLogOperatorList operatorIds; + for (const VerifiedSCT& verifiedSct : verifiedScts) { + CTLogOperatorId sctLogOperatorId = verifiedSct.logOperatorId; + // Check if |sctLogOperatorId| is CA-dependent. + bool isDependentOperator = false; + for (CTLogOperatorId dependentOperator : dependentOperators) { + if (sctLogOperatorId == dependentOperator) { + isDependentOperator = true; + break; + } + } + if (isDependentOperator || !selected(verifiedSct)) { + continue; + } + // Check if |sctLogOperatorId| is in |operatorIds|... + bool alreadyAdded = false; + for (CTLogOperatorId id : operatorIds) { + if (id == sctLogOperatorId) { + alreadyAdded = true; + break; + } + } + // ...and if not, add it. + if (!alreadyAdded) { + operatorIds.push_back(sctLogOperatorId); + } + } + count = operatorIds.size(); +} + +// Given a list of verified SCTs, counts the number of distinct CT logs +// that issued the SCTs that satisfy the |selected| predicate. +template <typename SelectFunc> +static void CountLogsForSelectedScts(const VerifiedSCTList& verifiedScts, + size_t& count, SelectFunc selected) { + // Keep pointers to log ids (of type Buffer) from |verifiedScts| to save on + // memory allocations. + std::vector<const Buffer*> logIds; + for (const VerifiedSCT& verifiedSct : verifiedScts) { + if (!selected(verifiedSct)) { + continue; + } + + const Buffer* sctLogId = &verifiedSct.sct.logId; + // Check if |sctLogId| points to data already in |logIds|... + bool alreadyAdded = false; + for (const Buffer* logId : logIds) { + if (*logId == *sctLogId) { + alreadyAdded = true; + break; + } + } + // ...and if not, add it. + if (!alreadyAdded) { + logIds.push_back(sctLogId); + } + } + count = logIds.size(); +} + +// Calculates the effective issuance time of connection's certificate using +// the SCTs present on the connection (we can't rely on notBefore validity +// field of the certificate since it can be backdated). +// Used to determine whether to accept SCTs issued by past qualified logs. +// The effective issuance time is defined as the earliest of all SCTs, +// rather than the latest of embedded SCTs, in order to give CAs the benefit +// of the doubt in the event a log is revoked in the midst of processing +// a precertificate and issuing the certificate. +// It is acceptable to ignore the origin of the SCTs because SCTs +// delivered via OCSP/TLS extension will cover the full certificate, +// which necessarily will exist only after the precertificate +// has been logged and the actual certificate issued. +static uint64_t GetEffectiveCertIssuanceTime( + const VerifiedSCTList& verifiedScts) { + uint64_t result = UINT64_MAX; + for (const VerifiedSCT& verifiedSct : verifiedScts) { + if (verifiedSct.status == VerifiedSCT::Status::Valid) { + result = std::min(result, verifiedSct.sct.timestamp); + } + } + return result; +} + +// Checks if the log that issued the given SCT is "once or currently qualified" +// (i.e. was qualified at the time of the certificate issuance). In addition, +// makes sure the SCT is before the disqualification. +static bool LogWasQualifiedForSct(const VerifiedSCT& verifiedSct, + uint64_t certIssuanceTime) { + if (verifiedSct.status == VerifiedSCT::Status::Valid) { + return true; + } + if (verifiedSct.status == VerifiedSCT::Status::ValidFromDisqualifiedLog) { + uint64_t logDisqualificationTime = verifiedSct.logDisqualificationTime; + return certIssuanceTime < logDisqualificationTime && + verifiedSct.sct.timestamp < logDisqualificationTime; + } + return false; +} + +// "A certificate is CT Qualified if it is presented with at least two SCTs +// from once or currently qualified logs run by a minimum of two entities +// independent of the CA and of each other." +// By the preexisting certificate exception provision (not currently +// implemented), certificates "are CT Qualified if they are presented with SCTs +// from once or currently qualified logs run by a minimum of one entity +// independent of the CA." +static void CheckOperatorDiversityCompliance( + const VerifiedSCTList& verifiedScts, uint64_t certIssuanceTime, + const CTLogOperatorList& dependentOperators, bool& compliant) { + size_t independentOperatorsCount; + CountIndependentLogOperatorsForSelectedScts( + verifiedScts, dependentOperators, independentOperatorsCount, + [certIssuanceTime](const VerifiedSCT& verifiedSct) -> bool { + return LogWasQualifiedForSct(verifiedSct, certIssuanceTime); + }); + // Having at least 2 operators implies we have at least 2 SCTs. + // For the preexisting certificate exception provision (1 operator) we will + // need to include an additional SCTs count check using + // CountLogsForSelectedScts(verifiedScts, sctsCount, + // [certIssuanceTime](const VerifiedSCT& verifiedSct)->bool { + // return LogWasQualifiedForSct(verifiedSct, certIssuanceTime); + // }); + compliant = independentOperatorsCount >= 2; +} + +// Qualification Case #1 (non-embedded SCTs) - the following must hold: +// a. An SCT from a log qualified at the time of check is presented via the +// TLS extension OR is embedded within a stapled OCSP response; +// AND +// b. There are at least two SCTs from logs qualified at the time of check, +// presented via any method. +static void CheckNonEmbeddedCompliance(const VerifiedSCTList& verifiedScts, + bool& compliant) { + if (!HasValidNonEmbeddedSct(verifiedScts)) { + compliant = false; + return; + } + + size_t validSctsCount; + CountLogsForSelectedScts( + verifiedScts, validSctsCount, [](const VerifiedSCT& verifiedSct) -> bool { + return verifiedSct.status == VerifiedSCT::Status::Valid; + }); + + compliant = validSctsCount >= 2; +} + +// Qualification Case #2 (embedded SCTs) - the following must hold: +// a. An Embedded SCT from a log qualified at the time of check is presented; +// AND +// b. There are Embedded SCTs from AT LEAST N + 1 once or currently qualified +// logs, where N is the lifetime of the certificate in years (normally +// rounding up, but rounding down when up to 3 months over), and must be +// at least 1. +static void CheckEmbeddedCompliance(const VerifiedSCTList& verifiedScts, + size_t certLifetimeInCalendarMonths, + uint64_t certIssuanceTime, + bool& compliant) { + if (!HasValidEmbeddedSct(verifiedScts)) { + compliant = false; + return; + } + + // Count the compliant embedded SCTs. Only a single SCT from each log + // is accepted. Note that a given log might return several different SCTs + // for the same precertificate (it is permitted, but advised against). + size_t embeddedSctsCount; + CountLogsForSelectedScts( + verifiedScts, embeddedSctsCount, + [certIssuanceTime](const VerifiedSCT& verifiedSct) -> bool { + return verifiedSct.origin == VerifiedSCT::Origin::Embedded && + LogWasQualifiedForSct(verifiedSct, certIssuanceTime); + }); + + size_t requiredSctsCount = + GetRequiredEmbeddedSctsCount(certLifetimeInCalendarMonths); + + compliant = embeddedSctsCount >= requiredSctsCount; +} + +void CTPolicyEnforcer::CheckCompliance( + const VerifiedSCTList& verifiedScts, size_t certLifetimeInCalendarMonths, + const CTLogOperatorList& dependentOperators, + CTPolicyCompliance& compliance) { + uint64_t certIssuanceTime = GetEffectiveCertIssuanceTime(verifiedScts); + + bool diversityOK; + CheckOperatorDiversityCompliance(verifiedScts, certIssuanceTime, + dependentOperators, diversityOK); + + bool nonEmbeddedCaseOK; + CheckNonEmbeddedCompliance(verifiedScts, nonEmbeddedCaseOK); + + bool embeddedCaseOK; + CheckEmbeddedCompliance(verifiedScts, certLifetimeInCalendarMonths, + certIssuanceTime, embeddedCaseOK); + + if (nonEmbeddedCaseOK || embeddedCaseOK) { + compliance = diversityOK ? CTPolicyCompliance::Compliant + : CTPolicyCompliance::NotDiverseScts; + } else { + // Not enough SCTs are present to satisfy either case of the policy. + compliance = CTPolicyCompliance::NotEnoughScts; + } + + switch (compliance) { + case CTPolicyCompliance::Compliant: + case CTPolicyCompliance::NotEnoughScts: + case CTPolicyCompliance::NotDiverseScts: + break; + case CTPolicyCompliance::Unknown: + default: + assert(false); + } +} + +} // namespace ct +} // namespace mozilla diff --git a/security/ct/CTPolicyEnforcer.h b/security/ct/CTPolicyEnforcer.h new file mode 100644 index 0000000000..0cbbec642f --- /dev/null +++ b/security/ct/CTPolicyEnforcer.h @@ -0,0 +1,65 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#ifndef CTPolicyEnforcer_h +#define CTPolicyEnforcer_h + +#include "CTLog.h" +#include "CTVerifyResult.h" +#include "mozpkix/Result.h" + +namespace mozilla { +namespace ct { + +// Information about the compliance of the TLS connection with the +// Certificate Transparency policy. +enum class CTPolicyCompliance { + // Compliance not checked or not applicable. + Unknown, + // The connection complied with the certificate policy + // by including SCTs that satisfy the policy. + Compliant, + // The connection did not have enough valid SCTs to comply. + NotEnoughScts, + // The connection had enough valid SCTs, but the diversity requirement + // was not met (the number of CT log operators independent of the CA + // and of each other is too low). + NotDiverseScts, +}; + +// Checks whether a TLS connection complies with the current CT policy. +// The implemented policy is based on the Mozilla CT Policy draft 0.1.0 +// (see https://docs.google.com/document/d/ +// 1rnqYYwscAx8WhS-MCdTiNzYQus9e37HuVyafQvEeNro/edit). +// +// NOTE: CT DIVERSITY REQUIREMENT IS TBD, PENDING FINALIZATION +// OF MOZILLA CT POLICY. Specifically: +// 1. CT log operators being CA-dependent is not currently taken into account +// (see CTDiversityPolicy.h). +// 2. The preexisting certificate exception provision of the operator diversity +// requirement is not implemented (see "CT Qualified" section of the policy and +// CheckOperatorDiversityCompliance in CTPolicyEnforcer.cpp). +class CTPolicyEnforcer { + public: + // |verifiedSct| - SCTs present on the connection along with their + // verification status. + // |certLifetimeInCalendarMonths| - certificate lifetime in full calendar + // months (i.e. rounded down), based on the notBefore/notAfter fields. + // |dependentOperators| - which CT log operators are dependent on the CA + // that issued the certificate. SCTs issued by logs associated with such + // operators are treated differenly when evaluating the policy. + // See CTDiversityPolicy class. + // |compliance| - the result of the compliance check. + void CheckCompliance(const VerifiedSCTList& verifiedScts, + size_t certLifetimeInCalendarMonths, + const CTLogOperatorList& dependentOperators, + CTPolicyCompliance& compliance); +}; + +} // namespace ct +} // namespace mozilla + +#endif // CTPolicyEnforcer_h diff --git a/security/ct/CTSerialization.cpp b/security/ct/CTSerialization.cpp new file mode 100644 index 0000000000..ed6cbb891a --- /dev/null +++ b/security/ct/CTSerialization.cpp @@ -0,0 +1,396 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include "CTSerialization.h" +#include "CTUtils.h" + +#include <stdint.h> +#include <type_traits> + +namespace mozilla { +namespace ct { + +using namespace mozilla::pkix; + +typedef mozilla::pkix::Result Result; + +// Note: length is always specified in bytes. +// Signed Certificate Timestamp (SCT) Version length +static const size_t kVersionLength = 1; + +// Members of a V1 SCT +static const size_t kLogIdLength = 32; +static const size_t kTimestampLength = 8; +static const size_t kExtensionsLengthBytes = 2; +static const size_t kHashAlgorithmLength = 1; +static const size_t kSigAlgorithmLength = 1; +static const size_t kSignatureLengthBytes = 2; + +// Members of the digitally-signed struct of a V1 SCT +static const size_t kSignatureTypeLength = 1; +static const size_t kLogEntryTypeLength = 2; +static const size_t kAsn1CertificateLengthBytes = 3; +static const size_t kTbsCertificateLengthBytes = 3; + +static const size_t kSCTListLengthBytes = 2; +static const size_t kSerializedSCTLengthBytes = 2; + +enum class SignatureType { + CertificateTimestamp = 0, + TreeHash = 1, +}; + +// Reads a serialized hash algorithm. +static Result ReadHashAlgorithm(Reader& in, + DigitallySigned::HashAlgorithm& out) { + unsigned int value; + Result rv = ReadUint<kHashAlgorithmLength>(in, value); + if (rv != Success) { + return rv; + } + DigitallySigned::HashAlgorithm algo = + static_cast<DigitallySigned::HashAlgorithm>(value); + switch (algo) { + case DigitallySigned::HashAlgorithm::None: + case DigitallySigned::HashAlgorithm::MD5: + case DigitallySigned::HashAlgorithm::SHA1: + case DigitallySigned::HashAlgorithm::SHA224: + case DigitallySigned::HashAlgorithm::SHA256: + case DigitallySigned::HashAlgorithm::SHA384: + case DigitallySigned::HashAlgorithm::SHA512: + out = algo; + return Success; + } + return Result::ERROR_BAD_DER; +} + +// Reads a serialized signature algorithm. +static Result ReadSignatureAlgorithm(Reader& in, + DigitallySigned::SignatureAlgorithm& out) { + unsigned int value; + Result rv = ReadUint<kSigAlgorithmLength>(in, value); + if (rv != Success) { + return rv; + } + DigitallySigned::SignatureAlgorithm algo = + static_cast<DigitallySigned::SignatureAlgorithm>(value); + switch (algo) { + case DigitallySigned::SignatureAlgorithm::Anonymous: + case DigitallySigned::SignatureAlgorithm::RSA: + case DigitallySigned::SignatureAlgorithm::DSA: + case DigitallySigned::SignatureAlgorithm::ECDSA: + out = algo; + return Success; + } + return Result::ERROR_BAD_DER; +} + +// Reads a serialized version enum. +static Result ReadVersion(Reader& in, + SignedCertificateTimestamp::Version& out) { + unsigned int value; + Result rv = ReadUint<kVersionLength>(in, value); + if (rv != Success) { + return rv; + } + SignedCertificateTimestamp::Version version = + static_cast<SignedCertificateTimestamp::Version>(value); + switch (version) { + case SignedCertificateTimestamp::Version::V1: + out = version; + return Success; + } + return Result::ERROR_BAD_DER; +} + +// Writes a TLS-encoded variable length unsigned integer to |output|. +// Note: range/overflow checks are not performed on the input parameters. +// |length| indicates the size (in bytes) of the integer to be written. +// |value| the value itself to be written. +static Result UncheckedWriteUint(size_t length, uint64_t value, + Buffer& output) { + output.reserve(length + output.size()); + for (; length > 0; --length) { + uint8_t nextByte = (value >> ((length - 1) * 8)) & 0xFF; + output.push_back(nextByte); + } + return Success; +} + +// Performs sanity checks on T and calls UncheckedWriteUint. +template <size_t length, typename T> +static inline Result WriteUint(T value, Buffer& output) { + static_assert(length <= 8, "At most 8 byte integers can be written"); + static_assert(sizeof(T) >= length, "T must be able to hold <length> bytes"); + if (std::is_signed<T>::value) { + // We accept signed integer types assuming the actual value is non-negative. + if (value < 0) { + return Result::FATAL_ERROR_INVALID_ARGS; + } + } + if (sizeof(T) > length) { + // We allow the value variable to take more bytes than is written, + // but the unwritten bytes must be zero. + // Note: when "sizeof(T) == length" holds, "value >> (length * 8)" is + // undefined since the shift is too big. On some compilers, this would + // produce a warning even though the actual code is unreachable. + if (value >> (length * 8 - 1) > 1) { + return Result::FATAL_ERROR_INVALID_ARGS; + } + } + return UncheckedWriteUint(length, static_cast<uint64_t>(value), output); +} + +// Writes an array to |output| from |input|. +// Should be used in one of two cases: +// * The length of |input| has already been encoded into the |output| stream. +// * The length of |input| is fixed and the reader is expected to specify that +// length when reading. +// If the length of |input| is dynamic and data is expected to follow it, +// WriteVariableBytes must be used. +static void WriteEncodedBytes(Input input, Buffer& output) { + output.insert(output.end(), input.UnsafeGetData(), + input.UnsafeGetData() + input.GetLength()); +} + +// Same as above, but the source data is in a Buffer. +static void WriteEncodedBytes(const Buffer& source, Buffer& output) { + output.insert(output.end(), source.begin(), source.end()); +} + +// A variable-length byte array is prefixed by its length when serialized. +// This writes the length prefix. +// |prefixLength| indicates the number of bytes needed to represent the length. +// |dataLength| is the length of the byte array following the prefix. +// Fails if |dataLength| is more than 2^|prefixLength| - 1. +template <size_t prefixLength> +static Result WriteVariableBytesPrefix(size_t dataLength, Buffer& output) { + const size_t maxAllowedInputSize = + static_cast<size_t>(((1 << (prefixLength * 8)) - 1)); + if (dataLength > maxAllowedInputSize) { + return Result::FATAL_ERROR_INVALID_ARGS; + } + + return WriteUint<prefixLength>(dataLength, output); +} + +// Writes a variable-length array to |output|. +// |prefixLength| indicates the number of bytes needed to represent the length. +// |input| is the array itself. +// Fails if the size of |input| is more than 2^|prefixLength| - 1. +template <size_t prefixLength> +static Result WriteVariableBytes(Input input, Buffer& output) { + Result rv = WriteVariableBytesPrefix<prefixLength>(input.GetLength(), output); + if (rv != Success) { + return rv; + } + WriteEncodedBytes(input, output); + return Success; +} + +// Same as above, but the source data is in a Buffer. +template <size_t prefixLength> +static Result WriteVariableBytes(const Buffer& source, Buffer& output) { + Input input; + Result rv = BufferToInput(source, input); + if (rv != Success) { + return rv; + } + return WriteVariableBytes<prefixLength>(input, output); +} + +// Writes a LogEntry of type X.509 cert to |output|. +// |input| is the LogEntry containing the certificate. +static Result EncodeAsn1CertLogEntry(const LogEntry& entry, Buffer& output) { + return WriteVariableBytes<kAsn1CertificateLengthBytes>(entry.leafCertificate, + output); +} + +// Writes a LogEntry of type PreCertificate to |output|. +// |input| is the LogEntry containing the TBSCertificate and issuer key hash. +static Result EncodePrecertLogEntry(const LogEntry& entry, Buffer& output) { + if (entry.issuerKeyHash.size() != kLogIdLength) { + return Result::FATAL_ERROR_INVALID_ARGS; + } + WriteEncodedBytes(entry.issuerKeyHash, output); + return WriteVariableBytes<kTbsCertificateLengthBytes>(entry.tbsCertificate, + output); +} + +Result EncodeDigitallySigned(const DigitallySigned& data, Buffer& output) { + Result rv = WriteUint<kHashAlgorithmLength>( + static_cast<unsigned int>(data.hashAlgorithm), output); + if (rv != Success) { + return rv; + } + rv = WriteUint<kSigAlgorithmLength>( + static_cast<unsigned int>(data.signatureAlgorithm), output); + if (rv != Success) { + return rv; + } + return WriteVariableBytes<kSignatureLengthBytes>(data.signatureData, output); +} + +Result DecodeDigitallySigned(Reader& reader, DigitallySigned& output) { + DigitallySigned result; + + Result rv = ReadHashAlgorithm(reader, result.hashAlgorithm); + if (rv != Success) { + return rv; + } + rv = ReadSignatureAlgorithm(reader, result.signatureAlgorithm); + if (rv != Success) { + return rv; + } + + Input signatureData; + rv = ReadVariableBytes<kSignatureLengthBytes>(reader, signatureData); + if (rv != Success) { + return rv; + } + InputToBuffer(signatureData, result.signatureData); + + output = std::move(result); + return Success; +} + +Result EncodeLogEntry(const LogEntry& entry, Buffer& output) { + Result rv = WriteUint<kLogEntryTypeLength>( + static_cast<unsigned int>(entry.type), output); + if (rv != Success) { + return rv; + } + switch (entry.type) { + case LogEntry::Type::X509: + return EncodeAsn1CertLogEntry(entry, output); + case LogEntry::Type::Precert: + return EncodePrecertLogEntry(entry, output); + default: + assert(false); + } + return Result::ERROR_BAD_DER; +} + +static Result WriteTimeSinceEpoch(uint64_t timestamp, Buffer& output) { + return WriteUint<kTimestampLength>(timestamp, output); +} + +Result EncodeV1SCTSignedData(uint64_t timestamp, Input serializedLogEntry, + Input extensions, Buffer& output) { + Result rv = WriteUint<kVersionLength>( + static_cast<unsigned int>(SignedCertificateTimestamp::Version::V1), + output); + if (rv != Success) { + return rv; + } + rv = WriteUint<kSignatureTypeLength>( + static_cast<unsigned int>(SignatureType::CertificateTimestamp), output); + if (rv != Success) { + return rv; + } + rv = WriteTimeSinceEpoch(timestamp, output); + if (rv != Success) { + return rv; + } + // NOTE: serializedLogEntry must already be serialized and contain the + // length as the prefix. + WriteEncodedBytes(serializedLogEntry, output); + return WriteVariableBytes<kExtensionsLengthBytes>(extensions, output); +} + +Result DecodeSCTList(Input input, Reader& listReader) { + Reader inputReader(input); + Input listData; + Result rv = ReadVariableBytes<kSCTListLengthBytes>(inputReader, listData); + if (rv != Success) { + return rv; + } + return listReader.Init(listData); +} + +Result ReadSCTListItem(Reader& listReader, Input& output) { + if (listReader.AtEnd()) { + return Result::FATAL_ERROR_INVALID_ARGS; + } + + Result rv = ReadVariableBytes<kSerializedSCTLengthBytes>(listReader, output); + if (rv != Success) { + return rv; + } + if (output.GetLength() == 0) { + return Result::ERROR_BAD_DER; + } + return Success; +} + +Result DecodeSignedCertificateTimestamp(Reader& reader, + SignedCertificateTimestamp& output) { + SignedCertificateTimestamp result; + + Result rv = ReadVersion(reader, result.version); + if (rv != Success) { + return rv; + } + + uint64_t timestamp; + Input logId; + Input extensions; + + rv = ReadFixedBytes(kLogIdLength, reader, logId); + if (rv != Success) { + return rv; + } + rv = ReadUint<kTimestampLength>(reader, timestamp); + if (rv != Success) { + return rv; + } + rv = ReadVariableBytes<kExtensionsLengthBytes>(reader, extensions); + if (rv != Success) { + return rv; + } + rv = DecodeDigitallySigned(reader, result.signature); + if (rv != Success) { + return rv; + } + + InputToBuffer(logId, result.logId); + InputToBuffer(extensions, result.extensions); + result.timestamp = timestamp; + + output = std::move(result); + return Success; +} + +Result EncodeSCTList(const std::vector<pkix::Input>& scts, Buffer& output) { + // Find out the total size of the SCT list to be written so we can + // write the prefix for the list before writing its contents. + size_t sctListLength = 0; + for (auto& sct : scts) { + sctListLength += + /* data size */ sct.GetLength() + + /* length prefix size */ kSerializedSCTLengthBytes; + } + + output.reserve(kSCTListLengthBytes + sctListLength); + + // Write the prefix for the SCT list. + Result rv = + WriteVariableBytesPrefix<kSCTListLengthBytes>(sctListLength, output); + if (rv != Success) { + return rv; + } + // Now write each SCT from the list. + for (auto& sct : scts) { + rv = WriteVariableBytes<kSerializedSCTLengthBytes>(sct, output); + if (rv != Success) { + return rv; + } + } + return Success; +} + +} // namespace ct +} // namespace mozilla diff --git a/security/ct/CTSerialization.h b/security/ct/CTSerialization.h new file mode 100644 index 0000000000..67b7c5f50c --- /dev/null +++ b/security/ct/CTSerialization.h @@ -0,0 +1,65 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#ifndef CTSerialization_h +#define CTSerialization_h + +#include <vector> + +#include "mozpkix/Input.h" +#include "mozpkix/Result.h" +#include "SignedCertificateTimestamp.h" + +// Utility functions for encoding/decoding structures used by Certificate +// Transparency to/from the TLS wire format encoding. +namespace mozilla { +namespace ct { + +// Encodes the DigitallySigned |data| to |output|. +pkix::Result EncodeDigitallySigned(const DigitallySigned& data, Buffer& output); + +// Reads and decodes a DigitallySigned object from |reader|. +// On failure, the cursor position of |reader| is undefined. +pkix::Result DecodeDigitallySigned(pkix::Reader& reader, + DigitallySigned& output); + +// Encodes the |input| LogEntry to |output|. The size of the entry +// must not exceed the allowed size in RFC6962. +pkix::Result EncodeLogEntry(const LogEntry& entry, Buffer& output); + +// Encodes the data signed by a Signed Certificate Timestamp (SCT) into +// |output|. The signature included in the SCT can then be verified over these +// bytes. +// |timestamp| timestamp from the SCT. +// |serializedLogEntry| the log entry signed by the SCT. +// |extensions| CT extensions. +pkix::Result EncodeV1SCTSignedData(uint64_t timestamp, + pkix::Input serializedLogEntry, + pkix::Input extensions, Buffer& output); + +// Decodes a list of Signed Certificate Timestamps +// (SignedCertificateTimestampList as defined in RFC6962). This list +// is typically obtained from the CT extension in a certificate. +// To extract the individual items of the list, call ReadSCTListItem on +// the returned reader until the reader reaches its end. +// Note that the validity of each extracted SCT should be checked separately. +pkix::Result DecodeSCTList(pkix::Input input, pkix::Reader& listReader); + +// Reads a single SCT from the reader returned by DecodeSCTList. +pkix::Result ReadSCTListItem(pkix::Reader& listReader, pkix::Input& result); + +// Decodes a single SCT from |input| to |output|. +pkix::Result DecodeSignedCertificateTimestamp( + pkix::Reader& input, SignedCertificateTimestamp& output); + +// Encodes a list of SCTs (|scts|) to |output|. +pkix::Result EncodeSCTList(const std::vector<pkix::Input>& scts, + Buffer& output); + +} // namespace ct +} // namespace mozilla + +#endif // CTSerialization_h diff --git a/security/ct/CTUtils.h b/security/ct/CTUtils.h new file mode 100644 index 0000000000..68ddd927f0 --- /dev/null +++ b/security/ct/CTUtils.h @@ -0,0 +1,111 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#ifndef CTUtils_h +#define CTUtils_h + +#include <memory> + +#include "cryptohi.h" +#include "keyhi.h" +#include "keythi.h" +#include "pk11pub.h" +#include "mozpkix/Input.h" +#include "mozpkix/Result.h" + +#define MOZILLA_CT_ARRAY_LENGTH(x) (sizeof(x) / sizeof((x)[0])) + +struct DeleteHelper { + void operator()(CERTSubjectPublicKeyInfo* value) { + SECKEY_DestroySubjectPublicKeyInfo(value); + } + void operator()(PK11Context* value) { PK11_DestroyContext(value, true); } + void operator()(PK11SlotInfo* value) { PK11_FreeSlot(value); } + void operator()(SECKEYPublicKey* value) { SECKEY_DestroyPublicKey(value); } + void operator()(SECItem* value) { SECITEM_FreeItem(value, true); } +}; + +template <class T> +struct MaybeDeleteHelper { + void operator()(T* ptr) { + if (ptr) { + DeleteHelper del; + del(ptr); + } + } +}; + +typedef std::unique_ptr<CERTSubjectPublicKeyInfo, + MaybeDeleteHelper<CERTSubjectPublicKeyInfo>> + UniqueCERTSubjectPublicKeyInfo; +typedef std::unique_ptr<PK11Context, MaybeDeleteHelper<PK11Context>> + UniquePK11Context; +typedef std::unique_ptr<PK11SlotInfo, MaybeDeleteHelper<PK11SlotInfo>> + UniquePK11SlotInfo; +typedef std::unique_ptr<SECKEYPublicKey, MaybeDeleteHelper<SECKEYPublicKey>> + UniqueSECKEYPublicKey; +typedef std::unique_ptr<SECItem, MaybeDeleteHelper<SECItem>> UniqueSECItem; + +namespace mozilla { +namespace ct { + +// Reads a TLS-encoded variable length unsigned integer from |in|. +// The integer is expected to be in big-endian order, which is used by TLS. +// Note: does not check if the output parameter overflows while reading. +// |length| indicates the size (in bytes) of the serialized integer. +inline static pkix::Result UncheckedReadUint(size_t length, pkix::Reader& in, + uint64_t& out) { + uint64_t result = 0; + for (size_t i = 0; i < length; ++i) { + uint8_t value; + pkix::Result rv = in.Read(value); + if (rv != pkix::Success) { + return rv; + } + result = (result << 8) | value; + } + out = result; + return pkix::Success; +} + +// Performs overflow sanity checks and calls UncheckedReadUint. +template <size_t length, typename T> +pkix::Result ReadUint(pkix::Reader& in, T& out) { + uint64_t value; + static_assert(std::is_unsigned<T>::value, "T must be unsigned"); + static_assert(length <= 8, "At most 8 byte integers can be read"); + static_assert(sizeof(T) >= length, "T must be able to hold <length> bytes"); + pkix::Result rv = UncheckedReadUint(length, in, value); + if (rv != pkix::Success) { + return rv; + } + out = static_cast<T>(value); + return pkix::Success; +} + +// Reads |length| bytes from |in|. +static inline pkix::Result ReadFixedBytes(size_t length, pkix::Reader& in, + pkix::Input& out) { + return in.Skip(length, out); +} + +// Reads a length-prefixed variable amount of bytes from |in|, updating |out| +// on success. |prefixLength| indicates the number of bytes needed to represent +// the length. +template <size_t prefixLength> +pkix::Result ReadVariableBytes(pkix::Reader& in, pkix::Input& out) { + size_t length; + pkix::Result rv = ReadUint<prefixLength>(in, length); + if (rv != pkix::Success) { + return rv; + } + return ReadFixedBytes(length, in, out); +} + +} // namespace ct +} // namespace mozilla + +#endif // CTUtils_h diff --git a/security/ct/CTVerifyResult.cpp b/security/ct/CTVerifyResult.cpp new file mode 100644 index 0000000000..72c3706186 --- /dev/null +++ b/security/ct/CTVerifyResult.cpp @@ -0,0 +1,26 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include "CTVerifyResult.h" + +#include <stdint.h> + +namespace mozilla { +namespace ct { + +VerifiedSCT::VerifiedSCT() + : status(Status::None), + origin(Origin::Unknown), + logOperatorId(-1), + logDisqualificationTime(UINT64_MAX) {} + +void CTVerifyResult::Reset() { + verifiedScts.clear(); + decodingErrors = 0; +} + +} // namespace ct +} // namespace mozilla diff --git a/security/ct/CTVerifyResult.h b/security/ct/CTVerifyResult.h new file mode 100644 index 0000000000..ad7f4774aa --- /dev/null +++ b/security/ct/CTVerifyResult.h @@ -0,0 +1,84 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#ifndef CTVerifyResult_h +#define CTVerifyResult_h + +#include <vector> + +#include "CTLog.h" +#include "SignedCertificateTimestamp.h" + +namespace mozilla { +namespace ct { + +// Holds a verified Signed Certificate Timestamp along with the verification +// status (e.g. valid/invalid) and additional information related to the +// verification. +struct VerifiedSCT { + VerifiedSCT(); + + // The original SCT. + SignedCertificateTimestamp sct; + + enum class Status { + None, + // The SCT is from a known log, and the signature is valid. + Valid, + // The SCT is from a known disqualified log, and the signature is valid. + // For the disqualification time of the log see |logDisqualificationTime|. + ValidFromDisqualifiedLog, + // The SCT is from an unknown log and can not be verified. + UnknownLog, + // The SCT is from a known log, but the signature is invalid. + InvalidSignature, + // The SCT signature is valid, but the timestamp is in the future. + // Such SCTs are considered invalid (see RFC 6962, Section 5.2). + InvalidTimestamp, + }; + + enum class Origin { + Unknown, + Embedded, + TLSExtension, + OCSPResponse, + }; + + Status status; + Origin origin; + CTLogOperatorId logOperatorId; + uint64_t logDisqualificationTime; +}; + +typedef std::vector<VerifiedSCT> VerifiedSCTList; + +// Holds Signed Certificate Timestamps verification results. +class CTVerifyResult { + public: + CTVerifyResult() { Reset(); } + + // SCTs that were processed during the verification along with their + // verification results. + VerifiedSCTList verifiedScts; + + // The verifier makes the best effort to extract the available SCTs + // from the binary sources provided to it. + // If some SCT cannot be extracted due to encoding errors, the verifier + // proceeds to the next available one. In other words, decoding errors are + // effectively ignored. + // Note that a serialized SCT may fail to decode for a "legitimate" reason, + // e.g. if the SCT is from a future version of the Certificate Transparency + // standard. + // |decodingErrors| field counts the errors of the above kind. + size_t decodingErrors; + + void Reset(); +}; + +} // namespace ct +} // namespace mozilla + +#endif // CTVerifyResult_h diff --git a/security/ct/MultiLogCTVerifier.cpp b/security/ct/MultiLogCTVerifier.cpp new file mode 100644 index 0000000000..78f7abb7a5 --- /dev/null +++ b/security/ct/MultiLogCTVerifier.cpp @@ -0,0 +1,194 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include "MultiLogCTVerifier.h" + +#include "CTObjectsExtractor.h" +#include "CTSerialization.h" + +namespace mozilla { +namespace ct { + +using namespace mozilla::pkix; + +// Note: this moves |verifiedSct| to the target list in |result|. +static void StoreVerifiedSct(CTVerifyResult& result, VerifiedSCT&& verifiedSct, + VerifiedSCT::Status status) { + verifiedSct.status = status; + result.verifiedScts.push_back(std::move(verifiedSct)); +} + +void MultiLogCTVerifier::AddLog(CTLogVerifier&& log) { + mLogs.push_back(std::move(log)); +} + +Result MultiLogCTVerifier::Verify(Input cert, Input issuerSubjectPublicKeyInfo, + Input sctListFromCert, + Input sctListFromOCSPResponse, + Input sctListFromTLSExtension, Time time, + CTVerifyResult& result) { + assert(cert.GetLength() > 0); + result.Reset(); + + Result rv; + + // Verify embedded SCTs + if (issuerSubjectPublicKeyInfo.GetLength() > 0 && + sctListFromCert.GetLength() > 0) { + LogEntry precertEntry; + rv = GetPrecertLogEntry(cert, issuerSubjectPublicKeyInfo, precertEntry); + if (rv != Success) { + return rv; + } + rv = VerifySCTs(sctListFromCert, precertEntry, + VerifiedSCT::Origin::Embedded, time, result); + if (rv != Success) { + return rv; + } + } + + LogEntry x509Entry; + GetX509LogEntry(cert, x509Entry); + + // Verify SCTs from a stapled OCSP response + if (sctListFromOCSPResponse.GetLength() > 0) { + rv = VerifySCTs(sctListFromOCSPResponse, x509Entry, + VerifiedSCT::Origin::OCSPResponse, time, result); + if (rv != Success) { + return rv; + } + } + + // Verify SCTs from a TLS extension + if (sctListFromTLSExtension.GetLength() > 0) { + rv = VerifySCTs(sctListFromTLSExtension, x509Entry, + VerifiedSCT::Origin::TLSExtension, time, result); + if (rv != Success) { + return rv; + } + } + return Success; +} + +void DecodeSCTs(Input encodedSctList, + std::vector<SignedCertificateTimestamp>& decodedSCTs, + size_t& decodingErrors) { + decodedSCTs.clear(); + + Reader listReader; + Result rv = DecodeSCTList(encodedSctList, listReader); + if (rv != Success) { + decodingErrors++; + return; + } + + while (!listReader.AtEnd()) { + Input encodedSct; + rv = ReadSCTListItem(listReader, encodedSct); + if (rv != Success) { + decodingErrors++; + return; + } + + Reader encodedSctReader(encodedSct); + SignedCertificateTimestamp sct; + rv = DecodeSignedCertificateTimestamp(encodedSctReader, sct); + if (rv != Success) { + decodingErrors++; + continue; + } + decodedSCTs.push_back(std::move(sct)); + } +} + +Result MultiLogCTVerifier::VerifySCTs(Input encodedSctList, + const LogEntry& expectedEntry, + VerifiedSCT::Origin origin, Time time, + CTVerifyResult& result) { + std::vector<SignedCertificateTimestamp> decodedSCTs; + DecodeSCTs(encodedSctList, decodedSCTs, result.decodingErrors); + for (auto sct : decodedSCTs) { + Result rv = + VerifySingleSCT(std::move(sct), expectedEntry, origin, time, result); + if (rv != Success) { + return rv; + } + } + return Success; +} + +Result MultiLogCTVerifier::VerifySingleSCT(SignedCertificateTimestamp&& sct, + const LogEntry& expectedEntry, + VerifiedSCT::Origin origin, + Time time, CTVerifyResult& result) { + VerifiedSCT verifiedSct; + verifiedSct.origin = origin; + verifiedSct.sct = std::move(sct); + + CTLogVerifier* matchingLog = nullptr; + for (auto& log : mLogs) { + if (log.keyId() == verifiedSct.sct.logId) { + matchingLog = &log; + break; + } + } + + if (!matchingLog) { + // SCT does not match any known log. + StoreVerifiedSct(result, std::move(verifiedSct), + VerifiedSCT::Status::UnknownLog); + return Success; + } + + verifiedSct.logOperatorId = matchingLog->operatorId(); + + if (!matchingLog->SignatureParametersMatch(verifiedSct.sct.signature)) { + // SCT signature parameters do not match the log's. + StoreVerifiedSct(result, std::move(verifiedSct), + VerifiedSCT::Status::InvalidSignature); + return Success; + } + + Result rv = matchingLog->Verify(expectedEntry, verifiedSct.sct); + if (rv != Success) { + if (rv == Result::ERROR_BAD_SIGNATURE) { + StoreVerifiedSct(result, std::move(verifiedSct), + VerifiedSCT::Status::InvalidSignature); + return Success; + } + return rv; + } + + // Make sure the timestamp is legitimate (not in the future). + // SCT's |timestamp| is measured in milliseconds since the epoch, + // ignoring leap seconds. When converting it to a second-level precision + // pkix::Time, we need to round it either up or down. In our case, rounding up + // (towards the future) is more "secure", although practically + // it does not matter. + Time sctTime = + TimeFromEpochInSeconds((verifiedSct.sct.timestamp + 999u) / 1000u); + if (sctTime > time) { + StoreVerifiedSct(result, std::move(verifiedSct), + VerifiedSCT::Status::InvalidTimestamp); + return Success; + } + + // SCT verified ok, see if the log is qualified. Since SCTs from + // disqualified logs are treated as valid under certain circumstances (see + // the CT Policy), the log qualification check must be the last one we do. + if (matchingLog->isDisqualified()) { + verifiedSct.logDisqualificationTime = matchingLog->disqualificationTime(); + StoreVerifiedSct(result, std::move(verifiedSct), + VerifiedSCT::Status::ValidFromDisqualifiedLog); + return Success; + } + + StoreVerifiedSct(result, std::move(verifiedSct), VerifiedSCT::Status::Valid); + return Success; +} + +} // namespace ct +} // namespace mozilla diff --git a/security/ct/MultiLogCTVerifier.h b/security/ct/MultiLogCTVerifier.h new file mode 100644 index 0000000000..983f96677b --- /dev/null +++ b/security/ct/MultiLogCTVerifier.h @@ -0,0 +1,89 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#ifndef MultiLogCTVerifier_h +#define MultiLogCTVerifier_h + +#include <vector> + +#include "CTLogVerifier.h" +#include "CTVerifyResult.h" +#include "mozpkix/Input.h" +#include "mozpkix/Result.h" +#include "mozpkix/Time.h" +#include "SignedCertificateTimestamp.h" + +namespace mozilla { +namespace ct { + +void DecodeSCTs(pkix::Input encodedSctList, + std::vector<SignedCertificateTimestamp>& decodedSCTs, + size_t& decodingErrors); + +// A Certificate Transparency verifier that can verify Signed Certificate +// Timestamps from multiple logs. +class MultiLogCTVerifier { + public: + // Adds a new log to the list of known logs to verify against. + void AddLog(CTLogVerifier&& log); + + // Verifies SCTs embedded in the certificate itself, SCTs embedded in a + // stapled OCSP response, and SCTs obtained via the + // signed_certificate_timestamp TLS extension on the given |cert|. + // + // A certificate is permitted but not required to use multiple sources for + // SCTs. It is expected that most certificates will use only one source + // (embedding, TLS extension or OCSP stapling). + // + // The verifier stops on fatal errors (such as out of memory or invalid + // DER encoding of |cert|), but it does not stop on SCT decoding errors. See + // CTVerifyResult for more details. + // + // The internal state of the verifier object is not modified + // during the verification process. + // + // |cert| DER-encoded certificate to be validated using the provided SCTs. + // |sctListFromCert| SCT list embedded in |cert|, empty if not present. + // |issuerSubjectPublicKeyInfo| SPKI of |cert|'s issuer. Can be empty, + // in which case the embedded SCT list + // won't be verified. + // |sctListFromOCSPResponse| SCT list included in a stapled OCSP response + // for |cert|. Empty if not available. + // |sctListFromTLSExtension| is the SCT list from the TLS extension. Empty + // if no extension was present. + // |time| the current time. Used to make sure SCTs are not in the future. + // |result| will be filled with the SCTs present, divided into categories + // based on the verification result. + pkix::Result Verify(pkix::Input cert, pkix::Input issuerSubjectPublicKeyInfo, + pkix::Input sctListFromCert, + pkix::Input sctListFromOCSPResponse, + pkix::Input sctListFromTLSExtension, pkix::Time time, + CTVerifyResult& result); + + private: + // Verifies a list of SCTs from |encodedSctList| over |expectedEntry|, + // placing the verification results in |result|. The SCTs in the list + // come from |origin| (as will be reflected in the origin field of each SCT). + pkix::Result VerifySCTs(pkix::Input encodedSctList, + const LogEntry& expectedEntry, + VerifiedSCT::Origin origin, pkix::Time time, + CTVerifyResult& result); + + // Verifies a single, parsed SCT against all known logs. + // Note: moves |sct| to the target list in |result|, invalidating |sct|. + pkix::Result VerifySingleSCT(SignedCertificateTimestamp&& sct, + const ct::LogEntry& expectedEntry, + VerifiedSCT::Origin origin, pkix::Time time, + CTVerifyResult& result); + + // The list of known logs. + std::vector<CTLogVerifier> mLogs; +}; + +} // namespace ct +} // namespace mozilla + +#endif // MultiLogCTVerifier_h diff --git a/security/ct/SignedCertificateTimestamp.cpp b/security/ct/SignedCertificateTimestamp.cpp new file mode 100644 index 0000000000..573006d599 --- /dev/null +++ b/security/ct/SignedCertificateTimestamp.cpp @@ -0,0 +1,27 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include "SignedCertificateTimestamp.h" + +namespace mozilla { +namespace ct { + +void LogEntry::Reset() { + type = LogEntry::Type::X509; + leafCertificate.clear(); + issuerKeyHash.clear(); + tbsCertificate.clear(); +} + +bool DigitallySigned::SignatureParametersMatch( + HashAlgorithm aHashAlgorithm, + SignatureAlgorithm aSignatureAlgorithm) const { + return (hashAlgorithm == aHashAlgorithm) && + (signatureAlgorithm == aSignatureAlgorithm); +} + +} // namespace ct +} // namespace mozilla diff --git a/security/ct/SignedCertificateTimestamp.h b/security/ct/SignedCertificateTimestamp.h new file mode 100644 index 0000000000..2803439295 --- /dev/null +++ b/security/ct/SignedCertificateTimestamp.h @@ -0,0 +1,92 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#ifndef SignedCertificateTimestamp_h +#define SignedCertificateTimestamp_h + +#include "Buffer.h" +#include "mozpkix/Input.h" +#include "mozpkix/Result.h" + +// Structures related to Certificate Transparency (RFC 6962). +namespace mozilla { +namespace ct { + +// LogEntry struct in RFC 6962, Section 3.1. +struct LogEntry { + // LogEntryType enum in RFC 6962, Section 3.1. + enum class Type { X509 = 0, Precert = 1 }; + + void Reset(); + + Type type; + + // Set if type == X509. + Buffer leafCertificate; + + // Set if type == Precert. + Buffer issuerKeyHash; + Buffer tbsCertificate; +}; + +// Helper structure to represent Digitally Signed data, as described in +// Sections 4.7 and 7.4.1.4.1 of RFC 5246. +struct DigitallySigned { + enum class HashAlgorithm { + None = 0, + MD5 = 1, + SHA1 = 2, + SHA224 = 3, + SHA256 = 4, + SHA384 = 5, + SHA512 = 6, + }; + + enum class SignatureAlgorithm { Anonymous = 0, RSA = 1, DSA = 2, ECDSA = 3 }; + + // Returns true if |aHashAlgorithm| and |aSignatureAlgorithm| + // match this DigitallySigned hash and signature algorithms. + bool SignatureParametersMatch(HashAlgorithm aHashAlgorithm, + SignatureAlgorithm aSignatureAlgorithm) const; + + HashAlgorithm hashAlgorithm; + SignatureAlgorithm signatureAlgorithm; + // 'signature' field. + Buffer signatureData; +}; + +// SignedCertificateTimestamp struct in RFC 6962, Section 3.2. +struct SignedCertificateTimestamp { + // Version enum in RFC 6962, Section 3.2. + enum class Version { + V1 = 0, + }; + + 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; + DigitallySigned signature; +}; + +inline pkix::Result BufferToInput(const Buffer& buffer, pkix::Input& input) { + if (buffer.empty()) { + return pkix::Result::FATAL_ERROR_LIBRARY_FAILURE; + } + return input.Init(buffer.data(), buffer.size()); +} + +inline void InputToBuffer(pkix::Input input, Buffer& buffer) { + buffer.assign(input.UnsafeGetData(), + input.UnsafeGetData() + input.GetLength()); +} + +} // namespace ct +} // namespace mozilla + +#endif // SignedCertificateTimestamp_h diff --git a/security/ct/moz.build b/security/ct/moz.build new file mode 100644 index 0000000000..b9a2a62b28 --- /dev/null +++ b/security/ct/moz.build @@ -0,0 +1,53 @@ +# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*- +# vim: set filetype=python: +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at http://mozilla.org/MPL/2.0/. + +with Files("**"): + BUG_COMPONENT = ("Core", "Security: PSM") + +EXPORTS += [ + "BTTypes.h", + "BTVerifier.h", + "Buffer.h", + "CTLog.h", + "CTPolicyEnforcer.h", + "CTVerifyResult.h", + "SignedCertificateTimestamp.h", +] + +UNIFIED_SOURCES += [ + "BTVerifier.cpp", + "Buffer.cpp", + "CTDiversityPolicy.cpp", + "CTLogVerifier.cpp", + "CTObjectsExtractor.cpp", + "CTPolicyEnforcer.cpp", + "CTSerialization.cpp", + "CTVerifyResult.cpp", + "MultiLogCTVerifier.cpp", + "SignedCertificateTimestamp.cpp", +] + +TEST_DIRS += [ + "tests/gtest", +] + +if not CONFIG["MOZ_DEBUG"]: + DEFINES["NDEBUG"] = True + +CXXFLAGS += [ + "-Wextra", + "-Wunreachable-code", +] + +# Gecko headers aren't warning-free enough for us to enable these warnings. +CXXFLAGS += [ + "-Wno-unused-parameter", +] + +FINAL_LIBRARY = "xul" + +if CONFIG["CC_TYPE"] == "clang-cl": + AllowCompilerWarnings() # workaround for bug 1090497 diff --git a/security/ct/tests/gtest/BTSerializationTest.cpp b/security/ct/tests/gtest/BTSerializationTest.cpp new file mode 100644 index 0000000000..75c16faf42 --- /dev/null +++ b/security/ct/tests/gtest/BTSerializationTest.cpp @@ -0,0 +1,140 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include "BTVerifier.h" +#include "CTTestUtils.h" +#include "gtest/gtest.h" + +namespace mozilla { +namespace ct { + +using namespace pkix; + +class BTSerializationTest : public ::testing::Test { + public: + void SetUp() override { + mTestInclusionProof = GetTestInclusionProof(); + mTestInclusionProofUnexpectedData = GetTestInclusionProofUnexpectedData(); + mTestInclusionProofInvalidHashSize = GetTestInclusionProofInvalidHashSize(); + mTestInclusionProofInvalidHash = GetTestInclusionProofInvalidHash(); + mTestInclusionProofMissingLogId = GetTestInclusionProofMissingLogId(); + mTestInclusionProofNullPathLength = GetTestInclusionProofNullPathLength(); + mTestInclusionProofPathLengthTooSmall = + GetTestInclusionProofPathLengthTooSmall(); + mTestInclusionProofPathLengthTooLarge = + GetTestInclusionProofPathLengthTooLarge(); + mTestInclusionProofNullTreeSize = GetTestInclusionProofNullTreeSize(); + mTestInclusionProofLeafIndexOutOfBounds = + GetTestInclusionProofLeafIndexOutOfBounds(); + mTestInclusionProofExtraData = GetTestInclusionProofExtraData(); + } + + protected: + Buffer mTestInclusionProof; + Buffer mTestInclusionProofUnexpectedData; + Buffer mTestInclusionProofInvalidHashSize; + Buffer mTestInclusionProofInvalidHash; + Buffer mTestInclusionProofMissingLogId; + Buffer mTestInclusionProofNullPathLength; + Buffer mTestInclusionProofPathLengthTooSmall; + Buffer mTestInclusionProofPathLengthTooLarge; + Buffer mTestInclusionProofNullTreeSize; + Buffer mTestInclusionProofLeafIndexOutOfBounds; + Buffer mTestInclusionProofExtraData; +}; + +TEST_F(BTSerializationTest, DecodesInclusionProof) { + const uint64_t expectedTreeSize = 4; + const uint64_t expectedLeafIndex = 2; + const uint64_t expectedInclusionPathElements = 2; + + Buffer expectedLogId = {0x01, 0x00}; + + Input encodedProofInput = InputForBuffer(mTestInclusionProof); + InclusionProofDataV2 ipr; + ASSERT_EQ(Success, DecodeInclusionProof(encodedProofInput, ipr)); + EXPECT_EQ(expectedLogId, ipr.logId); + EXPECT_EQ(expectedTreeSize, ipr.treeSize); + EXPECT_EQ(expectedLeafIndex, ipr.leafIndex); + EXPECT_EQ(expectedInclusionPathElements, ipr.inclusionPath.size()); + EXPECT_EQ(GetTestNodeHash0(), ipr.inclusionPath[0]); + EXPECT_EQ(GetTestNodeHash1(), ipr.inclusionPath[1]); +} + +TEST_F(BTSerializationTest, FailsDecodingInclusionProofUnexpectedData) { + Input encodedProofInput = InputForBuffer(mTestInclusionProofUnexpectedData); + InclusionProofDataV2 ipr; + ASSERT_EQ(pkix::Result::ERROR_BAD_DER, + DecodeInclusionProof(encodedProofInput, ipr)); +} + +TEST_F(BTSerializationTest, FailsDecodingInvalidHashSize) { + Input encodedProofInput = InputForBuffer(mTestInclusionProofInvalidHashSize); + InclusionProofDataV2 ipr; + ASSERT_EQ(pkix::Result::ERROR_BAD_DER, + DecodeInclusionProof(encodedProofInput, ipr)); +} + +TEST_F(BTSerializationTest, FailsDecodingInvalidHash) { + Input encodedProofInput = InputForBuffer(mTestInclusionProofInvalidHash); + InclusionProofDataV2 ipr; + ASSERT_EQ(pkix::Result::ERROR_BAD_DER, + DecodeInclusionProof(encodedProofInput, ipr)); +} + +TEST_F(BTSerializationTest, FailsDecodingMissingLogId) { + Input encodedProofInput = InputForBuffer(mTestInclusionProofMissingLogId); + InclusionProofDataV2 ipr; + ASSERT_EQ(pkix::Result::ERROR_BAD_DER, + DecodeInclusionProof(encodedProofInput, ipr)); +} + +TEST_F(BTSerializationTest, FailsDecodingNullPathLength) { + Input encodedProofInput = InputForBuffer(mTestInclusionProofNullPathLength); + InclusionProofDataV2 ipr; + ASSERT_EQ(pkix::Result::ERROR_BAD_DER, + DecodeInclusionProof(encodedProofInput, ipr)); +} + +TEST_F(BTSerializationTest, FailsDecodingPathLengthTooSmall) { + Input encodedProofInput = + InputForBuffer(mTestInclusionProofPathLengthTooSmall); + InclusionProofDataV2 ipr; + ASSERT_EQ(pkix::Result::ERROR_BAD_DER, + DecodeInclusionProof(encodedProofInput, ipr)); +} + +TEST_F(BTSerializationTest, FailsDecodingPathLengthTooLarge) { + Input encodedProofInput = + InputForBuffer(mTestInclusionProofPathLengthTooLarge); + InclusionProofDataV2 ipr; + ASSERT_EQ(pkix::Result::ERROR_BAD_DER, + DecodeInclusionProof(encodedProofInput, ipr)); +} + +TEST_F(BTSerializationTest, FailsDecodingNullTreeSize) { + Input encodedProofInput = InputForBuffer(mTestInclusionProofNullTreeSize); + InclusionProofDataV2 ipr; + ASSERT_EQ(pkix::Result::ERROR_BAD_DER, + DecodeInclusionProof(encodedProofInput, ipr)); +} + +TEST_F(BTSerializationTest, FailsDecodingLeafIndexOutOfBounds) { + Input encodedProofInput = + InputForBuffer(mTestInclusionProofLeafIndexOutOfBounds); + InclusionProofDataV2 ipr; + ASSERT_EQ(pkix::Result::ERROR_BAD_DER, + DecodeInclusionProof(encodedProofInput, ipr)); +} + +TEST_F(BTSerializationTest, FailsDecodingExtraData) { + Input encodedProofInput = InputForBuffer(mTestInclusionProofExtraData); + InclusionProofDataV2 ipr; + ASSERT_EQ(pkix::Result::ERROR_BAD_DER, + DecodeInclusionProof(encodedProofInput, ipr)); +} +} // namespace ct +} // namespace mozilla diff --git a/security/ct/tests/gtest/BTSignedTreeHeadTest.cpp b/security/ct/tests/gtest/BTSignedTreeHeadTest.cpp new file mode 100644 index 0000000000..580d6a84f3 --- /dev/null +++ b/security/ct/tests/gtest/BTSignedTreeHeadTest.cpp @@ -0,0 +1,266 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include "BTVerifier.h" +#include "CTTestUtils.h" +#include "gtest/gtest.h" + +#include "nss.h" + +namespace mozilla { +namespace ct { + +using namespace pkix; + +struct BTSignedTreeHeadTestParams { + const char* mSubjectPublicKeyInfoHex; + pkix::DigestAlgorithm mDigestAlgorithm; + pkix::der::PublicKeyAlgorithm mPublicKeyAlgorithm; + const char* mSignedTreeHeadHex; + + pkix::Result mExpectedSignedTreeHeadResult; + uint64_t mExpectedTimestamp; + uint64_t mExpectedTreeSize; + const char* mExpectedRootHashHex; +}; + +class BTSignedTreeHeadTest + : public ::testing::Test, + public ::testing::WithParamInterface<BTSignedTreeHeadTestParams> { + void SetUp() override { + if (!NSS_IsInitialized()) { + if (NSS_NoDB_Init(nullptr) != SECSuccess) { + abort(); + } + } + } +}; + +namespace ValidSTH { +#include "valid-sth.inc" +} +namespace ValidWithExtensionSTH { +#include "valid-with-extension-sth.inc" +} +namespace ValidSecp521r1SHA512STH { +#include "valid-secp521r1-sha512-sth.inc" +} +namespace SignatureCoversLogIDSTH { +#include "signature-covers-log-id-sth.inc" +} +namespace WrongSPKISTH { +#include "wrong-spki-sth.inc" +} +namespace WrongSigningKeySTH { +#include "wrong-signing-key-sth.inc" +} +namespace MissingLogIDSTH { +#include "missing-log-id-sth.inc" +} +namespace MissingTimestampSTH { +#include "missing-timestamp-sth.inc" +} +namespace MissingTreeSizeSTH { +#include "missing-tree-size-sth.inc" +} +namespace MissingRootHashSTH { +#include "missing-root-hash-sth.inc" +} +namespace MissingExtensionsSTH { +#include "missing-extensions-sth.inc" +} +namespace TruncatedLogIDSTH { +#include "truncated-log-id-sth.inc" +} +namespace TruncatedTimestampSTH { +#include "truncated-timestamp-sth.inc" +} +namespace TruncatedTreeSizeSTH { +#include "truncated-tree-size-sth.inc" +} +namespace TruncatedRootHashSTH { +#include "truncated-root-hash-sth.inc" +} +namespace TruncatedExtensionSTH { +#include "truncated-extension-sth.inc" +} +namespace RSASignerRSASPKISTH { +#include "rsa-signer-rsa-spki-sth.inc" +} +namespace RSASignerECSPKISTH { +#include "rsa-signer-ec-spki-sth.inc" +} +namespace ECSignerRSASPKISTH { +#include "ec-signer-rsa-spki-sth.inc" +} + +static const char* kValidRootHashHex = + "d1a0d3947db4ae8305f2ac32985957e02659b2ea3c10da52a48d2526e9af3bbc"; + +static const char* kValidRootHashSHA512Hex = + "374d794a95cdcfd8b35993185fef9ba368f160d8daf432d08ba9f1ed1e5abe6c" + "c69291e0fa2fe0006a52570ef18c19def4e617c33ce52ef0a6e5fbe318cb0387"; + +static const BTSignedTreeHeadTestParams BT_SIGNED_TREE_HEAD_TEST_PARAMS[] = { + {ValidSTH::kSPKIHex, pkix::DigestAlgorithm::sha256, + pkix::der::PublicKeyAlgorithm::ECDSA, ValidSTH::kSTHHex, Success, + 1541189938000, 7, kValidRootHashHex}, + {ValidSTH::kSPKIHex, pkix::DigestAlgorithm::sha512, + pkix::der::PublicKeyAlgorithm::ECDSA, ValidSTH::kSTHHex, + Result::ERROR_BAD_SIGNATURE, 0, 0, nullptr}, + {ValidSTH::kSPKIHex, pkix::DigestAlgorithm::sha256, + pkix::der::PublicKeyAlgorithm::RSA_PKCS1, ValidSTH::kSTHHex, + Result::FATAL_ERROR_INVALID_ARGS, 0, 0, nullptr}, + {ValidWithExtensionSTH::kSPKIHex, pkix::DigestAlgorithm::sha256, + pkix::der::PublicKeyAlgorithm::ECDSA, ValidWithExtensionSTH::kSTHHex, + Success, 1541189938000, 7, kValidRootHashHex}, + {ValidSecp521r1SHA512STH::kSPKIHex, pkix::DigestAlgorithm::sha512, + pkix::der::PublicKeyAlgorithm::ECDSA, ValidSecp521r1SHA512STH::kSTHHex, + Success, 1542136309473, 731393445, kValidRootHashSHA512Hex}, + {ValidSecp521r1SHA512STH::kSPKIHex, pkix::DigestAlgorithm::sha256, + pkix::der::PublicKeyAlgorithm::ECDSA, ValidSecp521r1SHA512STH::kSTHHex, + Result::ERROR_BAD_SIGNATURE, 0, 0, nullptr}, + {ValidSTH::kSPKIHex, pkix::DigestAlgorithm::sha512, + pkix::der::PublicKeyAlgorithm::ECDSA, ValidSecp521r1SHA512STH::kSTHHex, + Result::ERROR_BAD_SIGNATURE, 0, 0, nullptr}, + {SignatureCoversLogIDSTH::kSPKIHex, pkix::DigestAlgorithm::sha256, + pkix::der::PublicKeyAlgorithm::ECDSA, SignatureCoversLogIDSTH::kSTHHex, + Result::ERROR_BAD_SIGNATURE, 0, 0, nullptr}, + {WrongSPKISTH::kSPKIHex, pkix::DigestAlgorithm::sha256, + pkix::der::PublicKeyAlgorithm::ECDSA, WrongSPKISTH::kSTHHex, + Result::ERROR_BAD_SIGNATURE, 0, 0, nullptr}, + {WrongSigningKeySTH::kSPKIHex, pkix::DigestAlgorithm::sha256, + pkix::der::PublicKeyAlgorithm::ECDSA, WrongSigningKeySTH::kSTHHex, + Result::ERROR_BAD_SIGNATURE, 0, 0, nullptr}, + {MissingLogIDSTH::kSPKIHex, pkix::DigestAlgorithm::sha256, + pkix::der::PublicKeyAlgorithm::ECDSA, MissingLogIDSTH::kSTHHex, + Result::ERROR_BAD_DER, 0, 0, nullptr}, + {MissingTimestampSTH::kSPKIHex, pkix::DigestAlgorithm::sha256, + pkix::der::PublicKeyAlgorithm::ECDSA, MissingTimestampSTH::kSTHHex, + Result::ERROR_BAD_DER, 0, 0, nullptr}, + {MissingTreeSizeSTH::kSPKIHex, pkix::DigestAlgorithm::sha256, + pkix::der::PublicKeyAlgorithm::ECDSA, MissingTreeSizeSTH::kSTHHex, + Result::ERROR_BAD_DER, 0, 0, nullptr}, + {MissingRootHashSTH::kSPKIHex, pkix::DigestAlgorithm::sha256, + pkix::der::PublicKeyAlgorithm::ECDSA, MissingRootHashSTH::kSTHHex, + Result::ERROR_BAD_DER, 0, 0, nullptr}, + {MissingExtensionsSTH::kSPKIHex, pkix::DigestAlgorithm::sha256, + pkix::der::PublicKeyAlgorithm::ECDSA, MissingExtensionsSTH::kSTHHex, + Result::ERROR_BAD_DER, 0, 0, nullptr}, + {TruncatedLogIDSTH::kSPKIHex, pkix::DigestAlgorithm::sha256, + pkix::der::PublicKeyAlgorithm::ECDSA, TruncatedLogIDSTH::kSTHHex, + Result::ERROR_BAD_DER, 0, 0, nullptr}, + {TruncatedTimestampSTH::kSPKIHex, pkix::DigestAlgorithm::sha256, + pkix::der::PublicKeyAlgorithm::ECDSA, TruncatedTimestampSTH::kSTHHex, + Result::ERROR_BAD_DER, 0, 0, nullptr}, + {TruncatedTreeSizeSTH::kSPKIHex, pkix::DigestAlgorithm::sha256, + pkix::der::PublicKeyAlgorithm::ECDSA, TruncatedTreeSizeSTH::kSTHHex, + Result::ERROR_BAD_DER, 0, 0, nullptr}, + {TruncatedRootHashSTH::kSPKIHex, pkix::DigestAlgorithm::sha256, + pkix::der::PublicKeyAlgorithm::ECDSA, TruncatedRootHashSTH::kSTHHex, + Result::ERROR_BAD_DER, 0, 0, nullptr}, + {TruncatedExtensionSTH::kSPKIHex, pkix::DigestAlgorithm::sha256, + pkix::der::PublicKeyAlgorithm::ECDSA, TruncatedExtensionSTH::kSTHHex, + Result::ERROR_BAD_DER, 0, 0, nullptr}, + {RSASignerRSASPKISTH::kSPKIHex, pkix::DigestAlgorithm::sha256, + pkix::der::PublicKeyAlgorithm::ECDSA, RSASignerRSASPKISTH::kSTHHex, + Result::ERROR_BAD_SIGNATURE, 0, 0, nullptr}, + {RSASignerECSPKISTH::kSPKIHex, pkix::DigestAlgorithm::sha256, + pkix::der::PublicKeyAlgorithm::ECDSA, RSASignerECSPKISTH::kSTHHex, + Result::ERROR_BAD_SIGNATURE, 0, 0, nullptr}, + {ECSignerRSASPKISTH::kSPKIHex, pkix::DigestAlgorithm::sha256, + pkix::der::PublicKeyAlgorithm::ECDSA, ECSignerRSASPKISTH::kSTHHex, + Result::ERROR_INVALID_KEY, 0, 0, nullptr}, +}; + +TEST_P(BTSignedTreeHeadTest, BTSignedTreeHeadSimpleTest) { + const BTSignedTreeHeadTestParams& params(GetParam()); + + Buffer subjectPublicKeyInfoBuffer( + HexToBytes(params.mSubjectPublicKeyInfoHex)); + Input subjectPublicKeyInfoInput = InputForBuffer(subjectPublicKeyInfoBuffer); + + Buffer signedTreeHeadBuffer(HexToBytes(params.mSignedTreeHeadHex)); + Input signedTreeHeadInput = InputForBuffer(signedTreeHeadBuffer); + + SignedTreeHeadDataV2 sth; + EXPECT_EQ(params.mExpectedSignedTreeHeadResult, + DecodeAndVerifySignedTreeHead( + subjectPublicKeyInfoInput, params.mDigestAlgorithm, + params.mPublicKeyAlgorithm, signedTreeHeadInput, sth)); + + if (params.mExpectedSignedTreeHeadResult == Success) { + EXPECT_EQ(params.mExpectedTimestamp, sth.timestamp); + EXPECT_EQ(params.mExpectedTreeSize, sth.treeSize); + EXPECT_EQ(HexToBytes(params.mExpectedRootHashHex), sth.rootHash); + } +} + +INSTANTIATE_TEST_SUITE_P(BTSignedTreeHeadTest, BTSignedTreeHeadTest, + testing::ValuesIn(BT_SIGNED_TREE_HEAD_TEST_PARAMS)); + +TEST_F(BTSignedTreeHeadTest, BTSignedTreeHeadTamperedSignatureTest) { + Buffer subjectPublicKeyInfoBuffer(HexToBytes(ValidSTH::kSPKIHex)); + Input subjectPublicKeyInfoInput = InputForBuffer(subjectPublicKeyInfoBuffer); + + Buffer signedTreeHeadBuffer(HexToBytes(ValidSTH::kSTHHex)); + ASSERT_TRUE(signedTreeHeadBuffer.size() > 15); + signedTreeHeadBuffer[signedTreeHeadBuffer.size() - 15] ^= 0xff; + Input signedTreeHeadInput = InputForBuffer(signedTreeHeadBuffer); + + SignedTreeHeadDataV2 sth; + EXPECT_EQ(Result::ERROR_BAD_SIGNATURE, + DecodeAndVerifySignedTreeHead(subjectPublicKeyInfoInput, + pkix::DigestAlgorithm::sha256, + pkix::der::PublicKeyAlgorithm::ECDSA, + signedTreeHeadInput, sth)); +} + +TEST_F(BTSignedTreeHeadTest, BTSignedTreeHeadTruncatedSignatureTest) { + Buffer subjectPublicKeyInfoBuffer(HexToBytes(ValidSTH::kSPKIHex)); + Input subjectPublicKeyInfoInput = InputForBuffer(subjectPublicKeyInfoBuffer); + + Buffer signedTreeHeadBuffer(HexToBytes(ValidSTH::kSTHHex)); + ASSERT_TRUE(signedTreeHeadBuffer.size() > 17); + signedTreeHeadBuffer.resize(signedTreeHeadBuffer.size() - 17); + Input signedTreeHeadInput = InputForBuffer(signedTreeHeadBuffer); + + SignedTreeHeadDataV2 sth; + EXPECT_EQ(Result::ERROR_BAD_DER, + DecodeAndVerifySignedTreeHead(subjectPublicKeyInfoInput, + pkix::DigestAlgorithm::sha256, + pkix::der::PublicKeyAlgorithm::ECDSA, + signedTreeHeadInput, sth)); +} + +TEST_F(BTSignedTreeHeadTest, BTSignedTreeHeadMissingSignatureTest) { + Buffer subjectPublicKeyInfoBuffer(HexToBytes(ValidSTH::kSPKIHex)); + Input subjectPublicKeyInfoInput = InputForBuffer(subjectPublicKeyInfoBuffer); + + Buffer signedTreeHeadBuffer = { + 0x02, 0x00, 0x00, + // 1541189938000 milliseconds since the epoch + 0x00, 0x00, 0x01, 0x66, 0xd6, 0x14, 0x2b, 0x50, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x07, // 7 total nodes + 0x20, // 32 byte hash + 0xd1, 0xa0, 0xd3, 0x94, 0x7d, 0xb4, 0xae, 0x83, 0x05, 0xf2, 0xac, 0x32, + 0x98, 0x59, 0x57, 0xe0, 0x26, 0x59, 0xb2, 0xea, 0x3c, 0x10, 0xda, 0x52, + 0xa4, 0x8d, 0x25, 0x26, 0xe9, 0xaf, 0x3b, 0xbc, 0x00, + 0x00, // no extensions + // missing signature + }; + Input signedTreeHeadInput = InputForBuffer(signedTreeHeadBuffer); + + SignedTreeHeadDataV2 sth; + EXPECT_EQ(Result::ERROR_BAD_DER, + DecodeAndVerifySignedTreeHead(subjectPublicKeyInfoInput, + pkix::DigestAlgorithm::sha256, + pkix::der::PublicKeyAlgorithm::ECDSA, + signedTreeHeadInput, sth)); +} + +} // namespace ct +} // namespace mozilla diff --git a/security/ct/tests/gtest/BTVerificationTest.cpp b/security/ct/tests/gtest/BTVerificationTest.cpp new file mode 100644 index 0000000000..41d8a11d2c --- /dev/null +++ b/security/ct/tests/gtest/BTVerificationTest.cpp @@ -0,0 +1,257 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include "BTVerifier.h" +#include "CTTestUtils.h" +#include "gtest/gtest.h" + +#include "nss.h" + +namespace mozilla { +namespace ct { + +using namespace pkix; + +struct BTVerificationTestParams { + const char* mInclusionProofHex; + const char* mExpectedRootHashHex; + const char* mInputHex; + pkix::DigestAlgorithm mDigestAlgorithm; + pkix::Result mExpectedVerificationResult; +}; + +class BTVerificationTest + : public ::testing::Test, + public ::testing::WithParamInterface<BTVerificationTestParams> { + void SetUp() override { + if (!NSS_IsInitialized()) { + if (NSS_NoDB_Init(nullptr) != SECSuccess) { + abort(); + } + } + } +}; + +// This comes from testing/mozharness/test/test_mozilla_merkle.py +static const char* kValidInclusionProofHex = + "020000" + "0000000000000007" // 7 total nodes + "0000000000000005" // leaf index is 5 + "0063" // 99 (in base 10) bytes follow + "20" // 32 byte hash + "2ff3bdac9bec3580b82da8a357746f15919414d9cbe517e2dd96910c9814c30c" + "20" + "bb13dfb7b202a95f241ea1715c8549dc048d9936ec747028002f7c795de72fcf" + "20" + "380d0dc6fd7d4f37859a12dbfc7171b3cce29ab0688c6cffd2b15f3e0b21af49"; + +static const char* kExpectedRootHashHex = + "d1a0d3947db4ae8305f2ac32985957e02659b2ea3c10da52a48d2526e9af3bbc"; + +static const char* kInputHex = "81aae91cf90be172eedd1c75c349bf9e"; +static const char* kAlteredInputHex = "81aae91c0000000000000000c349bf9e"; + +// Same as kValidInclusionProofHex, but missing the last hash (the proof is +// still structurally valid, so it decodes successfully). +static const char* kShortInclusionProofHex = + "020000" + "0000000000000007" // 7 total nodes + "0000000000000005" // leaf index is 5 + "0042" // 66 (in base 10) bytes follow + "20" // 32 byte hash + "2ff3bdac9bec3580b82da8a357746f15919414d9cbe517e2dd96910c9814c30c" + "20" + "bb13dfb7b202a95f241ea1715c8549dc048d9936ec747028002f7c795de72fcf"; + +// Same as kValidInclusionProofHex, but has too many hashes (the proof is +// still structurally valid, so it decodes successfully). +static const char* kLongInclusionProofHex = + "020000" + "0000000000000007" // 7 total nodes + "0000000000000005" // leaf index is 5 + "0084" // 132 (in base 10) bytes follow + "20" // 32 byte hash + "2ff3bdac9bec3580b82da8a357746f15919414d9cbe517e2dd96910c9814c30c" + "20" + "bb13dfb7b202a95f241ea1715c8549dc048d9936ec747028002f7c795de72fcf" + "20" + "380d0dc6fd7d4f37859a12dbfc7171b3cce29ab0688c6cffd2b15f3e0b21af49" + "20" + "abbaabbaabbaabbaabbaabbaabbaabbaabbaabbaabbaabbaabbaabbaabbaabba"; + +static const char* kIncorrectRootHashHex = + "beebeebeebeebeebeebeebeebeebeebeebeebeebeebeebeebeebeebeebeebeee"; + +static const char* kWrongTreeSizeInclusionProofHex = + "020000" + "0000000000000009" // 9 total nodes (this is wrong) + "0000000000000005" // leaf index is 5 + "0063" // 99 (in base 10) bytes follow + "20" // 32 byte hash + "2ff3bdac9bec3580b82da8a357746f15919414d9cbe517e2dd96910c9814c30c" + "20" + "bb13dfb7b202a95f241ea1715c8549dc048d9936ec747028002f7c795de72fcf" + "20" + "380d0dc6fd7d4f37859a12dbfc7171b3cce29ab0688c6cffd2b15f3e0b21af49"; + +static const char* kWrongLeafIndexInclusionProofHex = + "020000" + "0000000000000007" // 7 total nodes + "0000000000000002" // leaf index is 2 (this is wrong) + "0063" // 99 (in base 10) bytes follow + "20" // 32 byte hash + "2ff3bdac9bec3580b82da8a357746f15919414d9cbe517e2dd96910c9814c30c" + "20" + "bb13dfb7b202a95f241ea1715c8549dc048d9936ec747028002f7c795de72fcf" + "20" + "380d0dc6fd7d4f37859a12dbfc7171b3cce29ab0688c6cffd2b15f3e0b21af49"; + +// This is a longer proof taken from the Firefox 62.0.2 release. +static const char* kValidInclusionProof2Hex = + "020000" + "0000000000000cb3" + "00000000000000e2" + "018c" + "20" + "6d3d060c72c28cd1967a6ce4e87a470c266981ee8beb6e5956745b5839120338" + "20" + "cd028053c51f52447cd2b1ffea7761be546b72832d776db155008dc3071231eb" + "20" + "36167b32f7ba4980257c79ed65d133d8c53efbfd7addaa391a121953c78b2b98" + "20" + "f2b4abf09140b05058a74e25f4008157655e73fa4aeece9b3f2a40dbbae95c16" + "20" + "a2be4813b2ca0e7bb755ed5459789c25a83465fb3f2808590c04c64cdbb404ef" + "20" + "43e4341b74ea11f075f35761b867cb256e9d42c4c1253d6b5c958174daa800da" + "20" + "1c925ec8d2d4014c0b84d7bed8065d9e7b326e3e4eaebc90d7a6a66431249851" + "20" + "bd275e9e4e48c3725f6ef68c2661d4d6ae415129cfb0ea0881177f5347d683ae" + "20" + "01e84996a7cc1719b19a4a7e48941657b125bcd0dfd03d183daff87de6a480c3" + "20" + "664b6962909692980661be55e8bdadc48350b2461d37372457e8ff97824ae8da" + "20" + "39f7ab1500a58643fccc9e07ed409be4c707c5536b95c57f6716207656464019" + "20" + "d68c856664d660456d4c85498c2cc2abfb955a63bd1fb23f3e25850da80153f6"; + +static const char* kExpectedRootHash2Hex = + "0d489b16a64b80035a8d08ce55549c89c4b67be64a0456382cc5be1e51ddae1a"; + +// This the hex encoding of the SHA-256 hash of +// linux-x86_64/en-US/firefox-62.0.2.tar.bz2, encoded (again) in hex (because +// that's how generate-checksums.py currently builds the binary transparency +// tree). +static const char* kInput2Hex = + "3231623461306361626538663030306261663236373235643530313462386434" + "3865353561333832336538653665656131343638313062313031623034396536"; + +// This is the hex encoding of an inclusion proof for +// update/win32/en-GB/firefox-61.0-62.0.2.partial.mar using SHA-512. +static const char* kValidInclusionProofSHA512Hex = + "020000" + "0000000000000cb3" + "00000000000007ac" + "030c" + "40" + "b39fbe359108724ec1daed2a8c88511c6b94a74563bfaaf25b774286ab03cab9" + "b2e64bbd4eeb02003b7397cd814b5d18a160e14ad08eca04559abe146201e027" + "40" + "de119745a41884ed482a41638cc1979d89e1bd0ca295e0c412e7ee4b5c2e042d" + "a1f4aa9c7f1e58b78fac10400b207197383177401976ef6baa80d482c102a94f" + "40" + "d0fe203b0e95cbe48e2e663538aba382718c85ae167d8c4ccf326519f81b3860" + "55058f6a49943b8ed228af88b3414ec00e939ad8dc736f502ca90d63494eddcd" + "40" + "ae825352bbe12845fb1d3be27375c27dd462ebd6dc6d7bd0b5929df97aaccf1a" + "9eedb304ae1e9742b1e2a2ba8b22408d2d52e3bb3d547632d5f03cfe112572d2" + "40" + "1ab0f7f7d674c691cf8e51d4409d3402d94927231b82f7f71f16828dd9e36152" + "1a69f7e571df06aea16c83df88dc74042444ae8771375a7118477275395b570d" + "40" + "9c6b94c5be456c68518753cf626daf11b30ce981e18b76c036664dcafeba19b7" + "3f5ee20ab35aa38b11c92e2c8f103d1b19113b7171c615ce0b9c5ff520242d8c" + "40" + "d395ef3cd63ae617a96bd6c5c361d5f6aaccff1ed90599115394310dfa0c528d" + "611998d325f374c01a827ee900d72ed66d299845438bba565ba666c588b232a0" + "40" + "ce3548561f5ec7a94a6c7a2adc604f5e32869d0727cee80f12c95cab2a996965" + "6ad61a02b35e0fb2d0031ea496381f1413d33b8cbfdf092accd903881e083ee0" + "40" + "a4247b649fee9017d9c9e729c4763719b7155056013abc488de0079dc7bddce6" + "57a93f2596967cfb123d812a901bc4ffe0338cbecf30adc1a821e7261f88e3cd" + "40" + "7d2056ea6645b0306373fa2e26fbc4a4ccd4c82cb7d605d2372fa297654cd3a0" + "b151f6b0fffe7472661de79c7ce7873cb75e05cad5ea40173be540e16d51414d" + "40" + "eed2c98b3d8b8035777af74a0d5dd38963f84c09e6f42b6ca4e290674c175035" + "b9bc85bb4304fcf3d042c444f951b3b134563c6936a1b176679e35e94c42416b" + "40" + "6167cccd9d03909f95bb389936766340234b5b3e35dfa8c81d330db9055d7b7d" + "bfaf262f1821b4a68f43af62c53987550b4174eaab8357f9bd83f2715c98d441"; + +// This is the hex encoding of the expected root hash of the SHA-512 tree. +static const char* kExpectedRootHashSHA512Hex = + "6f20812da2980393767e98dd7f79d89aa41e029c790d3908739c02c01d9c6525" + "8bb3e24ab608c95b063259620751ce258930d48929cbe543b166c33c4de65e60"; + +// This the hex encoding of the SHA-512 hash of +// update/win32/en-GB/firefox-61.0-62.0.2.partial.mar, encoded (again) in hex. +static const char* kInputSHA512Hex = + "6237656563626164353363386432346230383763666437323333303162326437" + "6234326136323536663462636336356134623231636437376133613830613734" + "3865396533623666306432333839666234623763396563356638643833373534" + "6239313635313165333661373238653863303236376564653761666561316638"; + +static const BTVerificationTestParams BT_VERIFICATION_TEST_PARAMS[] = { + {kValidInclusionProofHex, kExpectedRootHashHex, kInputHex, + pkix::DigestAlgorithm::sha256, Success}, + {kValidInclusionProofHex, kExpectedRootHashHex, kAlteredInputHex, + pkix::DigestAlgorithm::sha256, pkix::Result::ERROR_BAD_SIGNATURE}, + {kShortInclusionProofHex, kExpectedRootHashHex, kInputHex, + pkix::DigestAlgorithm::sha256, pkix::Result::ERROR_BAD_SIGNATURE}, + {kLongInclusionProofHex, kExpectedRootHashHex, kInputHex, + pkix::DigestAlgorithm::sha256, pkix::Result::ERROR_BAD_SIGNATURE}, + {kValidInclusionProofHex, kIncorrectRootHashHex, kInputHex, + pkix::DigestAlgorithm::sha256, pkix::Result::ERROR_BAD_SIGNATURE}, + {kWrongTreeSizeInclusionProofHex, kExpectedRootHashHex, kInputHex, + pkix::DigestAlgorithm::sha256, pkix::Result::ERROR_BAD_SIGNATURE}, + {kWrongLeafIndexInclusionProofHex, kExpectedRootHashHex, kInputHex, + pkix::DigestAlgorithm::sha256, pkix::Result::ERROR_BAD_SIGNATURE}, + {kValidInclusionProofHex, kExpectedRootHashHex, kInputHex, + pkix::DigestAlgorithm::sha512, pkix::Result::ERROR_BAD_SIGNATURE}, + {kValidInclusionProof2Hex, kExpectedRootHash2Hex, kInput2Hex, + pkix::DigestAlgorithm::sha256, Success}, + {kValidInclusionProofSHA512Hex, kExpectedRootHashSHA512Hex, kInputSHA512Hex, + pkix::DigestAlgorithm::sha512, Success}, + {kValidInclusionProofSHA512Hex, kExpectedRootHashSHA512Hex, kInputSHA512Hex, + pkix::DigestAlgorithm::sha256, pkix::Result::ERROR_BAD_SIGNATURE}, +}; + +TEST_P(BTVerificationTest, BTVerificationSimpleTest) { + const BTVerificationTestParams& params(GetParam()); + + Buffer encodedProofBuffer(HexToBytes(params.mInclusionProofHex)); + Input encodedProof = InputForBuffer(encodedProofBuffer); + InclusionProofDataV2 ipr; + ASSERT_EQ(Success, DecodeInclusionProof(encodedProof, ipr)); + + Buffer leafEntryBuffer(HexToBytes(params.mInputHex)); + Input leafEntry = InputForBuffer(leafEntryBuffer); + Buffer expectedRootHashBuffer(HexToBytes(params.mExpectedRootHashHex)); + Input expectedRootHash = InputForBuffer(expectedRootHashBuffer); + ASSERT_EQ(params.mExpectedVerificationResult, + VerifyInclusionProof(ipr, leafEntry, expectedRootHash, + params.mDigestAlgorithm)); +} + +INSTANTIATE_TEST_SUITE_P(BTVerificationTest, BTVerificationTest, + testing::ValuesIn(BT_VERIFICATION_TEST_PARAMS)); + +} // namespace ct +} // namespace mozilla diff --git a/security/ct/tests/gtest/CTDiversityPolicyTest.cpp b/security/ct/tests/gtest/CTDiversityPolicyTest.cpp new file mode 100644 index 0000000000..bf999acea4 --- /dev/null +++ b/security/ct/tests/gtest/CTDiversityPolicyTest.cpp @@ -0,0 +1,15 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include "CTDiversityPolicy.h" + +namespace mozilla { +namespace ct { + +// TBD, PENDING FINALIZATION OF MOZILLA CT POLICY. + +} +} // namespace mozilla diff --git a/security/ct/tests/gtest/CTLogVerifierTest.cpp b/security/ct/tests/gtest/CTLogVerifierTest.cpp new file mode 100644 index 0000000000..1b7374a836 --- /dev/null +++ b/security/ct/tests/gtest/CTLogVerifierTest.cpp @@ -0,0 +1,118 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include "CTLogVerifier.h" +#include "CTTestUtils.h" +#include "nss.h" + +#include "gtest/gtest.h" + +namespace mozilla { +namespace ct { + +using namespace pkix; + +class CTLogVerifierTest : public ::testing::Test { + public: + void SetUp() override { + // Does nothing if NSS is already initialized. + if (NSS_NoDB_Init(nullptr) != SECSuccess) { + abort(); + } + + ASSERT_EQ(Success, + mLog.Init(InputForBuffer(GetTestPublicKey()), -1 /*operator id*/, + CTLogStatus::Included, 0 /*disqualification time*/)); + ASSERT_EQ(GetTestPublicKeyId(), mLog.keyId()); + } + + protected: + CTLogVerifier mLog; +}; + +TEST_F(CTLogVerifierTest, VerifiesCertSCT) { + LogEntry certEntry; + GetX509CertLogEntry(certEntry); + + SignedCertificateTimestamp certSct; + GetX509CertSCT(certSct); + + EXPECT_EQ(Success, mLog.Verify(certEntry, certSct)); +} + +TEST_F(CTLogVerifierTest, VerifiesPrecertSCT) { + LogEntry precertEntry; + GetPrecertLogEntry(precertEntry); + + SignedCertificateTimestamp precertSct; + GetPrecertSCT(precertSct); + + EXPECT_EQ(Success, mLog.Verify(precertEntry, precertSct)); +} + +TEST_F(CTLogVerifierTest, FailsInvalidTimestamp) { + LogEntry certEntry; + GetX509CertLogEntry(certEntry); + + SignedCertificateTimestamp certSct; + GetX509CertSCT(certSct); + + // Mangle the timestamp, so that it should fail signature validation. + certSct.timestamp = 0; + + EXPECT_EQ(pkix::Result::ERROR_BAD_SIGNATURE, mLog.Verify(certEntry, certSct)); +} + +TEST_F(CTLogVerifierTest, FailsInvalidSignature) { + LogEntry certEntry; + GetX509CertLogEntry(certEntry); + + // Mangle the value of the signature, making the underlying signature + // verification code return ERROR_BAD_SIGNATURE. + SignedCertificateTimestamp certSct; + GetX509CertSCT(certSct); + certSct.signature.signatureData[20] ^= '\xFF'; + EXPECT_EQ(pkix::Result::ERROR_BAD_SIGNATURE, mLog.Verify(certEntry, certSct)); + + // Mangle the encoding of the signature, making the underlying implementation + // return ERROR_BAD_DER. We still expect the verifier to return + // ERROR_BAD_SIGNATURE. + SignedCertificateTimestamp certSct2; + GetX509CertSCT(certSct2); + certSct2.signature.signatureData[0] ^= '\xFF'; + EXPECT_EQ(pkix::Result::ERROR_BAD_SIGNATURE, + mLog.Verify(certEntry, certSct2)); +} + +TEST_F(CTLogVerifierTest, FailsInvalidLogID) { + LogEntry certEntry; + GetX509CertLogEntry(certEntry); + + SignedCertificateTimestamp certSct; + GetX509CertSCT(certSct); + + // Mangle the log ID, which should cause it to match a different log before + // attempting signature validation. + certSct.logId.push_back('\x0'); + + EXPECT_EQ(pkix::Result::FATAL_ERROR_INVALID_ARGS, + mLog.Verify(certEntry, certSct)); +} + +// Test that excess data after the public key is rejected. +TEST_F(CTLogVerifierTest, ExcessDataInPublicKey) { + Buffer key = GetTestPublicKey(); + std::string extra = "extra"; + key.insert(key.end(), extra.begin(), extra.end()); + + CTLogVerifier log; + EXPECT_NE(Success, + log.Init(InputForBuffer(key), -1 /*operator id*/, + CTLogStatus::Included, 0 /*disqualification time*/)); +} + +} // namespace ct +} // namespace mozilla diff --git a/security/ct/tests/gtest/CTObjectsExtractorTest.cpp b/security/ct/tests/gtest/CTObjectsExtractorTest.cpp new file mode 100644 index 0000000000..20b454b98c --- /dev/null +++ b/security/ct/tests/gtest/CTObjectsExtractorTest.cpp @@ -0,0 +1,82 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include "CTLogVerifier.h" +#include "CTObjectsExtractor.h" +#include "CTSerialization.h" +#include "CTTestUtils.h" +#include "gtest/gtest.h" +#include "nss.h" + +namespace mozilla { +namespace ct { + +using namespace pkix; + +class CTObjectsExtractorTest : public ::testing::Test { + public: + void SetUp() override { + // Does nothing if NSS is already initialized. + if (NSS_NoDB_Init(nullptr) != SECSuccess) { + abort(); + } + + mTestCert = GetDEREncodedX509Cert(); + mEmbeddedCert = GetDEREncodedTestEmbeddedCert(); + mCaCert = GetDEREncodedCACert(); + mCaCertSPKI = ExtractCertSPKI(mCaCert); + + Buffer logPublicKey = GetTestPublicKey(); + ASSERT_EQ(Success, + mLog.Init(InputForBuffer(logPublicKey), -1 /*operator id*/, + CTLogStatus::Included, 0 /*disqualification time*/)); + } + + protected: + Buffer mTestCert; + Buffer mEmbeddedCert; + Buffer mCaCert; + Buffer mCaCertSPKI; + CTLogVerifier mLog; +}; + +TEST_F(CTObjectsExtractorTest, ExtractPrecert) { + LogEntry entry; + ASSERT_EQ(Success, GetPrecertLogEntry(InputForBuffer(mEmbeddedCert), + InputForBuffer(mCaCertSPKI), entry)); + + EXPECT_EQ(LogEntry::Type::Precert, entry.type); + // Should have empty leaf cert for this log entry type. + EXPECT_TRUE(entry.leafCertificate.empty()); + EXPECT_EQ(GetDefaultIssuerKeyHash(), entry.issuerKeyHash); + EXPECT_EQ(GetDEREncodedTestTbsCert(), entry.tbsCertificate); +} + +TEST_F(CTObjectsExtractorTest, ExtractOrdinaryX509Cert) { + LogEntry entry; + GetX509LogEntry(InputForBuffer(mTestCert), entry); + + EXPECT_EQ(LogEntry::Type::X509, entry.type); + // Should have empty tbsCertificate / issuerKeyHash for this log entry type. + EXPECT_TRUE(entry.tbsCertificate.empty()); + EXPECT_TRUE(entry.issuerKeyHash.empty()); + // Length of leafCertificate should be 718, see the CT Serialization tests. + EXPECT_EQ(718U, entry.leafCertificate.size()); +} + +// Test that an externally-provided SCT verifies over the LogEntry +// of a regular X.509 Certificate +TEST_F(CTObjectsExtractorTest, ComplementarySCTVerifies) { + SignedCertificateTimestamp sct; + GetX509CertSCT(sct); + + LogEntry entry; + GetX509LogEntry(InputForBuffer(mTestCert), entry); + EXPECT_EQ(Success, mLog.Verify(entry, sct)); +} + +} // namespace ct +} // namespace mozilla diff --git a/security/ct/tests/gtest/CTPolicyEnforcerTest.cpp b/security/ct/tests/gtest/CTPolicyEnforcerTest.cpp new file mode 100644 index 0000000000..7bc5b296a5 --- /dev/null +++ b/security/ct/tests/gtest/CTPolicyEnforcerTest.cpp @@ -0,0 +1,387 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include "CTPolicyEnforcer.h" + +#include <algorithm> +#include <stdint.h> +#include <stdio.h> + +#include "CTLogVerifier.h" +#include "CTVerifyResult.h" +#include "SignedCertificateTimestamp.h" +#include "mozpkix/Time.h" +#include "gtest/gtest.h" +#include "hasht.h" +#include "prtime.h" + +// Implemented in CertVerifier.cpp. +extern mozilla::pkix::Result GetCertLifetimeInFullMonths( + mozilla::pkix::Time certNotBefore, mozilla::pkix::Time certNotAfter, + size_t& months); + +namespace mozilla { +namespace ct { + +using namespace mozilla::pkix; + +class CTPolicyEnforcerTest : public ::testing::Test { + public: + void SetUp() override { + OPERATORS_1_AND_2.push_back(OPERATOR_1); + OPERATORS_1_AND_2.push_back(OPERATOR_2); + } + + void GetLogId(Buffer& logId, size_t logNo) { + logId.resize(SHA256_LENGTH); + std::fill(logId.begin(), logId.end(), 0); + // Just raw-copy |logId| into the output buffer. + assert(sizeof(logNo) <= logId.size()); + memcpy(logId.data(), &logNo, sizeof(logNo)); + } + + void AddSct(VerifiedSCTList& verifiedScts, size_t logNo, + CTLogOperatorId operatorId, VerifiedSCT::Origin origin, + uint64_t timestamp, + VerifiedSCT::Status status = VerifiedSCT::Status::Valid) { + VerifiedSCT verifiedSct; + verifiedSct.status = status; + verifiedSct.origin = origin; + verifiedSct.logOperatorId = operatorId; + verifiedSct.logDisqualificationTime = + status == VerifiedSCT::Status::ValidFromDisqualifiedLog + ? DISQUALIFIED_AT + : UINT64_MAX; + verifiedSct.sct.version = SignedCertificateTimestamp::Version::V1; + verifiedSct.sct.timestamp = timestamp; + Buffer logId; + GetLogId(logId, logNo); + verifiedSct.sct.logId = std::move(logId); + verifiedScts.push_back(std::move(verifiedSct)); + } + + void AddMultipleScts( + VerifiedSCTList& verifiedScts, size_t logsCount, uint8_t operatorsCount, + VerifiedSCT::Origin origin, uint64_t timestamp, + VerifiedSCT::Status status = VerifiedSCT::Status::Valid) { + for (size_t logNo = 0; logNo < logsCount; logNo++) { + CTLogOperatorId operatorId = logNo % operatorsCount; + AddSct(verifiedScts, logNo, operatorId, origin, timestamp, status); + } + } + + void CheckCompliance(const VerifiedSCTList& verifiedSct, + size_t certLifetimeInCalendarMonths, + const CTLogOperatorList& dependentLogOperators, + CTPolicyCompliance expectedCompliance) { + CTPolicyCompliance compliance; + mPolicyEnforcer.CheckCompliance(verifiedSct, certLifetimeInCalendarMonths, + dependentLogOperators, compliance); + EXPECT_EQ(expectedCompliance, compliance); + } + + protected: + CTPolicyEnforcer mPolicyEnforcer; + + const size_t LOG_1 = 1; + const size_t LOG_2 = 2; + const size_t LOG_3 = 3; + const size_t LOG_4 = 4; + const size_t LOG_5 = 5; + + const CTLogOperatorId OPERATOR_1 = 1; + const CTLogOperatorId OPERATOR_2 = 2; + const CTLogOperatorId OPERATOR_3 = 3; + + CTLogOperatorList NO_OPERATORS; + CTLogOperatorList OPERATORS_1_AND_2; + + const VerifiedSCT::Origin ORIGIN_EMBEDDED = VerifiedSCT::Origin::Embedded; + const VerifiedSCT::Origin ORIGIN_TLS = VerifiedSCT::Origin::TLSExtension; + const VerifiedSCT::Origin ORIGIN_OCSP = VerifiedSCT::Origin::OCSPResponse; + + // 4 years of cert lifetime requires 5 SCTs for the embedded case. + const size_t DEFAULT_MONTHS = 4 * 12L; + + // Date.parse("2015-08-15T00:00:00Z") + const uint64_t TIMESTAMP_1 = 1439596800000L; + + // Date.parse("2016-04-15T00:00:00Z") + const uint64_t DISQUALIFIED_AT = 1460678400000L; + + // Date.parse("2016-04-01T00:00:00Z") + const uint64_t BEFORE_DISQUALIFIED = 1459468800000L; + + // Date.parse("2016-04-16T00:00:00Z") + const uint64_t AFTER_DISQUALIFIED = 1460764800000L; +}; + +TEST_F(CTPolicyEnforcerTest, ConformsToCTPolicyWithNonEmbeddedSCTs) { + VerifiedSCTList scts; + + AddSct(scts, LOG_1, OPERATOR_1, ORIGIN_TLS, TIMESTAMP_1); + AddSct(scts, LOG_2, OPERATOR_2, ORIGIN_TLS, TIMESTAMP_1); + + CheckCompliance(scts, DEFAULT_MONTHS, NO_OPERATORS, + CTPolicyCompliance::Compliant); +} + +TEST_F(CTPolicyEnforcerTest, DoesNotConformNotEnoughDiverseNonEmbeddedSCTs) { + VerifiedSCTList scts; + + AddSct(scts, LOG_1, OPERATOR_1, ORIGIN_TLS, TIMESTAMP_1); + AddSct(scts, LOG_2, OPERATOR_2, ORIGIN_TLS, TIMESTAMP_1); + + CheckCompliance(scts, DEFAULT_MONTHS, OPERATORS_1_AND_2, + CTPolicyCompliance::NotDiverseScts); +} + +TEST_F(CTPolicyEnforcerTest, ConformsToCTPolicyWithEmbeddedSCTs) { + VerifiedSCTList scts; + + // 5 embedded SCTs required for DEFAULT_MONTHS. + AddSct(scts, LOG_1, OPERATOR_1, ORIGIN_EMBEDDED, TIMESTAMP_1); + AddSct(scts, LOG_2, OPERATOR_1, ORIGIN_EMBEDDED, TIMESTAMP_1); + AddSct(scts, LOG_3, OPERATOR_1, ORIGIN_EMBEDDED, TIMESTAMP_1); + AddSct(scts, LOG_4, OPERATOR_1, ORIGIN_EMBEDDED, TIMESTAMP_1); + AddSct(scts, LOG_5, OPERATOR_2, ORIGIN_EMBEDDED, TIMESTAMP_1); + + CheckCompliance(scts, DEFAULT_MONTHS, NO_OPERATORS, + CTPolicyCompliance::Compliant); +} + +TEST_F(CTPolicyEnforcerTest, DoesNotConformNotEnoughDiverseEmbeddedSCTs) { + VerifiedSCTList scts; + + // 5 embedded SCTs required for DEFAULT_MONTHS. + AddSct(scts, LOG_1, OPERATOR_1, ORIGIN_EMBEDDED, TIMESTAMP_1); + AddSct(scts, LOG_2, OPERATOR_1, ORIGIN_EMBEDDED, TIMESTAMP_1); + AddSct(scts, LOG_3, OPERATOR_1, ORIGIN_EMBEDDED, TIMESTAMP_1); + AddSct(scts, LOG_4, OPERATOR_1, ORIGIN_EMBEDDED, TIMESTAMP_1); + AddSct(scts, LOG_5, OPERATOR_2, ORIGIN_EMBEDDED, TIMESTAMP_1); + + CheckCompliance(scts, DEFAULT_MONTHS, OPERATORS_1_AND_2, + CTPolicyCompliance::NotDiverseScts); +} + +TEST_F(CTPolicyEnforcerTest, ConformsToCTPolicyWithPooledNonEmbeddedSCTs) { + VerifiedSCTList scts; + + AddSct(scts, LOG_1, OPERATOR_1, ORIGIN_OCSP, TIMESTAMP_1); + AddSct(scts, LOG_2, OPERATOR_2, ORIGIN_TLS, TIMESTAMP_1); + + CheckCompliance(scts, DEFAULT_MONTHS, NO_OPERATORS, + CTPolicyCompliance::Compliant); +} + +TEST_F(CTPolicyEnforcerTest, ConformsToCTPolicyWithPooledEmbeddedSCTs) { + VerifiedSCTList scts; + + AddSct(scts, LOG_1, OPERATOR_1, ORIGIN_EMBEDDED, TIMESTAMP_1); + AddSct(scts, LOG_2, OPERATOR_2, ORIGIN_OCSP, TIMESTAMP_1); + + CheckCompliance(scts, DEFAULT_MONTHS, NO_OPERATORS, + CTPolicyCompliance::Compliant); +} + +TEST_F(CTPolicyEnforcerTest, DoesNotConformToCTPolicyNotEnoughSCTs) { + VerifiedSCTList scts; + + AddSct(scts, LOG_1, OPERATOR_1, ORIGIN_EMBEDDED, TIMESTAMP_1); + AddSct(scts, LOG_2, OPERATOR_2, ORIGIN_EMBEDDED, TIMESTAMP_1); + + CheckCompliance(scts, DEFAULT_MONTHS, NO_OPERATORS, + CTPolicyCompliance::NotEnoughScts); +} + +TEST_F(CTPolicyEnforcerTest, DoesNotConformToCTPolicyNotEnoughFreshSCTs) { + VerifiedSCTList scts; + + // The results should be the same before and after disqualification, + // regardless of the delivery method. + + // SCT from before disqualification. + scts.clear(); + AddSct(scts, LOG_1, OPERATOR_1, ORIGIN_TLS, TIMESTAMP_1); + AddSct(scts, LOG_2, OPERATOR_2, ORIGIN_TLS, BEFORE_DISQUALIFIED, + VerifiedSCT::Status::ValidFromDisqualifiedLog); + CheckCompliance(scts, DEFAULT_MONTHS, NO_OPERATORS, + CTPolicyCompliance::NotEnoughScts); + // SCT from after disqualification. + scts.clear(); + AddSct(scts, LOG_1, OPERATOR_1, ORIGIN_TLS, TIMESTAMP_1); + AddSct(scts, LOG_2, OPERATOR_2, ORIGIN_TLS, AFTER_DISQUALIFIED, + VerifiedSCT::Status::ValidFromDisqualifiedLog); + CheckCompliance(scts, DEFAULT_MONTHS, NO_OPERATORS, + CTPolicyCompliance::NotEnoughScts); + + // Embedded SCT from before disqualification. + scts.clear(); + AddSct(scts, LOG_1, OPERATOR_1, ORIGIN_TLS, TIMESTAMP_1); + AddSct(scts, LOG_2, OPERATOR_2, ORIGIN_EMBEDDED, BEFORE_DISQUALIFIED, + VerifiedSCT::Status::ValidFromDisqualifiedLog); + CheckCompliance(scts, DEFAULT_MONTHS, NO_OPERATORS, + CTPolicyCompliance::NotEnoughScts); + + // Embedded SCT from after disqualification. + scts.clear(); + AddSct(scts, LOG_1, OPERATOR_1, ORIGIN_TLS, TIMESTAMP_1); + AddSct(scts, LOG_2, OPERATOR_2, ORIGIN_EMBEDDED, AFTER_DISQUALIFIED, + VerifiedSCT::Status::ValidFromDisqualifiedLog); + CheckCompliance(scts, DEFAULT_MONTHS, NO_OPERATORS, + CTPolicyCompliance::NotEnoughScts); +} + +TEST_F(CTPolicyEnforcerTest, + ConformsWithDisqualifiedLogBeforeDisqualificationDate) { + VerifiedSCTList scts; + + // 5 embedded SCTs required for DEFAULT_MONTHS. + AddSct(scts, LOG_1, OPERATOR_1, ORIGIN_EMBEDDED, TIMESTAMP_1); + AddSct(scts, LOG_2, OPERATOR_1, ORIGIN_EMBEDDED, TIMESTAMP_1); + AddSct(scts, LOG_3, OPERATOR_1, ORIGIN_EMBEDDED, TIMESTAMP_1); + AddSct(scts, LOG_4, OPERATOR_1, ORIGIN_EMBEDDED, TIMESTAMP_1); + AddSct(scts, LOG_5, OPERATOR_2, ORIGIN_EMBEDDED, BEFORE_DISQUALIFIED, + VerifiedSCT::Status::ValidFromDisqualifiedLog); + + CheckCompliance(scts, DEFAULT_MONTHS, NO_OPERATORS, + CTPolicyCompliance::Compliant); +} + +TEST_F(CTPolicyEnforcerTest, + DoesNotConformWithDisqualifiedLogAfterDisqualificationDate) { + VerifiedSCTList scts; + + // 5 embedded SCTs required for DEFAULT_MONTHS. + AddSct(scts, LOG_1, OPERATOR_1, ORIGIN_EMBEDDED, TIMESTAMP_1); + AddSct(scts, LOG_2, OPERATOR_1, ORIGIN_EMBEDDED, TIMESTAMP_1); + AddSct(scts, LOG_3, OPERATOR_1, ORIGIN_EMBEDDED, TIMESTAMP_1); + AddSct(scts, LOG_4, OPERATOR_1, ORIGIN_EMBEDDED, TIMESTAMP_1); + AddSct(scts, LOG_5, OPERATOR_2, ORIGIN_EMBEDDED, AFTER_DISQUALIFIED, + VerifiedSCT::Status::ValidFromDisqualifiedLog); + + CheckCompliance(scts, DEFAULT_MONTHS, NO_OPERATORS, + CTPolicyCompliance::NotEnoughScts); +} + +TEST_F(CTPolicyEnforcerTest, + DoesNotConformWithIssuanceDateAfterDisqualificationDate) { + VerifiedSCTList scts; + + // 5 embedded SCTs required for DEFAULT_MONTHS. + AddSct(scts, LOG_1, OPERATOR_1, ORIGIN_EMBEDDED, AFTER_DISQUALIFIED, + VerifiedSCT::Status::ValidFromDisqualifiedLog); + AddSct(scts, LOG_2, OPERATOR_1, ORIGIN_EMBEDDED, AFTER_DISQUALIFIED); + AddSct(scts, LOG_3, OPERATOR_1, ORIGIN_EMBEDDED, AFTER_DISQUALIFIED); + AddSct(scts, LOG_4, OPERATOR_1, ORIGIN_EMBEDDED, AFTER_DISQUALIFIED); + AddSct(scts, LOG_5, OPERATOR_2, ORIGIN_EMBEDDED, AFTER_DISQUALIFIED); + + CheckCompliance(scts, DEFAULT_MONTHS, NO_OPERATORS, + CTPolicyCompliance::NotEnoughScts); +} + +TEST_F(CTPolicyEnforcerTest, + DoesNotConformToCTPolicyNotEnoughUniqueEmbeddedLogs) { + VerifiedSCTList scts; + + // Operator #1 + AddSct(scts, LOG_1, OPERATOR_1, ORIGIN_EMBEDDED, TIMESTAMP_1); + // Operator #2, different logs + AddSct(scts, LOG_2, OPERATOR_2, ORIGIN_EMBEDDED, TIMESTAMP_1); + AddSct(scts, LOG_3, OPERATOR_2, ORIGIN_EMBEDDED, TIMESTAMP_1); + // Operator #3, same log + AddSct(scts, LOG_4, OPERATOR_3, ORIGIN_EMBEDDED, TIMESTAMP_1); + AddSct(scts, LOG_4, OPERATOR_3, ORIGIN_EMBEDDED, TIMESTAMP_1); + + // 5 embedded SCTs required. However, only 4 are from distinct logs. + CheckCompliance(scts, DEFAULT_MONTHS, NO_OPERATORS, + CTPolicyCompliance::NotEnoughScts); +} + +TEST_F(CTPolicyEnforcerTest, + ConformsToPolicyExactNumberOfSCTsForValidityPeriod) { + // Test multiple validity periods. + const struct TestData { + size_t certLifetimeInCalendarMonths; + size_t sctsRequired; + } kTestData[] = {{3, 2}, {12 + 2, 2}, {12 + 3, 3}, + {2 * 12 + 2, 3}, {2 * 12 + 3, 4}, {3 * 12 + 2, 4}, + {3 * 12 + 4, 5}}; + + for (size_t i = 0; i < MOZILLA_CT_ARRAY_LENGTH(kTestData); ++i) { + SCOPED_TRACE(i); + + size_t months = kTestData[i].certLifetimeInCalendarMonths; + size_t sctsRequired = kTestData[i].sctsRequired; + + // Less SCTs than required is not enough. + for (size_t sctsAvailable = 0; sctsAvailable < sctsRequired; + ++sctsAvailable) { + VerifiedSCTList scts; + AddMultipleScts(scts, sctsAvailable, 1, ORIGIN_EMBEDDED, TIMESTAMP_1); + + CTPolicyCompliance compliance; + mPolicyEnforcer.CheckCompliance(scts, months, NO_OPERATORS, compliance); + EXPECT_EQ(CTPolicyCompliance::NotEnoughScts, compliance) + << "i=" << i << " sctsRequired=" << sctsRequired + << " sctsAvailable=" << sctsAvailable; + } + + // Add exactly the required number of SCTs (from 2 operators). + VerifiedSCTList scts; + AddMultipleScts(scts, sctsRequired, 2, ORIGIN_EMBEDDED, TIMESTAMP_1); + + CTPolicyCompliance compliance; + mPolicyEnforcer.CheckCompliance(scts, months, NO_OPERATORS, compliance); + EXPECT_EQ(CTPolicyCompliance::Compliant, compliance) << "i=" << i; + } +} + +TEST_F(CTPolicyEnforcerTest, TestEdgeCasesOfGetCertLifetimeInFullMonths) { + const struct TestData { + uint64_t notBefore; + uint64_t notAfter; + size_t expectedMonths; + } kTestData[] = { + { // 1 second less than 1 month + 1424863500000000, // Date.parse("2015-02-25T11:25:00Z") * 1000 + 1427196299000000, // Date.parse("2015-03-24T11:24:59Z") * 1000 + 0}, + { // exactly 1 month + 1424863500000000, // Date.parse("2015-02-25T11:25:00Z") * 1000 + 1427282700000000, // Date.parse("2015-03-25T11:25:00Z") * 1000 + 1}, + { // 1 year, 1 month + 1427282700000000, // Date.parse("2015-03-25T11:25:00Z") * 1000 + 1461583500000000, // Date.parse("2016-04-25T11:25:00Z") * 1000 + 13}, + {// 1 year, 1 month, first day of notBefore month, last of notAfter + 1425209100000000, // Date.parse("2015-03-01T11:25:00Z") * 1000 + 1462015500000000, // Date.parse("2016-04-30T11:25:00Z") * 1000 + 13}, + {// 1 year, adjacent months, last day of notBefore month, first of + // notAfter + 1427801100000000, // Date.parse("2015-03-31T11:25:00Z") * 1000 + 1459509900000000, // Date.parse("2016-04-01T11:25:00Z") * 1000 + 12}}; + + for (size_t i = 0; i < MOZILLA_CT_ARRAY_LENGTH(kTestData); ++i) { + SCOPED_TRACE(i); + + size_t months; + ASSERT_EQ(Success, + GetCertLifetimeInFullMonths(mozilla::pkix::TimeFromEpochInSeconds( + kTestData[i].notBefore / 1000000), + mozilla::pkix::TimeFromEpochInSeconds( + kTestData[i].notAfter / 1000000), + months)) + << "i=" << i; + EXPECT_EQ(kTestData[i].expectedMonths, months) << "i=" << i; + } +} + +} // namespace ct +} // namespace mozilla diff --git a/security/ct/tests/gtest/CTSerializationTest.cpp b/security/ct/tests/gtest/CTSerializationTest.cpp new file mode 100644 index 0000000000..983759cf71 --- /dev/null +++ b/security/ct/tests/gtest/CTSerializationTest.cpp @@ -0,0 +1,216 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include "CTSerialization.h" +#include "CTTestUtils.h" +#include "gtest/gtest.h" + +namespace mozilla { +namespace ct { + +using namespace pkix; + +class CTSerializationTest : public ::testing::Test { + public: + void SetUp() override { + mTestDigitallySigned = GetTestDigitallySigned(); + mTestSignatureData = GetTestDigitallySignedData(); + } + + protected: + Buffer mTestDigitallySigned; + Buffer mTestSignatureData; +}; + +TEST_F(CTSerializationTest, DecodesDigitallySigned) { + Input digitallySigned = InputForBuffer(mTestDigitallySigned); + Reader digitallySignedReader(digitallySigned); + + DigitallySigned parsed; + ASSERT_EQ(Success, DecodeDigitallySigned(digitallySignedReader, parsed)); + EXPECT_TRUE(digitallySignedReader.AtEnd()); + + EXPECT_EQ(DigitallySigned::HashAlgorithm::SHA256, parsed.hashAlgorithm); + EXPECT_EQ(DigitallySigned::SignatureAlgorithm::ECDSA, + parsed.signatureAlgorithm); + EXPECT_EQ(mTestSignatureData, parsed.signatureData); +} + +TEST_F(CTSerializationTest, FailsToDecodePartialDigitallySigned) { + Input partial; + ASSERT_EQ(Success, partial.Init(mTestDigitallySigned.data(), + mTestDigitallySigned.size() - 5)); + Reader partialReader(partial); + + DigitallySigned parsed; + + EXPECT_NE(Success, DecodeDigitallySigned(partialReader, parsed)); +} + +TEST_F(CTSerializationTest, EncodesDigitallySigned) { + DigitallySigned digitallySigned; + digitallySigned.hashAlgorithm = DigitallySigned::HashAlgorithm::SHA256; + digitallySigned.signatureAlgorithm = + DigitallySigned::SignatureAlgorithm::ECDSA; + digitallySigned.signatureData = mTestSignatureData; + + Buffer encoded; + + ASSERT_EQ(Success, EncodeDigitallySigned(digitallySigned, encoded)); + EXPECT_EQ(mTestDigitallySigned, encoded); +} + +TEST_F(CTSerializationTest, EncodesLogEntryForX509Cert) { + LogEntry entry; + GetX509CertLogEntry(entry); + + Buffer encoded; + ASSERT_EQ(Success, EncodeLogEntry(entry, encoded)); + EXPECT_EQ((718U + 5U), encoded.size()); + // First two bytes are log entry type. Next, length: + // Length is 718 which is 512 + 206, which is { 0, ..., 2, 206 }. + Buffer expectedPrefix = {0, 0, 0, 2, 206}; + Buffer encodedPrefix; + encodedPrefix.assign(encoded.begin(), encoded.begin() + 5); + EXPECT_EQ(expectedPrefix, encodedPrefix); +} + +TEST_F(CTSerializationTest, EncodesLogEntryForPrecert) { + LogEntry entry; + GetPrecertLogEntry(entry); + + Buffer encoded; + ASSERT_EQ(Success, EncodeLogEntry(entry, encoded)); + // log entry type + issuer key + length + tbsCertificate + EXPECT_EQ((2U + 32U + 3U + entry.tbsCertificate.size()), encoded.size()); + + // First two bytes are log entry type. + Buffer expectedPrefix = {0, 1}; + Buffer encodedPrefix; + encodedPrefix.assign(encoded.begin(), encoded.begin() + 2); + EXPECT_EQ(expectedPrefix, encodedPrefix); + + // Next is the issuer key (32 bytes). + Buffer encodedKeyHash; + encodedKeyHash.assign(encoded.begin() + 2, encoded.begin() + 2 + 32); + EXPECT_EQ(GetDefaultIssuerKeyHash(), encodedKeyHash); +} + +TEST_F(CTSerializationTest, EncodesV1SCTSignedData) { + uint64_t timestamp = UINT64_C(0x139fe353cf5); + const uint8_t DUMMY_BYTES[] = {0x61, 0x62, 0x63}; // abc + Input dummyEntry(DUMMY_BYTES); + Input emptyExtensions; + Buffer encoded; + ASSERT_EQ(Success, EncodeV1SCTSignedData(timestamp, dummyEntry, + emptyExtensions, encoded)); + EXPECT_EQ((size_t)15, encoded.size()); + + Buffer expectedBuffer = { + 0x00, // version + 0x00, // signature type + 0x00, 0x00, 0x01, 0x39, 0xFE, 0x35, 0x3C, 0xF5, // timestamp + 0x61, 0x62, 0x63, // log signature + 0x00, 0x00 // extensions (empty) + }; + EXPECT_EQ(expectedBuffer, encoded); +} + +TEST_F(CTSerializationTest, DecodesSCTList) { + // Two items in the list: "abc", "def" + const uint8_t ENCODED[] = {0x00, 0x0a, 0x00, 0x03, 0x61, 0x62, + 0x63, 0x00, 0x03, 0x64, 0x65, 0x66}; + const uint8_t DECODED_1[] = {0x61, 0x62, 0x63}; + const uint8_t DECODED_2[] = {0x64, 0x65, 0x66}; + + Reader listReader; + ASSERT_EQ(Success, DecodeSCTList(Input(ENCODED), listReader)); + + Input decoded1; + ASSERT_EQ(Success, ReadSCTListItem(listReader, decoded1)); + + Input decoded2; + ASSERT_EQ(Success, ReadSCTListItem(listReader, decoded2)); + + EXPECT_TRUE(listReader.AtEnd()); + EXPECT_TRUE(InputsAreEqual(decoded1, Input(DECODED_1))); + EXPECT_TRUE(InputsAreEqual(decoded2, Input(DECODED_2))); +} + +TEST_F(CTSerializationTest, FailsDecodingInvalidSCTList) { + // A list with one item that's too short (the second one) + const uint8_t ENCODED[] = {0x00, 0x0a, 0x00, 0x03, 0x61, 0x62, + 0x63, 0x00, 0x05, 0x64, 0x65, 0x66}; + + Reader listReader; + ASSERT_EQ(Success, DecodeSCTList(Input(ENCODED), listReader)); + Input decoded1; + EXPECT_EQ(Success, ReadSCTListItem(listReader, decoded1)); + Input decoded2; + EXPECT_NE(Success, ReadSCTListItem(listReader, decoded2)); +} + +TEST_F(CTSerializationTest, EncodesSCTList) { + const uint8_t SCT_1[] = {0x61, 0x62, 0x63}; + const uint8_t SCT_2[] = {0x64, 0x65, 0x66}; + + std::vector<Input> list; + list.push_back(Input(SCT_1)); + list.push_back(Input(SCT_2)); + + Buffer encodedList; + ASSERT_EQ(Success, EncodeSCTList(list, encodedList)); + + Reader listReader; + ASSERT_EQ(Success, DecodeSCTList(InputForBuffer(encodedList), listReader)); + + Input decoded1; + ASSERT_EQ(Success, ReadSCTListItem(listReader, decoded1)); + EXPECT_TRUE(InputsAreEqual(decoded1, Input(SCT_1))); + + Input decoded2; + ASSERT_EQ(Success, ReadSCTListItem(listReader, decoded2)); + EXPECT_TRUE(InputsAreEqual(decoded2, Input(SCT_2))); + + EXPECT_TRUE(listReader.AtEnd()); +} + +TEST_F(CTSerializationTest, DecodesSignedCertificateTimestamp) { + Buffer encodedSctBuffer = GetTestSignedCertificateTimestamp(); + 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_TRUE(sct.extensions.empty()); +} + +TEST_F(CTSerializationTest, FailsDecodingInvalidSignedCertificateTimestamp) { + SignedCertificateTimestamp sct; + + // Invalid version + const uint8_t INVALID_VERSION_BYTES[] = {0x02, 0x00}; + Input invalidVersionSctInput(INVALID_VERSION_BYTES); + Reader invalidVersionSctReader(invalidVersionSctInput); + EXPECT_EQ(pkix::Result::ERROR_BAD_DER, + DecodeSignedCertificateTimestamp(invalidVersionSctReader, sct)); + + // Valid version, invalid length (missing data) + const uint8_t INVALID_LENGTH_BYTES[] = {0x00, 0x0a, 0x0b, 0x0c}; + Input invalidLengthSctInput(INVALID_LENGTH_BYTES); + Reader invalidLengthSctReader(invalidLengthSctInput); + EXPECT_EQ(pkix::Result::ERROR_BAD_DER, + DecodeSignedCertificateTimestamp(invalidLengthSctReader, sct)); +} + +} // namespace ct +} // namespace mozilla diff --git a/security/ct/tests/gtest/CTTestUtils.cpp b/security/ct/tests/gtest/CTTestUtils.cpp new file mode 100644 index 0000000000..05f588b77f --- /dev/null +++ b/security/ct/tests/gtest/CTTestUtils.cpp @@ -0,0 +1,822 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include "CTTestUtils.h" + +#include <stdint.h> +#include <iomanip> + +#include "BTTypes.h" +#include "CTSerialization.h" +#include "gtest/gtest.h" +#include "mozpkix/Input.h" +#include "mozpkix/pkix.h" +#include "mozpkix/pkixnss.h" +#include "mozpkix/pkixtypes.h" +#include "mozpkix/Result.h" +#include "mozpkix/pkixcheck.h" +#include "mozpkix/pkixutil.h" +#include "SignedCertificateTimestamp.h" + +namespace mozilla { +namespace ct { + +using namespace mozilla::pkix; + +// The following test vectors are from the CT test data repository at +// https://github.com/google/certificate-transparency/tree/master/test/testdata + +// test-cert.pem +const char kDefaultDerCert[] = + "308202ca30820233a003020102020106300d06092a864886f70d01010505003055310b3009" + "06035504061302474231243022060355040a131b4365727469666963617465205472616e73" + "706172656e6379204341310e300c0603550408130557616c65733110300e06035504071307" + "4572772057656e301e170d3132303630313030303030305a170d3232303630313030303030" + "305a3052310b30090603550406130247423121301f060355040a1318436572746966696361" + "7465205472616e73706172656e6379310e300c0603550408130557616c65733110300e0603" + "55040713074572772057656e30819f300d06092a864886f70d010101050003818d00308189" + "02818100b1fa37936111f8792da2081c3fe41925008531dc7f2c657bd9e1de4704160b4c9f" + "19d54ada4470404c1c51341b8f1f7538dddd28d9aca48369fc5646ddcc7617f8168aae5b41" + "d43331fca2dadfc804d57208949061f9eef902ca47ce88c644e000f06eeeccabdc9dd2f68a" + "22ccb09dc76e0dbc73527765b1a37a8c676253dcc10203010001a381ac3081a9301d060355" + "1d0e041604146a0d982a3b62c44b6d2ef4e9bb7a01aa9cb798e2307d0603551d2304763074" + "80145f9d880dc873e654d4f80dd8e6b0c124b447c355a159a4573055310b30090603550406" + "1302474231243022060355040a131b4365727469666963617465205472616e73706172656e" + "6379204341310e300c0603550408130557616c65733110300e060355040713074572772057" + "656e82010030090603551d1304023000300d06092a864886f70d010105050003818100171c" + "d84aac414a9a030f22aac8f688b081b2709b848b4e5511406cd707fed028597a9faefc2eee" + "2978d633aaac14ed3235197da87e0f71b8875f1ac9e78b281749ddedd007e3ecf50645f8cb" + "f667256cd6a1647b5e13203bb8582de7d6696f656d1c60b95f456b7fcf338571908f1c6972" + "7d24c4fccd249295795814d1dac0e6"; + +// key hash of test-cert.pem's issuer (ca-cert.pem) +const char kDefaultIssuerKeyHash[] = + "02adddca08b8bf9861f035940c940156d8350fdff899a6239c6bd77255b8f8fc"; + +const char kDefaultDerTbsCert[] = + "30820233a003020102020107300d06092a864886f70d01010505003055310b300906035504" + "061302474231243022060355040a131b4365727469666963617465205472616e7370617265" + "6e6379204341310e300c0603550408130557616c65733110300e0603550407130745727720" + "57656e301e170d3132303630313030303030305a170d3232303630313030303030305a3052" + "310b30090603550406130247423121301f060355040a131843657274696669636174652054" + "72616e73706172656e6379310e300c0603550408130557616c65733110300e060355040713" + "074572772057656e30819f300d06092a864886f70d010101050003818d0030818902818100" + "beef98e7c26877ae385f75325a0c1d329bedf18faaf4d796bf047eb7e1ce15c95ba2f80ee4" + "58bd7db86f8a4b252191a79bd700c38e9c0389b45cd4dc9a120ab21e0cb41cd0e72805a410" + "cd9c5bdb5d4927726daf1710f60187377ea25b1a1e39eed0b88119dc154dc68f7da8e30caf" + "158a33e6c9509f4a05b01409ff5dd87eb50203010001a381ac3081a9301d0603551d0e0416" + "04142031541af25c05ffd8658b6843794f5e9036f7b4307d0603551d230476307480145f9d" + "880dc873e654d4f80dd8e6b0c124b447c355a159a4573055310b3009060355040613024742" + "31243022060355040a131b4365727469666963617465205472616e73706172656e63792043" + "41310e300c0603550408130557616c65733110300e060355040713074572772057656e8201" + "0030090603551d1304023000"; + +// DigitallySigned of test-cert.proof +const char kTestDigitallySigned[] = + "0403004730450220606e10ae5c2d5a1b0aed49dc4937f48de71a4e9784e9c208dfbfe9ef53" + "6cf7f2022100beb29c72d7d06d61d06bdb38a069469aa86fe12e18bb7cc45689a2c0187ef5" + "a5"; + +// test-cert.proof +const char kTestSignedCertificateTimestamp[] = + "00df1c2ec11500945247a96168325ddc5c7959e8f7c6d388fc002e0bbd3f74d7640000013d" + "db27ded900000403004730450220606e10ae5c2d5a1b0aed49dc4937f48de71a4e9784e9c2" + "08dfbfe9ef536cf7f2022100beb29c72d7d06d61d06bdb38a069469aa86fe12e18bb7cc456" + "89a2c0187ef5a5"; + +// ct-server-key-public.pem +const char kEcP256PublicKey[] = + "3059301306072a8648ce3d020106082a8648ce3d0301070342000499783cb14533c0161a5a" + "b45bf95d08a29cd0ea8dd4c84274e2be59ad15c676960cf0afa1074a57ac644b23479e5b3f" + "b7b245eb4b420ef370210371a944beaceb"; + +// key id (sha256) of ct-server-key-public.pem +const char kTestKeyId[] = + "df1c2ec11500945247a96168325ddc5c7959e8f7c6d388fc002e0bbd3f74d764"; + +// signature field of DigitallySigned from test-cert.proof +const char kTestSCTSignatureData[] = + "30450220606e10ae5c2d5a1b0aed49dc4937f48de71a4e9784e9c208dfbfe9ef536cf7f202" + "2100beb29c72d7d06d61d06bdb38a069469aa86fe12e18bb7cc45689a2c0187ef5a5"; + +// signature field of DigitallySigned from test-embedded-pre-cert.proof +const char kTestSCTPrecertSignatureData[] = + "30450220482f6751af35dba65436be1fd6640f3dbf9a41429495924530288fa3e5e23e0602" + "2100e4edc0db3ac572b1e2f5e8ab6a680653987dcf41027dfeffa105519d89edbf08"; + +// test-embedded-cert.pem +const char kTestEmbeddedCertData[] = + "30820359308202c2a003020102020107300d06092a864886f70d01010505" + "003055310b300906035504061302474231243022060355040a131b436572" + "7469666963617465205472616e73706172656e6379204341310e300c0603" + "550408130557616c65733110300e060355040713074572772057656e301e" + "170d3132303630313030303030305a170d3232303630313030303030305a" + "3052310b30090603550406130247423121301f060355040a131843657274" + "69666963617465205472616e73706172656e6379310e300c060355040813" + "0557616c65733110300e060355040713074572772057656e30819f300d06" + "092a864886f70d010101050003818d0030818902818100beef98e7c26877" + "ae385f75325a0c1d329bedf18faaf4d796bf047eb7e1ce15c95ba2f80ee4" + "58bd7db86f8a4b252191a79bd700c38e9c0389b45cd4dc9a120ab21e0cb4" + "1cd0e72805a410cd9c5bdb5d4927726daf1710f60187377ea25b1a1e39ee" + "d0b88119dc154dc68f7da8e30caf158a33e6c9509f4a05b01409ff5dd87e" + "b50203010001a382013a30820136301d0603551d0e041604142031541af2" + "5c05ffd8658b6843794f5e9036f7b4307d0603551d230476307480145f9d" + "880dc873e654d4f80dd8e6b0c124b447c355a159a4573055310b30090603" + "5504061302474231243022060355040a131b436572746966696361746520" + "5472616e73706172656e6379204341310e300c0603550408130557616c65" + "733110300e060355040713074572772057656e82010030090603551d1304" + "02300030818a060a2b06010401d679020402047c047a0078007600df1c2e" + "c11500945247a96168325ddc5c7959e8f7c6d388fc002e0bbd3f74d76400" + "00013ddb27df9300000403004730450220482f6751af35dba65436be1fd6" + "640f3dbf9a41429495924530288fa3e5e23e06022100e4edc0db3ac572b1" + "e2f5e8ab6a680653987dcf41027dfeffa105519d89edbf08300d06092a86" + "4886f70d0101050500038181008a0c4bef099d479279afa0a28e689f91e1" + "c4421be2d269a2ea6ca4e8215ddeddca1504a11e7c87c4b77e80f0e97903" + "5268f27ca20e166804ae556f316981f96a394ab7abfd3e255ac0044513fe" + "76570c6795abe4703133d303f89f3afa6bbcfc517319dfd95b934241211f" + "634035c3d078307a68c6075a2e20c89f36b8910ca0"; + +const char kTestTbsCertData[] = + "30820233a003020102020107300d06092a864886f70d0101050500305531" + "0b300906035504061302474231243022060355040a131b43657274696669" + "63617465205472616e73706172656e6379204341310e300c060355040813" + "0557616c65733110300e060355040713074572772057656e301e170d3132" + "303630313030303030305a170d3232303630313030303030305a3052310b" + "30090603550406130247423121301f060355040a13184365727469666963" + "617465205472616e73706172656e6379310e300c0603550408130557616c" + "65733110300e060355040713074572772057656e30819f300d06092a8648" + "86f70d010101050003818d0030818902818100beef98e7c26877ae385f75" + "325a0c1d329bedf18faaf4d796bf047eb7e1ce15c95ba2f80ee458bd7db8" + "6f8a4b252191a79bd700c38e9c0389b45cd4dc9a120ab21e0cb41cd0e728" + "05a410cd9c5bdb5d4927726daf1710f60187377ea25b1a1e39eed0b88119" + "dc154dc68f7da8e30caf158a33e6c9509f4a05b01409ff5dd87eb5020301" + "0001a381ac3081a9301d0603551d0e041604142031541af25c05ffd8658b" + "6843794f5e9036f7b4307d0603551d230476307480145f9d880dc873e654" + "d4f80dd8e6b0c124b447c355a159a4573055310b30090603550406130247" + "4231243022060355040a131b4365727469666963617465205472616e7370" + "6172656e6379204341310e300c0603550408130557616c65733110300e06" + "0355040713074572772057656e82010030090603551d1304023000"; + +// test-embedded-with-preca-cert.pem +const char kTestEmbeddedWithPreCaCertData[] = + "30820359308202c2a003020102020108300d06092a864886f70d01010505" + "003055310b300906035504061302474231243022060355040a131b436572" + "7469666963617465205472616e73706172656e6379204341310e300c0603" + "550408130557616c65733110300e060355040713074572772057656e301e" + "170d3132303630313030303030305a170d3232303630313030303030305a" + "3052310b30090603550406130247423121301f060355040a131843657274" + "69666963617465205472616e73706172656e6379310e300c060355040813" + "0557616c65733110300e060355040713074572772057656e30819f300d06" + "092a864886f70d010101050003818d0030818902818100afaeeacac51ab7" + "cebdf9eacae7dd175295e193955a17989aef8d97ab7cdff7761093c0b823" + "d2a4e3a51a17b86f28162b66a2538935ebecdc1036233da2dd6531b0c63b" + "cc68761ebdc854037b77399246b870a7b72b14c9b1667de09a9640ed9f3f" + "3c725d950b4d26559869fe7f1e919a66eb76d35c0117c6bcd0d8cfd21028" + "b10203010001a382013a30820136301d0603551d0e04160414612c64efac" + "79b728397c9d93e6df86465fa76a88307d0603551d230476307480145f9d" + "880dc873e654d4f80dd8e6b0c124b447c355a159a4573055310b30090603" + "5504061302474231243022060355040a131b436572746966696361746520" + "5472616e73706172656e6379204341310e300c0603550408130557616c65" + "733110300e060355040713074572772057656e82010030090603551d1304" + "02300030818a060a2b06010401d679020402047c047a0078007600df1c2e" + "c11500945247a96168325ddc5c7959e8f7c6d388fc002e0bbd3f74d76400" + "00013ddb27e05b000004030047304502207aa79604c47480f3727b084f90" + "b3989f79091885e00484431a2a297cbf3a355c022100b49fd8120b0d644c" + "d7e75269b4da6317a9356cb950224fc11cc296b2e39b2386300d06092a86" + "4886f70d010105050003818100a3a86c41ad0088a25aedc4e7b529a2ddbf" + "9e187ffb362157e9302d961b73b43cba0ae1e230d9e45049b7e8c924792e" + "bbe7d175baa87b170dfad8ee788984599d05257994084e2e0e796fca5836" + "881c3e053553e06ab230f919089b914e4a8e2da45f8a87f2c81a25a61f04" + "fe1cace60155653827d41fad9f0658f287d058192c"; + +// ca-cert.pem +const char kCaCertData[] = + "308202d030820239a003020102020100300d06092a864886f70d01010505" + "003055310b300906035504061302474231243022060355040a131b436572" + "7469666963617465205472616e73706172656e6379204341310e300c0603" + "550408130557616c65733110300e060355040713074572772057656e301e" + "170d3132303630313030303030305a170d3232303630313030303030305a" + "3055310b300906035504061302474231243022060355040a131b43657274" + "69666963617465205472616e73706172656e6379204341310e300c060355" + "0408130557616c65733110300e060355040713074572772057656e30819f" + "300d06092a864886f70d010101050003818d0030818902818100d58a6853" + "6210a27119936e778321181c2a4013c6d07b8c76eb9157d3d0fb4b3b516e" + "cecbd1c98d91c52f743fab635d55099cd13abaf31ae541442451a74c7816" + "f2243cf848cf2831cce67ba04a5a23819f3cba37e624d9c3bdb299b839dd" + "fe2631d2cb3a84fc7bb2b5c52fcfc14fff406f5cd44669cbb2f7cfdf86fb" + "6ab9d1b10203010001a381af3081ac301d0603551d0e041604145f9d880d" + "c873e654d4f80dd8e6b0c124b447c355307d0603551d230476307480145f" + "9d880dc873e654d4f80dd8e6b0c124b447c355a159a4573055310b300906" + "035504061302474231243022060355040a131b4365727469666963617465" + "205472616e73706172656e6379204341310e300c0603550408130557616c" + "65733110300e060355040713074572772057656e820100300c0603551d13" + "040530030101ff300d06092a864886f70d0101050500038181000608cc4a" + "6d64f2205e146c04b276f92b0efa94a5daf23afc3806606d3990d0a1ea23" + "3d40295769463b046661e7fa1d179915209aea2e0a775176411227d7c003" + "07c7470e61584fd7334224727f51d690bc47a9df354db0f6eb25955de189" + "3c4dd5202b24a2f3e440d274b54e1bd376269ca96289b76ecaa41090e14f" + "3b0a942e"; + +// intermediate-cert.pem +const char kIntermediateCertData[] = + "308202dd30820246a003020102020109300d06092a864886f70d01010505" + "003055310b300906035504061302474231243022060355040a131b436572" + "7469666963617465205472616e73706172656e6379204341310e300c0603" + "550408130557616c65733110300e060355040713074572772057656e301e" + "170d3132303630313030303030305a170d3232303630313030303030305a" + "3062310b30090603550406130247423131302f060355040a132843657274" + "69666963617465205472616e73706172656e637920496e7465726d656469" + "617465204341310e300c0603550408130557616c65733110300e06035504" + "0713074572772057656e30819f300d06092a864886f70d01010105000381" + "8d0030818902818100d76a678d116f522e55ff821c90642508b7074b14d7" + "71159064f7927efdedb87135a1365ee7de18cbd5ce865f860c78f433b4d0" + "d3d3407702e7a3ef542b1dfe9bbaa7cdf94dc5975fc729f86f105f381b24" + "3535cf9c800f5ca780c1d3c84400ee65d16ee9cf52db8adffe50f5c49335" + "0b2190bf50d5bc36f3cac5a8daae92cd8b0203010001a381af3081ac301d" + "0603551d0e04160414965508050278479e8773764131bc143a47e229ab30" + "7d0603551d230476307480145f9d880dc873e654d4f80dd8e6b0c124b447" + "c355a159a4573055310b300906035504061302474231243022060355040a" + "131b4365727469666963617465205472616e73706172656e637920434131" + "0e300c0603550408130557616c65733110300e0603550407130745727720" + "57656e820100300c0603551d13040530030101ff300d06092a864886f70d" + "0101050500038181002206dab1c66b71dce095c3f6aa2ef72cf7761be7ab" + "d7fc39c31a4cfe1bd96d6734ca82f22dde5a0c8bbbdd825d7b6f3e7612ad" + "8db300a7e21169886023262284c3aa5d2191efda10bf9235d37b3a2a340d" + "59419b94a48566f3fac3cd8b53d5a4e98270ead297b07210f9ce4a2138b1" + "8811143b93fa4e7a87dd37e1385f2c2908"; + +// test-embedded-with-intermediate-cert.pem +const char kTestEmbeddedWithIntermediateCertData[] = + "30820366308202cfa003020102020102300d06092a864886f70d01010505" + "003062310b30090603550406130247423131302f060355040a1328436572" + "7469666963617465205472616e73706172656e637920496e7465726d6564" + "69617465204341310e300c0603550408130557616c65733110300e060355" + "040713074572772057656e301e170d3132303630313030303030305a170d" + "3232303630313030303030305a3052310b30090603550406130247423121" + "301f060355040a13184365727469666963617465205472616e7370617265" + "6e6379310e300c0603550408130557616c65733110300e06035504071307" + "4572772057656e30819f300d06092a864886f70d010101050003818d0030" + "818902818100bb272b26e5deb5459d4acca027e8f12a4d839ac3730a6a10" + "9ff7e25498ddbd3f1895d08ba41f8de34967a3a086ce13a90dd5adbb5418" + "4bdc08e1ac7826adb8dc9c717bfd7da5b41b4db1736e00f1dac3cec9819c" + "cb1a28ba120b020a820e940dd61f95b5432a4bc05d0818f18ce2154eb38d" + "2fa7d22d72b976e560db0c7fc77f0203010001a382013a30820136301d06" + "03551d0e04160414b1b148e658e703f5f7f3105f20b3c384d7eff1bf307d" + "0603551d23047630748014965508050278479e8773764131bc143a47e229" + "aba159a4573055310b300906035504061302474231243022060355040a13" + "1b4365727469666963617465205472616e73706172656e6379204341310e" + "300c0603550408130557616c65733110300e060355040713074572772057" + "656e82010930090603551d130402300030818a060a2b06010401d6790204" + "02047c047a0078007600df1c2ec11500945247a96168325ddc5c7959e8f7" + "c6d388fc002e0bbd3f74d7640000013ddb27e2a400000403004730450221" + "00a6d34517f3392d9ec5d257adf1c597dc45bd4cd3b73856c616a9fb99e5" + "ae75a802205e26c8d1c7e222fe8cda29baeb04a834ee97d34fd81718f1aa" + "e0cd66f4b8a93f300d06092a864886f70d0101050500038181000f95a5b4" + "e128a914b1e88be8b32964221b58f4558433d020a8e246cca65a40bcbf5f" + "2d48933ebc99be6927ca756472fb0bdc7f505f41f462f2bc19d0b299c990" + "918df8820f3d31db37979e8bad563b17f00ae67b0f8731c106c943a73bf5" + "36af168afe21ef4adfcae19a3cc074899992bf506bc5ce1decaaf07ffeeb" + "c805c039"; + +// test-embedded-with-intermediate-preca-cert.pem +const char kTestEmbeddedWithIntermediatePreCaCertData[] = + "30820366308202cfa003020102020103300d06092a864886f70d01010505" + "003062310b30090603550406130247423131302f060355040a1328436572" + "7469666963617465205472616e73706172656e637920496e7465726d6564" + "69617465204341310e300c0603550408130557616c65733110300e060355" + "040713074572772057656e301e170d3132303630313030303030305a170d" + "3232303630313030303030305a3052310b30090603550406130247423121" + "301f060355040a13184365727469666963617465205472616e7370617265" + "6e6379310e300c0603550408130557616c65733110300e06035504071307" + "4572772057656e30819f300d06092a864886f70d010101050003818d0030" + "818902818100d4497056cdfc65e1342cc3df6e654b8af0104702acd2275c" + "7d3fb1fc438a89b212110d6419bcc13ae47d64bba241e6706b9ed627f8b3" + "4a0d7dff1c44b96287c54bea9d10dc017bceb64f7b6aff3c35a474afec40" + "38ab3640b0cd1fb0582ec03b179a2776c8c435d14ab4882d59d7b724fa37" + "7ca6db08392173f9c6056b3abadf0203010001a382013a30820136301d06" + "03551d0e0416041432da5518d87f1d26ea2767973c0bef286e786a4a307d" + "0603551d23047630748014965508050278479e8773764131bc143a47e229" + "aba159a4573055310b300906035504061302474231243022060355040a13" + "1b4365727469666963617465205472616e73706172656e6379204341310e" + "300c0603550408130557616c65733110300e060355040713074572772057" + "656e82010930090603551d130402300030818a060a2b06010401d6790204" + "02047c047a0078007600df1c2ec11500945247a96168325ddc5c7959e8f7" + "c6d388fc002e0bbd3f74d7640000013ddb27e3be00000403004730450221" + "00d9f61a07fee021e3159f3ca2f570d833ff01374b2096cba5658c5e16fb" + "43eb3002200b76fe475138d8cf76833831304dabf043eb1213c96e13ff4f" + "a37f7cd3c8dc1f300d06092a864886f70d01010505000381810088ee4e9e" + "5eed6b112cc764b151ed929400e9406789c15fbbcfcdab2f10b400234139" + "e6ce65c1e51b47bf7c8950f80bccd57168567954ed35b0ce9346065a5eae" + "5bf95d41da8e27cee9eeac688f4bd343f9c2888327abd8b9f68dcb1e3050" + "041d31bda8e2dd6d39b3664de5ce0870f5fc7e6a00d6ed00528458d953d2" + "37586d73"; + +// Given the ordered set of data [ 0x00, 0x01, 0x02, deadbeef ], +// the 'inclusion proof' of the leaf of index '2' (for '0x02') is created from +// the Merkle Tree generated for that set of data. +// A Merkle inclusion proof for a leaf in a Merkle Tree is the shortest list +// of additional nodes in the Merkle Tree required to compute the Merkle Tree +// Hash (also called 'Merkle Tree head') for that tree. +// This follows the structure defined in RFC 6962-bis. +// +// https://tools.ietf.org/html/draft-ietf-trans-rfc6962-bis-24#section-2.1 + +const char kTestInclusionProof[] = + "020100" // logId + "0000000000000004" // tree size + "0000000000000002" // leaf index + "0042" // inclusion path length + "2048c90c8ae24688d6bef5d48a30c2cc8b6754335a8db21793cc0a8e3bed32172" + "9" // node + // hash + // 0 + "20a20bf9a7cc2dc8a08f5f415a71b19f6ac427bab54d24eec868b5d3103449953" + "a"; // node + // hash + // 1 + +const char kTestNodeHash0[] = + "48c90c8ae24688d6bef5d48a30c2cc8b6754335a8db21793cc0a8e3bed321729"; + +const char kTestNodeHash1[] = + "a20bf9a7cc2dc8a08f5f415a71b19f6ac427bab54d24eec868b5d3103449953a"; + +const char kTestInclusionProofUnexpectedData[] = "12345678"; + +const char kTestInclusionProofInvalidHashSize[] = + "020100" // logId + "0000000000000004" // treesize + "0000000000000002" // leafindex + "0042" // inclusion path length + "3048c90c8ae24688d6bef5d48a30c2cc8b6754335a8db21793cc0a8e3bed32172" + "9" // invalid hash size + "20a20bf9a7cc2dc8a08f5f415a71b19f6ac427bab54d24eec868b5d3103449953" + "a"; // node hash 1 + +const char kTestInclusionProofInvalidHash[] = + "020100" // logId + "0000000000000004" // treesize + "0000000000000002" // leafindex + "0042" // inclusion path length + "2048c90c8ae24688d6bef5d48a30c2cc8b6754335a8db21793cc0a8e3bed32172" + "9" // node + // hash + // 0 + "20a20bf9a7cc2dc8a08f5f415a71b19f6ac427"; // truncated node hash 1 + +const char kTestInclusionProofMissingLogId[] = + "0000000000000004" // treesize + "0000000000000002" // leafindex + "0042" + "2048c90c8ae24688d6bef5d48a30c2cc8b6754335a8db21793cc0a8e3bed32172" + "9" // node + // hash + // 0 + "20a20bf9a7cc2dc8a08f5f415a71b19f6ac427bab54d24eec868b5d3103449953" + "a"; // node + // hash + // 1 + +const char kTestInclusionProofNullPathLength[] = + "020100" + "0000000000000004" // treesize + "0000000000000002" // leafindex + "0000" + "2048c90c8ae24688d6bef5d48a30c2cc8b6754335a8db21793cc0a8e3bed32172" + "9" // node + // hash + // 0 + "20a20bf9a7cc2dc8a08f5f415a71b19f6ac427bab54d24eec868b5d3103449953" + "a"; // node + // hash + // 1 + +const char kTestInclusionProofPathLengthTooSmall[] = + "020100" + "0000000000000004" // treesize + "0000000000000002" // leafindex + "0036" + "2048c90c8ae24688d6bef5d48a30c2cc8b6754335a8db21793cc0a8e3bed32172" + "9" // node + // hash + // 0 + "20a20bf9a7cc2dc8a08f5f415a71b19f6ac427bab54d24eec868b5d3103449953" + "a"; // node + // hash + // 1 + +const char kTestInclusionProofPathLengthTooLarge[] = + "020100" + "0000000000000004" // treesize + "0000000000000002" // leafindex + "0080" + "2048c90c8ae24688d6bef5d48a30c2cc8b6754335a8db21793cc0a8e3bed32172" + "9" // node + // hash + // 0 + "20a20bf9a7cc2dc8a08f5f415a71b19f6ac427bab54d24eec868b5d3103449953" + "a"; // node + // hash + // 1 + +const char kTestInclusionProofNullTreeSize[] = + "020100" + "0000000000000000" // treesize + "0000000000000002" // leafindex + "0042" + "2048c90c8ae24688d6bef5d48a30c2cc8b6754335a8db21793cc0a8e3bed32172" + "9" // node + // hash + // 0 + "20a20bf9a7cc2dc8a08f5f415a71b19f6ac427bab54d24eec868b5d3103449953" + "a"; // node + // hash + // 1 + +const char kTestInclusionProofLeafIndexOutOfBounds[] = + "020100" + "0000000000000004" // treesize + "0000000000000004" // leafindex + "0042" + "2048c90c8ae24688d6bef5d48a30c2cc8b6754335a8db21793cc0a8e3bed32172" + "9" // node + // hash + // 0 + "20a20bf9a7cc2dc8a08f5f415a71b19f6ac427bab54d24eec868b5d3103449953" + "a"; // node + // hash + // 1 + +const char kTestInclusionProofExtraData[] = + "020100" // logId + "0000000000000004" // tree size + "0000000000000002" // leaf index + "0042" // inclusion path length + "2048c90c8ae24688d6bef5d48a30c2cc8b6754335a8db21793cc0a8e3bed32172" + "9" // node + // hash + // 0 + "20a20bf9a7cc2dc8a08f5f415a71b19f6ac427bab54d24eec868b5d3103449953" + "a" // node + // hash + // 1 + "123456"; // extra data after the proof + +static uint8_t CharToByte(char c) { + if (c >= '0' && c <= '9') { + return c - '0'; + } + if (c >= 'a' && c <= 'f') { + return c - 'a' + 10; + } + if (c >= 'A' && c <= 'F') { + return c - 'A' + 10; + } + abort(); +} + +Buffer HexToBytes(const char* hexData) { + size_t hexLen = strlen(hexData); + if (!(hexLen > 0 && (hexLen % 2 == 0))) { + abort(); + } + size_t resultLen = hexLen / 2; + Buffer result; + result.reserve(resultLen); + for (size_t i = 0; i < resultLen; ++i) { + uint8_t hi = CharToByte(hexData[i * 2]); + uint8_t lo = CharToByte(hexData[i * 2 + 1]); + result.push_back((hi << 4) | lo); + } + return result; +} + +void GetX509CertLogEntry(LogEntry& entry) { + entry.Reset(); + entry.type = ct::LogEntry::Type::X509; + entry.leafCertificate = HexToBytes(kDefaultDerCert); +} + +Buffer GetDEREncodedX509Cert() { return HexToBytes(kDefaultDerCert); } + +void GetPrecertLogEntry(LogEntry& entry) { + entry.Reset(); + entry.type = ct::LogEntry::Type::Precert; + entry.issuerKeyHash = HexToBytes(kDefaultIssuerKeyHash); + entry.tbsCertificate = HexToBytes(kDefaultDerTbsCert); +} + +Buffer GetTestDigitallySigned() { return HexToBytes(kTestDigitallySigned); } + +Buffer GetTestDigitallySignedData() { + Buffer encoded = GetTestDigitallySigned(); + // The encoded buffer contains the signature data itself from the 4th byte. + // The first bytes are: + // 1 byte of hash algorithm + // 1 byte of signature algorithm + // 2 bytes - prefix containing length of the signature data. + Buffer result; + result.assign(encoded.begin() + 4, encoded.end()); + return result; +} + +Buffer GetTestSignedCertificateTimestamp() { + return HexToBytes(kTestSignedCertificateTimestamp); +} + +Buffer GetTestInclusionProof() { return HexToBytes(kTestInclusionProof); } + +Buffer GetTestInclusionProofUnexpectedData() { + return HexToBytes(kTestInclusionProofUnexpectedData); +} + +Buffer GetTestInclusionProofInvalidHashSize() { + return HexToBytes(kTestInclusionProofInvalidHashSize); +} + +Buffer GetTestInclusionProofInvalidHash() { + return HexToBytes(kTestInclusionProofInvalidHash); +} + +Buffer GetTestInclusionProofMissingLogId() { + return HexToBytes(kTestInclusionProofMissingLogId); +} + +Buffer GetTestInclusionProofNullPathLength() { + return HexToBytes(kTestInclusionProofNullPathLength); +} + +Buffer GetTestInclusionProofPathLengthTooSmall() { + return HexToBytes(kTestInclusionProofPathLengthTooSmall); +} + +Buffer GetTestInclusionProofPathLengthTooLarge() { + return HexToBytes(kTestInclusionProofPathLengthTooLarge); +} + +Buffer GetTestInclusionProofNullTreeSize() { + return HexToBytes(kTestInclusionProofNullTreeSize); +} + +Buffer GetTestInclusionProofLeafIndexOutOfBounds() { + return HexToBytes(kTestInclusionProofLeafIndexOutOfBounds); +} + +Buffer GetTestInclusionProofExtraData() { + return HexToBytes(kTestInclusionProofExtraData); +} + +Buffer GetTestNodeHash0() { return HexToBytes(kTestNodeHash0); } + +Buffer GetTestNodeHash1() { return HexToBytes(kTestNodeHash1); } + +Buffer GetTestPublicKey() { return HexToBytes(kEcP256PublicKey); } + +Buffer GetTestPublicKeyId() { return HexToBytes(kTestKeyId); } + +void GetX509CertSCT(SignedCertificateTimestamp& sct) { + sct.version = ct::SignedCertificateTimestamp::Version::V1; + sct.logId = HexToBytes(kTestKeyId); + // Time the log issued a SCT for this certificate, which is + // Fri Apr 5 10:04:16.089 2013 + sct.timestamp = INT64_C(1365181456089); + sct.extensions.clear(); + + sct.signature.hashAlgorithm = ct::DigitallySigned::HashAlgorithm::SHA256; + sct.signature.signatureAlgorithm = + ct::DigitallySigned::SignatureAlgorithm::ECDSA; + sct.signature.signatureData = HexToBytes(kTestSCTSignatureData); +} + +void GetPrecertSCT(SignedCertificateTimestamp& sct) { + sct.version = ct::SignedCertificateTimestamp::Version::V1; + sct.logId = HexToBytes(kTestKeyId); + // Time the log issued a SCT for this Precertificate, which is + // Fri Apr 5 10:04:16.275 2013 + sct.timestamp = INT64_C(1365181456275); + sct.extensions.clear(); + + sct.signature.hashAlgorithm = ct::DigitallySigned::HashAlgorithm::SHA256; + sct.signature.signatureAlgorithm = + ct::DigitallySigned::SignatureAlgorithm::ECDSA; + sct.signature.signatureData = HexToBytes(kTestSCTPrecertSignatureData); +} + +Buffer GetDefaultIssuerKeyHash() { return HexToBytes(kDefaultIssuerKeyHash); } + +Buffer GetDEREncodedTestEmbeddedCert() { + return HexToBytes(kTestEmbeddedCertData); +} + +Buffer GetDEREncodedTestTbsCert() { return HexToBytes(kTestTbsCertData); } + +Buffer GetDEREncodedTestEmbeddedWithPreCACert() { + return HexToBytes(kTestEmbeddedWithPreCaCertData); +} + +Buffer GetDEREncodedCACert() { return HexToBytes(kCaCertData); } + +Buffer GetDEREncodedIntermediateCert() { + return HexToBytes(kIntermediateCertData); +} + +Buffer GetDEREncodedTestEmbeddedWithIntermediateCert() { + return HexToBytes(kTestEmbeddedWithIntermediateCertData); +} + +Buffer GetDEREncodedTestEmbeddedWithIntermediatePreCACert() { + return HexToBytes(kTestEmbeddedWithIntermediatePreCaCertData); +} + +Buffer ExtractCertSPKI(Input cert) { + BackCert backCert(cert, EndEntityOrCA::MustBeEndEntity, nullptr); + if (backCert.Init() != Success) { + abort(); + } + + Input spkiInput = backCert.GetSubjectPublicKeyInfo(); + Buffer spki; + InputToBuffer(spkiInput, spki); + return spki; +} + +Buffer ExtractCertSPKI(const Buffer& cert) { + return ExtractCertSPKI(InputForBuffer(cert)); +} + +void ExtractEmbeddedSCTList(Input cert, Buffer& result) { + result.clear(); + BackCert backCert(cert, EndEntityOrCA::MustBeEndEntity, nullptr); + ASSERT_EQ(Success, backCert.Init()); + const Input* scts = backCert.GetSignedCertificateTimestamps(); + if (scts) { + Input sctList; + ASSERT_EQ(Success, ExtractSignedCertificateTimestampListFromExtension( + *scts, sctList)); + InputToBuffer(sctList, result); + } +} + +void ExtractEmbeddedSCTList(const Buffer& cert, Buffer& result) { + ExtractEmbeddedSCTList(InputForBuffer(cert), result); +} + +class OCSPExtensionTrustDomain : public TrustDomain { + public: + pkix::Result GetCertTrust(EndEntityOrCA, const CertPolicyId&, Input, + TrustLevel&) override { + ADD_FAILURE(); + return pkix::Result::FATAL_ERROR_LIBRARY_FAILURE; + } + + pkix::Result FindIssuer(Input, IssuerChecker&, Time) override { + ADD_FAILURE(); + return pkix::Result::FATAL_ERROR_LIBRARY_FAILURE; + } + + pkix::Result CheckRevocation(EndEntityOrCA, const CertID&, Time, Duration, + const Input*, const Input*, + const Input*) override { + ADD_FAILURE(); + return pkix::Result::FATAL_ERROR_LIBRARY_FAILURE; + } + + pkix::Result IsChainValid(const DERArray&, Time, + const CertPolicyId&) override { + ADD_FAILURE(); + return pkix::Result::FATAL_ERROR_LIBRARY_FAILURE; + } + + pkix::Result DigestBuf(Input item, DigestAlgorithm digestAlg, + /*out*/ uint8_t* digestBuf, + size_t digestBufLen) override { + return DigestBufNSS(item, digestAlg, digestBuf, digestBufLen); + } + + pkix::Result CheckSignatureDigestAlgorithm(DigestAlgorithm, EndEntityOrCA, + Time) override { + ADD_FAILURE(); + return pkix::Result::FATAL_ERROR_LIBRARY_FAILURE; + } + + pkix::Result CheckECDSACurveIsAcceptable(EndEntityOrCA, NamedCurve) override { + ADD_FAILURE(); + return pkix::Result::FATAL_ERROR_LIBRARY_FAILURE; + } + + pkix::Result VerifyECDSASignedData(Input data, + DigestAlgorithm digestAlgorithm, + Input signature, + Input subjectPublicKeyInfo) override { + return VerifyECDSASignedDataNSS(data, digestAlgorithm, signature, + subjectPublicKeyInfo, nullptr); + } + + pkix::Result CheckRSAPublicKeyModulusSizeInBits(EndEntityOrCA, + unsigned int) override { + ADD_FAILURE(); + return pkix::Result::FATAL_ERROR_LIBRARY_FAILURE; + } + + pkix::Result VerifyRSAPKCS1SignedData(Input data, + DigestAlgorithm digestAlgorithm, + Input signature, + Input subjectPublicKeyInfo) override { + return VerifyRSAPKCS1SignedDataNSS(data, digestAlgorithm, signature, + subjectPublicKeyInfo, nullptr); + } + + pkix::Result VerifyRSAPSSSignedData(Input data, + DigestAlgorithm digestAlgorithm, + Input signature, + Input subjectPublicKeyInfo) override { + return VerifyRSAPSSSignedDataNSS(data, digestAlgorithm, signature, + subjectPublicKeyInfo, nullptr); + } + + pkix::Result CheckValidityIsAcceptable(Time, Time, EndEntityOrCA, + KeyPurposeId) override { + ADD_FAILURE(); + return pkix::Result::FATAL_ERROR_LIBRARY_FAILURE; + } + + pkix::Result NetscapeStepUpMatchesServerAuth(Time, bool&) override { + ADD_FAILURE(); + return pkix::Result::FATAL_ERROR_LIBRARY_FAILURE; + } + + void NoteAuxiliaryExtension(AuxiliaryExtension extension, + Input data) override { + if (extension != AuxiliaryExtension::SCTListFromOCSPResponse) { + ADD_FAILURE(); + return; + } + InputToBuffer(data, signedCertificateTimestamps); + } + + Buffer signedCertificateTimestamps; +}; + +void ExtractSCTListFromOCSPResponse(Input cert, Input issuerSPKI, + Input encodedResponse, Time time, + Buffer& result) { + result.clear(); + + BackCert backCert(cert, EndEntityOrCA::MustBeEndEntity, nullptr); + ASSERT_EQ(Success, backCert.Init()); + + CertID certID(backCert.GetIssuer(), issuerSPKI, backCert.GetSerialNumber()); + + bool expired; + OCSPExtensionTrustDomain trustDomain; + pkix::Result rv = + VerifyEncodedOCSPResponse(trustDomain, certID, time, /*time*/ + 1000, /*maxLifetimeInDays*/ + encodedResponse, expired); + ASSERT_EQ(Success, rv); + + result = std::move(trustDomain.signedCertificateTimestamps); +} + +Input InputForBuffer(const Buffer& buffer) { + Input input; + if (input.Init(buffer.data(), buffer.size()) != Success) { + abort(); + } + return input; +} + +Input InputForSECItem(const SECItem& item) { + Input input; + if (input.Init(item.data, item.len) != Success) { + abort(); + } + return input; +} + +} // namespace ct +} // namespace mozilla + +namespace mozilla { + +std::ostream& operator<<(std::ostream& stream, const ct::Buffer& buffer) { + if (buffer.empty()) { + stream << "EMPTY"; + } else { + for (size_t i = 0; i < buffer.size(); ++i) { + if (i >= 1000) { + stream << "..."; + break; + } + stream << std::hex << std::setw(2) << std::setfill('0') + << static_cast<unsigned>(buffer[i]); + } + } + stream << std::dec; + return stream; +} + +} // namespace mozilla diff --git a/security/ct/tests/gtest/CTTestUtils.h b/security/ct/tests/gtest/CTTestUtils.h new file mode 100644 index 0000000000..3679d4411e --- /dev/null +++ b/security/ct/tests/gtest/CTTestUtils.h @@ -0,0 +1,137 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#ifndef CTTestUtils_h +#define CTTestUtils_h + +#include <iostream> + +#include "mozpkix/Input.h" +#include "mozpkix/Time.h" +#include "seccomon.h" +#include "SignedCertificateTimestamp.h" + +namespace mozilla { +namespace ct { + +Buffer HexToBytes(const char* hexData); + +// Note: unless specified otherwise, all test data is taken from +// Certificate Transparency test data repository at +// https://github.com/google/certificate-transparency/tree/master/test/testdata + +// Fills |entry| with test data for an X.509 entry. +void GetX509CertLogEntry(LogEntry& entry); + +// Returns a DER-encoded X509 cert. The SCT provided by +// GetX509CertSCT is signed over this certificate. +Buffer GetDEREncodedX509Cert(); + +// Fills |entry| with test data for a Precertificate entry. +void GetPrecertLogEntry(LogEntry& entry); + +// Returns the binary representation of a test DigitallySigned. +Buffer GetTestDigitallySigned(); + +// Returns the source data of the test DigitallySigned. +Buffer GetTestDigitallySignedData(); + +// Returns the binary representation of a test serialized SCT. +Buffer GetTestSignedCertificateTimestamp(); + +// Returns the binary representation of a test serialized InclusionProof. +Buffer GetTestInclusionProof(); +Buffer GetTestInclusionProofUnexpectedData(); +Buffer GetTestInclusionProofInvalidHashSize(); +Buffer GetTestInclusionProofInvalidHash(); +Buffer GetTestInclusionProofMissingLogId(); +Buffer GetTestInclusionProofNullPathLength(); +Buffer GetTestInclusionProofPathLengthTooSmall(); +Buffer GetTestInclusionProofPathLengthTooLarge(); +Buffer GetTestInclusionProofNullTreeSize(); +Buffer GetTestInclusionProofLeafIndexOutOfBounds(); +Buffer GetTestInclusionProofExtraData(); + +// Returns the binary representation of test serialized node hashs from an +// inclusion proof. +Buffer GetTestNodeHash0(); +Buffer GetTestNodeHash1(); + +// Test log key. +Buffer GetTestPublicKey(); + +// ID of test log key. +Buffer GetTestPublicKeyId(); + +// SCT for the X509Certificate provided above. +void GetX509CertSCT(SignedCertificateTimestamp& sct); + +// SCT for the Precertificate log entry provided above. +void GetPrecertSCT(SignedCertificateTimestamp& sct); + +// Issuer key hash. +Buffer GetDefaultIssuerKeyHash(); + +// The SHA256 root hash for the sample STH. +Buffer GetSampleSTHSHA256RootHash(); + +// The tree head signature for the sample STH. +Buffer GetSampleSTHTreeHeadSignature(); + +// The same signature as GetSampleSTHTreeHeadSignature, decoded. +void GetSampleSTHTreeHeadDecodedSignature(DigitallySigned& signature); + +// Certificate with embedded SCT in an X509v3 extension. +Buffer GetDEREncodedTestEmbeddedCert(); + +// For the above certificate, the corresponsing TBSCertificate without +// the embedded SCT extension. +Buffer GetDEREncodedTestTbsCert(); + +// As above, but signed with an intermediate CA certificate containing +// the CT extended key usage OID 1.3.6.1.4.1.11129.2.4.4 for issuing precerts +// (i.e. signed with a "precert CA certificate"). +Buffer GetDEREncodedTestEmbeddedWithPreCACert(); + +// The issuer of the above certificates (self-signed root CA certificate). +Buffer GetDEREncodedCACert(); + +// An intermediate CA certificate issued by the above CA. +Buffer GetDEREncodedIntermediateCert(); + +// Certificate with embedded SCT signed by the intermediate certificate above. +Buffer GetDEREncodedTestEmbeddedWithIntermediateCert(); + +// As above, but signed by the precert CA certificate. +Buffer GetDEREncodedTestEmbeddedWithIntermediatePreCACert(); + +// Given a DER-encoded certificate, returns its SubjectPublicKeyInfo. +Buffer ExtractCertSPKI(pkix::Input cert); +Buffer ExtractCertSPKI(const Buffer& cert); + +// Extracts a SignedCertificateTimestampList from the provided leaf certificate +// (kept in X.509v3 extension with OID 1.3.6.1.4.1.11129.2.4.2). +void ExtractEmbeddedSCTList(pkix::Input cert, Buffer& result); +void ExtractEmbeddedSCTList(const Buffer& cert, Buffer& result); + +// Extracts a SignedCertificateTimestampList that has been embedded within +// an OCSP response as an extension with the OID 1.3.6.1.4.1.11129.2.4.5. +// The OCSP response is verified, and the verification must succeed for the +// extension to be extracted. +void ExtractSCTListFromOCSPResponse(pkix::Input cert, pkix::Input issuerSPKI, + pkix::Input encodedResponse, + pkix::Time time, Buffer& result); + +// Returns Input for the data stored in the buffer, failing assertion on error. +pkix::Input InputForBuffer(const Buffer& buffer); + +// Returns Input for the data stored in the item, failing assertion on error. +pkix::Input InputForSECItem(const SECItem& item); + +} // namespace ct +} // namespace mozilla + +#endif // CTTestUtils_h diff --git a/security/ct/tests/gtest/MultiLogCTVerifierTest.cpp b/security/ct/tests/gtest/MultiLogCTVerifierTest.cpp new file mode 100644 index 0000000000..4ac3f42f28 --- /dev/null +++ b/security/ct/tests/gtest/MultiLogCTVerifierTest.cpp @@ -0,0 +1,254 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include "MultiLogCTVerifier.h" + +#include <stdint.h> + +#include "CTLogVerifier.h" +#include "CTObjectsExtractor.h" +#include "CTSerialization.h" +#include "CTTestUtils.h" +#include "gtest/gtest.h" +#include "nss.h" + +namespace mozilla { +namespace ct { + +using namespace mozilla::pkix; + +class MultiLogCTVerifierTest : public ::testing::Test { + public: + MultiLogCTVerifierTest() : mNow(Time::uninitialized), mLogOperatorID(123) {} + + void SetUp() override { + // Does nothing if NSS is already initialized. + if (NSS_NoDB_Init(nullptr) != SECSuccess) { + abort(); + } + + CTLogVerifier log; + ASSERT_EQ(Success, + log.Init(InputForBuffer(GetTestPublicKey()), mLogOperatorID, + CTLogStatus::Included, 0 /*disqualification time*/)); + mVerifier.AddLog(std::move(log)); + + mTestCert = GetDEREncodedX509Cert(); + mEmbeddedCert = GetDEREncodedTestEmbeddedCert(); + mCaCert = GetDEREncodedCACert(); + mCaCertSPKI = ExtractCertSPKI(mCaCert); + mIntermediateCert = GetDEREncodedIntermediateCert(); + mIntermediateCertSPKI = ExtractCertSPKI(mIntermediateCert); + + // Set the current time making sure all test timestamps are in the past. + mNow = + TimeFromEpochInSeconds(1451606400u); // Date.parse("2016-01-01")/1000 + } + + void CheckForSingleValidSCTInResult(const CTVerifyResult& result, + VerifiedSCT::Origin origin) { + EXPECT_EQ(0U, result.decodingErrors); + ASSERT_EQ(1U, result.verifiedScts.size()); + EXPECT_EQ(VerifiedSCT::Status::Valid, result.verifiedScts[0].status); + EXPECT_EQ(origin, result.verifiedScts[0].origin); + EXPECT_EQ(mLogOperatorID, result.verifiedScts[0].logOperatorId); + } + + // Writes an SCTList containing a single |sct| into |output|. + void EncodeSCTListForTesting(Input sct, Buffer& output) { + std::vector<Input> list; + list.push_back(std::move(sct)); + ASSERT_EQ(Success, EncodeSCTList(list, output)); + } + + void GetSCTListWithInvalidLogID(Buffer& result) { + result.clear(); + Buffer sct(GetTestSignedCertificateTimestamp()); + // Change a byte inside the Log ID part of the SCT so it does + // not match the log used in the tests. + sct[15] ^= '\xFF'; + EncodeSCTListForTesting(InputForBuffer(sct), result); + } + + void CheckPrecertVerification(const Buffer& cert, const Buffer& issuerSPKI) { + Buffer sctList; + ExtractEmbeddedSCTList(cert, sctList); + ASSERT_FALSE(sctList.empty()); + + CTVerifyResult result; + ASSERT_EQ(Success, + mVerifier.Verify(InputForBuffer(cert), InputForBuffer(issuerSPKI), + InputForBuffer(sctList), Input(), Input(), mNow, + result)); + CheckForSingleValidSCTInResult(result, VerifiedSCT::Origin::Embedded); + } + + protected: + MultiLogCTVerifier mVerifier; + Buffer mTestCert; + Buffer mEmbeddedCert; + Buffer mCaCert; + Buffer mCaCertSPKI; + Buffer mIntermediateCert; + Buffer mIntermediateCertSPKI; + Time mNow; + CTLogOperatorId mLogOperatorID; +}; + +// Test that an embedded SCT can be extracted and the extracted SCT contains +// the expected data. This tests the ExtractEmbeddedSCTList function from +// CTTestUtils.h that other tests here rely upon. +TEST_F(MultiLogCTVerifierTest, ExtractEmbeddedSCT) { + SignedCertificateTimestamp sct; + + // Extract the embedded SCT. + + Buffer sctList; + ExtractEmbeddedSCTList(mEmbeddedCert, sctList); + ASSERT_FALSE(sctList.empty()); + + Reader sctReader; + ASSERT_EQ(Success, DecodeSCTList(InputForBuffer(sctList), sctReader)); + Input sctItemInput; + ASSERT_EQ(Success, ReadSCTListItem(sctReader, sctItemInput)); + EXPECT_TRUE(sctReader.AtEnd()); // we only expect one sct in the list + + Reader sctItemReader(sctItemInput); + ASSERT_EQ(Success, DecodeSignedCertificateTimestamp(sctItemReader, sct)); + + // Make sure the SCT contains the expected data. + + EXPECT_EQ(SignedCertificateTimestamp::Version::V1, sct.version); + EXPECT_EQ(GetTestPublicKeyId(), sct.logId); + + uint64_t expectedTimestamp = 1365181456275; + EXPECT_EQ(expectedTimestamp, sct.timestamp); +} + +TEST_F(MultiLogCTVerifierTest, VerifiesEmbeddedSCT) { + CheckPrecertVerification(mEmbeddedCert, mCaCertSPKI); +} + +TEST_F(MultiLogCTVerifierTest, VerifiesEmbeddedSCTWithPreCA) { + CheckPrecertVerification(GetDEREncodedTestEmbeddedWithPreCACert(), + mCaCertSPKI); +} + +TEST_F(MultiLogCTVerifierTest, VerifiesEmbeddedSCTWithIntermediate) { + CheckPrecertVerification(GetDEREncodedTestEmbeddedWithIntermediateCert(), + mIntermediateCertSPKI); +} + +TEST_F(MultiLogCTVerifierTest, VerifiesEmbeddedSCTWithIntermediateAndPreCA) { + CheckPrecertVerification(GetDEREncodedTestEmbeddedWithIntermediatePreCACert(), + mIntermediateCertSPKI); +} + +TEST_F(MultiLogCTVerifierTest, VerifiesSCTFromOCSP) { + Buffer sct(GetTestSignedCertificateTimestamp()); + Buffer sctList; + EncodeSCTListForTesting(InputForBuffer(sct), sctList); + + CTVerifyResult result; + ASSERT_EQ(Success, + mVerifier.Verify(InputForBuffer(mTestCert), Input(), Input(), + InputForBuffer(sctList), Input(), mNow, result)); + + CheckForSingleValidSCTInResult(result, VerifiedSCT::Origin::OCSPResponse); +} + +TEST_F(MultiLogCTVerifierTest, VerifiesSCTFromTLS) { + Buffer sct(GetTestSignedCertificateTimestamp()); + Buffer sctList; + EncodeSCTListForTesting(InputForBuffer(sct), sctList); + + CTVerifyResult result; + ASSERT_EQ(Success, + mVerifier.Verify(InputForBuffer(mTestCert), Input(), Input(), + Input(), InputForBuffer(sctList), mNow, result)); + + CheckForSingleValidSCTInResult(result, VerifiedSCT::Origin::TLSExtension); +} + +TEST_F(MultiLogCTVerifierTest, VerifiesSCTFromMultipleSources) { + Buffer sct(GetTestSignedCertificateTimestamp()); + Buffer sctList; + EncodeSCTListForTesting(InputForBuffer(sct), sctList); + + CTVerifyResult result; + ASSERT_EQ(Success, mVerifier.Verify(InputForBuffer(mTestCert), Input(), + Input(), InputForBuffer(sctList), + InputForBuffer(sctList), mNow, result)); + + // The result should contain verified SCTs from TLS and OCSP origins. + size_t embeddedCount = 0; + size_t tlsExtensionCount = 0; + size_t ocspResponseCount = 0; + for (const VerifiedSCT& verifiedSct : result.verifiedScts) { + EXPECT_EQ(VerifiedSCT::Status::Valid, verifiedSct.status); + switch (verifiedSct.origin) { + case VerifiedSCT::Origin::Embedded: + embeddedCount++; + break; + case VerifiedSCT::Origin::TLSExtension: + tlsExtensionCount++; + break; + case VerifiedSCT::Origin::OCSPResponse: + ocspResponseCount++; + break; + case VerifiedSCT::Origin::Unknown: + default: + ASSERT_TRUE(false); + } + } + EXPECT_EQ(embeddedCount, 0u); + EXPECT_TRUE(tlsExtensionCount > 0); + EXPECT_TRUE(ocspResponseCount > 0); +} + +TEST_F(MultiLogCTVerifierTest, IdentifiesSCTFromUnknownLog) { + Buffer sctList; + GetSCTListWithInvalidLogID(sctList); + + CTVerifyResult result; + ASSERT_EQ(Success, + mVerifier.Verify(InputForBuffer(mTestCert), Input(), Input(), + Input(), InputForBuffer(sctList), mNow, result)); + + EXPECT_EQ(0U, result.decodingErrors); + ASSERT_EQ(1U, result.verifiedScts.size()); + EXPECT_EQ(VerifiedSCT::Status::UnknownLog, result.verifiedScts[0].status); +} + +TEST_F(MultiLogCTVerifierTest, IdentifiesSCTFromDisqualifiedLog) { + MultiLogCTVerifier verifier; + CTLogVerifier log; + const uint64_t disqualificationTime = 12345u; + ASSERT_EQ(Success, + log.Init(InputForBuffer(GetTestPublicKey()), mLogOperatorID, + CTLogStatus::Disqualified, disqualificationTime)); + verifier.AddLog(std::move(log)); + + Buffer sct(GetTestSignedCertificateTimestamp()); + Buffer sctList; + EncodeSCTListForTesting(InputForBuffer(sct), sctList); + + CTVerifyResult result; + ASSERT_EQ(Success, + verifier.Verify(InputForBuffer(mTestCert), Input(), Input(), + Input(), InputForBuffer(sctList), mNow, result)); + + EXPECT_EQ(0U, result.decodingErrors); + ASSERT_EQ(1U, result.verifiedScts.size()); + EXPECT_EQ(VerifiedSCT::Status::ValidFromDisqualifiedLog, + result.verifiedScts[0].status); + EXPECT_EQ(disqualificationTime, + result.verifiedScts[0].logDisqualificationTime); + EXPECT_EQ(mLogOperatorID, result.verifiedScts[0].logOperatorId); +} + +} // namespace ct +} // namespace mozilla diff --git a/security/ct/tests/gtest/createSTHTestData.py b/security/ct/tests/gtest/createSTHTestData.py new file mode 100755 index 0000000000..ab61d4ba0e --- /dev/null +++ b/security/ct/tests/gtest/createSTHTestData.py @@ -0,0 +1,133 @@ +#!/usr/bin/env python +# +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at http://mozilla.org/MPL/2.0/. + +""" +This utility is used by the build system to create test inputs for the +signed tree head decoding and verification implementation. The format is +generally lines of <key>:<value> pairs except for the to-be-signed +section, which consists of one or more lines of hex bytes. Comments may +appear at the end of lines and begin with '//'. +The following keys are valid: +signingKey: A pykey key identifier to use to sign the to-be-signed data. + Required. +spki: A pykey key identifier to create an encoded SubjectPublicKeyInfo + to be included with the test data. The tests will use this spki to + validate the signature. Required. +prefix: Hex bytes to include at the beginning of the signed tree head + data. This data is not covered by the signature (typically this + is used for the log_id field). Optional. Defaults to the empty + string. +hash: The name of a hash algorithm to use when signing. Optional. + Defaults to 'sha256'. +""" + +import binascii +import os +import sys + +from pyasn1.codec.der import encoder + +sys.path.append( + os.path.join(os.path.dirname(__file__), "..", "..", "..", "manager", "tools") +) +import pykey + + +def sign(signingKey, hashAlgorithm, hexToSign): + """Given a pykey, the name of a hash function, and hex bytes to + sign, signs the data (as binary) and returns a hex string consisting + of the signature.""" + # key.sign returns a hex string in the format "'<hex bytes>'H", + # so we have to strip off the "'"s and trailing 'H' + return signingKey.sign(binascii.unhexlify(hexToSign), "hash:%s" % hashAlgorithm)[ + 1:-2 + ] + + +class Error(Exception): + """Base class for exceptions in this module.""" + + pass + + +class UnknownParameterTypeError(Error): + """Base class for handling unexpected input in this module.""" + + def __init__(self, value): + super(Error, self).__init__() + self.value = value + self.category = "key" + + def __str__(self): + return 'Unknown %s type "%s"' % (self.category, repr(self.value)) + + +class InputTooLongError(Error): + """Helper exception type for inputs that are too long.""" + + def __init__(self, length): + super(InputTooLongError, self).__init__() + self.length = length + + def __str__(self): + return "Input too long: %s > 65535" % self.length + + +def getTwoByteLenAsHex(callLenOnMe): + """Given something that len can be called on, returns a hex string + representing the two-byte length of the something, in network byte + order (the length must be less than or equal to 65535).""" + length = len(callLenOnMe) + if length > 65535: + raise InputTooLongError(length) + return bytes([length // 256, length % 256]).hex() + + +def createSTH(configStream): + """Given a stream that will provide the specification for a signed + tree head (see the comment at the top of this file), creates the + corresponding signed tree head. Returns a string that can be + compiled as C/C++ that declares two const char*s kSTHHex and + kSPKIHex corresponding to the hex encoding of the signed tree head + and the hex encoding of the subject public key info from the + specification, respectively.""" + toSign = "" + prefix = "" + hashAlgorithm = "sha256" + for line in configStream.readlines(): + if ":" in line: + param = line.split(":")[0] + arg = line.split(":")[1].split("//")[0].strip() + if param == "signingKey": + signingKey = pykey.keyFromSpecification(arg) + elif param == "spki": + spki = pykey.keyFromSpecification(arg) + elif param == "prefix": + prefix = arg + elif param == "hash": + hashAlgorithm = arg + else: + raise UnknownParameterTypeError(param) + else: + toSign = toSign + line.split("//")[0].strip() + signature = sign(signingKey, hashAlgorithm, toSign) + lengthBytesHex = getTwoByteLenAsHex(binascii.unhexlify(signature)) + sth = prefix + toSign + lengthBytesHex + signature + spkiHex = encoder.encode(spki.asSubjectPublicKeyInfo()).hex() + return 'const char* kSTHHex = "%s";\nconst char* kSPKIHex = "%s";\n' % ( + sth, + spkiHex, + ) + + +def main(output, inputPath): + """Given a file-like output and the path to a signed tree head + specification (see the comment at the top of this file), reads the + specification, creates the signed tree head, and outputs test data + that can be included by a gtest corresponding to the + specification.""" + with open(inputPath) as configStream: + output.write(createSTH(configStream)) diff --git a/security/ct/tests/gtest/ec-signer-rsa-spki-sth.inc.tbs b/security/ct/tests/gtest/ec-signer-rsa-spki-sth.inc.tbs new file mode 100644 index 0000000000..a8593e27c4 --- /dev/null +++ b/security/ct/tests/gtest/ec-signer-rsa-spki-sth.inc.tbs @@ -0,0 +1,8 @@ +signingKey:secp256r1 +spki:default // this is the default 2048-bit RSA key +prefix:020000 +00000166d6142b50 // 1541189938000 milliseconds since the epoch +0000000000000007 // 7 total nodes +20 // 32 byte hash + d1a0d3947db4ae8305f2ac32985957e02659b2ea3c10da52a48d2526e9af3bbc +0000 // no extensions diff --git a/security/ct/tests/gtest/missing-extensions-sth.inc.tbs b/security/ct/tests/gtest/missing-extensions-sth.inc.tbs new file mode 100644 index 0000000000..98807b854d --- /dev/null +++ b/security/ct/tests/gtest/missing-extensions-sth.inc.tbs @@ -0,0 +1,8 @@ +signingKey:secp256r1 +spki:secp256r1 +prefix:020000 +00000166d6142b50 // 1541189938000 milliseconds since the epoch +0000000000000007 // 7 total nodes +20 // 32 byte hash + d1a0d3947db4ae8305f2ac32985957e02659b2ea3c10da52a48d2526e9af3bbc +// missing extensions diff --git a/security/ct/tests/gtest/missing-log-id-sth.inc.tbs b/security/ct/tests/gtest/missing-log-id-sth.inc.tbs new file mode 100644 index 0000000000..6c1bb21997 --- /dev/null +++ b/security/ct/tests/gtest/missing-log-id-sth.inc.tbs @@ -0,0 +1,8 @@ +signingKey:secp256r1 +spki:secp256r1 +// missing logid +00000166d6142b50 // 1541189938000 milliseconds since the epoch +0000000000000007 // 7 total nodes +20 // 32 byte hash + d1a0d3947db4ae8305f2ac32985957e02659b2ea3c10da52a48d2526e9af3bbc +0000 // no extensions diff --git a/security/ct/tests/gtest/missing-root-hash-sth.inc.tbs b/security/ct/tests/gtest/missing-root-hash-sth.inc.tbs new file mode 100644 index 0000000000..5accf70fe8 --- /dev/null +++ b/security/ct/tests/gtest/missing-root-hash-sth.inc.tbs @@ -0,0 +1,7 @@ +signingKey:secp256r1 +spki:secp256r1 +prefix:020000 +00000166d6142b50 // 1541189938000 milliseconds since the epoch +0000000000000007 // 7 total nodes +// missing root_hash +0000 // no extensions diff --git a/security/ct/tests/gtest/missing-timestamp-sth.inc.tbs b/security/ct/tests/gtest/missing-timestamp-sth.inc.tbs new file mode 100644 index 0000000000..5e7ebb6cf2 --- /dev/null +++ b/security/ct/tests/gtest/missing-timestamp-sth.inc.tbs @@ -0,0 +1,8 @@ +signingKey:secp256r1 +spki:secp256r1 +prefix:020000 +// missing timestamp +0000000000000007 // 7 total nodes +20 // 32 byte hash + d1a0d3947db4ae8305f2ac32985957e02659b2ea3c10da52a48d2526e9af3bbc +0000 // no extensions diff --git a/security/ct/tests/gtest/missing-tree-size-sth.inc.tbs b/security/ct/tests/gtest/missing-tree-size-sth.inc.tbs new file mode 100644 index 0000000000..128e6d64b1 --- /dev/null +++ b/security/ct/tests/gtest/missing-tree-size-sth.inc.tbs @@ -0,0 +1,8 @@ +signingKey:secp256r1 +spki:secp256r1 +prefix:020000 +00000166d6142b50 // 1541189938000 milliseconds since the epoch +// missing tree_size +20 // 32 byte hash + d1a0d3947db4ae8305f2ac32985957e02659b2ea3c10da52a48d2526e9af3bbc +0000 // no extensions diff --git a/security/ct/tests/gtest/moz.build b/security/ct/tests/gtest/moz.build new file mode 100644 index 0000000000..4c0ed38557 --- /dev/null +++ b/security/ct/tests/gtest/moz.build @@ -0,0 +1,56 @@ +# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*- +# vim: set filetype=python: +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at http://mozilla.org/MPL/2.0/. + + +@template +def STHTestFile(name): + if not CONFIG["COMPILE_ENVIRONMENT"]: + return + + GeneratedFile(name, script="createSTHTestData.py", inputs=["%s.tbs" % name]) + + +STHTestFile("valid-sth.inc") +STHTestFile("valid-with-extension-sth.inc") +STHTestFile("valid-secp521r1-sha512-sth.inc") +STHTestFile("signature-covers-log-id-sth.inc") +STHTestFile("wrong-spki-sth.inc") +STHTestFile("wrong-signing-key-sth.inc") +STHTestFile("missing-log-id-sth.inc") +STHTestFile("missing-timestamp-sth.inc") +STHTestFile("missing-tree-size-sth.inc") +STHTestFile("missing-root-hash-sth.inc") +STHTestFile("missing-extensions-sth.inc") +STHTestFile("truncated-log-id-sth.inc") +STHTestFile("truncated-timestamp-sth.inc") +STHTestFile("truncated-tree-size-sth.inc") +STHTestFile("truncated-root-hash-sth.inc") +STHTestFile("truncated-extension-sth.inc") +STHTestFile("rsa-signer-rsa-spki-sth.inc") +STHTestFile("rsa-signer-ec-spki-sth.inc") +STHTestFile("ec-signer-rsa-spki-sth.inc") + +UNIFIED_SOURCES += [ + "BTSerializationTest.cpp", + "BTSignedTreeHeadTest.cpp", + "BTVerificationTest.cpp", + "CTDiversityPolicyTest.cpp", + "CTLogVerifierTest.cpp", + "CTObjectsExtractorTest.cpp", + "CTPolicyEnforcerTest.cpp", + "CTSerializationTest.cpp", + "CTTestUtils.cpp", + "MultiLogCTVerifierTest.cpp", +] + +LOCAL_INCLUDES += [ + "../..", +] + +if not CONFIG["MOZ_DEBUG"]: + DEFINES["NDEBUG"] = True + +FINAL_LIBRARY = "xul-gtest" diff --git a/security/ct/tests/gtest/rsa-signer-ec-spki-sth.inc.tbs b/security/ct/tests/gtest/rsa-signer-ec-spki-sth.inc.tbs new file mode 100644 index 0000000000..92f1a47490 --- /dev/null +++ b/security/ct/tests/gtest/rsa-signer-ec-spki-sth.inc.tbs @@ -0,0 +1,8 @@ +signingKey:default // this is the default 2048-bit RSA key +spki:secp256r1 +prefix:020000 +00000166d6142b50 // 1541189938000 milliseconds since the epoch +0000000000000007 // 7 total nodes +20 // 32 byte hash + d1a0d3947db4ae8305f2ac32985957e02659b2ea3c10da52a48d2526e9af3bbc +0000 // no extensions diff --git a/security/ct/tests/gtest/rsa-signer-rsa-spki-sth.inc.tbs b/security/ct/tests/gtest/rsa-signer-rsa-spki-sth.inc.tbs new file mode 100644 index 0000000000..b412c87e12 --- /dev/null +++ b/security/ct/tests/gtest/rsa-signer-rsa-spki-sth.inc.tbs @@ -0,0 +1,8 @@ +signingKey:default // this is the default 2048-bit RSA key +spki:default +prefix:020000 +00000166d6142b50 // 1541189938000 milliseconds since the epoch +0000000000000007 // 7 total nodes +20 // 32 byte hash + d1a0d3947db4ae8305f2ac32985957e02659b2ea3c10da52a48d2526e9af3bbc +0000 // no extensions diff --git a/security/ct/tests/gtest/signature-covers-log-id-sth.inc.tbs b/security/ct/tests/gtest/signature-covers-log-id-sth.inc.tbs new file mode 100644 index 0000000000..723ee7a978 --- /dev/null +++ b/security/ct/tests/gtest/signature-covers-log-id-sth.inc.tbs @@ -0,0 +1,8 @@ +signingKey:secp256r1 +spki:secp256r1 +020000 // This is the log id. It should not be covered by the signature, but it is in this case. +00000166d6142b50 // 1541189938000 milliseconds since the epoch +0000000000000007 // 7 total nodes +20 // 32 byte hash + d1a0d3947db4ae8305f2ac32985957e02659b2ea3c10da52a48d2526e9af3bbc +0000 // no extensions diff --git a/security/ct/tests/gtest/truncated-extension-sth.inc.tbs b/security/ct/tests/gtest/truncated-extension-sth.inc.tbs new file mode 100644 index 0000000000..0fe30a58da --- /dev/null +++ b/security/ct/tests/gtest/truncated-extension-sth.inc.tbs @@ -0,0 +1,11 @@ +signingKey:secp256r1 +spki:secp256r1 +prefix:020000 +00000166d6142b50 // 1541189938000 milliseconds since the epoch +0000000000000007 // 7 total nodes +20 // 32 byte hash + d1a0d3947db4ae8305f2ac32985957e02659b2ea3c10da52a48d2526e9af3bbc +0014 // 20 bytes of extensions + ffff // extension type ffff + 0010 // 16 bytes of data + ffffffffff // truncated diff --git a/security/ct/tests/gtest/truncated-log-id-sth.inc.tbs b/security/ct/tests/gtest/truncated-log-id-sth.inc.tbs new file mode 100644 index 0000000000..267976af44 --- /dev/null +++ b/security/ct/tests/gtest/truncated-log-id-sth.inc.tbs @@ -0,0 +1,8 @@ +signingKey:secp256r1 +spki:secp256r1 +prefix:0200 // truncated log_id +00000166d6142b50 // 1541189938000 milliseconds since the epoch +0000000000000007 // 7 total nodes +20 // 32 byte hash + d1a0d3947db4ae8305f2ac32985957e02659b2ea3c10da52a48d2526e9af3bbc +0000 // no extensions diff --git a/security/ct/tests/gtest/truncated-root-hash-sth.inc.tbs b/security/ct/tests/gtest/truncated-root-hash-sth.inc.tbs new file mode 100644 index 0000000000..295b069880 --- /dev/null +++ b/security/ct/tests/gtest/truncated-root-hash-sth.inc.tbs @@ -0,0 +1,8 @@ +signingKey:secp256r1 +spki:secp256r1 +prefix:020000 +00000166d6142b50 // 1541189938000 milliseconds since the epoch +0000000000000007 // 7 total nodes +20 // 32 byte hash + d1a0d3947db4ae8305f2ac32985957e0 // truncated +0000 // no extensions diff --git a/security/ct/tests/gtest/truncated-timestamp-sth.inc.tbs b/security/ct/tests/gtest/truncated-timestamp-sth.inc.tbs new file mode 100644 index 0000000000..247bd4dddc --- /dev/null +++ b/security/ct/tests/gtest/truncated-timestamp-sth.inc.tbs @@ -0,0 +1,8 @@ +signingKey:secp256r1 +spki:secp256r1 +prefix:020000 +00000166d6 // truncated timestamp +0000000000000007 // 7 total nodes +20 // 32 byte hash + d1a0d3947db4ae8305f2ac32985957e02659b2ea3c10da52a48d2526e9af3bbc +0000 // no extensions diff --git a/security/ct/tests/gtest/truncated-tree-size-sth.inc.tbs b/security/ct/tests/gtest/truncated-tree-size-sth.inc.tbs new file mode 100644 index 0000000000..9e0a942827 --- /dev/null +++ b/security/ct/tests/gtest/truncated-tree-size-sth.inc.tbs @@ -0,0 +1,8 @@ +signingKey:secp256r1 +spki:secp256r1 +prefix:020000 +00000166d6142b50 // 1541189938000 milliseconds since the epoch +000000 // truncate tree_size +20 // 32 byte hash + d1a0d3947db4ae8305f2ac32985957e02659b2ea3c10da52a48d2526e9af3bbc +0000 // no extensions diff --git a/security/ct/tests/gtest/valid-secp521r1-sha512-sth.inc.tbs b/security/ct/tests/gtest/valid-secp521r1-sha512-sth.inc.tbs new file mode 100644 index 0000000000..674adfec56 --- /dev/null +++ b/security/ct/tests/gtest/valid-secp521r1-sha512-sth.inc.tbs @@ -0,0 +1,10 @@ +signingKey:secp521r1 +spki:secp521r1 +hash:sha512 +prefix:020000 +000001670e7ca6e1 // 1542136309473 milliseconds since the epoch +000000002b982da5 // 731393445 total nodes +40 // 64 byte hash + 374d794a95cdcfd8b35993185fef9ba368f160d8daf432d08ba9f1ed1e5abe6c + c69291e0fa2fe0006a52570ef18c19def4e617c33ce52ef0a6e5fbe318cb0387 +0000 // no extensions diff --git a/security/ct/tests/gtest/valid-sth.inc.tbs b/security/ct/tests/gtest/valid-sth.inc.tbs new file mode 100644 index 0000000000..b1bb592e93 --- /dev/null +++ b/security/ct/tests/gtest/valid-sth.inc.tbs @@ -0,0 +1,8 @@ +signingKey:secp256r1 +spki:secp256r1 +prefix:020000 +00000166d6142b50 // 1541189938000 milliseconds since the epoch +0000000000000007 // 7 total nodes +20 // 32 byte hash + d1a0d3947db4ae8305f2ac32985957e02659b2ea3c10da52a48d2526e9af3bbc +0000 // no extensions diff --git a/security/ct/tests/gtest/valid-with-extension-sth.inc.tbs b/security/ct/tests/gtest/valid-with-extension-sth.inc.tbs new file mode 100644 index 0000000000..62612de301 --- /dev/null +++ b/security/ct/tests/gtest/valid-with-extension-sth.inc.tbs @@ -0,0 +1,11 @@ +signingKey:secp256r1 +spki:secp256r1 +prefix:020000 +00000166d6142b50 // 1541189938000 milliseconds since the epoch +0000000000000007 // 7 total nodes +20 // 32 byte hash + d1a0d3947db4ae8305f2ac32985957e02659b2ea3c10da52a48d2526e9af3bbc +0014 // 20 bytes of extensions + ffff // extension type ffff + 0010 // 16 bytes of data + ffffffffffffffffffffffffffffffff diff --git a/security/ct/tests/gtest/wrong-signing-key-sth.inc.tbs b/security/ct/tests/gtest/wrong-signing-key-sth.inc.tbs new file mode 100644 index 0000000000..b8a93bb39e --- /dev/null +++ b/security/ct/tests/gtest/wrong-signing-key-sth.inc.tbs @@ -0,0 +1,8 @@ +signingKey:secp384r1 +spki:secp256r1 +prefix:020000 +00000166d6142b50 // 1541189938000 milliseconds since the epoch +0000000000000007 // 7 total nodes +20 // 32 byte hash + d1a0d3947db4ae8305f2ac32985957e02659b2ea3c10da52a48d2526e9af3bbc +0000 // no extensions diff --git a/security/ct/tests/gtest/wrong-spki-sth.inc.tbs b/security/ct/tests/gtest/wrong-spki-sth.inc.tbs new file mode 100644 index 0000000000..c02282c904 --- /dev/null +++ b/security/ct/tests/gtest/wrong-spki-sth.inc.tbs @@ -0,0 +1,8 @@ +signingKey:secp256r1 +spki:secp384r1 +prefix:020000 +00000166d6142b50 // 1541189938000 milliseconds since the epoch +0000000000000007 // 7 total nodes +20 // 32 byte hash + d1a0d3947db4ae8305f2ac32985957e02659b2ea3c10da52a48d2526e9af3bbc +0000 // no extensions |