// 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/>.
+#include <ctype.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
UUIDPrint(atom.v.uuid);
fputs("\n", stdout);
break;
+ case KEKSItemMagic:
+ fputs("Magic(", stdout);
+ for (size_t i = 0; i < atom.v.str.len; i++) {
+ if (isprint(atom.v.str.ptr[i]) == 0) {
+ printf("\\x%02x", atom.v.str.ptr[i]);
+ } else {
+ fputc(atom.v.str.ptr[i], stdout);
+ }
+ }
+ fputs(")\n", stdout);
+ break;
case KEKSItemPint:
fprintf(stdout, "%zu\n", atom.v.pint);
break;
// License along with this program. If not, see <http://www.gnu.org/licenses/>.
#include <assert.h>
+#include <ctype.h>
#include <errno.h>
#include <getopt.h>
#include <math.h>
UUIDPrint(item->atom.v.uuid);
fputs("\n", stdout);
break;
+ case KEKSItemMagic:
+ fputs("Magic(", stdout);
+ for (size_t i = 0; i < item->atom.v.str.len; i++) {
+ if (isprint(item->atom.v.str.ptr[i]) == 0) {
+ printf("\\x%02x", item->atom.v.str.ptr[i]);
+ } else {
+ fputc(item->atom.v.str.ptr[i], stdout);
+ }
+ }
+ fputs(")\n", stdout);
+ break;
case KEKSItemPint:
fprintf(stdout, "%zu\n", item->atom.v.pint);
break;
size_t off = 0;
struct timespec started;
struct timespec finished;
+AfterMagic:
errno = 0;
if (clock_gettime(CLOCK_MONOTONIC_PRECISE, &started) != 0) {
fprintf(stderr, "clock_gettime(started): %s\n", strerror(errno));
fprintf(stderr, "err: %s\n", KEKSErr2Str(err));
return EXIT_FAILURE;
}
+ if ((items.len == 1) && (items.list[0].atom.typ == KEKSItemMagic)) {
+ goto AfterMagic;
+ }
if (!onlyTotals) {
setenv("TZ", "UTC", 1);
err = printer(&items, 0, 0, false, 0, NULL);
fprintf(stderr, "err: %s\n", KEKSErr2Str(err));
return EXIT_FAILURE;
}
+ if ((items.len > 1) && (items.list[0].atom.typ == KEKSItemMagic)) {
+ err = printer(&items, 1, 0, false, 0, NULL);
+ if (err != KEKSErrNo) {
+ fprintf(stderr, "err: %s\n", KEKSErr2Str(err));
+ return EXIT_FAILURE;
+ }
+ }
}
if (!noTotals) {
printf(
unsigned char *bin = malloc((uint32_t)1 << (uint8_t)17);
assert(bin != NULL);
+ {
+ unsigned char *magic = (unsigned char *)"test-vector";
+ adder(KEKSAtomMagicEncode(
+ &Got, buf + Off, len - Off, magic, strlen((const char *)magic)));
+ }
+
adder(KEKSAtomMapEncode(&Got, buf + Off, len - Off)); // .
adder(
@DOCSTRING KEKSAtomNILEncode@
@DOCSTRING KEKSAtomBoolEncode@
@DOCSTRING KEKSAtomUUIDEncode@
+@DOCSTRING KEKSAtomMagicEncode@
@DOCSTRING KEKSAtomUintEncode@
@DOCSTRING KEKSAtomSintEncode@
@DOCSTRING KEKSAtomListEncode@
KEKSAtomTAI64 = 0x18,
KEKSAtomTAI64N = 0x19,
KEKSAtomTAI64NA = 0x1A,
+ KEKSAtomMagic = 0x4B,
KEKSAtomStrings = 0x80,
KEKSAtomIsUTF8 = 0x40,
KEKSItemBlob,
KEKSItemFloat,
KEKSItemTAI64,
+ KEKSItemMagic,
KEKSItemBin,
KEKSItemStr,
KEKSItemRaw,
// @code{.chunkLen} is the length of the chunk. @code{.chunks} is
// the number of chunks, including the terminating binary string.
// @item .v.str
-// @code{.ptr} points to the start of the binary/UTF-8/TAI64*
+// @code{.ptr} points to the start of the binary/UTF-8/TAI64*/Magic
// string. @code{.len} is its length in bytes.
// Raw values use it as a payload.
// @end table
}
atom->v.uuid = buf + 1;
break;
+ case KEKSAtomMagic:
+ atom->typ = KEKSItemMagic;
+ (*got) += 15;
+ if (len < (*got)) {
+ return KEKSErrNotEnough;
+ }
+ if (memcmp(buf, (const unsigned char *)"KEKS", 4) != 0) {
+ return KEKSErrBadMagic;
+ }
+ atom->v.str.ptr = buf + 4;
+ {
+ size_t l = 0;
+ for (; l < 12; l++) {
+ if (buf[4 + l] == 0) {
+ break;
+ }
+ }
+ atom->v.str.len = l;
+ }
+ break;
case KEKSAtomList:
atom->typ = KEKSItemList;
return true;
}
+bool
+KEKSAtomMagicEncode(
+ size_t *len,
+ unsigned char *buf,
+ const size_t cap,
+ const unsigned char *v,
+ const size_t vlen)
+{
+ if (vlen > 12) {
+ return false;
+ }
+ (*len) = 1 + 15;
+ if (cap < (1 + 15)) {
+ return false;
+ }
+ buf[0] = 'K';
+ buf[1] = 'E';
+ buf[2] = 'K';
+ buf[3] = 'S';
+ memcpy(buf + 4, v, vlen);
+ return true;
+}
+
static bool
keksAtomIntEncode(size_t *len, unsigned char *buf, const size_t cap, const uint64_t v)
{
const size_t cap,
const unsigned char v[16]);
+// TEXINFO: KEKSAtomMagicEncode
+// @deftypefun bool KEKSAtomMagicEncode @
+// (size_t *len, unsigned char *buf, const size_t cap, @
+// const unsigned char *v, const size_t vlen)
+// Encode Magic atom in provided @var{buf} with capacity of @var{cap}.
+// In case of success, true is returned and @var{len} will hold how many
+// bytes were written to buffer.
+// @end deftypefun
+bool
+KEKSAtomMagicEncode(
+ size_t *len,
+ unsigned char *buf,
+ const size_t cap,
+ const unsigned char *v,
+ const size_t vlen);
+
// TEXINFO: KEKSAtomUintEncode
// @deftypefun bool KEKSAtomUintEncode @
// (size_t *len, unsigned char *buf, const size_t cap, const uint64_t v)
return "DeepRecursion";
case KEKSErrUnexpectedEOC:
return "UnexpectedEOC";
+ case KEKSErrBadMagic:
+ return "BadMagic";
default:
return "unknown";
}
// Too deep recursion involved during parsing.
// @item KEKSErrUnexpectedEOC
// Unexpected EOC met.
+// @item KEKSErrBadMagic
+// Wrong magic value.
// @end table
// @end deftp
enum KEKSErr {
KEKSErrUnsatisfiedSchema,
KEKSErrDeepRecursion,
KEKSErrUnexpectedEOC,
+ KEKSErrBadMagic,
};
// TEXINFO: KEKSErr2Str
case KEKSItemUUID:
ok = KEKSAtomUUIDEncode(&got, buf + *off, cap - (*off), item->atom.v.uuid);
break;
+ case KEKSItemMagic:
+ ok = KEKSAtomMagicEncode(
+ &got, buf + *off, cap - (*off), item->atom.v.str.ptr, item->atom.v.str.len);
+ break;
case KEKSItemPint:
ok = KEKSAtomUintEncode(&got, buf + *off, cap - (*off), item->atom.v.pint);
break;
ErrUnknownType = errors.New("unknown type")
ErrBadUTF8 = errors.New("invalid UTF-8")
ErrBadInt = errors.New("bad int value")
+ ErrBadMagic = errors.New("bad magic value")
)
func (ctx *Decoder) DecodeAtom() (t types.Type, err error) {
tai := tai64n.TAI64NA(unsafe.Slice(unsafe.StringData(s), 16))
ctx.tai64nas = append(ctx.tai64nas, tai)
}
+ case AtomMagic:
+ var s string
+ s, err = ctx.getBytes(15)
+ if err != nil {
+ return
+ }
+ if s[:3] != "EKS" {
+ err = ErrBadMagic
+ return
+ }
+ t = types.Magic
+ ctx.strs = append(ctx.strs, strings.TrimRight(s[3:], "\x00"))
default:
err = ErrUnknownType
return
import (
"bytes"
+ "errors"
"io"
"math/big"
return io.Copy(w, bytes.NewReader(append([]byte{byte(AtomUUID)}, v[:]...)))
}
+// Write an encoded Magic atom.
+func MagicEncode(w io.Writer, m Magic) (written int64, err error) {
+ if len(m) > 12 {
+ return 0, errors.New("too long magic")
+ }
+ var v [16]byte
+ v[0] = 'K'
+ v[1] = 'E'
+ v[2] = 'K'
+ v[3] = 'S'
+ copy(v[4:], []byte(m))
+ return io.Copy(w, bytes.NewReader(v[:]))
+}
+
func atomUintEncode(w io.Writer, v uint64) (written int64, err error) {
if v == 0 {
return BinEncode(w, []byte{})
_ = x[AtomTAI64-24]
_ = x[AtomTAI64N-25]
_ = x[AtomTAI64NA-26]
+ _ = x[AtomMagic-75]
}
const (
_AtomType_name_2 = "AtomBLOBAtomPIntAtomNInt"
_AtomType_name_3 = "AtomFloat16AtomFloat32AtomFloat64AtomFloat128AtomFloat256"
_AtomType_name_4 = "AtomTAI64AtomTAI64NAtomTAI64NA"
+ _AtomType_name_5 = "AtomMagic"
)
var (
case 24 <= i && i <= 26:
i -= 24
return _AtomType_name_4[_AtomType_index_4[i]:_AtomType_index_4[i+1]]
+ case i == 75:
+ return _AtomType_name_5
default:
return "AtomType(" + strconv.FormatInt(int64(i), 10) + ")"
}
case types.TAI64NA:
t := iter.TAI64NA()
fmt.Printf("TAI64NA(%s)\n", hexenc(t[:]))
+ case types.Magic:
+ var builder strings.Builder
+ for _, b := range []byte(iter.Magic()) {
+ if strconv.IsPrint(rune(b)) {
+ builder.WriteByte(b)
+ } else {
+ builder.WriteString(fmt.Sprintf("\\x%02x", b))
+ }
+ }
+ fmt.Printf("Magic(%s)\n", builder.String())
case types.Bin:
printbin(iter.Bin())
case types.Str:
func main() {
flag.Parse()
- ctx := keks.NewDecoderFromReader(
- bufio.NewReader(os.Stdin),
- &keks.DecodeOpts{SaveOffsets: true},
- )
- _, err := ctx.Parse()
- if err != nil {
- log.Fatal(err)
+ br := bufio.NewReader(os.Stdin)
+ var off int64
+ for {
+ ctx := keks.NewDecoderFromReader(br, &keks.DecodeOpts{SaveOffsets: true})
+ ctx.Read = off
+ t, err := ctx.Parse()
+ if err != nil {
+ log.Fatal(err)
+ }
+ iter := ctx.Iter()
+ printer(iter, 1, false, false)
+ if t != types.Magic {
+ fmt.Println(ctx.Read, "bytes")
+ break
+ }
+ off += ctx.Read
}
- iter := ctx.Iter()
- printer(iter, 1, false, false)
- fmt.Println(ctx.Read, "bytes")
}
"uuid": uuid.MustParse("0e875e3f-d385-49eb-87b4-be42d641c367"),
}
var buf bytes.Buffer
- _, err := keks.Encode(&buf, data, nil)
+ _, err := keks.Encode(&buf, keks.Magic("test-vector"), nil)
+ if err != nil {
+ log.Fatal(err)
+ }
+ _, err = keks.Encode(&buf, data, nil)
if err != nil {
log.Fatal(err)
}
func main() {
var buf bytes.Buffer
+ mustEncode(keks.MagicEncode(&buf, keks.Magic("test-vector")))
{
mustEncode(keks.ByteEncode(&buf, byte(keks.AtomMap)))
{
if *our != their {
log.Fatalln("TAI64NA differs:", our, their)
}
+ case "MAGIC":
+ our, ok := v.(keks.Magic)
+ if !ok {
+ log.Fatalf("expected Magic, got %+v\n", v)
+ }
+ var their string
+ if len(fields) > 1 {
+ their = string(mustDecodeHex(fields[1]))
+ }
+ if our != keks.Magic(their) {
+ log.Fatalln("MAGIC differs:", our, their)
+ }
case "BIN":
our, ok := v.([]byte)
if !ok {
return IntEncode(w, v)
case string:
return StrEncode(w, v)
+ case Magic:
+ return MagicEncode(w, v)
}
vv := reflect.ValueOf(v)
switch reflect.TypeOf(v).Kind() {
iter.tai64ns++
case types.TAI64NA:
iter.tai64nas++
- case types.Bin, types.Str:
+ case types.Bin, types.Str, types.Magic:
iter.strs++
case types.Raw:
iter.raws++
return &iter.ctx.tai64nas[iter.tai64nas]
}
+func (iter *Iterator) Magic() Magic {
+ return Magic(iter.ctx.strs[iter.strs])
+}
+
func (iter *Iterator) Bin() []byte {
return []byte(iter.ctx.strs[iter.strs])
}
--- /dev/null
+// GoKEKS -- Go KEKS codec implementation
+// 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 keks
+
+import "go.cypherpunks.su/keks/types"
+
+// Magic string. Up to 12 bytes.
+type Magic string
+
+var MagicDecodeOpts = DecodeOpts{
+ MaxStrLen: 16,
+ MaxContLen: 0,
+ DisableUTF8Check: true,
+ LeaveTAI64: true,
+}
+
+// Strip off possible Magic atom at the beginning. If not Magic is
+// present, then return empty Magic and data unchanged.
+func StripMagic(data []byte) (Magic, []byte) {
+ d := NewDecoderFromBytes(data, &MagicDecodeOpts)
+ t, err := d.DecodeAtom()
+ if err != nil {
+ return "", data
+ }
+ if t != types.Magic {
+ return "", data
+ }
+ return d.Iter().Magic(), data[d.Read:]
+}
SNTRUP4591761X25519HKDFBLAKE2b = sntrup4591761x25519.SNTRUP4591761X25519HKDFBLAKE2b
BalloonBLAKE2bHKDF = "balloon-blake2b-hkdf"
ChaCha20Poly1305 = "chacha20poly1305"
+
+ EncryptedMagic = "pki/encryptd"
+ HashedMagic = "pki/hashed"
+ PrvKeyMagic = "pki/prvkey"
)
)
const (
- KUCA = "ca" // CA-capable key usage
- KUSig = "sig" // Signing-capable key usage
- KUKEM = "kem" // Key-encapsulation-mechanism key usage
+ KUCA = "ca" // CA-capable key usage
+ KUSig = "sig" // Signing-capable key usage
+ KUKEM = "kem" // Key-encapsulation-mechanism key usage
+ CerMagic = "pki/cer"
)
// Public key.
Pub []Pub `keks:"pub"`
}
-// Parse SignedData contents as CerLoad (certificate) and check its
+// Parse Signed contents as CerLoad (certificate) and check its
// signatures necessary structure. sd.Load.V will hold the CerLoad in
// case of success.
-func (sd *SignedData) CerParse() error {
+func (sd *Signed) CerParse() error {
if sd.Load.T != "cer" {
return errors.New("CerParse: wrong load type")
}
for _, sig := range sd.Sigs {
if sig.TBS.Hashes != nil {
- return errors.New("CerParse: prehashed SignedData")
+ return errors.New("CerParse: prehashed Signed")
}
if sig.TBS.CID == nil {
return errors.New("CerParse: missing cid")
return nil
}
-// Parse KEKS-encoded data as SignedData with the CerLoad (certificate) contents.
-func CerParse(data []byte) (sd *SignedData, err error) {
- sd, err = SignedDataParse(data)
+// Parse KEKS-encoded data as Signed with the CerLoad (certificate) contents.
+func CerParse(data []byte) (sd *Signed, err error) {
+ {
+ var magic keks.Magic
+ magic, data = keks.StripMagic(data)
+ if magic != "" && magic != CerMagic {
+ err = errors.New("wrong magic")
+ return
+ }
+ }
+ sd, err = SignedParse(data)
if err != nil {
return
}
return
}
-// Sign the current SignedData, having CerLoad payload with the provided
+// Sign the current Signed, having CerLoad payload with the provided
// parent's CerLoad and prv key. Certificate's CID will be automatically
// generated UUIDv7. since and till times must not have nanoseconds part.
-func (sd *SignedData) CerIssueWith(
+func (sd *Signed) CerIssueWith(
parent *CerLoad,
prv crypto.Signer,
since, till time.Time,
return
}
-// Verify SignedData CerLoad certificate's signature with provided parent.
+// Verify Signed CerLoad certificate's signature with provided parent.
// Currently only single signature can be verified.
-func (sd *SignedData) CerCheckSignatureFrom(parent *CerLoad) (err error) {
+func (sd *Signed) CerCheckSignatureFrom(parent *CerLoad) (err error) {
if len(sd.Sigs) != 1 {
err = errors.New("can verify only single signature")
return
err = errors.New("sid != parent pub id")
return
}
- tbs := SignedDataTBS{T: sd.Load.T, V: sd.Load.V, TBS: sig.TBS}
+ tbs := SignedTBS{T: sd.Load.T, V: sd.Load.V, TBS: sig.TBS}
buf, err := keks.EncodeBuf(tbs, nil)
if err != nil {
return
return parent.CheckSignature(buf, sig.Sign.V)
}
-// Get CerLoad from SignedData.
-// Returns nil if SignedData does not hold it (or it is not yet parsed).
-func (sd *SignedData) CerLoad() *CerLoad {
+// Get CerLoad from Signed.
+// Returns nil if Signed does not hold it (or it is not yet parsed).
+func (sd *Signed) CerLoad() *CerLoad {
if sd.Load.T != "cer" {
return nil
}
return nil
}
-// Verify sd SignedData CerLoad certificate against cers chain of
+// Verify sd Signed CerLoad certificate against cers chain of
// certificate authority ones at specified point of time t.
-func (sd *SignedData) CerVerify(cers []*SignedData, t time.Time) (err error) {
+func (sd *Signed) CerVerify(cers []*Signed, t time.Time) (err error) {
{
exp := *(sd.Sigs[0].TBS.Exp)
if t.Before(exp[0]) || t.Equal(exp[0]) {
if sid == sd.CerLoad().Pub[0].Id {
return sd.CerCheckSignatureFrom(sd.CerLoad())
}
- idToCer := make(map[uuid.UUID]*SignedData, len(cers))
+ idToCer := make(map[uuid.UUID]*Signed, len(cers))
for _, cer := range cers {
cerLoad := cer.CerLoad()
if !cerLoad.Can(KUSig) || len(cerLoad.Pub) != 1 {
ed25519-blake2b ed25519-blake2b" | while read caAlgo eeAlgo ; do
subj="-subj CN=CA -subj C=RU"
-test_expect_success "$caAlgo: CA generation" "certool \
+test_expect_success "$caAlgo: CA load generation" "certool \
-algo $caAlgo \
-ku ca -ku sig $subj \
-prv $TMPDIR/ca.prv -cer $TMPDIR/ca.cer"
+test_expect_success "$caAlgo: CA generation" "certool \
+ -algo $caAlgo \
+ -ku ca -ku sig $subj \
+ -reuse-key \
+ -prv $TMPDIR/ca.prv -cer $TMPDIR/ca.cer \
+ -ca-prv $TMPDIR/ca.prv -ca-cer $TMPDIR/ca.cer"
test_expect_success "$caAlgo: CA regeneration" "certool \
-algo $caAlgo \
-ku ca -ku sig $subj \
-prv $TMPDIR/ca.prv -cer $TMPDIR/ca.cer \
+ -ca-prv $TMPDIR/ca.prv -ca-cer $TMPDIR/ca.cer \
-reuse-key"
test_expect_success "$caAlgo: CA self-signature" "certool \
-ca-cer $TMPDIR/ca.cer \
package main
import (
+ "bytes"
"crypto"
"errors"
"flag"
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)
till := since.Add(time.Duration(*lifetime) * 24 * time.Hour)
var caPrv crypto.Signer
- var caCers []*pki.SignedData
+ var caCers []*pki.Signed
for _, issuingCer := range issuingCers {
- var sd *pki.SignedData
+ var sd *pki.Signed
sd, err = pki.CerParse(utils.MustReadFile(issuingCer))
if err != nil {
log.Fatal(err)
}
if *verify {
- var sd *pki.SignedData
+ var sd *pki.Signed
sd, err = pki.CerParse(utils.MustReadFile(*cerPath))
if err != nil {
log.Fatal(err)
log.Fatal("no -prv is set")
}
- var prv crypto.Signer
var prvRaw []byte
var pub []byte
if *reuseKey {
- prv, pub, err = pki.PrvParse(utils.MustReadFile(*prvPath))
+ _, pub, err = pki.PrvParse(utils.MustReadFile(*prvPath))
if err != nil {
log.Fatal(err)
}
} else {
switch *algo {
case pki.Ed25519BLAKE2b:
- prv, prvRaw, pub, err = ed25519blake2b.NewKeypair()
+ _, prvRaw, pub, err = ed25519blake2b.NewKeypair()
case pki.GOST3410256A, pki.GOST3410512C:
- prv, prvRaw, pub, err = gost.NewKeypair(*algo)
+ _, prvRaw, pub, err = gost.NewKeypair(*algo)
case pki.SNTRUP4591761X25519:
prvRaw, pub, err = sntrup4591761x25519.NewKeypair()
default:
if err != nil {
log.Fatal(err)
}
- var data []byte
- data, err = keks.EncodeBuf(pki.AV{A: *algo, V: prvRaw}, nil)
- if err != nil {
- log.Fatal(err)
- }
- err = os.WriteFile(*prvPath, data, 0o600)
- if err != nil {
- log.Fatal(err)
+ {
+ var buf bytes.Buffer
+ if _, err = keks.Encode(&buf, keks.Magic(pki.PrvKeyMagic), nil); err != nil {
+ log.Fatal(err)
+ }
+ if _, err = keks.Encode(&buf, pki.AV{A: *algo, V: prvRaw}, nil); err != nil {
+ log.Fatal(err)
+ }
+ err = os.WriteFile(*prvPath, buf.Bytes(), 0o600)
+ if err != nil {
+ log.Fatal(err)
+ }
}
}
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 = &cerLoad
} else {
caCerLoad = caCers[0].CerLoad()
}
- sd := pki.SignedData{Load: pki.SignedDataLoad{T: "cer", V: cerLoad}}
- if prv != nil {
+ sd := pki.Signed{Load: pki.SignedLoad{T: "cer", V: cerLoad}}
+ if caPrv != nil {
if err = sd.CerIssueWith(caCerLoad, caPrv, since, till); err != nil {
log.Fatal(err)
}
}
- if data, err = keks.EncodeBuf(sd, nil); err != nil {
- log.Fatal(err)
- }
- if err = os.WriteFile(*cerPath, data, 0o666); err != nil {
- log.Fatal(err)
+ {
+ var buf bytes.Buffer
+ if _, err = keks.Encode(&buf, keks.Magic(pki.CerMagic), nil); err != nil {
+ log.Fatal(err)
+ }
+ if _, err = keks.Encode(&buf, sd, nil); err != nil {
+ log.Fatal(err)
+ }
+ if err = os.WriteFile(*cerPath, buf.Bytes(), 0o666); err != nil {
+ log.Fatal(err)
+ }
}
}
-// enctool -- dealing with KEKS-encoded encrypted-data utility
+// enctool -- dealing with KEKS-encoded pki-encrypted utility
// Copyright (C) 2024-2025 Sergey Matveev <stargrave@stargrave.org>
//
// This program is free software: you can redistribute it and/or modify
-// enctool -- dealing with KEKS-encoded encrypted-data utility
+// enctool -- dealing with KEKS-encoded pki-encrypted utility
// Copyright (C) 2024-2025 Sergey Matveev <stargrave@stargrave.org>
//
// This program is free software: you can redistribute it and/or modify
"go.cypherpunks.su/keks"
"go.cypherpunks.su/keks/pki"
"go.cypherpunks.su/keks/pki/utils"
+ "go.cypherpunks.su/keks/types"
)
const (
BalloonSaltLen = 8
- BalloonHKDFSalt = "keks/enveloped-data/balloon-blake2b-hkdf"
- SNTRUP4591761X25519Salt = "keks/enveloped-data/sntrup4591761-x25519-hkdf-blake2b"
+ BalloonHKDFSalt = "keks/pki/encrypted/balloon-blake2b-hkdf"
+ SNTRUP4591761X25519Salt = "keks/pki/encrypted/sntrup4591761-x25519-hkdf-blake2b"
)
type BalloonCost struct {
A string `keks:"a"`
}
-type Envelope struct {
+type Encrypted struct {
DEM DEM `keks:"dem"`
KEM []KEM `keks:"kem"`
Bind uuid.UUID `keks:"bind"`
var err error
var cek []byte
if *doDecrypt {
- var envelope Envelope
+ {
+ d := keks.NewDecoderFromReader(os.Stdin, &keks.MagicDecodeOpts)
+ var t types.Type
+ t, err = d.DecodeAtom()
+ if err != nil {
+ log.Fatal(err)
+ }
+ if t != types.Magic {
+ log.Fatal("no magic met")
+ }
+ if d.Iter().Magic() != pki.EncryptedMagic {
+ log.Fatal("wrong magic")
+ }
+ }
+ var encrypted Encrypted
{
d := keks.NewDecoderFromReader(os.Stdin, nil)
- err = d.DecodeStruct(&envelope)
+ err = d.DecodeStruct(&encrypted)
if err != nil {
log.Fatal(err)
}
}
- if envelope.Bind == uuid.Nil {
+ if encrypted.Bind == uuid.Nil {
log.Fatalln("unll bind")
}
- if envelope.DEM.A != pki.ChaCha20Poly1305 {
- log.Fatalln("unsupported DEM:", envelope.DEM.A)
+ if encrypted.DEM.A != pki.ChaCha20Poly1305 {
+ log.Fatalln("unsupported DEM:", encrypted.DEM.A)
}
- if len(envelope.KEM) == 0 {
+ if len(encrypted.KEM) == 0 {
log.Fatalln("no KEMs")
}
- for kemIdx, kem := range envelope.KEM {
+ for kemIdx, kem := range encrypted.KEM {
switch kem.A {
case pki.BalloonBLAKE2bHKDF:
if *passwd == "" {
{
kek := hkdf.Extract(blake2b256, balloon.H(blake2b256,
[]byte(*passwd),
- append(envelope.Bind[:], *kem.Salt...),
+ append(encrypted.Bind[:], *kem.Salt...),
int(kem.Cost.S), int(kem.Cost.T), int(kem.Cost.P),
), []byte(BalloonHKDFSalt))
var cekp []byte
}
{
ikm := bytes.Join([][]byte{
- envelope.Bind[:],
+ encrypted.Bind[:],
*kem.Encap, pubs[prvIdx].V,
keySNTRUP[:], keyX25519,
}, []byte{})
log.Fatal("no KEMs specified")
}
{
- var hdr []byte
- hdr, err = keks.EncodeBuf(&Envelope{
+ var hdr bytes.Buffer
+ if _, err = keks.Encode(&hdr, keks.Magic(pki.EncryptedMagic), nil); err != nil {
+ log.Fatal(err)
+ }
+ if _, err = keks.Encode(&hdr, &Encrypted{
Bind: binding,
KEM: kems,
DEM: DEM{A: pki.ChaCha20Poly1305},
- }, nil)
- if err != nil {
+ }, nil); err != nil {
log.Fatal(err)
}
- if _, err = io.Copy(os.Stdout, bytes.NewReader(hdr)); err != nil {
+ if _, err = io.Copy(os.Stdout, &hdr); err != nil {
log.Fatal(err)
}
}
-// sigtool -- dealing with KEKS-encoded signed-data utility
+// sigtool -- dealing with KEKS-encoded pki-signed utility
// Copyright (C) 2024-2025 Sergey Matveev <stargrave@stargrave.org>
//
// This program is free software: you can redistribute it and/or modify
func main() {
prvPath := flag.String("prv", "", "Path to private key file")
cerPath := flag.String("cer", "", "Path to certificate file")
- sdPath := flag.String("sd", "", "Path to signed-data file")
+ sdPath := flag.String("sd", "", "Path to pki-signed file")
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")
+ encryptedBindingHex := flag.String("encrypted-binding", "", "Set encrypted-binding")
flag.Parse()
log.SetFlags(log.Lshortfile)
- envelopeBinding := uuid.Nil
+ encryptedBinding := uuid.Nil
var err error
- if *envelopeBindingHex != "" {
- envelopeBinding, err = uuid.Parse(*envelopeBindingHex)
+ if *encryptedBindingHex != "" {
+ encryptedBinding, err = uuid.Parse(*encryptedBindingHex)
if err != nil {
log.Fatal(err)
}
log.Fatal(err)
}
if *verify {
- var sd *pki.SignedData
- sd, err = pki.SignedDataParse(utils.MustReadFile(*sdPath))
+ var sd *pki.Signed
+ sd, err = pki.SignedParse(utils.MustReadFile(*sdPath))
if err != nil {
log.Fatal(err)
}
log.Fatal(err)
}
} else {
- var sd pki.SignedData
+ var sd pki.Signed
sd.Load.T = *typ
sdHashes := map[string]*struct{}{*hashAlgo: nil}
sd.Hashes = &sdHashes
Hashes: &sigHashes,
When: &when,
}
- if envelopeBinding != uuid.Nil {
- sigTbs.EnvelopeBinding = &envelopeBinding
+ if encryptedBinding != uuid.Nil {
+ sigTbs.EncryptedBinding = &encryptedBinding
}
err = sd.SignWith(cer.CerLoad(), signer, sigTbs)
if err != nil {
// Parse private key contained in AV KEKS-encoded structure.
func PrvParse(data []byte) (prv crypto.Signer, pub []byte, err error) {
- var av AV
+ {
+ var magic keks.Magic
+ magic, data = keks.StripMagic(data)
+ if magic != "" && magic != PrvKeyMagic {
+ err = errors.New("wrong magic")
+ return
+ }
+ }
d := keks.NewDecoderFromBytes(data, &keks.DecodeOpts{MaxStrLen: 1 << 16})
- err = d.DecodeStruct(&av)
- if err != nil {
+ var av AV
+ if err = d.DecodeStruct(&av); err != nil {
return
}
if len(d.B) != 0 {
"go.cypherpunks.su/keks"
)
-type SignedDataLoad struct {
+const SignedMagic = "pki/signed"
+
+type SignedLoad struct {
V any `keks:"v,omitempty"`
T string `keks:"t"`
}
When *time.Time `keks:"when,omitempty"`
SID uuid.UUID `keks:"sid"`
- EnvelopeBinding *uuid.UUID `keks:"envelope-binding,omitempty"`
+ EncryptedBinding *uuid.UUID `keks:"encrypted-binding,omitempty"`
}
type Sig struct {
Sign AV `keks:"sign"`
}
-type SignedDataTBS struct {
+type SignedTBS struct {
V any `keks:"v"`
T string `keks:"t"`
TBS SigTBS `keks:"tbs"`
}
-type SignedData struct {
+type Signed struct {
Hashes *map[string]*struct{} `keks:"hash,omitempty"`
- Cers *[]*SignedData `keks:"certs,omitempty"`
- Load SignedDataLoad `keks:"load"`
+ Cers *[]*Signed `keks:"certs,omitempty"`
+ Load SignedLoad `keks:"load"`
Sigs []*Sig `keks:"sigs"`
}
-// Validate parsed signed-data structure.
-func SignedDataValidate(sd *SignedData) (err error) {
+// Validate parsed pki-signed structure.
+func SignedValidate(sd *Signed) (err error) {
if sd.Hashes != nil && len(*sd.Hashes) == 0 {
- err = errors.New("SignedDataParse: empty /hash")
+ err = errors.New("SignedParse: empty /hash")
return
}
if sd.Cers != nil {
if len(*sd.Cers) == 0 {
- err = errors.New("SignedDataParse: empty /certs")
+ err = errors.New("SignedParse: empty /certs")
return
}
for _, cer := range *sd.Cers {
}
for _, sig := range sd.Sigs {
if sig.CerLoc != nil && len(*sig.CerLoc) == 0 {
- err = errors.New("SignedDataParse: empty cer-loc")
+ err = errors.New("SignedParse: empty cer-loc")
return
}
if sig.TBS.Hashes != nil && len(*sig.TBS.Hashes) == 0 {
- err = errors.New("SignedDataParse: empty hash")
+ err = errors.New("SignedParse: empty hash")
return
}
if sig.TBS.Exp != nil {
if len(*sig.TBS.Exp) != 2 {
- err = errors.New("SignedDataParse: wrong exp len")
+ err = errors.New("SignedParse: wrong exp len")
return
}
for _, t := range *sig.TBS.Exp {
if t.Nanosecond() != 0 {
- err = errors.New("SignedDataParse: exp with nanoseconds")
+ err = errors.New("SignedParse: exp with nanoseconds")
return
}
}
}
if sd.Hashes != nil {
if sig.TBS.Hashes == nil {
- err = errors.New("SignedDataParse: /sigs: no hash")
+ err = errors.New("SignedParse: /sigs: no hash")
return
}
var exists bool
}
}
if !exists {
- err = errors.New("SignedDataParse: /sigs: no hash")
+ err = errors.New("SignedParse: /sigs: no hash")
return
}
}
return
}
-// Parse signed-data from KEKS-encoded data. This is just a wrapper over
-// SignedDataParseItem.
-func SignedDataParse(data []byte) (*SignedData, error) {
+// Parse pki-signed from KEKS-encoded data. This is just a wrapper over
+// SignedParseItem.
+func SignedParse(data []byte) (*Signed, error) {
+ {
+ var magic keks.Magic
+ magic, data = keks.StripMagic(data)
+ if magic != "" && magic != SignedMagic {
+ return nil, errors.New("wrong magic")
+ }
+ }
d := keks.NewDecoderFromBytes(data, nil)
- var sd SignedData
+ var sd Signed
err := d.DecodeStruct(&sd)
if err != nil {
return nil, err
}
- err = SignedDataValidate(&sd)
+ err = SignedValidate(&sd)
return &sd, err
}
-// Sign SignedData's contents and sigTBS corresponding data with the
+// Sign Signed's contents and sigTBS corresponding data with the
// provided prv signer, having parent certificate. Signature is appended
// to the sd.Sigs. parent certificate must have "sig" key-usage.
-func (sd *SignedData) SignWith(
+func (sd *Signed) SignWith(
parent *CerLoad,
prv crypto.Signer,
sigTBS SigTBS,
return errors.New("parent can not sign")
}
sigTBS.SID = parent.Pub[0].Id
- sdTBS := SignedDataTBS{T: sd.Load.T, V: sd.Load.V, TBS: sigTBS}
+ sdTBS := SignedTBS{T: sd.Load.T, V: sd.Load.V, TBS: sigTBS}
sig := Sig{TBS: sigTBS}
sig.Sign.A = parent.Pub[0].A
var buf []byte
AtomTAI64 AtomType = 0x18
AtomTAI64N AtomType = 0x19
AtomTAI64NA AtomType = 0x1A
+ AtomMagic AtomType = 0x4B
AtomStrings = 0x80
AtomIsUTF8 = 0x40
TAI64
TAI64N
TAI64NA
+ Magic
Bin
Str
Raw
_ = x[TAI64-12]
_ = x[TAI64N-13]
_ = x[TAI64NA-14]
- _ = x[Bin-15]
- _ = x[Str-16]
- _ = x[Raw-17]
+ _ = x[Magic-15]
+ _ = x[Bin-16]
+ _ = x[Str-17]
+ _ = x[Raw-18]
}
-const _Type_name = "InvalidEOCNILBoolUUIDUIntIntBigIntListMapBlobFloatTAI64TAI64NTAI64NABinStrRaw"
+const _Type_name = "InvalidEOCNILBoolUUIDUIntIntBigIntListMapBlobFloatTAI64TAI64NTAI64NAMagicBinStrRaw"
-var _Type_index = [...]uint8{0, 7, 10, 13, 17, 21, 25, 28, 34, 38, 41, 45, 50, 55, 61, 68, 71, 74, 77}
+var _Type_index = [...]uint8{0, 7, 10, 13, 17, 21, 25, 28, 34, 38, 41, 45, 50, 55, 61, 68, 73, 76, 79, 82}
func (i Type) String() string {
if i >= Type(len(_Type_index)-1) {
return toUTC(t.Time())
case types.TAI64NA:
return iter.TAI64NA(), nil
+ case types.Magic:
+ return iter.Magic(), nil
case types.Bin:
return iter.Bin(), nil
case types.Str:
TagTAI64 = 0x18
TagTAI64N = 0x19
TagTAI64NA = 0x1A
+TagMagic = 0x4B
TagStr = 0x80
TagUTF8 = 0x40
TagNIntb = _byte(TagNInt)
TagTAI64b = _byte(TagTAI64)
TagTAI64Nb = _byte(TagTAI64N)
+TagMagicb = _byte(TagMagic)
class DecodeError(ValueError):
return "Raw(%s)" % self.v.hex()
+class Magic:
+ __slots__ = ("v",)
+
+ def __init__(self, v: bytes):
+ if len(v) > 12:
+ raise ValueError("too long")
+ self.v = v
+
+ def __bytes__(self) -> bytes:
+ return self.v
+
+ def __eq__(self, other) -> bool:
+ if not isinstance(other, self.__class__):
+ return False
+ return self.v == other.v
+
+ def __repr__(self) -> str:
+ return "Magic(%r)" % self.v
+
+
Blob = namedtuple("Blob", ("l", "v"))
append(dumps(v[k]))
append(TagEOCb)
return b"".join(raws)
+ if isinstance(v, Magic):
+ return b"".join((b"KEKS", v.v, b"\x00" * (12 - len(v.v))))
raise NotImplementedError("unsupported type")
else:
raise DecodeError("wrong chunk len")
return Blob(l, b"".join(raws)), v
+ if b == TagMagic:
+ if len(v) < 16:
+ raise NotEnoughData(16-len(v))
+ if v[:4] != b"KEKS":
+ raise DecodeError("wrong magic")
+ return Magic(v[4:16].rstrip(b"\x00")), v[16:]
raise DecodeError("unknown tag")
parser.add_argument("file", type=FileType("rb"))
args = parser.parse_args()
data = args.file.read()
- data, tail = loads(
- data,
- sets=not args.nosets,
- leapsecUTCAllow=args.leapsec_utc_allow is True,
- )
+ obj = Magic(b"")
from pprint import pprint
- pprint(data)
- if tail != b"":
- print("tail:", tail.hex())
+ while isinstance(obj, Magic):
+ obj, data = loads(
+ data,
+ sets=not args.nosets,
+ leapsecUTCAllow=args.leapsec_utc_allow is True,
+ )
+ pprint(obj)
+ if data != b"":
+ print("tail:", data.hex())
keks.Raw(keks._byte(keks.TagTAI64N) + bytes.fromhex("40000000499602F40006F855")),
keks.Raw(keks._byte(keks.TagTAI64NA) + bytes.fromhex("40000000499602F40006F855075BCD15")),
]
-raw = keks.dumps(data)
+raw = keks.dumps(keks.Magic(b"test-vector")) + keks.dumps(data)
dec, tail = keks.loads(raw)
+assert dec == keks.Magic(b"test-vector")
+dec, tail = keks.loads(tail)
assert tail == b""
-assert keks.dumps(dec) == raw
+assert keks.dumps(keks.Magic(b"test-vector")) + keks.dumps(dec) == raw
assert dec == data
print(raw.hex())
from keks import _byte
from keks import Blob
+from keks import Magic
from keks import Raw
from keks import TagTAI64NA
from keks import TAI64Base
tai64na_st = tuples(integers(1, 100), integers(1, 100)).map(lambda x: Raw(
TagTAI64NAb + (TAI64Base + x[0]).to_bytes(8, "big") + (x[1] + 1).to_bytes(8, "big")
))
+magic_st = binary(min_size=0, max_size=12).filter(lambda x: b"\x00" not in x).map(Magic)
any_st = one_of(
booleans(),
integers(),
datetimes(),
blobs_st,
tai64na_st,
+ magic_st,
)
everything_st = deferred(
lambda: any_st |
--- /dev/null
+# PyKEKS -- Python KEKS implementation
+# 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/>.
+
+from unittest import TestCase
+
+from hypothesis import given
+from hypothesis.strategies import integers
+
+from keks import DecodeError
+from keks import dumps
+from keks import loads
+from keks import Magic
+from keks import NotEnoughData
+from tests.strategies import junk_st
+from tests.strategies import magic_st
+
+
+class TestMagic(TestCase):
+ def test_test_vector(self) -> None:
+ self.assertSequenceEqual(
+ dumps(Magic(b"test-vector")),
+ b"KEKS" + b"test-vector" + b"\x00",
+ )
+
+ def test_empty(self) -> None:
+ self.assertSequenceEqual(dumps(Magic(b"")), b"KEKS" + 12 * b"\x00")
+
+ @given(junk_st, magic_st)
+ def test_symmetric(self, junk: bytes, magic: Magic) -> None:
+ got, tail = loads(dumps(magic) + junk)
+ self.assertEqual(got, magic)
+ self.assertSequenceEqual(tail, junk)
+
+ def test_raises_if_too_long(self) -> None:
+ with self.assertRaises(ValueError):
+ Magic(b"i-am-too-long-string")
+
+ @given(integers(min_value=1, max_value=4))
+ def test_not_enough_data(self, striplen: int) -> None:
+ raw = dumps(Magic(b"short-one"))
+ with self.assertRaises(NotEnoughData) as err:
+ loads(raw[:-striplen])
+ self.assertEqual(err.exception.n, striplen)
+
+ def test_bad_magic(self) -> None:
+ with self.assertRaises(DecodeError) as err:
+ loads(b"KEKs" + 12 * b"\x00")
+ self.assertEqual(str(err.exception), "wrong magic")
# * UTC xxxx-xx-xxTxx:xx:xxZ -- TAI64-encoded UTC is expected
# * UTC xxxx-xx-xxTxx:xx:xx.xxxxxxZ -- TAI64N-encoded UTC is expected
# * TAI64NA HEX(...) -- external TAI64NA-encoded time
+# * MAGIC HEX(...) -- Magic is expected
# * BIN HEX(...) -- BIN is expected
# * STR HEX(...) -- STR is expected
# * INT DEC(...) -- ±INT is expected
from uuid import UUID
from keks import Blob
+from keks import Magic
from keks import Raw
from keks import TagTAI64NA
if v[0] != TagTAI64NA:
raise NotImplementedError("unsupported Raw type")
print("TAI64NA " + v[1:].hex())
+ elif isinstance(v, Magic):
+ print("MAGIC " + v.v.hex())
else:
raise NotImplementedError("unsupported type")
@item 025 @tab 19 @tab @code{00011001} @tab 12 @tab @ref{TAI64, TAI64N}
@item 026 @tab 1A @tab @code{00011010} @tab 16 @tab @ref{TAI64, TAI64NA}
@item [...]
+@item 075 @tab 4B @tab @code{01001011} @tab 15 @tab @ref{Magic, MAGIC}
+@item [...]
@item 128 @tab 80 @tab @code{10LLLLLL} @tab 0 @tab @ref{Strings, BIN(len=0)}
@item [...]
@item 188 @tab BC @tab @code{10111100} @tab 60 @tab @ref{Strings, BIN(len=60)}
@include encoding/int.texi
@include encoding/float.texi
@include encoding/tai64.texi
+@include encoding/magic.texi
@include encoding/cont.texi
@include encoding/blob.texi
--- /dev/null
+@node Magic
+@cindex MAGIC
+@section Magic
+
+MAGIC is an atom holding magic number/string aimed to identify the data
+following it. It is a concatenation of ASCII("KEKS") and 12-byte magic,
+padded with zeros if necessary.
+It is intended to be prepended to the KEKS-encoded data in files.
+
+@multitable @columnfractions .5 .5
+
+@item MAGIC(pki/cer) @tab @code{4B454B53 706B692F636572 0000000000}
+@item MAGIC(pki/signed) @tab @code{4B454B53 706B692F7369676E6564 0000}
+@item MAGIC(pki/encryptd) @tab @code{4B454B53 706B692F656E637279707464}
+
+@end multitable
@item 072 @tab 48 @tab @code{01001000} @tab 0 @tab
@item 073 @tab 49 @tab @code{01001001} @tab 0 @tab
@item 074 @tab 4A @tab @code{01001010} @tab 0 @tab
-@item 075 @tab 4B @tab @code{01001011} @tab 0 @tab
+@item 075 @tab 4B @tab @code{01001011} @tab 15 @tab @ref{Magic, MAGIC}
@item 076 @tab 4C @tab @code{01001100} @tab 0 @tab
@item 077 @tab 4D @tab @code{01001101} @tab 0 @tab
@item 078 @tab 4E @tab @code{01001110} @tab 0 @tab
@cindex cer
@section cer format
-Certificate is the @code{@ref{signed-data}} structure.
+Certificate is the @code{@ref{pki-signed}} structure.
+Stored in a file, it should start with "pki/cer" @ref{Magic, magic}.
+
Its @code{/load/t} equals to @code{cer}.
@code{/load/v} contains @code{cer-load}:
@end table
-signed-data's sig-tbs @strong{must} contain additional fields:
+@code{pki-signed}'s @code{sig-tbs} @strong{must} contain additional fields:
@verbatiminclude format/cer-sig-tbs.cddl
@subsection cer with GOST R 34.10-2012
Same rules of serialisation must be used as with
-@code{@ref{signed-data-gost3410}}. Public key's
+@code{@ref{pki-signed-gost3410}}. Public key's
identifier and @code{cid} should be calculated
using big-endian Streebog-256 hash.
@subsection cer with Ed25519-BLAKE2b
Same calculation and serialisation rules must be used as with
-@code{@ref{signed-data-ed25519-blake2b}}.
+@code{@ref{pki-signed-ed25519-blake2b}}.
Public key's identifier and @code{cid} should be calculated
using BLAKE2b hash with 128 or 256 bit output length specified.
ai = text ; algorithm identifier
-enveloped-data = {
+pki-encrypted = {
dem: dem,
kem: [+ kem],
bind: uuid,
-@node enveloped-data
-@cindex enveloped-data
-@section enveloped-data format
+@node pki-encrypted
+@cindex pki-encrypted
+@section pki-encrypted format
-Enveloped data is an encrypted data.
+Stored in a file, it should start with "pki/encryptd" @ref{Magic, magic}.
-@verbatiminclude format/enveloped-data.cddl
+@verbatiminclude format/encrypted.cddl
@code{/ciphertext} contains the ciphertext. It is encrypted with random
"content encryption key" (CEK) with an algorithm specified in
@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.
+for example just following the @code{pki-encrypted} structure.
CEK is encapsulated in @code{/kem/*} entries (key encapsulation
mechanism), using @code{/kem/*/a} algorithm. @code{/kem/*/cek} field
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.
+@code{/bind} value can be used to bind the encrypted
+@code{@ref{pki-signed, pki-signed}} to the envelope.
Either UUIDv4 or UUIDv7 are recommended.
-@node enveloped-data-chacha20poly1305
-@subsection enveloped-data with ChaCha20-Poly1305 DEM
+@node pki-encrypted-chacha20poly1305
+@subsection pki-encrypted with ChaCha20-Poly1305 DEM
@code{/dem/a} equals to "chacha20poly1305". Data is split on 64 KiB
chunks which are encrypted the following way:
@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
+@node pki-encrypted-kuznechik-ctracpkm-hmac-hkdf
+@subsection pki-encrypted-kuznechik-ctracpkm-hmac-hkdf
@code{/dem/a} equals to "kuznechik-ctracpkm-hmac-hkdf".
@code{/dem/seed} contains 16 bytes for the HKDF invocation below.
@verbatim
Kenc, Kauth = HKDF-Extract(Streebog-512,
- salt="keks/enveloped-data/kuznechik-ctracpkm-hmac-hkdf",
+ salt="keks/pki/encrypted/kuznechik-ctracpkm-hmac-hkdf",
secret=seed || CEK)
@end verbatim
@code{/ciphertext}'s chunk length equals to 64KiB bytes.
-@node enveloped-data-balloon-blake2b-hkdf
-@subsection enveloped-data with Balloon-BLAKE2b+HKDF-BLAKE2b KEM
+@node pki-encrypted-balloon-blake2b-hkdf
+@subsection pki-encrypted with Balloon-BLAKE2b+HKDF-BLAKE2b KEM
@code{/kem/*/a} equals to "balloon-blake2b-hkdf".
Recipient map must also contain additional fields:
@verbatim
KEK = HKDF-Extract(BLAKE2b-256,
- salt="keks/enveloped-data/balloon-blake2b-hkdf",
+ salt="keks/pki/encrypted/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-hkdf-kexp15
-@subsection enveloped-data-gost3410-hkdf-kexp15
+@node pki-encrypted-gost3410-hkdf-kexp15
+@subsection pki-encrypted-gost3410-hkdf-kexp15
@code{/kem/*/a} equals to "gost3410-hkdf-kexp15".
Recipient map must also contain additional fields:
@verbatim
KEKenv, KEKauth = HKDF-Extract(Streebog-512,
- salt="keks/enveloped-data/gost3410-hkdf-kexp15",
+ salt="keks/pki/encrypted/gost3410-hkdf-kexp15",
secret=bind || VKO(...))
KExp15(KEKenc, KEKauth, 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
+@node pki-encrypted-sntrup4591761-x25519-hkdf-blake2b
+@subsection pki-encrypted with SNTRUP4591761+x25519+HKDF-BLAKE2b KEM
@code{/kem/*/a} equals to "sntrup4591761-x25519-hkdf-blake2b".
Recipient certificate with
@verbatim
KEK = HKDF-Extract(BLAKE2b-256,
- salt="keks/enveloped-data/sntrup4591761-x25519-hkdf-blake2b",
+ salt="keks/pki/encrypted/sntrup4591761-x25519-hkdf-blake2b",
secret=bind ||
sntrup4591761-sender-ciphertext ||
x25519-sender-public-key ||
ai = text ; algorithm identifier
-hashed-data = {
+pki-hashed = {
a: [+ ai],
t: text, ; type of the content
v: bytes / text / blob / map / list, ; content itself
-@node hashed-data
-@cindex hashed-data
-@section hashed-data format
+@node pki-hashed
+@cindex pki-hashed
+@section pki-hashed format
Integrity protected container, CMS'es DigestedData analogue.
+Stored in a file, it should start with "pki/hashed" @ref{Magic, magic}.
-@verbatiminclude format/hashed-data.cddl
+@verbatiminclude format/hashed.cddl
@code{/a} tells what algorithms will be used to hash the data.
@code{/hash} contains the hash values for all corresponding @code{/a}
algorithms.
-@node hashed-data-blake2b
-@subsection hashed-data with BLAKE2b
+@node pki-hashed-blake2b
+@subsection pki-hashed with BLAKE2b
@url{https://www.blake2.net/, BLAKE2b} with
512-bit output has @code{blake2b} algorithm identifier.
256-bit output has @code{blake2b256} algorithm identifier.
-@node hashed-data-blake3
-@subsection hashed-data with BLAKE3
+@node pki-hashed-blake3
+@subsection pki-hashed with BLAKE3
@url{https://github.com/BLAKE3-team/BLAKE3/, BLAKE3} with fixed
256-bit output has @code{blake3} algorithm identifier.
-@node hashed-data-sha2
-@subsection hashed-data with SHA2
+@node pki-hashed-sha2
+@subsection pki-hashed with SHA2
SHA2-256 has @code{sha2-256} algorithm identifier.
SHA2-512 has @code{sha2-512} algorithm identifier.
-@node hashed-data-shake
-@subsection hashed-data with SHAKE
+@node pki-hashed-shake
+@subsection pki-hashed with SHAKE
@url{https://keccak.team/, SHAKE} XOF function with fixed
256 (SHAKE128) or 512 (SHAKE256) bit output.
Following algorithm identifiers are acceptable:
@code{shake128}, @code{shake256}.
-@node hashed-data-skein512
-@subsection hashed-data with Skein-512
+@node pki-hashed-skein512
+@subsection pki-hashed with Skein-512
512-bit @url{https://www.schneier.com/academic/skein/, Skein-512} hash.
@code{skein512} is acceptable algorithm identifier.
-@node hashed-data-gost3411
-@subsection hashed-data with GOST R 34.11-2012
+@node pki-hashed-gost3411
+@subsection pki-hashed with GOST R 34.11-2012
Streebog must be big-endian serialised.
Following algorithm identifiers are acceptable:
@code{streebog256}, @code{streebog512}.
-@node hashed-data-xxh3-128
-@subsection hashed-data with XXH3-128
+@node pki-hashed-xxh3-128
+@subsection pki-hashed with XXH3-128
128-bit @url{https://xxhash.com/, XXH3} hash must be big-endian encoded.
@url{https://datatracker.ietf.org/doc/html/rfc8610, CDDL}-like format.
@include format/private-key.texi
-@include format/signed-data.texi
+@include format/signed.texi
@include format/cer.texi
-@include format/hashed-data.texi
-@include format/enveloped-data.texi
+@include format/hashed.texi
+@include format/encrypted.texi
@include format/registry.texi
@verbatiminclude format/private-key.cddl
+Stored in a file, it should start with "pki/prvkey" @ref{Magic, magic}.
+
@node private-key-gost3410
@subsection private-key with GOST R 34.10-2012
@table @code
@item blake2b, blake2b256
@code{@ref{cer-ed25519-blake2b}},
- @code{@ref{hashed-data-blake2b}},
- @code{@ref{signed-data-ed25519-blake2b}}
+ @code{@ref{pki-hashed-blake2b}},
+ @code{@ref{pki-signed-ed25519-blake2b}}
@item blake3
- @code{@ref{hashed-data-blake3}}
+ @code{@ref{pki-hashed-blake3}}
@item sha2-256, sha2-512
- @code{@ref{hashed-data-sha2}}
+ @code{@ref{pki-hashed-sha2}}
@item shake128, shake256
- @code{@ref{hashed-data-shake}}
+ @code{@ref{pki-hashed-shake}}
@item skein512
- @code{@ref{hashed-data-skein512}}
+ @code{@ref{pki-hashed-skein512}}
@item streebog256, streebog512
@code{@ref{cer-gost3410}},
- @code{@ref{hashed-data-gost3411}},
- @code{@ref{signed-data-gost3410}}
+ @code{@ref{pki-hashed-gost3411}},
+ @code{@ref{pki-signed-gost3410}}
@item xxh3-128
- @code{@ref{hashed-data-xxh3-128}}
+ @code{@ref{pki-hashed-xxh3-128}}
@end table
@node AI DH
@table @code
@item chacha20poly1305
- @code{@ref{enveloped-data-chacha20poly1305}}
+ @code{@ref{pki-encrypted-chacha20poly1305}}
@item kuznechik-ctracpkm-hmac-hkdf
- @code{@ref{enveloped-data-kuznechik-ctracpkm-hmac-hkdf}}
+ @code{@ref{pki-encrypted-kuznechik-ctracpkm-hmac-hkdf}}
@end table
@node AI KEM
@table @code
@item argon2id-hkdf-blake2b
@item balloon-blake2b-hkdf
- @code{@ref{enveloped-data-balloon-blake2b-hkdf}}
+ @code{@ref{pki-encrypted-balloon-blake2b-hkdf}}
@item gost3410-hkdf-kexp15
- @code{@ref{enveloped-data-gost3410-hkdf-kexp15}}
+ @code{@ref{pki-encrypted-gost3410-hkdf-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}}
+ @code{@ref{pki-encrypted-sntrup4591761-x25519-hkdf-blake2b}}
@item sntrup761-x25519-hkdf-blake2b
@end table
@item ecdsa-nist256p, ecdsa-nist521p
@item ed25519-blake2b
@code{@ref{private-key-ed25519-blake2b}}
- @code{@ref{signed-data-ed25519-blake2b}},
+ @code{@ref{pki-signed-ed25519-blake2b}},
@code{@ref{cer-ed25519-blake2b}}
@item ed448
@item gost3410-256A, gost3410-512C
@code{@ref{cer-gost3410}},
@code{@ref{private-key-gost3410}},
- @code{@ref{signed-data-gost3410}}
+ @code{@ref{pki-signed-gost3410}}
@end table
@node AI Content types
@itemize
@item @ref{cer, @code{cer}}
-@item @ref{signed-data, @code{data}}
+@item @ref{pki-signed, @code{data}}
@item @ref{private-key, @code{prv}}
@end itemize
-signed-data-tbs = {
+pki-signed-tbs = {
t: text, ; = /load/t
v: nil / any,
tbs: map, ; = /sigs/?/tbs
ai = text ; algorithm identifier
av = {a: ai, v: bytes}
-signed-data = {
+pki-signed = {
? hash: set, ; when using prehashing
load: {
t: text,
? certs: [+ cer],
}
-cer = signed-data ; with /load/t = "cer", /load/v = cer-load
+cer = pki-signed ; with /load/t = "cer", /load/v = cer-load
sig = {
tbs: sig-tbs,
-@node signed-data
-@cindex signed-data
-@section signed-data format
+@node pki-signed
+@cindex pki-signed
+@section pki-signed format
That resembles @url{https://datatracker.ietf.org/doc/html/rfc5652, CMS}
(PKCS#7) ASN.1-based format.
-@verbatiminclude format/signed-data.cddl
+Stored in a file, it should start with "pki/signed" @ref{Magic, magic},
+unless this is a @ref{cer, certificate}.
+
+@verbatiminclude format/signed.cddl
Signature is created by signing the following encoded MAP:
-@verbatiminclude format/signed-data-tbs.cddl
+@verbatiminclude format/signed-tbs.cddl
If prehashing is used, then @code{/hash} tells what algorithms will be
used to hash the data of @code{/load/v}. If @code{/load/v} is either a
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.
+If signed data is also intended to be @ref{pki-encrypted, encrypted},
+then @code{/sigs/*/tbs/encrypted-binding} should be set to
+envelope's @code{/bind} value.
-@node signed-data-gost3410
-@subsection signed-data with GOST R 34.10-2012
+@node pki-signed-gost3410
+@subsection pki-signed with GOST R 34.10-2012
GOST R 34.10-2012 must be used with Streebog (GOST R 34.11-2012) hash
function. Its digest must be big-endian serialised. Public key must be
Following algorithm identifiers are acceptable for the public key and
signature: @code{gost3410-256A}, @code{gost3410-512C}.
-@node signed-data-ed25519-blake2b
-@subsection signed-data with Ed25519-BLAKE2b
+@node pki-signed-ed25519-blake2b
+@subsection pki-signed with Ed25519-BLAKE2b
@url{https://datatracker.ietf.org/doc/html/rfc8032, EdDSA} with
Edwards25519 is used similarly as in RFC 8032.
proc UUID {v} {
set v [binary decode hex [string map {- ""} $v]]
- if {[string length $v] != 16} { error "bad UUID len" }
+ if {[string length $v] != 16} { error "bad len" }
char [expr 0x04]
add $v
}
+proc MAGIC {v} {
+ set l [string length $v]
+ if {$l > 12} { error "too long" }
+ add "KEKS"
+ add $v
+ add [string repeat [binary format c 0] [expr {12 - $l}]]
+}
+
proc toBEbin {l v} {
set a [list]
for {set i 0} {$i < $l} {incr i} {
add $v
}
-namespace export EOC NIL FALSE TRUE UUID INT STR BIN RAW
+namespace export EOC NIL FALSE TRUE UUID MAGIC INT STR BIN RAW
namespace export TAI64 UTCFromISO
namespace export LIST MAP SET LenFirstSort BLOB
dump 'RAW [expr 0x18] [binary decode hex "40000000586846A4"]' >tai-leap
dump 'TAI64 1234 1234' >tai-ns
dump 'TAI64 1234 1234 1234' >tai-as
+dump "MAGIC fuzz" >magic
source keks.tcl
namespace import KEKS::*
+MAGIC test-vector
MAP {
ints {MAP {
pos {LIST {