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