Coverage Report

Created: 2018-09-25 14:53

/src/mozilla-central/security/manager/ssl/PublicKeyPinningService.cpp
Line
Count
Source (jump to first uncovered line)
1
/* This Source Code Form is subject to the terms of the Mozilla Public
2
 * License, v. 2.0. If a copy of the MPL was not distributed with this
3
 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
4
5
#include "PublicKeyPinningService.h"
6
7
#include "RootCertificateTelemetryUtils.h"
8
#include "mozilla/ArrayUtils.h"
9
#include "mozilla/Base64.h"
10
#include "mozilla/BinarySearch.h"
11
#include "mozilla/Casting.h"
12
#include "mozilla/Logging.h"
13
#include "mozilla/Telemetry.h"
14
#include "nsDependentString.h"
15
#include "nsISiteSecurityService.h"
16
#include "nsServiceManagerUtils.h"
17
#include "nsSiteSecurityService.h"
18
#include "pkix/pkixtypes.h"
19
#include "seccomon.h"
20
#include "sechash.h"
21
22
#include "StaticHPKPins.h" // autogenerated by genHPKPStaticpins.js
23
24
using namespace mozilla;
25
using namespace mozilla::pkix;
26
using namespace mozilla::psm;
27
28
LazyLogModule gPublicKeyPinningLog("PublicKeyPinningService");
29
30
/**
31
 Computes in the location specified by base64Out the SHA256 digest
32
 of the DER Encoded subject Public Key Info for the given cert
33
*/
34
static nsresult
35
GetBase64HashSPKI(const CERTCertificate* cert, nsACString& hashSPKIDigest)
36
0
{
37
0
  hashSPKIDigest.Truncate();
38
0
  Digest digest;
39
0
  nsresult rv = digest.DigestBuf(SEC_OID_SHA256, cert->derPublicKey.data,
40
0
                                 cert->derPublicKey.len);
41
0
  if (NS_FAILED(rv)) {
42
0
    return rv;
43
0
  }
44
0
  return Base64Encode(nsDependentCSubstring(
45
0
                        BitwiseCast<char*, unsigned char*>(digest.get().data),
46
0
                        digest.get().len),
47
0
                      hashSPKIDigest);
48
0
}
49
50
/*
51
 * Sets certMatchesPinset to true if a given cert matches any fingerprints from
52
 * the given pinset or the dynamicFingerprints array, or to false otherwise.
53
 */
54
static nsresult
55
EvalCert(const CERTCertificate* cert, const StaticFingerprints* fingerprints,
56
         const nsTArray<nsCString>* dynamicFingerprints,
57
 /*out*/ bool& certMatchesPinset)
58
0
{
59
0
  certMatchesPinset = false;
60
0
  if (!fingerprints && !dynamicFingerprints) {
61
0
    MOZ_LOG(gPublicKeyPinningLog, LogLevel::Debug,
62
0
           ("pkpin: No hashes found\n"));
63
0
    return NS_ERROR_INVALID_ARG;
64
0
  }
65
0
66
0
  nsAutoCString base64Out;
67
0
  nsresult rv = GetBase64HashSPKI(cert, base64Out);
68
0
  if (NS_FAILED(rv)) {
69
0
    MOZ_LOG(gPublicKeyPinningLog, LogLevel::Debug,
70
0
           ("pkpin: GetBase64HashSPKI failed!\n"));
71
0
    return rv;
72
0
  }
73
0
74
0
  if (fingerprints) {
75
0
    for (size_t i = 0; i < fingerprints->size; i++) {
76
0
      if (base64Out.Equals(fingerprints->data[i])) {
77
0
        MOZ_LOG(gPublicKeyPinningLog, LogLevel::Debug,
78
0
               ("pkpin: found pin base_64 ='%s'\n", base64Out.get()));
79
0
        certMatchesPinset = true;
80
0
        return NS_OK;
81
0
      }
82
0
    }
83
0
  }
84
0
  if (dynamicFingerprints) {
85
0
    for (size_t i = 0; i < dynamicFingerprints->Length(); i++) {
86
0
      if (base64Out.Equals((*dynamicFingerprints)[i])) {
87
0
        MOZ_LOG(gPublicKeyPinningLog, LogLevel::Debug,
88
0
               ("pkpin: found pin base_64 ='%s'\n", base64Out.get()));
89
0
        certMatchesPinset = true;
90
0
        return NS_OK;
91
0
      }
92
0
    }
93
0
  }
94
0
  return NS_OK;
95
0
}
96
97
/*
98
 * Sets certListIntersectsPinset to true if a given chain matches any
99
 * fingerprints from the given static fingerprints or the
100
 * dynamicFingerprints array, or to false otherwise.
101
 */
