144 lines
5.8 KiB
Go
144 lines
5.8 KiB
Go
package main
|
|
|
|
import (
|
|
"context"
|
|
"crypto/tls"
|
|
"fmt"
|
|
"net"
|
|
"slices"
|
|
"strings"
|
|
"time"
|
|
|
|
"git.novox.be/novox/mesh-sdk/go/health"
|
|
)
|
|
|
|
// The module's health (issue 1510, novox/hq ADR 0240): the node-engine asks the tool brother_health, and the tool
|
|
// asks the printer for the certificates it serves, one for each key type, with no login, and compares them with
|
|
// the pinned list (the config value printer-certificate). A certificate that is not pinned (the printer was reset,
|
|
// new certificates were made, a firmware update changed them) is unhealthy and names the new fingerprint, so the
|
|
// operator can approve the new pin before the admin pages are needed. A printer that does not answer is not
|
|
// unhealthy (it is switched off, or asleep): nothing can be compared, and the answer says so.
|
|
|
|
// keyKinds are the two certificates the printer serves for one name. Which one it serves depends on the cipher
|
|
// suites the client offers, so each handshake offers only one kind's, under TLS 1.2 (where the suite decides).
|
|
var keyKinds = []struct {
|
|
Name string
|
|
Suites []uint16
|
|
}{
|
|
{"ecdsa", []uint16{
|
|
tls.TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256, tls.TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384,
|
|
tls.TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305_SHA256, tls.TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA, tls.TLS_ECDHE_ECDSA_WITH_AES_256_CBC_SHA,
|
|
}},
|
|
{"rsa", []uint16{
|
|
tls.TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256, tls.TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384,
|
|
tls.TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305_SHA256, tls.TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA, tls.TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA,
|
|
tls.TLS_RSA_WITH_AES_128_GCM_SHA256, tls.TLS_RSA_WITH_AES_256_GCM_SHA384, tls.TLS_RSA_WITH_AES_128_CBC_SHA, tls.TLS_RSA_WITH_AES_256_CBC_SHA,
|
|
}},
|
|
}
|
|
|
|
// certDial bounds one handshake and certTotal the whole check (two handshakes), so the node-engine's look, which has
|
|
// ten seconds, is always answered (issue 1510).
|
|
var (
|
|
certDial = 3 * time.Second
|
|
certTotal = 7 * time.Second
|
|
)
|
|
|
|
// Served is a certificate the printer served to a handshake of one key type.
|
|
type Served struct {
|
|
Key string `json:"key"`
|
|
Fingerprint string `json:"fingerprint,omitempty"`
|
|
Pinned bool `json:"pinned"`
|
|
Note string `json:"note,omitempty"`
|
|
}
|
|
|
|
// CertHealth is brother_health's answer: healthy, why when it is not (or what could not be compared), and what the
|
|
// printer served.
|
|
type CertHealth struct {
|
|
Healthy bool `json:"healthy"`
|
|
State string `json:"state"` // healthy, unhealthy or waits (ADR 0283)
|
|
Why string `json:"why"`
|
|
Served []Served `json:"served"`
|
|
}
|
|
|
|
// CertWaits is the answer while printer or printer-certificate is not given: the module waits for the operator,
|
|
// which is not a fault (ADR 0283), and names each setting as the SDK's health.Wait asks.
|
|
func CertWaits(s Settings) (health.Answer, bool) {
|
|
var waits []health.Wait
|
|
var names []string
|
|
for _, c := range []struct{ name, value, what string }{
|
|
{"printer", s.Printer, "the printer's address"},
|
|
{"printer-certificate", s.PrinterCertificate, "the SHA-256 fingerprints of the certificates the printer serves"},
|
|
} {
|
|
if c.value == "" {
|
|
waits = append(waits, health.Wait{Part: "the printer's certificate check", Setting: c.name, What: c.what})
|
|
names = append(names, c.name)
|
|
}
|
|
}
|
|
if len(waits) == 0 {
|
|
return health.Answer{}, false
|
|
}
|
|
return health.Waiting("waits for the operator to give the config value "+strings.Join(names, " and "), waits...), true
|
|
}
|
|
|
|
// serveAddr is the host with the HTTPS port when it names none.
|
|
func serveAddr(host string) string {
|
|
if _, _, err := net.SplitHostPort(host); err == nil {
|
|
return host
|
|
}
|
|
return net.JoinHostPort(strings.Trim(host, "[]"), "443")
|
|
}
|
|
|
|
// served is the leaf's fingerprint the printer gives a handshake that offers only these suites, or why it gave none.
|
|
func served(ctx context.Context, addr string, suites []uint16) (string, error) {
|
|
ctx, cancel := context.WithTimeout(ctx, certDial)
|
|
defer cancel()
|
|
d := tls.Dialer{NetDialer: &net.Dialer{Timeout: certDial}, Config: &tls.Config{
|
|
MinVersion: tls.VersionTLS12, MaxVersion: tls.VersionTLS12, CipherSuites: suites,
|
|
// Nothing is sent over this connection, and the certificate is only read, to be compared with the pins.
|
|
InsecureSkipVerify: true,
|
|
}}
|
|
conn, err := d.DialContext(ctx, "tcp", addr)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
defer conn.Close()
|
|
state := conn.(*tls.Conn).ConnectionState()
|
|
if len(state.PeerCertificates) == 0 {
|
|
return "", fmt.Errorf("the printer served no certificate")
|
|
}
|
|
return Fingerprint(state.PeerCertificates[0].Raw), nil
|
|
}
|
|
|
|
// CheckCertificates compares what the printer serves with the pins. The login is never used.
|
|
func CheckCertificates(ctx context.Context, host, pins string) CertHealth {
|
|
ctx, cancel := context.WithTimeout(ctx, certTotal)
|
|
defer cancel()
|
|
want := pinList(pins)
|
|
addr := serveAddr(host)
|
|
h := CertHealth{Healthy: true, State: "healthy"}
|
|
var changed, unreachable []string
|
|
for _, k := range keyKinds {
|
|
s := Served{Key: k.Name}
|
|
fp, err := served(ctx, addr, k.Suites)
|
|
if err != nil {
|
|
s.Note = err.Error()
|
|
unreachable = append(unreachable, k.Name+": "+err.Error())
|
|
} else {
|
|
s.Fingerprint, s.Pinned = fp, slices.Contains(want, fp)
|
|
if !s.Pinned {
|
|
changed = append(changed, fmt.Sprintf("the %s certificate is now %s", k.Name, fp))
|
|
}
|
|
}
|
|
h.Served = append(h.Served, s)
|
|
}
|
|
switch {
|
|
case len(changed) > 0:
|
|
h.Healthy, h.State = false, "unhealthy"
|
|
h.Why = fmt.Sprintf("the printer serves a certificate that is not pinned: %s; the pinned printer-certificate is %s. If the printer was reset or given new certificates, set printer-certificate to the new fingerprint(s) (with the old ones kept if they still serve)",
|
|
strings.Join(changed, "; "), strings.Join(want, ", "))
|
|
case len(unreachable) == len(keyKinds):
|
|
h.Why = "the printer gave no certificate, so none was compared (it may be switched off or asleep): " + strings.Join(unreachable, "; ")
|
|
}
|
|
return h
|
|
}
|