mirror of
https://github.com/FiloSottile/age.git
synced 2026-01-05 03:43:57 +00:00
internal/age: add ssh-ed25519 recipients
This commit is contained in:
2
go.mod
2
go.mod
@@ -1,5 +1,5 @@
|
||||
module github.com/FiloSottile/age
|
||||
|
||||
go 1.12
|
||||
go 1.13
|
||||
|
||||
require golang.org/x/crypto v0.0.0-20191002192127-34f69633bfdc
|
||||
|
||||
@@ -8,6 +8,7 @@ package age_test
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"crypto/ed25519"
|
||||
"crypto/rand"
|
||||
"crypto/rsa"
|
||||
"testing"
|
||||
@@ -135,3 +136,46 @@ func TestSSHRSARoundTrip(t *testing.T) {
|
||||
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)
|
||||
}
|
||||
t.Logf("%#v", block)
|
||||
|
||||
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[:])
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,13 +8,20 @@ package age
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"crypto/ed25519"
|
||||
"crypto/rand"
|
||||
"crypto/rsa"
|
||||
"crypto/sha256"
|
||||
"crypto/sha512"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"math/big"
|
||||
|
||||
"github.com/FiloSottile/age/internal/format"
|
||||
"golang.org/x/crypto/chacha20poly1305"
|
||||
"golang.org/x/crypto/curve25519"
|
||||
"golang.org/x/crypto/hkdf"
|
||||
"golang.org/x/crypto/ssh"
|
||||
)
|
||||
|
||||
@@ -122,3 +129,199 @@ func (i *SSHRSAIdentity) Unwrap(block *format.Recipient) ([]byte, error) {
|
||||
}
|
||||
return fileKey, nil
|
||||
}
|
||||
|
||||
type SSHEd25519Recipient struct {
|
||||
sshKey ssh.PublicKey
|
||||
theirPublicKey [32]byte
|
||||
}
|
||||
|
||||
var _ Recipient = &SSHEd25519Recipient{}
|
||||
|
||||
func (*SSHEd25519Recipient) Type() string { return "ssh-ed25519" }
|
||||
|
||||
func NewSSHEd25519Recipient(pk ssh.PublicKey) (*SSHEd25519Recipient, error) {
|
||||
if pk.Type() != "ssh-ed25519" {
|
||||
return nil, errors.New("SSH public key is not an Ed25519 key")
|
||||
}
|
||||
r := &SSHEd25519Recipient{
|
||||
sshKey: pk,
|
||||
}
|
||||
|
||||
if pk, ok := pk.(ssh.CryptoPublicKey); ok {
|
||||
if pk, ok := pk.CryptoPublicKey().(ed25519.PublicKey); ok {
|
||||
pubKey := ed25519PublicKeyToCurve25519(pk)
|
||||
copy(r.theirPublicKey[:], pubKey)
|
||||
} else {
|
||||
return nil, errors.New("unexpected public key type")
|
||||
}
|
||||
} else {
|
||||
return nil, errors.New("pk does not implement ssh.CryptoPublicKey")
|
||||
}
|
||||
return r, nil
|
||||
}
|
||||
|
||||
var curve25519P, _ = new(big.Int).SetString("57896044618658097711785492504343953926634992332820282019728792003956564819949", 10)
|
||||
|
||||
func ed25519PublicKeyToCurve25519(pk ed25519.PublicKey) []byte {
|
||||
// ed25519.PublicKey is a little endian representation of the y-coordinate,
|
||||
// with the most significant bit set based on the sign of the x-ccordinate.
|
||||
bigEndianY := make([]byte, ed25519.PublicKeySize)
|
||||
for i, b := range pk {
|
||||
bigEndianY[ed25519.PublicKeySize-i-1] = b
|
||||
}
|
||||
bigEndianY[0] &= 0b0111_1111
|
||||
|
||||
// The Montgomery u-coordinate is derived through the bilinear map
|
||||
//
|
||||
// u = (1 + y) / (1 - y)
|
||||
//
|
||||
// See https://blog.filippo.io/using-ed25519-keys-for-encryption.
|
||||
y := new(big.Int).SetBytes(bigEndianY)
|
||||
denom := big.NewInt(1)
|
||||
denom.ModInverse(denom.Sub(denom, y), curve25519P) // 1 / (1 - y)
|
||||
u := y.Mul(y.Add(y, big.NewInt(1)), denom)
|
||||
u.Mod(u, curve25519P)
|
||||
|
||||
out := make([]byte, 32)
|
||||
uBytes := u.Bytes()
|
||||
for i, b := range uBytes {
|
||||
out[len(uBytes)-i-1] = b
|
||||
}
|
||||
|
||||
return out
|
||||
}
|
||||
|
||||
const ed25519Label = "age-tool.com ssh-ed25519"
|
||||
|
||||
func (r *SSHEd25519Recipient) Wrap(fileKey []byte) (*format.Recipient, error) {
|
||||
// TODO: DRY this up with the X25519 implementation.
|
||||
var ephemeral, ourPublicKey [32]byte
|
||||
if _, err := rand.Read(ephemeral[:]); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
curve25519.ScalarBaseMult(&ourPublicKey, &ephemeral)
|
||||
|
||||
var sharedSecret, tweak [32]byte
|
||||
tH := hkdf.New(sha256.New, nil, r.sshKey.Marshal(), []byte(ed25519Label))
|
||||
if _, err := io.ReadFull(tH, tweak[:]); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
curve25519.ScalarMult(&sharedSecret, &ephemeral, &r.theirPublicKey)
|
||||
curve25519.ScalarMult(&sharedSecret, &tweak, &sharedSecret)
|
||||
|
||||
sH := sha256.New()
|
||||
sH.Write(r.sshKey.Marshal())
|
||||
hh := sH.Sum(nil)
|
||||
|
||||
l := &format.Recipient{
|
||||
Type: "ssh-ed25519",
|
||||
Args: []string{format.EncodeToString(hh[:4]),
|
||||
format.EncodeToString(ourPublicKey[:])},
|
||||
}
|
||||
|
||||
salt := make([]byte, 0, 32*2)
|
||||
salt = append(salt, ourPublicKey[:]...)
|
||||
salt = append(salt, r.theirPublicKey[:]...)
|
||||
h := hkdf.New(sha256.New, sharedSecret[:], salt, []byte(ed25519Label))
|
||||
wrappingKey := make([]byte, chacha20poly1305.KeySize)
|
||||
if _, err := io.ReadFull(h, wrappingKey); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
wrappedKey, err := aeadEncrypt(wrappingKey, fileKey)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
l.Body = []byte(format.EncodeToString(wrappedKey) + "\n")
|
||||
|
||||
return l, nil
|
||||
}
|
||||
|
||||
type SSHEd25519Identity struct {
|
||||
secretKey, ourPublicKey [32]byte
|
||||
sshKey ssh.PublicKey
|
||||
}
|
||||
|
||||
var _ Identity = &SSHEd25519Identity{}
|
||||
|
||||
func (*SSHEd25519Identity) Type() string { return "ssh-ed25519" }
|
||||
|
||||
func NewSSHEd25519Identity(key ed25519.PrivateKey) (*SSHEd25519Identity, error) {
|
||||
s, err := ssh.NewSignerFromKey(key)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
i := &SSHEd25519Identity{
|
||||
sshKey: s.PublicKey(),
|
||||
}
|
||||
secretKey := ed25519PrivateKeyToCurve25519(key)
|
||||
copy(i.secretKey[:], secretKey)
|
||||
curve25519.ScalarBaseMult(&i.ourPublicKey, &i.secretKey)
|
||||
return i, nil
|
||||
}
|
||||
|
||||
func ed25519PrivateKeyToCurve25519(pk ed25519.PrivateKey) []byte {
|
||||
h := sha512.New()
|
||||
h.Write(pk[:32])
|
||||
out := h.Sum(nil)
|
||||
return out[:32]
|
||||
}
|
||||
|
||||
func (i *SSHEd25519Identity) Unwrap(block *format.Recipient) ([]byte, error) {
|
||||
// TODO: DRY this up with the X25519 implementation.
|
||||
if block.Type != "ssh-ed25519" {
|
||||
return nil, errors.New("wrong recipient block type")
|
||||
}
|
||||
if len(block.Args) != 2 {
|
||||
return nil, errors.New("invalid ssh-ed25519 recipient block")
|
||||
}
|
||||
hash, err := format.DecodeString(block.Args[0])
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to parse ssh-ed25519 recipient: %v", err)
|
||||
}
|
||||
if len(hash) != 4 {
|
||||
return nil, errors.New("invalid ssh-ed25519 recipient block")
|
||||
}
|
||||
publicKey, err := format.DecodeString(block.Args[1])
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to parse ssh-ed25519 recipient: %v", err)
|
||||
}
|
||||
if len(publicKey) != 32 {
|
||||
return nil, errors.New("invalid ssh-ed25519 recipient block")
|
||||
}
|
||||
wrappedKey, err := format.DecodeString(string(block.Body))
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to parse ssh-ed25519 recipient: %v", err)
|
||||
}
|
||||
|
||||
sH := sha256.New()
|
||||
sH.Write(i.sshKey.Marshal())
|
||||
hh := sH.Sum(nil)
|
||||
if !bytes.Equal(hh[:4], hash) {
|
||||
return nil, errors.New("wrong ssh-ed25519 key")
|
||||
}
|
||||
|
||||
var sharedSecret, theirPublicKey, tweak [32]byte
|
||||
copy(theirPublicKey[:], publicKey)
|
||||
tH := hkdf.New(sha256.New, nil, i.sshKey.Marshal(), []byte(ed25519Label))
|
||||
if _, err := io.ReadFull(tH, tweak[:]); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
curve25519.ScalarMult(&sharedSecret, &i.secretKey, &theirPublicKey)
|
||||
curve25519.ScalarMult(&sharedSecret, &tweak, &sharedSecret)
|
||||
|
||||
salt := make([]byte, 0, 32*2)
|
||||
salt = append(salt, theirPublicKey[:]...)
|
||||
salt = append(salt, i.ourPublicKey[:]...)
|
||||
h := hkdf.New(sha256.New, sharedSecret[:], salt, []byte(ed25519Label))
|
||||
wrappingKey := make([]byte, chacha20poly1305.KeySize)
|
||||
if _, err := io.ReadFull(h, wrappingKey); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
fileKey, err := aeadDecrypt(wrappingKey, wrappedKey)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to decrypt file key: %v", err)
|
||||
}
|
||||
return fileKey, nil
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user