1 /* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
2 /* vim: set ts=2 et sw=2 tw=80: */
3 /* This Source Code Form is subject to the terms of the Mozilla Public
4  * License, v. 2.0. If a copy of the MPL was not distributed with this
5  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
6 
7 #include "ContentSignatureVerifier.h"
8 
9 #include "BRNameMatchingPolicy.h"
10 #include "CryptoTask.h"
11 #include "CSTrustDomain.h"
12 #include "ScopedNSSTypes.h"
13 #include "SharedCertVerifier.h"
14 #include "cryptohi.h"
15 #include "keyhi.h"
16 #include "mozilla/Base64.h"
17 #include "mozilla/dom/Promise.h"
18 #include "nsCOMPtr.h"
19 #include "nsPromiseFlatString.h"
20 #include "nsSecurityHeaderParser.h"
21 #include "nsWhitespaceTokenizer.h"
22 #include "mozpkix/pkix.h"
23 #include "mozpkix/pkixtypes.h"
24 #include "mozpkix/pkixutil.h"
25 #include "secerr.h"
26 #include "ssl.h"
27 
28 NS_IMPL_ISUPPORTS(ContentSignatureVerifier, nsIContentSignatureVerifier)
29 
30 using namespace mozilla;
31 using namespace mozilla::pkix;
32 using namespace mozilla::psm;
33 using dom::Promise;
34 
35 static LazyLogModule gCSVerifierPRLog("ContentSignatureVerifier");
36 #define CSVerifier_LOG(args) MOZ_LOG(gCSVerifierPRLog, LogLevel::Debug, args)
37 
38 // Content-Signature prefix
39 const unsigned char kPREFIX[] = {'C', 'o', 'n', 't', 'e', 'n', 't',
40                                  '-', 'S', 'i', 'g', 'n', 'a', 't',
41                                  'u', 'r', 'e', ':', 0};
42 
43 class VerifyContentSignatureTask : public CryptoTask {
44  public:
VerifyContentSignatureTask(const nsACString & aData,const nsACString & aCSHeader,const nsACString & aCertChain,const nsACString & aHostname,RefPtr<Promise> & aPromise)45   VerifyContentSignatureTask(const nsACString& aData,
46                              const nsACString& aCSHeader,
47                              const nsACString& aCertChain,
48                              const nsACString& aHostname,
49                              RefPtr<Promise>& aPromise)
50       : mData(aData),
51         mCSHeader(aCSHeader),
52         mCertChain(aCertChain),
53         mHostname(aHostname),
54         mSignatureVerified(false),
55         mPromise(new nsMainThreadPtrHolder<Promise>(
56             "VerifyContentSignatureTask::mPromise", aPromise)) {}
57 
58  private:
59   virtual nsresult CalculateResult() override;
60   virtual void CallCallback(nsresult rv) override;
61 
62   nsCString mData;
63   nsCString mCSHeader;
64   nsCString mCertChain;
65   nsCString mHostname;
66   bool mSignatureVerified;
67   nsMainThreadPtrHandle<Promise> mPromise;
68 };
69 
70 NS_IMETHODIMP
AsyncVerifyContentSignature(const nsACString & aData,const nsACString & aCSHeader,const nsACString & aCertChain,const nsACString & aHostname,JSContext * aCx,Promise ** aPromise)71 ContentSignatureVerifier::AsyncVerifyContentSignature(
72     const nsACString& aData, const nsACString& aCSHeader,
73     const nsACString& aCertChain, const nsACString& aHostname, JSContext* aCx,
74     Promise** aPromise) {
75   NS_ENSURE_ARG_POINTER(aCx);
76 
77   nsIGlobalObject* globalObject = xpc::CurrentNativeGlobal(aCx);
78   if (NS_WARN_IF(!globalObject)) {
79     return NS_ERROR_UNEXPECTED;
80   }
81 
82   ErrorResult result;
83   RefPtr<Promise> promise = Promise::Create(globalObject, result);
84   if (NS_WARN_IF(result.Failed())) {
85     return result.StealNSResult();
86   }
87 
88   RefPtr<VerifyContentSignatureTask> task(new VerifyContentSignatureTask(
89       aData, aCSHeader, aCertChain, aHostname, promise));
90   nsresult rv = task->Dispatch();
91   if (NS_FAILED(rv)) {
92     return rv;
93   }
94 
95   promise.forget(aPromise);
96   return NS_OK;
97 }
98 
99 static nsresult VerifyContentSignatureInternal(
100     const nsACString& aData, const nsACString& aCSHeader,
101     const nsACString& aCertChain, const nsACString& aHostname,
102     /* out */
103     mozilla::Telemetry::LABELS_CONTENT_SIGNATURE_VERIFICATION_ERRORS&
104         aErrorLabel,
105     /* out */ nsACString& aCertFingerprint, /* out */ uint32_t& aErrorValue);
106 static nsresult ParseContentSignatureHeader(
107     const nsACString& aContentSignatureHeader,
108     /* out */ nsCString& aSignature);
109 
CalculateResult()110 nsresult VerifyContentSignatureTask::CalculateResult() {
111   // 3 is the default, non-specific, "something failed" error.
112   Telemetry::LABELS_CONTENT_SIGNATURE_VERIFICATION_ERRORS errorLabel =
113       Telemetry::LABELS_CONTENT_SIGNATURE_VERIFICATION_ERRORS::err3;
114   nsAutoCString certFingerprint;
115   uint32_t errorValue = 3;
116   nsresult rv =
117       VerifyContentSignatureInternal(mData, mCSHeader, mCertChain, mHostname,
118                                      errorLabel, certFingerprint, errorValue);
119   if (NS_FAILED(rv)) {
120     CSVerifier_LOG(("CSVerifier: Signature verification failed"));
121     if (certFingerprint.Length() > 0) {
122       Telemetry::AccumulateCategoricalKeyed(certFingerprint, errorLabel);
123     }
124     Accumulate(Telemetry::CONTENT_SIGNATURE_VERIFICATION_STATUS, errorValue);
125     if (rv == NS_ERROR_INVALID_SIGNATURE) {
126       return NS_OK;
127     }
128     return rv;
129   }
130 
131   mSignatureVerified = true;
132   Accumulate(Telemetry::CONTENT_SIGNATURE_VERIFICATION_STATUS, 0);
133 
134   return NS_OK;
135 }
136 
CallCallback(nsresult rv)137 void VerifyContentSignatureTask::CallCallback(nsresult rv) {
138   if (NS_FAILED(rv)) {
139     mPromise->MaybeReject(rv);
140   } else {
141     mPromise->MaybeResolve(mSignatureVerified);
142   }
143 }
144 
IsNewLine(char16_t c)145 bool IsNewLine(char16_t c) { return c == '\n' || c == '\r'; }
146 
ReadChainIntoCertList(const nsACString & aCertChain,nsTArray<nsTArray<uint8_t>> & aCertList)147 nsresult ReadChainIntoCertList(const nsACString& aCertChain,
148                                nsTArray<nsTArray<uint8_t>>& aCertList) {
149   bool inBlock = false;
150   bool certFound = false;
151 
152   const nsCString header = "-----BEGIN CERTIFICATE-----"_ns;
153   const nsCString footer = "-----END CERTIFICATE-----"_ns;
154 
155   nsCWhitespaceTokenizerTemplate<IsNewLine> tokenizer(aCertChain);
156 
157   nsAutoCString blockData;
158   while (tokenizer.hasMoreTokens()) {
159     nsDependentCSubstring token = tokenizer.nextToken();
160     if (token.IsEmpty()) {
161       continue;
162     }
163     if (inBlock) {
164       if (token.Equals(footer)) {
165         inBlock = false;
166         certFound = true;
167         // base64 decode data, make certs, append to chain
168         nsAutoCString derString;
169         nsresult rv = Base64Decode(blockData, derString);
170         if (NS_FAILED(rv)) {
171           CSVerifier_LOG(("CSVerifier: decoding the signature failed"));
172           return rv;
173         }
174         nsTArray<uint8_t> derBytes(derString.Data(), derString.Length());
175         aCertList.AppendElement(std::move(derBytes));
176       } else {
177         blockData.Append(token);
178       }
179     } else if (token.Equals(header)) {
180       inBlock = true;
181       blockData = "";
182     }
183   }
184   if (inBlock || !certFound) {
185     // the PEM data did not end; bad data.
186     CSVerifier_LOG(("CSVerifier: supplied chain contains bad data"));
187     return NS_ERROR_FAILURE;
188   }
189   return NS_OK;
190 }
191 
192 // Given data to verify, a content signature header value, a string representing
193 // a list of PEM-encoded certificates, and a hostname to validate the
194 // certificates against, this function attempts to validate the certificate
195 // chain, extract the signature from the header, and verify the data using the
196 // key in the end-entity certificate from the chain. Returns NS_OK if everything
197 // is satisfactory and a failing nsresult otherwise. The output parameters are
198 // filled with telemetry data to report in the case of failures.
VerifyContentSignatureInternal(const nsACString & aData,const nsACString & aCSHeader,const nsACString & aCertChain,const nsACString & aHostname,Telemetry::LABELS_CONTENT_SIGNATURE_VERIFICATION_ERRORS & aErrorLabel,nsACString & aCertFingerprint,uint32_t & aErrorValue)199 static nsresult VerifyContentSignatureInternal(
200     const nsACString& aData, const nsACString& aCSHeader,
201     const nsACString& aCertChain, const nsACString& aHostname,
202     /* out */
203     Telemetry::LABELS_CONTENT_SIGNATURE_VERIFICATION_ERRORS& aErrorLabel,
204     /* out */ nsACString& aCertFingerprint,
205     /* out */ uint32_t& aErrorValue) {
206   nsTArray<nsTArray<uint8_t>> certList;
207   nsresult rv = ReadChainIntoCertList(aCertChain, certList);
208   if (NS_FAILED(rv)) {
209     return rv;
210   }
211   if (certList.Length() < 1) {
212     return NS_ERROR_FAILURE;
213   }
214   // The 0th element should be the end-entity that issued the content
215   // signature.
216   nsTArray<uint8_t>& certBytes(certList.ElementAt(0));
217   Input certInput;
218   mozilla::pkix::Result result =
219       certInput.Init(certBytes.Elements(), certBytes.Length());
220   if (result != Success) {
221     return NS_ERROR_FAILURE;
222   }
223 
224   // Get EE certificate fingerprint for telemetry.
225   unsigned char fingerprint[SHA256_LENGTH] = {0};
226   SECStatus srv =
227       PK11_HashBuf(SEC_OID_SHA256, fingerprint, certInput.UnsafeGetData(),
228                    certInput.GetLength());
229   if (srv != SECSuccess) {
230     return NS_ERROR_FAILURE;
231   }
232   SECItem fingerprintItem = {siBuffer, fingerprint, SHA256_LENGTH};
233   UniquePORTString tmpFingerprintString(
234       CERT_Hexify(&fingerprintItem, false /* don't use colon delimiters */));
235   if (!tmpFingerprintString) {
236     return NS_ERROR_OUT_OF_MEMORY;
237   }
238   aCertFingerprint.Assign(tmpFingerprintString.get());
239 
240   // Check the signerCert chain is good
241   CSTrustDomain trustDomain(certList);
242   result = BuildCertChain(
243       trustDomain, certInput, Now(), EndEntityOrCA::MustBeEndEntity,
244       KeyUsage::noParticularKeyUsageRequired, KeyPurposeId::id_kp_codeSigning,
245       CertPolicyId::anyPolicy, nullptr /*stapledOCSPResponse*/);
246   if (result != Success) {
247     // if there was a library error, return an appropriate error
248     if (IsFatalError(result)) {
249       return NS_ERROR_FAILURE;
250     }
251     // otherwise, assume the signature was invalid
252     if (result == mozilla::pkix::Result::ERROR_EXPIRED_CERTIFICATE) {
253       aErrorLabel =
254           Telemetry::LABELS_CONTENT_SIGNATURE_VERIFICATION_ERRORS::err4;
255       aErrorValue = 4;
256     } else if (result ==
257                mozilla::pkix::Result::ERROR_NOT_YET_VALID_CERTIFICATE) {
258       aErrorLabel =
259           Telemetry::LABELS_CONTENT_SIGNATURE_VERIFICATION_ERRORS::err5;
260       aErrorValue = 5;
261     } else {
262       // Building cert chain failed for some other reason.
263       aErrorLabel =
264           Telemetry::LABELS_CONTENT_SIGNATURE_VERIFICATION_ERRORS::err6;
265       aErrorValue = 6;
266     }
267     CSVerifier_LOG(("CSVerifier: The supplied chain is bad (%s)",
268                     MapResultToName(result)));
269     return NS_ERROR_INVALID_SIGNATURE;
270   }
271 
272   // Check the SAN
273   Input hostnameInput;
274 
275   result = hostnameInput.Init(
276       BitwiseCast<const uint8_t*, const char*>(aHostname.BeginReading()),
277       aHostname.Length());
278   if (result != Success) {
279     return NS_ERROR_FAILURE;
280   }
281 
282   BRNameMatchingPolicy nameMatchingPolicy(BRNameMatchingPolicy::Mode::Enforce);
283   result = CheckCertHostname(certInput, hostnameInput, nameMatchingPolicy);
284   if (result != Success) {
285     // EE cert isnot valid for the given host name.
286     aErrorLabel = Telemetry::LABELS_CONTENT_SIGNATURE_VERIFICATION_ERRORS::err7;
287     aErrorValue = 7;
288     return NS_ERROR_INVALID_SIGNATURE;
289   }
290 
291   pkix::BackCert backCert(certInput, EndEntityOrCA::MustBeEndEntity, nullptr);
292   result = backCert.Init();
293   // This should never fail, because we've already built a verified certificate
294   // chain with this certificate.
295   if (result != Success) {
296     aErrorLabel = Telemetry::LABELS_CONTENT_SIGNATURE_VERIFICATION_ERRORS::err8;
297     aErrorValue = 8;
298     CSVerifier_LOG(("CSVerifier: couldn't decode certificate to get spki"));
299     return NS_ERROR_INVALID_SIGNATURE;
300   }
301   Input spkiInput = backCert.GetSubjectPublicKeyInfo();
302   SECItem spkiItem = {siBuffer, const_cast<uint8_t*>(spkiInput.UnsafeGetData()),
303                       spkiInput.GetLength()};
304   UniqueCERTSubjectPublicKeyInfo spki(
305       SECKEY_DecodeDERSubjectPublicKeyInfo(&spkiItem));
306   if (!spki) {
307     aErrorLabel = Telemetry::LABELS_CONTENT_SIGNATURE_VERIFICATION_ERRORS::err8;
308     aErrorValue = 8;
309     CSVerifier_LOG(("CSVerifier: couldn't decode spki"));
310     return NS_ERROR_INVALID_SIGNATURE;
311   }
312   mozilla::UniqueSECKEYPublicKey key(SECKEY_ExtractPublicKey(spki.get()));
313   if (!key) {
314     aErrorLabel = Telemetry::LABELS_CONTENT_SIGNATURE_VERIFICATION_ERRORS::err8;
315     aErrorValue = 8;
316     CSVerifier_LOG(("CSVerifier: unable to extract a key"));
317     return NS_ERROR_INVALID_SIGNATURE;
318   }
319 
320   nsAutoCString signature;
321   rv = ParseContentSignatureHeader(aCSHeader, signature);
322   if (NS_FAILED(rv)) {
323     return rv;
324   }
325 
326   // Base 64 decode the signature
327   nsAutoCString rawSignature;
328   rv = Base64Decode(signature, rawSignature);
329   if (NS_FAILED(rv)) {
330     CSVerifier_LOG(("CSVerifier: decoding the signature failed"));
331     return rv;
332   }
333 
334   // get signature object
335   ScopedAutoSECItem signatureItem;
336   SECItem rawSignatureItem = {
337       siBuffer,
338       BitwiseCast<unsigned char*, const char*>(rawSignature.get()),
339       uint32_t(rawSignature.Length()),
340   };
341   // We have a raw ecdsa signature r||s so we have to DER-encode it first
342   // Note that we have to check rawSignatureItem->len % 2 here as
343   // DSAU_EncodeDerSigWithLen asserts this
344   if (rawSignatureItem.len == 0 || rawSignatureItem.len % 2 != 0) {
345     CSVerifier_LOG(("CSVerifier: signature length is bad"));
346     return NS_ERROR_FAILURE;
347   }
348   if (DSAU_EncodeDerSigWithLen(&signatureItem, &rawSignatureItem,
349                                rawSignatureItem.len) != SECSuccess) {
350     CSVerifier_LOG(("CSVerifier: encoding the signature failed"));
351     return NS_ERROR_FAILURE;
352   }
353 
354   // this is the only OID we support for now
355   SECOidTag oid = SEC_OID_ANSIX962_ECDSA_SHA384_SIGNATURE;
356   mozilla::UniqueVFYContext cx(
357       VFY_CreateContext(key.get(), &signatureItem, oid, nullptr));
358   if (!cx) {
359     // Creating context failed.
360     aErrorLabel = Telemetry::LABELS_CONTENT_SIGNATURE_VERIFICATION_ERRORS::err9;
361     aErrorValue = 9;
362     return NS_ERROR_INVALID_SIGNATURE;
363   }
364 
365   if (VFY_Begin(cx.get()) != SECSuccess) {
366     // Creating context failed.
367     aErrorLabel = Telemetry::LABELS_CONTENT_SIGNATURE_VERIFICATION_ERRORS::err9;
368     aErrorValue = 9;
369     return NS_ERROR_INVALID_SIGNATURE;
370   }
371   if (VFY_Update(cx.get(), kPREFIX, sizeof(kPREFIX)) != SECSuccess) {
372     aErrorLabel = Telemetry::LABELS_CONTENT_SIGNATURE_VERIFICATION_ERRORS::err1;
373     aErrorValue = 1;
374     return NS_ERROR_INVALID_SIGNATURE;
375   }
376   if (VFY_Update(cx.get(),
377                  reinterpret_cast<const unsigned char*>(aData.BeginReading()),
378                  aData.Length()) != SECSuccess) {
379     aErrorLabel = Telemetry::LABELS_CONTENT_SIGNATURE_VERIFICATION_ERRORS::err1;
380     aErrorValue = 1;
381     return NS_ERROR_INVALID_SIGNATURE;
382   }
383   if (VFY_End(cx.get()) != SECSuccess) {
384     aErrorLabel = Telemetry::LABELS_CONTENT_SIGNATURE_VERIFICATION_ERRORS::err1;
385     aErrorValue = 1;
386     return NS_ERROR_INVALID_SIGNATURE;
387   }
388 
389   return NS_OK;
390 }
391 
ParseContentSignatureHeader(const nsACString & aContentSignatureHeader,nsCString & aSignature)392 static nsresult ParseContentSignatureHeader(
393     const nsACString& aContentSignatureHeader,
394     /* out */ nsCString& aSignature) {
395   // We only support p384 ecdsa.
396   constexpr auto signature_var = "p384ecdsa"_ns;
397 
398   aSignature.Truncate();
399 
400   const nsCString& flatHeader = PromiseFlatCString(aContentSignatureHeader);
401   nsSecurityHeaderParser parser(flatHeader);
402   nsresult rv = parser.Parse();
403   if (NS_FAILED(rv)) {
404     CSVerifier_LOG(("CSVerifier: could not parse ContentSignature header"));
405     return NS_ERROR_FAILURE;
406   }
407   LinkedList<nsSecurityHeaderDirective>* directives = parser.GetDirectives();
408 
409   for (nsSecurityHeaderDirective* directive = directives->getFirst();
410        directive != nullptr; directive = directive->getNext()) {
411     CSVerifier_LOG(
412         ("CSVerifier: found directive '%s'", directive->mName.get()));
413     if (directive->mName.Length() == signature_var.Length() &&
414         directive->mName.EqualsIgnoreCase(signature_var.get(),
415                                           signature_var.Length())) {
416       if (!aSignature.IsEmpty()) {
417         CSVerifier_LOG(("CSVerifier: found two ContentSignatures"));
418         return NS_ERROR_INVALID_SIGNATURE;
419       }
420 
421       CSVerifier_LOG(("CSVerifier: found a ContentSignature directive"));
422       aSignature.Assign(directive->mValue);
423     }
424   }
425 
426   // we have to ensure that we found a signature at this point
427   if (aSignature.IsEmpty()) {
428     CSVerifier_LOG(
429         ("CSVerifier: got a Content-Signature header but didn't find a "
430          "signature."));
431     return NS_ERROR_FAILURE;
432   }
433 
434   // Bug 769521: We have to change b64 url to regular encoding as long as we
435   // don't have a b64 url decoder. This should change soon, but in the meantime
436   // we have to live with this.
437   aSignature.ReplaceChar('-', '+');
438   aSignature.ReplaceChar('_', '/');
439 
440   return NS_OK;
441 }
442