Skip to content

Commit

Permalink
ssh: support RSA SHA-2 (RFC8332) signatures
Browse files Browse the repository at this point in the history
This change adds support for RSA SHA-2 based signatures for host keys and certificates. It also switches the default certificate signature algorithm for RSA to use SHA-512. This is implemented by treating ssh.Signer specially when the key type is `ssh-rsa` by also allowing SHA-256 and SHA-512 signatures.

Fixes golang/go#37278

Change-Id: I2ee1ac4ae4c9c1de441a2d6cf1e806357ef18910
Reviewed-on: https://go-review.googlesource.com/c/crypto/+/220037
Trust: Jason A. Donenfeld <Jason@zx2c4.com>
Run-TryBot: Jason A. Donenfeld <Jason@zx2c4.com>
TryBot-Result: Go Bot <gobot@golang.org>
Reviewed-by: Jason A. Donenfeld <Jason@zx2c4.com>
Reviewed-by: Roland Shoemaker <roland@golang.org>
  • Loading branch information
hansnielsen authored and zx2c4 committed Nov 15, 2021
1 parent ceb1ce7 commit b4de73f
Show file tree
Hide file tree
Showing 12 changed files with 213 additions and 69 deletions.
24 changes: 22 additions & 2 deletions ssh/certs.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ import (
"time"
)

