package policy import ( "encoding/json" "fmt" "strings" "time" "rgdd.se/silentct/pkg/crtutil" ) // Wildcards implement the monitor.Matcher interface for a list of wildcards. // // Warning: parsing of SANs in certificates is hard. This matcher depends on // the parsing defined in github.com/google/certificate-transparency-go/x509. type Wildcards []Wildcard func (w *Wildcards) Match(leafData, extraData []byte) (bool, error) { crt, err := crtutil.CertificateFromLogEntry(leafData, extraData) if err != nil { return false, err } return w.match(crt.DNSNames, crt.NotAfter), nil } func (w *Wildcards) match(sans []string, notAfter time.Time) bool { for _, wildcard := range *w { if wildcard.Match(sans, notAfter) { return true } } return false } // Wildcard matches any string that ends with `Wildcard`, unless: // // 1. `Excludes[i] + "." + Wildcard` is a longer suffix match, or // 2. the certificate expired before the BootstrapAt timestamp. type Wildcard struct { BootstrapAt time.Time `json:"bootstrap_at"` Wildcard string `json:"suffix"` Excludes []string `json:"excludes,omitempty"` } func (w *Wildcard) UnmarshalJSON(data []byte) error { type internal Wildcard if err := json.Unmarshal(data, (*internal)(w)); err != nil { return err } return w.Validate() } func (w *Wildcard) Validate() error { if w.BootstrapAt.IsZero() { return fmt.Errorf("bootstrap_at is required") } if len(w.Wildcard) == 0 { return fmt.Errorf("suffix is required") } return nil } func (w *Wildcard) Match(sans []string, expiresAt time.Time) bool { for _, san := range sans { if san == w.Wildcard { return w.BootstrapAt.Before(expiresAt) } if strings.HasSuffix(san, "."+w.Wildcard) && !w.exclude(san) { return w.BootstrapAt.Before(expiresAt) } } return false } func (w *Wildcard) exclude(san string) bool { for _, exclude := range w.Excludes { suffix := exclude + "." + w.Wildcard if strings.HasSuffix(san, suffix) { return true } } return false }