102
static nsresult
103
EvalChain(const RefPtr<nsNSSCertList>& certList,
104
          const StaticFingerprints* fingerprints,
105
          const nsTArray<nsCString>* dynamicFingerprints,
106
  /*out*/ bool& certListIntersectsPinset)
107
0
{
108
0
  certListIntersectsPinset = false;
109
0
  if (!fingerprints && !dynamicFingerprints) {
110
0
    MOZ_ASSERT(false, "Must pass in at least one type of pinset");
111
0
    return NS_ERROR_FAILURE;
112
0
  }
113
0
114
0
  certList->ForEachCertificateInChain(
115
0
    [&certListIntersectsPinset, &fingerprints, &dynamicFingerprints]
116
0
      (nsCOMPtr<nsIX509Cert> aCert, bool aHasMore, /* out */ bool& aContinue) {
117
0
      // We need an owning handle when calling nsIX509Cert::GetCert().
118
0
      UniqueCERTCertificate nssCert(aCert->GetCert());
119
0
      MOZ_LOG(gPublicKeyPinningLog, LogLevel::Debug,
120
0
             ("pkpin: certArray subject: '%s'\n", nssCert->subjectName));
121
0
      MOZ_LOG(gPublicKeyPinningLog, LogLevel::Debug,
122
0
             ("pkpin: certArray issuer: '%s'\n", nssCert->issuerName));
123
0
      nsresult rv = EvalCert(nssCert.get(), fingerprints, dynamicFingerprints,
124
0
                             certListIntersectsPinset);
125
0
      if (NS_FAILED(rv)) {
126
0
        return rv;
127
0
      }
128
0
129
0
      if (certListIntersectsPinset) {
130
0
        aContinue = false;
131
0
      }
132
0
      return NS_OK;
133
0
  });
134
0
135
0
  if (!certListIntersectsPinset) {
136
0
    MOZ_LOG(gPublicKeyPinningLog, LogLevel::Debug, ("pkpin: no matches found\n"));
137
0
  }
138
0
  return NS_OK;
139
0
}
140
141
class TransportSecurityPreloadBinarySearchComparator
142
{
143
public:
144
  explicit TransportSecurityPreloadBinarySearchComparator(
145
    const char* aTargetHost)
146
0
    : mTargetHost(aTargetHost) { }
147
148
  int operator()(const TransportSecurityPreload& val) const
149
0
  {
150
0
    return strcmp(mTargetHost, val.mHost);
151
0
  }
152
153
private:
154
  const char* mTargetHost; // non-owning
155
};
156
157
nsresult
158
PublicKeyPinningService::ChainMatchesPinset(const RefPtr<nsNSSCertList>& certList,
159
                                            const nsTArray<nsCString>& aSHA256keys,
160
                                    /*out*/ bool& chainMatchesPinset)
161
0
{
162
0
  return EvalChain(certList, nullptr, &aSHA256keys, chainMatchesPinset);
163
0
}
164
165
// Returns via one of the output parameters the most relevant pinning
166
// information that is valid for the given host at the given time.
167
// Dynamic pins are prioritized over static pins.
168
static nsresult
169
FindPinningInformation(const char* hostname, mozilla::pkix::Time time,
170
                       const OriginAttributes& originAttributes,
171
               /*out*/ nsTArray<nsCString>& dynamicFingerprints,
172
               /*out*/ const TransportSecurityPreload*& staticFingerprints)
