mirror of
https://github.com/luxfi/age.git
synced 2026-07-27 03:39:38 +00:00
Both hybrid PQ KEMs are first-class; callers choose at runtime. Auto-detect from Bech32 prefix (zero-config at decode): age1pq1… → HPKE MLKEM768X25519 (existing, kept for compat) age1xw1… → X-Wing (IETF draft-connolly-cfrg-xwing-kem-10) Configurable at keygen: • GeneratePQIdentity(kem PQKemType) picks the KEM at generation time • Empty kem → AGE_PQ_KEM env var → default PQKemXWing • SupportedPQKems() enumerates options for CLIs Real X-Wing construction (unchanged from prior commit — verified): SHA3-256(ssM || ssX || ctX || pkX || XWingLabel) XWingLabel = 0x5c2e2f2f5e5c (6 bytes) Seed 32 · Pub 1216 · CT 1120 · SS 32 HPKE KEM codepoint 25722 Downgrade protection: "postquantum" label on WrapWithLabels prevents mixing X-Wing with non-PQ recipients (same rule as HybridRecipient). Migration path: age supports multi-recipient encryption natively, so operators can encrypt to both age1pq1… and age1xw1… during transition: age -r age1pq1aaa… -r age1xw1bbb… -o file.age plaintext Tests: 12 X-Wing tests pass including combiner spec vector, round-trip, multi-recipient, cross-KEM (X-Wing file decrypted by X-Wing only).
86 lines
3.1 KiB
Go
86 lines
3.1 KiB
Go
// Copyright 2026 The age Authors. All rights reserved.
|
|
// Use of this source code is governed by a BSD-style
|
|
// license that can be found in the LICENSE file.
|
|
|
|
package age
|
|
|
|
import (
|
|
"fmt"
|
|
"os"
|
|
)
|
|
|
|
// PQKemType selects between the two hybrid post-quantum KEMs that age
|
|
// supports. Both are first-class and produce distinct recipient prefixes:
|
|
//
|
|
// age1pq1… → PQKemHPKEMLKEM768X25519 (HPKE RFC 9180 with MLKEM768-X25519)
|
|
// age1xw1… → PQKemXWing (IETF draft-connolly-cfrg-xwing-kem-10)
|
|
//
|
|
// At parse time the selection is automatic from the Bech32 prefix, so
|
|
// callers that already have a key string don't need to set this.
|
|
// PQKemType is only needed at *keygen* time when callers want to choose
|
|
// which KEM to produce.
|
|
type PQKemType string
|
|
|
|
const (
|
|
// PQKemHPKEMLKEM768X25519 is the original post-quantum hybrid introduced
|
|
// in age v1.3.0: HPKE (RFC 9180) with MLKEM768-X25519, HKDF-SHA256,
|
|
// ChaCha20-Poly1305. Recipient prefix: age1pq1, identity prefix:
|
|
// AGE-SECRET-KEY-PQ-1.
|
|
PQKemHPKEMLKEM768X25519 PQKemType = "hpke-mlkem768x25519"
|
|
|
|
// PQKemXWing is the X-Wing KEM per IETF draft-connolly-cfrg-xwing-kem-10.
|
|
// Simpler combiner (SHA3-256 direct) with the 6-byte XWingLabel, smaller
|
|
// proof surface, HPKE KEM codepoint 25722. Recipient prefix: age1xw1,
|
|
// identity prefix: AGE-SECRET-KEY-XW-1.
|
|
PQKemXWing PQKemType = "xwing"
|
|
|
|
// DefaultPQKemEnv is the environment variable consulted by
|
|
// [GeneratePQIdentity] when callers pass an empty PQKemType.
|
|
DefaultPQKemEnv = "AGE_PQ_KEM"
|
|
)
|
|
|
|
// resolvePQKem returns the effective KEM type, consulting the env var when
|
|
// the caller didn't specify one. Defaults to X-Wing for new keys because
|
|
// the IETF draft is the recommended future direction (smaller combiner,
|
|
// simpler binding analysis, assigned HPKE codepoint).
|
|
func resolvePQKem(kem PQKemType) PQKemType {
|
|
if kem != "" {
|
|
return kem
|
|
}
|
|
if v := PQKemType(os.Getenv(DefaultPQKemEnv)); v != "" {
|
|
return v
|
|
}
|
|
return PQKemXWing
|
|
}
|
|
|
|
// GeneratePQIdentity generates a new post-quantum hybrid identity using the
|
|
// selected KEM. If kem is empty, the AGE_PQ_KEM environment variable is
|
|
// consulted; if that is also unset, X-Wing is used.
|
|
//
|
|
// The returned [Identity] is either an [*XWingIdentity] or a
|
|
// [*HybridIdentity]; callers can type-assert if they need KEM-specific
|
|
// methods, but usually the generic [Identity] interface is sufficient.
|
|
//
|
|
// Example:
|
|
//
|
|
// id, err := age.GeneratePQIdentity(age.PQKemXWing)
|
|
// if err != nil { … }
|
|
// fmt.Println(id.(interface{ Recipient() Recipient }).Recipient())
|
|
func GeneratePQIdentity(kem PQKemType) (Identity, error) {
|
|
switch resolvePQKem(kem) {
|
|
case PQKemXWing:
|
|
return GenerateXWingIdentity()
|
|
case PQKemHPKEMLKEM768X25519:
|
|
return GenerateHybridIdentity()
|
|
default:
|
|
return nil, fmt.Errorf("age: unknown PQ KEM %q (valid: %s, %s)",
|
|
kem, PQKemXWing, PQKemHPKEMLKEM768X25519)
|
|
}
|
|
}
|
|
|
|
// SupportedPQKems returns the list of post-quantum KEM types supported by
|
|
// this build of age. Useful for CLIs that enumerate options.
|
|
func SupportedPQKems() []PQKemType {
|
|
return []PQKemType{PQKemXWing, PQKemHPKEMLKEM768X25519}
|
|
}
|