mirror of
https://github.com/FiloSottile/age.git
synced 2026-01-03 10:55:14 +00:00
age: make Identity and Recipient work on multiple stanzas
This is a breaking change, but like the other changes to these interfaces it should not matter to consumers of the API that don't implement custom Recipients or Identities, which is all of them so far, as far as I can tell. It became clear working on plugins that we might want Recipient to return multiple recipient stanzas, for example if the plugin recipient is an alias or a group. The Identity side is less important, but it might help avoid round-trips and it makes sense to keep things symmetric.
This commit is contained in:
@@ -68,7 +68,7 @@ func NewRSARecipient(pk ssh.PublicKey) (*RSARecipient, error) {
|
||||
return r, nil
|
||||
}
|
||||
|
||||
func (r *RSARecipient) Wrap(fileKey []byte) (*age.Stanza, error) {
|
||||
func (r *RSARecipient) Wrap(fileKey []byte) ([]*age.Stanza, error) {
|
||||
l := &age.Stanza{
|
||||
Type: "ssh-rsa",
|
||||
Args: []string{sshFingerprint(r.sshKey)},
|
||||
@@ -81,7 +81,7 @@ func (r *RSARecipient) Wrap(fileKey []byte) (*age.Stanza, error) {
|
||||
}
|
||||
l.Body = wrappedKey
|
||||
|
||||
return l, nil
|
||||
return []*age.Stanza{l}, nil
|
||||
}
|
||||
|
||||
type RSAIdentity struct {
|
||||
@@ -102,7 +102,11 @@ func NewRSAIdentity(key *rsa.PrivateKey) (*RSAIdentity, error) {
|
||||
return i, nil
|
||||
}
|
||||
|
||||
func (i *RSAIdentity) Unwrap(block *age.Stanza) ([]byte, error) {
|
||||
func (i *RSAIdentity) Unwrap(stanzas []*age.Stanza) ([]byte, error) {
|
||||
return multiUnwrap(i.unwrap, stanzas)
|
||||
}
|
||||
|
||||
func (i *RSAIdentity) unwrap(block *age.Stanza) ([]byte, error) {
|
||||
if block.Type != "ssh-rsa" {
|
||||
return nil, age.ErrIncorrectIdentity
|
||||
}
|
||||
@@ -187,7 +191,7 @@ func ed25519PublicKeyToCurve25519(pk ed25519.PublicKey) ([]byte, error) {
|
||||
|
||||
const ed25519Label = "age-encryption.org/v1/ssh-ed25519"
|
||||
|
||||
func (r *Ed25519Recipient) Wrap(fileKey []byte) (*age.Stanza, error) {
|
||||
func (r *Ed25519Recipient) Wrap(fileKey []byte) ([]*age.Stanza, error) {
|
||||
ephemeral := make([]byte, curve25519.ScalarSize)
|
||||
if _, err := rand.Read(ephemeral); err != nil {
|
||||
return nil, err
|
||||
@@ -230,7 +234,7 @@ func (r *Ed25519Recipient) Wrap(fileKey []byte) (*age.Stanza, error) {
|
||||
}
|
||||
l.Body = wrappedKey
|
||||
|
||||
return l, nil
|
||||
return []*age.Stanza{l}, nil
|
||||
}
|
||||
|
||||
type Ed25519Identity struct {
|
||||
@@ -276,7 +280,11 @@ func ed25519PrivateKeyToCurve25519(pk ed25519.PrivateKey) []byte {
|
||||
return out[:curve25519.ScalarSize]
|
||||
}
|
||||
|
||||
func (i *Ed25519Identity) Unwrap(block *age.Stanza) ([]byte, error) {
|
||||
func (i *Ed25519Identity) Unwrap(stanzas []*age.Stanza) ([]byte, error) {
|
||||
return multiUnwrap(i.unwrap, stanzas)
|
||||
}
|
||||
|
||||
func (i *Ed25519Identity) unwrap(block *age.Stanza) ([]byte, error) {
|
||||
if block.Type != "ssh-ed25519" {
|
||||
return nil, age.ErrIncorrectIdentity
|
||||
}
|
||||
@@ -323,6 +331,26 @@ func (i *Ed25519Identity) Unwrap(block *age.Stanza) ([]byte, error) {
|
||||
return fileKey, nil
|
||||
}
|
||||
|
||||
// multiUnwrap is copied from package age. It's a helper that implements
|
||||
// Identity.Unwrap in terms of a function that unwraps a single recipient
|
||||
// stanza.
|
||||
func multiUnwrap(unwrap func(*age.Stanza) ([]byte, error), stanzas []*age.Stanza) ([]byte, error) {
|
||||
for _, s := range stanzas {
|
||||
fileKey, err := unwrap(s)
|
||||
if errors.Is(err, age.ErrIncorrectIdentity) {
|
||||
// If we ever start returning something interesting wrapping
|
||||
// ErrIncorrectIdentity, we should let it make its way up through
|
||||
// Decrypt into NoIdentityMatchError.Errors.
|
||||
continue
|
||||
}
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return fileKey, nil
|
||||
}
|
||||
return nil, age.ErrIncorrectIdentity
|
||||
}
|
||||
|
||||
// aeadEncrypt and aeadDecrypt are copied from package age.
|
||||
//
|
||||
// They don't limit the file key size because multi-key attacks are irrelevant
|
||||
|
||||
@@ -14,7 +14,6 @@ import (
|
||||
"testing"
|
||||
|
||||
"filippo.io/age/agessh"
|
||||
"filippo.io/age/internal/format"
|
||||
"golang.org/x/crypto/ssh"
|
||||
)
|
||||
|
||||
@@ -41,15 +40,12 @@ func TestSSHRSARoundTrip(t *testing.T) {
|
||||
if _, err := rand.Read(fileKey); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
block, err := r.Wrap(fileKey)
|
||||
stanzas, err := r.Wrap(fileKey)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
b := &bytes.Buffer{}
|
||||
(*format.Stanza)(block).Marshal(b)
|
||||
t.Logf("%s", b.Bytes())
|
||||
|
||||
out, err := i.Unwrap(block)
|
||||
out, err := i.Unwrap(stanzas)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
@@ -82,15 +78,12 @@ func TestSSHEd25519RoundTrip(t *testing.T) {
|
||||
if _, err := rand.Read(fileKey); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
block, err := r.Wrap(fileKey)
|
||||
stanzas, err := r.Wrap(fileKey)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
b := &bytes.Buffer{}
|
||||
(*format.Stanza)(block).Marshal(b)
|
||||
t.Logf("%s", b.Bytes())
|
||||
|
||||
out, err := i.Unwrap(block)
|
||||
out, err := i.Unwrap(stanzas)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
@@ -56,20 +56,28 @@ func NewEncryptedSSHIdentity(pubKey ssh.PublicKey, pemBytes []byte, passphrase f
|
||||
var _ age.Identity = &EncryptedSSHIdentity{}
|
||||
|
||||
// Unwrap implements age.Identity. If the private key is still encrypted, and
|
||||
// the block matches the public key, it will request the passphrase. The
|
||||
// any of the stanzas match the public key, it will request the passphrase. The
|
||||
// decrypted private key will be cached after the first successful invocation.
|
||||
func (i *EncryptedSSHIdentity) Unwrap(block *age.Stanza) (fileKey []byte, err error) {
|
||||
func (i *EncryptedSSHIdentity) Unwrap(stanzas []*age.Stanza) (fileKey []byte, err error) {
|
||||
if i.decrypted != nil {
|
||||
return i.decrypted.Unwrap(block)
|
||||
return i.decrypted.Unwrap(stanzas)
|
||||
}
|
||||
|
||||
if block.Type != i.pubKey.Type() {
|
||||
return nil, age.ErrIncorrectIdentity
|
||||
var match bool
|
||||
for _, s := range stanzas {
|
||||
if s.Type != i.pubKey.Type() {
|
||||
continue
|
||||
}
|
||||
if len(s.Args) < 1 {
|
||||
return nil, fmt.Errorf("invalid %v recipient block", i.pubKey.Type())
|
||||
}
|
||||
if s.Args[0] != sshFingerprint(i.pubKey) {
|
||||
continue
|
||||
}
|
||||
match = true
|
||||
break
|
||||
}
|
||||
if len(block.Args) < 1 {
|
||||
return nil, fmt.Errorf("invalid %v recipient block", i.pubKey.Type())
|
||||
}
|
||||
if block.Args[0] != sshFingerprint(i.pubKey) {
|
||||
if !match {
|
||||
return nil, age.ErrIncorrectIdentity
|
||||
}
|
||||
|
||||
@@ -85,6 +93,8 @@ func (i *EncryptedSSHIdentity) Unwrap(block *age.Stanza) (fileKey []byte, err er
|
||||
switch k := k.(type) {
|
||||
case *ed25519.PrivateKey:
|
||||
i.decrypted, err = NewEd25519Identity(*k)
|
||||
// TODO: here and below, better check that the two public keys match,
|
||||
// rather than just the type.
|
||||
if i.pubKey.Type() != ssh.KeyAlgoED25519 {
|
||||
return nil, fmt.Errorf("mismatched private (%s) and public (%s) SSH key types", ssh.KeyAlgoED25519, i.pubKey.Type())
|
||||
}
|
||||
@@ -100,5 +110,5 @@ func (i *EncryptedSSHIdentity) Unwrap(block *age.Stanza) (fileKey []byte, err er
|
||||
return nil, fmt.Errorf("invalid SSH key: %v", err)
|
||||
}
|
||||
|
||||
return i.decrypted.Unwrap(block)
|
||||
return i.decrypted.Unwrap(stanzas)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user