diff options
Diffstat (limited to 'chromium/third_party/blink/renderer/platform/loader/subresource_integrity.cc')
-rw-r--r-- | chromium/third_party/blink/renderer/platform/loader/subresource_integrity.cc | 503 |
1 files changed, 503 insertions, 0 deletions
diff --git a/chromium/third_party/blink/renderer/platform/loader/subresource_integrity.cc b/chromium/third_party/blink/renderer/platform/loader/subresource_integrity.cc new file mode 100644 index 00000000000..7d6141afa47 --- /dev/null +++ b/chromium/third_party/blink/renderer/platform/loader/subresource_integrity.cc @@ -0,0 +1,503 @@ +// Copyright 2014 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#include "third_party/blink/renderer/platform/loader/subresource_integrity.h" + +#include "third_party/blink/public/platform/web_crypto.h" +#include "third_party/blink/public/platform/web_crypto_algorithm.h" +#include "third_party/blink/renderer/platform/crypto.h" +#include "third_party/blink/renderer/platform/loader/fetch/resource.h" +#include "third_party/blink/renderer/platform/weborigin/kurl.h" +#include "third_party/blink/renderer/platform/weborigin/security_origin.h" +#include "third_party/blink/renderer/platform/wtf/ascii_ctype.h" +#include "third_party/blink/renderer/platform/wtf/dtoa/utils.h" +#include "third_party/blink/renderer/platform/wtf/text/base64.h" +#include "third_party/blink/renderer/platform/wtf/text/parsing_utilities.h" +#include "third_party/blink/renderer/platform/wtf/text/string_utf8_adaptor.h" +#include "third_party/blink/renderer/platform/wtf/text/wtf_string.h" +#include "third_party/blink/renderer/platform/wtf/vector.h" +#include "third_party/boringssl/src/include/openssl/curve25519.h" + +namespace blink { + +// FIXME: This should probably use common functions with ContentSecurityPolicy. +static bool IsIntegrityCharacter(UChar c) { + // Check if it's a base64 encoded value. We're pretty loose here, as there's + // not much risk in it, and it'll make it simpler for developers. + return IsASCIIAlphanumeric(c) || c == '_' || c == '-' || c == '+' || + c == '/' || c == '='; +} + +static bool IsValueCharacter(UChar c) { + // VCHAR per https://tools.ietf.org/html/rfc5234#appendix-B.1 + return c >= 0x21 && c <= 0x7e; +} + +static bool DigestsEqual(const DigestValue& digest1, + const DigestValue& digest2) { + if (digest1.size() != digest2.size()) + return false; + + for (size_t i = 0; i < digest1.size(); i++) { + if (digest1[i] != digest2[i]) + return false; + } + + return true; +} + +inline bool IsSpaceOrComma(UChar c) { + return IsASCIISpace(c) || c == ','; +} + +static String DigestToString(const DigestValue& digest) { + return Base64Encode(reinterpret_cast<const char*>(digest.data()), + digest.size(), kBase64DoNotInsertLFs); +} + +void SubresourceIntegrity::ReportInfo::AddUseCount(UseCounterFeature feature) { + use_counts_.push_back(feature); +} + +void SubresourceIntegrity::ReportInfo::AddConsoleErrorMessage( + const String& message) { + console_error_messages_.push_back(message); +} + +void SubresourceIntegrity::ReportInfo::Clear() { + use_counts_.clear(); + console_error_messages_.clear(); +} + +bool SubresourceIntegrity::CheckSubresourceIntegrity( + const IntegrityMetadataSet& metadata_set, + const char* content, + size_t size, + const KURL& resource_url, + const Resource& resource, + ReportInfo& report_info) { + if (!resource.IsSameOriginOrCORSSuccessful()) { + report_info.AddConsoleErrorMessage( + "Subresource Integrity: The resource '" + resource_url.ElidedString() + + "' has an integrity attribute, but the resource " + "requires the request to be CORS enabled to check " + "the integrity, and it is not. The resource has been " + "blocked because the integrity cannot be enforced."); + report_info.AddUseCount(ReportInfo::UseCounterFeature:: + kSRIElementIntegrityAttributeButIneligible); + return false; + } + + return CheckSubresourceIntegrityImpl( + metadata_set, content, size, resource_url, + resource.GetResponse().HttpHeaderField("Integrity"), report_info); +} + +bool SubresourceIntegrity::CheckSubresourceIntegrity( + const String& integrity_metadata, + IntegrityFeatures features, + const char* content, + size_t size, + const KURL& resource_url, + ReportInfo& report_info) { + if (integrity_metadata.IsEmpty()) + return true; + + IntegrityMetadataSet metadata_set; + IntegrityParseResult integrity_parse_result = ParseIntegrityAttribute( + integrity_metadata, features, metadata_set, &report_info); + if (integrity_parse_result != kIntegrityParseValidResult) + return true; + // TODO(vogelheim): crbug.com/753349, figure out how deal with Ed25519 + // checking here. + String integrity_header; + return CheckSubresourceIntegrityImpl( + metadata_set, content, size, resource_url, integrity_header, report_info); +} + +bool SubresourceIntegrity::CheckSubresourceIntegrityImpl( + const IntegrityMetadataSet& metadata_set, + const char* content, + size_t size, + const KURL& resource_url, + const String integrity_header, + ReportInfo& report_info) { + if (!metadata_set.size()) + return true; + + // Check any of the "strongest" integrity constraints. + IntegrityAlgorithm max_algorithm = FindBestAlgorithm(metadata_set); + CheckFunction checker = GetCheckFunctionForAlgorithm(max_algorithm); + bool report_ed25519 = max_algorithm == IntegrityAlgorithm::kEd25519; + if (report_ed25519) { + report_info.AddUseCount(ReportInfo::UseCounterFeature::kSRISignatureCheck); + } + for (const IntegrityMetadata& metadata : metadata_set) { + if (metadata.Algorithm() == max_algorithm && + (*checker)(metadata, content, size, integrity_header)) { + report_info.AddUseCount(ReportInfo::UseCounterFeature:: + kSRIElementWithMatchingIntegrityAttribute); + if (report_ed25519) { + report_info.AddUseCount( + ReportInfo::UseCounterFeature::kSRISignatureSuccess); + } + return true; + } + } + + // If we arrive here, none of the "strongest" constaints have validated + // the data we received. Report this fact. + DigestValue digest; + if (ComputeDigest(kHashAlgorithmSha256, content, size, digest)) { + // This message exposes the digest of the resource to the console. + // Because this is only to the console, that's okay for now, but we + // need to be very careful not to expose this in exceptions or + // JavaScript, otherwise it risks exposing information about the + // resource cross-origin. + report_info.AddConsoleErrorMessage( + "Failed to find a valid digest in the 'integrity' attribute for " + "resource '" + + resource_url.ElidedString() + "' with computed SHA-256 integrity '" + + DigestToString(digest) + "'. The resource has been blocked."); + } else { + report_info.AddConsoleErrorMessage( + "There was an error computing an integrity value for resource '" + + resource_url.ElidedString() + "'. The resource has been blocked."); + } + report_info.AddUseCount(ReportInfo::UseCounterFeature:: + kSRIElementWithNonMatchingIntegrityAttribute); + return false; +} + +IntegrityAlgorithm SubresourceIntegrity::FindBestAlgorithm( + const IntegrityMetadataSet& metadata_set) { + // Find the "strongest" algorithm in the set. (This relies on + // IntegrityAlgorithm declaration order matching the "strongest" order, so + // make the compiler check this assumption first.) + static_assert(IntegrityAlgorithm::kSha256 < IntegrityAlgorithm::kSha384 && + IntegrityAlgorithm::kSha384 < IntegrityAlgorithm::kSha512 && + IntegrityAlgorithm::kSha512 < IntegrityAlgorithm::kEd25519, + "IntegrityAlgorithm enum order should match the priority " + "of the integrity algorithms."); + + // metadata_set is non-empty, so we are guaranteed to always have a result. + // This is effectively an implemenation of std::max_element (C++17). + DCHECK(!metadata_set.IsEmpty()); + auto iter = metadata_set.begin(); + IntegrityAlgorithm max_algorithm = iter->second; + ++iter; + for (; iter != metadata_set.end(); ++iter) { + max_algorithm = std::max(iter->second, max_algorithm); + } + return max_algorithm; +} + +SubresourceIntegrity::CheckFunction +SubresourceIntegrity::GetCheckFunctionForAlgorithm( + IntegrityAlgorithm algorithm) { + switch (algorithm) { + case IntegrityAlgorithm::kSha256: + case IntegrityAlgorithm::kSha384: + case IntegrityAlgorithm::kSha512: + return SubresourceIntegrity::CheckSubresourceIntegrityDigest; + case IntegrityAlgorithm::kEd25519: + return SubresourceIntegrity::CheckSubresourceIntegritySignature; + } + NOTREACHED(); + return nullptr; +} + +bool SubresourceIntegrity::CheckSubresourceIntegrityDigest( + const IntegrityMetadata& metadata, + const char* content, + size_t size, + const String& integrity_header) { + blink::HashAlgorithm hash_algo = kHashAlgorithmSha256; + switch (metadata.Algorithm()) { + case IntegrityAlgorithm::kSha256: + hash_algo = kHashAlgorithmSha256; + break; + case IntegrityAlgorithm::kSha384: + hash_algo = kHashAlgorithmSha384; + break; + case IntegrityAlgorithm::kSha512: + hash_algo = kHashAlgorithmSha512; + break; + case IntegrityAlgorithm::kEd25519: + NOTREACHED(); + break; + } + + DigestValue digest; + if (!ComputeDigest(hash_algo, content, size, digest)) + return false; + + Vector<char> hash_vector; + Base64Decode(metadata.Digest(), hash_vector); + DigestValue converted_hash_vector; + converted_hash_vector.Append(reinterpret_cast<uint8_t*>(hash_vector.data()), + hash_vector.size()); + return DigestsEqual(digest, converted_hash_vector); +} + +bool SubresourceIntegrity::CheckSubresourceIntegritySignature( + const IntegrityMetadata& metadata, + const char* content, + size_t size, + const String& integrity_header) { + DCHECK_EQ(IntegrityAlgorithm::kEd25519, metadata.Algorithm()); + + Vector<char> pubkey; + if (!Base64Decode(metadata.Digest(), pubkey) || + pubkey.size() != ED25519_PUBLIC_KEY_LEN) + return false; + + // Parse the Integrity:-header containing the signature(s). + Vector<UChar> integrity_header_chars; + integrity_header.AppendTo(integrity_header_chars); + const UChar* position = integrity_header_chars.begin(); + + const UChar* const end_position = integrity_header_chars.end(); + while (position < end_position) { + // We expect substrings of the form "ed25519-<BASE64>* ,". + // We'll move all of our UChar* pointers up front (before any early exits + // from the loop), since we should cleanly skip the next token in the + // header in all cases, even if the current token doesn't validate. + SkipWhile<UChar, IsSpaceOrComma>(position, end_position); + IntegrityAlgorithm algorithm; + bool found_ed25519 = + kAlgorithmValid == + ParseIntegrityHeaderAlgorithm(position, end_position, algorithm) && + IntegrityAlgorithm::kEd25519 == algorithm; + const UChar* digest_begin = position; + SkipUntil<UChar, IsSpaceOrComma>(position, end_position); + const UChar* const digest_end = position; + + // Now, algorithm contains the parsed algorithm specifier, the digest is + // found at digest_begin..digest_end, and position sits before the next + // token. + + if (!found_ed25519) + continue; + + String signature_raw; + if (!ParseDigest(digest_begin, digest_end, signature_raw)) + continue; + + Vector<char> signature; + Base64Decode(signature_raw, signature); + if (signature.size() != ED25519_SIGNATURE_LEN) + continue; + + // BoringSSL/OpenSSL functions return 1 for success. + if (1 == + ED25519_verify(reinterpret_cast<const uint8_t*>(content), size, + reinterpret_cast<const uint8_t*>(&*signature.begin()), + reinterpret_cast<const uint8_t*>(&*pubkey.begin()))) { + return true; + } + } + return false; +} + +SubresourceIntegrity::AlgorithmParseResult +SubresourceIntegrity::ParseAttributeAlgorithm(const UChar*& begin, + const UChar* end, + IntegrityFeatures features, + IntegrityAlgorithm& algorithm) { + static const AlgorithmPrefixPair kPrefixes[] = { + {"sha256", IntegrityAlgorithm::kSha256}, + {"sha-256", IntegrityAlgorithm::kSha256}, + {"sha384", IntegrityAlgorithm::kSha384}, + {"sha-384", IntegrityAlgorithm::kSha384}, + {"sha512", IntegrityAlgorithm::kSha512}, + {"sha-512", IntegrityAlgorithm::kSha512}, + {"ed25519", IntegrityAlgorithm::kEd25519}}; + + // The last algorithm prefix is the ed25519 signature algorithm, which should + // only be enabled if kSignatures is requested. We'll implement this by + // adjusting the last_prefix index into the array. + size_t last_prefix = WTF_ARRAY_LENGTH(kPrefixes); + if (features != IntegrityFeatures::kSignatures) + last_prefix--; + + return ParseAlgorithmPrefix(begin, end, kPrefixes, last_prefix, algorithm); +} + +SubresourceIntegrity::AlgorithmParseResult +SubresourceIntegrity::ParseIntegrityHeaderAlgorithm( + const UChar*& begin, + const UChar* end, + IntegrityAlgorithm& algorithm) { + static const AlgorithmPrefixPair kPrefixes[] = { + {"ed25519", IntegrityAlgorithm::kEd25519}}; + return ParseAlgorithmPrefix(begin, end, kPrefixes, + WTF_ARRAY_LENGTH(kPrefixes), algorithm); +} + +SubresourceIntegrity::AlgorithmParseResult +SubresourceIntegrity::ParseAlgorithmPrefix( + const UChar*& string_position, + const UChar* string_end, + const AlgorithmPrefixPair* prefix_table, + size_t prefix_table_size, + IntegrityAlgorithm& algorithm) { + for (size_t i = 0; i < prefix_table_size; i++) { + const UChar* pos = string_position; + if (SkipToken<UChar>(pos, string_end, prefix_table[i].first) && + SkipExactly<UChar>(pos, string_end, '-')) { + string_position = pos; + algorithm = prefix_table[i].second; + return kAlgorithmValid; + } + } + + const UChar* dash_position = string_position; + SkipUntil<UChar>(dash_position, string_end, '-'); + return dash_position < string_end ? kAlgorithmUnknown : kAlgorithmUnparsable; +} + +// Before: +// +// [algorithm]-[hash] OR [algorithm]-[hash]?[options] +// ^ ^ ^ ^ +// position end position end +// +// After (if successful: if the method returns false, we make no promises and +// the caller should exit early): +// +// [algorithm]-[hash] OR [algorithm]-[hash]?[options] +// ^ ^ ^ +// position/end position end +bool SubresourceIntegrity::ParseDigest(const UChar*& position, + const UChar* end, + String& digest) { + const UChar* begin = position; + SkipWhile<UChar, IsIntegrityCharacter>(position, end); + if (position == begin || (position != end && *position != '?')) { + digest = g_empty_string; + return false; + } + + // We accept base64url encoding, but normalize to "normal" base64 internally: + digest = NormalizeToBase64(String(begin, position - begin)); + return true; +} + +SubresourceIntegrity::IntegrityParseResult +SubresourceIntegrity::ParseIntegrityAttribute( + const WTF::String& attribute, + IntegrityFeatures features, + IntegrityMetadataSet& metadata_set) { + return ParseIntegrityAttribute(attribute, features, metadata_set, nullptr); +} + +SubresourceIntegrity::IntegrityParseResult +SubresourceIntegrity::ParseIntegrityAttribute( + const WTF::String& attribute, + IntegrityFeatures features, + IntegrityMetadataSet& metadata_set, + ReportInfo* report_info) { + // We expect a "clean" metadata_set, since metadata_set should only be filled + // once. + DCHECK(metadata_set.IsEmpty()); + + Vector<UChar> characters; + attribute.StripWhiteSpace().AppendTo(characters); + const UChar* position = characters.data(); + const UChar* end = characters.end(); + const UChar* current_integrity_end; + + bool error = false; + + // The integrity attribute takes the form: + // *WSP hash-with-options *( 1*WSP hash-with-options ) *WSP / *WSP + // To parse this, break on whitespace, parsing each algorithm/digest/option + // in order. + while (position < end) { + WTF::String digest; + IntegrityAlgorithm algorithm; + + SkipWhile<UChar, IsASCIISpace>(position, end); + current_integrity_end = position; + SkipUntil<UChar, IsASCIISpace>(current_integrity_end, end); + + // Algorithm parsing errors are non-fatal (the subresource should + // still be loaded) because strong hash algorithms should be used + // without fear of breaking older user agents that don't support + // them. + AlgorithmParseResult parse_result = ParseAttributeAlgorithm( + position, current_integrity_end, features, algorithm); + if (parse_result == kAlgorithmUnknown) { + // Unknown hash algorithms are treated as if they're not present, + // and thus are not marked as an error, they're just skipped. + SkipUntil<UChar, IsASCIISpace>(position, end); + if (report_info) { + report_info->AddConsoleErrorMessage( + "Error parsing 'integrity' attribute ('" + attribute + + "'). The specified hash algorithm must be one of " + "'sha256', 'sha384', or 'sha512'."); + report_info->AddUseCount( + ReportInfo::UseCounterFeature:: + kSRIElementWithUnparsableIntegrityAttribute); + } + continue; + } + + if (parse_result == kAlgorithmUnparsable) { + error = true; + SkipUntil<UChar, IsASCIISpace>(position, end); + if (report_info) { + report_info->AddConsoleErrorMessage( + "Error parsing 'integrity' attribute ('" + attribute + + "'). The hash algorithm must be one of 'sha256', " + "'sha384', or 'sha512', followed by a '-' " + "character."); + report_info->AddUseCount( + ReportInfo::UseCounterFeature:: + kSRIElementWithUnparsableIntegrityAttribute); + } + continue; + } + + DCHECK_EQ(parse_result, kAlgorithmValid); + + if (!ParseDigest(position, current_integrity_end, digest)) { + error = true; + SkipUntil<UChar, IsASCIISpace>(position, end); + if (report_info) { + report_info->AddConsoleErrorMessage( + "Error parsing 'integrity' attribute ('" + attribute + + "'). The digest must be a valid, base64-encoded value."); + report_info->AddUseCount( + ReportInfo::UseCounterFeature:: + kSRIElementWithUnparsableIntegrityAttribute); + } + continue; + } + + // The spec defines a space in the syntax for options, separated by a + // '?' character followed by unbounded VCHARs, but no actual options + // have been defined yet. Thus, for forward compatibility, ignore any + // options specified. + if (SkipExactly<UChar>(position, end, '?')) { + const UChar* begin = position; + SkipWhile<UChar, IsValueCharacter>(position, end); + if (begin != position && report_info) { + report_info->AddConsoleErrorMessage( + "Ignoring unrecogized 'integrity' attribute option '" + + String(begin, position - begin) + "'."); + } + } + + IntegrityMetadata integrity_metadata(digest, algorithm); + metadata_set.insert(integrity_metadata.ToPair()); + } + if (metadata_set.size() == 0 && error) + return kIntegrityParseNoValidResult; + + return kIntegrityParseValidResult; +} + +} // namespace blink |