Skip to content

Commit

Permalink
ssh: add sk-ecdsa-sha2-nistp256 and sk-ed25519
Browse files Browse the repository at this point in the history
This adds server-side support for the newly introduced OpenSSH
keytypes [email protected] and [email protected]
(including their corresponding certificates), which are backed
by U2F/FIDO2 tokens.

Change-Id: I53d5ed3d0457ae4758ee986055e187ee5787a2d1
Reviewed-on: https://go-review.googlesource.com/c/crypto/+/208017
Reviewed-by: Han-Wen Nienhuys <[email protected]>
Run-TryBot: Han-Wen Nienhuys <[email protected]>
TryBot-Result: Gobot Gobot <[email protected]>
  • Loading branch information
sebkinne authored and hanwen committed Dec 2, 2019
1 parent b544559 commit 86a7050
Show file tree
Hide file tree
Showing 5 changed files with 314 additions and 21 deletions.
35 changes: 23 additions & 12 deletions ssh/certs.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,12 +17,14 @@ import (
// These constants from [PROTOCOL.certkeys] represent the algorithm names
// for certificate types supported by this package.
const (
CertAlgoRSAv01 = "[email protected]"
CertAlgoDSAv01 = "[email protected]"
CertAlgoECDSA256v01 = "[email protected]"
CertAlgoECDSA384v01 = "[email protected]"
CertAlgoECDSA521v01 = "[email protected]"
CertAlgoED25519v01 = "[email protected]"
CertAlgoRSAv01 = "[email protected]"
CertAlgoDSAv01 = "[email protected]"
CertAlgoECDSA256v01 = "[email protected]"
CertAlgoECDSA384v01 = "[email protected]"
CertAlgoECDSA521v01 = "[email protected]"
CertAlgoSKECDSA256v01 = "[email protected]"
CertAlgoED25519v01 = "[email protected]"
CertAlgoSKED25519v01 = "[email protected]"
)

// Certificate types distinguish between host and user
Expand All @@ -37,6 +39,7 @@ const (
type Signature struct {
Format string
Blob []byte
Rest []byte `ssh:"rest"`
}

// CertTimeInfinity can be used for OpenSSHCertV01.ValidBefore to indicate that
Expand Down Expand Up @@ -429,12 +432,14 @@ func (c *Certificate) SignCert(rand io.Reader, authority Signer) error {
}

var certAlgoNames = map[string]string{
KeyAlgoRSA: CertAlgoRSAv01,
KeyAlgoDSA: CertAlgoDSAv01,
KeyAlgoECDSA256: CertAlgoECDSA256v01,
KeyAlgoECDSA384: CertAlgoECDSA384v01,
KeyAlgoECDSA521: CertAlgoECDSA521v01,
KeyAlgoED25519: CertAlgoED25519v01,
KeyAlgoRSA: CertAlgoRSAv01,
KeyAlgoDSA: CertAlgoDSAv01,
KeyAlgoECDSA256: CertAlgoECDSA256v01,
KeyAlgoECDSA384: CertAlgoECDSA384v01,
KeyAlgoECDSA521: CertAlgoECDSA521v01,
KeyAlgoSKECDSA256: CertAlgoSKECDSA256v01,
KeyAlgoED25519: CertAlgoED25519v01,
KeyAlgoSKED25519: CertAlgoSKED25519v01,
}

// certToPrivAlgo returns the underlying algorithm for a certificate algorithm.
Expand Down Expand Up @@ -518,6 +523,12 @@ func parseSignatureBody(in []byte) (out *Signature, rest []byte, ok bool) {
return
}

switch out.Format {
case KeyAlgoSKECDSA256, CertAlgoSKECDSA256v01, KeyAlgoSKED25519, CertAlgoSKED25519v01:
out.Rest = in
return out, nil, ok
}

return out, in, ok
}

Expand Down
232 changes: 225 additions & 7 deletions ssh/keys.go
Original file line number Diff line number Diff line change
Expand Up @@ -30,12 +30,14 @@ import (
// These constants represent the algorithm names for key types supported by this
// package.
const (
KeyAlgoRSA = "ssh-rsa"
KeyAlgoDSA = "ssh-dss"
KeyAlgoECDSA256 = "ecdsa-sha2-nistp256"
KeyAlgoECDSA384 = "ecdsa-sha2-nistp384"
KeyAlgoECDSA521 = "ecdsa-sha2-nistp521"
KeyAlgoED25519 = "ssh-ed25519"
KeyAlgoRSA = "ssh-rsa"
KeyAlgoDSA = "ssh-dss"
KeyAlgoECDSA256 = "ecdsa-sha2-nistp256"
KeyAlgoSKECDSA256 = "[email protected]"
KeyAlgoECDSA384 = "ecdsa-sha2-nistp384"
KeyAlgoECDSA521 = "ecdsa-sha2-nistp521"
KeyAlgoED25519 = "ssh-ed25519"
KeyAlgoSKED25519 = "[email protected]"
)

// These constants represent non-default signature algorithms that are supported
Expand All @@ -58,9 +60,13 @@ func parsePubKey(in []byte, algo string) (pubKey PublicKey, rest []byte, err err
return parseDSA(in)
case KeyAlgoECDSA256, KeyAlgoECDSA384, KeyAlgoECDSA521:
return parseECDSA(in)
case KeyAlgoSKECDSA256:
return parseSKECDSA(in)
case KeyAlgoED25519:
return parseED25519(in)
case CertAlgoRSAv01, CertAlgoDSAv01, CertAlgoECDSA256v01, CertAlgoECDSA384v01, CertAlgoECDSA521v01, CertAlgoED25519v01:
case KeyAlgoSKED25519:
return parseSKEd25519(in)
case CertAlgoRSAv01, CertAlgoDSAv01, CertAlgoECDSA256v01, CertAlgoECDSA384v01, CertAlgoECDSA521v01, CertAlgoSKECDSA256v01, CertAlgoED25519v01, CertAlgoSKED25519v01:
cert, err := parseCert(in, certToPrivAlgo(algo))
if err != nil {
return nil, nil, err
Expand Down Expand Up @@ -685,6 +691,218 @@ func (k *ecdsaPublicKey) CryptoPublicKey() crypto.PublicKey {
return (*ecdsa.PublicKey)(k)
}

// skFields holds the additional fields present in U2F/FIDO2 signatures.
// See openssh/PROTOCOL.u2f 'SSH U2F Signatures' for details.
type skFields struct {
// Flags contains U2F/FIDO2 flags such as 'user present'
Flags byte
// Counter is a monotonic signature counter which can be
// used to detect concurrent use of a private key, should
// it be extracted from hardware.
Counter uint32
}

type skECDSAPublicKey struct {
// application is a URL-like string, typically "ssh:" for SSH.
// see openssh/PROTOCOL.u2f for details.
application string
ecdsa.PublicKey
}

func (k *skECDSAPublicKey) Type() string {
return KeyAlgoSKECDSA256
}

func (k *skECDSAPublicKey) nistID() string {
return "nistp256"
}

func parseSKECDSA(in []byte) (out PublicKey, rest []byte, err error) {
var w struct {
Curve string
KeyBytes []byte
Application string
Rest []byte `ssh:"rest"`
}

if err := Unmarshal(in, &w); err != nil {
return nil, nil, err
}

key := new(skECDSAPublicKey)
key.application = w.Application

if w.Curve != "nistp256" {
return nil, nil, errors.New("ssh: unsupported curve")
}
key.Curve = elliptic.P256()

key.X, key.Y = elliptic.Unmarshal(key.Curve, w.KeyBytes)
if key.X == nil || key.Y == nil {
return nil, nil, errors.New("ssh: invalid curve point")
}

return key, w.Rest, nil
}

func (k *skECDSAPublicKey) Marshal() []byte {
// See RFC 5656, section 3.1.
keyBytes := elliptic.Marshal(k.Curve, k.X, k.Y)
w := struct {
Name string
ID string
Key []byte
Application string
}{
k.Type(),
k.nistID(),
keyBytes,
k.application,
}

return Marshal(&w)
}

func (k *skECDSAPublicKey) Verify(data []byte, sig *Signature) error {
if sig.Format != k.Type() {
return fmt.Errorf("ssh: signature type %s for key type %s", sig.Format, k.Type())
}

h := ecHash(k.Curve).New()
h.Write([]byte(k.application))
appDigest := h.Sum(nil)

h.Reset()
h.Write(data)
dataDigest := h.Sum(nil)

var ecSig struct {
R *big.Int
S *big.Int
}
if err := Unmarshal(sig.Blob, &ecSig); err != nil {
return err
}

var skf skFields
if err := Unmarshal(sig.Rest, &skf); err != nil {
return err
}

blob := struct {
ApplicationDigest []byte `ssh:"rest"`
Flags byte
Counter uint32
MessageDigest []byte `ssh:"rest"`
}{
appDigest,
skf.Flags,
skf.Counter,
dataDigest,
}

original := Marshal(blob)

h.Reset()
h.Write(original)
digest := h.Sum(nil)

if ecdsa.Verify((*ecdsa.PublicKey)(&k.PublicKey), digest, ecSig.R, ecSig.S) {
return nil
}
return errors.New("ssh: signature did not verify")
}

type skEd25519PublicKey struct {
// application is a URL-like string, typically "ssh:" for SSH.
// see openssh/PROTOCOL.u2f for details.
application string
ed25519.PublicKey
}

func (k *skEd25519PublicKey) Type() string {
return KeyAlgoSKED25519
}

func parseSKEd25519(in []byte) (out PublicKey, rest []byte, err error) {
var w struct {
KeyBytes []byte
Application string
Rest []byte `ssh:"rest"`
}

if err := Unmarshal(in, &w); err != nil {
return nil, nil, err
}

key := new(skEd25519PublicKey)
key.application = w.Application
key.PublicKey = ed25519.PublicKey(w.KeyBytes)

return key, w.Rest, nil
}

func (k *skEd25519PublicKey) Marshal() []byte {
w := struct {
Name string
KeyBytes []byte
Application string
}{
KeyAlgoSKED25519,
[]byte(k.PublicKey),
k.application,
}
return Marshal(&w)
}

func (k *skEd25519PublicKey) Verify(data []byte, sig *Signature) error {
if sig.Format != k.Type() {
return fmt.Errorf("ssh: signature type %s for key type %s", sig.Format, k.Type())
}

h := sha256.New()
h.Write([]byte(k.application))
appDigest := h.Sum(nil)

h.Reset()
h.Write(data)
dataDigest := h.Sum(nil)

var edSig struct {
Signature []byte `ssh:"rest"`
}

if err := Unmarshal(sig.Blob, &edSig); err != nil {
return err
}

var skf skFields
if err := Unmarshal(sig.Rest, &skf); err != nil {
return err
}

blob := struct {
ApplicationDigest []byte `ssh:"rest"`
Flags byte
Counter uint32
MessageDigest []byte `ssh:"rest"`
}{
appDigest,
skf.Flags,
skf.Counter,
dataDigest,
}

original := Marshal(blob)

edKey := (ed25519.PublicKey)(k.PublicKey)
if ok := ed25519.Verify(edKey, original, edSig.Signature); !ok {
return errors.New("ssh: signature did not verify")
}

return nil
}

// NewSignerFromKey takes an *rsa.PrivateKey, *dsa.PrivateKey,
// *ecdsa.PrivateKey or any other crypto.Signer and returns a
// corresponding Signer instance. ECDSA keys must use P-256, P-384 or
Expand Down
43 changes: 43 additions & 0 deletions ssh/keys_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import (
"crypto/rsa"
"crypto/x509"
"encoding/base64"
"encoding/hex"
"encoding/pem"
"fmt"
"io"
Expand Down Expand Up @@ -572,3 +573,45 @@ func TestInvalidKeys(t *testing.T) {
}
}
}

func TestSKKeys(t *testing.T) {
for _, d := range testdata.SKData {
pk, _, _, _, err := ParseAuthorizedKey(d.PubKey)
if err != nil {
t.Fatalf("parseAuthorizedKey returned error: %v", err)
}

sigBuf := make([]byte, hex.DecodedLen(len(d.HexSignature)))
if _, err := hex.Decode(sigBuf, d.HexSignature); err != nil {
t.Fatalf("hex.Decode() failed: %v", err)
}

dataBuf := make([]byte, hex.DecodedLen(len(d.HexData)))
if _, err := hex.Decode(dataBuf, d.HexData); err != nil {
t.Fatalf("hex.Decode() failed: %v", err)
}

sig, _, ok := parseSignature(sigBuf)
if !ok {
t.Fatalf("parseSignature(%v) failed", sigBuf)
}

// Test that good data and signature pass verification
if err := pk.Verify(dataBuf, sig); err != nil {
t.Errorf("%s: PublicKey.Verify(%v, %v) failed: %v", d.Name, dataBuf, sig, err)
}

// Invalid data being passed in
invalidData := []byte("INVALID DATA")
if err := pk.Verify(invalidData, sig); err == nil {
t.Errorf("%s with invalid data: PublicKey.Verify(%v, %v) passed unexpectedly", d.Name, invalidData, sig)
}

// Change byte in blob to corrup signature
sig.Blob[5] = byte('A')
// Corrupted data being passed in
if err := pk.Verify(dataBuf, sig); err == nil {
t.Errorf("%s with corrupted signature: PublicKey.Verify(%v, %v) passed unexpectedly", d.Name, dataBuf, sig)
}
}
}
4 changes: 2 additions & 2 deletions ssh/server.go
Original file line number Diff line number Diff line change
Expand Up @@ -284,8 +284,8 @@ func (s *connection) serverHandshake(config *ServerConfig) (*Permissions, error)

func isAcceptableAlgo(algo string) bool {
switch algo {
case KeyAlgoRSA, KeyAlgoDSA, KeyAlgoECDSA256, KeyAlgoECDSA384, KeyAlgoECDSA521, KeyAlgoED25519,
CertAlgoRSAv01, CertAlgoDSAv01, CertAlgoECDSA256v01, CertAlgoECDSA384v01, CertAlgoECDSA521v01, CertAlgoED25519v01:
case KeyAlgoRSA, KeyAlgoDSA, KeyAlgoECDSA256, KeyAlgoECDSA384, KeyAlgoECDSA521, KeyAlgoSKECDSA256, KeyAlgoED25519, KeyAlgoSKED25519,
CertAlgoRSAv01, CertAlgoDSAv01, CertAlgoECDSA256v01, CertAlgoECDSA384v01, CertAlgoECDSA521v01, CertAlgoSKECDSA256v01, CertAlgoED25519v01, CertAlgoSKED25519v01:
return true
}
return false
Expand Down
Loading

0 comments on commit 86a7050

Please sign in to comment.