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/. */
7 #include "ContentSignatureVerifier.h"
9 #include "AppTrustDomain.h"
10 #include "CryptoTask.h"
11 #include "ScopedNSSTypes.h"
12 #include "SharedCertVerifier.h"
15 #include "mozilla/Base64.h"
16 #include "mozilla/Logging.h"
17 #include "mozilla/dom/Promise.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"
28 NS_IMPL_ISUPPORTS(ContentSignatureVerifier
, nsIContentSignatureVerifier
)
30 using namespace mozilla
;
31 using namespace mozilla::pkix
;
32 using namespace mozilla::psm
;
35 static LazyLogModule
gCSVerifierPRLog("ContentSignatureVerifier");
36 #define CSVerifier_LOG(args) MOZ_LOG(gCSVerifierPRLog, LogLevel::Debug, args)
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};
43 class VerifyContentSignatureTask
: public CryptoTask
{
45 VerifyContentSignatureTask(const nsACString
& aData
,
46 const nsACString
& aCSHeader
,
47 const nsACString
& aCertChain
,
48 const nsACString
& aHostname
,
49 AppTrustedRoot aTrustedRoot
,
50 RefPtr
<Promise
>& aPromise
)
53 mCertChain(aCertChain
),
55 mTrustedRoot(aTrustedRoot
),
56 mSignatureVerified(false),
57 mPromise(new nsMainThreadPtrHolder
<Promise
>(
58 "VerifyContentSignatureTask::mPromise", aPromise
)) {}
61 virtual nsresult
CalculateResult() override
;
62 virtual void CallCallback(nsresult rv
) override
;
68 AppTrustedRoot mTrustedRoot
;
69 bool mSignatureVerified
;
70 nsMainThreadPtrHandle
<Promise
> mPromise
;
74 ContentSignatureVerifier::AsyncVerifyContentSignature(
75 const nsACString
& aData
, const nsACString
& aCSHeader
,
76 const nsACString
& aCertChain
, const nsACString
& aHostname
,
77 AppTrustedRoot aTrustedRoot
, JSContext
* aCx
, Promise
** aPromise
) {
78 NS_ENSURE_ARG_POINTER(aCx
);
80 nsIGlobalObject
* globalObject
= xpc::CurrentNativeGlobal(aCx
);
81 if (NS_WARN_IF(!globalObject
)) {
82 return NS_ERROR_UNEXPECTED
;
86 RefPtr
<Promise
> promise
= Promise::Create(globalObject
, result
);
87 if (NS_WARN_IF(result
.Failed())) {
88 return result
.StealNSResult();
91 RefPtr
<VerifyContentSignatureTask
> task(new VerifyContentSignatureTask(
92 aData
, aCSHeader
, aCertChain
, aHostname
, aTrustedRoot
, promise
));
93 nsresult rv
= task
->Dispatch();
98 promise
.forget(aPromise
);
102 static nsresult
VerifyContentSignatureInternal(
103 const nsACString
& aData
, const nsACString
& aCSHeader
,
104 const nsACString
& aCertChain
, const nsACString
& aHostname
,
105 AppTrustedRoot aTrustedRoot
,
107 mozilla::Telemetry::LABELS_CONTENT_SIGNATURE_VERIFICATION_ERRORS
&
109 /* out */ nsACString
& aCertFingerprint
, /* out */ uint32_t& aErrorValue
);
110 static nsresult
ParseContentSignatureHeader(
111 const nsACString
& aContentSignatureHeader
,
112 /* out */ nsCString
& aSignature
);
114 nsresult
VerifyContentSignatureTask::CalculateResult() {
115 // 3 is the default, non-specific, "something failed" error.
116 Telemetry::LABELS_CONTENT_SIGNATURE_VERIFICATION_ERRORS errorLabel
=
117 Telemetry::LABELS_CONTENT_SIGNATURE_VERIFICATION_ERRORS::err3
;
118 nsAutoCString certFingerprint
;
119 uint32_t errorValue
= 3;
120 nsresult rv
= VerifyContentSignatureInternal(
121 mData
, mCSHeader
, mCertChain
, mHostname
, mTrustedRoot
, errorLabel
,
122 certFingerprint
, errorValue
);
124 CSVerifier_LOG(("CSVerifier: Signature verification failed"));
125 if (certFingerprint
.Length() > 0) {
126 Telemetry::AccumulateCategoricalKeyed(certFingerprint
, errorLabel
);
128 Accumulate(Telemetry::CONTENT_SIGNATURE_VERIFICATION_STATUS
, errorValue
);
129 if (rv
== NS_ERROR_INVALID_SIGNATURE
) {
135 mSignatureVerified
= true;
136 Accumulate(Telemetry::CONTENT_SIGNATURE_VERIFICATION_STATUS
, 0);
141 void VerifyContentSignatureTask::CallCallback(nsresult rv
) {
143 mPromise
->MaybeReject(rv
);
145 mPromise
->MaybeResolve(mSignatureVerified
);
149 bool IsNewLine(char16_t c
) { return c
== '\n' || c
== '\r'; }
151 nsresult
ReadChainIntoCertList(const nsACString
& aCertChain
,
152 nsTArray
<nsTArray
<uint8_t>>& aCertList
) {
153 bool inBlock
= false;
154 bool certFound
= false;
156 const nsCString header
= "-----BEGIN CERTIFICATE-----"_ns
;
157 const nsCString footer
= "-----END CERTIFICATE-----"_ns
;
159 nsCWhitespaceTokenizerTemplate
<IsNewLine
> tokenizer(aCertChain
);
161 nsAutoCString blockData
;
162 while (tokenizer
.hasMoreTokens()) {
163 nsDependentCSubstring token
= tokenizer
.nextToken();
164 if (token
.IsEmpty()) {
168 if (token
.Equals(footer
)) {
171 // base64 decode data, make certs, append to chain
172 nsAutoCString derString
;
173 nsresult rv
= Base64Decode(blockData
, derString
);
175 CSVerifier_LOG(("CSVerifier: decoding the signature failed"));
178 nsTArray
<uint8_t> derBytes(derString
.Data(), derString
.Length());
179 aCertList
.AppendElement(std::move(derBytes
));
181 blockData
.Append(token
);
183 } else if (token
.Equals(header
)) {
188 if (inBlock
|| !certFound
) {
189 // the PEM data did not end; bad data.
190 CSVerifier_LOG(("CSVerifier: supplied chain contains bad data"));
191 return NS_ERROR_FAILURE
;
196 // Given data to verify, a content signature header value, a string representing
197 // a list of PEM-encoded certificates, and a hostname to validate the
198 // certificates against, this function attempts to validate the certificate
199 // chain, extract the signature from the header, and verify the data using the
200 // key in the end-entity certificate from the chain. Returns NS_OK if everything
201 // is satisfactory and a failing nsresult otherwise. The output parameters are
202 // filled with telemetry data to report in the case of failures.
203 static nsresult
VerifyContentSignatureInternal(
204 const nsACString
& aData
, const nsACString
& aCSHeader
,
205 const nsACString
& aCertChain
, const nsACString
& aHostname
,
206 AppTrustedRoot aTrustedRoot
,
208 Telemetry::LABELS_CONTENT_SIGNATURE_VERIFICATION_ERRORS
& aErrorLabel
,
209 /* out */ nsACString
& aCertFingerprint
,
210 /* out */ uint32_t& aErrorValue
) {
211 nsTArray
<nsTArray
<uint8_t>> certList
;
212 nsresult rv
= ReadChainIntoCertList(aCertChain
, certList
);
216 if (certList
.Length() < 1) {
217 return NS_ERROR_FAILURE
;
219 // The 0th element should be the end-entity that issued the content
221 nsTArray
<uint8_t>& certBytes(certList
.ElementAt(0));
223 mozilla::pkix::Result result
=
224 certInput
.Init(certBytes
.Elements(), certBytes
.Length());
225 if (result
!= Success
) {
226 return NS_ERROR_FAILURE
;
229 // Get EE certificate fingerprint for telemetry.
230 unsigned char fingerprint
[SHA256_LENGTH
] = {0};
232 PK11_HashBuf(SEC_OID_SHA256
, fingerprint
, certInput
.UnsafeGetData(),
233 certInput
.GetLength());
234 if (srv
!= SECSuccess
) {
235 return NS_ERROR_FAILURE
;
237 SECItem fingerprintItem
= {siBuffer
, fingerprint
, SHA256_LENGTH
};
238 UniquePORTString
tmpFingerprintString(
239 CERT_Hexify(&fingerprintItem
, false /* don't use colon delimiters */));
240 if (!tmpFingerprintString
) {
241 return NS_ERROR_OUT_OF_MEMORY
;
243 aCertFingerprint
.Assign(tmpFingerprintString
.get());
245 nsTArray
<Span
<const uint8_t>> certSpans
;
246 // Collect just the CAs.
247 for (size_t i
= 1; i
< certList
.Length(); i
++) {
248 Span
<const uint8_t> certSpan(certList
.ElementAt(i
).Elements(),
249 certList
.ElementAt(i
).Length());
250 certSpans
.AppendElement(std::move(certSpan
));
252 AppTrustDomain
trustDomain(std::move(certSpans
));
253 rv
= trustDomain
.SetTrustedRoot(aTrustedRoot
);
257 // Check the signerCert chain is good
258 result
= BuildCertChain(
259 trustDomain
, certInput
, Now(), EndEntityOrCA::MustBeEndEntity
,
260 KeyUsage::noParticularKeyUsageRequired
, KeyPurposeId::id_kp_codeSigning
,
261 CertPolicyId::anyPolicy
, nullptr /*stapledOCSPResponse*/);
262 if (result
!= Success
) {
263 // if there was a library error, return an appropriate error
264 if (IsFatalError(result
)) {
265 return NS_ERROR_FAILURE
;
267 // otherwise, assume the signature was invalid
268 if (result
== mozilla::pkix::Result::ERROR_EXPIRED_CERTIFICATE
) {
270 Telemetry::LABELS_CONTENT_SIGNATURE_VERIFICATION_ERRORS::err4
;
273 mozilla::pkix::Result::ERROR_NOT_YET_VALID_CERTIFICATE
) {
275 Telemetry::LABELS_CONTENT_SIGNATURE_VERIFICATION_ERRORS::err5
;
278 // Building cert chain failed for some other reason.
280 Telemetry::LABELS_CONTENT_SIGNATURE_VERIFICATION_ERRORS::err6
;
283 CSVerifier_LOG(("CSVerifier: The supplied chain is bad (%s)",
284 MapResultToName(result
)));
285 return NS_ERROR_INVALID_SIGNATURE
;
291 result
= hostnameInput
.Init(
292 BitwiseCast
<const uint8_t*, const char*>(aHostname
.BeginReading()),
294 if (result
!= Success
) {
295 return NS_ERROR_FAILURE
;
298 result
= CheckCertHostname(certInput
, hostnameInput
);
299 if (result
!= Success
) {
300 // EE cert isnot valid for the given host name.
301 aErrorLabel
= Telemetry::LABELS_CONTENT_SIGNATURE_VERIFICATION_ERRORS::err7
;
303 return NS_ERROR_INVALID_SIGNATURE
;
306 pkix::BackCert
backCert(certInput
, EndEntityOrCA::MustBeEndEntity
, nullptr);
307 result
= backCert
.Init();
308 // This should never fail, because we've already built a verified certificate
309 // chain with this certificate.
310 if (result
!= Success
) {
311 aErrorLabel
= Telemetry::LABELS_CONTENT_SIGNATURE_VERIFICATION_ERRORS::err8
;
313 CSVerifier_LOG(("CSVerifier: couldn't decode certificate to get spki"));
314 return NS_ERROR_INVALID_SIGNATURE
;
316 Input spkiInput
= backCert
.GetSubjectPublicKeyInfo();
317 SECItem spkiItem
= {siBuffer
, const_cast<uint8_t*>(spkiInput
.UnsafeGetData()),
318 spkiInput
.GetLength()};
319 UniqueCERTSubjectPublicKeyInfo
spki(
320 SECKEY_DecodeDERSubjectPublicKeyInfo(&spkiItem
));
322 aErrorLabel
= Telemetry::LABELS_CONTENT_SIGNATURE_VERIFICATION_ERRORS::err8
;
324 CSVerifier_LOG(("CSVerifier: couldn't decode spki"));
325 return NS_ERROR_INVALID_SIGNATURE
;
327 mozilla::UniqueSECKEYPublicKey
key(SECKEY_ExtractPublicKey(spki
.get()));
329 aErrorLabel
= Telemetry::LABELS_CONTENT_SIGNATURE_VERIFICATION_ERRORS::err8
;
331 CSVerifier_LOG(("CSVerifier: unable to extract a key"));
332 return NS_ERROR_INVALID_SIGNATURE
;
335 nsAutoCString signature
;
336 rv
= ParseContentSignatureHeader(aCSHeader
, signature
);
341 // Base 64 decode the signature
342 nsAutoCString rawSignature
;
343 rv
= Base64Decode(signature
, rawSignature
);
345 CSVerifier_LOG(("CSVerifier: decoding the signature failed"));
349 // get signature object
350 ScopedAutoSECItem signatureItem
;
351 SECItem rawSignatureItem
= {
353 BitwiseCast
<unsigned char*, const char*>(rawSignature
.get()),
354 uint32_t(rawSignature
.Length()),
356 // We have a raw ecdsa signature r||s so we have to DER-encode it first
357 // Note that we have to check rawSignatureItem->len % 2 here as
358 // DSAU_EncodeDerSigWithLen asserts this
359 if (rawSignatureItem
.len
== 0 || rawSignatureItem
.len
% 2 != 0) {
360 CSVerifier_LOG(("CSVerifier: signature length is bad"));
361 return NS_ERROR_FAILURE
;
363 if (DSAU_EncodeDerSigWithLen(&signatureItem
, &rawSignatureItem
,
364 rawSignatureItem
.len
) != SECSuccess
) {
365 CSVerifier_LOG(("CSVerifier: encoding the signature failed"));
366 return NS_ERROR_FAILURE
;
369 // this is the only OID we support for now
370 SECOidTag oid
= SEC_OID_ANSIX962_ECDSA_SHA384_SIGNATURE
;
371 mozilla::UniqueVFYContext
cx(
372 VFY_CreateContext(key
.get(), &signatureItem
, oid
, nullptr));
374 // Creating context failed.
375 aErrorLabel
= Telemetry::LABELS_CONTENT_SIGNATURE_VERIFICATION_ERRORS::err9
;
377 return NS_ERROR_INVALID_SIGNATURE
;
380 if (VFY_Begin(cx
.get()) != SECSuccess
) {
381 // Creating context failed.
382 aErrorLabel
= Telemetry::LABELS_CONTENT_SIGNATURE_VERIFICATION_ERRORS::err9
;
384 return NS_ERROR_INVALID_SIGNATURE
;
386 if (VFY_Update(cx
.get(), kPREFIX
, sizeof(kPREFIX
)) != SECSuccess
) {
387 aErrorLabel
= Telemetry::LABELS_CONTENT_SIGNATURE_VERIFICATION_ERRORS::err1
;
389 return NS_ERROR_INVALID_SIGNATURE
;
391 if (VFY_Update(cx
.get(),
392 reinterpret_cast<const unsigned char*>(aData
.BeginReading()),
393 aData
.Length()) != SECSuccess
) {
394 aErrorLabel
= Telemetry::LABELS_CONTENT_SIGNATURE_VERIFICATION_ERRORS::err1
;
396 return NS_ERROR_INVALID_SIGNATURE
;
398 if (VFY_End(cx
.get()) != SECSuccess
) {
399 aErrorLabel
= Telemetry::LABELS_CONTENT_SIGNATURE_VERIFICATION_ERRORS::err1
;
401 return NS_ERROR_INVALID_SIGNATURE
;
407 static nsresult
ParseContentSignatureHeader(
408 const nsACString
& aContentSignatureHeader
,
409 /* out */ nsCString
& aSignature
) {
410 // We only support p384 ecdsa.
411 constexpr auto signature_var
= "p384ecdsa"_ns
;
413 aSignature
.Truncate();
415 const nsCString
& flatHeader
= PromiseFlatCString(aContentSignatureHeader
);
416 nsSecurityHeaderParser
parser(flatHeader
);
417 nsresult rv
= parser
.Parse();
419 CSVerifier_LOG(("CSVerifier: could not parse ContentSignature header"));
420 return NS_ERROR_FAILURE
;
422 LinkedList
<nsSecurityHeaderDirective
>* directives
= parser
.GetDirectives();
424 for (nsSecurityHeaderDirective
* directive
= directives
->getFirst();
425 directive
!= nullptr; directive
= directive
->getNext()) {
427 ("CSVerifier: found directive '%s'", directive
->mName
.get()));
428 if (directive
->mName
.EqualsIgnoreCase(signature_var
)) {
429 if (!aSignature
.IsEmpty()) {
430 CSVerifier_LOG(("CSVerifier: found two ContentSignatures"));
431 return NS_ERROR_INVALID_SIGNATURE
;
433 if (directive
->mValue
.isNothing()) {
434 CSVerifier_LOG(("CSVerifier: found empty ContentSignature directive"));
435 return NS_ERROR_INVALID_SIGNATURE
;
438 CSVerifier_LOG(("CSVerifier: found a ContentSignature directive"));
439 aSignature
.Assign(*(directive
->mValue
));
443 // we have to ensure that we found a signature at this point
444 if (aSignature
.IsEmpty()) {
446 ("CSVerifier: got a Content-Signature header but didn't find a "
448 return NS_ERROR_FAILURE
;
451 // Bug 769521: We have to change b64 url to regular encoding as long as we
452 // don't have a b64 url decoder. This should change soon, but in the meantime
453 // we have to live with this.
454 aSignature
.ReplaceChar('-', '+');
455 aSignature
.ReplaceChar('_', '/');