From 5442e71e7710897126a0034f487fab7e5013b3cc Mon Sep 17 00:00:00 2001 From: Rasmus Dahlberg Date: Sun, 17 Dec 2023 19:10:46 +0100 Subject: Drafty server package to receive node requests curl http://localhost:2009/get-status curl -X POST --data-binary @/home/rgdd/fullchain.pem -u node_a:aaaa http://localhost:2009/add-chain --- internal/manager/manager.go | 2 +- internal/x509util/x509util.go | 44 ++++++++++++++++ main.go | 1 + pkg/server/.nodes.go.swp | Bin 0 -> 12288 bytes pkg/server/errors.go | 1 - pkg/server/handler.go | 32 ++++++++++++ pkg/server/messages.go | 19 +++++++ pkg/server/nodes.go | 53 +++++++++++++++++++ pkg/server/server.go | 117 ++++++++++++++++++++++++++---------------- 9 files changed, 222 insertions(+), 47 deletions(-) create mode 100644 internal/x509util/x509util.go create mode 100644 pkg/server/.nodes.go.swp delete mode 100644 pkg/server/errors.go create mode 100644 pkg/server/handler.go create mode 100644 pkg/server/nodes.go diff --git a/internal/manager/manager.go b/internal/manager/manager.go index 2210c9b..33207e9 100644 --- a/internal/manager/manager.go +++ b/internal/manager/manager.go @@ -70,7 +70,7 @@ func (mgr *Manager) Run(ctx context.Context, case ev := <-monitorCh: fmt.Printf("DEBUG: received event from monitor with %d matches\n", len(ev.Matches)) case ev := <-serverCh: - fmt.Printf("DEBUG: received event from server\n: %v", ev) + fmt.Printf("DEBUG: received event from server: %v\n", ev) case err := <-errorCh: fmt.Printf("DEBUG: received error: %v\n", err) } diff --git a/internal/x509util/x509util.go b/internal/x509util/x509util.go new file mode 100644 index 0000000..912d1b4 --- /dev/null +++ b/internal/x509util/x509util.go @@ -0,0 +1,44 @@ +package x509util + +import ( + "crypto/x509" + "encoding/pem" + "fmt" +) + +// ParseChain parses a certificate chain in PEM format. At least one +// certificate must be in the chain. The first certificate must be a leaf, +// whereas all other certificates must CA certificates (intermdiates/roots). +// +// Note: it is not checked if the certificate chain's root is trusted or not. +func ParseChain(b []byte) ([]x509.Certificate, error) { + var chain []x509.Certificate + + for { + block, rest := pem.Decode(b) + if block == nil { + break + } + crt, err := x509.ParseCertificate(block.Bytes) + if err != nil { + return nil, fmt.Errorf("parse certificate: %v", err) + } + + chain = append(chain, *crt) + b = rest + } + + if len(chain) == 0 { + return nil, fmt.Errorf("no certificates in the provided chain") + } + if chain[0].IsCA { + return nil, fmt.Errorf("leaf certificate has the CA bit set") + } + for _, crt := range chain[1:] { + if !crt.IsCA { + return nil, fmt.Errorf("non-leaf certificate without the CA bit set") + } + } + + return chain, nil +} diff --git a/main.go b/main.go index b602222..14c1b71 100644 --- a/main.go +++ b/main.go @@ -28,6 +28,7 @@ func main() { if err := c.FromFile(opts.ConfigFile); err != nil { die("configuration: %v", err) } + fmt.Printf("%v\n", c) srv, err := server.New(server.Config{Address: opts.ListenAddr, Nodes: c.Nodes}) if err != nil { diff --git a/pkg/server/.nodes.go.swp b/pkg/server/.nodes.go.swp new file mode 100644 index 0000000..b775c96 Binary files /dev/null and b/pkg/server/.nodes.go.swp differ diff --git a/pkg/server/errors.go b/pkg/server/errors.go deleted file mode 100644 index abb4e43..0000000 --- a/pkg/server/errors.go +++ /dev/null @@ -1 +0,0 @@ -package server diff --git a/pkg/server/handler.go b/pkg/server/handler.go new file mode 100644 index 0000000..6d17af7 --- /dev/null +++ b/pkg/server/handler.go @@ -0,0 +1,32 @@ +package server + +import ( + "fmt" + "net/http" +) + +// handler implements the http.Handler interface +type handler struct { + method string + endpoint string + callback func(w http.ResponseWriter, r *http.Request) (int, string) +} + +func (h handler) ServeHTTP(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Content-Type", "text/plain; charset=utf-8") + w.Header().Set("Cache-Control", "no-cache") + if r.Method != h.method { + http.Error(w, "Invalid HTTP method, expected "+h.method, http.StatusMethodNotAllowed) + return + } + code, text := h.callback(w, r) + if code != http.StatusOK { + http.Error(w, text, code) + return + } + fmt.Fprintf(w, fmt.Sprintf("%s\n", text)) +} + +func (h handler) register(mux *http.ServeMux) { + mux.Handle("/"+h.endpoint, h) +} diff --git a/pkg/server/messages.go b/pkg/server/messages.go index 50edded..a6ea243 100644 --- a/pkg/server/messages.go +++ b/pkg/server/messages.go @@ -1,4 +1,23 @@ package server +import ( + "fmt" + "time" +) + type MessageNodeSubmission struct { + SerialNumber string + NotBefore time.Time + DomainNames []string + PEMChain []byte +} + +type ErrorUnauthorizedDomainName struct { + PEMChain []byte + Node Node + Err error +} + +func (e ErrorUnauthorizedDomainName) Error() string { + return fmt.Sprintf("%v", e.Err) } diff --git a/pkg/server/nodes.go b/pkg/server/nodes.go new file mode 100644 index 0000000..164c06f --- /dev/null +++ b/pkg/server/nodes.go @@ -0,0 +1,53 @@ +package server + +import ( + "crypto/x509" + "fmt" + "net/http" +) + +// Node is an identified system that can request certificates +type Node struct { + Name string `json:"name"` // Artbirary node name for authentication + Secret string `json:"secret"` // Arbitrary node secret for authentication + Domains []string `json:"issues"` // Exact-match domain names that are allowed +} + +func (node *Node) authenticate(r *http.Request) error { + user, password, ok := r.BasicAuth() + if !ok { + return fmt.Errorf("no http basic auth credentials") + } + if user != node.Name || password != node.Secret { + return fmt.Errorf("invalid http basic auth credentials") + } + return nil +} + +func (node *Node) check(crt x509.Certificate) error { + for _, san := range crt.DNSNames { + ok := false + for _, domain := range node.Domains { + if domain == san { + ok = true + break + } + } + if !ok { + return fmt.Errorf("%s: not authorized to issue certificates for %s", node.Name, san) + } + } + return nil +} + +// Nodes is a list of nodes that can request certificates +type Nodes []Node + +func (nodes *Nodes) authenticate(r *http.Request) (Node, error) { + for _, node := range (*nodes)[:] { + if err := node.authenticate(r); err == nil { + return node, nil + } + } + return Node{}, fmt.Errorf("no valid HTTP basic auth credentials") +} diff --git a/pkg/server/server.go b/pkg/server/server.go index 2d10c4b..06eb258 100644 --- a/pkg/server/server.go +++ b/pkg/server/server.go @@ -3,104 +3,131 @@ package server import ( "context" "fmt" + "io" "net" "net/http" - "strings" "time" + + "rgdd.se/silent-ct/internal/x509util" ) const ( - EndpointAddChain = "/add-chain" - EndpointGetStatus = "/get-status" + EndpointAddChain = "add-chain" + EndpointGetStatus = "get-status" + DefaultNetwork = "tcp" DefaultAddress = "localhost:2009" DefaultConfigFile = "/home/rgdd/.config/silent-ct/config.json" // FIXME ) -type Nodes []Node - -type Node struct { - Name string `json:"name"` - Secret string `json:"secret"` - Domains []string `json:"issues"` -} - type Config struct { - Address string // hostname[:port] or unix:///path/to/file.sock + Network string // tcp or unix + Address string // hostname[:port] or path to a unix socket Nodes Nodes // Which nodes are trusted to issue what certificates } type Server struct { Config - http.Server - unixSocket bool // true if listening with a unix socket - eventCh chan MessageNodeSubmission - errorCh chan error + eventCh chan MessageNodeSubmission + errorCh chan error } func New(cfg Config) (Server, error) { - mux := http.NewServeMux() - srv := Server{Config: cfg, Server: http.Server{Handler: mux}} - mux.HandleFunc(EndpointAddChain, func(w http.ResponseWriter, r *http.Request) { srv.addChain(w, r) }) - mux.HandleFunc(EndpointGetStatus, func(w http.ResponseWriter, r *http.Request) { srv.getStatus(w, r) }) - if len(srv.Address) == 0 { - srv.Config.Address = DefaultAddress + if cfg.Network == "" { + cfg.Network = DefaultNetwork } - if strings.HasPrefix(srv.Config.Address, "unix://") { - srv.Config.Address = srv.Config.Address[7:] - srv.unixSocket = true + if cfg.Address == "" { + cfg.Network = DefaultAddress } - return srv, nil + return Server{Config: cfg}, nil } func (srv *Server) Run(ctx context.Context, submitCh chan MessageNodeSubmission, errorCh chan error) error { srv.eventCh = submitCh srv.errorCh = errorCh - network := "unix" - if !srv.unixSocket { - network = "tcp" + mux := http.NewServeMux() + for _, handler := range srv.handlers() { + handler.register(mux) } - listener, err := net.Listen(network, srv.Address) + listener, err := net.Listen(srv.Network, srv.Address) if err != nil { return fmt.Errorf("listen: %v", err) } defer listener.Close() - exitErr := make(chan error, 1) - defer close(exitErr) + s := http.Server{Handler: mux} + exitCh := make(chan error, 1) + defer close(exitCh) go func() { - exitErr <- srv.Serve(listener) + exitCh <- s.Serve(listener) }() select { - case err := <-exitErr: + case err := <-exitCh: if err != nil && err != http.ErrServerClosed { return fmt.Errorf("serve: %v", err) } case <-ctx.Done(): tctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) defer cancel() - if err := srv.Shutdown(tctx); err != nil { + if err := s.Shutdown(tctx); err != nil { return fmt.Errorf("shutdown: %v", err) } } return nil } -func (srv *Server) getStatus(w http.ResponseWriter, r *http.Request) { - if r.Method != http.MethodGet { - http.Error(w, "Only HTTP GET method is allowed", http.StatusMethodNotAllowed) - return +func (srv *Server) handlers() []handler { + return []handler{ + handler{ + method: http.MethodGet, + endpoint: EndpointGetStatus, + callback: func(w http.ResponseWriter, r *http.Request) (int, string) { return srv.getStatus(w, r) }, + }, + handler{ + method: http.MethodPost, + endpoint: EndpointAddChain, + callback: func(w http.ResponseWriter, r *http.Request) (int, string) { return srv.addChain(w, r) }, + }, } - fmt.Fprintf(w, "OK\n") } -func (srv *Server) addChain(w http.ResponseWriter, r *http.Request) { - if r.Method != http.MethodGet { - http.Error(w, "Only HTTP POST method is allowed", http.StatusMethodNotAllowed) - return +func (srv *Server) getStatus(w http.ResponseWriter, r *http.Request) (int, string) { + return http.StatusOK, "OK" +} + +func (srv *Server) addChain(w http.ResponseWriter, r *http.Request) (int, string) { + node, err := srv.Nodes.authenticate(r) + if err != nil { + return http.StatusForbidden, "Invalid HTTP Basic Auth credentials" } - fmt.Fprintf(w, "TODO: HTTP POST /add-chain\n") + + b, err := io.ReadAll(r.Body) + if err != nil { + return http.StatusBadRequest, "Read HTTP POST body failed" + } + defer r.Body.Close() + + chain, err := x509util.ParseChain(b) + if err != nil { + return http.StatusBadRequest, "Malformed HTTP POST body" + } + if err := node.check(chain[0]); err != nil { + srv.errorCh <- ErrorUnauthorizedDomainName{ + PEMChain: b, + Node: node, + Err: err, + } + } else { + srv.eventCh <- MessageNodeSubmission{ + SerialNumber: chain[0].SerialNumber.String(), + NotBefore: chain[0].NotBefore, + DomainNames: chain[0].DNSNames, + PEMChain: b, + } + } + + return http.StatusOK, "OK" } -- cgit v1.2.3