// 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 }