]> Cypherpunks repositories - keks.git/commitdiff
Use schema validation for signed, encrypted and pub
authorSergey Matveev <stargrave@stargrave.org>
Fri, 4 Apr 2025 10:58:12 +0000 (13:58 +0300)
committerSergey Matveev <stargrave@stargrave.org>
Fri, 4 Apr 2025 12:48:35 +0000 (15:48 +0300)
14 files changed:
go/cm/cmd/enctool/main.go
go/cm/cmd/sigtool/main.go
go/cm/default.schema.keks.do [new file with mode: 0644]
go/cm/enc/schema.go [new file with mode: 0644]
go/cm/sign/prv.go
go/cm/sign/pub.go
go/cm/sign/schema.go [new file with mode: 0644]
go/cm/sign/signed.go
spec/cm/encrypted.cddl
spec/schema/tcl.texi
tcl/schema2bin
tcl/schemas/encrypted.tcl [new file with mode: 0644]
tcl/schemas/pub.tcl
tcl/schemas/signed.tcl [new file with mode: 0644]

index 2b4b16c1d90dbece14d46616aa0e032caf8c99db186647b921f1aaf4ba9c156e..370810dc173775da302a12008ab97b0fcfdf4ec89e43b7d951547392fe3b560c 100644 (file)
@@ -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)
index 4bcafa2fb89697eaba65b4281b4443509d5dd42b9cbb4aa1ca7f95b0fc6fff7d..3ce84cb3b69682791b3fc510f65f7d77e0bd2b337ea7d04d673b894cbbe00273 100644 (file)
@@ -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 (file)
index 0000000..220156c
--- /dev/null
@@ -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 (file)
index 0000000..b0bade2
--- /dev/null
@@ -0,0 +1,41 @@
+// GoKEKS/CM -- KEKS-encoded cryptographic messages
+// 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 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)
+       }
+}
index e18331c7059f716b09011babb9fa2314cec3fb40a0e306b7822967528edf9d1b..eb0a2997995a19da392e65d88ace9ba30e32b9c0d619bd1fb5aec3c782883f56 100644 (file)
@@ -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 {
index 24bda01f3efaef5b16eb04be70512bcf54d6dddfc725ef7470c1c280bbadcde5..b55060ee7ecf1f400281fe1dd61edfa33802f07b1e85e3a660911c5a6c4980ad 100644 (file)
@@ -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 (file)
index 0000000..0b77eca
--- /dev/null
@@ -0,0 +1,57 @@
+// GoKEKS/CM -- KEKS-encoded cryptographic messages
+// 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 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)
+       }
+}
index 6d17ea89f0f37628d0cd6ae2b5e24c8eecb0979455255d24db412b6d16f6da11..67968418b95916a04392254c88198d0eb2ae0272117ced4e14a5b4d79334039f 100644 (file)
@@ -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
 }
 
index ddc4d91daf266c1c1f1f6ed48a5841737c17dcd4128529f1647eebac950c9c6b..34e5897d19c73447e0f6d70491e7e13ce1803ef9926d1bb132ddb66949e4addd 100644 (file)
@@ -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
index d42f227506c9e0620897469ceb6e731bbc3b16e8c56673824ad7831c025c9820..c5293144ffde29b69f48ad8dad480f490d288fe96e1634f8ded45b0559f8cfd2 100644 (file)
@@ -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
index 9da26c79110681286e22e2ed82037383275a20b27c450959490e48c99bcb92ef..e345f488f587f382bc660b21fc4896e2503c92ed8cb5f4111c516365b4652b20 100755 (executable)
@@ -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 (file)
index 0000000..c864fea
--- /dev/null
@@ -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}
+}
+
+}
index 097ff63f2fe432836480a520848f2d57311a826453417f4574d471f6dda9cc6d..376d65b348744b62bc337d20977af4e030bea9fb7b66ce9011e612025215229b 100644 (file)
@@ -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 (file)
index 0000000..ea11e97
--- /dev/null
@@ -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}
+}
+
+}