github.com/zmap/zlint@v1.1.0/lints/lint_ct_sct_policy_count_unsatisfied.go (about)

     1  /*
     2   * ZLint Copyright 2019 Regents of the University of Michigan
     3   *
     4   * Licensed under the Apache License, Version 2.0 (the "License"); you may not
     5   * use this file except in compliance with the License. You may obtain a copy
     6   * of the License at http://www.apache.org/licenses/LICENSE-2.0
     7   *
     8   * Unless required by applicable law or agreed to in writing, software
     9   * distributed under the License is distributed on an "AS IS" BASIS,
    10   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
    11   * implied. See the License for the specific language governing
    12   * permissions and limitations under the License.
    13   */
    14  
    15  package lints
    16  
    17  import (
    18  	"fmt"
    19  	"time"
    20  
    21  	"github.com/zmap/zcrypto/x509"
    22  	"github.com/zmap/zcrypto/x509/ct"
    23  	"github.com/zmap/zlint/util"
    24  )
    25  
    26  type sctPolicyCount struct{}
    27  
    28  // Initialize for a sctPolicyCount instance does nothing.
    29  func (l *sctPolicyCount) Initialize() error {
    30  	return nil
    31  }
    32  
    33  // CheckApplies returns true for any subscriber certificates that are not
    34  // precertificates (e.g. that do not have the CT poison extension defined in RFC
    35  // 6962.
    36  func (l *sctPolicyCount) CheckApplies(c *x509.Certificate) bool {
    37  	return util.IsSubscriberCert(c) && !util.IsExtInCert(c, util.CtPoisonOID)
    38  }
    39  
    40  // Execute checks if the provided certificate has embedded SCTs from
    41  // a sufficient number of unique CT logs to meet Apple's CT log policy[0],
    42  // effective Oct 15th, 2018.
    43  //
    44  // The number of required SCTs from different logs is calculated based on the
    45  // Certificate's lifetime. If the number of required SCTs are not embedded in
    46  // the certificate a Notice level LintResult is returned.
    47  //
    48  // | Certificate lifetime | # of SCTs from separate logs |
    49  // -------------------------------------------------------
    50  // | Less than 15 months  | 2                            |
    51  // | 15 to 27 months      | 3                            |
    52  // | 27 to 39 months      | 4                            |
    53  // | More than 39 months  | 5                            |
    54  // -------------------------------------------------------
    55  //
    56  // Important note 1: We can't know whether additional SCTs were presented
    57  // alongside the certificate via OCSP stapling. This linter assumes only
    58  // embedded SCTs are used and ignores the portion of the Apple policy related to
    59  // SCTs delivered via OCSP. This is one limitation that restricts the linter's
    60  // findings to Notice level. See more background discussion in Issue 226[1].
    61  //
    62  // Important note 2: The linter doesn't maintain a list of Apple's trusted
    63  // logs. The SCTs embedded in the certificate may not be from log's Apple
    64  // actually trusts. Similarly the embedded SCT signatures are not validated
    65  // in any way.
    66  //
    67  // [0]: https://support.apple.com/en-us/HT205280
    68  // [1]: https://github.com/zmap/zlint/issues/226
    69  func (l *sctPolicyCount) Execute(c *x509.Certificate) *LintResult {
    70  	// Determine the required number of SCTs from separate logs
    71  	expected := appleCTPolicyExpectedSCTs(c)
    72  
    73  	// If there are no SCTs then the job is easy. We can return a Notice
    74  	// LintResult immediately.
    75  	if len(c.SignedCertificateTimestampList) == 0 && expected > 0 {
    76  		return &LintResult{
    77  			Status: Notice,
    78  			Details: fmt.Sprintf(
    79  				"Certificate had 0 embedded SCTs. Browser policy may require %d for this certificate.",
    80  				expected),
    81  		}
    82  	}
    83  
    84  	// Build a map from LogID to SCT so that we can count embedded SCTs by unique
    85  	// log.
    86  	sctsByLogID := make(map[ct.SHA256Hash]*ct.SignedCertificateTimestamp)
    87  	for _, sct := range c.SignedCertificateTimestampList {
    88  		sctsByLogID[sct.LogID] = sct
    89  	}
    90  
    91  	// If the number of embedded SCTs from separate logs meets expected return
    92  	// a Pass result.
    93  	if len(sctsByLogID) >= expected {
    94  		return &LintResult{Status: Pass}
    95  	}
    96  
    97  	// Otherwise return a Notice result - there weren't enough SCTs embedded in
    98  	// the certificate. More must be provided by OCSP stapling if the certificate
    99  	// is to meet Apple's CT policy.
   100  	return &LintResult{
   101  		Status: Notice,
   102  		Details: fmt.Sprintf(
   103  			"Certificate had %d embedded SCTs from distinct log IDs. "+
   104  				"Browser policy may require %d for this certificate.",
   105  			len(sctsByLogID), expected),
   106  	}
   107  }
   108  
   109  // appleCTPolicyExpectedSCTs returns a count of the number of SCTs expected to
   110  // be embedded in the given certificate based on its lifetime.
   111  //
   112  // For this function the relevant portion of Apple's policy is the table
   113  // "Number of embedded SCTs based on certificate lifetime" (Also reproduced in
   114  // the `Execute` godoc comment).
   115  func appleCTPolicyExpectedSCTs(cert *x509.Certificate) int {
   116  	// Lifetime is relative to the certificate's NotBefore date.
   117  	start := cert.NotBefore
   118  
   119  	// Thresholds is an ordered array of lifetime periods and their expected # of
   120  	// SCTs. A lifetime period is defined by the cutoff date relative to the
   121  	// start of the certificate's lifetime.
   122  	thresholds := []struct {
   123  		CutoffDate time.Time
   124  		Expected   int
   125  	}{
   126  		// Start date ... 15 months
   127  		{CutoffDate: start.AddDate(0, 15, 0), Expected: 2},
   128  		// Start date ... 27 months
   129  		{CutoffDate: start.AddDate(0, 27, 0), Expected: 3},
   130  		// Start date ... 39 months
   131  		{CutoffDate: start.AddDate(0, 39, 0), Expected: 4},
   132  	}
   133  
   134  	// If the certificate's lifetime falls into any of the cutoff date ranges then
   135  	// we expect that range's expected # of SCTs for this certificate. This loop
   136  	// assumes the `thresholds` list is sorted in ascending order.
   137  	for _, threshold := range thresholds {
   138  		if cert.NotAfter.Before(threshold.CutoffDate) {
   139  			return threshold.Expected
   140  		}
   141  	}
   142  
   143  	// The certificate had a validity > 39 months.
   144  	return 5
   145  }
   146  
   147  func init() {
   148  	RegisterLint(&Lint{
   149  		Name:          "w_ct_sct_policy_count_unsatisfied",
   150  		Description:   "Check if certificate has enough embedded SCTs to meet Apple CT Policy",
   151  		Citation:      "https://support.apple.com/en-us/HT205280",
   152  		Source:        AppleCTPolicy,
   153  		EffectiveDate: util.AppleCTPolicyDate,
   154  		Lint:          &sctPolicyCount{},
   155  	})
   156  }