173
0
{
174
0
  if (!hostname || hostname[0] == 0) {
175
0
    return NS_ERROR_INVALID_ARG;
176
0
  }
177
0
  staticFingerprints = nullptr;
178
0
  dynamicFingerprints.Clear();
179
0
  nsCOMPtr<nsISiteSecurityService> sssService =
180
0
    do_GetService(NS_SSSERVICE_CONTRACTID);
181
0
  if (!sssService) {
182
0
    return NS_ERROR_FAILURE;
183
0
  }
184
0
  const TransportSecurityPreload* foundEntry = nullptr;
185
0
  const char* evalHost = hostname;
186
0
  const char* evalPart;
187
0
  // Notice how the (xx = strchr) prevents pins for unqualified domain names.
188
0
  while (!foundEntry && (evalPart = strchr(evalHost, '.'))) {
189
0
    MOZ_LOG(gPublicKeyPinningLog, LogLevel::Debug,
190
0
           ("pkpin: Querying pinsets for host: '%s'\n", evalHost));
191
0
    // Attempt dynamic pins first
192
0
    nsresult rv;
193
0
    bool found;
194
0
    bool includeSubdomains;
195
0
    nsTArray<nsCString> pinArray;
196
0
    rv = sssService->GetKeyPinsForHostname(nsDependentCString(evalHost), time,
197
0
                                           originAttributes, pinArray,
198
0
                                           &includeSubdomains, &found);
199
0
    if (NS_FAILED(rv)) {
200
0
      return rv;
201
0
    }
202
0
    if (found && (evalHost == hostname || includeSubdomains)) {
203
0
      MOZ_LOG(gPublicKeyPinningLog, LogLevel::Debug,
204
0
             ("pkpin: Found dyn match for host: '%s'\n", evalHost));
205
0
      dynamicFingerprints = pinArray;
206
0
      return NS_OK;
207
0
    }
208
0
209
0
    size_t foundEntryIndex;
210
0
    if (BinarySearchIf(kPublicKeyPinningPreloadList, 0,
211
0
                       ArrayLength(kPublicKeyPinningPreloadList),
212
0
                       TransportSecurityPreloadBinarySearchComparator(evalHost),
213
0
                       &foundEntryIndex)) {
214
0
      foundEntry = &kPublicKeyPinningPreloadList[foundEntryIndex];
215
0
      MOZ_LOG(gPublicKeyPinningLog, LogLevel::Debug,
216
0
             ("pkpin: Found pinset for host: '%s'\n", evalHost));
217
0
      if (evalHost != hostname) {
218
0
        if (!foundEntry->mIncludeSubdomains) {
219
0
          // Does not apply to this host, continue iterating
220
0
          foundEntry = nullptr;
221
0
        }
222
0
      }
223
0
    } else {
224
0
      MOZ_LOG(gPublicKeyPinningLog, LogLevel::Debug,
225
0
             ("pkpin: Didn't find pinset for host: '%s'\n", evalHost));
226
0
    }
227
0
    // Add one for '.'
228
0
    evalHost = evalPart + 1;
229
0
  }
230
0
231
0
  if (foundEntry && foundEntry->pinset) {
232
0
    if (time > TimeFromEpochInSeconds(kPreloadPKPinsExpirationTime /
233
0
                                      PR_USEC_PER_SEC)) {
234
0
      return NS_OK;
235
0
    }
236
0
    staticFingerprints = foundEntry;
237
0
  }
238
0
  return NS_OK;
239
0
}
240
241
// Returns true via the output parameter if the given certificate list meets
242
// pinning requirements for the given host at the given time. It must be the
243
// case that either there is an intersection between the set of hashes of
244
// subject public key info data in the list and the most relevant non-expired
245
// pinset for the host or there is no pinning information for the host.
246
static nsresult
247
CheckPinsForHostname(const RefPtr<nsNSSCertList>& certList, const char* hostname,
248
                     bool enforceTestMode, mozilla::pkix::Time time,
249
                     const OriginAttributes& originAttributes,
250
             /*out*/ bool& chainHasValidPins,
251
    /*optional out*/ PinningTelemetryInfo* pinningTelemetryInfo)
