Skip to content

Commit 7fa810b

Browse files
committed
tag: add Recipient.Tag and Bytes methods, and update tag scheme
1 parent 1b18d6b commit 7fa810b

File tree

2 files changed

+41
-15
lines changed

2 files changed

+41
-15
lines changed

tag/internal/tagtest/tagtest.go

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -6,8 +6,6 @@ package tagtest
66

77
import (
88
"crypto/ecdh"
9-
"crypto/hkdf"
10-
"crypto/sha256"
119
"crypto/subtle"
1210
"fmt"
1311
"testing"
@@ -73,7 +71,7 @@ func (i *ClassicIdentity) Unwrap(ss []*age.Stanza) ([]byte, error) {
7371
return nil, fmt.Errorf("invalid encrypted file key length: %d", len(s.Body))
7472
}
7573

76-
expTag, err := hkdf.Extract(sha256.New, append(enc, i.k.PublicKey().Bytes()...), []byte("age-encryption.org/p256tag"))
74+
expTag, err := i.Recipient().Tag(enc)
7775
if err != nil {
7876
return nil, fmt.Errorf("failed to compute tag: %v", err)
7977
}
@@ -139,7 +137,7 @@ func (i *HybridIdentity) Unwrap(ss []*age.Stanza) ([]byte, error) {
139137
return nil, fmt.Errorf("invalid encrypted file key length: %d", len(s.Body))
140138
}
141139

142-
expTag, err := hkdf.Extract(sha256.New, append(enc[1088:], i.k.PublicKey().Bytes()[1184:]...), []byte("age-encryption.org/mlkem768p256tag"))
140+
expTag, err := i.Recipient().Tag(enc)
143141
if err != nil {
144142
return nil, fmt.Errorf("failed to compute tag: %v", err)
145143
}

tag/tag.go

Lines changed: 39 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ import (
1818
"crypto/mlkem"
1919
"crypto/sha256"
2020
"fmt"
21+
"slices"
2122

2223
"filippo.io/age"
2324
"filippo.io/age/internal/format"
@@ -63,6 +64,7 @@ func ParseRecipient(s string) (*Recipient, error) {
6364
}
6465

6566
const compressedPointSize = 1 + 32
67+
const uncompressedPointSize = 1 + 32 + 32
6668

6769
// NewClassicRecipient returns a new P-256 [Recipient] from a raw public key.
6870
func NewClassicRecipient(publicKey []byte) (*Recipient, error) {
@@ -100,6 +102,30 @@ func (r *Recipient) Wrap(fileKey []byte) ([]*age.Stanza, error) {
100102
return s, err
101103
}
102104

105+
// Tag computes the 4-byte tag for the given ciphertext enc.
106+
//
107+
// This is a low-level method exposed for use by plugins that implement
108+
// identities compatible with tagged recipients.
109+
func (r *Recipient) Tag(enc []byte) ([]byte, error) {
110+
label, tagRecipient := "age-encryption.org/p256tag", r.Bytes()
111+
if r.Hybrid() {
112+
label = "age-encryption.org/mlkem768p256tag"
113+
// In hybrid mode, the tag is computed over just the P-256 part.
114+
tagRecipient = tagRecipient[mlkem.EncapsulationKeySize768:]
115+
if len(enc) != mlkem.CiphertextSize768+uncompressedPointSize {
116+
return nil, fmt.Errorf("invalid ciphertext size")
117+
}
118+
} else if len(enc) != uncompressedPointSize {
119+
return nil, fmt.Errorf("invalid ciphertext size")
120+
}
121+
rh := sha256.Sum256(tagRecipient)
122+
tag, err := hkdf.Extract(sha256.New, append(slices.Clip(enc), rh[:4]...), []byte(label))
123+
if err != nil {
124+
return nil, fmt.Errorf("failed to compute tag: %v", err)
125+
}
126+
return tag[:4], nil
127+
}
128+
103129
// WrapWithLabels implements [age.RecipientWithLabels], returning a single
104130
// "postquantum" label if r is a hybrid P-256 + ML-KEM-768 recipient. This
105131
// ensures a hybrid Recipient can't be mixed with other recipients that would
@@ -122,13 +148,7 @@ func (r *Recipient) WrapWithLabels(fileKey []byte) ([]*age.Stanza, []string, err
122148
return nil, nil, fmt.Errorf("failed to encrypt file key: %v", err)
123149
}
124150

125-
tagEnc, tagRecipient := enc, r.pk.Bytes()
126-
if r.Hybrid() {
127-
// In hybrid mode, the tag is computed over just the P-256 part.
128-
tagEnc = enc[mlkem.CiphertextSize768:]
129-
tagRecipient = tagRecipient[mlkem.EncapsulationKeySize768:]
130-
}
131-
tag, err := hkdf.Extract(sha256.New, append(tagEnc, tagRecipient...), []byte(label))
151+
tag, err := r.Tag(enc)
132152
if err != nil {
133153
return nil, nil, fmt.Errorf("failed to compute tag: %v", err)
134154
}
@@ -148,14 +168,22 @@ func (r *Recipient) WrapWithLabels(fileKey []byte) ([]*age.Stanza, []string, err
148168
return []*age.Stanza{l}, nil, nil
149169
}
150170

151-
// String returns the Bech32 public key encoding of r.
152-
func (r *Recipient) String() string {
171+
// Bytes returns the raw recipient encoding.
172+
func (r *Recipient) Bytes() []byte {
153173
if r.Hybrid() {
154-
return plugin.EncodeRecipient("tagpq", r.pk.Bytes())
174+
return r.pk.Bytes()
155175
}
156176
p, err := nistec.NewP256Point().SetBytes(r.pk.Bytes())
157177
if err != nil {
158178
panic("internal error: invalid P-256 public key")
159179
}
160-
return plugin.EncodeRecipient("tag", p.BytesCompressed())
180+
return p.BytesCompressed()
181+
}
182+
183+
// String returns the Bech32 public key encoding of r.
184+
func (r *Recipient) String() string {
185+
if r.Hybrid() {
186+
return plugin.EncodeRecipient("tagpq", r.Bytes())
187+
}
188+
return plugin.EncodeRecipient("tag", r.Bytes())
161189
}

0 commit comments

Comments
 (0)