ssh-client: the mesh's region first in ~/.ssh/config, its hosts in config.d, tools in Go
The region at the end let earlier Host lines win over the mesh's (research 027/03). A roster fact cannot be placed at the start, so the region holds one Include of config.d, and the hosts are config.d/00-mesh, read first. Eight tools; authorized_keys and known_hosts stay found until the controller holds those facts.
This commit is contained in:
@@ -0,0 +1,110 @@
|
||||
package main
|
||||
|
||||
// Keys, by their public half only. A fingerprint is computed here from the public key's bytes, as
|
||||
// ssh-keygen -l does (SHA256 of the key blob, unpadded base64); a private key is never read by this
|
||||
// process — its first line, the PEM header, says it is one, and ssh-keygen is asked about it.
|
||||
|
||||
import (
|
||||
"crypto/sha256"
|
||||
"encoding/base64"
|
||||
"encoding/binary"
|
||||
"fmt"
|
||||
"math/big"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// KeyTypes are the public key algorithms an authorized_keys or known_hosts line can carry.
|
||||
var KeyTypes = map[string]bool{
|
||||
"ssh-ed25519": true, "ssh-rsa": true, "ssh-dss": true,
|
||||
"ecdsa-sha2-nistp256": true, "ecdsa-sha2-nistp384": true, "ecdsa-sha2-nistp521": true,
|
||||
"sk-ssh-ed25519@openssh.com": true, "sk-ecdsa-sha2-nistp256@openssh.com": true,
|
||||
}
|
||||
|
||||
// PublicKey is one public key as a line carries it.
|
||||
type PublicKey struct {
|
||||
Type string `json:"type"`
|
||||
Bits int `json:"bits"`
|
||||
Fingerprint string `json:"fingerprint"`
|
||||
Comment string `json:"comment"`
|
||||
Options string `json:"options,omitempty"`
|
||||
Weak string `json:"weak,omitempty"`
|
||||
}
|
||||
|
||||
// ParseKeyLine reads one authorized_keys line (options, type, key, comment) or a public key file's.
|
||||
func ParseKeyLine(line string) (PublicKey, error) {
|
||||
fields := fieldsQuoted(strings.TrimSpace(line))
|
||||
for i, f := range fields {
|
||||
if !KeyTypes[f] || i+1 >= len(fields) {
|
||||
continue
|
||||
}
|
||||
k := PublicKey{Type: f, Options: strings.Join(fields[:i], " "), Comment: strings.Join(fields[i+2:], " ")}
|
||||
blob, err := base64.StdEncoding.DecodeString(fields[i+1])
|
||||
if err != nil {
|
||||
return k, fmt.Errorf("the key after %s is not base64", f)
|
||||
}
|
||||
sum := sha256.Sum256(blob)
|
||||
k.Fingerprint = "SHA256:" + base64.RawStdEncoding.EncodeToString(sum[:])
|
||||
k.Bits = bitsOf(f, blob)
|
||||
switch {
|
||||
case f == "ssh-dss":
|
||||
k.Weak = "DSA: refused by current OpenSSH"
|
||||
case f == "ssh-rsa" && k.Bits > 0 && k.Bits < 3072:
|
||||
k.Weak = fmt.Sprintf("RSA of %d bits: below 3072", k.Bits)
|
||||
}
|
||||
return k, nil
|
||||
}
|
||||
return PublicKey{}, fmt.Errorf("no public key on this line")
|
||||
}
|
||||
|
||||
// fieldsQuoted splits on spaces outside double quotes, as sshd reads an options field.
|
||||
func fieldsQuoted(s string) []string {
|
||||
var out []string
|
||||
var cur strings.Builder
|
||||
quoted := false
|
||||
for _, ch := range s {
|
||||
switch {
|
||||
case ch == '"':
|
||||
quoted = !quoted
|
||||
cur.WriteRune(ch)
|
||||
case (ch == ' ' || ch == '\t') && !quoted:
|
||||
if cur.Len() > 0 {
|
||||
out = append(out, cur.String())
|
||||
cur.Reset()
|
||||
}
|
||||
default:
|
||||
cur.WriteRune(ch)
|
||||
}
|
||||
}
|
||||
if cur.Len() > 0 {
|
||||
out = append(out, cur.String())
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
// bitsOf reads a key's size from its blob: the RSA modulus, the curve, or ed25519's fixed 256.
|
||||
func bitsOf(kind string, blob []byte) int {
|
||||
strs := [][]byte{}
|
||||
for len(blob) >= 4 {
|
||||
n := binary.BigEndian.Uint32(blob)
|
||||
if int(n) > len(blob)-4 {
|
||||
break
|
||||
}
|
||||
strs = append(strs, blob[4:4+n])
|
||||
blob = blob[4+n:]
|
||||
}
|
||||
switch {
|
||||
case strings.Contains(kind, "ed25519"):
|
||||
return 256
|
||||
case kind == "ssh-rsa" && len(strs) >= 3:
|
||||
return new(big.Int).SetBytes(strs[2]).BitLen()
|
||||
case kind == "ssh-dss" && len(strs) >= 2:
|
||||
return new(big.Int).SetBytes(strs[1]).BitLen()
|
||||
case strings.Contains(kind, "nistp256"):
|
||||
return 256
|
||||
case strings.Contains(kind, "nistp384"):
|
||||
return 384
|
||||
case strings.Contains(kind, "nistp521"):
|
||||
return 521
|
||||
}
|
||||
return 0
|
||||
}
|
||||
Reference in New Issue
Block a user