From 88299a50e4e2cf322f60d20ec4218f433018c53c Mon Sep 17 00:00:00 2001 From: Filippo Valsorda Date: Sat, 29 Aug 2026 17:32:14 +0200 Subject: [PATCH] cmd/age-inspect: classify unknown recipient stanzas Reported by Joe Doyle of Trail of Bits. --- cmd/age-inspect/inspect.go | 3 +++ internal/inspect/inspect.go | 6 +++-- internal/inspect/inspect_test.go | 39 ++++++++++++++++++++++++++++---- 3 files changed, 41 insertions(+), 7 deletions(-) diff --git a/cmd/age-inspect/inspect.go b/cmd/age-inspect/inspect.go index f599453..a2f9f37 100644 --- a/cmd/age-inspect/inspect.go +++ b/cmd/age-inspect/inspect.go @@ -101,6 +101,9 @@ func main() { case "no": fmt.Printf("This file does NOT use post-quantum encryption.\n") fmt.Printf("\n") + case "unknown": + fmt.Printf("It is unknown whether this file uses post-quantum encryption.\n") + fmt.Printf("\n") } fmt.Printf("Size breakdown (assuming it decrypts successfully):\n") fmt.Printf("\n") diff --git a/internal/inspect/inspect.go b/internal/inspect/inspect.go index 628979e..55d8229 100644 --- a/internal/inspect/inspect.go +++ b/internal/inspect/inspect.go @@ -33,7 +33,7 @@ type Metadata struct { func Inspect(r io.Reader, fileSize int64) (*Metadata, error) { data := &Metadata{ Version: "age-encryption.org/v1", - Postquantum: "unknown", + Postquantum: "yes", } tr := &trackReader{r: r} @@ -64,8 +64,10 @@ func Inspect(r io.Reader, fileSize int64) (*Metadata, error) { case "X25519", "ssh-rsa", "ssh-ed25519", "p256tag", "piv-p256": data.Postquantum = "no" case "mlkem768x25519", "scrypt", "mlkem768p256tag": + // Keep "yes". + default: if data.Postquantum != "no" { - data.Postquantum = "yes" + data.Postquantum = "unknown" } } } diff --git a/internal/inspect/inspect_test.go b/internal/inspect/inspect_test.go index a9e8ac2..209017e 100644 --- a/internal/inspect/inspect_test.go +++ b/internal/inspect/inspect_test.go @@ -10,14 +10,14 @@ import ( "filippo.io/age/internal/stream" ) -// buildFile serializes a header with a single stanza of the given type, +// buildFile serializes a header with the given stanza types, // followed by the minimal valid encrypted payload (a 16-byte stream nonce // and a single empty ChaCha20-Poly1305 chunk). -func buildFile(t *testing.T, stanzaType string) []byte { +func buildFile(t *testing.T, stanzaTypes ...string) []byte { t.Helper() - hdr := &format.Header{ - Recipients: []*format.Stanza{{Type: stanzaType}}, - MAC: make([]byte, 32), + hdr := &format.Header{MAC: make([]byte, 32)} + for _, stanzaType := range stanzaTypes { + hdr.Recipients = append(hdr.Recipients, &format.Stanza{Type: stanzaType}) } buf := &bytes.Buffer{} if err := hdr.Marshal(buf); err != nil { @@ -53,6 +53,35 @@ func TestInspectTagStanzas(t *testing.T) { } } +func TestInspectPostquantum(t *testing.T) { + tests := []struct { + name string + stanzaTypes []string + want string + }{ + {"postquantum", []string{"mlkem768x25519"}, "yes"}, + {"classical", []string{"X25519"}, "no"}, + {"both", []string{"mlkem768x25519", "X25519"}, "no"}, + {"unrecognized", []string{"tpm-ecc"}, "unknown"}, + {"postquantum and unrecognized", []string{"mlkem768x25519", "tpm-ecc"}, "unknown"}, + {"unrecognized and postquantum", []string{"tpm-ecc", "mlkem768x25519"}, "unknown"}, + {"classical and unrecognized", []string{"X25519", "tpm-ecc"}, "no"}, + {"unrecognized and classical", []string{"tpm-ecc", "X25519"}, "no"}, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + f := buildFile(t, tt.stanzaTypes...) + md, err := Inspect(bytes.NewReader(f), int64(len(f))) + if err != nil { + t.Fatalf("Inspect: %v", err) + } + if md.Postquantum != tt.want { + t.Errorf("Postquantum = %q, want %q", md.Postquantum, tt.want) + } + }) + } +} + // readAfterEOFReader returns io.EOF along with the last of its data, and then // more data from subsequent Reads, like a terminal that received Ctrl-D // followed by more input.