aboutsummaryrefslogtreecommitdiff
path: root/pkg/policy
diff options
context:
space:
mode:
authorRasmus Dahlberg <rasmus@rgdd.se>2023-12-31 09:39:25 +0100
committerRasmus Dahlberg <rasmus@rgdd.se>2024-01-07 20:22:23 +0100
commite18d36ebae30536c77c61cd5da123991e0ca1629 (patch)
treebf4880c0019a6009ab1b671e23ef4a1a4a5e8e08 /pkg/policy
parent54d980afcbd6f0011d6a162e0003587d26a3e311 (diff)
Add drafty prototype
Diffstat (limited to 'pkg/policy')
-rw-r--r--pkg/policy/node.go93
-rw-r--r--pkg/policy/policy.go18
-rw-r--r--pkg/policy/wildcard.go83
3 files changed, 194 insertions, 0 deletions
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
+}