summaryrefslogtreecommitdiffstats
path: root/security/manager/ssl/ContentSignatureVerifier.cpp
diff options
context:
space:
mode:
Diffstat (limited to 'security/manager/ssl/ContentSignatureVerifier.cpp')
-rw-r--r--security/manager/ssl/ContentSignatureVerifier.cpp454
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;
+}