checker: enforce prober-as-observation, move all analysis to rules layer
This commit is contained in:
parent
1e6254c289
commit
f77895dcab
12 changed files with 174 additions and 171 deletions
102
checker/sshfp.go
102
checker/sshfp.go
|
|
@ -22,10 +22,55 @@
|
|||
package checker
|
||||
|
||||
import (
|
||||
"crypto/sha1"
|
||||
"crypto/sha256"
|
||||
"encoding/hex"
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"golang.org/x/crypto/ssh"
|
||||
)
|
||||
|
||||
// sshfpAlgoForKeyType maps an SSH host-key type string to the SSHFP
|
||||
// algorithm number defined in RFC 4255 / RFC 6594 / RFC 7479.
|
||||
func sshfpAlgoForKeyType(t string) uint8 {
|
||||
switch t {
|
||||
case "ssh-rsa", "rsa-sha2-256", "rsa-sha2-512":
|
||||
return 1
|
||||
case "ssh-dss":
|
||||
return 2
|
||||
case "ecdsa-sha2-nistp256", "ecdsa-sha2-nistp384", "ecdsa-sha2-nistp521":
|
||||
return 3
|
||||
case "ssh-ed25519":
|
||||
return 4
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
// hostKeyFingerprints returns the SHA-1 and SHA-256 hex fingerprints of k.
|
||||
func hostKeyFingerprints(k HostKeyInfo) (sha1hex, sha256hex string) {
|
||||
sum1 := sha1.Sum(k.RawKey)
|
||||
sum256 := sha256.Sum256(k.RawKey)
|
||||
return hex.EncodeToString(sum1[:]), hex.EncodeToString(sum256[:])
|
||||
}
|
||||
|
||||
// hostKeyBits returns the key size in bits (RSA/EC) or 0 if not applicable.
|
||||
func hostKeyBits(k HostKeyInfo) int {
|
||||
pub, err := ssh.ParsePublicKey(k.RawKey)
|
||||
if err != nil {
|
||||
return 0
|
||||
}
|
||||
cp, ok := pub.(ssh.CryptoPublicKey)
|
||||
if !ok {
|
||||
return 0
|
||||
}
|
||||
type bitSizer interface{ Size() int }
|
||||
if bs, ok := cp.CryptoPublicKey().(bitSizer); ok {
|
||||
return bs.Size() * 8
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
// analyseHandshakeHostKey flags an endpoint where the full handshake
|
||||
// never yielded any host key.
|
||||
func analyseHandshakeHostKey(addr string, reached bool, keys []HostKeyInfo) []Issue {
|
||||
|
|
@ -46,11 +91,15 @@ func analyseHandshakeHostKey(addr string, reached bool, keys []HostKeyInfo) []Is
|
|||
func analyseHostKeyStrength(addr string, keys []HostKeyInfo) []Issue {
|
||||
var issues []Issue
|
||||
for _, k := range keys {
|
||||
if k.SSHFPAlgo == 1 && k.Bits > 0 && k.Bits < 2048 {
|
||||
if sshfpAlgoForKeyType(k.Type) != 1 {
|
||||
continue
|
||||
}
|
||||
bits := hostKeyBits(k)
|
||||
if bits > 0 && bits < 2048 {
|
||||
issues = append(issues, Issue{
|
||||
Code: "short_rsa_host_key",
|
||||
Severity: SeverityCrit,
|
||||
Message: fmt.Sprintf("RSA host key is %d bits; OpenSSH has rejected < 2048 bits since 8.2.", k.Bits),
|
||||
Message: fmt.Sprintf("RSA host key is %d bits; OpenSSH has rejected < 2048 bits since 8.2.", bits),
|
||||
Fix: "Regenerate the host key: rm /etc/ssh/ssh_host_rsa_key && ssh-keygen -t rsa -b 4096 -f /etc/ssh/ssh_host_rsa_key -N ''",
|
||||
Endpoint: addr,
|
||||
})
|
||||
|
|
@ -59,6 +108,29 @@ func analyseHostKeyStrength(addr string, keys []HostKeyInfo) []Issue {
|
|||
return issues
|
||||
}
|
||||
|
||||
// keyMatchesSSHFP reports whether k's fingerprints match any record in s.
|
||||
func keyMatchesSSHFP(k HostKeyInfo, s SSHFPSummary) (sha1Match, sha256Match bool) {
|
||||
algo := sshfpAlgoForKeyType(k.Type)
|
||||
sha1hex, sha256hex := hostKeyFingerprints(k)
|
||||
for _, rr := range s.Records {
|
||||
if rr.Algorithm != algo {
|
||||
continue
|
||||
}
|
||||
want := strings.ToLower(rr.Fingerprint)
|
||||
switch rr.Type {
|
||||
case 1:
|
||||
if want == sha1hex {
|
||||
sha1Match = true
|
||||
}
|
||||
case 2:
|
||||
if want == sha256hex {
|
||||
sha256Match = true
|
||||
}
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// analyseSSHFPAlignment returns per-key alignment issues: match,
|
||||
// no coverage for a key family, or mismatch between DNS and server.
|
||||
func analyseSSHFPAlignment(addr string, keys []HostKeyInfo, s SSHFPSummary) []Issue {
|
||||
|
|
@ -80,21 +152,18 @@ func analyseSSHFPAlignment(addr string, keys []HostKeyInfo, s SSHFPSummary) []Is
|
|||
coveredFamily[rr.Algorithm] = true
|
||||
}
|
||||
for _, k := range keys {
|
||||
if k.SSHFPMatchSHA256 || k.SSHFPMatchSHA1 {
|
||||
issues = append(issues, Issue{
|
||||
Code: "sshfp_verified",
|
||||
Severity: SeverityInfo,
|
||||
Message: fmt.Sprintf("Host key %s (%s) matches the published SSHFP record.", k.Type, shortFP(k.SHA256)),
|
||||
Endpoint: addr,
|
||||
})
|
||||
sha1Match, sha256Match := keyMatchesSSHFP(k, s)
|
||||
if sha256Match || sha1Match {
|
||||
continue
|
||||
}
|
||||
if !coveredFamily[k.SSHFPAlgo] {
|
||||
algo := sshfpAlgoForKeyType(k.Type)
|
||||
_, sha256hex := hostKeyFingerprints(k)
|
||||
if !coveredFamily[algo] {
|
||||
issues = append(issues, Issue{
|
||||
Code: "sshfp_not_covered",
|
||||
Severity: SeverityWarn,
|
||||
Message: fmt.Sprintf("No SSHFP record covers host-key algorithm %s.", k.Type),
|
||||
Fix: fmt.Sprintf("Add `IN SSHFP %d 2 %s` to the zone.", k.SSHFPAlgo, k.SHA256),
|
||||
Fix: fmt.Sprintf("Add `IN SSHFP %d 2 %s` to the zone.", algo, sha256hex),
|
||||
Endpoint: addr,
|
||||
})
|
||||
continue
|
||||
|
|
@ -103,7 +172,7 @@ func analyseSSHFPAlignment(addr string, keys []HostKeyInfo, s SSHFPSummary) []Is
|
|||
Code: "sshfp_mismatch",
|
||||
Severity: SeverityCrit,
|
||||
Message: fmt.Sprintf("Published SSHFP record does not match the %s host key presented by %s. Either the server key was rotated without updating DNS, or the server is impersonated.", k.Type, addr),
|
||||
Fix: fmt.Sprintf("Update the SSHFP record to the current fingerprint: `IN SSHFP %d 2 %s`, and investigate why DNS and the server disagree.", k.SSHFPAlgo, k.SHA256),
|
||||
Fix: fmt.Sprintf("Update the SSHFP record to the current fingerprint: `IN SSHFP %d 2 %s`, and investigate why DNS and the server disagree.", algo, sha256hex),
|
||||
Endpoint: addr,
|
||||
})
|
||||
}
|
||||
|
|
@ -119,7 +188,8 @@ func analyseSSHFPHashes(addr string, keys []HostKeyInfo, s SSHFPSummary) []Issue
|
|||
}
|
||||
matchedAny := false
|
||||
for _, k := range keys {
|
||||
if k.SSHFPMatchSHA256 || k.SSHFPMatchSHA1 {
|
||||
sha1Match, sha256Match := keyMatchesSSHFP(k, s)
|
||||
if sha256Match || sha1Match {
|
||||
matchedAny = true
|
||||
break
|
||||
}
|
||||
|
|
@ -157,11 +227,13 @@ func analyseHostKeys(addr string, keys []HostKeyInfo, s SSHFPSummary, reachedKex
|
|||
func firstSHA256(keys []HostKeyInfo) string {
|
||||
for _, k := range keys {
|
||||
if k.Type == "ssh-ed25519" {
|
||||
return k.SHA256
|
||||
_, sha256hex := hostKeyFingerprints(k)
|
||||
return sha256hex
|
||||
}
|
||||
}
|
||||
if len(keys) > 0 {
|
||||
return keys[0].SHA256
|
||||
_, sha256hex := hostKeyFingerprints(keys[0])
|
||||
return sha256hex
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue