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 }