diff options
Diffstat (limited to 'security/manager/ssl/ContentSignatureVerifier.cpp')
-rw-r--r-- | security/manager/ssl/ContentSignatureVerifier.cpp | 562 |
1 files changed, 562 insertions, 0 deletions
diff --git a/security/manager/ssl/ContentSignatureVerifier.cpp b/security/manager/ssl/ContentSignatureVerifier.cpp new file mode 100644 index 000000000..bdeb3f264 --- /dev/null +++ b/security/manager/ssl/ContentSignatureVerifier.cpp @@ -0,0 +1,562 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=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 "ContentSignatureVerifier.h" + +#include "BRNameMatchingPolicy.h" +#include "SharedCertVerifier.h" +#include "cryptohi.h" +#include "keyhi.h" +#include "mozilla/Assertions.h" +#include "mozilla/Casting.h" +#include "mozilla/Unused.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, + 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) + +// Content-Signature prefix +const nsLiteralCString kPREFIX = NS_LITERAL_CSTRING("Content-Signature:\x00"); + +ContentSignatureVerifier::~ContentSignatureVerifier() +{ + nsNSSShutDownPreventionLock locker; + if (isAlreadyShutDown()) { + return; + } + destructorSafeDestroyNSSReference(); + shutdown(ShutdownCalledFrom::Object); +} + +NS_IMETHODIMP +ContentSignatureVerifier::VerifyContentSignature( + const nsACString& aData, const nsACString& aCSHeader, + const nsACString& aCertChain, const nsACString& aName, bool* _retval) +{ + NS_ENSURE_ARG(_retval); + nsresult rv = CreateContext(aData, aCSHeader, aCertChain, aName); + if (NS_FAILED(rv)) { + *_retval = false; + CSVerifier_LOG(("CSVerifier: Signature verification failed\n")); + if (rv == NS_ERROR_INVALID_SIGNATURE) { + return NS_OK; + } + return rv; + } + + return End(_retval); +} + +bool +IsNewLine(char16_t c) +{ + return c == '\n' || c == '\r'; +} + +nsresult +ReadChainIntoCertList(const nsACString& aCertChain, CERTCertList* aCertList, + const nsNSSShutDownPreventionLock& /*proofOfLock*/) +{ + bool inBlock = false; + bool certFound = false; + + const nsCString header = NS_LITERAL_CSTRING("-----BEGIN CERTIFICATE-----"); + const nsCString footer = NS_LITERAL_CSTRING("-----END CERTIFICATE-----"); + + nsCWhitespaceTokenizerTemplate<IsNewLine> tokenizer(aCertChain); + + nsAutoCString blockData; + while (tokenizer.hasMoreTokens()) { + nsDependentCSubstring token = tokenizer.nextToken(); + if (token.IsEmpty()) { + continue; + } + if (inBlock) { + if (token.Equals(footer)) { + inBlock = false; + certFound = true; + // base64 decode data, make certs, append to chain + ScopedAutoSECItem der; + if (!NSSBase64_DecodeBuffer(nullptr, &der, blockData.BeginReading(), + blockData.Length())) { + CSVerifier_LOG(("CSVerifier: decoding the signature failed\n")); + return NS_ERROR_FAILURE; + } + UniqueCERTCertificate tmpCert( + CERT_NewTempCertificate(CERT_GetDefaultCertDB(), &der, nullptr, false, + true)); + if (!tmpCert) { + return NS_ERROR_FAILURE; + } + // if adding tmpCert succeeds, tmpCert will now be owned by aCertList + SECStatus res = CERT_AddCertToListTail(aCertList, tmpCert.get()); + if (res != SECSuccess) { + return MapSECStatus(res); + } + Unused << tmpCert.release(); + } else { + blockData.Append(token); + } + } else if (token.Equals(header)) { + inBlock = true; + blockData = ""; + } + } + 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; +} + +nsresult +ContentSignatureVerifier::CreateContextInternal(const nsACString& aData, + const nsACString& aCertChain, + const nsACString& aName) +{ + MOZ_ASSERT(NS_IsMainThread()); + nsNSSShutDownPreventionLock locker; + if (isAlreadyShutDown()) { + CSVerifier_LOG(("CSVerifier: nss is already shutdown\n")); + return NS_ERROR_FAILURE; + } + + 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; + } + + CERTCertListNode* node = CERT_LIST_HEAD(certCertList.get()); + if (!node || CERT_LIST_END(node, certCertList.get()) || !node->cert) { + return NS_ERROR_FAILURE; + } + + SECItem* certSecItem = &node->cert->derCert; + + Input certDER; + mozilla::pkix::Result result = + certDER.Init(BitwiseCast<uint8_t*, unsigned char*>(certSecItem->data), + certSecItem->len); + if (result != Success) { + return NS_ERROR_FAILURE; + } + + + // Check the signerCert chain is good + CSTrustDomain trustDomain(certCertList); + result = BuildCertChain(trustDomain, certDER, Now(), + EndEntityOrCA::MustBeEndEntity, + KeyUsage::noParticularKeyUsageRequired, + KeyPurposeId::id_kp_codeSigning, + CertPolicyId::anyPolicy, + nullptr/*stapledOCSPResponse*/); + if (result != Success) { + // if there was a library error, return an appropriate error + if (IsFatalError(result)) { + return NS_ERROR_FAILURE; + } + // otherwise, assume the signature was invalid + CSVerifier_LOG(("CSVerifier: The supplied chain is bad\n")); + return NS_ERROR_INVALID_SIGNATURE; + } + + // Check the SAN + Input hostnameInput; + + result = hostnameInput.Init( + BitwiseCast<const uint8_t*, const char*>(aName.BeginReading()), + aName.Length()); + if (result != Success) { + return NS_ERROR_FAILURE; + } + + BRNameMatchingPolicy nameMatchingPolicy(BRNameMatchingPolicy::Mode::Enforce); + result = CheckCertHostname(certDER, hostnameInput, nameMatchingPolicy); + if (result != Success) { + return NS_ERROR_INVALID_SIGNATURE; + } + + 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; + } + + // Base 64 decode the signature + ScopedAutoSECItem rawSignatureItem; + if (!NSSBase64_DecodeBuffer(nullptr, &rawSignatureItem, mSignature.get(), + mSignature.Length())) { + CSVerifier_LOG(("CSVerifier: decoding the signature failed\n")); + return NS_ERROR_FAILURE; + } + + // get signature object + ScopedAutoSECItem signatureItem; + // 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) { + CSVerifier_LOG(("CSVerifier: signature length is bad\n")); + return NS_ERROR_FAILURE; + } + if (DSAU_EncodeDerSigWithLen(&signatureItem, &rawSignatureItem, + rawSignatureItem.len) != SECSuccess) { + CSVerifier_LOG(("CSVerifier: encoding the signature failed\n")); + return NS_ERROR_FAILURE; + } + + // this is the only OID we support for now + SECOidTag oid = SEC_OID_ANSIX962_ECDSA_SHA384_SIGNATURE; + + mCx = UniqueVFYContext( + VFY_CreateContext(mKey.get(), &signatureItem, oid, nullptr)); + if (!mCx) { + return NS_ERROR_INVALID_SIGNATURE; + } + + if (VFY_Begin(mCx.get()) != SECSuccess) { + return NS_ERROR_INVALID_SIGNATURE; + } + + rv = UpdateInternal(kPREFIX, locker); + if (NS_FAILED(rv)) { + return rv; + } + // add data if we got any + return UpdateInternal(aData, locker); +} + +nsresult +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()); + MOZ_ASSERT(aCallback); + 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; + } + + // 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); + 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(); + + for (nsSecurityHeaderDirective* directive = directives->getFirst(); + directive != nullptr; directive = directive->getNext()) { + CSVerifier_LOG(("CSVerifier: found directive %s\n", directive->mName.get())); + if (directive->mName.Length() == signature_var.Length() && + directive->mName.EqualsIgnoreCase(signature_var.get(), + signature_var.Length())) { + 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); +} |