Bug 1941046 - Part 4: Send a callback request for impression and clicks of MARS Top...
[gecko.git] / dom / security / SRICheck.cpp
blob3f188636d385e85992811275f13b55df13c07bf2
1 /* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
2 /* vim: set ts=8 sts=2 et sw=2 tw=80: */
3 /* This Source Code Form is subject to the terms of the Mozilla Public
4 * License, v. 2.0. If a copy of the MPL was not distributed with this
5 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
7 #include "SRICheck.h"
9 #include "mozilla/Base64.h"
10 #include "mozilla/LoadTainting.h"
11 #include "mozilla/Logging.h"
12 #include "mozilla/Preferences.h"
13 #include "mozilla/dom/SRILogHelper.h"
14 #include "mozilla/dom/SRIMetadata.h"
15 #include "nsComponentManagerUtils.h"
16 #include "nsContentUtils.h"
17 #include "nsIChannel.h"
18 #include "nsIConsoleReportCollector.h"
19 #include "nsIScriptError.h"
20 #include "nsIURI.h"
21 #include "nsNetUtil.h"
22 #include "nsWhitespaceTokenizer.h"
24 #define SRIVERBOSE(args) \
25 MOZ_LOG(SRILogHelper::GetSriLog(), mozilla::LogLevel::Verbose, args)
26 #define SRILOG(args) \
27 MOZ_LOG(SRILogHelper::GetSriLog(), mozilla::LogLevel::Debug, args)
28 #define SRIERROR(args) \
29 MOZ_LOG(SRILogHelper::GetSriLog(), mozilla::LogLevel::Error, args)
31 namespace mozilla::dom {
33 /**
34 * Returns whether or not the sub-resource about to be loaded is eligible
35 * for integrity checks. If it's not, the checks will be skipped and the
36 * sub-resource will be loaded.
38 static nsresult IsEligible(nsIChannel* aChannel,
39 mozilla::LoadTainting aTainting,
40 const nsACString& aSourceFileURI,
41 nsIConsoleReportCollector* aReporter) {
42 NS_ENSURE_ARG_POINTER(aReporter);
44 if (!aChannel) {
45 SRILOG(("SRICheck::IsEligible, null channel"));
46 return NS_ERROR_SRI_NOT_ELIGIBLE;
49 // Was the sub-resource loaded via CORS?
50 if (aTainting == LoadTainting::CORS) {
51 SRILOG(("SRICheck::IsEligible, CORS mode"));
52 return NS_OK;
55 nsCOMPtr<nsIURI> finalURI;
56 nsresult rv = NS_GetFinalChannelURI(aChannel, getter_AddRefs(finalURI));
57 NS_ENSURE_SUCCESS(rv, rv);
58 nsCOMPtr<nsIURI> originalURI;
59 rv = aChannel->GetOriginalURI(getter_AddRefs(originalURI));
60 NS_ENSURE_SUCCESS(rv, rv);
61 nsAutoCString requestSpec;
62 rv = originalURI->GetSpec(requestSpec);
63 NS_ENSURE_SUCCESS(rv, rv);
65 if (MOZ_LOG_TEST(SRILogHelper::GetSriLog(), mozilla::LogLevel::Debug)) {
66 SRILOG(("SRICheck::IsEligible, requestURI=%s; finalURI=%s",
67 requestSpec.get(),
68 finalURI ? finalURI->GetSpecOrDefault().get() : ""));
71 // Is the sub-resource same-origin?
72 if (aTainting == LoadTainting::Basic) {
73 SRILOG(("SRICheck::IsEligible, same-origin"));
74 return NS_OK;
76 SRILOG(("SRICheck::IsEligible, NOT same-origin"));
78 NS_ConvertUTF8toUTF16 requestSpecUTF16(requestSpec);
79 nsTArray<nsString> params;
80 params.AppendElement(requestSpecUTF16);
81 aReporter->AddConsoleReport(
82 nsIScriptError::errorFlag, "Sub-resource Integrity"_ns,
83 nsContentUtils::eSECURITY_PROPERTIES, aSourceFileURI, 0, 0,
84 "IneligibleResource"_ns, const_cast<const nsTArray<nsString>&>(params));
85 return NS_ERROR_SRI_NOT_ELIGIBLE;
88 /* static */
89 nsresult SRICheck::IntegrityMetadata(const nsAString& aMetadataList,
90 const nsACString& aSourceFileURI,
91 nsIConsoleReportCollector* aReporter,
92 SRIMetadata* outMetadata) {
93 NS_ENSURE_ARG_POINTER(outMetadata);
94 NS_ENSURE_ARG_POINTER(aReporter);
95 MOZ_ASSERT(outMetadata->IsEmpty()); // caller must pass empty metadata
97 NS_ConvertUTF16toUTF8 metadataList(aMetadataList);
98 SRILOG(("SRICheck::IntegrityMetadata, metadataList=%s", metadataList.get()));
100 // the integrity attribute is a list of whitespace-separated hashes
101 // and options so we need to look at them one by one and pick the
102 // strongest (valid) one
103 nsCWhitespaceTokenizer tokenizer(metadataList);
104 nsAutoCString token;
105 while (tokenizer.hasMoreTokens()) {
106 token = tokenizer.nextToken();
108 SRIMetadata metadata(token);
109 if (metadata.IsMalformed()) {
110 NS_ConvertUTF8toUTF16 tokenUTF16(token);
111 nsTArray<nsString> params;
112 params.AppendElement(tokenUTF16);
113 aReporter->AddConsoleReport(
114 nsIScriptError::warningFlag, "Sub-resource Integrity"_ns,
115 nsContentUtils::eSECURITY_PROPERTIES, aSourceFileURI, 0, 0,
116 "MalformedIntegrityHash"_ns,
117 const_cast<const nsTArray<nsString>&>(params));
118 } else if (!metadata.IsAlgorithmSupported()) {
119 nsAutoCString alg;
120 metadata.GetAlgorithm(&alg);
121 NS_ConvertUTF8toUTF16 algUTF16(alg);
122 nsTArray<nsString> params;
123 params.AppendElement(algUTF16);
124 aReporter->AddConsoleReport(
125 nsIScriptError::warningFlag, "Sub-resource Integrity"_ns,
126 nsContentUtils::eSECURITY_PROPERTIES, aSourceFileURI, 0, 0,
127 "UnsupportedHashAlg"_ns,
128 const_cast<const nsTArray<nsString>&>(params));
131 nsAutoCString alg1, alg2;
132 if (MOZ_LOG_TEST(SRILogHelper::GetSriLog(), mozilla::LogLevel::Debug)) {
133 outMetadata->GetAlgorithm(&alg1);
134 metadata.GetAlgorithm(&alg2);
136 if (*outMetadata == metadata) {
137 SRILOG(("SRICheck::IntegrityMetadata, alg '%s' is the same as '%s'",
138 alg1.get(), alg2.get()));
139 *outMetadata += metadata; // add new hash to strongest metadata
140 } else if (*outMetadata < metadata) {
141 SRILOG(("SRICheck::IntegrityMetadata, alg '%s' is weaker than '%s'",
142 alg1.get(), alg2.get()));
143 *outMetadata = metadata; // replace strongest metadata with current
147 outMetadata->mIntegrityString = aMetadataList;
149 if (MOZ_LOG_TEST(SRILogHelper::GetSriLog(), mozilla::LogLevel::Debug)) {
150 if (outMetadata->IsValid()) {
151 nsAutoCString alg;
152 outMetadata->GetAlgorithm(&alg);
153 SRILOG(("SRICheck::IntegrityMetadata, using a '%s' hash", alg.get()));
154 } else if (outMetadata->IsEmpty()) {
155 SRILOG(("SRICheck::IntegrityMetadata, no metadata"));
156 } else {
157 SRILOG(("SRICheck::IntegrityMetadata, no valid metadata found"));
160 return NS_OK;
163 //////////////////////////////////////////////////////////////
165 //////////////////////////////////////////////////////////////
166 SRICheckDataVerifier::SRICheckDataVerifier(const SRIMetadata& aMetadata,
167 const nsACString& aSourceFileURI,
168 nsIConsoleReportCollector* aReporter)
169 : mCryptoHash(nullptr),
170 mBytesHashed(0),
171 mHashLength(0),
172 mHashType('\0'),
173 mInvalidMetadata(false),
174 mComplete(false) {
175 MOZ_ASSERT(!aMetadata.IsEmpty()); // should be checked by caller
176 MOZ_ASSERT(aReporter);
178 if (!aMetadata.IsValid()) {
179 nsTArray<nsString> params;
180 aReporter->AddConsoleReport(
181 nsIScriptError::warningFlag, "Sub-resource Integrity"_ns,
182 nsContentUtils::eSECURITY_PROPERTIES, aSourceFileURI, 0, 0,
183 "NoValidMetadata"_ns, const_cast<const nsTArray<nsString>&>(params));
184 mInvalidMetadata = true;
185 return; // ignore invalid metadata for forward-compatibility
188 aMetadata.GetHashType(&mHashType, &mHashLength);
191 nsresult SRICheckDataVerifier::EnsureCryptoHash() {
192 MOZ_ASSERT(!mInvalidMetadata);
194 if (mCryptoHash) {
195 return NS_OK;
198 nsCOMPtr<nsICryptoHash> cryptoHash;
199 nsresult rv = NS_NewCryptoHash(mHashType, getter_AddRefs(cryptoHash));
200 NS_ENSURE_SUCCESS(rv, rv);
202 mCryptoHash = cryptoHash;
203 return NS_OK;
206 nsresult SRICheckDataVerifier::Update(uint32_t aStringLen,
207 const uint8_t* aString) {
208 NS_ENSURE_ARG_POINTER(aString);
209 if (mInvalidMetadata) {
210 return NS_OK; // ignoring any data updates, see mInvalidMetadata usage
213 nsresult rv;
214 rv = EnsureCryptoHash();
215 NS_ENSURE_SUCCESS(rv, rv);
217 mBytesHashed += aStringLen;
219 return mCryptoHash->Update(aString, aStringLen);
222 nsresult SRICheckDataVerifier::Finish() {
223 if (mInvalidMetadata || mComplete) {
224 return NS_OK; // already finished or invalid metadata
227 nsresult rv;
228 rv = EnsureCryptoHash(); // we need computed hash even for 0-length data
229 NS_ENSURE_SUCCESS(rv, rv);
231 rv = mCryptoHash->Finish(false, mComputedHash);
232 mCryptoHash = nullptr;
233 mComplete = true;
234 return rv;
237 nsresult SRICheckDataVerifier::VerifyHash(
238 const SRIMetadata& aMetadata, uint32_t aHashIndex,
239 const nsACString& aSourceFileURI, nsIConsoleReportCollector* aReporter) {
240 NS_ENSURE_ARG_POINTER(aReporter);
242 nsAutoCString base64Hash;
243 aMetadata.GetHash(aHashIndex, &base64Hash);
244 SRILOG(("SRICheckDataVerifier::VerifyHash, hash[%u]=%s", aHashIndex,
245 base64Hash.get()));
247 nsAutoCString binaryHash;
249 // We're decoding the supplied hash twice. Trying base64 first.
250 nsresult rv = Base64Decode(base64Hash, binaryHash);
252 if (NS_FAILED(rv)) {
253 SRILOG(
254 ("SRICheckDataVerifier::VerifyHash, base64 decoding failed. Trying "
255 "base64url next."));
256 FallibleTArray<uint8_t> decoded;
257 rv = Base64URLDecode(base64Hash, Base64URLDecodePaddingPolicy::Ignore,
258 decoded);
259 if (NS_FAILED(rv)) {
260 SRILOG(
261 ("SRICheckDataVerifier::VerifyHash, base64url decoding failed too. "
262 "Bailing out."));
263 // if neither succeeded, we can bail out and warn
264 nsTArray<nsString> params;
265 aReporter->AddConsoleReport(
266 nsIScriptError::errorFlag, "Sub-resource Integrity"_ns,
267 nsContentUtils::eSECURITY_PROPERTIES, aSourceFileURI, 0, 0,
268 "InvalidIntegrityBase64"_ns,
269 const_cast<const nsTArray<nsString>&>(params));
270 return NS_ERROR_SRI_CORRUPT;
272 binaryHash.Assign(reinterpret_cast<const char*>(decoded.Elements()),
273 decoded.Length());
274 SRILOG(
275 ("SRICheckDataVerifier::VerifyHash, decoded supplied base64url hash "
276 "successfully."));
277 } else {
278 SRILOG(
279 ("SRICheckDataVerifier::VerifyHash, decoded supplied base64 hash "
280 "successfully."));
283 uint32_t hashLength;
284 int8_t hashType;
285 aMetadata.GetHashType(&hashType, &hashLength);
286 if (binaryHash.Length() != hashLength) {
287 SRILOG(
288 ("SRICheckDataVerifier::VerifyHash, supplied base64(url) hash was "
289 "incorrect length after decoding."));
290 nsTArray<nsString> params;
291 aReporter->AddConsoleReport(
292 nsIScriptError::errorFlag, "Sub-resource Integrity"_ns,
293 nsContentUtils::eSECURITY_PROPERTIES, aSourceFileURI, 0, 0,
294 "InvalidIntegrityLength"_ns,
295 const_cast<const nsTArray<nsString>&>(params));
296 return NS_ERROR_SRI_CORRUPT;
299 // the decoded supplied hash should match our computed binary hash.
300 if (!binaryHash.Equals(mComputedHash)) {
301 SRILOG(("SRICheckDataVerifier::VerifyHash, hash[%u] did not match",
302 aHashIndex));
303 return NS_ERROR_SRI_CORRUPT;
306 SRILOG(("SRICheckDataVerifier::VerifyHash, hash[%u] verified successfully",
307 aHashIndex));
308 return NS_OK;
311 nsresult SRICheckDataVerifier::Verify(const SRIMetadata& aMetadata,
312 nsIChannel* aChannel,
313 const nsACString& aSourceFileURI,
314 nsIConsoleReportCollector* aReporter) {
315 NS_ENSURE_ARG_POINTER(aReporter);
317 if (MOZ_LOG_TEST(SRILogHelper::GetSriLog(), mozilla::LogLevel::Debug)) {
318 nsAutoCString requestURL;
319 nsCOMPtr<nsIRequest> request = aChannel;
320 request->GetName(requestURL);
321 SRILOG(("SRICheckDataVerifier::Verify, url=%s (length=%zu)",
322 requestURL.get(), mBytesHashed));
325 nsresult rv = Finish();
326 NS_ENSURE_SUCCESS(rv, rv);
328 nsCOMPtr<nsILoadInfo> loadInfo = aChannel->LoadInfo();
329 LoadTainting tainting = loadInfo->GetTainting();
331 if (NS_FAILED(IsEligible(aChannel, tainting, aSourceFileURI, aReporter))) {
332 return NS_ERROR_SRI_NOT_ELIGIBLE;
335 if (mInvalidMetadata) {
336 return NS_OK; // ignore invalid metadata for forward-compatibility
339 for (uint32_t i = 0; i < aMetadata.HashCount(); i++) {
340 if (NS_SUCCEEDED(VerifyHash(aMetadata, i, aSourceFileURI, aReporter))) {
341 return NS_OK; // stop at the first valid hash
345 nsAutoCString alg;
346 aMetadata.GetAlgorithm(&alg);
348 nsCOMPtr<nsIURI> originalURI;
349 rv = aChannel->GetOriginalURI(getter_AddRefs(originalURI));
350 NS_ENSURE_SUCCESS(rv, rv);
351 nsAutoCString requestSpec;
352 rv = originalURI->GetSpec(requestSpec);
353 NS_ENSURE_SUCCESS(rv, rv);
355 nsAutoCString encodedHash;
356 rv = Base64Encode(mComputedHash, encodedHash);
357 NS_ENSURE_SUCCESS(rv, rv);
359 nsTArray<nsString> params;
360 params.AppendElement(NS_ConvertUTF8toUTF16(alg));
361 params.AppendElement(NS_ConvertUTF8toUTF16(requestSpec));
362 params.AppendElement(NS_ConvertUTF8toUTF16(encodedHash));
363 aReporter->AddConsoleReport(
364 nsIScriptError::errorFlag, "Sub-resource Integrity"_ns,
365 nsContentUtils::eSECURITY_PROPERTIES, aSourceFileURI, 0, 0,
366 "IntegrityMismatch3"_ns, const_cast<const nsTArray<nsString>&>(params));
368 return NS_ERROR_SRI_CORRUPT;
371 uint32_t SRICheckDataVerifier::DataSummaryLength() {
372 MOZ_ASSERT(!mInvalidMetadata);
373 return sizeof(mHashType) + sizeof(mHashLength) + mHashLength;
376 uint32_t SRICheckDataVerifier::EmptyDataSummaryLength() {
377 return sizeof(int8_t) + sizeof(uint32_t);
380 nsresult SRICheckDataVerifier::DataSummaryLength(uint32_t aDataLen,
381 const uint8_t* aData,
382 uint32_t* length) {
383 *length = 0;
384 NS_ENSURE_ARG_POINTER(aData);
386 // we expect to always encode an SRI, even if it is empty or incomplete
387 if (aDataLen < EmptyDataSummaryLength()) {
388 SRILOG(
389 ("SRICheckDataVerifier::DataSummaryLength, encoded length[%u] is too "
390 "small",
391 aDataLen));
392 return NS_ERROR_SRI_IMPORT;
395 // decode the content of the buffer
396 size_t offset = sizeof(mHashType);
397 decltype(mHashLength) len = 0;
398 memcpy(&len, &aData[offset], sizeof(mHashLength));
399 offset += sizeof(mHashLength);
401 SRIVERBOSE(
402 ("SRICheckDataVerifier::DataSummaryLength, header {%x, %x, %x, %x, %x, "
403 "...}",
404 aData[0], aData[1], aData[2], aData[3], aData[4]));
406 if (offset + len > aDataLen) {
407 SRILOG(
408 ("SRICheckDataVerifier::DataSummaryLength, encoded length[%u] overflow "
409 "the buffer size",
410 aDataLen));
411 SRIVERBOSE(("SRICheckDataVerifier::DataSummaryLength, offset[%u], len[%u]",
412 uint32_t(offset), uint32_t(len)));
413 return NS_ERROR_SRI_IMPORT;
415 *length = uint32_t(offset + len);
416 return NS_OK;
419 nsresult SRICheckDataVerifier::ImportDataSummary(uint32_t aDataLen,
420 const uint8_t* aData) {
421 MOZ_ASSERT(!mInvalidMetadata); // mHashType and mHashLength should be valid
422 MOZ_ASSERT(!mCryptoHash); // EnsureCryptoHash should not have been called
423 NS_ENSURE_ARG_POINTER(aData);
424 if (mInvalidMetadata) {
425 return NS_OK; // ignoring any data updates, see mInvalidMetadata usage
428 // we expect to always encode an SRI, even if it is empty or incomplete
429 if (aDataLen < DataSummaryLength()) {
430 SRILOG(
431 ("SRICheckDataVerifier::ImportDataSummary, encoded length[%u] is too "
432 "small",
433 aDataLen));
434 return NS_ERROR_SRI_IMPORT;
437 SRIVERBOSE(
438 ("SRICheckDataVerifier::ImportDataSummary, header {%x, %x, %x, %x, %x, "
439 "...}",
440 aData[0], aData[1], aData[2], aData[3], aData[4]));
442 // decode the content of the buffer
443 size_t offset = 0;
444 decltype(mHashType) hashType;
445 memcpy(&hashType, &aData[offset], sizeof(mHashType));
446 if (hashType != mHashType) {
447 SRILOG(
448 ("SRICheckDataVerifier::ImportDataSummary, hash type[%d] does not "
449 "match[%d]",
450 hashType, mHashType));
451 return NS_ERROR_SRI_UNEXPECTED_HASH_TYPE;
453 offset += sizeof(mHashType);
455 decltype(mHashLength) hashLength;
456 memcpy(&hashLength, &aData[offset], sizeof(mHashLength));
457 if (hashLength != mHashLength) {
458 SRILOG(
459 ("SRICheckDataVerifier::ImportDataSummary, hash length[%d] does not "
460 "match[%d]",
461 hashLength, mHashLength));
462 return NS_ERROR_SRI_UNEXPECTED_HASH_TYPE;
464 offset += sizeof(mHashLength);
466 // copy the hash to mComputedHash, as-if we had finished streaming the bytes
467 mComputedHash.Assign(reinterpret_cast<const char*>(&aData[offset]),
468 mHashLength);
469 mCryptoHash = nullptr;
470 mComplete = true;
471 return NS_OK;
474 nsresult SRICheckDataVerifier::ExportDataSummary(uint32_t aDataLen,
475 uint8_t* aData) {
476 MOZ_ASSERT(!mInvalidMetadata); // mHashType and mHashLength should be valid
477 MOZ_ASSERT(mComplete); // finished streaming
478 NS_ENSURE_ARG_POINTER(aData);
479 NS_ENSURE_TRUE(aDataLen >= DataSummaryLength(), NS_ERROR_INVALID_ARG);
481 // serialize the hash in the buffer
482 size_t offset = 0;
483 memcpy(&aData[offset], &mHashType, sizeof(mHashType));
484 offset += sizeof(mHashType);
485 memcpy(&aData[offset], &mHashLength, sizeof(mHashLength));
486 offset += sizeof(mHashLength);
488 SRIVERBOSE(
489 ("SRICheckDataVerifier::ExportDataSummary, header {%x, %x, %x, %x, %x, "
490 "...}",
491 aData[0], aData[1], aData[2], aData[3], aData[4]));
493 // copy the hash to mComputedHash, as-if we had finished streaming the bytes
494 nsCharTraits<char>::copy(reinterpret_cast<char*>(&aData[offset]),
495 mComputedHash.get(), mHashLength);
496 return NS_OK;
499 nsresult SRICheckDataVerifier::ExportEmptyDataSummary(uint32_t aDataLen,
500 uint8_t* aData) {
501 NS_ENSURE_ARG_POINTER(aData);
502 NS_ENSURE_TRUE(aDataLen >= EmptyDataSummaryLength(), NS_ERROR_INVALID_ARG);
504 // serialize an unknown hash in the buffer, to be able to skip it later
505 size_t offset = 0;
506 memset(&aData[offset], 0, sizeof(mHashType));
507 offset += sizeof(mHashType);
508 memset(&aData[offset], 0, sizeof(mHashLength));
510 SRIVERBOSE(
511 ("SRICheckDataVerifier::ExportEmptyDataSummary, header {%x, %x, %x, %x, "
512 "%x, ...}",
513 aData[0], aData[1], aData[2], aData[3], aData[4]));
515 return NS_OK;
518 } // namespace mozilla::dom