diff options
Diffstat (limited to 'security/manager/ssl/ContentSignatureVerifier.cpp')
-rw-r--r-- | security/manager/ssl/ContentSignatureVerifier.cpp | 454 |
1 files changed, 454 insertions, 0 deletions
diff --git a/security/manager/ssl/ContentSignatureVerifier.cpp b/security/manager/ssl/ContentSignatureVerifier.cpp new file mode 100644 index 0000000000..bc0a7c5d06 --- /dev/null +++ b/security/manager/ssl/ContentSignatureVerifier.cpp @@ -0,0 +1,454 @@ +/* -*- 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 "AppTrustDomain.h" +#include "CryptoTask.h" +#include "ScopedNSSTypes.h" +#include "SharedCertVerifier.h" +#include "cryptohi.h" +#include "keyhi.h" +#include "mozilla/Base64.h" +#include "mozilla/Logging.h" +#include "mozilla/dom/Promise.h" +#include "nsCOMPtr.h" +#include "nsPromiseFlatString.h" +#include "nsSecurityHeaderParser.h" +#include "nsWhitespaceTokenizer.h" +#include "mozpkix/pkix.h" +#include "mozpkix/pkixtypes.h" +#include "mozpkix/pkixutil.h" +#include "secerr.h" +#include "ssl.h" + +NS_IMPL_ISUPPORTS(ContentSignatureVerifier, nsIContentSignatureVerifier) + +using namespace mozilla; +using namespace mozilla::pkix; +using namespace mozilla::psm; +using dom::Promise; + +static LazyLogModule gCSVerifierPRLog("ContentSignatureVerifier"); +#define CSVerifier_LOG(args) MOZ_LOG(gCSVerifierPRLog, LogLevel::Debug, args) + +// Content-Signature prefix +const unsigned char kPREFIX[] = {'C', 'o', 'n', 't', 'e', 'n', 't', + '-', 'S', 'i', 'g', 'n', 'a', 't', + 'u', 'r', 'e', ':', 0}; + +class VerifyContentSignatureTask : public CryptoTask { + public: + VerifyContentSignatureTask(const nsACString& aData, + const nsACString& aCSHeader, + const nsACString& aCertChain, + const nsACString& aHostname, + AppTrustedRoot aTrustedRoot, + RefPtr<Promise>& aPromise) + : mData(aData), + mCSHeader(aCSHeader), + mCertChain(aCertChain), + mHostname(aHostname), + mTrustedRoot(aTrustedRoot), + mSignatureVerified(false), + mPromise(new nsMainThreadPtrHolder<Promise>( + "VerifyContentSignatureTask::mPromise", aPromise)) {} + + private: + virtual nsresult CalculateResult() override; + virtual void CallCallback(nsresult rv) override; + + nsCString mData; + nsCString mCSHeader; + nsCString mCertChain; + nsCString mHostname; + AppTrustedRoot mTrustedRoot; + bool mSignatureVerified; + nsMainThreadPtrHandle<Promise> mPromise; +}; + +NS_IMETHODIMP +ContentSignatureVerifier::AsyncVerifyContentSignature( + const nsACString& aData, const nsACString& aCSHeader, + const nsACString& aCertChain, const nsACString& aHostname, + AppTrustedRoot aTrustedRoot, JSContext* aCx, Promise** aPromise) { + NS_ENSURE_ARG_POINTER(aCx); + + nsIGlobalObject* globalObject = xpc::CurrentNativeGlobal(aCx); + if (NS_WARN_IF(!globalObject)) { + return NS_ERROR_UNEXPECTED; + } + + ErrorResult result; + RefPtr<Promise> promise = Promise::Create(globalObject, result); + if (NS_WARN_IF(result.Failed())) { + return result.StealNSResult(); + } + + RefPtr<VerifyContentSignatureTask> task(new VerifyContentSignatureTask( + aData, aCSHeader, aCertChain, aHostname, aTrustedRoot, promise)); + nsresult rv = task->Dispatch(); + if (NS_FAILED(rv)) { + return rv; + } + + promise.forget(aPromise); + return NS_OK; +} + +static nsresult VerifyContentSignatureInternal( + const nsACString& aData, const nsACString& aCSHeader, + const nsACString& aCertChain, const nsACString& aHostname, + AppTrustedRoot aTrustedRoot, + /* out */ + mozilla::Telemetry::LABELS_CONTENT_SIGNATURE_VERIFICATION_ERRORS& + aErrorLabel, + /* out */ nsACString& aCertFingerprint, /* out */ uint32_t& aErrorValue); +static nsresult ParseContentSignatureHeader( + const nsACString& aContentSignatureHeader, + /* out */ nsCString& aSignature); + +nsresult VerifyContentSignatureTask::CalculateResult() { + // 3 is the default, non-specific, "something failed" error. + Telemetry::LABELS_CONTENT_SIGNATURE_VERIFICATION_ERRORS errorLabel = + Telemetry::LABELS_CONTENT_SIGNATURE_VERIFICATION_ERRORS::err3; + nsAutoCString certFingerprint; + uint32_t errorValue = 3; + nsresult rv = VerifyContentSignatureInternal( + mData, mCSHeader, mCertChain, mHostname, mTrustedRoot, errorLabel, + certFingerprint, errorValue); + if (NS_FAILED(rv)) { + CSVerifier_LOG(("CSVerifier: Signature verification failed")); + if (certFingerprint.Length() > 0) { + Telemetry::AccumulateCategoricalKeyed(certFingerprint, errorLabel); + } + Accumulate(Telemetry::CONTENT_SIGNATURE_VERIFICATION_STATUS, errorValue); + if (rv == NS_ERROR_INVALID_SIGNATURE) { + return NS_OK; + } + return rv; + } + + mSignatureVerified = true; + Accumulate(Telemetry::CONTENT_SIGNATURE_VERIFICATION_STATUS, 0); + + return NS_OK; +} + +void VerifyContentSignatureTask::CallCallback(nsresult rv) { + if (NS_FAILED(rv)) { + mPromise->MaybeReject(rv); + } else { + mPromise->MaybeResolve(mSignatureVerified); + } +} + +bool IsNewLine(char16_t c) { return c == '\n' || c == '\r'; } + +nsresult ReadChainIntoCertList(const nsACString& aCertChain, + nsTArray<nsTArray<uint8_t>>& aCertList) { + bool inBlock = false; + bool certFound = false; + + const nsCString header = "-----BEGIN CERTIFICATE-----"_ns; + const nsCString footer = "-----END CERTIFICATE-----"_ns; + + 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 + nsAutoCString derString; + nsresult rv = Base64Decode(blockData, derString); + if (NS_FAILED(rv)) { + CSVerifier_LOG(("CSVerifier: decoding the signature failed")); + return rv; + } + nsTArray<uint8_t> derBytes(derString.Data(), derString.Length()); + aCertList.AppendElement(std::move(derBytes)); + } 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")); + return NS_ERROR_FAILURE; + } + return NS_OK; +} + +// Given data to verify, a content signature header value, a string representing +// a list of PEM-encoded certificates, and a hostname to validate the +// certificates against, this function attempts to validate the certificate +// chain, extract the signature from the header, and verify the data using the +// key in the end-entity certificate from the chain. Returns NS_OK if everything +// is satisfactory and a failing nsresult otherwise. The output parameters are +// filled with telemetry data to report in the case of failures. +static nsresult VerifyContentSignatureInternal( + const nsACString& aData, const nsACString& aCSHeader, + const nsACString& aCertChain, const nsACString& aHostname, + AppTrustedRoot aTrustedRoot, + /* out */ + Telemetry::LABELS_CONTENT_SIGNATURE_VERIFICATION_ERRORS& aErrorLabel, + /* out */ nsACString& aCertFingerprint, + /* out */ uint32_t& aErrorValue) { + nsTArray<nsTArray<uint8_t>> certList; + nsresult rv = ReadChainIntoCertList(aCertChain, certList); + if (NS_FAILED(rv)) { + return rv; + } + if (certList.Length() < 1) { + return NS_ERROR_FAILURE; + } + // The 0th element should be the end-entity that issued the content + // signature. + nsTArray<uint8_t>& certBytes(certList.ElementAt(0)); + Input certInput; + mozilla::pkix::Result result = + certInput.Init(certBytes.Elements(), certBytes.Length()); + if (result != Success) { + return NS_ERROR_FAILURE; + } + + // Get EE certificate fingerprint for telemetry. + unsigned char fingerprint[SHA256_LENGTH] = {0}; + SECStatus srv = + PK11_HashBuf(SEC_OID_SHA256, fingerprint, certInput.UnsafeGetData(), + certInput.GetLength()); + if (srv != SECSuccess) { + return NS_ERROR_FAILURE; + } + SECItem fingerprintItem = {siBuffer, fingerprint, SHA256_LENGTH}; + UniquePORTString tmpFingerprintString( + CERT_Hexify(&fingerprintItem, false /* don't use colon delimiters */)); + if (!tmpFingerprintString) { + return NS_ERROR_OUT_OF_MEMORY; + } + aCertFingerprint.Assign(tmpFingerprintString.get()); + + nsTArray<Span<const uint8_t>> certSpans; + // Collect just the CAs. + for (size_t i = 1; i < certList.Length(); i++) { + Span<const uint8_t> certSpan(certList.ElementAt(i).Elements(), + certList.ElementAt(i).Length()); + certSpans.AppendElement(std::move(certSpan)); + } + AppTrustDomain trustDomain(std::move(certSpans)); + rv = trustDomain.SetTrustedRoot(aTrustedRoot); + if (NS_FAILED(rv)) { + return rv; + } + // Check the signerCert chain is good + result = BuildCertChain( + trustDomain, certInput, 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 + if (result == mozilla::pkix::Result::ERROR_EXPIRED_CERTIFICATE) { + aErrorLabel = + Telemetry::LABELS_CONTENT_SIGNATURE_VERIFICATION_ERRORS::err4; + aErrorValue = 4; + } else if (result == + mozilla::pkix::Result::ERROR_NOT_YET_VALID_CERTIFICATE) { + aErrorLabel = + Telemetry::LABELS_CONTENT_SIGNATURE_VERIFICATION_ERRORS::err5; + aErrorValue = 5; + } else { + // Building cert chain failed for some other reason. + aErrorLabel = + Telemetry::LABELS_CONTENT_SIGNATURE_VERIFICATION_ERRORS::err6; + aErrorValue = 6; + } + CSVerifier_LOG(("CSVerifier: The supplied chain is bad (%s)", + MapResultToName(result))); + return NS_ERROR_INVALID_SIGNATURE; + } + + // Check the SAN + Input hostnameInput; + + result = hostnameInput.Init( + BitwiseCast<const uint8_t*, const char*>(aHostname.BeginReading()), + aHostname.Length()); + if (result != Success) { + return NS_ERROR_FAILURE; + } + + result = CheckCertHostname(certInput, hostnameInput); + if (result != Success) { + // EE cert isnot valid for the given host name. + aErrorLabel = Telemetry::LABELS_CONTENT_SIGNATURE_VERIFICATION_ERRORS::err7; + aErrorValue = 7; + return NS_ERROR_INVALID_SIGNATURE; + } + + pkix::BackCert backCert(certInput, EndEntityOrCA::MustBeEndEntity, nullptr); + result = backCert.Init(); + // This should never fail, because we've already built a verified certificate + // chain with this certificate. + if (result != Success) { + aErrorLabel = Telemetry::LABELS_CONTENT_SIGNATURE_VERIFICATION_ERRORS::err8; + aErrorValue = 8; + CSVerifier_LOG(("CSVerifier: couldn't decode certificate to get spki")); + return NS_ERROR_INVALID_SIGNATURE; + } + Input spkiInput = backCert.GetSubjectPublicKeyInfo(); + SECItem spkiItem = {siBuffer, const_cast<uint8_t*>(spkiInput.UnsafeGetData()), + spkiInput.GetLength()}; + UniqueCERTSubjectPublicKeyInfo spki( + SECKEY_DecodeDERSubjectPublicKeyInfo(&spkiItem)); + if (!spki) { + aErrorLabel = Telemetry::LABELS_CONTENT_SIGNATURE_VERIFICATION_ERRORS::err8; + aErrorValue = 8; + CSVerifier_LOG(("CSVerifier: couldn't decode spki")); + return NS_ERROR_INVALID_SIGNATURE; + } + mozilla::UniqueSECKEYPublicKey key(SECKEY_ExtractPublicKey(spki.get())); + if (!key) { + aErrorLabel = Telemetry::LABELS_CONTENT_SIGNATURE_VERIFICATION_ERRORS::err8; + aErrorValue = 8; + CSVerifier_LOG(("CSVerifier: unable to extract a key")); + return NS_ERROR_INVALID_SIGNATURE; + } + + nsAutoCString signature; + rv = ParseContentSignatureHeader(aCSHeader, signature); + if (NS_FAILED(rv)) { + return rv; + } + + // Base 64 decode the signature + nsAutoCString rawSignature; + rv = Base64Decode(signature, rawSignature); + if (NS_FAILED(rv)) { + CSVerifier_LOG(("CSVerifier: decoding the signature failed")); + return rv; + } + + // get signature object + ScopedAutoSECItem signatureItem; + SECItem rawSignatureItem = { + siBuffer, + BitwiseCast<unsigned char*, const char*>(rawSignature.get()), + uint32_t(rawSignature.Length()), + }; + // 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")); + return NS_ERROR_FAILURE; + } + if (DSAU_EncodeDerSigWithLen(&signatureItem, &rawSignatureItem, + rawSignatureItem.len) != SECSuccess) { + CSVerifier_LOG(("CSVerifier: encoding the signature failed")); + return NS_ERROR_FAILURE; + } + + // this is the only OID we support for now + SECOidTag oid = SEC_OID_ANSIX962_ECDSA_SHA384_SIGNATURE; + mozilla::UniqueVFYContext cx( + VFY_CreateContext(key.get(), &signatureItem, oid, nullptr)); + if (!cx) { + // Creating context failed. + aErrorLabel = Telemetry::LABELS_CONTENT_SIGNATURE_VERIFICATION_ERRORS::err9; + aErrorValue = 9; + return NS_ERROR_INVALID_SIGNATURE; + } + + if (VFY_Begin(cx.get()) != SECSuccess) { + // Creating context failed. + aErrorLabel = Telemetry::LABELS_CONTENT_SIGNATURE_VERIFICATION_ERRORS::err9; + aErrorValue = 9; + return NS_ERROR_INVALID_SIGNATURE; + } + if (VFY_Update(cx.get(), kPREFIX, sizeof(kPREFIX)) != SECSuccess) { + aErrorLabel = Telemetry::LABELS_CONTENT_SIGNATURE_VERIFICATION_ERRORS::err1; + aErrorValue = 1; + return NS_ERROR_INVALID_SIGNATURE; + } + if (VFY_Update(cx.get(), + reinterpret_cast<const unsigned char*>(aData.BeginReading()), + aData.Length()) != SECSuccess) { + aErrorLabel = Telemetry::LABELS_CONTENT_SIGNATURE_VERIFICATION_ERRORS::err1; + aErrorValue = 1; + return NS_ERROR_INVALID_SIGNATURE; + } + if (VFY_End(cx.get()) != SECSuccess) { + aErrorLabel = Telemetry::LABELS_CONTENT_SIGNATURE_VERIFICATION_ERRORS::err1; + aErrorValue = 1; + return NS_ERROR_INVALID_SIGNATURE; + } + + return NS_OK; +} + +static nsresult ParseContentSignatureHeader( + const nsACString& aContentSignatureHeader, + /* out */ nsCString& aSignature) { + // We only support p384 ecdsa. + constexpr auto signature_var = "p384ecdsa"_ns; + + aSignature.Truncate(); + + const nsCString& flatHeader = PromiseFlatCString(aContentSignatureHeader); + nsSecurityHeaderParser parser(flatHeader); + nsresult rv = parser.Parse(); + if (NS_FAILED(rv)) { + CSVerifier_LOG(("CSVerifier: could not parse ContentSignature header")); + 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'", directive->mName.get())); + if (directive->mName.EqualsIgnoreCase(signature_var)) { + if (!aSignature.IsEmpty()) { + CSVerifier_LOG(("CSVerifier: found two ContentSignatures")); + return NS_ERROR_INVALID_SIGNATURE; + } + + CSVerifier_LOG(("CSVerifier: found a ContentSignature directive")); + aSignature.Assign(directive->mValue); + } + } + + // we have to ensure that we found a signature at this point + if (aSignature.IsEmpty()) { + CSVerifier_LOG( + ("CSVerifier: got a Content-Signature header but didn't find a " + "signature.")); + 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. + aSignature.ReplaceChar('-', '+'); + aSignature.ReplaceChar('_', '/'); + + return NS_OK; +} |