ssh/knownhosts: compare only public key portions for revocation
Revocation matching compared full marshaled key blobs, so a @revoked line containing a plain host key did not revoke a certificate certifying that key: when the server presented such a certificate, IsRevoked only matched the whole certificate blob or the CA key, and verification succeeded even though the underlying host key was revoked. OpenSSH's check_key_not_revoked compares keys with sshkey_equal_public, which considers only the public portions and explicitly allows comparisons between certificates and plain keys. Match that behavior by normalizing both @revoked entries and lookups to the underlying public key: for a certificate, the certified key rather than the certificate blob. This also makes a @revoked line containing a certificate revoke the plain key it certifies Change-Id: I3029d59e68fb01f2340763e2eae25cac23c6193d Reviewed-on: https://go-review.googlesource.com/c/crypto/+/816841 Reviewed-by: Filippo Valsorda <filippo@golang.org> LUCI-TryBot-Result: golang-scoped@luci-project-accounts.iam.gserviceaccount.com <golang-scoped@luci-project-accounts.iam.gserviceaccount.com> Reviewed-by: David Chase <drchase@google.com> Reviewed-by: Dmitri Shuralyov <dmitshur@google.com>
This commit is contained in:
@@ -142,6 +142,15 @@ func keyEq(a, b ssh.PublicKey) bool {
|
|||||||
return bytes.Equal(a.Marshal(), b.Marshal())
|
return bytes.Equal(a.Marshal(), b.Marshal())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// plainKeyBlob returns the serialized public portion of key: for a
|
||||||
|
// certificate this is the certified key, otherwise the key itself.
|
||||||
|
func plainKeyBlob(key ssh.PublicKey) string {
|
||||||
|
if cert, ok := key.(*ssh.Certificate); ok {
|
||||||
|
return string(cert.Key.Marshal())
|
||||||
|
}
|
||||||
|
return string(key.Marshal())
|
||||||
|
}
|
||||||
|
|
||||||
// IsHostAuthority can be used as a callback in ssh.CertChecker
|
// IsHostAuthority can be used as a callback in ssh.CertChecker
|
||||||
func (db *hostKeyDB) IsHostAuthority(remote ssh.PublicKey, address string) bool {
|
func (db *hostKeyDB) IsHostAuthority(remote ssh.PublicKey, address string) bool {
|
||||||
h, p, err := net.SplitHostPort(address)
|
h, p, err := net.SplitHostPort(address)
|
||||||
@@ -160,10 +169,10 @@ func (db *hostKeyDB) IsHostAuthority(remote ssh.PublicKey, address string) bool
|
|||||||
|
|
||||||
// IsRevoked can be used as a callback in ssh.CertChecker
|
// IsRevoked can be used as a callback in ssh.CertChecker
|
||||||
func (db *hostKeyDB) IsRevoked(key *ssh.Certificate) bool {
|
func (db *hostKeyDB) IsRevoked(key *ssh.Certificate) bool {
|
||||||
if _, ok := db.revoked[string(key.Marshal())]; ok {
|
if _, ok := db.revoked[plainKeyBlob(key)]; ok {
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
if _, ok := db.revoked[string(key.SignatureKey.Marshal())]; ok {
|
if _, ok := db.revoked[plainKeyBlob(key.SignatureKey)]; ok {
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
return false
|
return false
|
||||||
@@ -228,7 +237,7 @@ func (db *hostKeyDB) parseLine(line []byte, filename string, linenum int) error
|
|||||||
}
|
}
|
||||||
|
|
||||||
if marker == markerRevoked {
|
if marker == markerRevoked {
|
||||||
db.revoked[string(key.Marshal())] = &KnownKey{
|
db.revoked[plainKeyBlob(key)] = &KnownKey{
|
||||||
Key: key,
|
Key: key,
|
||||||
Filename: filename,
|
Filename: filename,
|
||||||
Line: linenum,
|
Line: linenum,
|
||||||
@@ -341,7 +350,7 @@ func (r *RevokedError) Error() string {
|
|||||||
// check checks a key against the host database. This should not be
|
// check checks a key against the host database. This should not be
|
||||||
// used for verifying certificates.
|
// used for verifying certificates.
|
||||||
func (db *hostKeyDB) check(address string, remote net.Addr, remoteKey ssh.PublicKey) error {
|
func (db *hostKeyDB) check(address string, remote net.Addr, remoteKey ssh.PublicKey) error {
|
||||||
if revoked := db.revoked[string(remoteKey.Marshal())]; revoked != nil {
|
if revoked := db.revoked[plainKeyBlob(remoteKey)]; revoked != nil {
|
||||||
return &RevokedError{Revoked: *revoked}
|
return &RevokedError{Revoked: *revoked}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -316,6 +316,68 @@ func TestRevokedCA(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestRevokedCertifiedKey(t *testing.T) {
|
||||||
|
_, caPriv, err := ed25519.GenerateKey(rand.Reader)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
caSigner, err := ssh.NewSignerFromKey(caPriv)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
_, hostPriv, err := ed25519.GenerateKey(rand.Reader)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
hostKey, err := ssh.NewPublicKey(hostPriv.Public())
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
signCert := func(serial uint64, principal string) *ssh.Certificate {
|
||||||
|
cert := &ssh.Certificate{
|
||||||
|
CertType: ssh.HostCert,
|
||||||
|
Key: hostKey,
|
||||||
|
Serial: serial,
|
||||||
|
ValidBefore: ssh.CertTimeInfinity,
|
||||||
|
ValidPrincipals: []string{principal},
|
||||||
|
}
|
||||||
|
if err := cert.SignCert(rand.Reader, caSigner); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
return cert
|
||||||
|
}
|
||||||
|
cert := signCert(1, "server.org")
|
||||||
|
|
||||||
|
// Revoking the host key revokes any certificate for it, even when
|
||||||
|
// the CA is trusted, matching OpenSSH's check_key_not_revoked.
|
||||||
|
db := testDB(t, "@revoked server.org "+serialize(hostKey)+"\n"+
|
||||||
|
"@cert-authority server.org "+serialize(caSigner.PublicKey())+"\n")
|
||||||
|
if !db.IsRevoked(cert) {
|
||||||
|
t.Error("IsRevoked returned false for certificate with revoked certified key")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Conversely, a @revoked line containing a certificate revokes the
|
||||||
|
// plain key it certifies.
|
||||||
|
db = testDB(t, "@revoked server.org "+serialize(cert)+"\n")
|
||||||
|
if err := db.check("server.org:22", testAddr, hostKey); err == nil {
|
||||||
|
t.Error("no error for plain key revoked via certificate")
|
||||||
|
} else if _, ok := err.(*RevokedError); !ok {
|
||||||
|
t.Errorf("got %T (%v), want *RevokedError", err, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Since only public portions are compared, revoking a certificate
|
||||||
|
// also revokes any sibling certificate certifying the same key,
|
||||||
|
// whatever its serial number and principals.
|
||||||
|
sibling := signCert(2, "other.org")
|
||||||
|
db = testDB(t, "@revoked * "+serialize(cert)+"\n"+
|
||||||
|
"@cert-authority * "+serialize(caSigner.PublicKey())+"\n")
|
||||||
|
if !db.IsRevoked(sibling) {
|
||||||
|
t.Error("IsRevoked returned false for sibling certificate of a revoked certificate")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const testHostname = "hostname"
|
const testHostname = "hostname"
|
||||||
|
|
||||||
// generated with keygen -H -f
|
// generated with keygen -H -f
|
||||||
|
|||||||
Reference in New Issue
Block a user