From e18d36ebae30536c77c61cd5da123991e0ca1629 Mon Sep 17 00:00:00 2001 From: Rasmus Dahlberg Date: Sun, 31 Dec 2023 09:39:25 +0100 Subject: Add drafty prototype --- pkg/policy/node.go | 93 ++++++++++++++++++++++++++++++++++++++++++++++++++ pkg/policy/policy.go | 18 ++++++++++ pkg/policy/wildcard.go | 83 ++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 194 insertions(+) create mode 100644 pkg/policy/node.go create mode 100644 pkg/policy/policy.go create mode 100644 pkg/policy/wildcard.go (limited to 'pkg/policy') diff --git a/pkg/policy/node.go b/pkg/policy/node.go new file mode 100644 index 0000000..23f04ca --- /dev/null +++ b/pkg/policy/node.go @@ -0,0 +1,93 @@ +package policy + +import ( + "crypto/hmac" + "crypto/sha256" + "encoding/json" + "fmt" + "io" + + "golang.org/x/crypto/hkdf" +) + +type Node struct { + Name string `json:"name"` // Artbirary node name to authenticate + Secret string `json:"secret"` // Arbitrary node secret for authentication + URL string `json:"url"` // Where the node's submissions can be downloaded + Domains []string `json:"issues"` // Exact-match domain names allowed to be issued + + key [16]byte +} + +func NewNode(name, secret, url string, domains []string) (Node, error) { + n := Node{Name: name, Secret: secret, Domains: domains, URL: url} + if err := n.deriveKey(); err != nil { + return Node{}, err + } + return n, n.Validate() +} + +func (n *Node) UnmarshalJSON(data []byte) error { + type internal Node + if err := json.Unmarshal(data, (*internal)(n)); err != nil { + return err + } + if err := n.deriveKey(); err != nil { + return err + } + return n.Validate() +} + +func (n *Node) Validate() error { + if n.Name == "" { + return fmt.Errorf("name is required") + } + if n.Secret == "" { + return fmt.Errorf("secret is required") + } + if n.URL == "" { + return fmt.Errorf("url is required") + } + if n.key == [16]byte{} { + return fmt.Errorf("key needs to be derived") + } + return nil +} + +func (n *Node) Authorize(sans []string) error { + for _, san := range sans { + ok := false + for _, domain := range n.Domains { + if domain == san { + ok = true + break + } + } + + if !ok { + return fmt.Errorf("node %s is not authorized to issue certificate with name %s", n.Name, san) + } + } + return nil +} + +func (n *Node) HMAC(data []byte) (mac [sha256.Size]byte, err error) { + if err = n.Validate(); err != nil { + return + } + + h := hmac.New(sha256.New, n.key[:]) + _, err = h.Write(data) + + copy(mac[:], h.Sum(nil)) + return +} + +func (n *Node) deriveKey() error { + const salt = "silent-ct" + + hkdf := hkdf.New(sha256.New, []byte(n.Secret), []byte(salt), []byte(n.Name)) + _, err := io.ReadFull(hkdf, n.key[:]) + + return err +} diff --git a/pkg/policy/policy.go b/pkg/policy/policy.go new file mode 100644 index 0000000..8ee4867 --- /dev/null +++ b/pkg/policy/policy.go @@ -0,0 +1,18 @@ +// Package policy specifies which certificates to look for while monitoring, and +// how to pull legitimately issued certificates from trusted nodes based on a +// shared secret. Statically configured logs can also be specified, as well as +// logs that should not be monitored even if they appear in any dynamic list. +package policy + +import ( + "gitlab.torproject.org/rgdd/ct/pkg/metadata" +) + +type Policy struct { + Monitor Wildcards `json:"monitor"` + Nodes []Node `json:"nodes"` + + // Optional + StaticLogs []metadata.Log `json:"static_logs"` + RemoveLogs []metadata.LogKey `json:"remove_logs"` +} diff --git a/pkg/policy/wildcard.go b/pkg/policy/wildcard.go new file mode 100644 index 0000000..58b0d17 --- /dev/null +++ b/pkg/policy/wildcard.go @@ -0,0 +1,83 @@ +package policy + +import ( + "encoding/json" + "fmt" + "strings" + "time" + + "rgdd.se/silent-ct/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:"wildcard"` + 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 time is required") + } + if len(w.Wildcard) == 0 { + return fmt.Errorf("wildcard 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 +} -- cgit v1.2.3