From de5b7de74a76cdde6985822f7b3c8a907860cb617a307c49106de4372547d337 Mon Sep 17 00:00:00 2001 From: Sergey Matveev Date: Wed, 22 Jan 2025 15:07:32 +0300 Subject: [PATCH] Add Magic --- c/cmd/deatomiser/deatomiser.c | 12 ++++ c/cmd/pp/pp.c | 23 +++++++ c/cmd/test-vector/test-vector.c | 6 ++ c/doc/atom.texi | 1 + c/lib/atom.h | 4 +- c/lib/dec.c | 20 ++++++ c/lib/enc.c | 23 +++++++ c/lib/enc.h | 16 +++++ c/lib/err.c | 2 + c/lib/err.h | 3 + c/lib/items.c | 4 ++ go/atom-decode.go | 13 ++++ go/atom-encode.go | 15 +++++ go/atomtype_string.go | 4 ++ go/cmd/pp/main.go | 36 +++++++--- go/cmd/test-vector-anys/main.go | 6 +- go/cmd/test-vector-manual/main.go | 1 + go/cmd/textdump-tester/main.go | 12 ++++ go/encode.go | 2 + go/iter.go | 6 +- go/magic.go | 42 ++++++++++++ go/pki/algo.go | 4 ++ go/pki/cer.go | 49 ++++++++------ go/pki/cmd/certool/basic.t | 9 ++- go/pki/cmd/certool/main.go | 66 +++++++++---------- go/pki/cmd/enctool/chapoly.go | 2 +- go/pki/cmd/enctool/main.go | 53 ++++++++++----- go/pki/cmd/sigtool/main.go | 22 +++---- go/pki/prv.go | 13 +++- go/pki/{signed-data.go => signed.go} | 57 +++++++++------- go/type.go | 1 + go/types/type.go | 1 + go/types/type_string.go | 11 ++-- go/unmarshal.go | 2 + py3/keks.py | 48 +++++++++++--- py3/test-vector.py | 6 +- py3/tests/strategies.py | 3 + py3/tests/test_magic.py | 60 +++++++++++++++++ py3/tests/textdump-tester | 4 ++ spec/encoding/index.texi | 3 + spec/encoding/magic.texi | 16 +++++ spec/encoding/table.texi | 2 +- spec/format/cer.texi | 10 +-- .../{enveloped-data.cddl => encrypted.cddl} | 2 +- .../{enveloped-data.texi => encrypted.texi} | 44 ++++++------- spec/format/{hashed-data.cddl => hashed.cddl} | 2 +- spec/format/{hashed-data.texi => hashed.texi} | 37 ++++++----- spec/format/index.texi | 6 +- spec/format/private-key.texi | 2 + spec/format/registry.texi | 34 +++++----- .../{signed-data-tbs.cddl => signed-tbs.cddl} | 2 +- spec/format/{signed-data.cddl => signed.cddl} | 4 +- spec/format/{signed-data.texi => signed.texi} | 27 ++++---- tcl/keks.tcl | 12 +++- tcl/mk-fuzz-inputs | 1 + tcl/test-vector.tcl | 1 + 56 files changed, 643 insertions(+), 224 deletions(-) create mode 100644 go/magic.go rename go/pki/{signed-data.go => signed.go} (70%) create mode 100644 py3/tests/test_magic.py create mode 100644 spec/encoding/magic.texi rename spec/format/{enveloped-data.cddl => encrypted.cddl} (98%) rename spec/format/{enveloped-data.texi => encrypted.texi} (81%) rename spec/format/{hashed-data.cddl => hashed.cddl} (91%) rename spec/format/{hashed-data.texi => hashed.texi} (71%) rename spec/format/{signed-data-tbs.cddl => signed-tbs.cddl} (78%) rename spec/format/{signed-data.cddl => signed.cddl} (86%) rename spec/format/{signed-data.texi => signed.texi} (76%) diff --git a/c/cmd/deatomiser/deatomiser.c b/c/cmd/deatomiser/deatomiser.c index 50c3966..6e6c5a8 100644 --- a/c/cmd/deatomiser/deatomiser.c +++ b/c/cmd/deatomiser/deatomiser.c @@ -13,6 +13,7 @@ // You should have received a copy of the GNU Lesser General Public // License along with this program. If not, see . +#include #include #include #include @@ -79,6 +80,17 @@ main(int argc, char **argv) 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; diff --git a/c/cmd/pp/pp.c b/c/cmd/pp/pp.c index 25763e8..717893b 100644 --- a/c/cmd/pp/pp.c +++ b/c/cmd/pp/pp.c @@ -14,6 +14,7 @@ // License along with this program. If not, see . #include +#include #include #include #include @@ -112,6 +113,17 @@ printer( // NOLINT(misc-no-recursion) 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; @@ -341,6 +353,7 @@ main(int argc, char **argv) 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)); @@ -356,6 +369,9 @@ main(int argc, char **argv) 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); @@ -363,6 +379,13 @@ main(int argc, char **argv) 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( diff --git a/c/cmd/test-vector/test-vector.c b/c/cmd/test-vector/test-vector.c index d8251ed..d16c141 100644 --- a/c/cmd/test-vector/test-vector.c +++ b/c/cmd/test-vector/test-vector.c @@ -29,6 +29,12 @@ main(void) 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( diff --git a/c/doc/atom.texi b/c/doc/atom.texi index 22718fa..51ba86c 100644 --- a/c/doc/atom.texi +++ b/c/doc/atom.texi @@ -15,6 +15,7 @@ @DOCSTRING KEKSAtomNILEncode@ @DOCSTRING KEKSAtomBoolEncode@ @DOCSTRING KEKSAtomUUIDEncode@ +@DOCSTRING KEKSAtomMagicEncode@ @DOCSTRING KEKSAtomUintEncode@ @DOCSTRING KEKSAtomSintEncode@ @DOCSTRING KEKSAtomListEncode@ diff --git a/c/lib/atom.h b/c/lib/atom.h index f747b18..a0f91f5 100644 --- a/c/lib/atom.h +++ b/c/lib/atom.h @@ -23,6 +23,7 @@ enum KEKSAtomType { KEKSAtomTAI64 = 0x18, KEKSAtomTAI64N = 0x19, KEKSAtomTAI64NA = 0x1A, + KEKSAtomMagic = 0x4B, KEKSAtomStrings = 0x80, KEKSAtomIsUTF8 = 0x40, @@ -64,6 +65,7 @@ enum KEKSItemType { KEKSItemBlob, KEKSItemFloat, KEKSItemTAI64, + KEKSItemMagic, KEKSItemBin, KEKSItemStr, KEKSItemRaw, @@ -94,7 +96,7 @@ enum KEKSItemType { // @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 diff --git a/c/lib/dec.c b/c/lib/dec.c index b844af3..93a2f2a 100644 --- a/c/lib/dec.c +++ b/c/lib/dec.c @@ -117,6 +117,26 @@ KEKSAtomDecode( // NOLINT(misc-no-recursion) } 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; diff --git a/c/lib/enc.c b/c/lib/enc.c index f18a92b..ff30afc 100644 --- a/c/lib/enc.c +++ b/c/lib/enc.c @@ -71,6 +71,29 @@ KEKSAtomUUIDEncode( 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) { diff --git a/c/lib/enc.h b/c/lib/enc.h index 3527c8f..1256646 100644 --- a/c/lib/enc.h +++ b/c/lib/enc.h @@ -49,6 +49,22 @@ KEKSAtomUUIDEncode( 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) diff --git a/c/lib/err.c b/c/lib/err.c index 51fe649..c4b1b75 100644 --- a/c/lib/err.c +++ b/c/lib/err.c @@ -48,6 +48,8 @@ KEKSErr2Str(const enum KEKSErr err) return "DeepRecursion"; case KEKSErrUnexpectedEOC: return "UnexpectedEOC"; + case KEKSErrBadMagic: + return "BadMagic"; default: return "unknown"; } diff --git a/c/lib/err.h b/c/lib/err.h index c6d59e6..f3b60f6 100644 --- a/c/lib/err.h +++ b/c/lib/err.h @@ -41,6 +41,8 @@ // Too deep recursion involved during parsing. // @item KEKSErrUnexpectedEOC // Unexpected EOC met. +// @item KEKSErrBadMagic +// Wrong magic value. // @end table // @end deftp enum KEKSErr { @@ -66,6 +68,7 @@ enum KEKSErr { KEKSErrUnsatisfiedSchema, KEKSErrDeepRecursion, KEKSErrUnexpectedEOC, + KEKSErrBadMagic, }; // TEXINFO: KEKSErr2Str diff --git a/c/lib/items.c b/c/lib/items.c index 763b944..4483c35 100644 --- a/c/lib/items.c +++ b/c/lib/items.c @@ -308,6 +308,10 @@ KEKSItemsEncode( // NOLINT(misc-no-recursion) 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; diff --git a/go/atom-decode.go b/go/atom-decode.go index 17e15f3..7bfd0a3 100644 --- a/go/atom-decode.go +++ b/go/atom-decode.go @@ -34,6 +34,7 @@ var ( 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) { @@ -243,6 +244,18 @@ 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 diff --git a/go/atom-encode.go b/go/atom-encode.go index d69a46e..88d818c 100644 --- a/go/atom-encode.go +++ b/go/atom-encode.go @@ -17,6 +17,7 @@ package keks import ( "bytes" + "errors" "io" "math/big" @@ -54,6 +55,20 @@ func UUIDEncode(w io.Writer, v *uuid.UUID) (written int64, err error) { 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{}) diff --git a/go/atomtype_string.go b/go/atomtype_string.go index 8a29a79..dfb719f 100644 --- a/go/atomtype_string.go +++ b/go/atomtype_string.go @@ -26,6 +26,7 @@ func _() { _ = x[AtomTAI64-24] _ = x[AtomTAI64N-25] _ = x[AtomTAI64NA-26] + _ = x[AtomMagic-75] } const ( @@ -34,6 +35,7 @@ const ( _AtomType_name_2 = "AtomBLOBAtomPIntAtomNInt" _AtomType_name_3 = "AtomFloat16AtomFloat32AtomFloat64AtomFloat128AtomFloat256" _AtomType_name_4 = "AtomTAI64AtomTAI64NAtomTAI64NA" + _AtomType_name_5 = "AtomMagic" ) var ( @@ -60,6 +62,8 @@ func (i AtomType) String() string { 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) + ")" } diff --git a/go/cmd/pp/main.go b/go/cmd/pp/main.go index 197232f..00b73d8 100644 --- a/go/cmd/pp/main.go +++ b/go/cmd/pp/main.go @@ -137,6 +137,16 @@ func printer(iter *keks.Iterator, count int, inList, inMap bool) { 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: @@ -160,15 +170,21 @@ func printer(iter *keks.Iterator, count int, inList, inMap bool) { 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") } diff --git a/go/cmd/test-vector-anys/main.go b/go/cmd/test-vector-anys/main.go index ffb653d..dced936 100644 --- a/go/cmd/test-vector-anys/main.go +++ b/go/cmd/test-vector-anys/main.go @@ -99,7 +99,11 @@ func main() { "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) } diff --git a/go/cmd/test-vector-manual/main.go b/go/cmd/test-vector-manual/main.go index 3eb3794..0f1c043 100644 --- a/go/cmd/test-vector-manual/main.go +++ b/go/cmd/test-vector-manual/main.go @@ -33,6 +33,7 @@ func mustEncode(n int64, err error) { func main() { var buf bytes.Buffer + mustEncode(keks.MagicEncode(&buf, keks.Magic("test-vector"))) { mustEncode(keks.ByteEncode(&buf, byte(keks.AtomMap))) { diff --git a/go/cmd/textdump-tester/main.go b/go/cmd/textdump-tester/main.go index 717738d..cab6ad9 100644 --- a/go/cmd/textdump-tester/main.go +++ b/go/cmd/textdump-tester/main.go @@ -120,6 +120,18 @@ func checker(v any) { 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 { diff --git a/go/encode.go b/go/encode.go index 2f9cf01..5e01ecc 100644 --- a/go/encode.go +++ b/go/encode.go @@ -106,6 +106,8 @@ func Encode(w io.Writer, v any, opts *EncodeOpts) (written int64, err error) { 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() { diff --git a/go/iter.go b/go/iter.go index 4776138..d7cd6a1 100644 --- a/go/iter.go +++ b/go/iter.go @@ -77,7 +77,7 @@ func (iter *Iterator) Next() bool { 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++ @@ -138,6 +138,10 @@ func (iter *Iterator) TAI64NA() *tai64n.TAI64NA { 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]) } diff --git a/go/magic.go b/go/magic.go new file mode 100644 index 0000000..3016347 --- /dev/null +++ b/go/magic.go @@ -0,0 +1,42 @@ +// GoKEKS -- Go KEKS codec implementation +// Copyright (C) 2024-2025 Sergey Matveev +// +// 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 . + +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:] +} diff --git a/go/pki/algo.go b/go/pki/algo.go index d0abc7d..bced6af 100644 --- a/go/pki/algo.go +++ b/go/pki/algo.go @@ -14,4 +14,8 @@ const ( SNTRUP4591761X25519HKDFBLAKE2b = sntrup4591761x25519.SNTRUP4591761X25519HKDFBLAKE2b BalloonBLAKE2bHKDF = "balloon-blake2b-hkdf" ChaCha20Poly1305 = "chacha20poly1305" + + EncryptedMagic = "pki/encryptd" + HashedMagic = "pki/hashed" + PrvKeyMagic = "pki/prvkey" ) diff --git a/go/pki/cer.go b/go/pki/cer.go index 528aac5..1f99071 100644 --- a/go/pki/cer.go +++ b/go/pki/cer.go @@ -29,9 +29,10 @@ import ( ) 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. @@ -49,16 +50,16 @@ type CerLoad struct { 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") @@ -108,9 +109,17 @@ func (sd *SignedData) CerParse() error { 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 } @@ -127,10 +136,10 @@ func (cer *CerLoad) Can(ku string) (yes bool) { 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, @@ -171,9 +180,9 @@ func (cer *CerLoad) CheckSignature(signed, signature []byte) (err error) { 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 @@ -187,7 +196,7 @@ func (sd *SignedData) CerCheckSignatureFrom(parent *CerLoad) (err error) { 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 @@ -195,9 +204,9 @@ func (sd *SignedData) CerCheckSignatureFrom(parent *CerLoad) (err error) { 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 } @@ -208,9 +217,9 @@ func (sd *SignedData) CerLoad() *CerLoad { 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]) { @@ -226,7 +235,7 @@ func (sd *SignedData) CerVerify(cers []*SignedData, t time.Time) (err error) { 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 { diff --git a/go/pki/cmd/certool/basic.t b/go/pki/cmd/certool/basic.t index 039bd6d..abe3ed4 100755 --- a/go/pki/cmd/certool/basic.t +++ b/go/pki/cmd/certool/basic.t @@ -9,14 +9,21 @@ echo "gost3410-512C gost3410-256A 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 \ diff --git a/go/pki/cmd/certool/main.go b/go/pki/cmd/certool/main.go index b767e01..7b40f29 100644 --- a/go/pki/cmd/certool/main.go +++ b/go/pki/cmd/certool/main.go @@ -16,6 +16,7 @@ package main import ( + "bytes" "crypto" "errors" "flag" @@ -76,7 +77,6 @@ func main() { 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) @@ -102,9 +102,9 @@ func main() { 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) @@ -122,7 +122,7 @@ func main() { } if *verify { - var sd *pki.SignedData + var sd *pki.Signed sd, err = pki.CerParse(utils.MustReadFile(*cerPath)) if err != nil { log.Fatal(err) @@ -138,20 +138,19 @@ func main() { 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: @@ -160,14 +159,18 @@ func main() { 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) + } } } @@ -180,34 +183,29 @@ func main() { 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) + } } } diff --git a/go/pki/cmd/enctool/chapoly.go b/go/pki/cmd/enctool/chapoly.go index 6366cc9..7c745ab 100644 --- a/go/pki/cmd/enctool/chapoly.go +++ b/go/pki/cmd/enctool/chapoly.go @@ -1,4 +1,4 @@ -// enctool -- dealing with KEKS-encoded encrypted-data utility +// enctool -- dealing with KEKS-encoded pki-encrypted utility // Copyright (C) 2024-2025 Sergey Matveev // // This program is free software: you can redistribute it and/or modify diff --git a/go/pki/cmd/enctool/main.go b/go/pki/cmd/enctool/main.go index 4d4d203..d15b158 100644 --- a/go/pki/cmd/enctool/main.go +++ b/go/pki/cmd/enctool/main.go @@ -1,4 +1,4 @@ -// enctool -- dealing with KEKS-encoded encrypted-data utility +// enctool -- dealing with KEKS-encoded pki-encrypted utility // Copyright (C) 2024-2025 Sergey Matveev // // This program is free software: you can redistribute it and/or modify @@ -36,12 +36,13 @@ import ( "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 { @@ -67,7 +68,7 @@ type DEM struct { A string `keks:"a"` } -type Envelope struct { +type Encrypted struct { DEM DEM `keks:"dem"` KEM []KEM `keks:"kem"` Bind uuid.UUID `keks:"bind"` @@ -130,24 +131,38 @@ func main() { 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 == "" { @@ -163,7 +178,7 @@ func main() { { 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 @@ -220,7 +235,7 @@ func main() { } { ikm := bytes.Join([][]byte{ - envelope.Bind[:], + encrypted.Bind[:], *kem.Encap, pubs[prvIdx].V, keySNTRUP[:], keyX25519, }, []byte{}) @@ -356,16 +371,18 @@ func main() { 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) } } diff --git a/go/pki/cmd/sigtool/main.go b/go/pki/cmd/sigtool/main.go index 685cd0d..93cdf5f 100644 --- a/go/pki/cmd/sigtool/main.go +++ b/go/pki/cmd/sigtool/main.go @@ -1,4 +1,4 @@ -// sigtool -- dealing with KEKS-encoded signed-data utility +// sigtool -- dealing with KEKS-encoded pki-signed utility // Copyright (C) 2024-2025 Sergey Matveev // // This program is free software: you can redistribute it and/or modify @@ -36,19 +36,19 @@ import ( 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) } @@ -82,8 +82,8 @@ func main() { 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) } @@ -112,7 +112,7 @@ func main() { 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 @@ -122,8 +122,8 @@ func main() { 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 { diff --git a/go/pki/prv.go b/go/pki/prv.go index 6bead41..fd8f885 100644 --- a/go/pki/prv.go +++ b/go/pki/prv.go @@ -27,10 +27,17 @@ import ( // 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 { diff --git a/go/pki/signed-data.go b/go/pki/signed.go similarity index 70% rename from go/pki/signed-data.go rename to go/pki/signed.go index 7d9b949..4de2cd8 100644 --- a/go/pki/signed-data.go +++ b/go/pki/signed.go @@ -26,7 +26,9 @@ import ( "go.cypherpunks.su/keks" ) -type SignedDataLoad struct { +const SignedMagic = "pki/signed" + +type SignedLoad struct { V any `keks:"v,omitempty"` T string `keks:"t"` } @@ -38,7 +40,7 @@ type SigTBS struct { 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 { @@ -47,28 +49,28 @@ 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 { @@ -80,28 +82,28 @@ func SignedDataValidate(sd *SignedData) (err error) { } 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 @@ -112,7 +114,7 @@ func SignedDataValidate(sd *SignedData) (err error) { } } if !exists { - err = errors.New("SignedDataParse: /sigs: no hash") + err = errors.New("SignedParse: /sigs: no hash") return } } @@ -120,23 +122,30 @@ func SignedDataValidate(sd *SignedData) (err error) { 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, @@ -145,7 +154,7 @@ func (sd *SignedData) SignWith( 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 diff --git a/go/type.go b/go/type.go index f094e08..26bee98 100644 --- a/go/type.go +++ b/go/type.go @@ -22,6 +22,7 @@ const ( AtomTAI64 AtomType = 0x18 AtomTAI64N AtomType = 0x19 AtomTAI64NA AtomType = 0x1A + AtomMagic AtomType = 0x4B AtomStrings = 0x80 AtomIsUTF8 = 0x40 diff --git a/go/types/type.go b/go/types/type.go index e333044..6fd03eb 100644 --- a/go/types/type.go +++ b/go/types/type.go @@ -19,6 +19,7 @@ const ( TAI64 TAI64N TAI64NA + Magic Bin Str Raw diff --git a/go/types/type_string.go b/go/types/type_string.go index a1ed2f9..ab7ab4d 100644 --- a/go/types/type_string.go +++ b/go/types/type_string.go @@ -23,14 +23,15 @@ func _() { _ = 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) { diff --git a/go/unmarshal.go b/go/unmarshal.go index 08b4943..9716988 100644 --- a/go/unmarshal.go +++ b/go/unmarshal.go @@ -116,6 +116,8 @@ func (ctx *Decoder) unmarshal(iter *Iterator) (v any, err error) { 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: diff --git a/py3/keks.py b/py3/keks.py index 973d3a6..f00262e 100755 --- a/py3/keks.py +++ b/py3/keks.py @@ -58,6 +58,7 @@ TagFloat256 = 0x14 TagTAI64 = 0x18 TagTAI64N = 0x19 TagTAI64NA = 0x1A +TagMagic = 0x4B TagStr = 0x80 TagUTF8 = 0x40 @@ -78,6 +79,7 @@ TagPIntb = _byte(TagPInt) TagNIntb = _byte(TagNInt) TagTAI64b = _byte(TagTAI64) TagTAI64Nb = _byte(TagTAI64N) +TagMagicb = _byte(TagMagic) class DecodeError(ValueError): @@ -114,6 +116,26 @@ class Raw: 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")) @@ -261,6 +283,8 @@ def dumps(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") @@ -441,6 +465,12 @@ def _loads(v, sets=False, leapsecUTCAllow=False, _allowContainers=True): 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") @@ -476,12 +506,14 @@ if __name__ == "__main__": 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()) diff --git a/py3/test-vector.py b/py3/test-vector.py index 60156f9..115341d 100644 --- a/py3/test-vector.py +++ b/py3/test-vector.py @@ -53,9 +53,11 @@ data["dates"] = [ 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()) diff --git a/py3/tests/strategies.py b/py3/tests/strategies.py index 02b0b9b..56da544 100644 --- a/py3/tests/strategies.py +++ b/py3/tests/strategies.py @@ -31,6 +31,7 @@ from hypothesis.strategies import uuids from keks import _byte from keks import Blob +from keks import Magic from keks import Raw from keks import TagTAI64NA from keks import TAI64Base @@ -45,6 +46,7 @@ mapkey_st = text(alphabet=unicode_allowed, min_size=1, max_size=8) 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(), @@ -57,6 +59,7 @@ any_st = one_of( datetimes(), blobs_st, tai64na_st, + magic_st, ) everything_st = deferred( lambda: any_st | diff --git a/py3/tests/test_magic.py b/py3/tests/test_magic.py new file mode 100644 index 0000000..838798b --- /dev/null +++ b/py3/tests/test_magic.py @@ -0,0 +1,60 @@ +# PyKEKS -- Python KEKS implementation +# Copyright (C) 2024-2025 Sergey Matveev +# +# 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 . + +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") diff --git a/py3/tests/textdump-tester b/py3/tests/textdump-tester index 84282bc..b648fd4 100755 --- a/py3/tests/textdump-tester +++ b/py3/tests/textdump-tester @@ -16,6 +16,7 @@ # * 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 @@ -30,6 +31,7 @@ from datetime import datetime from uuid import UUID from keks import Blob +from keks import Magic from keks import Raw from keks import TagTAI64NA @@ -71,6 +73,8 @@ def textdump(v): 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") diff --git a/spec/encoding/index.texi b/spec/encoding/index.texi index c4d6c6a..987c423 100644 --- a/spec/encoding/index.texi +++ b/spec/encoding/index.texi @@ -34,6 +34,8 @@ Possible values for the tag: @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)} @@ -57,5 +59,6 @@ Possible values for the tag: @include encoding/int.texi @include encoding/float.texi @include encoding/tai64.texi +@include encoding/magic.texi @include encoding/cont.texi @include encoding/blob.texi diff --git a/spec/encoding/magic.texi b/spec/encoding/magic.texi new file mode 100644 index 0000000..739b201 --- /dev/null +++ b/spec/encoding/magic.texi @@ -0,0 +1,16 @@ +@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 diff --git a/spec/encoding/table.texi b/spec/encoding/table.texi index aedc196..754fac5 100644 --- a/spec/encoding/table.texi +++ b/spec/encoding/table.texi @@ -79,7 +79,7 @@ @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 diff --git a/spec/format/cer.texi b/spec/format/cer.texi index 4bcfe35..7672b06 100644 --- a/spec/format/cer.texi +++ b/spec/format/cer.texi @@ -2,7 +2,9 @@ @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}: @@ -47,7 +49,7 @@ It @strong{must} be absent if empty. Values are extension specific. @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 @@ -92,7 +94,7 @@ Example minimal certificate may look like: @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. @@ -100,7 +102,7 @@ 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. diff --git a/spec/format/enveloped-data.cddl b/spec/format/encrypted.cddl similarity index 98% rename from spec/format/enveloped-data.cddl rename to spec/format/encrypted.cddl index 1eedab6..829423c 100644 --- a/spec/format/enveloped-data.cddl +++ b/spec/format/encrypted.cddl @@ -1,6 +1,6 @@ ai = text ; algorithm identifier -enveloped-data = { +pki-encrypted = { dem: dem, kem: [+ kem], bind: uuid, diff --git a/spec/format/enveloped-data.texi b/spec/format/encrypted.texi similarity index 81% rename from spec/format/enveloped-data.texi rename to spec/format/encrypted.texi index 93a28eb..3c07a6d 100644 --- a/spec/format/enveloped-data.texi +++ b/spec/format/encrypted.texi @@ -1,10 +1,10 @@ -@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 @@ -14,7 +14,7 @@ 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. +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 @@ -26,12 +26,12 @@ 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. +@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: @@ -49,8 +49,8 @@ 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 +@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. @@ -58,7 +58,7 @@ Last chunk should be smaller than previous ones, maybe even empty. @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 @@ -69,8 +69,8 @@ size. Authentication of ciphertext is performed with Streebog-512 (ГОСТ @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: @@ -93,13 +93,13 @@ password hasher must be used with BLAKE2b-256 hash. @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: @@ -120,14 +120,14 @@ and KExp15 (Р 1323565.1.017) key wrapping algorithm: @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 @@ -146,7 +146,7 @@ them to get the decryption key of the CEK. @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 || diff --git a/spec/format/hashed-data.cddl b/spec/format/hashed.cddl similarity index 91% rename from spec/format/hashed-data.cddl rename to spec/format/hashed.cddl index 5d0c8b1..39465b0 100644 --- a/spec/format/hashed-data.cddl +++ b/spec/format/hashed.cddl @@ -1,6 +1,6 @@ ai = text ; algorithm identifier -hashed-data = { +pki-hashed = { a: [+ ai], t: text, ; type of the content v: bytes / text / blob / map / list, ; content itself diff --git a/spec/format/hashed-data.texi b/spec/format/hashed.texi similarity index 71% rename from spec/format/hashed-data.texi rename to spec/format/hashed.texi index 9db082d..f44efca 100644 --- a/spec/format/hashed-data.texi +++ b/spec/format/hashed.texi @@ -1,10 +1,11 @@ -@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. @@ -18,29 +19,29 @@ converted from BIN to BLOB. @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. @@ -48,23 +49,23 @@ algorithms. 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. diff --git a/spec/format/index.texi b/spec/format/index.texi index 8941326..db281c9 100644 --- a/spec/format/index.texi +++ b/spec/format/index.texi @@ -6,8 +6,8 @@ They are written in @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 diff --git a/spec/format/private-key.texi b/spec/format/private-key.texi index 52e4850..5b1c004 100644 --- a/spec/format/private-key.texi +++ b/spec/format/private-key.texi @@ -6,6 +6,8 @@ Private key is stored in trivial map: @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 diff --git a/spec/format/registry.texi b/spec/format/registry.texi index f9e614a..4439102 100644 --- a/spec/format/registry.texi +++ b/spec/format/registry.texi @@ -10,22 +10,22 @@ There is example registry of known algorithm identifiers. @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 @@ -45,9 +45,9 @@ There is example registry of known algorithm identifiers. @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 @@ -56,16 +56,16 @@ There is example registry of known algorithm identifiers. @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 @@ -76,13 +76,13 @@ There is example registry of known algorithm identifiers. @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 @@ -90,6 +90,6 @@ There is example registry of known algorithm identifiers. @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 diff --git a/spec/format/signed-data-tbs.cddl b/spec/format/signed-tbs.cddl similarity index 78% rename from spec/format/signed-data-tbs.cddl rename to spec/format/signed-tbs.cddl index b6f05c2..137d8a6 100644 --- a/spec/format/signed-data-tbs.cddl +++ b/spec/format/signed-tbs.cddl @@ -1,4 +1,4 @@ -signed-data-tbs = { +pki-signed-tbs = { t: text, ; = /load/t v: nil / any, tbs: map, ; = /sigs/?/tbs diff --git a/spec/format/signed-data.cddl b/spec/format/signed.cddl similarity index 86% rename from spec/format/signed-data.cddl rename to spec/format/signed.cddl index 4c7a14d..6579cb1 100644 --- a/spec/format/signed-data.cddl +++ b/spec/format/signed.cddl @@ -1,7 +1,7 @@ ai = text ; algorithm identifier av = {a: ai, v: bytes} -signed-data = { +pki-signed = { ? hash: set, ; when using prehashing load: { t: text, @@ -11,7 +11,7 @@ signed-data = { ? 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, diff --git a/spec/format/signed-data.texi b/spec/format/signed.texi similarity index 76% rename from spec/format/signed-data.texi rename to spec/format/signed.texi index f5c490e..5882407 100644 --- a/spec/format/signed-data.texi +++ b/spec/format/signed.texi @@ -1,15 +1,18 @@ -@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 @@ -30,12 +33,12 @@ are placed outside it. 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 @@ -48,8 +51,8 @@ Following algorithm identifiers should be used for the hash: 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. diff --git a/tcl/keks.tcl b/tcl/keks.tcl index ea9b572..c21af7c 100644 --- a/tcl/keks.tcl +++ b/tcl/keks.tcl @@ -31,11 +31,19 @@ proc TRUE {} { char [expr 0x03] } 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} { @@ -228,7 +236,7 @@ proc RAW {t v} { 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 diff --git a/tcl/mk-fuzz-inputs b/tcl/mk-fuzz-inputs index a08bbf6..c3ad2bf 100755 --- a/tcl/mk-fuzz-inputs +++ b/tcl/mk-fuzz-inputs @@ -31,3 +31,4 @@ dump 'TAI64 -11' >tai-before 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 diff --git a/tcl/test-vector.tcl b/tcl/test-vector.tcl index f2167bf..74b1281 100644 --- a/tcl/test-vector.tcl +++ b/tcl/test-vector.tcl @@ -1,6 +1,7 @@ source keks.tcl namespace import KEKS::* +MAGIC test-vector MAP { ints {MAP { pos {LIST { -- 2.48.1