aboutsummaryrefslogtreecommitdiff
path: root/pkg/storage/index
diff options
context:
space:
mode:
Diffstat (limited to 'pkg/storage/index')
-rw-r--r--pkg/storage/index/index.go103
-rw-r--r--pkg/storage/index/inmem.go113
2 files changed, 216 insertions, 0 deletions
diff --git a/pkg/storage/index/index.go b/pkg/storage/index/index.go
new file mode 100644
index 0000000..ef9ad60
--- /dev/null
+++ b/pkg/storage/index/index.go
@@ -0,0 +1,103 @@
+// Package index provides an index of locally stored certificates. If a method
+// succeeds, the index and the data that it tracks has been persisted to disk.
+// If a method does not succeed, restore from the persisted index on disk.
+package index
+
+import (
+ "crypto/sha256"
+ "fmt"
+ "time"
+
+ "rgdd.se/silent-ct/internal/ioutil"
+ "rgdd.se/silent-ct/internal/monitor"
+ "rgdd.se/silent-ct/pkg/crtutil"
+)
+
+type Config struct {
+ PermitBootstrap bool // Create a new index if a valid one does not exist on disk yet
+ IndexFile string // Path to an index file that can be read/written
+ TrustDirectory string // Absolute path to an existing directory where legitimate certificates are stored
+ MatchDirectory string // Absolute path to an existing directory where matching certificates are stored
+
+ // Optional
+ AlertDelay time.Duration // Time before alerting on certificates that are unaccounted for
+}
+
+type Index struct {
+ mem index
+ cfg Config
+}
+
+func New(cfg Config) (Index, error) {
+ ix := Index{cfg: cfg}
+ if err := ioutil.DirectoriesExist([]string{cfg.TrustDirectory, cfg.MatchDirectory}); err != nil {
+ return Index{}, err
+ }
+ if err := ioutil.ReadJSON(cfg.IndexFile, &ix.mem); err != nil {
+ if !cfg.PermitBootstrap {
+ return Index{}, err
+ }
+
+ ix.mem = newIndex()
+ if err := ioutil.CommitJSON(cfg.IndexFile, ix.mem); err != nil {
+ return Index{}, err
+ }
+ }
+ return ix, ix.Validate()
+}
+
+func (ix *Index) AddChain(node string, pem []byte) error {
+ chain, err := crtutil.CertificateChainFromPEM(pem)
+ if err != nil {
+ return err
+ }
+
+ var crtID CertificateID
+ crtID.Set(chain[0])
+ path := fmt.Sprintf("%s/%s-%s.pem", ix.cfg.TrustDirectory, node, crtID)
+ if !ix.mem.addChain(crtID, path) {
+ return nil // duplicate
+ }
+
+ if ioutil.CommitData(path, pem); err != nil {
+ return err
+ }
+ return ioutil.CommitJSON(ix.cfg.IndexFile, ix.mem)
+}
+
+func (ix *Index) AddEntries(logID [sha256.Size]byte, entries []monitor.LogEntry) error {
+ addEntry := func(entry monitor.LogEntry) error {
+ crt, err := crtutil.CertificateFromLogEntry(entry.LeafData, entry.ExtraData)
+ if err != nil {
+ return err
+ }
+
+ var crtID CertificateID
+ crtID.Set(crt)
+ path := fmt.Sprintf("%s/%x-%d.json", ix.cfg.MatchDirectory, logID[:], entry.LeafIndex)
+ if !ix.mem.addEntry(crtID, path) {
+ return nil // duplicate
+ }
+
+ return ioutil.CommitJSON(path, entry)
+ }
+
+ for _, entry := range entries {
+ if err := addEntry(entry); err != nil {
+ return err
+ }
+ }
+ return ioutil.CommitJSON(ix.cfg.IndexFile, ix.mem)
+}
+
+func (ix *Index) TriggerAlerts() ([]CertificateInfo, error) {
+ alerts := ix.mem.triggerAlerts(ix.cfg.AlertDelay)
+ if len(alerts) == 0 {
+ return []CertificateInfo{}, nil
+ }
+ return alerts, ioutil.CommitJSON(ix.cfg.IndexFile, ix.mem)
+}
+
+func (index *Index) Validate() error {
+ return nil // FIXME: check that the index is populated with valid values
+}
diff --git a/pkg/storage/index/inmem.go b/pkg/storage/index/inmem.go
new file mode 100644
index 0000000..0a084bf
--- /dev/null
+++ b/pkg/storage/index/inmem.go
@@ -0,0 +1,113 @@
+package index
+
+import (
+ "encoding/json"
+ "fmt"
+ "time"
+
+ "github.com/google/certificate-transparency-go/x509"
+ "rgdd.se/silent-ct/pkg/crtutil"
+)
+
+type CertificateID string
+
+func (crtID *CertificateID) Set(crt x509.Certificate) {
+ *crtID = CertificateID(crtutil.UniqueID(crt))
+}
+
+type CertificateInfo struct {
+ ObservedAt time.Time `json:"observed_at"`
+ StoredAt string `json:"stored_at"`
+}
+
+// index is an in-memory index of certificates
+type index struct {
+ Alerting map[CertificateID][]CertificateInfo `json:"alerting"` // Certificates that were not marked as "good" on time
+ Legitimate map[CertificateID][]CertificateInfo `json:"legitimate"` // Certificates that are considered "good"
+ Pending map[CertificateID][]CertificateInfo `json:"pending"` // Certificates that have yet to be marked as "good"
+}
+
+func newIndex() index {
+ return index{
+ Alerting: make(map[CertificateID][]CertificateInfo),
+ Legitimate: make(map[CertificateID][]CertificateInfo),
+ Pending: make(map[CertificateID][]CertificateInfo),
+ }
+}
+
+func (ix *index) JSONUnmarshal(b []byte) error {
+ type internal index
+ if err := json.Unmarshal(b, (*internal)(ix)); err != nil {
+ return err
+ }
+ for i, m := range []map[CertificateID][]CertificateInfo{ix.Alerting, ix.Legitimate, ix.Pending} {
+ if m == nil {
+ return fmt.Errorf("dictionary named %q is not in the index", []string{"alerting", "legitimate", "pending"}[i])
+ }
+ }
+ return nil
+}
+
+func (ix *index) triggerAlerts(delay time.Duration) []CertificateInfo {
+ var alerts []CertificateInfo
+
+ for key, certInfos := range ix.Pending {
+ certInfo := certInfos[0]
+ if time.Since(certInfo.ObservedAt) < delay {
+ continue
+ }
+
+ alerts = append(alerts, certInfo)
+ ix.Alerting[key] = certInfos
+ delete(ix.Pending, key)
+ }
+
+ return alerts
+}
+
+func (ix *index) addChain(crtID CertificateID, path string) bool {
+ if _, ok := ix.Legitimate[crtID]; ok {
+ return false // we already marked this certificate as "good"
+ }
+
+ entry := CertificateInfo{ObservedAt: time.Now(), StoredAt: path}
+ crtInfos := []CertificateInfo{entry}
+ if v, ok := ix.Alerting[crtID]; ok {
+ crtInfos = append(crtInfos, v...)
+ delete(ix.Alerting, crtID) // no longer alerting
+ } else if v, ok := ix.Pending[crtID]; ok {
+ crtInfos = append(crtInfos, v...)
+ delete(ix.Pending, crtID) // no longer pending
+ }
+
+ ix.Legitimate[crtID] = crtInfos
+ return true // index updated such that this certificate is marked as "good"
+}
+
+func (ix *index) addEntry(crtID CertificateID, path string) bool {
+ crtInfo := CertificateInfo{ObservedAt: time.Now(), StoredAt: path}
+ if _, ok := ix.Legitimate[crtID]; ok {
+ return add(ix.Legitimate, crtID, crtInfo)
+ } else if _, ok := ix.Alerting[crtID]; ok {
+ return add(ix.Alerting, crtID, crtInfo)
+ }
+ return add(ix.Pending, crtID, crtInfo)
+}
+
+func add(m map[CertificateID][]CertificateInfo, key CertificateID, value CertificateInfo) bool {
+ crtInfos, ok := m[key]
+ if !ok {
+ m[key] = []CertificateInfo{value}
+ return true
+ }
+
+ for _, crtInfo := range crtInfos {
+ if value.StoredAt == crtInfo.StoredAt {
+ return false // duplicate
+ }
+ }
+
+ crtInfos = append(crtInfos, value)
+ m[key] = crtInfos
+ return true
+}