From b5bb02e7ba7d52589fe271e14129a3178eb1de7f Mon Sep 17 00:00:00 2001 From: Mike Beaumont Date: Fri, 31 Jul 2026 23:09:52 +0200 Subject: [PATCH 1/2] test: add cases Signed-off-by: Mike Beaumont --- age/keysource_test.go | 26 ++++++++++++++++++++++++++ 1 file changed, 26 insertions(+) diff --git a/age/keysource_test.go b/age/keysource_test.go index da315d1a40..dd3313f9f1 100644 --- a/age/keysource_test.go +++ b/age/keysource_test.go @@ -14,6 +14,8 @@ import ( const ( // mockRecipient is a mock age recipient, it matches mockIdentity. mockRecipient string = "age1lzd99uklcjnc0e7d860axevet2cz99ce9pq6tzuzd05l5nr28ams36nvun" + // mockTagRecipient is a mock age tagged recipient. + mockTagRecipient string = "age1tag1qv0h7q4p5zr5g0cy8rduxr8twtjgu2td4ufsyjgmrgm4zv655fk2v24507v" // mockIdentity is a mock age identity. mockIdentity string = "AGE-SECRET-KEY-1G0Q5K9TV4REQ3ZSQRMTMG8NSWQGYT0T7TZ33RAZEE0GZYVZN0APSU24RK7" // mockHybridIdentity is a mock post-quantum age identity using a hybrid ML-KEM-768 KEM. @@ -577,6 +579,30 @@ func TestMasterKey_loadIdentities(t *testing.T) { }) } +func TestMasterKeyFromRecipient_TaggedRecipient(t *testing.T) { + t.Run("encrypts without an age-plugin-tag binary", func(t *testing.T) { + // Empty PATH: a regression to the plugin path would try to exec + // age-plugin-tag here and fail. + t.Setenv("PATH", t.TempDir()) + + key, err := MasterKeyFromRecipient(mockTagRecipient) + assert.NoError(t, err) + + assert.NoError(t, key.Encrypt([]byte("data"))) + assert.NotEmpty(t, key.EncryptedKey) + }) + + t.Run("routes malformed age1tag to the tag parser, not the plugin", func(t *testing.T) { + _, err := MasterKeyFromRecipient("age1tag1qqqq") + assert.ErrorContains(t, err, "tagged") + }) + + t.Run("routes malformed age1tagpq to the tag parser, not the plugin", func(t *testing.T) { + _, err := MasterKeyFromRecipient("age1tagpq1qqqq") + assert.ErrorContains(t, err, "tagged") + }) +} + // overwriteUserConfigDir sets the user config directory and the user home directory // based on the os.UserConfigDir logic. func overwriteUserConfigDir(t *testing.T, path string) { From da8505407cf1700dd1c8514398a1cccae90c232c Mon Sep 17 00:00:00 2001 From: Mike Beaumont Date: Fri, 31 Jul 2026 22:55:45 +0200 Subject: [PATCH 2/2] feat: support tagged age recipients Signed-off-by: Mike Beaumont --- age/keysource.go | 8 ++++++++ go.mod | 1 + go.sum | 2 ++ 3 files changed, 11 insertions(+) diff --git a/age/keysource.go b/age/keysource.go index 4eb9a200ed..3fa53d858c 100644 --- a/age/keysource.go +++ b/age/keysource.go @@ -16,6 +16,7 @@ import ( "filippo.io/age/agessh" "filippo.io/age/armor" "filippo.io/age/plugin" + "filippo.io/age/tag" "github.com/sirupsen/logrus" "golang.org/x/crypto/ssh" @@ -488,6 +489,13 @@ func (key *MasterKey) loadIdentities() (ParsedIdentities, []string, errSet) { // key or a public ssh key. func parseRecipient(recipient string) (age.Recipient, error) { switch { + case strings.HasPrefix(recipient, "age1tag1") || strings.HasPrefix(recipient, "age1tagpq1"): + parsedRecipient, err := tag.ParseRecipient(recipient) + if err != nil { + return nil, fmt.Errorf("failed to parse input as Bech32-encoded age tagged public key: %w", err) + } + + return parsedRecipient, nil case strings.HasPrefix(recipient, "age1pq1"): parsedRecipient, err := age.ParseHybridRecipient(recipient) if err != nil { diff --git a/go.mod b/go.mod index 4862659165..4ec34a5894 100644 --- a/go.mod +++ b/go.mod @@ -60,6 +60,7 @@ require ( dario.cat/mergo v1.0.2 // indirect filippo.io/edwards25519 v1.2.0 // indirect filippo.io/hpke v0.4.0 // indirect + filippo.io/nistec v0.0.4 // indirect github.com/Azure/azure-sdk-for-go/sdk/internal v1.12.0 // indirect github.com/Azure/azure-sdk-for-go/sdk/security/keyvault/internal v1.2.0 // indirect github.com/Azure/go-ansiterm v0.0.0-20250102033503-faa5f7b0171c // indirect diff --git a/go.sum b/go.sum index 7912efdf2d..0b23fb3ecd 100644 --- a/go.sum +++ b/go.sum @@ -33,6 +33,8 @@ filippo.io/edwards25519 v1.2.0 h1:crnVqOiS4jqYleHd9vaKZ+HKtHfllngJIiOpNpoJsjo= filippo.io/edwards25519 v1.2.0/go.mod h1:xzAOLCNug/yB62zG1bQ8uziwrIqIuxhctzJT18Q77mc= filippo.io/hpke v0.4.0 h1:p575VVQ6ted4pL+it6M00V/f2qTZITO0zgmdKCkd5+A= filippo.io/hpke v0.4.0/go.mod h1:EmAN849/P3qdeK+PCMkDpDm83vRHM5cDipBJ8xbQLVY= +filippo.io/nistec v0.0.4 h1:F14ZHT5htWlMnQVPndX9ro9arf56cBhQxq4LnDI491s= +filippo.io/nistec v0.0.4/go.mod h1:PK/lw8I1gQT4hUML4QGaqljwdDaFcMyFKSXN7kjrtKI= github.com/Azure/azure-sdk-for-go/sdk/azcore v1.22.0 h1:aokoqcHvaGjiM3VpjKDfMMnF/8epJ+Q1HLJ7CudztqE= github.com/Azure/azure-sdk-for-go/sdk/azcore v1.22.0/go.mod h1:/WYEx9pcM9Y+Dd/APJaNlSvVSvzl54rrMdZT5+Oi2LM= github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.14.0 h1:CU4+EJeJi3TKYWEcYuSdWsjzw0nVsK/H0MSQOiPcymU=