From: Sergey Matveev Date: Fri, 4 Apr 2025 10:58:12 +0000 (+0300) Subject: Use schema validation for signed, encrypted and pub X-Git-Url: http://www.git.cypherpunks.su/?a=commitdiff_plain;h=59bfc34073c448a72028eeb925838f272ef71dd0c15e937f4e8edbe62a9a5315;p=keks.git Use schema validation for signed, encrypted and pub --- diff --git a/go/cm/cmd/enctool/main.go b/go/cm/cmd/enctool/main.go index 2b4b16c..370810d 100644 --- a/go/cm/cmd/enctool/main.go +++ b/go/cm/cmd/enctool/main.go @@ -48,6 +48,7 @@ import ( sntrup4591761x25519 "go.cypherpunks.su/keks/cm/enc/sntrup4591761-x25519" cmhash "go.cypherpunks.su/keks/cm/hash" "go.cypherpunks.su/keks/cm/sign" + "go.cypherpunks.su/keks/schema" "go.cypherpunks.su/keks/types" ) @@ -95,9 +96,18 @@ func parsePrv(data []byte) (av cm.AV, tail []byte, err error) { case sign.PrvMagic: case cmenc.Magic: var encrypted cmenc.Encrypted + var v any { d := keks.NewDecoderFromBytes(data, nil) - err = d.DecodeStruct(&encrypted) + v, err = d.Decode() + if err != nil { + return + } + err = schema.Check("encrypted", cmenc.EncryptedSchemas, v) + if err != nil { + return + } + err = d.UnmarshalStruct(&encrypted) if err != nil { return } @@ -112,6 +122,12 @@ func parsePrv(data []byte) (av cm.AV, tail []byte, err error) { err = errors.New("wrong prv encryption KEM") return } + v = v.(map[string]any)["kem"].([]any)[0] + err = schema.Check("kem-balloon-blake2b-hkdf", cmenc.EncryptedSchemas, v) + if err != nil { + return + } + passwd := readPasswd("Passphrase for private key:") var cek []byte cek, err = cmballoon.Decapsulate(encrypted.KEM[0], encrypted.Id[:], passwd) @@ -133,6 +149,7 @@ func parsePrv(data []byte) (av cm.AV, tail []byte, err error) { err = errors.New("wrong magic") return } + sign.PrvParse(data) d := keks.NewDecoderFromBytes(data, &keks.DecodeOpts{MaxStrLen: 1 << 16}) err = d.DecodeStruct(&av) tail = d.B @@ -213,10 +230,33 @@ func main() { var encrypted cmenc.Encrypted { d := keks.NewDecoderFromReader(os.Stdin, nil) - err = d.DecodeStruct(&encrypted) + var v any + v, err = d.Decode() if err != nil { log.Fatal(err) } + if err = schema.Check("encrypted", cmenc.EncryptedSchemas, v); err != nil { + log.Fatal(err) + } + for _, kem := range v.(map[string]any)["kem"].([]any) { + kemMap := kem.(map[string]any) + switch kemMap["a"] { + case cmballoon.BalloonBLAKE2bHKDF: + err = schema.Check("kem-balloon-blake2b-hkdf", cmenc.EncryptedSchemas, kem) + case sntrup4591761x25519.SNTRUP4591761X25519HKDFBLAKE2b: + fallthrough + case mceliece6960119x25519.ClassicMcEliece6960119X25519HKDFSHAKE256: + err = schema.Check("kem-with-encap", cmenc.EncryptedSchemas, kem) + default: + log.Fatal("unsupported KEM algorithm") + } + } + if err != nil { + log.Fatal(err) + } + if err = d.UnmarshalStruct(&encrypted); err != nil { + log.Fatal(err) + } } if encrypted.DEM.A != chapoly.DEMAlgo { log.Fatalln("unsupported DEM:", encrypted.DEM.A) diff --git a/go/cm/cmd/sigtool/main.go b/go/cm/cmd/sigtool/main.go index 4bcafa2..3ce84cb 100644 --- a/go/cm/cmd/sigtool/main.go +++ b/go/cm/cmd/sigtool/main.go @@ -30,6 +30,7 @@ import ( cmhash "go.cypherpunks.su/keks/cm/hash" "go.cypherpunks.su/keks/cm/sign" "go.cypherpunks.su/keks/cm/sign/mode" + "go.cypherpunks.su/keks/schema" "go.cypherpunks.su/keks/types" ) @@ -87,18 +88,20 @@ func main() { if t != types.Magic || decoder.Iter().Magic() != sign.SignedMagic { log.Fatal("wrong magic") } - decoder = keks.NewDecoderFromReader(stdin, nil) - if _, err = decoder.Parse(); err != nil { + decoder = keks.NewDecoderFromReader(stdin, &keks.DecodeOpts{LeaveTAI64: true}) + var v any + v, err = decoder.Decode() + if err != nil { log.Fatal(err) } var prehash sign.Prehash + err = schema.Check("prehash", sign.SignedSchemas, v) + if err == nil { + err = decoder.UnmarshalStruct(&prehash) + } var signed sign.Signed - err = decoder.UnmarshalStruct(&prehash) var hasher hash.Hash if err == nil && prehash.T == mode.PrehashT { - if len(prehash.Sigs) == 0 { - log.Fatal("prehash: no sigs") - } if len(prehash.Sigs) > 1 { log.Fatal("prehash: currently only single signature support") } @@ -127,15 +130,18 @@ func main() { log.Fatal(err) } } - decoder = keks.NewDecoderFromReader(stdin, nil) - err = decoder.DecodeStruct(&signed) - } else { - err = decoder.UnmarshalStruct(&signed) + decoder = keks.NewDecoderFromReader(stdin, &keks.DecodeOpts{LeaveTAI64: true}) + v, err = decoder.Decode() + if err != nil { + log.Fatal(err) + } } + err = schema.Check("signed", sign.SignedSchemas, v) if err != nil { log.Fatal(err) } - if err = sign.SignedValidate(&signed); err != nil { + err = decoder.UnmarshalStruct(&signed) + if err != nil { log.Fatal(err) } if len(signed.Sigs) == 0 { diff --git a/go/cm/default.schema.keks.do b/go/cm/default.schema.keks.do new file mode 100644 index 0000000..220156c --- /dev/null +++ b/go/cm/default.schema.keks.do @@ -0,0 +1,3 @@ +n=${2##*/}.tcl +redo-ifchange ../../tcl/schema2bin ../../tcl/schemas/$n +../../tcl/schema2bin ../../tcl/schemas/$n | xxd -r -p diff --git a/go/cm/enc/schema.go b/go/cm/enc/schema.go new file mode 100644 index 0000000..b0bade2 --- /dev/null +++ b/go/cm/enc/schema.go @@ -0,0 +1,41 @@ +// GoKEKS/CM -- KEKS-encoded cryptographic messages +// 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 encrypted + +import ( + _ "embed" + + "go.cypherpunks.su/keks" + "go.cypherpunks.su/keks/schema" +) + +//go:embed encrypted.schema.keks +var EncryptedSchemasRaw []byte + +var EncryptedSchemas map[string][][]any + +func init() { + var magic keks.Magic + magic, EncryptedSchemasRaw = keks.StripMagic(EncryptedSchemasRaw) + if magic != schema.Magic { + panic("wrong magic in signed.schema.keks") + } + if err := keks.NewDecoderFromBytes( + EncryptedSchemasRaw, nil, + ).DecodeStruct(&EncryptedSchemas); err != nil { + panic(err) + } +} diff --git a/go/cm/sign/prv.go b/go/cm/sign/prv.go index e18331c..eb0a299 100644 --- a/go/cm/sign/prv.go +++ b/go/cm/sign/prv.go @@ -23,6 +23,7 @@ import ( "go.cypherpunks.su/keks/cm" ed25519blake2b "go.cypherpunks.su/keks/cm/sign/ed25519-blake2b" "go.cypherpunks.su/keks/cm/sign/gost" + "go.cypherpunks.su/keks/schema" ) const PrvMagic = keks.Magic("cm/prv") @@ -38,12 +39,19 @@ func PrvParse(data []byte) (prv Iface, pub []byte, err error) { } } d := keks.NewDecoderFromBytes(data, &keks.DecodeOpts{MaxStrLen: 1 << 16}) - var av cm.AV - if err = d.DecodeStruct(&av); err != nil { - return + { + var v any + v, err = d.Decode() + if err != nil { + return + } + err = schema.Check("av", PubSchemas, v) + if err != nil { + return + } } - if len(d.B) != 0 { - err = errors.New("trailing data") + var av cm.AV + if err = d.UnmarshalStruct(&av); err != nil { return } switch av.A { diff --git a/go/cm/sign/pub.go b/go/cm/sign/pub.go index 24bda01..b55060e 100644 --- a/go/cm/sign/pub.go +++ b/go/cm/sign/pub.go @@ -28,14 +28,15 @@ import ( "go.cypherpunks.su/keks/cm" ed25519blake2b "go.cypherpunks.su/keks/cm/sign/ed25519-blake2b" "go.cypherpunks.su/keks/cm/sign/gost" + "go.cypherpunks.su/keks/schema" ) const ( KUCA = "ca" // CA-capable key usage KUSig = "sig" // Signing-capable key usage KUKEM = "kem" // Key-encapsulation-mechanism key usage + FPRLen = 32 // fingerprint's length PubMagic = keks.Magic("cm/pub") - FPRLen = 32 // fingerprint's length ) var ( @@ -52,56 +53,6 @@ type PubLoad struct { Id []byte `keks:"id"` } -// Parse Signed contents as PubLoad (certificate) and check its -// signatures necessary structure. signed.Load.V will hold the -// PubLoad in case of success. -func (signed *Signed) PubParse() error { - if signed.Load.T != "pub" { - return errors.New("PubParse: wrong load type") - } - for _, sig := range signed.Sigs { - if _, ok := sig.TBS["cid"]; !ok { - return errors.New("PubParse: missing cid") - } - if _, ok := sig.TBS["exp"]; !ok { - return errors.New("PubParse: missing exp") - } - } - if signed.Load.V == nil { - return errors.New("PubParse: missing /load/v") - } - var load PubLoad - var err error - if v, ok := (*signed.Load.V).(map[string]any); ok { - mapAny := any(v) - signed.Load.V = &mapAny - err = keks.Map2Struct(&load, v) - } else { - err = errors.New("PubParse: wrong /load/v") - } - if err != nil { - return err - } - if len(load.Sub) == 0 { - return errors.New("PubParse: empty sub") - } - if len(load.Crit) != 0 { - return errors.New("PubParse: currently no critical extensions are supported") - } - if len(load.Pub) == 0 { - return errors.New("PubParse: empty pub") - } - if len(load.Id) != FPRLen { - return errors.New("PubParse: invalid id len") - } - for _, pub := range load.Pub { - if len(pub.A) == 0 || len(pub.V) == 0 { - return errors.New("PubParse: non-filled pub") - } - } - return nil -} - // Parse KEKS-encoded data as Signed with the PubLoad (certificate) contents. func PubParse(data []byte) (signed *Signed, tail []byte, err error) { { @@ -112,11 +63,29 @@ func PubParse(data []byte) (signed *Signed, tail []byte, err error) { return } } - signed, tail, err = SignedParse(data) + d := keks.NewDecoderFromBytes(data, &keks.DecodeOpts{LeaveTAI64: true}) + { + var v any + v, err = d.Decode() + if err != nil { + return + } + err = schema.Check("pub", PubSchemas, v) + if err != nil { + return + } + } + d = keks.NewDecoderFromBytes(data, nil) + var sd Signed + err = d.DecodeStruct(&sd) if err != nil { return } - err = signed.PubParse() + tail = d.B + signed = &sd + if sd.Load.T != "pub" { + err = errors.New("PubParse: wrong load type") + } return } diff --git a/go/cm/sign/schema.go b/go/cm/sign/schema.go new file mode 100644 index 0000000..0b77eca --- /dev/null +++ b/go/cm/sign/schema.go @@ -0,0 +1,57 @@ +// GoKEKS/CM -- KEKS-encoded cryptographic messages +// 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 sign + +import ( + _ "embed" + + "go.cypherpunks.su/keks" + "go.cypherpunks.su/keks/schema" +) + +//go:embed signed.schema.keks +var SignedSchemasRaw []byte + +//go:embed pub.schema.keks +var PubSchemasRaw []byte + +var ( + SignedSchemas map[string][][]any + PubSchemas map[string][][]any +) + +func init() { + var magic keks.Magic + magic, SignedSchemasRaw = keks.StripMagic(SignedSchemasRaw) + if magic != schema.Magic { + panic("wrong magic in signed.schema.keks") + } + if err := keks.NewDecoderFromBytes( + SignedSchemasRaw, nil, + ).DecodeStruct(&SignedSchemas); err != nil { + panic(err) + } + + magic, PubSchemasRaw = keks.StripMagic(PubSchemasRaw) + if magic != schema.Magic { + panic("wrong magic in pub.schema.keks") + } + if err := keks.NewDecoderFromBytes( + PubSchemasRaw, nil, + ).DecodeStruct(&PubSchemas); err != nil { + panic(err) + } +} diff --git a/go/cm/sign/signed.go b/go/cm/sign/signed.go index 6d17ea8..6796841 100644 --- a/go/cm/sign/signed.go +++ b/go/cm/sign/signed.go @@ -27,6 +27,7 @@ import ( "go.cypherpunks.su/keks" "go.cypherpunks.su/keks/cm" "go.cypherpunks.su/keks/cm/sign/mode" + "go.cypherpunks.su/keks/schema" ) const SignedMagic = keks.Magic("cm/signed") @@ -48,70 +49,45 @@ type TBS struct { CID uuid.UUID `keks:"cid"` } -type Sig struct { - TBS map[string]any `keks:"tbs"` - Sign cm.AV `keks:"sign"` -} - func (sig *Sig) TBSGet() (*TBS, error) { var tbs TBS return &tbs, keks.Map2Struct(&tbs, sig.TBS) } +type Sig struct { + TBS map[string]any `keks:"tbs"` + Sign cm.AV `keks:"sign"` +} + type Signed struct { Load Load `keks:"load"` Pubs *[]*Signed `keks:"pubs,omitempty"` Sigs []*Sig `keks:"sigs,omitempty"` } -// Validate parsed cm-signed structure. -func SignedValidate(signed *Signed) (err error) { - if signed.Pubs != nil { - if len(*signed.Pubs) == 0 { - err = errors.New("SignedParse: empty /pubs") +// Parse and validate cm-signed from KEKS-encoded data. +func Parse(data []byte) (signed *Signed, tail []byte, err error) { + { + var magic keks.Magic + magic, data = keks.StripMagic(data) + if magic != "" && magic != SignedMagic { + err = errors.New("wrong magic") return } - for _, pub := range *signed.Pubs { - err = pub.PubParse() - if err != nil { - return - } - } } - for _, sig := range signed.Sigs { - var tbs *TBS - tbs, err = sig.TBSGet() + d := keks.NewDecoderFromBytes(data, &keks.DecodeOpts{LeaveTAI64: true}) + { + var v any + v, err = d.Decode() if err != nil { return } - if tbs.Exp != nil { - if len(tbs.Exp) != 2 { - err = errors.New("SignedParse: wrong exp len") - return - } - for _, t := range tbs.Exp { - if t.Nanosecond() != 0 { - err = errors.New("SignedParse: exp with nanoseconds") - return - } - } - } - } - return -} - -// Parse cm-signed from KEKS-encoded data. This is just a wrapper over -// DecodeStruct and SignedValidate. -func SignedParse(data []byte) (signed *Signed, tail []byte, err error) { - { - var magic keks.Magic - magic, data = keks.StripMagic(data) - if magic != "" && magic != SignedMagic { - err = errors.New("wrong magic") + err = schema.Check("signed", SignedSchemas, v) + if err != nil { return } } - d := keks.NewDecoderFromBytes(data, nil) + d = keks.NewDecoderFromBytes(data, nil) var sd Signed err = d.DecodeStruct(&sd) if err != nil { @@ -119,7 +95,6 @@ func SignedParse(data []byte) (signed *Signed, tail []byte, err error) { } tail = d.B signed = &sd - err = SignedValidate(signed) return } diff --git a/spec/cm/encrypted.cddl b/spec/cm/encrypted.cddl index ddc4d91..34e5897 100644 --- a/spec/cm/encrypted.cddl +++ b/spec/cm/encrypted.cddl @@ -44,7 +44,7 @@ kem-sntrup4591761-x25519-hkdf-blake2b = { } kem-mceliece6960119-x25519-hkdf-shake256 = { - a: "mceliece6960119-x25519-hkdf-shake256 ", + a: "mceliece6960119-x25519-hkdf-shake256", cek: bytes, encap: bytes, ? to: fpr, ; recipient's public key fingerprint diff --git a/spec/schema/tcl.texi b/spec/schema/tcl.texi index d42f227..c529314 100644 --- a/spec/schema/tcl.texi +++ b/spec/schema/tcl.texi @@ -15,8 +15,7 @@ SCHEMAS { our { {HAS a} {TYPE= a {STR}} - {TAKE a} - {GT 0} + {!EMPTY a} {HAS v} {TYPE= v {BIN STR}} @@ -59,4 +58,10 @@ Check "k" against "s" schema. @item SCHEMA* k s Check each element of "k" against "s" schema. +@item !EMPTY k +Check that "k" element's length is greater than zero. + +@item IS-SET k +Check that "k" is non-empty set (map with NIL values). + @end table diff --git a/tcl/schema2bin b/tcl/schema2bin index 9da26c7..e345f48 100755 --- a/tcl/schema2bin +++ b/tcl/schema2bin @@ -109,6 +109,25 @@ proc SCHEMA* {k schema} { }] } +proc !EMPTY {k} { + evals [subst { + {TAKE $k} + {GT 0} + }] +} + +proc IS-SET {k} { + evals [subst { + {TAKE $k} + {TYPE {MAP}} + {TAKE $k} + {GT 0} + {TAKE $k} + {EACH} + {TYPE {NIL}} + }] +} + MAGIC schema source [lindex $::argv 0] puts [binary encode hex $::KEKS::buf] diff --git a/tcl/schemas/encrypted.tcl b/tcl/schemas/encrypted.tcl new file mode 100644 index 0000000..c864fea --- /dev/null +++ b/tcl/schemas/encrypted.tcl @@ -0,0 +1,66 @@ +SCHEMAS { + +encrypted { + {HAS dem} + {HAS kem} + {TYPE= id {HEXLET}} + {TYPE= payload {BIN}} + {TYPE= dem {MAP}} + {TYPE= kem {LIST}} + {!EMPTY kem} + {SCHEMA= dem dem} + {SCHEMA* kem kem} +} + +dem { + {HAS a} + {TYPE= a {STR}} + {!EMPTY a} +} + +kem { + {HAS a} + {TYPE= a {STR}} + {!EMPTY a} + {HAS cek} + {TYPE= cek {BIN}} +} + +balloon-cost { + {HAS s} + {HAS t} + {HAS p} + {TYPE= s INT} + {TYPE= t INT} + {TYPE= p INT} +} + +kem-balloon-blake2b-hkdf { + {HAS a} + {TYPE= a {STR}} + {!EMPTY a} + {HAS cek} + {TYPE= cek {BIN}} + {HAS salt} + {TYPE= salt {BIN}} + {HAS cost} + {SCHEMA= cost balloon-cost} +} + +fpr { + {TYPE= . {BIN}} + {LEN= . 32} +} + +kem-with-encap { + {HAS a} + {TYPE= a {STR}} + {!EMPTY a} + {HAS cek} + {TYPE= cek {BIN}} + {HAS encap} + {TYPE= encap {BIN}} + {SCHEMA= to fpr} +} + +} diff --git a/tcl/schemas/pub.tcl b/tcl/schemas/pub.tcl index 097ff63..376d65b 100644 --- a/tcl/schemas/pub.tcl +++ b/tcl/schemas/pub.tcl @@ -3,12 +3,11 @@ SCHEMAS { av { {HAS a} {TYPE= a {STR}} - {TAKE a} - {GT 0} - + {!EMPTY a} {HAS v} {TYPE= v {BIN}} } + pub { {HAS load} {SCHEMA= load load} @@ -16,70 +15,71 @@ pub { {SCHEMA* sigs sig} {TYPE= pubs {LIST}} - {TAKE pubs} - {GT 0} + {!EMPTY pubs} {SCHEMA* pubs pub} } + load { {HAS t} {TYPE= t {STR}} - {TAKE t} - {GT 0} - + {!EMPTY t} {HAS v} {SCHEMA= v pub-load} } + sig { {HAS tbs} {HAS sign} - {SCHEMA= sign av} {SCHEMA= tbs tbs} + {SCHEMA= sign av} +} + +exp { + {TYPE= . {LIST}} + {LEN= . 2} + {TYPE* . {TAI64}} + {TAKE .} + {EACH} + {TIMEMAXPREC 0} } + +fpr { + {TYPE= . {BIN}} + {LEN= . 32} +} + tbs { {HAS sid} - {TYPE= sid {BIN}} - {LEN= sid 32} + {SCHEMA= sid fpr} {HAS cid} {TYPE= cid {HEXLET}} {HAS exp} - {TYPE= exp {LIST}} - {LEN= exp 2} - {TYPE* exp {TAI64}} - {TAKE exp} - {EACH} - {TIMEMAXPREC 0} - - {TYPE= when {TAI64}} + {SCHEMA= exp exp} {TYPE= nonce {BIN}} - {TAKE nonce} - {GT 0} + {!EMPTY nonce} + + {TYPE= when {TAI64}} } + pub-load { {HAS id} - {TYPE= id {BIN}} - {LEN= id 32} + {SCHEMA= id fpr} {!HAS crit} - {TYPE= ku {MAP}} - {TAKE ku} - {GT 0} - {TYPE* ku {NIL}} + {IS-SET ku} {HAS pub} {TYPE= pub {LIST}} - {TAKE pub} - {GT 0} + {!EMPTY pub} {SCHEMA* pub av} {HAS sub} - {TAKE sub} - {TYPE {MAP}} - {TAKE sub} - {GT 0} + {TYPE= sub {MAP}} + {!EMPTY sub} {TYPE* sub {STR}} } diff --git a/tcl/schemas/signed.tcl b/tcl/schemas/signed.tcl new file mode 100644 index 0000000..ea11e97 --- /dev/null +++ b/tcl/schemas/signed.tcl @@ -0,0 +1,59 @@ +SCHEMAS { + +prehash { + {HAS t} + {TYPE= t {STR}} + {HAS sigs} + {IS-SET sigs} +} + +av { + {HAS a} + {TYPE= a {STR}} + {!EMPTY a} + {HAS v} + {TYPE= v {BIN}} +} + +signed { + {HAS load} + {SCHEMA= load load} + {TYPE= sigs {LIST}} + {SCHEMA* sigs sig} + + {TYPE= pubs {LIST}} + {!EMPTY pubs} +} + +load { + {HAS t} + {TYPE= t {STR}} + {!EMPTY t} +} + +sig { + {HAS tbs} + {HAS sign} + {SCHEMA= tbs tbs} + {SCHEMA= sign av} +} + +fpr { + {TYPE= . {BIN}} + {LEN= . 32} +} + +tbs { + {HAS sid} + {TYPE= sid {BIN}} + {LEN= sid 32} + {TYPE= nonce {BIN}} + {!EMPTY nonce} + {TYPE= when {TAI64}} + + {TYPE= encrypted-to {LIST}} + {!EMPTY encrypted-to} + {SCHEMA* encrypted-to fpr} +} + +}