From 292c3aaeea0695dbba356dfe18a70f10efb17d75 Mon Sep 17 00:00:00 2001 From: Filippo Valsorda Date: Mon, 18 May 2020 01:18:42 -0400 Subject: [PATCH] internal/agessh: new package Move the SSH recipient types out of the main package to declutter the godoc. This also allows us to drop the x/crypto/ssh build dependency entirely from the age package import tree. --- cmd/age/encrypted_keys.go | 5 +- cmd/age/parse.go | 5 +- internal/age/recipients_test.go | 93 ------------------- internal/{age/ssh.go => agessh/agessh.go} | 99 ++++++++++++-------- internal/agessh/agessh_test.go | 108 ++++++++++++++++++++++ 5 files changed, 177 insertions(+), 133 deletions(-) rename internal/{age/ssh.go => agessh/agessh.go} (74%) create mode 100644 internal/agessh/agessh_test.go diff --git a/cmd/age/encrypted_keys.go b/cmd/age/encrypted_keys.go index d58db53..7867045 100644 --- a/cmd/age/encrypted_keys.go +++ b/cmd/age/encrypted_keys.go @@ -14,6 +14,7 @@ import ( "os" "filippo.io/age/internal/age" + "filippo.io/age/internal/agessh" "filippo.io/age/internal/format" "golang.org/x/crypto/ssh" "golang.org/x/crypto/ssh/terminal" @@ -62,9 +63,9 @@ func (i *EncryptedSSHIdentity) Unwrap(block *format.Recipient) (fileKey []byte, switch k := k.(type) { case *ed25519.PrivateKey: - i.decrypted, err = age.NewSSHEd25519Identity(*k) + i.decrypted, err = agessh.NewEd25519Identity(*k) case *rsa.PrivateKey: - i.decrypted, err = age.NewSSHRSAIdentity(k) + i.decrypted, err = agessh.NewRSAIdentity(k) default: return nil, fmt.Errorf("unexpected SSH key type: %T", k) } diff --git a/cmd/age/parse.go b/cmd/age/parse.go index 22ed5b2..6dfeedc 100644 --- a/cmd/age/parse.go +++ b/cmd/age/parse.go @@ -16,6 +16,7 @@ import ( "strings" "filippo.io/age/internal/age" + "filippo.io/age/internal/agessh" "golang.org/x/crypto/ssh" ) @@ -24,7 +25,7 @@ func parseRecipient(arg string) (age.Recipient, error) { case strings.HasPrefix(arg, "age1"): return age.ParseX25519Recipient(arg) case strings.HasPrefix(arg, "ssh-"): - return age.ParseSSHRecipient(arg) + return agessh.ParseRecipient(arg) } return nil, fmt.Errorf("unknown recipient type: %q", arg) @@ -82,7 +83,7 @@ func parseIdentitiesFile(name string) ([]age.Identity, error) { } func parseSSHIdentity(name string, pemBytes []byte) ([]age.Identity, error) { - id, err := age.ParseSSHIdentity(pemBytes) + id, err := agessh.ParseIdentity(pemBytes) if sshErr, ok := err.(*ssh.PassphraseMissingError); ok { pubKey := sshErr.PublicKey if pubKey == nil { diff --git a/internal/age/recipients_test.go b/internal/age/recipients_test.go index 6d58cc3..799b418 100644 --- a/internal/age/recipients_test.go +++ b/internal/age/recipients_test.go @@ -8,14 +8,11 @@ package age_test import ( "bytes" - "crypto/ed25519" "crypto/rand" - "crypto/rsa" "testing" "filippo.io/age/internal/age" "golang.org/x/crypto/curve25519" - "golang.org/x/crypto/ssh" ) func TestX25519RoundTrip(t *testing.T) { @@ -109,93 +106,3 @@ func TestScryptRoundTrip(t *testing.T) { t.Errorf("invalid output: %x, expected %x", out, fileKey) } } - -func TestSSHRSARoundTrip(t *testing.T) { - pk, err := rsa.GenerateKey(rand.Reader, 768) - if err != nil { - t.Fatal(err) - } - pub, err := ssh.NewPublicKey(&pk.PublicKey) - if err != nil { - t.Fatal(err) - } - - r, err := age.NewSSHRSARecipient(pub) - if err != nil { - t.Fatal(err) - } - i, err := age.NewSSHRSAIdentity(pk) - if err != nil { - t.Fatal(err) - } - - if r.Type() != i.Type() || r.Type() != "ssh-rsa" { - t.Errorf("invalid Type values: %v, %v", r.Type(), i.Type()) - } - - fileKey := make([]byte, 16) - if _, err := rand.Read(fileKey); err != nil { - t.Fatal(err) - } - block, err := r.Wrap(fileKey) - if err != nil { - t.Fatal(err) - } - b := &bytes.Buffer{} - block.Marshal(b) - t.Logf("%s", b.Bytes()) - - out, err := i.Unwrap(block) - if err != nil { - t.Fatal(err) - } - - if !bytes.Equal(fileKey, out) { - t.Errorf("invalid output: %x, expected %x", out, fileKey) - } -} - -func TestSSHEd25519RoundTrip(t *testing.T) { - pub, priv, err := ed25519.GenerateKey(rand.Reader) - if err != nil { - t.Fatal(err) - } - sshPubKey, err := ssh.NewPublicKey(pub) - if err != nil { - t.Fatal(err) - } - - r, err := age.NewSSHEd25519Recipient(sshPubKey) - if err != nil { - t.Fatal(err) - } - i, err := age.NewSSHEd25519Identity(priv) - if err != nil { - t.Fatal(err) - } - - if r.Type() != i.Type() || r.Type() != "ssh-ed25519" { - t.Errorf("invalid Type values: %v, %v", r.Type(), i.Type()) - } - - fileKey := make([]byte, 16) - if _, err := rand.Read(fileKey); err != nil { - t.Fatal(err) - } - block, err := r.Wrap(fileKey) - if err != nil { - t.Fatal(err) - } - b := &bytes.Buffer{} - block.Marshal(b) - t.Logf("%s", b.Bytes()) - - out, err := i.Unwrap(block) - if err != nil { - t.Fatal(err) - } - - if !bytes.Equal(fileKey, out) { - t.Errorf("invalid output: %x, expected %x", out, fileKey) - } -} diff --git a/internal/age/ssh.go b/internal/agessh/agessh.go similarity index 74% rename from internal/age/ssh.go rename to internal/agessh/agessh.go index 47a1e23..e3a51c9 100644 --- a/internal/age/ssh.go +++ b/internal/agessh/agessh.go @@ -4,7 +4,13 @@ // license that can be found in the LICENSE file or at // https://developers.google.com/open-source/licenses/bsd -package age +// Package agessh provides age.Identity and age.Recipient implementations of +// types "ssh-rsa" and "ssh-ed25519", which allow reusing existing SSH key files +// for encryption with age-encryption.org/v1. +// +// These should only be used for compatibility with existing keys, and native +// X25519 keys should be preferred otherwise. +package agessh import ( "crypto/ed25519" @@ -17,6 +23,7 @@ import ( "io" "math/big" + "filippo.io/age/internal/age" "filippo.io/age/internal/format" "golang.org/x/crypto/chacha20poly1305" "golang.org/x/crypto/curve25519" @@ -31,20 +38,20 @@ func sshFingerprint(pk ssh.PublicKey) string { const oaepLabel = "age-encryption.org/v1/ssh-rsa" -type SSHRSARecipient struct { +type RSARecipient struct { sshKey ssh.PublicKey pubKey *rsa.PublicKey } -var _ Recipient = &SSHRSARecipient{} +var _ age.Recipient = &RSARecipient{} -func (*SSHRSARecipient) Type() string { return "ssh-rsa" } +func (*RSARecipient) Type() string { return "ssh-rsa" } -func NewSSHRSARecipient(pk ssh.PublicKey) (*SSHRSARecipient, error) { +func NewRSARecipient(pk ssh.PublicKey) (*RSARecipient, error) { if pk.Type() != "ssh-rsa" { return nil, errors.New("SSH public key is not an RSA key") } - r := &SSHRSARecipient{ + r := &RSARecipient{ sshKey: pk, } @@ -60,7 +67,7 @@ func NewSSHRSARecipient(pk ssh.PublicKey) (*SSHRSARecipient, error) { return r, nil } -func (r *SSHRSARecipient) Wrap(fileKey []byte) (*format.Recipient, error) { +func (r *RSARecipient) Wrap(fileKey []byte) (*format.Recipient, error) { l := &format.Recipient{ Type: "ssh-rsa", Args: []string{sshFingerprint(r.sshKey)}, @@ -76,36 +83,36 @@ func (r *SSHRSARecipient) Wrap(fileKey []byte) (*format.Recipient, error) { return l, nil } -type SSHRSAIdentity struct { +type RSAIdentity struct { k *rsa.PrivateKey sshKey ssh.PublicKey } -var _ Identity = &SSHRSAIdentity{} +var _ age.Identity = &RSAIdentity{} -func (*SSHRSAIdentity) Type() string { return "ssh-rsa" } +func (*RSAIdentity) Type() string { return "ssh-rsa" } -func NewSSHRSAIdentity(key *rsa.PrivateKey) (*SSHRSAIdentity, error) { +func NewRSAIdentity(key *rsa.PrivateKey) (*RSAIdentity, error) { s, err := ssh.NewSignerFromKey(key) if err != nil { return nil, err } - i := &SSHRSAIdentity{ + i := &RSAIdentity{ k: key, sshKey: s.PublicKey(), } return i, nil } -func (i *SSHRSAIdentity) Unwrap(block *format.Recipient) ([]byte, error) { +func (i *RSAIdentity) Unwrap(block *format.Recipient) ([]byte, error) { if block.Type != "ssh-rsa" { - return nil, ErrIncorrectIdentity + return nil, age.ErrIncorrectIdentity } if len(block.Args) != 1 { return nil, errors.New("invalid ssh-rsa recipient block") } if block.Args[0] != sshFingerprint(i.sshKey) { - return nil, ErrIncorrectIdentity + return nil, age.ErrIncorrectIdentity } fileKey, err := rsa.DecryptOAEP(sha256.New(), rand.Reader, i.k, @@ -116,20 +123,20 @@ func (i *SSHRSAIdentity) Unwrap(block *format.Recipient) ([]byte, error) { return fileKey, nil } -type SSHEd25519Recipient struct { +type Ed25519Recipient struct { sshKey ssh.PublicKey theirPublicKey []byte } -var _ Recipient = &SSHEd25519Recipient{} +var _ age.Recipient = &Ed25519Recipient{} -func (*SSHEd25519Recipient) Type() string { return "ssh-ed25519" } +func (*Ed25519Recipient) Type() string { return "ssh-ed25519" } -func NewSSHEd25519Recipient(pk ssh.PublicKey) (*SSHEd25519Recipient, error) { +func NewEd25519Recipient(pk ssh.PublicKey) (*Ed25519Recipient, error) { if pk.Type() != "ssh-ed25519" { return nil, errors.New("SSH public key is not an Ed25519 key") } - r := &SSHEd25519Recipient{ + r := &Ed25519Recipient{ sshKey: pk, } @@ -145,18 +152,18 @@ func NewSSHEd25519Recipient(pk ssh.PublicKey) (*SSHEd25519Recipient, error) { return r, nil } -func ParseSSHRecipient(s string) (Recipient, error) { +func ParseRecipient(s string) (age.Recipient, error) { pubKey, _, _, _, err := ssh.ParseAuthorizedKey([]byte(s)) if err != nil { return nil, fmt.Errorf("malformed SSH recipient: %q: %v", s, err) } - var r Recipient + var r age.Recipient switch t := pubKey.Type(); t { case "ssh-rsa": - r, err = NewSSHRSARecipient(pubKey) + r, err = NewRSARecipient(pubKey) case "ssh-ed25519": - r, err = NewSSHEd25519Recipient(pubKey) + r, err = NewEd25519Recipient(pubKey) default: return nil, fmt.Errorf("unknown SSH recipient type: %q", t) } @@ -200,7 +207,7 @@ func ed25519PublicKeyToCurve25519(pk ed25519.PublicKey) []byte { const ed25519Label = "age-encryption.org/v1/ssh-ed25519" -func (r *SSHEd25519Recipient) Wrap(fileKey []byte) (*format.Recipient, error) { +func (r *Ed25519Recipient) Wrap(fileKey []byte) (*format.Recipient, error) { ephemeral := make([]byte, curve25519.ScalarSize) if _, err := rand.Read(ephemeral); err != nil { return nil, err @@ -246,21 +253,21 @@ func (r *SSHEd25519Recipient) Wrap(fileKey []byte) (*format.Recipient, error) { return l, nil } -type SSHEd25519Identity struct { +type Ed25519Identity struct { secretKey, ourPublicKey []byte sshKey ssh.PublicKey } -var _ Identity = &SSHEd25519Identity{} +var _ age.Identity = &Ed25519Identity{} -func (*SSHEd25519Identity) Type() string { return "ssh-ed25519" } +func (*Ed25519Identity) Type() string { return "ssh-ed25519" } -func NewSSHEd25519Identity(key ed25519.PrivateKey) (*SSHEd25519Identity, error) { +func NewEd25519Identity(key ed25519.PrivateKey) (*Ed25519Identity, error) { s, err := ssh.NewSignerFromKey(key) if err != nil { return nil, err } - i := &SSHEd25519Identity{ + i := &Ed25519Identity{ sshKey: s.PublicKey(), secretKey: ed25519PrivateKeyToCurve25519(key), } @@ -268,7 +275,7 @@ func NewSSHEd25519Identity(key ed25519.PrivateKey) (*SSHEd25519Identity, error) return i, nil } -func ParseSSHIdentity(pemBytes []byte) (Identity, error) { +func ParseIdentity(pemBytes []byte) (age.Identity, error) { k, err := ssh.ParseRawPrivateKey(pemBytes) if err != nil { return nil, err @@ -276,9 +283,9 @@ func ParseSSHIdentity(pemBytes []byte) (Identity, error) { switch k := k.(type) { case *ed25519.PrivateKey: - return NewSSHEd25519Identity(*k) + return NewEd25519Identity(*k) case *rsa.PrivateKey: - return NewSSHRSAIdentity(k) + return NewRSAIdentity(k) } return nil, fmt.Errorf("unsupported SSH identity type: %T", k) @@ -291,9 +298,9 @@ func ed25519PrivateKeyToCurve25519(pk ed25519.PrivateKey) []byte { return out[:curve25519.ScalarSize] } -func (i *SSHEd25519Identity) Unwrap(block *format.Recipient) ([]byte, error) { +func (i *Ed25519Identity) Unwrap(block *format.Recipient) ([]byte, error) { if block.Type != "ssh-ed25519" { - return nil, ErrIncorrectIdentity + return nil, age.ErrIncorrectIdentity } if len(block.Args) != 2 { return nil, errors.New("invalid ssh-ed25519 recipient block") @@ -307,7 +314,7 @@ func (i *SSHEd25519Identity) Unwrap(block *format.Recipient) ([]byte, error) { } if block.Args[0] != sshFingerprint(i.sshKey) { - return nil, ErrIncorrectIdentity + return nil, age.ErrIncorrectIdentity } sharedSecret, err := curve25519.X25519(i.secretKey, publicKey) @@ -337,3 +344,23 @@ func (i *SSHEd25519Identity) Unwrap(block *format.Recipient) ([]byte, error) { } return fileKey, nil } + +// aeadEncrypt and aeadDecrypt are copied from package age. + +func aeadEncrypt(key, plaintext []byte) ([]byte, error) { + aead, err := chacha20poly1305.New(key) + if err != nil { + return nil, err + } + nonce := make([]byte, chacha20poly1305.NonceSize) + return aead.Seal(nil, nonce, plaintext, nil), nil +} + +func aeadDecrypt(key, ciphertext []byte) ([]byte, error) { + aead, err := chacha20poly1305.New(key) + if err != nil { + return nil, err + } + nonce := make([]byte, chacha20poly1305.NonceSize) + return aead.Open(nil, nonce, ciphertext, nil) +} diff --git a/internal/agessh/agessh_test.go b/internal/agessh/agessh_test.go new file mode 100644 index 0000000..cdb5f5c --- /dev/null +++ b/internal/agessh/agessh_test.go @@ -0,0 +1,108 @@ +// Copyright 2019 Google LLC +// +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file or at +// https://developers.google.com/open-source/licenses/bsd + +package agessh_test + +import ( + "bytes" + "crypto/ed25519" + "crypto/rand" + "crypto/rsa" + "testing" + + "filippo.io/age/internal/agessh" + "golang.org/x/crypto/ssh" +) + +func TestSSHRSARoundTrip(t *testing.T) { + pk, err := rsa.GenerateKey(rand.Reader, 768) + if err != nil { + t.Fatal(err) + } + pub, err := ssh.NewPublicKey(&pk.PublicKey) + if err != nil { + t.Fatal(err) + } + + r, err := agessh.NewRSARecipient(pub) + if err != nil { + t.Fatal(err) + } + i, err := agessh.NewRSAIdentity(pk) + if err != nil { + t.Fatal(err) + } + + if r.Type() != i.Type() || r.Type() != "ssh-rsa" { + t.Errorf("invalid Type values: %v, %v", r.Type(), i.Type()) + } + + fileKey := make([]byte, 16) + if _, err := rand.Read(fileKey); err != nil { + t.Fatal(err) + } + block, err := r.Wrap(fileKey) + if err != nil { + t.Fatal(err) + } + b := &bytes.Buffer{} + block.Marshal(b) + t.Logf("%s", b.Bytes()) + + out, err := i.Unwrap(block) + if err != nil { + t.Fatal(err) + } + + if !bytes.Equal(fileKey, out) { + t.Errorf("invalid output: %x, expected %x", out, fileKey) + } +} + +func TestSSHEd25519RoundTrip(t *testing.T) { + pub, priv, err := ed25519.GenerateKey(rand.Reader) + if err != nil { + t.Fatal(err) + } + sshPubKey, err := ssh.NewPublicKey(pub) + if err != nil { + t.Fatal(err) + } + + r, err := agessh.NewEd25519Recipient(sshPubKey) + if err != nil { + t.Fatal(err) + } + i, err := agessh.NewEd25519Identity(priv) + if err != nil { + t.Fatal(err) + } + + if r.Type() != i.Type() || r.Type() != "ssh-ed25519" { + t.Errorf("invalid Type values: %v, %v", r.Type(), i.Type()) + } + + fileKey := make([]byte, 16) + if _, err := rand.Read(fileKey); err != nil { + t.Fatal(err) + } + block, err := r.Wrap(fileKey) + if err != nil { + t.Fatal(err) + } + b := &bytes.Buffer{} + block.Marshal(b) + t.Logf("%s", b.Bytes()) + + out, err := i.Unwrap(block) + if err != nil { + t.Fatal(err) + } + + if !bytes.Equal(fileKey, out) { + t.Errorf("invalid output: %x, expected %x", out, fileKey) + } +}