☠☠ backed out by 7f816e3b56be ☠ ☠ | |
author | Franziskus Kiefer <franziskuskiefer@gmail.com> |
Thu, 19 May 2016 10:59:48 +0200 | |
changeset 305858 | 21d8bb5af7b4619ee5c594fa57797aa94f2ac910 |
parent 305857 | 234f9780a4cd5caa3f09cd4d0a7dbcf507d478cd |
child 305859 | aa1eab6436badfd108056b392c06ee1c95e533dc |
push id | 79684 |
push user | franziskuskiefer@gmail.com |
push date | Wed, 20 Jul 2016 15:34:57 +0000 |
treeherder | mozilla-inbound@21d8bb5af7b4 [default view] [failures only] |
perfherder | [talos] [build metrics] [platform microbench] (compared to previous push) |
reviewers | keeler, mayhemer |
bugs | 1263793 |
milestone | 50.0a1 |
first release with | nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
|
last release without | nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
|
--- a/dom/security/ContentVerifier.cpp +++ b/dom/security/ContentVerifier.cpp @@ -2,374 +2,225 @@ /* 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 "ContentVerifier.h" #include "mozilla/fallible.h" #include "mozilla/Logging.h" -#include "mozilla/Preferences.h" -#include "mozilla/StaticPtr.h" -#include "nsCharSeparatedTokenizer.h" +#include "MainThreadUtils.h" #include "nsIInputStream.h" #include "nsIRequest.h" -#include "nssb64.h" -#include "nsSecurityHeaderParser.h" #include "nsServiceManagerUtils.h" #include "nsStringStream.h" -#include "nsThreadUtils.h" using namespace mozilla; static LazyLogModule gContentVerifierPRLog("ContentVerifier"); #define CSV_LOG(args) MOZ_LOG(gContentVerifierPRLog, LogLevel::Debug, args) -// Content-Signature prefix -const nsLiteralCString kPREFIX = NS_LITERAL_CSTRING("Content-Signature:\x00"); - -NS_IMPL_ISUPPORTS(ContentVerifier, nsIStreamListener, nsISupports); +NS_IMPL_ISUPPORTS(ContentVerifier, + nsIContentSignatureReceiverCallback, + nsIStreamListener); nsresult -ContentVerifier::Init(const nsAString& aContentSignatureHeader) +ContentVerifier::Init(const nsACString& aContentSignatureHeader, + nsIRequest* aRequest, nsISupports* aContext) { - mVks = Preferences::GetString("browser.newtabpage.remote.keys"); - - if (aContentSignatureHeader.IsEmpty() || mVks.IsEmpty()) { - CSV_LOG( - ("Content-Signature header and verification keys must not be empty!\n")); + MOZ_ASSERT(NS_IsMainThread()); + if (aContentSignatureHeader.IsEmpty()) { + CSV_LOG(("Content-Signature header must not be empty!\n")); return NS_ERROR_INVALID_SIGNATURE; } - nsresult rv = ParseContentSignatureHeader(aContentSignatureHeader); - NS_ENSURE_SUCCESS(rv, NS_ERROR_INVALID_SIGNATURE); - return CreateContext(); + // initialise the content signature "service" + nsresult rv; + mVerifier = + do_CreateInstance("@mozilla.org/security/contentsignatureverifier;1", &rv); + if (NS_FAILED(rv) || !mVerifier) { + return NS_ERROR_INVALID_SIGNATURE; + } + + // Keep references to the request and context. We need them in FinishSignature + // and the ContextCreated callback. + mContentRequest = aRequest; + mContentContext = aContext; + + return mVerifier->CreateContextWithoutCertChain( + this, aContentSignatureHeader, + NS_LITERAL_CSTRING("remote-newtab-signer.mozilla.org")); } /** * Implement nsIStreamListener * We buffer the entire content here and kick off verification */ NS_METHOD AppendNextSegment(nsIInputStream* aInputStream, void* aClosure, const char* aRawSegment, uint32_t aToOffset, uint32_t aCount, uint32_t* outWrittenCount) { FallibleTArray<nsCString>* decodedData = static_cast<FallibleTArray<nsCString>*>(aClosure); - nsAutoCString segment(aRawSegment, aCount); + nsDependentCSubstring segment(aRawSegment, aCount); if (!decodedData->AppendElement(segment, fallible)) { return NS_ERROR_OUT_OF_MEMORY; } *outWrittenCount = aCount; return NS_OK; } +void +ContentVerifier::FinishSignature() +{ + MOZ_ASSERT(NS_IsMainThread()); + nsCOMPtr<nsIStreamListener> nextListener; + nextListener.swap(mNextListener); + + // Verify the content: + // If this fails, we return an invalid signature error to load a fallback page. + // If everthing is good, we return a new stream to the next listener and kick + // that one off. + bool verified = false; + nsresult rv = NS_OK; + + // If the content signature check fails, stop the load + // and return a signature error. NSS resources are freed by the + // ContentSignatureVerifier on destruction. + if (NS_FAILED(mVerifier->End(&verified)) || !verified) { + CSV_LOG(("failed to verify content\n")); + (void)nextListener->OnStopRequest(mContentRequest, mContentContext, + NS_ERROR_INVALID_SIGNATURE); + return; + } + CSV_LOG(("Successfully verified content signature.\n")); + + // We emptied the input stream so we have to create a new one from mContent + // to hand it to the consuming listener. + uint64_t offset = 0; + for (uint32_t i = 0; i < mContent.Length(); ++i) { + nsCOMPtr<nsIInputStream> oInStr; + rv = NS_NewCStringInputStream(getter_AddRefs(oInStr), mContent[i]); + if (NS_FAILED(rv)) { + break; + } + // let the next listener know that there is data in oInStr + rv = nextListener->OnDataAvailable(mContentRequest, mContentContext, oInStr, + offset, mContent[i].Length()); + offset += mContent[i].Length(); + if (NS_FAILED(rv)) { + break; + } + } + + // propagate OnStopRequest and return + nextListener->OnStopRequest(mContentRequest, mContentContext, rv); +} + NS_IMETHODIMP ContentVerifier::OnStartRequest(nsIRequest* aRequest, nsISupports* aContext) { + MOZ_CRASH("This OnStartRequest should've never been called!"); return NS_OK; } NS_IMETHODIMP ContentVerifier::OnStopRequest(nsIRequest* aRequest, nsISupports* aContext, nsresult aStatus) { - // Verify the content: - // If this fails, we return an invalid signature error to load a fallback page. - // If everthing is good, we return a new stream to the next listener and kick - // that one of. - CSV_LOG(("VerifySignedContent, b64signature: %s\n", mSignature.get())); - CSV_LOG(("VerifySignedContent, key: \n[\n%s\n]\n", mKey.get())); - bool verified = false; - nsresult rv = End(&verified); - if (NS_FAILED(rv) || !verified || NS_FAILED(aStatus)) { - // cancel the request and return error - if (NS_FAILED(aStatus)) { - rv = aStatus; - } else { - rv = NS_ERROR_INVALID_SIGNATURE; - } - CSV_LOG(("failed to verify content\n")); - mNextListener->OnStartRequest(aRequest, aContext); - mNextListener->OnStopRequest(aRequest, aContext, rv); - return NS_ERROR_INVALID_SIGNATURE; - } - CSV_LOG(("Successfully verified content signature.\n")); - - // start the next listener - rv = mNextListener->OnStartRequest(aRequest, aContext); - if (NS_SUCCEEDED(rv)) { - // We emptied aInStr so we have to create a new one from buf to hand it - // to the consuming listener. - for (uint32_t i = 0; i < mContent.Length(); ++i) { - nsCOMPtr<nsIInputStream> oInStr; - rv = NS_NewCStringInputStream(getter_AddRefs(oInStr), mContent[i]); - if (NS_FAILED(rv)) { - break; - } - // let the next listener know that there is data in oInStr - rv = mNextListener->OnDataAvailable(aRequest, aContext, oInStr, 0, - mContent[i].Length()); - if (NS_FAILED(rv)) { - break; - } - } + // If we don't have a next listener, we handed off this request already. + // Return, there's nothing to do here. + if (!mNextListener) { + return NS_OK; } - // propagate OnStopRequest and return - return mNextListener->OnStopRequest(aRequest, aContext, rv); + if (NS_FAILED(aStatus)) { + CSV_LOG(("Stream failed\n")); + nsCOMPtr<nsIStreamListener> nextListener; + nextListener.swap(mNextListener); + return nextListener->OnStopRequest(aRequest, aContext, aStatus); + } + + mContentRead = true; + + // If the ContentSignatureVerifier is initialised, finish the verification. + if (mContextCreated) { + FinishSignature(); + return aStatus; + } + + return NS_OK; } NS_IMETHODIMP ContentVerifier::OnDataAvailable(nsIRequest* aRequest, nsISupports* aContext, nsIInputStream* aInputStream, uint64_t aOffset, uint32_t aCount) { // buffer the entire stream uint32_t read; nsresult rv = aInputStream->ReadSegments(AppendNextSegment, &mContent, aCount, &read); if (NS_FAILED(rv)) { return rv; } - // update the signature verifier - return Update(mContent[mContent.Length()-1]); -} - -/** - * ContentVerifier logic and utils - */ - -nsresult -ContentVerifier::GetVerificationKey(const nsAString& aKeyId) -{ - // get verification keys from the pref and see if we have |aKeyId| - nsCharSeparatedTokenizer tokenizerVK(mVks, ';'); - while (tokenizerVK.hasMoreTokens()) { - nsDependentSubstring token = tokenizerVK.nextToken(); - nsCharSeparatedTokenizer tokenizerKey(token, '='); - nsString prefKeyId; - if (tokenizerKey.hasMoreTokens()) { - prefKeyId = tokenizerKey.nextToken(); - } - nsString key; - if (tokenizerKey.hasMoreTokens()) { - key = tokenizerKey.nextToken(); - } - if (prefKeyId.Equals(aKeyId)) { - mKey.Assign(NS_ConvertUTF16toUTF8(key)); - return NS_OK; - } - } - - // we didn't find the appropriate key - return NS_ERROR_INVALID_SIGNATURE; -} - -nsresult -ContentVerifier::ParseContentSignatureHeader( - const nsAString& aContentSignatureHeader) -{ - // We only support p384 ecdsa according to spec - NS_NAMED_LITERAL_CSTRING(keyid_var, "keyid"); - NS_NAMED_LITERAL_CSTRING(signature_var, "p384ecdsa"); - - nsAutoString contentSignature; - nsAutoString keyId; - nsAutoCString header = NS_ConvertUTF16toUTF8(aContentSignatureHeader); - nsSecurityHeaderParser parser(header.get()); - nsresult rv = parser.Parse(); - if (NS_FAILED(rv)) { - CSV_LOG(("ContentVerifier: could not parse ContentSignature header\n")); - return NS_ERROR_INVALID_SIGNATURE; - } - LinkedList<nsSecurityHeaderDirective>* directives = parser.GetDirectives(); - - for (nsSecurityHeaderDirective* directive = directives->getFirst(); - directive != nullptr; directive = directive->getNext()) { - CSV_LOG(("ContentVerifier: found directive %s\n", directive->mName.get())); - if (directive->mName.Length() == keyid_var.Length() && - directive->mName.EqualsIgnoreCase(keyid_var.get(), - keyid_var.Length())) { - if (!keyId.IsEmpty()) { - CSV_LOG(("ContentVerifier: found two keyIds\n")); - return NS_ERROR_INVALID_SIGNATURE; - } - - CSV_LOG(("ContentVerifier: found a keyid directive\n")); - keyId = NS_ConvertUTF8toUTF16(directive->mValue); - rv = GetVerificationKey(keyId); - NS_ENSURE_SUCCESS(rv, NS_ERROR_INVALID_SIGNATURE); - } - if (directive->mName.Length() == signature_var.Length() && - directive->mName.EqualsIgnoreCase(signature_var.get(), - signature_var.Length())) { - if (!contentSignature.IsEmpty()) { - CSV_LOG(("ContentVerifier: found two ContentSignatures\n")); - return NS_ERROR_INVALID_SIGNATURE; - } - - CSV_LOG(("ContentVerifier: found a ContentSignature directive\n")); - contentSignature = NS_ConvertUTF8toUTF16(directive->mValue); - mSignature = directive->mValue; - } - } - - // we have to ensure that we found a key and a signature at this point - if (mKey.IsEmpty()) { - CSV_LOG(("ContentVerifier: got a Content-Signature header but didn't find " - "an appropriate key.\n")); - return NS_ERROR_INVALID_SIGNATURE; - } - if (mSignature.IsEmpty()) { - CSV_LOG(("ContentVerifier: got a Content-Signature header but didn't find " - "a signature.\n")); - return NS_ERROR_INVALID_SIGNATURE; + // Update the signature verifier if the context has been created. + if (mContextCreated) { + return mVerifier->Update(mContent.LastElement()); } return NS_OK; } -/** - * Parse signature, public key, and algorithm data for input to verification - * functions in VerifyData and CreateContext. - * - * https://datatracker.ietf.org/doc/draft-thomson-http-content-signature/ - * If aSignature is a content signature, the function returns - * NS_ERROR_INVALID_SIGNATURE if anything goes wrong. Only p384 with sha384 - * is supported and aSignature is a raw signature (r||s). - */ -nsresult -ContentVerifier::ParseInput(ScopedSECKEYPublicKey& aPublicKeyOut, - ScopedSECItem& aSignatureItemOut, - SECOidTag& aOidOut, - const nsNSSShutDownPreventionLock&) +NS_IMETHODIMP +ContentVerifier::ContextCreated(bool successful) { - // Base 64 decode the key - ScopedSECItem keyItem(::SECITEM_AllocItem(nullptr, nullptr, 0)); - if (!keyItem || - !NSSBase64_DecodeBuffer(nullptr, keyItem, - mKey.get(), - mKey.Length())) { - return NS_ERROR_INVALID_SIGNATURE; - } + MOZ_ASSERT(NS_IsMainThread()); + if (!successful) { + // If we don't have a next listener, the request has been handed off already. + if (!mNextListener) { + return NS_OK; + } + // Get local reference to mNextListener and null it to ensure that we don't + // call it twice. + nsCOMPtr<nsIStreamListener> nextListener; + nextListener.swap(mNextListener); + + // Make sure that OnStartRequest was called and we have a request. + MOZ_ASSERT(mContentRequest); - // Extract the public key from the keyItem - ScopedCERTSubjectPublicKeyInfo pki( - SECKEY_DecodeDERSubjectPublicKeyInfo(keyItem)); - if (!pki) { - return NS_ERROR_INVALID_SIGNATURE; - } - aPublicKeyOut = SECKEY_ExtractPublicKey(pki.get()); + // In this case something went wrong with the cert. Let's stop this load. + CSV_LOG(("failed to get a valid cert chain\n")); + if (mContentRequest && nextListener) { + mContentRequest->Cancel(NS_ERROR_INVALID_SIGNATURE); + nsresult rv = nextListener->OnStopRequest(mContentRequest, mContentContext, + NS_ERROR_INVALID_SIGNATURE); + mContentRequest = nullptr; + mContentContext = nullptr; + return rv; + } - // in case we were not able to extract a key - if (!aPublicKeyOut) { - return NS_ERROR_INVALID_SIGNATURE; - } - - // Base 64 decode the signature - ScopedSECItem rawSignatureItem(::SECITEM_AllocItem(nullptr, nullptr, 0)); - if (!rawSignatureItem || - !NSSBase64_DecodeBuffer(nullptr, rawSignatureItem, - mSignature.get(), - mSignature.Length())) { - return NS_ERROR_INVALID_SIGNATURE; + // We should never get here! + MOZ_ASSERT_UNREACHABLE( + "ContentVerifier was used without getting OnStartRequest!"); + return NS_OK; } - // get signature object and oid - if (!aSignatureItemOut) { - return NS_ERROR_INVALID_SIGNATURE; - } - // We have a raw ecdsa signature r||s so we have to DER-encode it first - // Note that we have to check rawSignatureItem->len % 2 here as - // DSAU_EncodeDerSigWithLen asserts this - if (rawSignatureItem->len == 0 || rawSignatureItem->len % 2 != 0) { - return NS_ERROR_INVALID_SIGNATURE; - } - if (DSAU_EncodeDerSigWithLen(aSignatureItemOut, rawSignatureItem, - rawSignatureItem->len) != SECSuccess) { - return NS_ERROR_INVALID_SIGNATURE; - } - aOidOut = SEC_OID_ANSIX962_ECDSA_SHA384_SIGNATURE; - - return NS_OK; -} - -/** - * Create a context for a signature verification. - * It sets signature, public key, and algorithms that should be used to verify - * the data. It also updates the verification buffer with the content-signature - * prefix. - */ -nsresult -ContentVerifier::CreateContext() -{ - nsNSSShutDownPreventionLock locker; - if (isAlreadyShutDown()) { - return NS_ERROR_INVALID_SIGNATURE; + // In this case the content verifier is initialised and we have to feed it + // the buffered content. + mContextCreated = true; + for (size_t i = 0; i < mContent.Length(); ++i) { + if (NS_FAILED(mVerifier->Update(mContent[i]))) { + // Bail out if this fails. We can't return an error here, but if this + // failed, NS_ERROR_INVALID_SIGNATURE is returned in FinishSignature. + break; + } } - // Bug 769521: We have to change b64 url to regular encoding as long as we - // don't have a b64 url decoder. This should change soon, but in the meantime - // we have to live with this. - mSignature.ReplaceChar('-', '+'); - mSignature.ReplaceChar('_', '/'); - - ScopedSECKEYPublicKey publicKey; - ScopedSECItem signatureItem(::SECITEM_AllocItem(nullptr, nullptr, 0)); - SECOidTag oid; - nsresult rv = ParseInput(publicKey, signatureItem, oid, locker); - if (NS_FAILED(rv)) { - return NS_ERROR_INVALID_SIGNATURE; - } - - mCx = UniqueVFYContext(VFY_CreateContext(publicKey, signatureItem, oid, NULL)); - if (!mCx) { - return NS_ERROR_INVALID_SIGNATURE; - } - - if (VFY_Begin(mCx.get()) != SECSuccess) { - return NS_ERROR_INVALID_SIGNATURE; - } - - // add the prefix to the verification buffer - return Update(kPREFIX); -} - -/** - * Add data to the context that should be verified. - */ -nsresult -ContentVerifier::Update(const nsACString& aData) -{ - nsNSSShutDownPreventionLock locker; - if (isAlreadyShutDown()) { - return NS_ERROR_INVALID_SIGNATURE; - } - - if (!aData.IsEmpty()) { - if (VFY_Update(mCx.get(), - (const unsigned char*)nsPromiseFlatCString(aData).get(), - aData.Length()) != SECSuccess) { - return NS_ERROR_INVALID_SIGNATURE; - } + // We read all content, let's verify the signature. + if (mContentRead) { + FinishSignature(); } return NS_OK; } - -/** - * Finish signature verification and return the result in _retval. - */ -nsresult -ContentVerifier::End(bool* _retval) -{ - nsNSSShutDownPreventionLock locker; - if (isAlreadyShutDown()) { - return NS_ERROR_INVALID_SIGNATURE; - } - - *_retval = (VFY_End(mCx.get()) == SECSuccess); - - return NS_OK; -} \ No newline at end of file
--- a/dom/security/ContentVerifier.h +++ b/dom/security/ContentVerifier.h @@ -2,97 +2,63 @@ /* 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 mozilla_dom_ContentVerifier_h #define mozilla_dom_ContentVerifier_h #include "nsCOMPtr.h" +#include "nsIContentSignatureVerifier.h" #include "nsIObserver.h" #include "nsIStreamListener.h" -#include "nsNSSShutDown.h" #include "nsString.h" #include "nsTArray.h" -#include "ScopedNSSTypes.h" /** * Mediator intercepting OnStartRequest in nsHttpChannel, blocks until all * data is read from the input stream, verifies the content signature and * releases the request to the next listener if the verification is successful. * If the verification fails or anything else goes wrong, a * NS_ERROR_INVALID_SIGNATURE is thrown. */ class ContentVerifier : public nsIStreamListener - , public nsNSSShutDownObject + , public nsIContentSignatureReceiverCallback { public: NS_DECL_ISUPPORTS NS_DECL_NSISTREAMLISTENER NS_DECL_NSIREQUESTOBSERVER + NS_DECL_NSICONTENTSIGNATURERECEIVERCALLBACK explicit ContentVerifier(nsIStreamListener* aMediatedListener, nsISupports* aMediatedContext) : mNextListener(aMediatedListener) - , mContext(aMediatedContext) - , mCx(nullptr) {} - - nsresult Init(const nsAString& aContentSignatureHeader); + , mContextCreated(false) + , mContentRead(false) {} - // nsNSSShutDownObject - virtual void virtualDestroyNSSReference() override - { - destructorSafeDestroyNSSReference(); - } + nsresult Init(const nsACString& aContentSignatureHeader, nsIRequest* aRequest, + nsISupports* aContext); protected: - virtual ~ContentVerifier() - { - nsNSSShutDownPreventionLock locker; - if (isAlreadyShutDown()) { - return; - } - destructorSafeDestroyNSSReference(); - shutdown(calledFromObject); - } - - void destructorSafeDestroyNSSReference() - { - mCx = nullptr; - } + virtual ~ContentVerifier() {} private: - nsresult ParseContentSignatureHeader(const nsAString& aContentSignatureHeader); - nsresult GetVerificationKey(const nsAString& aKeyId); - - // utility function to parse input before put into verification functions - nsresult ParseInput(mozilla::ScopedSECKEYPublicKey& aPublicKeyOut, - mozilla::ScopedSECItem& aSignatureItemOut, - SECOidTag& aOidOut, - const nsNSSShutDownPreventionLock&); + void FinishSignature(); - // create a verifier context and store it in mCx - nsresult CreateContext(); - - // Adds data to the context that was used to generate the signature. - nsresult Update(const nsACString& aData); - - // Finalises the signature and returns the result of the signature - // verification. - nsresult End(bool* _retval); - + // buffered content to verify + FallibleTArray<nsCString> mContent; // content and next listener for nsIStreamListener nsCOMPtr<nsIStreamListener> mNextListener; - nsCOMPtr<nsISupports> mContext; - - // verifier context for incrementel verifications - mozilla::UniqueVFYContext mCx; - // buffered content to verify - FallibleTArray<nsCString> mContent; - // signature to verify - nsCString mSignature; - // verification key - nsCString mKey; - // verification key preference - nsString mVks; + // the verifier + nsCOMPtr<nsIContentSignatureVerifier> mVerifier; + // holding a pointer to the content request and context to resume/cancel it + nsCOMPtr<nsIRequest> mContentRequest; + nsCOMPtr<nsISupports> mContentContext; + // Semaphors to indicate that the verifying context was created, the entire + // content was read resp. The context gets created by ContentSignatureVerifier + // and mContextCreated is set in the ContextCreated callback. The content is + // read, i.e. mContentRead is set, when the content OnStopRequest is called. + bool mContextCreated; + bool mContentRead; }; #endif /* mozilla_dom_ContentVerifier_h */
--- a/dom/security/nsCSPUtils.cpp +++ b/dom/security/nsCSPUtils.cpp @@ -1,15 +1,16 @@ /* -*- 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 "nsAttrValue.h" +#include "nsCharSeparatedTokenizer.h" #include "nsContentUtils.h" #include "nsCSPUtils.h" #include "nsDebug.h" #include "nsIConsoleService.h" #include "nsICryptoHash.h" #include "nsIScriptError.h" #include "nsIServiceManager.h" #include "nsIStringBundle.h"
--- a/dom/security/test/contentverifier/browser.ini +++ b/dom/security/test/contentverifier/browser.ini @@ -5,12 +5,15 @@ support-files = file_about_newtab_bad.html file_about_newtab_bad_csp.html file_about_newtab_bad_csp_signature file_about_newtab_good_signature file_about_newtab_bad_signature file_about_newtab_broken_signature file_about_newtab_sri.html file_about_newtab_sri_signature + goodChain.pem + head.js script.js style.css [browser_verify_content_about_newtab.js] +[browser_verify_content_about_newtab2.js]
--- a/dom/security/test/contentverifier/browser_verify_content_about_newtab.js +++ b/dom/security/test/contentverifier/browser_verify_content_about_newtab.js @@ -1,243 +1,20 @@ -/* - * Test Content-Signature for remote about:newtab - * - Bug 1226928 - allow about:newtab to load remote content - * - * This tests content-signature verification on remote about:newtab in the - * following cases (see TESTS, all failed loads display about:blank fallback): - * - good case (signature should verify and correct page is displayed) - * - reload of newtab when the siganture was invalidated after the last correct - * load - * - malformed content-signature header - * - malformed keyid directive - * - malformed p384ecdsa directive - * - wrong signature (this is not a siganture for the delivered document) - * - invalid signature (this is not even a signature) - * - loading a file that doesn't fit the key or signature - * - cache poisoning (load a malicious remote page not in newtab, subsequent - * newtab load has to load the fallback) - */ - -const ABOUT_NEWTAB_URI = "about:newtab"; - -const BASE = "https://example.com/browser/dom/security/test/contentverifier/file_contentserver.sjs?"; -const URI_GOOD = BASE + "sig=good&key=good&file=good&header=good"; - -const INVALIDATE_FILE = BASE + "invalidateFile=yep"; -const VALIDATE_FILE = BASE + "validateFile=yep"; - -const URI_HEADER_BASE = BASE + "sig=good&key=good&file=good&header="; -const URI_ERROR_HEADER = URI_HEADER_BASE + "error"; -const URI_KEYERROR_HEADER = URI_HEADER_BASE + "errorInKeyid"; -const URI_SIGERROR_HEADER = URI_HEADER_BASE + "errorInSignature"; -const URI_NO_HEADER = URI_HEADER_BASE + "noHeader"; - -const URI_BAD_SIG = BASE + "sig=bad&key=good&file=good&header=good"; -const URI_BROKEN_SIG = BASE + "sig=broken&key=good&file=good&header=good"; -const URI_BAD_KEY = BASE + "sig=good&key=bad&file=good&header=good"; -const URI_BAD_FILE = BASE + "sig=good&key=good&file=bad&header=good"; -const URI_BAD_ALL = BASE + "sig=bad&key=bad&file=bad&header=bad"; -const URI_BAD_CSP = BASE + "sig=bad-csp&key=good&file=bad-csp&header=good"; - -const URI_BAD_FILE_CACHED = BASE + "sig=good&key=good&file=bad&header=good&cached=true"; - -const GOOD_ABOUT_STRING = "Just a fully good testpage for Bug 1226928"; -const BAD_ABOUT_STRING = "Just a bad testpage for Bug 1226928"; -const ABOUT_BLANK = "<head></head><body></body>"; - -const URI_CLEANUP = BASE + "cleanup=true"; -const CLEANUP_DONE = "Done"; - -const URI_SRI = BASE + "sig=sri&key=good&file=sri&header=good"; -const STYLESHEET_WITHOUT_SRI_BLOCKED = "Stylesheet without SRI blocked"; -const STYLESHEET_WITH_SRI_BLOCKED = "Stylesheet with SRI blocked"; -const STYLESHEET_WITH_SRI_LOADED = "Stylesheet with SRI loaded"; -const SCRIPT_WITHOUT_SRI_BLOCKED = "Script without SRI blocked"; -const SCRIPT_WITH_SRI_BLOCKED = "Script with SRI blocked"; -const SCRIPT_WITH_SRI_LOADED = "Script with SRI loaded"; - -const CSP_TEST_SUCCESS_STRING = "CSP violation test succeeded."; - -// Needs to sync with pref "security.signed_content.CSP.default". -const SIGNED_CONTENT_CSP = `{"csp-policies":[{"report-only":false,"script-src":["https://example.com","'unsafe-inline'"],"style-src":["https://example.com"]}]}`; const TESTS = [ // { newtab (aboutURI) or regular load (url) : url, // testStrings : expected strings in the loaded page } { "aboutURI" : URI_GOOD, "testStrings" : [GOOD_ABOUT_STRING] }, { "aboutURI" : URI_ERROR_HEADER, "testStrings" : [ABOUT_BLANK] }, - { "aboutURI" : URI_KEYERROR_HEADER, "testStrings" : [ABOUT_BLANK] }, - { "aboutURI" : URI_SIGERROR_HEADER, "testStrings" : [ABOUT_BLANK] }, - { "aboutURI" : URI_NO_HEADER, "testStrings" : [ABOUT_BLANK] }, - { "aboutURI" : URI_BAD_SIG, "testStrings" : [ABOUT_BLANK] }, - { "aboutURI" : URI_BROKEN_SIG, "testStrings" : [ABOUT_BLANK] }, - { "aboutURI" : URI_BAD_KEY, "testStrings" : [ABOUT_BLANK] }, - { "aboutURI" : URI_BAD_FILE, "testStrings" : [ABOUT_BLANK] }, - { "aboutURI" : URI_BAD_ALL, "testStrings" : [ABOUT_BLANK] }, { "url" : URI_BAD_FILE_CACHED, "testStrings" : [BAD_ABOUT_STRING] }, { "aboutURI" : URI_BAD_FILE_CACHED, "testStrings" : [ABOUT_BLANK] }, { "aboutURI" : URI_GOOD, "testStrings" : [GOOD_ABOUT_STRING] }, { "aboutURI" : URI_SRI, "testStrings" : [ STYLESHEET_WITHOUT_SRI_BLOCKED, STYLESHEET_WITH_SRI_LOADED, SCRIPT_WITHOUT_SRI_BLOCKED, SCRIPT_WITH_SRI_LOADED, ]}, { "aboutURI" : URI_BAD_CSP, "testStrings" : [CSP_TEST_SUCCESS_STRING] }, { "url" : URI_CLEANUP, "testStrings" : [CLEANUP_DONE] }, ]; -var browser = null; -var aboutNewTabService = Cc["@mozilla.org/browser/aboutnewtab-service;1"] - .getService(Ci.nsIAboutNewTabService); - -function pushPrefs(...aPrefs) { - return new Promise((resolve) => { - SpecialPowers.pushPrefEnv({"set": aPrefs}, resolve); - }); -} - -/* - * run tests with input from TESTS - */ -function doTest(aExpectedStrings, reload, aUrl, aNewTabPref) { - // set about:newtab location for this test if it's a newtab test - if (aNewTabPref) { - aboutNewTabService.newTabURL = aNewTabPref; - } - - // set prefs - yield pushPrefs( - ["browser.newtabpage.remote.content-signing-test", true], - ["browser.newtabpage.remote", true], [ - "browser.newtabpage.remote.keys", - "RemoteNewTabNightlyv0=BO9QHuP6E2eLKybql8iuD4o4Np9YFDfW3D+k" + - "a70EcXXTqZcikc7Am1CwyP1xBDTpEoe6gb9SWzJmaDW3dNh1av2u90VkUM" + - "B7aHIrImjTjLNg/1oC8GRcTKM4+WzbKF00iA==;OtherKey=eKQJ2fNSId" + - "CFzL6N326EzZ/5LCeFU5eyq3enwZ5MLmvOw+3gycr4ZVRc36/EiSPsQYHE" + - "3JvJs1EKs0QCaguHFOZsHwqXMPicwp/gLdeYbuOmN2s1SEf/cxw8GtcxSA" + - "kG;RemoteNewTab=MHYwEAYHKoZIzj0CAQYFK4EEACIDYgAE4k3FmG7dFo" + - "Ot3Tuzl76abTRtK8sb/r/ibCSeVKa96RbrOX2ciscz/TT8wfqBYS/8cN4z" + - "Me1+f7wRmkNrCUojZR1ZKmYM2BeiUOMlMoqk2O7+uwsn1DwNQSYP58TkvZt6" - ]); - - if (aNewTabPref === URI_BAD_CSP) { - // Use stricter CSP to test CSP violation. - yield pushPrefs(["security.signed_content.CSP.default", "script-src 'self'; style-src 'self'"]); - } else { - // Use weaker CSP to test normal content. - yield pushPrefs(["security.signed_content.CSP.default", "script-src 'self' 'unsafe-inline'; style-src 'self'"]); - } - - // start the test - yield BrowserTestUtils.withNewTab({ - gBrowser, - url: aUrl, - }, - function * (browser) { - // check if everything's set correct for testing - ok(Services.prefs.getBoolPref( - "browser.newtabpage.remote.content-signing-test"), - "sanity check: remote newtab signing test should be used"); - ok(Services.prefs.getBoolPref("browser.newtabpage.remote"), - "sanity check: remote newtab should be used"); - // we only check this if we really do a newtab test - if (aNewTabPref) { - ok(aboutNewTabService.overridden, - "sanity check: default URL for about:newtab should be overriden"); - is(aboutNewTabService.newTabURL, aNewTabPref, - "sanity check: default URL for about:newtab should return the new URL"); - } - - // Every valid remote newtab page must have built-in CSP. - let shouldHaveCSP = ((aUrl === ABOUT_NEWTAB_URI) && - (aNewTabPref === URI_GOOD || aNewTabPref === URI_SRI)); - - if (shouldHaveCSP) { - is(browser.contentDocument.nodePrincipal.cspJSON, SIGNED_CONTENT_CSP, - "Valid remote newtab page must have built-in CSP."); - } - - yield ContentTask.spawn( - browser, aExpectedStrings, function * (aExpectedStrings) { - for (let expectedString of aExpectedStrings) { - ok(content.document.documentElement.innerHTML.includes(expectedString), - "Expect the following value in the result\n" + expectedString + - "\nand got " + content.document.documentElement.innerHTML); - } - }); - - // for good test cases we check if a reload fails if the remote page - // changed from valid to invalid in the meantime - if (reload) { - yield BrowserTestUtils.withNewTab({ - gBrowser, - url: INVALIDATE_FILE, - }, - function * (browser2) { - yield ContentTask.spawn(browser2, null, function * () { - ok(content.document.documentElement.innerHTML.includes("Done"), - "Expect the following value in the result\n" + "Done" + - "\nand got " + content.document.documentElement.innerHTML); - }); - } - ); - - browser.reload(); - yield BrowserTestUtils.browserLoaded(browser); - - let expectedStrings = [ABOUT_BLANK]; - if (aNewTabPref == URI_SRI) { - expectedStrings = [ - STYLESHEET_WITHOUT_SRI_BLOCKED, - STYLESHEET_WITH_SRI_BLOCKED, - SCRIPT_WITHOUT_SRI_BLOCKED, - SCRIPT_WITH_SRI_BLOCKED - ]; - } - yield ContentTask.spawn(browser, expectedStrings, - function * (expectedStrings) { - for (let expectedString of expectedStrings) { - ok(content.document.documentElement.innerHTML.includes(expectedString), - "Expect the following value in the result\n" + expectedString + - "\nand got " + content.document.documentElement.innerHTML); - } - } - ); - - yield BrowserTestUtils.withNewTab({ - gBrowser, - url: VALIDATE_FILE, - }, - function * (browser2) { - yield ContentTask.spawn(browser2, null, function * () { - ok(content.document.documentElement.innerHTML.includes("Done"), - "Expect the following value in the result\n" + "Done" + - "\nand got " + content.document.documentElement.innerHTML); - }); - } - ); - } - } - ); -} - -add_task(function * test() { - // run tests from TESTS - for (let i = 0; i < TESTS.length; i++) { - let testCase = TESTS[i]; - let url = "", aNewTabPref = ""; - let reload = false; - var aExpectedStrings = testCase.testStrings; - if (testCase.aboutURI) { - url = ABOUT_NEWTAB_URI; - aNewTabPref = testCase.aboutURI; - if (aNewTabPref == URI_GOOD || aNewTabPref == URI_SRI) { - reload = true; - } - } else { - url = testCase.url; - } - - yield doTest(aExpectedStrings, reload, url, aNewTabPref); - } -}); +add_task(runTests);
new file mode 100644 --- /dev/null +++ b/dom/security/test/contentverifier/browser_verify_content_about_newtab2.js @@ -0,0 +1,19 @@ + +const TESTS = [ + // { newtab (aboutURI) or regular load (url) : url, + // testStrings : expected strings in the loaded page } + { "aboutURI" : URI_GOOD, "testStrings" : [GOOD_ABOUT_STRING] }, + { "aboutURI" : URI_ERROR_HEADER, "testStrings" : [ABOUT_BLANK] }, + { "aboutURI" : URI_KEYERROR_HEADER, "testStrings" : [ABOUT_BLANK] }, + { "aboutURI" : URI_SIGERROR_HEADER, "testStrings" : [ABOUT_BLANK] }, + { "aboutURI" : URI_NO_HEADER, "testStrings" : [ABOUT_BLANK] }, + { "aboutURI" : URI_BAD_SIG, "testStrings" : [ABOUT_BLANK] }, + { "aboutURI" : URI_BROKEN_SIG, "testStrings" : [ABOUT_BLANK] }, + { "aboutURI" : URI_BAD_X5U, "testStrings" : [ABOUT_BLANK] }, + { "aboutURI" : URI_HTTP_X5U, "testStrings" : [ABOUT_BLANK] }, + { "aboutURI" : URI_BAD_FILE, "testStrings" : [ABOUT_BLANK] }, + { "aboutURI" : URI_BAD_ALL, "testStrings" : [ABOUT_BLANK] }, + { "url" : URI_CLEANUP, "testStrings" : [CLEANUP_DONE] }, +]; + +add_task(runTests);
--- a/dom/security/test/contentverifier/file_about_newtab_bad_csp_signature +++ b/dom/security/test/contentverifier/file_about_newtab_bad_csp_signature @@ -1,1 +1,1 @@ -8qXVAqzuF3TsF6C750u_v_JiRu90WJXf_0xT9x0S4Fgmvolgtfu-KSWq3lYpmk2dxO8u64zaHM3iguZdWAqcSL82RFtV7OPiprt16omCbHCKfVi-Bt_rXILRlexgmRl_ \ No newline at end of file +oiypz3lb-IyJsmKNsnlp2zDrqncste8yONn9WUE6ksgJWMhSEQ9lp8vRqN0W3JPwJb6uSk16RI-tDv7uy0jxon5jL1BZpqlqIpvimg7FCQEedMKoHZwtE9an-e95sOTd \ No newline at end of file
--- a/dom/security/test/contentverifier/file_about_newtab_good_signature +++ b/dom/security/test/contentverifier/file_about_newtab_good_signature @@ -1,1 +1,1 @@ -XBKzej3i6TAFZc3VZsuCekn-4dYWJBE4-b3OOtKrOV-JIzIvAnAhnOV1aj-kEm07kh-FciIxV-Xk2QUQlRQzHO7oW7E4mXkMKkbbAcvL0CFrItTObhfhKnBnpAE9ql1O \ No newline at end of file +-mqpvTYdZX4HYQDW1nScojL7ICw5yj8UF2gzxyLbSCx9UIfHH-gWZ40F_PFtqjHxoC1J3dHDb3VedVhOYczdaLrNKbRvPrlnkdGx7Rl8qEBrtZpF1py1Z9uAGoCrgUHa \ No newline at end of file
--- a/dom/security/test/contentverifier/file_about_newtab_sri_signature +++ b/dom/security/test/contentverifier/file_about_newtab_sri_signature @@ -1,1 +1,1 @@ -i5jOnrZWwyNwrTcIjfJ6fUR-8MhhvhtMvQbdrUD7j8aHTybNolv25v9NwJAT6rVU6kgkxmD_st9Kla086CQmzYQdLhKfzgLbTDXz0-1j23fQnyjsP1_4MNIu2xTea11p \ No newline at end of file +yoIyAYiiEzdP1zpkRy3KaqdsjUy62Notku89cytwVwcH0x6fKsMCdM-df1wbk9N28CSTaIOW5kcSenFy5K3nU-zPIoqZDjQo6aSjF8hF6lrw1a1xbhfl9K3g4YJsuWsO \ No newline at end of file
--- a/dom/security/test/contentverifier/file_contentserver.sjs +++ b/dom/security/test/contentverifier/file_contentserver.sjs @@ -1,37 +1,51 @@ +/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */ +/* 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/. */ // sjs for remote about:newtab (bug 1226928) +"use strict"; const {classes: Cc, interfaces: Ci, utils: Cu} = Components; Cu.import("resource://gre/modules/NetUtil.jsm"); Cu.import("resource://gre/modules/FileUtils.jsm"); Cu.importGlobalProperties(["URLSearchParams"]); const path = "browser/dom/security/test/contentverifier/"; const goodFileName = "file_about_newtab.html"; const goodFileBase = path + goodFileName; const goodFile = FileUtils.getDir("TmpD", [], true); goodFile.append(goodFileName); const goodSignature = path + "file_about_newtab_good_signature"; -const goodKeyId = "RemoteNewTab"; +const goodX5UString = "\"https://example.com/browser/dom/security/test/contentverifier/file_contentserver.sjs?x5u=default\""; const scriptFileName = "script.js"; const cssFileName = "style.css"; const badFile = path + "file_about_newtab_bad.html"; const brokenSignature = path + "file_about_newtab_broken_signature"; const badSignature = path + "file_about_newtab_bad_signature"; -const badKeyId = "OldRemoteNewTabKey"; +const badX5UString = "\"https://example.com/browser/dom/security/test/contentverifier/file_contentserver.sjs?x5u=bad\""; +const httpX5UString = "\"http://example.com/browser/dom/security/test/contentverifier/file_contentserver.sjs?x5u=default\""; const sriFile = path + "file_about_newtab_sri.html"; const sriSignature = path + "file_about_newtab_sri_signature"; const badCspFile = path + "file_about_newtab_bad_csp.html"; const badCspSignature = path + "file_about_newtab_bad_csp_signature"; +// This cert chain is copied from +// security/manager/ssl/tests/unit/test_content_signing/ +// using the certificates +// * content_signing_remote_newtab_ee.pem +// * content_signing_int.pem +// * content_signing_root.pem +const goodCertChainPath = path + "goodChain.pem"; + const tempFileNames = [goodFileName, scriptFileName, cssFileName]; // we copy the file to serve as newtab to a temp directory because // we modify it during tests. setupTestFiles(); function setupTestFiles() { for (let fileName of tempFileNames) { @@ -110,24 +124,25 @@ function cleanupTestFiles() { * sig=good&key=good&file=good&header=good&cached=no to serve pages with * content signatures * * it further handles invalidateFile=yep and validateFile=yep to change the * served file */ function handleRequest(request, response) { let params = new URLSearchParams(request.queryString); - let keyType = params.get("key"); + let x5uType = params.get("x5u"); let signatureType = params.get("sig"); let fileType = params.get("file"); let headerType = params.get("header"); let cached = params.get("cached"); let invalidateFile = params.get("invalidateFile"); let validateFile = params.get("validateFile"); let resource = params.get("resource"); + let x5uParam = params.get("x5u"); if (params.get("cleanup")) { cleanupTestFiles(); response.setHeader("Content-Type", "text/html", false); response.write("Done"); return; } @@ -166,36 +181,46 @@ function handleRequest(request, response r = "Error"; } } response.setHeader("Content-Type", "text/html", false); response.write(r); return; } + // we have to return the certificate chain on request for the x5u parameter + if (x5uParam && x5uParam == "default") { + response.setHeader("Cache-Control", "max-age=216000", false); + response.setHeader("Content-Type", "text/plain", false); + response.write(loadFile(getFileName(goodCertChainPath, "CurWorkD"))); + return; + } + // avoid confusing cache behaviours if (!cached) { response.setHeader("Cache-Control", "no-cache", false); } else { response.setHeader("Cache-Control", "max-age=3600", false); } // send HTML to test allowed/blocked behaviours response.setHeader("Content-Type", "text/html", false); // set signature header and key for Content-Signature header /* By default a good content-signature header is returned. Any broken return * value has to be indicated in the url. */ let csHeader = ""; - let keyId = goodKeyId; + let x5uString = goodX5UString; let signature = goodSignature; let file = goodFile; - if (keyType == "bad") { - keyId = badKeyId; + if (x5uType == "bad") { + x5uString = badX5UString; + } else if (x5uType == "http") { + x5uString = httpX5UString; } if (signatureType == "bad") { signature = badSignature; } else if (signatureType == "broken") { signature = brokenSignature; } else if (signatureType == "sri") { signature = sriSignature; } else if (signatureType == "bad-csp") { @@ -206,29 +231,29 @@ function handleRequest(request, response } else if (fileType == "sri") { file = getFileName(sriFile, "CurWorkD"); } else if (fileType == "bad-csp") { file = getFileName(badCspFile, "CurWorkD"); } if (headerType == "good") { // a valid content-signature header - csHeader = "keyid=" + keyId + ";p384ecdsa=" + + csHeader = "x5u=" + x5uString + ";p384ecdsa=" + loadFile(getFileName(signature, "CurWorkD")); } else if (headerType == "error") { // this content-signature header is missing ; before p384ecdsa - csHeader = "keyid=" + keyId + "p384ecdsa=" + + csHeader = "x5u=" + x5uString + "p384ecdsa=" + loadFile(getFileName(signature, "CurWorkD")); - } else if (headerType == "errorInKeyid") { + } else if (headerType == "errorInX5U") { // this content-signature header is missing the keyid directive - csHeader = "keid=" + keyId + ";p384ecdsa=" + + csHeader = "x6u=" + x5uString + ";p384ecdsa=" + loadFile(getFileName(signature, "CurWorkD")); } else if (headerType == "errorInSignature") { // this content-signature header is missing the p384ecdsa directive - csHeader = "keyid=" + keyId + ";p385ecdsa=" + + csHeader = "x5u=" + x5uString + ";p385ecdsa=" + loadFile(getFileName(signature, "CurWorkD")); } if (csHeader) { response.setHeader("Content-Signature", csHeader, false); } let result = loadFile(file);
new file mode 100644 --- /dev/null +++ b/dom/security/test/contentverifier/goodChain.pem @@ -0,0 +1,51 @@ +-----BEGIN CERTIFICATE----- +MIICSTCCATOgAwIBAgIUWQzTTfKLNZgX5ngi/ENiI2DO2kowCwYJKoZIhvcNAQEL +MBExDzANBgNVBAMMBmludC1DQTAiGA8yMDE0MTEyNzAwMDAwMFoYDzIwMTcwMjA0 +MDAwMDAwWjAUMRIwEAYDVQQDDAllZS1pbnQtQ0EwdjAQBgcqhkjOPQIBBgUrgQQA +IgNiAAShaHJDNitcexiJ83kVRhWhxz+0je6GPgIpFdtgjiUt5LcTLajOmOgxU05q +nAwLCcjWOa3oMgbluoE0c6EfozDgXajJbkOD/ieHPalxA74oiM/wAvBa9xof3cyD +dKpuqc6jRDBCMBMGA1UdJQQMMAoGCCsGAQUFBwMDMCsGA1UdEQQkMCKCIHJlbW90 +ZS1uZXd0YWItc2lnbmVyLm1vemlsbGEub3JnMAsGCSqGSIb3DQEBCwOCAQEAc2nE +feYpA8WFyiPfZi56NgVgc8kXSKRNgplDtBHXK7gT7ICNQTSKkt+zHxnS9tAoXoix +OGKsyp/8LNIYGMr4vHVNyOGnxuiLzAYjmDxXhp3t36xOFlU5Y7UaKf9G4feMXrNH ++q1SPYlP84keo1MaC5yhTZTTmJMKkRBsCbIVhfDnL3BUczxVZmk9F+7qK/trL222 +RoAaTZW5hdXUZrX630CYs1sQHWgL0B5rg2y9bwFk7toQ34JbjS0Z25e/MZUtFz19 +5tSjAZQHlLE6fAYZ3knrxF9xVMJCZf7gQqVphJzBtgy9yvTAtlMsrf6XS6sRRngz +27HBxIpd4tYniYrtfg== +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIIC0TCCAbugAwIBAgIULYyr3v/0zZ+XiR22NH7hOcnj2FcwCwYJKoZIhvcNAQEL +MA0xCzAJBgNVBAMMAmNhMCIYDzIwMTQxMTI3MDAwMDAwWhgPMjAxNzAyMDQwMDAw +MDBaMBExDzANBgNVBAMMBmludC1DQTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCC +AQoCggEBALqIUahEjhbWQf1utogGNhA9PBPZ6uQ1SrTs9WhXbCR7wcclqODYH72x +nAabbhqG8mvir1p1a2pkcQh6pVqnRYf3HNUknAJ+zUP8HmnQOCApk6sgw0nk27lM +wmtsDu0Vgg/xfq1pGrHTAjqLKkHup3DgDw2N/WYLK7AkkqR9uYhheZCxV5A90jvF +4LhIH6g304hD7ycW2FW3ZlqqfgKQLzp7EIAGJMwcbJetlmFbt+KWEsB1MaMMkd20 +yvf8rR0l0wnvuRcOp2jhs3svIm9p47SKlWEd7ibWJZ2rkQhONsscJAQsvxaLL+Xx +j5kXMbiz/kkj+nJRxDHVA6zaGAo17Y0CAwEAAaMlMCMwDAYDVR0TBAUwAwEB/zAT +BgNVHSUEDDAKBggrBgEFBQcDAzALBgkqhkiG9w0BAQsDggEBADfRBKSM08JF6vqz +0EA+KNc0XIEAWApuHuwX6XXWeLgo6QN4E/9qfrsaO+C366WT+JDsjDOi40wW46SA +XbguxtZQeZasNDUWp/leZix4RSJoHB7OllG1rgZJfN76zKVaXRGUmyQObkMMOJZe +wIA0OBURT8ik9Z89pD0IWrqscds71Edfjt0hHgg63wVvIaklReZXvFOD3VmSCPNn +2wB6ZzECcbhJpnzxZdsoMSGH0C6apYnNNTjqZjO90JVm/Ph/7nbi/KncYXA6ccl6 +Jz2mfiAquWIua2+CzBGbqjZVSATTpWCp+cXQJE1xka+hWUaL5HPTq1bTULRFlauZ +HGl5lJk= +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIICzTCCAbegAwIBAgIUIVkGGA8HiO3RIKGjdOjVi+d6EVkwCwYJKoZIhvcNAQEL +MA0xCzAJBgNVBAMMAmNhMCIYDzIwMTQxMTI3MDAwMDAwWhgPMjAxNzAyMDQwMDAw +MDBaMA0xCzAJBgNVBAMMAmNhMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKC +AQEAuohRqESOFtZB/W62iAY2ED08E9nq5DVKtOz1aFdsJHvBxyWo4NgfvbGcBptu +Gobya+KvWnVramRxCHqlWqdFh/cc1SScAn7NQ/weadA4ICmTqyDDSeTbuUzCa2wO +7RWCD/F+rWkasdMCOosqQe6ncOAPDY39ZgsrsCSSpH25iGF5kLFXkD3SO8XguEgf +qDfTiEPvJxbYVbdmWqp+ApAvOnsQgAYkzBxsl62WYVu34pYSwHUxowyR3bTK9/yt +HSXTCe+5Fw6naOGzey8ib2njtIqVYR3uJtYlnauRCE42yxwkBCy/Fosv5fGPmRcx +uLP+SSP6clHEMdUDrNoYCjXtjQIDAQABoyUwIzAMBgNVHRMEBTADAQH/MBMGA1Ud +JQQMMAoGCCsGAQUFBwMDMAsGCSqGSIb3DQEBCwOCAQEAlpbRzRIPnf43AwGfMvKP +zOtntRy2nE9GlmY9I00uioHUnUrPLs8aw3UDtyiDWMGqcYysXGx9EX2Vk0POS4gf +G6PA95F6GxTtbzIEZmTPVuzA/cfc9HU3HXDPqh+dySJ8/Ta4c4vX1lgeGGAvstNe +q+9DaCGXs8MqMF8KtXNmOm3eS9q622hKEvTVEoxqj1t365kwKHaNpbObddQ6Xcny +akvfh2L+8QbJSflcm8fL/JTup/2/cRG1ytOsaiXEr9JBEITOtQO0Ot/4Qzq+MJjv +weaJ3hZ0c+cTy3tEvt+I7+lnW4Q5dB7aLR2/BZfLubhxz1SUVMuHfLH64fc0Uf1Q +Nw== +-----END CERTIFICATE-----
new file mode 100644 --- /dev/null +++ b/dom/security/test/contentverifier/head.js @@ -0,0 +1,210 @@ +/* + * Test Content-Signature for remote about:newtab + * - Bug 1226928 - allow about:newtab to load remote content + * + * This tests content-signature verification on remote about:newtab in the + * following cases (see TESTS, all failed loads display about:blank fallback): + * - good case (signature should verify and correct page is displayed) + * - reload of newtab when the siganture was invalidated after the last correct + * load + * - malformed content-signature header + * - malformed keyid directive + * - malformed p384ecdsa directive + * - wrong signature (this is not a siganture for the delivered document) + * - invalid signature (this is not even a signature) + * - loading a file that doesn't fit the key or signature + * - cache poisoning (load a malicious remote page not in newtab, subsequent + * newtab load has to load the fallback) + */ + +const ABOUT_NEWTAB_URI = "about:newtab"; + +const BASE = "https://example.com/browser/dom/security/test/contentverifier/file_contentserver.sjs?"; +const URI_GOOD = BASE + "sig=good&x5u=good&file=good&header=good"; + +const INVALIDATE_FILE = BASE + "invalidateFile=yep"; +const VALIDATE_FILE = BASE + "validateFile=yep"; + +const URI_HEADER_BASE = BASE + "sig=good&x5u=good&file=good&header="; +const URI_ERROR_HEADER = URI_HEADER_BASE + "error"; +const URI_KEYERROR_HEADER = URI_HEADER_BASE + "errorInX5U"; +const URI_SIGERROR_HEADER = URI_HEADER_BASE + "errorInSignature"; +const URI_NO_HEADER = URI_HEADER_BASE + "noHeader"; + +const URI_BAD_SIG = BASE + "sig=bad&x5u=good&file=good&header=good"; +const URI_BROKEN_SIG = BASE + "sig=broken&x5u=good&file=good&header=good"; +const URI_BAD_X5U = BASE + "sig=good&x5u=bad&file=good&header=good"; +const URI_HTTP_X5U = BASE + "sig=good&x5u=http&file=good&header=good"; +const URI_BAD_FILE = BASE + "sig=good&x5u=good&file=bad&header=good"; +const URI_BAD_ALL = BASE + "sig=bad&x5u=bad&file=bad&header=bad"; +const URI_BAD_CSP = BASE + "sig=bad-csp&x5u=good&file=bad-csp&header=good"; + +const URI_BAD_FILE_CACHED = BASE + "sig=good&x5u=good&file=bad&header=good&cached=true"; + +const GOOD_ABOUT_STRING = "Just a fully good testpage for Bug 1226928"; +const BAD_ABOUT_STRING = "Just a bad testpage for Bug 1226928"; +const ABOUT_BLANK = "<head></head><body></body>"; + +const URI_CLEANUP = BASE + "cleanup=true"; +const CLEANUP_DONE = "Done"; + +const URI_SRI = BASE + "sig=sri&x5u=good&file=sri&header=good"; +const STYLESHEET_WITHOUT_SRI_BLOCKED = "Stylesheet without SRI blocked"; +const STYLESHEET_WITH_SRI_BLOCKED = "Stylesheet with SRI blocked"; +const STYLESHEET_WITH_SRI_LOADED = "Stylesheet with SRI loaded"; +const SCRIPT_WITHOUT_SRI_BLOCKED = "Script without SRI blocked"; +const SCRIPT_WITH_SRI_BLOCKED = "Script with SRI blocked"; +const SCRIPT_WITH_SRI_LOADED = "Script with SRI loaded"; + +const CSP_TEST_SUCCESS_STRING = "CSP violation test succeeded."; + +// Needs to sync with pref "security.signed_content.CSP.default". +const SIGNED_CONTENT_CSP = `{"csp-policies":[{"report-only":false,"script-src":["https://example.com","'unsafe-inline'"],"style-src":["https://example.com"]}]}`; + +var browser = null; +var aboutNewTabService = Cc["@mozilla.org/browser/aboutnewtab-service;1"] + .getService(Ci.nsIAboutNewTabService); + +function pushPrefs(...aPrefs) { + return new Promise((resolve) => { + SpecialPowers.pushPrefEnv({"set": aPrefs}, resolve); + }); +} + +/* + * run tests with input from TESTS + */ +function doTest(aExpectedStrings, reload, aUrl, aNewTabPref) { + // set about:newtab location for this test if it's a newtab test + if (aNewTabPref) { + aboutNewTabService.newTabURL = aNewTabPref; + } + + // set prefs + yield pushPrefs( + ["browser.newtabpage.remote.content-signing-test", true], + ["browser.newtabpage.remote", true], + ["security.content.signature.root_hash", + "65:AE:D8:1E:B5:12:AE:B0:6B:38:58:BC:7C:47:35:3D:D4:EA:25:F1:63:DA:08:BB:86:3A:2E:97:39:66:8F:55"]); + + if (aNewTabPref === URI_BAD_CSP) { + // Use stricter CSP to test CSP violation. + yield pushPrefs(["security.signed_content.CSP.default", "script-src 'self'; style-src 'self'"]); + } else { + // Use weaker CSP to test normal content. + yield pushPrefs(["security.signed_content.CSP.default", "script-src 'self' 'unsafe-inline'; style-src 'self'"]); + } + + // start the test + yield BrowserTestUtils.withNewTab({ + gBrowser, + url: aUrl, + }, + function * (browser) { + // check if everything's set correct for testing + ok(Services.prefs.getBoolPref( + "browser.newtabpage.remote.content-signing-test"), + "sanity check: remote newtab signing test should be used"); + ok(Services.prefs.getBoolPref("browser.newtabpage.remote"), + "sanity check: remote newtab should be used"); + // we only check this if we really do a newtab test + if (aNewTabPref) { + ok(aboutNewTabService.overridden, + "sanity check: default URL for about:newtab should be overriden"); + is(aboutNewTabService.newTabURL, aNewTabPref, + "sanity check: default URL for about:newtab should return the new URL"); + } + + // Every valid remote newtab page must have built-in CSP. + let shouldHaveCSP = ((aUrl === ABOUT_NEWTAB_URI) && + (aNewTabPref === URI_GOOD || aNewTabPref === URI_SRI)); + + if (shouldHaveCSP) { + is(browser.contentDocument.nodePrincipal.cspJSON, SIGNED_CONTENT_CSP, + "Valid remote newtab page must have built-in CSP."); + } + + yield ContentTask.spawn( + browser, aExpectedStrings, function * (aExpectedStrings) { + for (let expectedString of aExpectedStrings) { + ok(content.document.documentElement.innerHTML.includes(expectedString), + "Expect the following value in the result\n" + expectedString + + "\nand got " + content.document.documentElement.innerHTML); + } + }); + + // for good test cases we check if a reload fails if the remote page + // changed from valid to invalid in the meantime + if (reload) { + yield BrowserTestUtils.withNewTab({ + gBrowser, + url: INVALIDATE_FILE, + }, + function * (browser2) { + yield ContentTask.spawn(browser2, null, function * () { + ok(content.document.documentElement.innerHTML.includes("Done"), + "Expect the following value in the result\n" + "Done" + + "\nand got " + content.document.documentElement.innerHTML); + }); + } + ); + + browser.reload(); + yield BrowserTestUtils.browserLoaded(browser); + + let expectedStrings = [ABOUT_BLANK]; + if (aNewTabPref == URI_SRI) { + expectedStrings = [ + STYLESHEET_WITHOUT_SRI_BLOCKED, + STYLESHEET_WITH_SRI_BLOCKED, + SCRIPT_WITHOUT_SRI_BLOCKED, + SCRIPT_WITH_SRI_BLOCKED + ]; + } + yield ContentTask.spawn(browser, expectedStrings, + function * (expectedStrings) { + for (let expectedString of expectedStrings) { + ok(content.document.documentElement.innerHTML.includes(expectedString), + "Expect the following value in the result\n" + expectedString + + "\nand got " + content.document.documentElement.innerHTML); + } + } + ); + + yield BrowserTestUtils.withNewTab({ + gBrowser, + url: VALIDATE_FILE, + }, + function * (browser2) { + yield ContentTask.spawn(browser2, null, function * () { + ok(content.document.documentElement.innerHTML.includes("Done"), + "Expect the following value in the result\n" + "Done" + + "\nand got " + content.document.documentElement.innerHTML); + }); + } + ); + } + } + ); +} + +function runTests() { + // run tests from TESTS + for (let i = 0; i < TESTS.length; i++) { + let testCase = TESTS[i]; + let url = "", aNewTabPref = ""; + let reload = false; + var aExpectedStrings = testCase.testStrings; + if (testCase.aboutURI) { + url = ABOUT_NEWTAB_URI; + aNewTabPref = testCase.aboutURI; + if (aNewTabPref == URI_GOOD || aNewTabPref == URI_SRI) { + reload = true; + } + } else { + url = testCase.url; + } + + yield doTest(aExpectedStrings, reload, url, aNewTabPref); + } +}
--- a/netwerk/protocol/http/nsHttpChannel.cpp +++ b/netwerk/protocol/http/nsHttpChannel.cpp @@ -1126,30 +1126,16 @@ nsHttpChannel::CallOnStartRequest() if (NS_ERROR_FILE_TOO_BIG == rv) { // Don't throw the entry away, we will need it later. LOG((" entry too big")); } else { NS_ENSURE_SUCCESS(rv, rv); } } - // Check for a Content-Signature header and inject mediator if the header is - // requested and available. - // If requested (mLoadInfo->GetVerifySignedContent), but not present, or - // present but not valid, fail this channel and return - // NS_ERROR_INVALID_SIGNATURE to indicate a signature error and trigger a - // fallback load in nsDocShell. - if (!mCanceled) { - rv = ProcessContentSignatureHeader(mResponseHead); - if (NS_FAILED(rv)) { - LOG(("Content-signature verification failed.\n")); - return rv; - } - } - LOG((" calling mListener->OnStartRequest\n")); if (mListener) { MOZ_ASSERT(!mOnStartRequestCalled, "We should not call OsStartRequest twice"); rv = mListener->OnStartRequest(this, mListenerContext); mOnStartRequestCalled = true; if (NS_FAILED(rv)) return rv; @@ -1202,16 +1188,34 @@ nsHttpChannel::CallOnStartRequest() if (NS_FAILED(rv)) return rv; } } else if (mApplicationCacheForWrite) { LOG(("offline cache is up to date, not updating")); CloseOfflineCacheEntry(); } } + // Check for a Content-Signature header and inject mediator if the header is + // requested and available. + // If requested (mLoadInfo->GetVerifySignedContent), but not present, or + // present but not valid, fail this channel and return + // NS_ERROR_INVALID_SIGNATURE to indicate a signature error and trigger a + // fallback load in nsDocShell. + // Note that OnStartRequest has already been called on the target stream + // listener at this point. We have to add the listener here that late to + // ensure that it's the last listener and can thus block the load in + // OnStopRequest. + if (!mCanceled) { + rv = ProcessContentSignatureHeader(mResponseHead); + if (NS_FAILED(rv)) { + LOG(("Content-signature verification failed.\n")); + return rv; + } + } + return NS_OK; } nsresult nsHttpChannel::ProcessFailedProxyConnect(uint32_t httpStatus) { // Failure to set up a proxy tunnel via CONNECT means one of the following: // 1) Proxy wants authorization, or forbids. @@ -1523,18 +1527,18 @@ nsHttpChannel::ProcessContentSignatureHe if (!aResponseHead->HasContentType()) { NS_WARNING("Empty content type can get us in trouble when verifying " "content signatures"); return NS_ERROR_INVALID_SIGNATURE; } // create a new listener that meadiates the content RefPtr<ContentVerifier> contentVerifyingMediator = new ContentVerifier(mListener, mListenerContext); - rv = contentVerifyingMediator->Init( - NS_ConvertUTF8toUTF16(contentSignatureHeader)); + rv = contentVerifyingMediator->Init(contentSignatureHeader, this, + mListenerContext); NS_ENSURE_SUCCESS(rv, NS_ERROR_INVALID_SIGNATURE); mListener = contentVerifyingMediator; return NS_OK; } /** * Decide whether or not to send a security report and, if so, give the
--- a/security/manager/ssl/ContentSignatureVerifier.cpp +++ b/security/manager/ssl/ContentSignatureVerifier.cpp @@ -5,28 +5,36 @@ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ #include "ContentSignatureVerifier.h" #include "BRNameMatchingPolicy.h" #include "SharedCertVerifier.h" #include "cryptohi.h" #include "keyhi.h" +#include "mozilla/Assertions.h" #include "mozilla/Casting.h" #include "nsCOMPtr.h" +#include "nsContentUtils.h" +#include "nsISupportsPriority.h" +#include "nsIURI.h" #include "nsNSSComponent.h" #include "nsSecurityHeaderParser.h" +#include "nsStreamUtils.h" #include "nsWhitespaceTokenizer.h" #include "nsXPCOMStrings.h" #include "nssb64.h" #include "pkix/pkix.h" #include "pkix/pkixtypes.h" #include "secerr.h" -NS_IMPL_ISUPPORTS(ContentSignatureVerifier, nsIContentSignatureVerifier) +NS_IMPL_ISUPPORTS(ContentSignatureVerifier, + nsIContentSignatureVerifier, + nsIInterfaceRequestor, + nsIStreamListener) using namespace mozilla; using namespace mozilla::pkix; using namespace mozilla::psm; static LazyLogModule gCSVerifierPRLog("ContentSignatureVerifier"); #define CSVerifier_LOG(args) MOZ_LOG(gCSVerifierPRLog, LogLevel::Debug, args) @@ -121,37 +129,28 @@ ReadChainIntoCertList(const nsACString& if (inBlock || !certFound) { // the PEM data did not end; bad data. CSVerifier_LOG(("CSVerifier: supplied chain contains bad data\n")); return NS_ERROR_FAILURE; } return NS_OK; } -// Create a context for a content signature verification. -// It sets signature, certificate chain and name that should be used to verify -// the data. The data parameter is the first part of the data to verify (this -// can be the empty string). -NS_IMETHODIMP -ContentSignatureVerifier::CreateContext(const nsACString& aData, - const nsACString& aCSHeader, - const nsACString& aCertChain, - const nsACString& aName) +nsresult +ContentSignatureVerifier::CreateContextInternal(const nsACString& aData, + const nsACString& aCertChain, + const nsACString& aName) { - MutexAutoLock lock(mMutex); + MOZ_ASSERT(NS_IsMainThread()); nsNSSShutDownPreventionLock locker; if (isAlreadyShutDown()) { CSVerifier_LOG(("CSVerifier: nss is already shutdown\n")); return NS_ERROR_FAILURE; } - if (mCx) { - return NS_ERROR_ALREADY_INITIALIZED; - } - UniqueCERTCertList certCertList(CERT_NewCertList()); if (!certCertList) { return NS_ERROR_OUT_OF_MEMORY; } nsresult rv = ReadChainIntoCertList(aCertChain, certCertList.get(), locker); if (NS_FAILED(rv)) { return rv; @@ -209,22 +208,16 @@ ContentSignatureVerifier::CreateContext( mKey.reset(CERT_ExtractPublicKey(node->cert)); // in case we were not able to extract a key if (!mKey) { CSVerifier_LOG(("CSVerifier: unable to extract a key\n")); return NS_ERROR_INVALID_SIGNATURE; } - // we get the raw content-signature header here, so first parse aCSHeader - rv = ParseContentSignatureHeader(aCSHeader); - if (NS_FAILED(rv)) { - return rv; - } - // Base 64 decode the signature UniqueSECItem rawSignatureItem(::SECITEM_AllocItem(nullptr, nullptr, 0)); if (!rawSignatureItem || !NSSBase64_DecodeBuffer(nullptr, rawSignatureItem.get(), mSignature.get(), mSignature.Length())) { CSVerifier_LOG(("CSVerifier: decoding the signature failed\n")); return NS_ERROR_FAILURE; } @@ -256,78 +249,197 @@ ContentSignatureVerifier::CreateContext( if (!mCx) { return NS_ERROR_INVALID_SIGNATURE; } if (VFY_Begin(mCx.get()) != SECSuccess) { return NS_ERROR_INVALID_SIGNATURE; } - rv = UpdateInternal(kPREFIX, lock, locker); + rv = UpdateInternal(kPREFIX, locker); if (NS_FAILED(rv)) { return rv; } // add data if we got any - return UpdateInternal(aData, lock, locker); + return UpdateInternal(aData, locker); } nsresult -ContentSignatureVerifier::UpdateInternal(const nsACString& aData, - MutexAutoLock& /*proofOfLock*/, - const nsNSSShutDownPreventionLock& /*proofOfLock*/) +ContentSignatureVerifier::DownloadCertChain() +{ + MOZ_ASSERT(NS_IsMainThread()); + + if (mCertChainURL.IsEmpty()) { + return NS_ERROR_INVALID_SIGNATURE; + } + + nsCOMPtr<nsIURI> certChainURI; + nsresult rv = NS_NewURI(getter_AddRefs(certChainURI), mCertChainURL); + if (NS_FAILED(rv) || !certChainURI) { + return rv; + } + + // If the address is not https, fail. + bool isHttps = false; + rv = certChainURI->SchemeIs("https", &isHttps); + if (NS_FAILED(rv)) { + return rv; + } + if (!isHttps) { + return NS_ERROR_INVALID_SIGNATURE; + } + + rv = NS_NewChannel(getter_AddRefs(mChannel), certChainURI, + nsContentUtils::GetSystemPrincipal(), + nsILoadInfo::SEC_ALLOW_CROSS_ORIGIN_DATA_IS_NULL, + nsIContentPolicy::TYPE_OTHER); + if (NS_FAILED(rv)) { + return rv; + } + + // we need this chain soon + nsCOMPtr<nsISupportsPriority> priorityChannel = do_QueryInterface(mChannel); + if (priorityChannel) { + priorityChannel->AdjustPriority(nsISupportsPriority::PRIORITY_HIGHEST); + } + + rv = mChannel->AsyncOpen2(this); + if (NS_FAILED(rv)) { + return rv; + } + + return NS_OK; +} + +// Create a context for content signature verification using CreateContext below. +// This function doesn't require a cert chain to be passed, but instead aCSHeader +// must contain an x5u value that is then used to download the cert chain. +NS_IMETHODIMP +ContentSignatureVerifier::CreateContextWithoutCertChain( + nsIContentSignatureReceiverCallback *aCallback, const nsACString& aCSHeader, + const nsACString& aName) +{ + MOZ_ASSERT(NS_IsMainThread()); + if (mInitialised) { + return NS_ERROR_ALREADY_INITIALIZED; + } + mInitialised = true; + + // we get the raw content-signature header here, so first parse aCSHeader + nsresult rv = ParseContentSignatureHeader(aCSHeader); + if (NS_FAILED(rv)) { + return rv; + } + + mCallback = aCallback; + mName.Assign(aName); + + // We must download the cert chain now. + // This is async and blocks createContextInternal calls. + return DownloadCertChain(); +} + +// Create a context for a content signature verification. +// It sets signature, certificate chain and name that should be used to verify +// the data. The data parameter is the first part of the data to verify (this +// can be the empty string). +NS_IMETHODIMP +ContentSignatureVerifier::CreateContext(const nsACString& aData, + const nsACString& aCSHeader, + const nsACString& aCertChain, + const nsACString& aName) +{ + if (mInitialised) { + return NS_ERROR_ALREADY_INITIALIZED; + } + mInitialised = true; + // The cert chain is given in aCertChain so we don't have to download anything. + mHasCertChain = true; + + // we get the raw content-signature header here, so first parse aCSHeader + nsresult rv = ParseContentSignatureHeader(aCSHeader); + if (NS_FAILED(rv)) { + return rv; + } + + return CreateContextInternal(aData, aCertChain, aName); +} + +nsresult +ContentSignatureVerifier::UpdateInternal( + const nsACString& aData, const nsNSSShutDownPreventionLock& /*proofOfLock*/) { if (!aData.IsEmpty()) { if (VFY_Update(mCx.get(), (const unsigned char*)nsPromiseFlatCString(aData).get(), aData.Length()) != SECSuccess){ return NS_ERROR_INVALID_SIGNATURE; } } return NS_OK; } /** * Add data to the context that shold be verified. */ NS_IMETHODIMP ContentSignatureVerifier::Update(const nsACString& aData) { + MOZ_ASSERT(NS_IsMainThread()); nsNSSShutDownPreventionLock locker; if (isAlreadyShutDown()) { CSVerifier_LOG(("CSVerifier: nss is already shutdown\n")); return NS_ERROR_FAILURE; } - MutexAutoLock lock(mMutex); - return UpdateInternal(aData, lock, locker); + + // If we didn't create the context yet, bail! + if (!mHasCertChain) { + MOZ_ASSERT_UNREACHABLE( + "Someone called ContentSignatureVerifier::Update before " + "downloading the cert chain."); + return NS_ERROR_FAILURE; + } + + return UpdateInternal(aData, locker); } /** * Finish signature verification and return the result in _retval. */ NS_IMETHODIMP ContentSignatureVerifier::End(bool* _retval) { NS_ENSURE_ARG(_retval); - MutexAutoLock lock(mMutex); + MOZ_ASSERT(NS_IsMainThread()); nsNSSShutDownPreventionLock locker; if (isAlreadyShutDown()) { CSVerifier_LOG(("CSVerifier: nss is already shutdown\n")); return NS_ERROR_FAILURE; } + // If we didn't create the context yet, bail! + if (!mHasCertChain) { + MOZ_ASSERT_UNREACHABLE( + "Someone called ContentSignatureVerifier::End before " + "downloading the cert chain."); + return NS_ERROR_FAILURE; + } + *_retval = (VFY_End(mCx.get()) == SECSuccess); return NS_OK; } nsresult ContentSignatureVerifier::ParseContentSignatureHeader( const nsACString& aContentSignatureHeader) { + MOZ_ASSERT(NS_IsMainThread()); // We only support p384 ecdsa according to spec NS_NAMED_LITERAL_CSTRING(signature_var, "p384ecdsa"); + NS_NAMED_LITERAL_CSTRING(certChainURL_var, "x5u"); nsSecurityHeaderParser parser(aContentSignatureHeader.BeginReading()); nsresult rv = parser.Parse(); if (NS_FAILED(rv)) { CSVerifier_LOG(("CSVerifier: could not parse ContentSignature header\n")); return NS_ERROR_FAILURE; } LinkedList<nsSecurityHeaderDirective>* directives = parser.GetDirectives(); @@ -341,24 +453,113 @@ ContentSignatureVerifier::ParseContentSi if (!mSignature.IsEmpty()) { CSVerifier_LOG(("CSVerifier: found two ContentSignatures\n")); return NS_ERROR_INVALID_SIGNATURE; } CSVerifier_LOG(("CSVerifier: found a ContentSignature directive\n")); mSignature = directive->mValue; } + if (directive->mName.Length() == certChainURL_var.Length() && + directive->mName.EqualsIgnoreCase(certChainURL_var.get(), + certChainURL_var.Length())) { + if (!mCertChainURL.IsEmpty()) { + CSVerifier_LOG(("CSVerifier: found two x5u values\n")); + return NS_ERROR_INVALID_SIGNATURE; + } + + CSVerifier_LOG(("CSVerifier: found an x5u directive\n")); + mCertChainURL = directive->mValue; + } } // we have to ensure that we found a signature at this point if (mSignature.IsEmpty()) { CSVerifier_LOG(("CSVerifier: got a Content-Signature header but didn't find a signature.\n")); return NS_ERROR_FAILURE; } // Bug 769521: We have to change b64 url to regular encoding as long as we // don't have a b64 url decoder. This should change soon, but in the meantime // we have to live with this. mSignature.ReplaceChar('-', '+'); mSignature.ReplaceChar('_', '/'); return NS_OK; } + +/* nsIStreamListener implementation */ + +NS_IMETHODIMP +ContentSignatureVerifier::OnStartRequest(nsIRequest* aRequest, + nsISupports* aContext) +{ + MOZ_ASSERT(NS_IsMainThread()); + return NS_OK; +} + +NS_IMETHODIMP +ContentSignatureVerifier::OnStopRequest(nsIRequest* aRequest, + nsISupports* aContext, nsresult aStatus) +{ + MOZ_ASSERT(NS_IsMainThread()); + nsCOMPtr<nsIContentSignatureReceiverCallback> callback; + callback.swap(mCallback); + nsresult rv; + + // Check HTTP status code and return if it's not 200. + nsCOMPtr<nsIHttpChannel> http = do_QueryInterface(aRequest, &rv); + uint32_t httpResponseCode; + if (NS_FAILED(rv) || NS_FAILED(http->GetResponseStatus(&httpResponseCode)) || + httpResponseCode != 200) { + callback->ContextCreated(false); + return NS_OK; + } + + if (NS_FAILED(aStatus)) { + callback->ContextCreated(false); + return NS_OK; + } + + nsAutoCString certChain; + for (uint32_t i = 0; i < mCertChain.Length(); ++i) { + certChain.Append(mCertChain[i]); + } + + // We got the cert chain now. Let's create the context. + rv = CreateContextInternal(NS_LITERAL_CSTRING(""), certChain, mName); + if (NS_FAILED(rv)) { + callback->ContextCreated(false); + return NS_OK; + } + + mHasCertChain = true; + callback->ContextCreated(true); + return NS_OK; +} + +NS_IMETHODIMP +ContentSignatureVerifier::OnDataAvailable(nsIRequest* aRequest, + nsISupports* aContext, + nsIInputStream* aInputStream, + uint64_t aOffset, uint32_t aCount) +{ + MOZ_ASSERT(NS_IsMainThread()); + nsAutoCString buffer; + + nsresult rv = NS_ConsumeStream(aInputStream, aCount, buffer); + if (NS_FAILED(rv)) { + return rv; + } + + if (!mCertChain.AppendElement(buffer, fallible)) { + mCertChain.TruncateLength(0); + return NS_ERROR_OUT_OF_MEMORY; + } + + return NS_OK; +} + +NS_IMETHODIMP +ContentSignatureVerifier::GetInterface(const nsIID& uuid, void** result) +{ + return QueryInterface(uuid, result); +}
--- a/security/manager/ssl/ContentSignatureVerifier.h +++ b/security/manager/ssl/ContentSignatureVerifier.h @@ -6,62 +6,87 @@ #ifndef ContentSignatureVerifier_h #define ContentSignatureVerifier_h #include "cert.h" #include "CSTrustDomain.h" #include "nsIContentSignatureVerifier.h" +#include "nsIStreamListener.h" #include "nsNSSShutDown.h" #include "ScopedNSSTypes.h" // 45a5fe2f-c350-4b86-962d-02d5aaaa955a #define NS_CONTENTSIGNATUREVERIFIER_CID \ { 0x45a5fe2f, 0xc350, 0x4b86, \ { 0x96, 0x2d, 0x02, 0xd5, 0xaa, 0xaa, 0x95, 0x5a } } #define NS_CONTENTSIGNATUREVERIFIER_CONTRACTID \ "@mozilla.org/security/contentsignatureverifier;1" class ContentSignatureVerifier final : public nsIContentSignatureVerifier + , public nsIStreamListener , public nsNSSShutDownObject + , public nsIInterfaceRequestor { public: NS_DECL_ISUPPORTS NS_DECL_NSICONTENTSIGNATUREVERIFIER + NS_DECL_NSIINTERFACEREQUESTOR + NS_DECL_NSISTREAMLISTENER + NS_DECL_NSIREQUESTOBSERVER ContentSignatureVerifier() : mCx(nullptr) - , mMutex("CSVerifier::mMutex") + , mInitialised(false) + , mHasCertChain(false) { } // nsNSSShutDownObject virtual void virtualDestroyNSSReference() override { destructorSafeDestroyNSSReference(); } private: ~ContentSignatureVerifier(); nsresult UpdateInternal(const nsACString& aData, - MutexAutoLock& /*proofOfLock*/, const nsNSSShutDownPreventionLock& /*proofOfLock*/); + nsresult DownloadCertChain(); + nsresult CreateContextInternal(const nsACString& aData, + const nsACString& aCertChain, + const nsACString& aName); void destructorSafeDestroyNSSReference() { mCx = nullptr; mKey = nullptr; } nsresult ParseContentSignatureHeader(const nsACString& aContentSignatureHeader); // verifier context for incremental verifications mozilla::UniqueVFYContext mCx; + bool mInitialised; + // Indicates whether we hold a cert chain to verify the signature or not. + // It's set by default in CreateContext or when the channel created in + // DownloadCertChain finished. Update and End must only be called after + // mHashCertChain is set. + bool mHasCertChain; // signature to verify nsCString mSignature; + // x5u (X.509 URL) value pointing to pem cert chain + nsCString mCertChainURL; + // the downloaded cert chain to verify against + FallibleTArray<nsCString> mCertChain; // verification key mozilla::UniqueSECKEYPublicKey mKey; - mozilla::Mutex mMutex; + // name of the verifying context + nsCString mName; + // callback to notify when finished + nsCOMPtr<nsIContentSignatureReceiverCallback> mCallback; + // channel to download the cert chain + nsCOMPtr<nsIChannel> mChannel; }; #endif // ContentSignatureVerifier_h
--- a/security/manager/ssl/nsIContentSignatureVerifier.idl +++ b/security/manager/ssl/nsIContentSignatureVerifier.idl @@ -1,15 +1,17 @@ /* 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 "nsISupports.idl" +interface nsIContentSignatureReceiverCallback; + /** * An interface for verifying content-signatures, inspired by * https://tools.ietf.org/html/draft-thomson-http-content-signature-00 * described here https://github.com/franziskuskiefer/content-signature/tree/pki * * A new signature verifier instance should be created for each signature * verification - you can create these instances with do_CreateInstance. * @@ -33,44 +35,82 @@ interface nsIContentSignatureVerifier : * url-safe base64 encoded. * @param aCertificateChain The certificate chain to use for verification. * PEM encoded string. * @param aName The (host)name for which the end entity must be valid. * @returns true if the signature matches the data and aCertificateChain is * valid within aContext, false if not. */ - boolean verifyContentSignature(in ACString aData, in ACString aSignature, + boolean verifyContentSignature(in ACString aData, + in ACString aContentSignatureHeader, in ACString aCertificateChain, in ACString aName); /** * Creates a context to verify a content signature against data that is added * later with update calls. * * @param aData The first chunk of data to be tested. * @param aContentSignatureHeader The signature of the data, url-safe base64 * encoded. * @param aCertificateChain The certificate chain to use for * verification. PEM encoded string. * @param aName The (host)name for which the end entity must be valid. */ - void createContext(in ACString aData, in ACString aSignature, + void createContext(in ACString aData, in ACString aContentSignatureHeader, in ACString aCertificateChain, in ACString aName); /** + * Creates a context to verify a content signature against data that is added + * later with update calls. + * This does not require the caller to download the certificate chain. It's + * done internally. + * It requires the x5u parameter to be present in aContentSignatureHeader + * + * NOTE: Callers have to wait for aCallback to return before invoking anything + * else. Otherwise the ContentSignatureVerifier will fail. + * + * @param aCallback Callback that's invoked when the cert chain + * got fetched. + * @param aContentSignatureHeader The signature of the data, url-safe base64 + * encoded, and the x5u value. + * @param aName The (host)name for which the end entity must + be valid. + */ + void createContextWithoutCertChain(in nsIContentSignatureReceiverCallback aCallback, + in ACString aContentSignatureHeader, + in ACString aName); + + /** * Adds data to the context that was used to generate the signature. * * @param aData More data to be tested. */ void update(in ACString aData); /** * Finalises the signature and returns the result of the signature * verification. * * @returns true if the signature matches the data added with createContext * and update, false if not. */ boolean end(); +}; +/** + * Callback for nsIContentSignatureVerifier. + * { 0x1eb90707, 0xdf59, 0x48b7, \ + * { 0x9d, 0x42, 0xd8, 0xbf, 0x63, 0x0a, 0xe7, 0x44 } } + */ +[scriptable, uuid(1eb90707-df59-48b7-9d42-d8bf630ae744)] +interface nsIContentSignatureReceiverCallback : nsISupports +{ + /** + * Notification callback that's called by nsIContentSignatureVerifier when + * the cert chain is downloaded. + * If download and initialisation were successful, successful is true, + * otherwise false. If successful is false, the verification must be aborted. + */ + void contextCreated(in boolean successful); };