package pki
import (
- "go.cypherpunks.su/keks/pki/ed25519-blake2b"
+ ed25519blake2b "go.cypherpunks.su/keks/pki/ed25519-blake2b"
"go.cypherpunks.su/keks/pki/gost"
+ sntrup4591761x25519 "go.cypherpunks.su/keks/pki/sntrup4591761-x25519"
)
const (
- Ed25519BLAKE2b = ed25519blake2b.Ed25519BLAKE2b
- GOST3410256A = gost.GOST3410256A
- GOST3410512C = gost.GOST3410512C
+ Ed25519BLAKE2b = ed25519blake2b.Ed25519BLAKE2b
+ GOST3410256A = gost.GOST3410256A
+ GOST3410512C = gost.GOST3410512C
+ SNTRUP4591761X25519 = sntrup4591761x25519.SNTRUP4591761X25519
+ SNTRUP4591761X25519HKDFBLAKE2b = sntrup4591761x25519.SNTRUP4591761X25519HKDFBLAKE2b
+ BalloonBLAKE2bHKDF = "balloon-blake2b-hkdf"
+ ChaCha20Poly1305 = "chacha20poly1305"
)
func (av *AV) Id() (id uuid.UUID) {
var hasher hash.Hash
switch av.A {
- case Ed25519BLAKE2b:
+ case Ed25519BLAKE2b, SNTRUP4591761X25519:
hasher = pkihash.ByName(pkihash.BLAKE2b256)
case GOST3410256A, GOST3410512C:
hasher = pkihash.ByName(pkihash.Streebog256)
const (
KUCA = "ca" // CA-capable key usage
KUSig = "sig" // Signing-capable key usage
+ KUKEM = "kem" // Key-encapsulation-mechanism key usage
)
// Public key.
if sd.Load.T != "cer" {
return errors.New("CerParse: wrong load type")
}
- if len(sd.Sigs) == 0 {
- return errors.New("CerParse: missing sigs")
- }
for _, sig := range sd.Sigs {
if sig.TBS.Hashes != nil {
return errors.New("CerParse: prehashed SignedData")
"go.cypherpunks.su/keks/pki"
ed25519blake2b "go.cypherpunks.su/keks/pki/ed25519-blake2b"
"go.cypherpunks.su/keks/pki/gost"
+ sntrup4591761x25519 "go.cypherpunks.su/keks/pki/sntrup4591761-x25519"
"go.cypherpunks.su/keks/pki/utils"
)
prvPath := flag.String("prv", "", "Path to private key file")
cerPath := flag.String("cer", "", "Path to certificate file")
verify := flag.Bool("verify", false, "Verify provided -cer with -ca-cer")
+ onlyLoad := flag.Bool("only-load", false, "Store only cer-load in -cer")
flag.Parse()
log.SetFlags(log.Lshortfile)
prv, prvRaw, pub, err = ed25519blake2b.NewKeypair()
case pki.GOST3410256A, pki.GOST3410512C:
prv, prvRaw, pub, err = gost.NewKeypair(*algo)
+ case pki.SNTRUP4591761X25519:
+ prvRaw, pub, err = sntrup4591761x25519.NewKeypair()
default:
err = errors.New("unknown -algo specified")
}
if len(ku) > 0 {
cerLoad.KU = &ku
}
+ var data []byte
+ if *onlyLoad {
+ if data, err = keks.EncodeBuf(cerLoad, nil); err != nil {
+ log.Fatal(err)
+ }
+ if err = os.WriteFile(*cerPath, data, 0o666); err != nil {
+ log.Fatal(err)
+ }
+ return
+ }
var caCerLoad *pki.CerLoad
if caPrv == nil {
caPrv = prv
caCerLoad = caCers[0].CerLoad()
}
sd := pki.SignedData{Load: pki.SignedDataLoad{T: "cer", V: cerLoad}}
- err = sd.CerIssueWith(caCerLoad, caPrv, since, till)
- if err != nil {
- log.Fatal(err)
+ if prv != nil {
+ if err = sd.CerIssueWith(caCerLoad, caPrv, since, till); err != nil {
+ log.Fatal(err)
+ }
}
- var data []byte
- data, err = keks.EncodeBuf(sd, nil)
- if err != nil {
+ if data, err = keks.EncodeBuf(sd, nil); err != nil {
log.Fatal(err)
}
- err = os.WriteFile(*cerPath, data, 0o666)
- if err != nil {
+ if err = os.WriteFile(*cerPath, data, 0o666); err != nil {
log.Fatal(err)
}
}
--- /dev/null
+// kekscertool -- dealing with KEKS-encoded certificates utility
+// Copyright (C) 2024-2025 Sergey Matveev <stargrave@stargrave.org>
+//
+// This program is free software: you can redistribute it and/or modify
+// it under the terms of the GNU Lesser General Public License as
+// published by the Free Software Foundation, version 3 of the License.
+//
+// This program is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU Lesser General Public License for more details.
+//
+// You should have received a copy of the GNU Lesser General Public
+// License along with this program. If not, see <http://www.gnu.org/licenses/>.
+
+package main
+
+import (
+ "bufio"
+ "crypto/subtle"
+ "errors"
+ "io"
+ "os"
+
+ "golang.org/x/crypto/chacha20poly1305"
+)
+
+const ChaPolyChunkLen = 64 * 1024
+
+var ChaPolyPad = make([]byte, 16)
+
+func incr(data []byte) {
+ for i := len(data) - 1; i >= 0; i-- {
+ data[i]++
+ if data[i] != 0 {
+ return
+ }
+ }
+}
+
+func demChaPolySeal(cek []byte) error {
+ ciph, err := chacha20poly1305.New(cek)
+ if err != nil {
+ return err
+ }
+ in := make([]byte, len(ChaPolyPad)+ChaPolyChunkLen)
+ out := make([]byte, len(ChaPolyPad)+ChaPolyChunkLen+ciph.Overhead())
+ br := bufio.NewReaderSize(os.Stdin, ChaPolyChunkLen)
+ bw := bufio.NewWriterSize(os.Stdout, len(out))
+ nonce := make([]byte, ciph.NonceSize())
+ var n int
+ var eof bool
+ for !eof {
+ n, err = io.ReadFull(br, in[len(ChaPolyPad):])
+ if err != nil {
+ if err != io.ErrUnexpectedEOF {
+ return err
+ }
+ eof = true
+ }
+ incr(nonce[:len(nonce)-1])
+ if n != ChaPolyChunkLen {
+ nonce[len(nonce)-1] = 0x01
+ }
+ _, err = bw.Write(ciph.Seal(out[:0], nonce, in[:len(ChaPolyPad)+n], nil))
+ if err != nil {
+ return err
+ }
+ }
+ return bw.Flush()
+}
+
+func demChaPolyOpen(cek []byte) error {
+ ciph, err := chacha20poly1305.New(cek)
+ if err != nil {
+ return err
+ }
+ in := make([]byte, len(ChaPolyPad)+ChaPolyChunkLen+ciph.Overhead())
+ out := make([]byte, len(ChaPolyPad)+ChaPolyChunkLen)
+ var n int
+ var eof bool
+ var chunk []byte
+ br := bufio.NewReaderSize(os.Stdin, len(in))
+ bw := bufio.NewWriterSize(os.Stdout, ChaPolyChunkLen)
+ nonce := make([]byte, ciph.NonceSize())
+ for !eof {
+ n, err = io.ReadFull(br, in)
+ if err != nil {
+ if err != io.ErrUnexpectedEOF {
+ return err
+ }
+ eof = true
+ }
+ incr(nonce[:len(nonce)-1])
+ if n != len(in) {
+ nonce[len(nonce)-1] = 0x01
+ }
+ chunk, err = ciph.Open(out[:0], nonce, in[:n], nil)
+ if err != nil {
+ return err
+ }
+ if subtle.ConstantTimeCompare(chunk[:len(ChaPolyPad)], ChaPolyPad) != 1 {
+ return errors.New("bad pad")
+ }
+ if _, err = bw.Write(chunk[len(ChaPolyPad):]); err != nil {
+ return err
+ }
+ }
+ return bw.Flush()
+}
+
+func kemChaPolySeal(kek, cek []byte) ([]byte, error) {
+ ciph, err := chacha20poly1305.New(kek)
+ if err != nil {
+ return nil, err
+ }
+ nonce := make([]byte, ciph.NonceSize())
+ plaintext := append(ChaPolyPad, cek...)
+ return ciph.Seal(nil, nonce, plaintext, nil), nil
+}
+
+func kemChaPolyOpen(kek, ciphertext []byte, cekLenExpected int) ([]byte, error) {
+ ciph, err := chacha20poly1305.New(kek)
+ if err != nil {
+ return nil, err
+ }
+ nonce := make([]byte, ciph.NonceSize())
+ var cek []byte
+ cek, err = ciph.Open(nil, nonce, ciphertext, nil)
+ if err != nil {
+ return nil, err
+ }
+ if subtle.ConstantTimeCompare(cek[:len(ChaPolyPad)], ChaPolyPad) != 1 {
+ return nil, errors.New("bad pad")
+ }
+ cek = cek[len(ChaPolyPad):]
+ if len(cek) != cekLenExpected {
+ return nil, errors.New("invalid CEK len")
+ }
+ return cek, nil
+}
--- /dev/null
+// kekscertool -- dealing with KEKS-encoded certificates utility
+// Copyright (C) 2024-2025 Sergey Matveev <stargrave@stargrave.org>
+//
+// This program is free software: you can redistribute it and/or modify
+// it under the terms of the GNU Lesser General Public License as
+// published by the Free Software Foundation, version 3 of the License.
+//
+// This program is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU Lesser General Public License for more details.
+//
+// You should have received a copy of the GNU Lesser General Public
+// License along with this program. If not, see <http://www.gnu.org/licenses/>.
+
+package main
+
+import (
+ "bytes"
+ "crypto/ecdh"
+ "crypto/rand"
+ "errors"
+ "flag"
+ "hash"
+ "io"
+ "log"
+ "os"
+
+ "github.com/companyzero/sntrup4591761"
+ "github.com/google/uuid"
+ "go.cypherpunks.su/balloon/v3"
+ "golang.org/x/crypto/blake2b"
+ "golang.org/x/crypto/chacha20poly1305"
+ "golang.org/x/crypto/hkdf"
+
+ "go.cypherpunks.su/keks"
+ "go.cypherpunks.su/keks/pki"
+ "go.cypherpunks.su/keks/pki/utils"
+)
+
+const (
+ BalloonSaltLen = 8
+ BalloonHKDFSalt = "keks/enveloped-data/balloon-blake2b-hkdf"
+ SNTRUP4591761X25519Salt = "keks/enveloped-data/sntrup4591761-x25519-hkdf-blake2b"
+)
+
+type BalloonCost struct {
+ S uint64 `keks:"s"`
+ T uint64 `keks:"t"`
+ P uint64 `keks:"p"`
+}
+
+type KEM struct {
+ A string `keks:"a"`
+ CEK []byte `keks:"cek"`
+ To *uuid.UUID `keks:"to,omitempty"`
+
+ // balloon-blake2b-hkdf related
+ Cost *BalloonCost `keks:"cost,omitempty"`
+ Salt *[]byte `keks:"salt,omitempty"`
+
+ // sntrup4591761-x25519-hkdf-blake2b related
+ Encap *[]byte `keks:"encap,omitempty"`
+}
+
+type DEM struct {
+ A string `keks:"a"`
+}
+
+type Envelope struct {
+ DEM DEM `keks:"dem"`
+ KEM []KEM `keks:"kem"`
+ Bind uuid.UUID `keks:"bind"`
+
+ Ciphertext *keks.BlobChunked `keks:"ciphertext,omitempty"`
+}
+
+func blake2b256() hash.Hash {
+ h, err := blake2b.New256(nil)
+ if err != nil {
+ panic(err)
+ }
+ return h
+}
+
+func main() {
+ log.SetFlags(log.Lshortfile)
+ setBind := flag.String("bind", "", "Set that /bind instead of autogeneration")
+ includeTo := flag.Bool("include-to", false, `Include "to" field in KEMs`)
+ passwd := flag.String("passwd", "", "Passphrase")
+ balloonS := flag.Int("balloon-s", 1<<16, "Balloon's space cost")
+ balloonT := flag.Int("balloon-t", 3, "Balloon's time cost")
+ balloonP := flag.Int("balloon-p", 2, "Balloon's number of threads")
+ doDecrypt := flag.Bool("d", false, "Decrypt")
+ var pubs []*pki.Pub
+ flag.Func("pub", "Encrypt to, path to .cer", func(v string) error {
+ sd, err := pki.CerParse(utils.MustReadFile(v))
+ if err != nil {
+ return err
+ }
+ load := sd.CerLoad()
+ if load.KU == nil {
+ log.Println(v, "does not have key usages")
+ } else {
+ if _, ok := (*load.KU)[pki.KUKEM]; !ok {
+ log.Println(v, "does not have", pki.KUKEM, "key usage")
+ }
+ }
+ if len(load.Pub) != 1 {
+ return errors.New("expected single public key")
+ }
+ pubs = append(pubs, &load.Pub[0])
+ return err
+ })
+ var prvs []*pki.AV
+ flag.Func("prv", "Our private keys for decryption", func(v string) (err error) {
+ var av pki.AV
+ d := keks.NewDecoderFromBytes(
+ utils.MustReadFile(v),
+ &keks.DecodeOpts{MaxStrLen: 1 << 16},
+ )
+ if err = d.DecodeStruct(&av); err != nil {
+ return err
+ }
+ prvs = append(prvs, &av)
+ return nil
+ })
+ flag.Parse()
+
+ var err error
+ var cek []byte
+ if *doDecrypt {
+ var envelope Envelope
+ {
+ d := keks.NewDecoderFromReader(os.Stdin, nil)
+ err = d.DecodeStruct(&envelope)
+ if err != nil {
+ log.Fatal(err)
+ }
+ }
+ if envelope.Bind == uuid.Nil {
+ log.Fatalln("unll bind")
+ }
+ if envelope.DEM.A != pki.ChaCha20Poly1305 {
+ log.Fatalln("unsupported DEM:", envelope.DEM.A)
+ }
+ if len(envelope.KEM) == 0 {
+ log.Fatalln("no KEMs")
+ }
+ for kemIdx, kem := range envelope.KEM {
+ switch kem.A {
+ case pki.BalloonBLAKE2bHKDF:
+ if *passwd == "" {
+ log.Println(kemIdx, kem.A, "skipping because no -passwd")
+ continue
+ }
+ if kem.Salt == nil {
+ log.Fatalln("missing salt")
+ }
+ if kem.Cost == nil {
+ log.Fatalln("missing cost")
+ }
+ {
+ kek := hkdf.Extract(blake2b256, balloon.H(blake2b256,
+ []byte(*passwd),
+ append(envelope.Bind[:], *kem.Salt...),
+ int(kem.Cost.S), int(kem.Cost.T), int(kem.Cost.P),
+ ), []byte(BalloonHKDFSalt))
+ var cekp []byte
+ cekp, err = kemChaPolyOpen(kek, kem.CEK, chacha20poly1305.KeySize)
+ if err != nil {
+ log.Println(kemIdx, kem.A, err, "skipping")
+ continue
+ }
+ cek = cekp
+ }
+ case pki.SNTRUP4591761X25519HKDFBLAKE2b:
+ if len(prvs) == 0 {
+ log.Println(kemIdx, kem.A, "skipping because no -prv")
+ continue
+ }
+ if kem.Encap == nil {
+ log.Fatalln("missing encap")
+ }
+ if len(*kem.Encap) != sntrup4591761.CiphertextSize+32 {
+ log.Fatalln("invalid encap len")
+ }
+ for prvIdx, prv := range prvs {
+ if len(prv.V) != sntrup4591761.PrivateKeySize+32 {
+ log.Fatalln("invalid private keys len")
+ }
+ var ourSNTRUP sntrup4591761.PrivateKey
+ copy(ourSNTRUP[:], prv.V)
+ x25519 := ecdh.X25519()
+ var ourX25519 *ecdh.PrivateKey
+ ourX25519, err = x25519.NewPrivateKey(
+ prv.V[sntrup4591761.PrivateKeySize:],
+ )
+ if err != nil {
+ log.Fatal(err)
+ }
+ var theirSNTRUP sntrup4591761.Ciphertext
+ copy(theirSNTRUP[:], *kem.Encap)
+ keySNTRUP, eq := sntrup4591761.Decapsulate(&theirSNTRUP, &ourSNTRUP)
+ if eq != 1 {
+ log.Println("can not KEM, skipping")
+ continue
+ }
+ var theirX25519 *ecdh.PublicKey
+ theirX25519, err = x25519.NewPublicKey(
+ (*kem.Encap)[sntrup4591761.CiphertextSize:],
+ )
+ if err != nil {
+ log.Fatal(err)
+ }
+ var keyX25519 []byte
+ keyX25519, err = ourX25519.ECDH(theirX25519)
+ if err != nil {
+ log.Fatal(err)
+ }
+ {
+ ikm := bytes.Join([][]byte{
+ envelope.Bind[:],
+ *kem.Encap, pubs[prvIdx].V,
+ keySNTRUP[:], keyX25519,
+ }, []byte{})
+ kek := hkdf.Extract(blake2b256,
+ ikm, []byte(SNTRUP4591761X25519Salt))
+ var cekp []byte
+ cekp, err = kemChaPolyOpen(kek, kem.CEK, chacha20poly1305.KeySize)
+ if err != nil {
+ log.Println(kemIdx, kem.A, err, "skipping")
+ continue
+ }
+ cek = cekp
+ }
+ }
+ default:
+ log.Println("unsupported KEM:", kem.A)
+ continue
+ }
+ if cek != nil {
+ break
+ }
+ }
+ if cek == nil {
+ log.Fatal("no KEMs processed")
+ }
+ err = demChaPolyOpen(cek)
+ if err != nil {
+ log.Fatal(err)
+ }
+ } else {
+ var binding uuid.UUID
+ if *setBind == "" {
+ binding, err = uuid.NewV7()
+ } else {
+ binding, err = uuid.Parse(*setBind)
+ }
+ if err != nil {
+ log.Fatal(err)
+ }
+ var kems []KEM
+ cek = make([]byte, chacha20poly1305.KeySize)
+ _, err = io.ReadFull(rand.Reader, cek)
+ if err != nil {
+ log.Fatal(err)
+ }
+ if *passwd != "" {
+ salt := make([]byte, BalloonSaltLen)
+ if _, err = io.ReadFull(rand.Reader, salt); err != nil {
+ log.Fatal(err)
+ }
+ kem := KEM{
+ A: pki.BalloonBLAKE2bHKDF,
+ Salt: &salt,
+ Cost: &BalloonCost{
+ S: uint64(*balloonS),
+ T: uint64(*balloonT),
+ P: uint64(*balloonP),
+ },
+ }
+ {
+ kek := hkdf.Extract(blake2b256, balloon.H(blake2b256,
+ []byte(*passwd),
+ append(binding[:], salt...),
+ *balloonS, *balloonT, *balloonP,
+ ), []byte(BalloonHKDFSalt))
+ kem.CEK, err = kemChaPolySeal(kek, cek)
+ if err != nil {
+ log.Fatal(err)
+ }
+ }
+ kems = append(kems, kem)
+ }
+ for _, pub := range pubs {
+ switch pub.A {
+ case pki.SNTRUP4591761X25519:
+ if len(pub.V) != sntrup4591761.PublicKeySize+32 {
+ log.Fatalln("invalid public keys len")
+ }
+ var theirSNTRUP sntrup4591761.PublicKey
+ copy(theirSNTRUP[:], pub.V[:sntrup4591761.PublicKeySize])
+ x25519 := ecdh.X25519()
+ var theirX25519 *ecdh.PublicKey
+ theirX25519, err = x25519.NewPublicKey(
+ pub.V[sntrup4591761.PublicKeySize:],
+ )
+ if err != nil {
+ log.Fatal(err)
+ }
+ var ciphertext *sntrup4591761.Ciphertext
+ var keySNTRUP *sntrup4591761.SharedKey
+ ciphertext, keySNTRUP, err = sntrup4591761.Encapsulate(
+ rand.Reader, &theirSNTRUP,
+ )
+ if err != nil {
+ log.Fatal(err)
+ }
+ var ourPrvX25519 *ecdh.PrivateKey
+ ourPrvX25519, err = ecdh.X25519().GenerateKey(rand.Reader)
+ if err != nil {
+ log.Fatal(err)
+ }
+ ourPubX25519 := ourPrvX25519.PublicKey()
+ var keyX25519 []byte
+ keyX25519, err = ourPrvX25519.ECDH(theirX25519)
+ if err != nil {
+ log.Fatal(err)
+ }
+ kem := KEM{A: pki.SNTRUP4591761X25519HKDFBLAKE2b}
+ encap := append(ciphertext[:], ourPubX25519.Bytes()...)
+ kem.Encap = &encap
+ {
+ ikm := bytes.Join([][]byte{
+ binding[:],
+ encap, pub.V,
+ keySNTRUP[:], keyX25519,
+ }, []byte{})
+ kek := hkdf.Extract(blake2b256,
+ ikm, []byte(SNTRUP4591761X25519Salt))
+ kem.CEK, err = kemChaPolySeal(kek, cek)
+ if err != nil {
+ log.Fatal(err)
+ }
+ }
+ if *includeTo {
+ kem.To = &pub.Id
+ }
+ kems = append(kems, kem)
+ default:
+ log.Fatalln("unsupported KEM:", pub.A)
+ }
+ }
+ if len(kems) == 0 {
+ log.Fatal("no KEMs specified")
+ }
+ {
+ var hdr []byte
+ hdr, err = keks.EncodeBuf(&Envelope{
+ Bind: binding,
+ KEM: kems,
+ DEM: DEM{A: pki.ChaCha20Poly1305},
+ }, nil)
+ if err != nil {
+ log.Fatal(err)
+ }
+ if _, err = io.Copy(os.Stdout, bytes.NewReader(hdr)); err != nil {
+ log.Fatal(err)
+ }
+ }
+ if err = demChaPolySeal(cek); err != nil {
+ log.Fatal(err)
+ }
+ }
+}
"os"
"time"
+ "github.com/google/uuid"
+
"go.cypherpunks.su/keks"
"go.cypherpunks.su/keks/pki"
pkihash "go.cypherpunks.su/keks/pki/hash"
typ := flag.String("type", "data", "Type of the content, /load/t value")
hashAlgo := flag.String("hash", "", "Algorithm identifier of the hash to use")
verify := flag.Bool("verify", false, "Verify with provided -cer")
+ envelopeBindingHex := flag.String("envelope-binding", "", "Set envelope-binding")
flag.Parse()
log.SetFlags(log.Lshortfile)
+ envelopeBinding := uuid.Nil
+ var err error
+ if *envelopeBindingHex != "" {
+ envelopeBinding, err = uuid.Parse(*envelopeBindingHex)
+ if err != nil {
+ log.Fatal(err)
+ }
+ }
+
if *cerPath == "" {
log.Fatal("no -cer is set")
}
sd.Hashes = &sdHashes
sigHashes := map[string][]byte{*hashAlgo: hasher.Sum(nil)}
when := time.Now().UTC().Truncate(1000 * time.Microsecond)
- err = sd.SignWith(cer.CerLoad(), signer, pki.SigTBS{
+ sigTbs := pki.SigTBS{
Hashes: &sigHashes,
When: &when,
- })
+ }
+ if envelopeBinding != uuid.Nil {
+ sigTbs.EnvelopeBinding = &envelopeBinding
+ }
+ err = sd.SignWith(cer.CerLoad(), signer, sigTbs)
if err != nil {
log.Fatal(err)
}
go.cypherpunks.su/gogost/v6 v6.0.1
)
-require golang.org/x/crypto v0.26.0 // indirect
+require (
+ go.cypherpunks.su/balloon/v3 v3.0.0 // indirect
+ golang.org/x/crypto v0.32.0 // indirect
+)
Exp *[]time.Time `keks:"exp,omitempty"`
When *time.Time `keks:"when,omitempty"`
SID uuid.UUID `keks:"sid"`
+
+ EnvelopeBinding *uuid.UUID `keks:"envelope-binding,omitempty"`
}
type Sig struct {
--- /dev/null
+package sntrup4591761x25519
+
+const (
+ SNTRUP4591761X25519 = "sntrup4591761-x25519"
+ SNTRUP4591761X25519HKDFBLAKE2b = "sntrup4591761-x25519-hkdf-blake2b"
+)
--- /dev/null
+module go.cypherpunks.su/keks/pki/sntrup4591761-x25519
+
+go 1.22
+
+require github.com/companyzero/sntrup4591761 v0.0.0-20220309191932-9e0f3af2f07a
--- /dev/null
+github.com/companyzero/sntrup4591761 v0.0.0-20220309191932-9e0f3af2f07a h1:clYxJ3Os0EQUKDDVU8M0oipllX0EkuFNBfhVQuIfyF0=
+github.com/companyzero/sntrup4591761 v0.0.0-20220309191932-9e0f3af2f07a/go.mod h1:z/9Ck1EDixEbBbZ2KH2qNHekEmDLTOZ+FyoIPWWSVOI=
--- /dev/null
+// GoKEKS/PKI -- PKI-related capabilities based on KEKS encoded formats
+// Copyright (C) 2024-2025 Sergey Matveev <stargrave@stargrave.org>
+//
+// This program is free software: you can redistribute it and/or modify
+// it under the terms of the GNU Lesser General Public License as
+// published by the Free Software Foundation, version 3 of the License.
+//
+// This program is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU Lesser General Public License for more details.
+//
+// You should have received a copy of the GNU Lesser General Public
+// License along with this program. If not, see <http://www.gnu.org/licenses/>.
+
+package sntrup4591761x25519
+
+import (
+ "crypto/ecdh"
+ "crypto/rand"
+
+ "github.com/companyzero/sntrup4591761"
+)
+
+func NewKeypair() (prv, pub []byte, err error) {
+ var pubSNTRUP *sntrup4591761.PublicKey
+ var prvSNTRUP *sntrup4591761.PrivateKey
+ pubSNTRUP, prvSNTRUP, err = sntrup4591761.GenerateKey(rand.Reader)
+ if err != nil {
+ return
+ }
+ var prvX25519 *ecdh.PrivateKey
+ prvX25519, err = ecdh.X25519().GenerateKey(rand.Reader)
+ if err != nil {
+ return
+ }
+ pubX25519 := prvX25519.PublicKey()
+ prv = append(prvSNTRUP[:], prvX25519.Bytes()...)
+ pub = append(pubSNTRUP[:], pubX25519.Bytes()...)
+ return
+}
Same rules of serialisation must be used as with
@code{@ref{signed-data-gost3410}}. Public key's
-identifier and and @code{cid} should be calculated
+identifier and @code{cid} should be calculated
using big-endian Streebog-256 hash.
@node cer-ed25519-blake2b
Same calculation and serialisation rules must be used as with
@code{@ref{signed-data-ed25519-blake2b}}.
-Public key's identifier and and @code{cid} should be calculated
+Public key's identifier and @code{cid} should be calculated
+using BLAKE2b hash with 128 or 256 bit output length specified.
+
+@node cer-sntrup4591761-x25519
+@subsection cer with SNTRUP4591761-X25519
+
+Certificate with combined Streamlined NTRU Prime 4591^761 and Curve25519
+public keys is used for KEM purposes, so should have "kem" key usage set.
+
+Its algorithm identifier is @code{sntrup4591761-x25519}. Its public key
+value is a concatenation of 1218-byte SNTRUP4591761 public key and
+32-byte Curve25519 one.
+
+Public key's identifier and @code{cid} should be calculated
using BLAKE2b hash with 128 or 256 bit output length specified.
--- /dev/null
+ai = text ; algorithm identifier
+
+enveloped-data = {
+ dem: dem,
+ kem: [+ kem],
+ bind: uuid,
+ ? ciphertext: blob,
+}
+
+dem = dem-chacha20poly1305 / dem-kuznechik-ctracpkm-hmac-hkdf
+
+dem-chacha20poly1305 = {a: "chacha20poly1305"}
+
+dem-kuznechik-ctracpkm-hmac-hkdf = {
+ a: "kuznechik-ctracpkm-hmac-hkdf",
+ seed: bytes,
+ iv: bytes,
+}
+
+kem = kem-generic /
+ kem-balloon-blake2b-hkdf /
+ kem-gost3410-kexp15 /
+ kem-sntrup4591761-x25519-hkdf-blake2b
+
+kem-generic = {
+ a: ai,
+ cek: bytes,
+ * text => any
+}
+
+kem-balloon-blake2b = {
+ a: "balloon-blake2b-hkdf",
+ cek: bytes,
+ cost: {
+ s: int, ; space cost
+ t: int, ; time cost
+ p: int, ; parallel cost
+ },
+ salt: bytes,
+}
+
+kem-gost3410-kexp15 = {
+ a: "gost3410-kexp15",
+ cek: bytes,
+ ukm: bytes,
+ pub: bytes,
+ iv: bytes,
+ ? to: uuid,
+}
+
+kem-sntrup4591761-x25519-hkdf-blake2b = {
+ a: ai,
+ cek: bytes,
+ encap: bytes,
+ ? to: uuid,
+}
--- /dev/null
+@node enveloped-data
+@cindex enveloped-data
+@section enveloped-data format
+
+Enveloped data is an encrypted data.
+
+@verbatiminclude format/enveloped-data.cddl
+
+@code{/ciphertext} contains the ciphertext. It is encrypted with random
+"content encryption key" (CEK) with an algorithm specified in
+@code{/dem/a} (data encapsulation mechanism). @code{/dem} may contain
+additional fields supplementing the decryption process, like
+initialisation vector.
+
+@code{/ciphertext} is a BLOB, which chunk's length depends on DEM
+algorithm. If it is absent, then ciphertext is provided by other means,
+for example just following the enveloped-data structure.
+
+CEK is encapsulated in @code{/kem/*} entries (key encapsulation
+mechanism), using @code{/kem/*/a} algorithm. @code{/kem/*/cek} field
+contains an encrypted CEK.
+
+If KEM uses public-key based cryptography, then recipient's public
+key(s) should be provided by @ref{cer, certificate}, which may lack the
+signatures at all. Optional @code{/kem/*/to}, public key's identifier,
+may provide a hint for quickly searching for the key on the recipient's
+side.
+
+@code{/bind} value can be used to bind the enveloped
+@ref{signed-data, signed-data} to the envelope.
+Either UUIDv4 or UUIDv7 are recommended.
+
+@node enveloped-data-chacha20poly1305
+@subsection enveloped-data with ChaCha20-Poly1305 DEM
+
+@code{/dem/a} equals to "chacha20poly1305". Data is split on 64 KiB
+chunks which are encrypted the following way:
+
+@verbatim
+ChaCha20-Poly1305(
+ key=cek, nonce=BE(11-byte counter) || tail-flag,
+ data=16*0x00 || chunk, ad="")
+@end verbatim
+
+where @code{counter} starts at zero and incremented with each chunk.
+@code{tail-flag} is a byte indicating if that is the last chunk in the
+payload. It equals to 0x01 for the last chunk and to 0x00 for other ones.
+Last chunk should be smaller than previous ones, maybe even empty.
+
+@code{/ciphertext}'s chunk length equals to 16+64KiB+16 bytes.
+
+@node enveloped-data-kuznechik-ctracpkm-hmac-hkdf
+@subsection enveloped-data-kuznechik-ctracpkm-hmac-hkdf
+
+@code{/dem/a} equals to "kuznechik-ctracpkm-hmac-hkdf".
+@code{/dem/seed} contains 16 bytes for the HKDF invocation below.
+@code{/dem/iv} contains 8 bytes of initialisation vector.
+
+@verbatim
+Kenc, Kauth = HKDF-Extract(Streebog-512,
+ salt="keks/enveloped-data/kuznechik-ctracpkm-hmac-hkdf",
+ secret=seed || CEK)
+@end verbatim
+
+Encryption is performed with Kuznechik (ГОСТ Р 34.12-2015) block cipher
+in CTR-ACPKM mode of operation (Р 1323565.1.017) with 256KiB section
+size. Authentication of ciphertext is performed with Streebog-512 (ГОСТ
+Р 34.11-2012) in HMAC mode.
+
+@code{/ciphertext}'s chunk length equals to 64KiB bytes.
+
+@node enveloped-data-balloon-blake2b-hkdf
+@subsection enveloped-data with Balloon-BLAKE2b+HKDF-BLAKE2b KEM
+
+@code{/kem/*/a} equals to "balloon-blake2b-hkdf".
+Recipient map must also contain additional fields:
+
+@table @code
+@item /to/*/cost/s: uint64
+ Balloon's space cost (buffer size, number of hash-output sized blocks).
+@item /to/*/cost/t: uint64
+ Balloon's time cost (number of rounds).
+@item /to/*/cost/p: uint64
+ Balloon's parallel cost (number of threads).
+@item /to/*/salt: bin
+ Salt.
+@end table
+
+@url{https://crypto.stanford.edu/balloon/, Balloon} memory-hardened
+password hasher must be used with BLAKE2b-256 hash.
+
+@code{/kem/*/cek} is encrypted the following way:
+
+@verbatim
+KEK = HKDF-Extract(BLAKE2b-256,
+ salt="keks/enveloped-data/balloon-blake2b-hkdf",
+ secret=balloon(BLAKE2b-256, password, bind || salt, s, t, p))
+ChaCha20-Poly1305(data=16*0x00 || CEK, key=KEK, nonce=12*0x00, ad="")
+@end verbatim
+
+@node enveloped-data-gost3410-kexp15
+@subsection enveloped-data-gost3410-kexp15
+
+@code{/kem/*/a} equals to "gost3410-kexp15".
+Recipient map must also contain additional fields:
+
+@table @code
+@item /to/*/ukm: bytes
+ Additional 16-bytes keying material.
+@item /to/*/pub: bytes
+ Sender's ephemeral 512-bit public key.
+@item /to/*/iv: bytes
+ 8-byte initialisation vector for KExp15.
+@end table
+
+ГОСТ Р 34.10-2012 VKO 512-bit parameter set C ("gost3410-512C") must be
+used for DH operation, with UKM taken from the structure. Its 512-bit
+output result is used for KExp15 (Р 1323565.1.017) key wrapping algorithm:
+
+@verbatim
+KExp15(Kenc, Kauth, IV, CEK):
+ return CTR(Kenc, CEK+CMAC(Kauth, IV+CEK), IV=IV)
+@end verbatim
+
+@node enveloped-data-sntrup4591761-x25519-hkdf-blake2b
+@subsection enveloped-data with SNTRUP4591761+x25519+HKDF-BLAKE2b KEM
+
+@code{/kem/*/a} equals to "sntrup4591761-x25519-hkdf-blake2b".
+Recipient certificate with
+@ref{cer-sntrup4591761-x25519, @code{sntrup4591761-x25519}} public key
+must be used. It should have "kem" key usage set.
+
+Recipient map must also contain additional field:
+@code{/kem/*/encap: bytes} -- concatenation of 1047 bytes of Streamlined
+NTRU Prime 4591^761's ciphertext with 32 bytes of ephemeral
+Curve25519 public key.
+
+Recipient performs Curve25519 and SNTRUP computation to
+derive/decapsulate two 32-byte shared keys. Then it combines
+them to get the decryption key of the CEK.
+@code{/kem/*/cek} is encrypted the following way:
+
+@verbatim
+KEK = HKDF-Extract(BLAKE2b-256,
+ salt="keks/enveloped-data/sntrup4591761-x25519-hkdf-blake2b",
+ secret=bind ||
+ sntrup4591761-sender-ciphertext ||
+ x25519-sender-public-key ||
+ sntrup4591761-recipient-public-key ||
+ x25519-recipient-public-key ||
+ sntrup4591761-shared-key ||
+ x25519-shared-key)
+ChaCha20-Poly1305(data=16*0x00 || CEK, key=KEK, nonce=12*0x00, ad="")
+@end verbatim
@include format/signed-data.texi
@include format/cer.texi
@include format/hashed-data.texi
+@include format/enveloped-data.texi
@include format/registry.texi
@code{ed25519-blake2b} algorithm identifier is used, however actually no
hash is involved in private key storage.
+
+@node private-key-sntrup4591761-x25519
+@subsection private-key with SNTRUP4591761-X25519
+
+Concatenation of Streamlined NTRU Prime 4591^761's 1600-byte private key
+and Curve25519's 32-byte one.
+
+@code{sntrup4591761-x25519} algorithm identifier is used.
@table @code
@item ecdsa-nist256p, ecdsa-nist521p
-@item x25519
-@item x448
@item gost3410-256A, gost3410-512C
@code{@ref{cer-gost3410}},
@code{@ref{private-key-gost3410}}
+@item x25519
+@item x448
+@end table
+
+@node AI DEM
+@subsection DEM
+
+@table @code
+@item chacha20poly1305
+ @code{@ref{enveloped-data-chacha20poly1305}}
+@item kuznechik-ctracpkm-hmac-hkdf
+ @code{@ref{enveloped-data-kuznechik-ctracpkm-hmac-hkdf}}
+@end table
+
+@node AI KEM
+@subsection KEM
+
+@table @code
+@item argon2id-hkdf-blake2b
+@item balloon-blake2b-hkdf
+ @code{@ref{enveloped-data-balloon-blake2b-hkdf}}
+@item gost3410-kexp15
+ @code{@ref{enveloped-data-gost3410-kexp15}}
+@item mlkem768-x25519
+@item sntrup761-x25519
+@item sntrup4591761-x25519
+ @code{@ref{cer-sntrup4591761-x25519}},
+ @code{@ref{private-key-sntrup4591761-x25519}}
+@item sntrup4591761-x25519-hkdf-blake2b
+ @code{@ref{enveloped-data-sntrup4591761-x25519-hkdf-blake2b}}
+@item sntrup761-x25519-hkdf-blake2b
@end table
@node AI Sign
sig-tbs = {
sid: uuid, ; signer's public key id
? hash: {ai => bytes}, ; when using prehashing
+ ? envelope-binding: uuid,
? when: tai64 / tai64n,
* text => any
}
help creating the whole verification chain. They are placed outside
@code{/sigs}, because some of them may be shared among signers.
+If signed data is also intended to be @ref{enveloped-data, enveloped}
+(encrypted), then @code{/sigs/*/tbs/envelope-binding} should be set to
+envelop's @code{/bind} value.
+
@node signed-data-gost3410
@subsection signed-data with GOST R 34.10-2012