// These constants from [PROTOCOL.certkeys] represent the algorithm names
// These constants from [PROTOCOL.certkeys] represent the key algorithm names
// for certificate types supported by this package.
const (
CertAlgoRSAv01 = "ssh-rsa-cert-v01@openssh.com"
Expand All @@ -27,6 +27,14 @@ const (
CertAlgoSKED25519v01 = "sk-ssh-ed25519-cert-v01@openssh.com"
)

// These constants from [PROTOCOL.certkeys] represent additional signature
// algorithm names for certificate types supported by this package.
const (
CertSigAlgoRSAv01 = "ssh-rsa-cert-v01@openssh.com"
CertSigAlgoRSASHA2256v01 = "rsa-sha2-256-cert-v01@openssh.com"
CertSigAlgoRSASHA2512v01 = "rsa-sha2-512-cert-v01@openssh.com"
)

// Certificate types distinguish between host and user
// certificates. The values can be set in the CertType field of
// Certificate.
Expand Down Expand Up @@ -423,6 +431,12 @@ func (c *Certificate) SignCert(rand io.Reader, authority Signer) error {
}
c.SignatureKey = authority.PublicKey()

if v, ok := authority.(AlgorithmSigner); ok {
if v.PublicKey().Type() == KeyAlgoRSA {
authority = &rsaSigner{v, SigAlgoRSASHA2512}
}
}

sig, err := authority.Sign(rand, c.bytesForSigning())
if err != nil {
return err
Expand All @@ -431,8 +445,14 @@ func (c *Certificate) SignCert(rand io.Reader, authority Signer) error {
return nil
}

// certAlgoNames includes a mapping from signature algorithms to the
// corresponding certificate signature algorithm. When a key type (such
// as ED25516) is associated with only one algorithm, the KeyAlgo
// constant is used instead of the SigAlgo.
var certAlgoNames = map[string]string{
KeyAlgoRSA: CertAlgoRSAv01,
SigAlgoRSA: CertSigAlgoRSAv01,
SigAlgoRSASHA2256: CertSigAlgoRSASHA2256v01,
SigAlgoRSASHA2512: CertSigAlgoRSASHA2512v01,
KeyAlgoDSA: CertAlgoDSAv01,
KeyAlgoECDSA256: CertAlgoECDSA256v01,
KeyAlgoECDSA384: CertAlgoECDSA384v01,
Expand Down
78 changes: 31 additions & 47 deletions ssh/certs_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,12 +9,12 @@ import (
"crypto/ecdsa"
"crypto/elliptic"
"crypto/rand"
"fmt"
"io"
"net"
"reflect"
"testing"
"time"

"golang.org/x/crypto/ssh/testdata"
)

// Cert generated by ssh-keygen 6.0p1 Debian-4.
Expand Down Expand Up @@ -226,53 +226,33 @@ func TestHostKeyCert(t *testing.T) {
}
}

type legacyRSASigner struct {
Signer
}

func (s *legacyRSASigner) Sign(rand io.Reader, data []byte) (*Signature, error) {
v, ok := s.Signer.(AlgorithmSigner)
if !ok {
return nil, fmt.Errorf("invalid signer")
}
return v.SignWithAlgorithm(rand, data, SigAlgoRSA)
}

func TestCertTypes(t *testing.T) {
var testVars = []struct {
name string
keys func() Signer
name string
signer Signer
algo string
}{
{
name: CertAlgoECDSA256v01,
keys: func() Signer {
s, _ := ParsePrivateKey(testdata.PEMBytes["ecdsap256"])
return s
},
},
{
name: CertAlgoECDSA384v01,
keys: func() Signer {
s, _ := ParsePrivateKey(testdata.PEMBytes["ecdsap384"])
return s
},
},
{
name: CertAlgoECDSA521v01,
keys: func() Signer {
s, _ := ParsePrivateKey(testdata.PEMBytes["ecdsap521"])
return s
},
},
{
name: CertAlgoED25519v01,
keys: func() Signer {
s, _ := ParsePrivateKey(testdata.PEMBytes["ed25519"])
return s
},
},
{
name: CertAlgoRSAv01,
keys: func() Signer {
s, _ := ParsePrivateKey(testdata.PEMBytes["rsa"])
return s
},
},
{
name: CertAlgoDSAv01,
keys: func() Signer {
s, _ := ParsePrivateKey(testdata.PEMBytes["dsa"])
return s
},
},
{CertAlgoECDSA256v01, testSigners["ecdsap256"], ""},
{CertAlgoECDSA384v01, testSigners["ecdsap384"], ""},
{CertAlgoECDSA521v01, testSigners["ecdsap521"], ""},
{CertAlgoED25519v01, testSigners["ed25519"], ""},
{CertAlgoRSAv01, testSigners["rsa"], SigAlgoRSASHA2512},
{CertAlgoRSAv01, &legacyRSASigner{testSigners["rsa"]}, SigAlgoRSA},
{CertAlgoRSAv01, testSigners["rsa-sha2-256"], SigAlgoRSASHA2512},
{CertAlgoRSAv01, testSigners["rsa-sha2-512"], SigAlgoRSASHA2512},
{CertAlgoDSAv01, testSigners["dsa"], ""},
}

k, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
Expand Down Expand Up @@ -304,7 +284,7 @@ func TestCertTypes(t *testing.T) {

go NewServerConn(c1, conf)

priv := m.keys()
priv := m.signer
if err != nil {
t.Fatalf("error generating ssh pubkey: %v", err)
}
Expand All @@ -320,6 +300,10 @@ func TestCertTypes(t *testing.T) {
t.Fatalf("error generating cert signer: %v", err)
}

if m.algo != "" && cert.Signature.Format != m.algo {
t.Errorf("expected %q signature format, got %q", m.algo, cert.Signature.Format)
}

config := &ClientConfig{
User: "user",
HostKeyCallback: func(h string, r net.Addr, k PublicKey) error { return nil },
Expand Down
15 changes: 14 additions & 1 deletion ssh/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -115,12 +115,25 @@ func (c *connection) clientHandshake(dialAddress string, config *ClientConfig) e

// verifyHostKeySignature verifies the host key obtained in the key
// exchange.
func verifyHostKeySignature(hostKey PublicKey, result *kexResult) error {
func verifyHostKeySignature(hostKey PublicKey, algo string, result *kexResult) error {
sig, rest, ok := parseSignatureBody(result.Signature)
if len(rest) > 0 || !ok {
return errors.New("ssh: signature parse error")
}

// For keys, underlyingAlgo is exactly algo. For certificates,
// we have to look up the underlying key algorithm that SSH
// uses to evaluate signatures.
underlyingAlgo := algo
for sigAlgo, certAlgo := range certAlgoNames {
if certAlgo == algo {
underlyingAlgo = sigAlgo
}
}
if sig.Format != underlyingAlgo {
return fmt.Errorf("ssh: invalid signature algorithm %q, expected %q", sig.Format, underlyingAlgo)
}

return hostKey.Verify(result.H, sig)
}

Expand Down
41 changes: 41 additions & 0 deletions ssh/client_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@
package ssh

import (
"bytes"
"crypto/rand"
"strings"
"testing"
)
Expand Down Expand Up @@ -116,6 +118,45 @@ func TestHostKeyCheck(t *testing.T) {
}
}

func TestVerifyHostKeySignature(t *testing.T) {
for _, tt := range []struct {
key string
signAlgo string
verifyAlgo string
wantError string
}{
{"rsa", SigAlgoRSA, SigAlgoRSA, ""},
{"rsa", SigAlgoRSASHA2256, SigAlgoRSASHA2256, ""},
{"rsa", SigAlgoRSA, SigAlgoRSASHA2512, `ssh: invalid signature algorithm "ssh-rsa", expected "rsa-sha2-512"`},
{"ed25519", KeyAlgoED25519, KeyAlgoED25519, ""},
} {
key := testSigners[tt.key].PublicKey()
s, ok := testSigners[tt.key].(AlgorithmSigner)
if !ok {
t.Fatalf("needed an AlgorithmSigner")
}
sig, err := s.SignWithAlgorithm(rand.Reader, []byte("test"), tt.signAlgo)
if err != nil {
t.Fatalf("couldn't sign: %q", err)
}

b := bytes.Buffer{}
writeString(&b, []byte(sig.Format))
writeString(&b, sig.Blob)

result := kexResult{Signature: b.Bytes(), H: []byte("test")}

err = verifyHostKeySignature(key, tt.verifyAlgo, &result)
if err != nil {
if tt.wantError == "" || !strings.Contains(err.Error(), tt.wantError) {
t.Errorf("got error %q, expecting %q", err.Error(), tt.wantError)
}
} else if tt.wantError != "" {
t.Errorf("succeeded, but want error string %q", tt.wantError)
}
}
}

func TestBannerCallback(t *testing.T) {
c1, c2, err := netPipe()
if err != nil {
Expand Down
30 changes: 18 additions & 12 deletions ssh/common.go
Original file line number Diff line number Diff line change
Expand Up @@ -69,11 +69,13 @@ var preferredKexAlgos = []string{
// supportedHostKeyAlgos specifies the supported host-key algorithms (i.e. methods
// of authenticating servers) in preference order.
var supportedHostKeyAlgos = []string{
CertAlgoRSAv01, CertAlgoDSAv01, CertAlgoECDSA256v01,
CertSigAlgoRSASHA2512v01, CertSigAlgoRSASHA2256v01,
CertSigAlgoRSAv01, CertAlgoDSAv01, CertAlgoECDSA256v01,
CertAlgoECDSA384v01, CertAlgoECDSA521v01, CertAlgoED25519v01,

KeyAlgoECDSA256, KeyAlgoECDSA384, KeyAlgoECDSA521,
KeyAlgoRSA, KeyAlgoDSA,
SigAlgoRSASHA2512, SigAlgoRSASHA2256,
SigAlgoRSA, KeyAlgoDSA,

KeyAlgoED25519,
}
Expand All @@ -90,16 +92,20 @@ var supportedCompressions = []string{compressionNone}
// hashFuncs keeps the mapping of supported algorithms to their respective
// hashes needed for signature verification.
var hashFuncs = map[string]crypto.Hash{
KeyAlgoRSA: crypto.SHA1,
KeyAlgoDSA: crypto.SHA1,
KeyAlgoECDSA256: crypto.SHA256,
KeyAlgoECDSA384: crypto.SHA384,
KeyAlgoECDSA521: crypto.SHA512,
CertAlgoRSAv01: crypto.SHA1,
CertAlgoDSAv01: crypto.SHA1,
CertAlgoECDSA256v01: crypto.SHA256,
CertAlgoECDSA384v01: crypto.SHA384,
CertAlgoECDSA521v01: crypto.SHA512,
SigAlgoRSA: crypto.SHA1,
SigAlgoRSASHA2256: crypto.SHA256,
SigAlgoRSASHA2512: crypto.SHA512,
KeyAlgoDSA: crypto.SHA1,
KeyAlgoECDSA256: crypto.SHA256,
KeyAlgoECDSA384: crypto.SHA384,
KeyAlgoECDSA521: crypto.SHA512,
CertSigAlgoRSAv01: crypto.SHA1,
CertSigAlgoRSASHA2256v01: crypto.SHA256,
CertSigAlgoRSASHA2512v01: crypto.SHA512,
CertAlgoDSAv01: crypto.SHA1,
CertAlgoECDSA256v01: crypto.SHA256,
CertAlgoECDSA384v01: crypto.SHA384,
CertAlgoECDSA521v01: crypto.SHA512,
}

// unexpectedMessageError results when the SSH message that we received didn't
Expand Down
29 changes: 25 additions & 4 deletions ssh/handshake.go
Original file line number Diff line number Diff line change
Expand Up @@ -457,8 +457,15 @@ func (t *handshakeTransport) sendKexInit() error {

if len(t.hostKeys) > 0 {
for _, k := range t.hostKeys {
msg.ServerHostKeyAlgos = append(
msg.ServerHostKeyAlgos, k.PublicKey().Type())
algo := k.PublicKey().Type()
switch algo {
case KeyAlgoRSA:
msg.ServerHostKeyAlgos = append(msg.ServerHostKeyAlgos, []string{SigAlgoRSASHA2512, SigAlgoRSASHA2256, SigAlgoRSA}...)
case CertAlgoRSAv01:
msg.ServerHostKeyAlgos = append(msg.ServerHostKeyAlgos, []string{CertSigAlgoRSASHA2512v01, CertSigAlgoRSASHA2256v01, CertSigAlgoRSAv01}...)
default:
msg.ServerHostKeyAlgos = append(msg.ServerHostKeyAlgos, algo)
}
}
} else {
msg.ServerHostKeyAlgos = t.hostKeyAlgorithms
Expand Down Expand Up @@ -614,8 +621,22 @@ func (t *handshakeTransport) enterKeyExchange(otherInitPacket []byte) error {
func (t *handshakeTransport) server(kex kexAlgorithm, algs *algorithms, magics *handshakeMagics) (*kexResult, error) {
var hostKey Signer
for _, k := range t.hostKeys {
if algs.hostKey == k.PublicKey().Type() {
kt := k.PublicKey().Type()
if kt == algs.hostKey {
hostKey = k
} else if signer, ok := k.(AlgorithmSigner); ok {
// Some signature algorithms don't show up as key types
// so we have to manually check for a compatible host key.
switch kt {
case KeyAlgoRSA:
if algs.hostKey == SigAlgoRSASHA2256 || algs.hostKey == SigAlgoRSASHA2512 {
hostKey = &rsaSigner{signer, algs.hostKey}
}
case CertAlgoRSAv01:
if algs.hostKey == CertSigAlgoRSASHA2256v01 || algs.hostKey == CertSigAlgoRSASHA2512v01 {
hostKey = &rsaSigner{signer, certToPrivAlgo(algs.hostKey)}
}
}
}
}

Expand All @@ -634,7 +655,7 @@ func (t *handshakeTransport) client(kex kexAlgorithm, algs *algorithms, magics *
return nil, err
}

if err := verifyHostKeySignature(hostKey, result); err != nil {
if err := verifyHostKeySignature(hostKey, algs.hostKey, result); err != nil {
return nil, err
}

Expand Down
9 changes: 9 additions & 0 deletions ssh/keys.go
Original file line number Diff line number Diff line change
Expand Up @@ -939,6 +939,15 @@ func newDSAPrivateKey(key *dsa.PrivateKey) (Signer, error) {
return &dsaPrivateKey{key}, nil
}

type rsaSigner struct {
AlgorithmSigner
defaultAlgorithm string
}

func (s *rsaSigner) Sign(rand io.Reader, data []byte) (*Signature, error) {
return s.AlgorithmSigner.SignWithAlgorithm(rand, data, s.defaultAlgorithm)
}

type wrappedSigner struct {
signer crypto.Signer
pubKey PublicKey
Expand Down
2 changes: 1 addition & 1 deletion ssh/server.go
Original file line number Diff line number Diff line change
Expand Up @@ -284,7 +284,7 @@ func (s *connection) serverHandshake(config *ServerConfig) (*Permissions, error)

func isAcceptableAlgo(algo string) bool {
switch algo {
case KeyAlgoRSA, KeyAlgoDSA, KeyAlgoECDSA256, KeyAlgoECDSA384, KeyAlgoECDSA521, KeyAlgoSKECDSA256, KeyAlgoED25519, KeyAlgoSKED25519,
case SigAlgoRSA, SigAlgoRSASHA2256, SigAlgoRSASHA2512, KeyAlgoDSA, KeyAlgoECDSA256, KeyAlgoECDSA384, KeyAlgoECDSA521, KeyAlgoSKECDSA256, KeyAlgoED25519, KeyAlgoSKED25519,
CertAlgoRSAv01, CertAlgoDSAv01, CertAlgoECDSA256v01, CertAlgoECDSA384v01, CertAlgoECDSA521v01, CertAlgoSKECDSA256v01, CertAlgoED25519v01, CertAlgoSKED25519v01:
return true
}
Expand Down
8 changes: 7 additions & 1 deletion ssh/session_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -757,7 +757,13 @@ func TestHostKeyAlgorithms(t *testing.T) {
connect(clientConf, KeyAlgoECDSA256)

// Client asks for RSA explicitly.
clientConf.HostKeyAlgorithms = []string{KeyAlgoRSA}
clientConf.HostKeyAlgorithms = []string{SigAlgoRSA}
connect(clientConf, KeyAlgoRSA)

// Client asks for RSA-SHA2-512 explicitly.
clientConf.HostKeyAlgorithms = []string{SigAlgoRSASHA2512}
// We get back an "ssh-rsa" key but the verification happened
// with an RSA-SHA2-512 signature.
connect(clientConf, KeyAlgoRSA)

c1, c2, err := netPipe()
Expand Down
2 changes: 1 addition & 1 deletion ssh/test/test_unix_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ Banner {{.Dir}}/banner
HostKey {{.Dir}}/id_rsa
HostKey {{.Dir}}/id_dsa
HostKey {{.Dir}}/id_ecdsa
HostCertificate {{.Dir}}/id_rsa-cert.pub
HostCertificate {{.Dir}}/id_rsa-sha2-512-cert.pub
Pidfile {{.Dir}}/sshd.pid
#UsePrivilegeSeparation no
KeyRegenerationInterval 3600
Expand Down

0 comments on commit b4de73f

Please sign in to comment.