1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
|
package feedback
import (
"context"
"fmt"
"io"
"net/http"
"os"
"time"
"rgdd.se/silentct/internal/logger"
"rgdd.se/silentct/pkg/crtutil"
"rgdd.se/silentct/pkg/policy"
"rgdd.se/silentct/pkg/submission"
)
type Event struct {
NodeName string // Name of the node that generated a submission
PEMChain []byte // A certificate chain found in the submission
}
type Config struct {
Policy policy.Policy
// Optional
Logger *logger.Logger // Debug and info prints only (no output by default)
PullInterval time.Duration // How often nodes are pulled via HTTP GET
HTTPTimeout time.Duration // Timeout to use when pulling nodes
}
type Feedback struct {
cfg Config
nodes []policy.Node
eventCh chan []Event
}
func New(cfg Config, eventCh chan []Event) (Feedback, error) {
if !cfg.Logger.IsConfigured() {
cfg.Logger = logger.New(logger.Config{Level: logger.LevelNotice, File: os.Stdout})
}
if cfg.PullInterval == 0 {
cfg.PullInterval = 1 * time.Hour
}
if cfg.HTTPTimeout == 0 {
cfg.HTTPTimeout = 10 * time.Second
}
for i, node := range cfg.Policy.Nodes {
if err := node.Validate(); err != nil {
return Feedback{}, fmt.Errorf("node %d: %v", i, err)
}
}
return Feedback{cfg: cfg, nodes: cfg.Policy.Nodes, eventCh: eventCh}, nil
}
// RunForever collects legitimately issued certificates from nodes
func (fb *Feedback) RunForever(ctx context.Context) {
ticker := time.NewTicker(fb.cfg.PullInterval)
defer ticker.Stop()
fb.pullOnce(ctx)
for {
select {
case <-ticker.C:
fb.pullOnce(ctx)
case <-ctx.Done():
return
}
}
}
func (fb *Feedback) pullOnce(ctx context.Context) {
fb.cfg.Logger.Debugf("pull %d nodes\n", len(fb.nodes))
for _, node := range fb.nodes {
data, err := fb.pull(ctx, node)
if err != nil {
fb.cfg.Logger.Debugf("failed to pull node %s: %v", node.Name, err)
continue
}
var events []Event
for _, pemChain := range data {
chain, err := crtutil.CertificateChainFromPEM(pemChain)
if err != nil {
fb.cfg.Logger.Infof("failed to parse certificate from node %s: %v", node.Name, err)
continue
}
if err := node.Authorize(chain[0].DNSNames); err != nil {
fb.cfg.Logger.Infof("%s\n", err.Error())
continue
}
events = append(events, Event{NodeName: node.Name, PEMChain: pemChain})
}
fb.eventCh <- events
}
}
func (fb *Feedback) pull(ctx context.Context, node policy.Node) ([][]byte, error) {
req, err := http.NewRequest(http.MethodGet, node.URL, nil)
if err != nil {
return nil, fmt.Errorf("new request: %v", err)
}
req.WithContext(ctx)
cli := http.Client{Timeout: fb.cfg.HTTPTimeout}
rsp, err := cli.Do(req)
if err != nil {
return nil, fmt.Errorf("HTTP %s: %v", req.Method, err)
}
defer rsp.Body.Close()
b, err := io.ReadAll(rsp.Body)
if err != nil {
return nil, fmt.Errorf("read response: %v", err)
}
if rsp.StatusCode != http.StatusOK {
return nil, fmt.Errorf("%s: %s", rsp.Status, string(b))
}
s := submission.Submission(b)
data, err := s.Open(node)
if err != nil {
return nil, fmt.Errorf("open: %v", err)
}
return data, nil
}
|