Files
zeekayandHanzo Dev 0bead0130d Go 1.27.1, and the code it can say more simply
Modules declare go 1.27.1 and the build images follow. go fix applied the
modernizers; omitzero is not applied, since it changes what the JSON says.

Co-authored-by: Hanzo Dev <[email protected]>
2026-09-16 19:07:22 -07:00

200 lines
4.8 KiB
Go

// Copyright (C) 2025-2026, Lux Industries Inc. All rights reserved.
// See the file LICENSE for licensing terms.
package envelope
import (
"crypto/rand"
"crypto/sha512"
"errors"
"fmt"
"io"
"time"
"github.com/luxfi/zwing"
"github.com/luxfi/pqsafe/pkg/aead"
"github.com/luxfi/pqsafe/pkg/capsule"
"github.com/luxfi/pqsafe/pkg/ctx"
"github.com/luxfi/pqsafe/pkg/manifest"
)
// Canonical algorithm strings for PQSAFE-IL5-HYBRID-v1. The Open path
// allow-lists exactly these values; any deviation refuses the envelope.
const (
AlgHash = "SHA-384"
AlgKDF = "HKDF-SHA-384"
AlgKEM = "X-Wing-MLKEM768"
AlgSigClassical = "Ed25519"
AlgSigPQ = "ML-DSA-65"
)
// SealInput is the input bundle to Seal.
type SealInput struct {
Plaintext []byte
Sender *zwing.Identity
Recipients []Recipient
PolicyLabel string
PolicyURI string
TTL time.Duration
BulkAEAD aead.Profile
}
// Recipient is one recipient bundle.
type Recipient struct {
ID string
Public *zwing.IdentityPublic
}
// Sealed is the result of Seal.
type Sealed struct {
ManifestBytes []byte
ManifestSig []byte
Chunks [][]byte
Capsules [][]byte
EnvelopeID []byte
}
// Errors.
var ErrNoRecipient = errors.New("envelope: at least one recipient required")
// Seal encrypts the plaintext under a fresh DEK and produces a fully
// signed envelope. zwing supplies the canonical X-Wing + hybrid
// identity primitives.
func Seal(in SealInput) (*Sealed, error) {
if len(in.Recipients) == 0 {
return nil, ErrNoRecipient
}
if in.BulkAEAD == 0 {
in.BulkAEAD = aead.ProfileAESGCM
}
if in.TTL == 0 {
in.TTL = 7 * 24 * time.Hour
}
dek := make([]byte, aead.KeySize)
if _, err := io.ReadFull(rand.Reader, dek); err != nil {
return nil, err
}
nonceSeed := make([]byte, aead.NonceSeedLen)
if _, err := io.ReadFull(rand.Reader, nonceSeed); err != nil {
return nil, err
}
chunkCount := (len(in.Plaintext) + ChunkSize - 1) / ChunkSize
if chunkCount == 0 {
chunkCount = 1
}
senderEd := []byte(in.Sender.EdPub)
senderML := in.Sender.DSAPriv.PublicKey.Bytes()
created := time.Now().UTC().Format(time.RFC3339)
recipients := make([]manifest.RecipientRef, len(in.Recipients))
for i, r := range in.Recipients {
recipients[i] = manifest.RecipientRef{
ID: r.ID,
XWingPub: r.Public.XWingPub.MarshalBinary(),
CapsulePath: fmt.Sprintf("capsules/%s.bin", r.ID),
}
}
// Pre-encryption AEAD AAD: deterministic at chunk-seal time, derived
// from fields known before any chunk is encrypted. NOT the envelope
// identifier — see envelope.AAD for the threat model.
chunkAAD := AAD(AADInput{
Profile: Profile,
Version: Version,
SenderEd: senderEd,
SenderML: senderML,
NonceSeed: nonceSeed,
Created: created,
Recipients: recipients,
})
chunks := make([][]byte, chunkCount)
chunkRefs := make([]manifest.ChunkRef, chunkCount)
for i := 0; i < chunkCount; i++ {
lo := i * ChunkSize
hi := min(lo+ChunkSize, len(in.Plaintext))
ct, err := aead.Seal(in.BulkAEAD, dek, nonceSeed, uint32(i+1), chunkAAD, in.Plaintext[lo:hi])
if err != nil {
return nil, err
}
chunks[i] = ct
hh := sha512.Sum384(ct)
chunkRefs[i] = manifest.ChunkRef{
Index: uint32(i + 1),
Len: uint32(len(ct)),
SHA: hh[:],
}
}
mh := sha512.New384()
for _, cr := range chunkRefs {
mh.Write(cr.SHA)
}
merkleRoot := mh.Sum(nil)
mf := &manifest.Manifest{
Profile: Profile,
Version: Version,
Created: created,
TTL: in.TTL.String(),
PolicyLabel: in.PolicyLabel,
PolicyURI: in.PolicyURI,
BulkAEAD: in.BulkAEAD.String(),
HashAlgo: AlgHash,
KDF: AlgKDF,
KEM: AlgKEM,
SigClassical: AlgSigClassical,
SigPQ: AlgSigPQ,
ChunkSize: ChunkSize,
NonceSeed: nonceSeed,
Chunks: chunkRefs,
MerkleRoot: merkleRoot,
Sender: manifest.Sender{
Ed25519Pub: senderEd,
MLDSA65Pub: senderML,
},
Recipients: recipients,
}
manifestBytes, err := mf.MarshalCanonical()
if err != nil {
return nil, err
}
// envelope_id := SHA-384(canonical(manifest)). This is the same value
// the sender signs (binding every content-bearing field) and the value
// recipient capsules use as AAD. External dedupe / audit keys on
// envelope_id are now forgery-resistant: any change to any signed
// field changes envelope_id.
envID, err := mf.Hash()
if err != nil {
return nil, err
}
capsules := make([][]byte, len(in.Recipients))
for i, r := range in.Recipients {
c, err := capsule.Seal(r.Public.XWingPub, dek, envID)
if err != nil {
return nil, err
}
capsules[i] = c.MarshalBinary()
}
sig := in.Sender.Sign([]byte(ctx.Manifest), envID)
for i := range dek {
dek[i] = 0
}
return &Sealed{
ManifestBytes: manifestBytes,
ManifestSig: sig,
Chunks: chunks,
Capsules: capsules,
EnvelopeID: envID,
}, nil
}