ssh: add sk-ecdsa-sha2-nistp256 and sk-ed25519
This adds server-side support for the newly introduced OpenSSH
keytypes sk-ecdsa-sha2-nistp256@openssh.com and sk-ed25519@openssh.com
(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 <hanwen@google.com>
Run-TryBot: Han-Wen Nienhuys <hanwen@google.com>
TryBot-Result: Gobot Gobot <gobot@golang.org>
diff --git a/ssh/certs.go b/ssh/certs.go
index 00ed992..0f89aec 100644
--- a/ssh/certs.go
+++ b/ssh/certs.go
@@ -17,12 +17,14 @@
// These constants from [PROTOCOL.certkeys] represent the algorithm names
// for certificate types supported by this package.
const (
- CertAlgoRSAv01 = "ssh-rsa-cert-v01@openssh.com"
- CertAlgoDSAv01 = "ssh-dss-cert-v01@openssh.com"
- CertAlgoECDSA256v01 = "ecdsa-sha2-nistp256-cert-v01@openssh.com"
- CertAlgoECDSA384v01 = "ecdsa-sha2-nistp384-cert-v01@openssh.com"
- CertAlgoECDSA521v01 = "ecdsa-sha2-nistp521-cert-v01@openssh.com"
- CertAlgoED25519v01 = "ssh-ed25519-cert-v01@openssh.com"
+ CertAlgoRSAv01 = "ssh-rsa-cert-v01@openssh.com"
+ CertAlgoDSAv01 = "ssh-dss-cert-v01@openssh.com"
+ CertAlgoECDSA256v01 = "ecdsa-sha2-nistp256-cert-v01@openssh.com"
+ CertAlgoECDSA384v01 = "ecdsa-sha2-nistp384-cert-v01@openssh.com"
+ CertAlgoECDSA521v01 = "ecdsa-sha2-nistp521-cert-v01@openssh.com"
+ CertAlgoSKECDSA256v01 = "sk-ecdsa-sha2-nistp256-cert-v01@openssh.com"
+ CertAlgoED25519v01 = "ssh-ed25519-cert-v01@openssh.com"
+ CertAlgoSKED25519v01 = "sk-ssh-ed25519-cert-v01@openssh.com"
)
// Certificate types distinguish between host and user
@@ -37,6 +39,7 @@
type Signature struct {
Format string
Blob []byte
+ Rest []byte `ssh:"rest"`
}
// CertTimeInfinity can be used for OpenSSHCertV01.ValidBefore to indicate that
@@ -429,12 +432,14 @@
}
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.
@@ -518,6 +523,12 @@
return
}
+ switch out.Format {
+ case KeyAlgoSKECDSA256, CertAlgoSKECDSA256v01, KeyAlgoSKED25519, CertAlgoSKED25519v01:
+ out.Rest = in
+ return out, nil, ok
+ }
+
return out, in, ok
}
diff --git a/ssh/keys.go b/ssh/keys.go
index 9698047..1b536a5 100644
--- a/ssh/keys.go
+++ b/ssh/keys.go
@@ -30,12 +30,14 @@
// 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 = "sk-ecdsa-sha2-nistp256@openssh.com"
+ KeyAlgoECDSA384 = "ecdsa-sha2-nistp384"
+ KeyAlgoECDSA521 = "ecdsa-sha2-nistp521"
+ KeyAlgoED25519 = "ssh-ed25519"
+ KeyAlgoSKED25519 = "sk-ssh-ed25519@openssh.com"
)
// These constants represent non-default signature algorithms that are supported
@@ -58,9 +60,13 @@
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
@@ -685,6 +691,218 @@
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
diff --git a/ssh/keys_test.go b/ssh/keys_test.go
index 3847b3b..23a3566 100644
--- a/ssh/keys_test.go
+++ b/ssh/keys_test.go
@@ -13,6 +13,7 @@
"crypto/rsa"
"crypto/x509"
"encoding/base64"
+ "encoding/hex"
"encoding/pem"
"fmt"
"io"
@@ -572,3 +573,45 @@
}
}
}
+
+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)
+ }
+ }
+}
diff --git a/ssh/server.go b/ssh/server.go
index 7a5a1d7..7d42a8c 100644
--- a/ssh/server.go
+++ b/ssh/server.go
@@ -284,8 +284,8 @@
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
diff --git a/ssh/testdata/keys.go b/ssh/testdata/keys.go
index bfae85f..90181bc 100644
--- a/ssh/testdata/keys.go
+++ b/ssh/testdata/keys.go
@@ -225,3 +225,24 @@
`),
},
}
+
+// SKData contains a list of PubKeys backed by U2F/FIDO2 Security Keys and their test data.
+var SKData = []struct {
+ Name string
+ PubKey []byte
+ HexData []byte
+ HexSignature []byte
+}{
+ {
+ Name: "sk-ecdsa-sha2-nistp256@openssh.com",
+ PubKey: []byte("sk-ecdsa-sha2-nistp256@openssh.com AAAAInNrLWVjZHNhLXNoYTItbmlzdHAyNTZAb3BlbnNzaC5jb20AAAAIbmlzdHAyNTYAAABBBGRNqlFgED/pf4zXz8IzqA6CALNwYcwgd4MQDmIS1GOtn1SySFObiuyJaOlpqkV5FeEifhxfIC2ejKKtNyO4CysAAAAEc3NoOg== user@host"),
+ HexData: []byte("00000020A4DE1F50DE0EF3F66DCD156C78F5C93B07EEE89D5B5A6531656E835FA1C87B323200000006736B696E6E650000000E7373682D636F6E6E656374696F6E000000097075626C69636B65790100000022736B2D65636473612D736861322D6E69737470323536406F70656E7373682E636F6D0000007F00000022736B2D65636473612D736861322D6E69737470323536406F70656E7373682E636F6D000000086E697374703235360000004104644DAA5160103FE97F8CD7CFC233A80E8200B37061CC207783100E6212D463AD9F54B248539B8AEC8968E969AA457915E1227E1C5F202D9E8CA2AD3723B80B2B000000047373683A"),
+ HexSignature: []byte("0000007800000022736B2D65636473612D736861322D6E69737470323536406F70656E7373682E636F6D000000490000002016CC1A3070E180621CB206C2C6313D1CC5F094DB844A61D06001E243C608875F0000002100E4BD45D6B9DAA11489AEA8D76C222AA3FD6D50FBFFDA8049526D5D61F63B2C5601000000F9"),
+ },
+ {
+ Name: "sk-ssh-ed25519@openssh.com",
+ PubKey: []byte("sk-ssh-ed25519@openssh.com AAAAGnNrLXNzaC1lZDI1NTE5QG9wZW5zc2guY29tAAAAIJjzc2a20RjCvN/0ibH6UpGuN9F9hDvD7x182bOesNhHAAAABHNzaDo= user@host"),
+ HexData: []byte("000000204CFE6EA65CCB99B69348339165C7F38E359D95807A377EEE8E603C71DC3316FA3200000006736B696E6E650000000E7373682D636F6E6E656374696F6E000000097075626C69636B6579010000001A736B2D7373682D65643235353139406F70656E7373682E636F6D0000004A0000001A736B2D7373682D65643235353139406F70656E7373682E636F6D0000002098F37366B6D118C2BCDFF489B1FA5291AE37D17D843BC3EF1D7CD9B39EB0D847000000047373683A"),
+ HexSignature: []byte("000000670000001A736B2D7373682D65643235353139406F70656E7373682E636F6D000000404BF5CA0CAA553099306518732317B3FE4BA6C75365BC0CB02019FBE65A1647016CBD7A682C26928DF234C378ADDBC5077B47F72381144840BF00FB2DA2FB6A0A010000009E"),
+ },
+}