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.
This commit is contained in:
Filippo Valsorda
2020-05-18 01:18:42 -04:00
parent b32ea4c1f6
commit 292c3aaeea
5 changed files with 177 additions and 133 deletions

View File

@@ -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)
}

View File

@@ -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 {

View File

@@ -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)
}
}

View File

@@ -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)
}

View File

@@ -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)
}
}