252
0
{
253
0
  chainHasValidPins = false;
254
0
  if (!certList) {
255
0
    return NS_ERROR_INVALID_ARG;
256
0
  }
257
0
  if (!hostname || hostname[0] == 0) {
258
0
    return NS_ERROR_INVALID_ARG;
259
0
  }
260
0
261
0
  nsTArray<nsCString> dynamicFingerprints;
262
0
  const TransportSecurityPreload* staticFingerprints = nullptr;
263
0
  nsresult rv = FindPinningInformation(hostname, time, originAttributes,
264
0
                                       dynamicFingerprints, staticFingerprints);
265
0
  // If we have no pinning information, the certificate chain trivially
266
0
  // validates with respect to pinning.
267
0
  if (dynamicFingerprints.Length() == 0 && !staticFingerprints) {
268
0
    chainHasValidPins = true;
269
0
    return NS_OK;
270
0
  }
271
0
  if (dynamicFingerprints.Length() > 0) {
272
0
    return EvalChain(certList, nullptr, &dynamicFingerprints, chainHasValidPins);
273
0
  }
274
0
  if (staticFingerprints) {
275
0
    bool enforceTestModeResult;
276
0
    rv = EvalChain(certList, staticFingerprints->pinset, nullptr,
277
0
                   enforceTestModeResult);
278
0
    if (NS_FAILED(rv)) {
279
0
      return rv;
280
0
    }
281
0
    chainHasValidPins = enforceTestModeResult;
282
0
    Telemetry::HistogramID histogram = staticFingerprints->mIsMoz
283
0
      ? Telemetry::CERT_PINNING_MOZ_RESULTS
284
0
      : Telemetry::CERT_PINNING_RESULTS;
285
0
    if (staticFingerprints->mTestMode) {
286
0
      histogram = staticFingerprints->mIsMoz
287
0
        ? Telemetry::CERT_PINNING_MOZ_TEST_RESULTS
288
0
        : Telemetry::CERT_PINNING_TEST_RESULTS;
289
0
      if (!enforceTestMode) {
290
0
        chainHasValidPins = true;
291
0
      }
292
0
    }
293
0
    // We can collect per-host pinning violations for this host because it is
294
0
    // operationally critical to Firefox.
295
0
    if (pinningTelemetryInfo) {
296
0
      if (staticFingerprints->mId != kUnknownId) {
297
0
        int32_t bucket = staticFingerprints->mId * 2
298
0
                         + (enforceTestModeResult ? 1 : 0);
299
0
        histogram = staticFingerprints->mTestMode
300
0
          ? Telemetry::CERT_PINNING_MOZ_TEST_RESULTS_BY_HOST
301
0
          : Telemetry::CERT_PINNING_MOZ_RESULTS_BY_HOST;
302
0
        pinningTelemetryInfo->certPinningResultBucket = bucket;
303
0
      } else {
304
0
        pinningTelemetryInfo->certPinningResultBucket =
305
0
            enforceTestModeResult ? 1 : 0;
306
0
      }
307
0
      pinningTelemetryInfo->accumulateResult = true;
308
0
      pinningTelemetryInfo->certPinningResultHistogram = Some(histogram);
309
0
    }
310
0
311
0
    // We only collect per-CA pinning statistics upon failures.
312
0
    nsCOMPtr<nsIX509Cert> rootCert;
313
0
    rv = certList->GetRootCertificate(rootCert);
314
0
    if (NS_FAILED(rv)) {
315
0
      return rv;
316
0
    }
317
0
318
0
    // Only log telemetry if the certificate list is non-empty.
319
0
    if (rootCert && !enforceTestModeResult && pinningTelemetryInfo) {
320
0
      UniqueCERTCertificate rootCertObj =
321
0
        UniqueCERTCertificate(rootCert.get()->GetCert());
322
0
      if (rootCertObj) {
323
0
        int32_t binNumber = RootCABinNumber(&rootCertObj->derCert);
324
0
        if (binNumber != ROOT_CERTIFICATE_UNKNOWN ) {
325
0
          pinningTelemetryInfo->accumulateForRoot = true;
326
0
          pinningTelemetryInfo->rootBucket = binNumber;
327
0
        }
328
0
      }
329
0
    }
330
0
331
0
    MOZ_LOG(gPublicKeyPinningLog, LogLevel::Debug,
332
0
           ("pkpin: Pin check %s for %s host '%s' (mode=%s)\n",
333
0
            enforceTestModeResult ? "passed" : "failed",
334
0
            staticFingerprints->mIsMoz ? "mozilla" : "non-mozilla",
335
0
            hostname, staticFingerprints->mTestMode ? "test" : "production"));
336
0
  }
337
0
338
0
  return NS_OK;
339
0
}
340
341
nsresult
342
PublicKeyPinningService::ChainHasValidPins(
343
  const RefPtr<nsNSSCertList>& certList,
344
  const char* hostname,
345
  mozilla::pkix::Time time,
346
  bool enforceTestMode,
347
  const OriginAttributes& originAttributes,
348
  /*out*/ bool& chainHasValidPins,
349
  /*optional out*/ PinningTelemetryInfo* pinningTelemetryInfo)
