1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224
|
// Copyright 2018 Google LLC. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
// Package ctpolicy contains structs describing CT policy requirements and corresponding logic.
package ctpolicy
import (
"fmt"
"sync"
"github.com/google/certificate-transparency-go/loglist3"
"github.com/google/certificate-transparency-go/x509"
)
const (
// BaseName is name for the group covering all logs.
BaseName = "All-logs"
)
// LogGroupInfo holds information on a single group of logs specified by Policy.
type LogGroupInfo struct {
Name string
LogURLs map[string]bool // set of members
MinInclusions int // Required number of submissions.
IsBase bool // True only for Log-group covering all logs.
LogWeights map[string]float32 // weights used for submission, default weight is 1
wMu sync.RWMutex // guards weights
}
func (group *LogGroupInfo) setMinInclusions(i int) error {
if i < 0 {
return fmt.Errorf("cannot assign negative minimal inclusions number")
}
// Assign given number even if it's bigger than group size.
group.MinInclusions = i
if i > len(group.LogURLs) {
return fmt.Errorf("trying to assign %d minimal inclusion number while only %d logs are part of group %q", i, len(group.LogURLs), group.Name)
}
return nil
}
func (group *LogGroupInfo) populate(ll *loglist3.LogList, included func(op *loglist3.Operator) bool) {
group.LogURLs = make(map[string]bool)
group.LogWeights = make(map[string]float32)
for _, op := range ll.Operators {
if included(op) {
for _, l := range op.Logs {
group.LogURLs[l.URL] = true
group.LogWeights[l.URL] = 1.0
}
}
}
}
// satisfyMinimalInclusion returns whether number of positive weights is
// bigger or equal to minimal inclusion number.
func (group *LogGroupInfo) satisfyMinimalInclusion(weights map[string]float32) bool {
nonZeroNum := 0
for logURL, w := range weights {
if group.LogURLs[logURL] && w > 0.0 {
nonZeroNum++
if nonZeroNum >= group.MinInclusions {
return true
}
}
}
return false
}
// SetLogWeights applies suggested weights to the Log-group. Does not reset
// weights and returns error when there are not enough positive weights
// provided to reach minimal inclusion number.
func (group *LogGroupInfo) SetLogWeights(weights map[string]float32) error {
for logURL, w := range weights {
if w < 0.0 {
return fmt.Errorf("trying to assign negative weight %v to Log %q", w, logURL)
}
}
if !group.satisfyMinimalInclusion(weights) {
return fmt.Errorf("trying to assign weights %v resulting in inability to reach minimal inclusion number %d", weights, group.MinInclusions)
}
group.wMu.Lock()
defer group.wMu.Unlock()
// All group weights initially reset to 0.0
for logURL := range group.LogURLs {
group.LogWeights[logURL] = 0.0
}
for logURL, w := range weights {
if group.LogURLs[logURL] {
group.LogWeights[logURL] = w
}
}
return nil
}
// SetLogWeight tries setting the weight for a single Log of the Log-group.
// Does not reset the weight and returns error if weight is non-positive and
// its setting will result in inability to reach minimal inclusion number.
func (group *LogGroupInfo) SetLogWeight(logURL string, w float32) error {
if !group.LogURLs[logURL] {
return fmt.Errorf("trying to assign weight to Log %q not belonging to the group", logURL)
}
if w < 0.0 {
return fmt.Errorf("trying to assign negative weight %v to Log %q", w, logURL)
}
newWeights := make(map[string]float32)
for l, wt := range group.LogWeights {
newWeights[l] = wt
}
newWeights[logURL] = w
if !group.satisfyMinimalInclusion(newWeights) {
return fmt.Errorf("assigning weight %v to Log %q will result in inability to reach minimal inclusion number %d", w, logURL, group.MinInclusions)
}
group.wMu.Lock()
defer group.wMu.Unlock()
group.LogWeights = newWeights
return nil
}
// GetSubmissionSession produces list of log-URLs of the Log-group.
// Order of the list is weighted random defined by Log-weights within the group
func (group *LogGroupInfo) GetSubmissionSession() []string {
if len(group.LogURLs) == 0 {
return make([]string, 0)
}
session := make([]string, 0)
// modelling weighted random with exclusion
unProcessedWeights := make(map[string]float32)
for logURL, w := range group.LogWeights {
unProcessedWeights[logURL] = w
}
group.wMu.RLock()
defer group.wMu.RUnlock()
for range group.LogURLs {
sampleLog, err := weightedRandomSample(unProcessedWeights)
if err != nil {
// session still valid, not covering all Logs
return session
}
session = append(session, sampleLog)
delete(unProcessedWeights, sampleLog)
}
return session
}
// LogPolicyData contains info on log-partition and submission requirements
// for a single cert. Key always matches value Name field.
type LogPolicyData map[string]*LogGroupInfo
// TotalLogs returns number of logs within set of Log-groups.
// Taking possible intersection into account.
func (groups LogPolicyData) TotalLogs() int {
unifiedLogs := make(map[string]bool)
for _, g := range groups {
if g.IsBase {
return len(g.LogURLs)
}
for l := range g.LogURLs {
unifiedLogs[l] = true
}
}
return len(unifiedLogs)
}
// CTPolicy interface describes requirements determined for logs in terms of
// per-group-submit.
type CTPolicy interface {
// LogsByGroup provides info on Log-grouping. Returns an error if it's not
// possible to satisfy the policy with the provided loglist.
LogsByGroup(cert *x509.Certificate, approved *loglist3.LogList) (LogPolicyData, error)
Name() string
}
// BaseGroupFor creates and propagates all-log group.
func BaseGroupFor(approved *loglist3.LogList, incCount int) (*LogGroupInfo, error) {
baseGroup := LogGroupInfo{Name: BaseName, IsBase: true}
baseGroup.populate(approved, func(op *loglist3.Operator) bool { return true })
err := baseGroup.setMinInclusions(incCount)
return &baseGroup, err
}
// lifetimeInMonths calculates and returns cert lifetime expressed in months
// flooring incomplete month.
func lifetimeInMonths(cert *x509.Certificate) int {
startYear, startMonth, startDay := cert.NotBefore.Date()
endYear, endMonth, endDay := cert.NotAfter.Date()
lifetimeInMonths := (int(endYear)-int(startYear))*12 + (int(endMonth) - int(startMonth))
if endDay < startDay {
// partial month
lifetimeInMonths--
}
return lifetimeInMonths
}
// GroupSet is set of Log-group names.
type GroupSet map[string]bool
// GroupByLogs reverses match-map between Logs and Groups.
// Returns map from log-URLs to set of Group-names that contain the log.
func GroupByLogs(lg LogPolicyData) map[string]GroupSet {
result := make(map[string]GroupSet)
for groupname, g := range lg {
for logURL := range g.LogURLs {
if _, seen := result[logURL]; !seen {
result[logURL] = make(GroupSet)
}
result[logURL][groupname] = true
}
}
return result
}
|