/* -*- 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& aPromise) : mData(aData), mCSHeader(aCSHeader), mCertChain(aCertChain), mHostname(aHostname), mTrustedRoot(aTrustedRoot), mSignatureVerified(false), mPromise(new nsMainThreadPtrHolder( "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 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::Create(globalObject, result); if (NS_WARN_IF(result.Failed())) { return result.StealNSResult(); } RefPtr 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>& aCertList) { bool inBlock = false; bool certFound = false; const nsCString header = "-----BEGIN CERTIFICATE-----"_ns; const nsCString footer = "-----END CERTIFICATE-----"_ns; nsCWhitespaceTokenizerTemplate 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 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> 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& 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> certSpans; // Collect just the CAs. for (size_t i = 1; i < certList.Length(); i++) { Span 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(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(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(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(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* 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; }