350
0
{
351
0
  chainHasValidPins = false;
352
0
  if (!certList) {
353
0
    return NS_ERROR_INVALID_ARG;
354
0
  }
355
0
  if (!hostname || hostname[0] == 0) {
356
0
    return NS_ERROR_INVALID_ARG;
357
0
  }
358
0
  nsAutoCString canonicalizedHostname(CanonicalizeHostname(hostname));
359
0
  return CheckPinsForHostname(certList, canonicalizedHostname.get(),
360
0
                              enforceTestMode, time, originAttributes,
361
0
                              chainHasValidPins, pinningTelemetryInfo);
362
0
}
363
364
nsresult
365
PublicKeyPinningService::HostHasPins(const char* hostname,
366
                                     mozilla::pkix::Time time,
367
                                     bool enforceTestMode,
368
                                     const OriginAttributes& originAttributes,
369
                                     /*out*/ bool& hostHasPins)
370
0
{
371
0
  hostHasPins = false;
372
0
  nsAutoCString canonicalizedHostname(CanonicalizeHostname(hostname));
373
0
  nsTArray<nsCString> dynamicFingerprints;
374
0
  const TransportSecurityPreload* staticFingerprints = nullptr;
375
0
  nsresult rv = FindPinningInformation(canonicalizedHostname.get(), time,
376
0
                                       originAttributes, dynamicFingerprints,
377
0
                                       staticFingerprints);
378
0
  if (NS_FAILED(rv)) {
379
0
    return rv;
380
0
  }
381
0
  if (dynamicFingerprints.Length() > 0) {
382
0
    hostHasPins = true;
383
0
  } else if (staticFingerprints) {
384
0
    hostHasPins = !staticFingerprints->mTestMode || enforceTestMode;
385
0
  }
386
0
  return NS_OK;
387
0
}
388
389
nsAutoCString
390
PublicKeyPinningService::CanonicalizeHostname(const char* hostname)
391
0
{
392
0
  nsAutoCString canonicalizedHostname(hostname);
393
0
  ToLowerCase(canonicalizedHostname);
394
0
  while (canonicalizedHostname.Length() > 0 &&
395
0
         canonicalizedHostname.Last() == '.') {
396
0
    canonicalizedHostname.Truncate(canonicalizedHostname.Length() - 1);
397
0
  }
398
0
  return canonicalizedHostname;
399
0
}