]> Cypherpunks repositories - keks.git/commitdiff
Prehashed certificate and CDDLs
authorSergey Matveev <stargrave@stargrave.org>
Wed, 9 Oct 2024 09:00:00 +0000 (12:00 +0300)
committerSergey Matveev <stargrave@stargrave.org>
Wed, 9 Oct 2024 09:00:00 +0000 (12:00 +0300)
15 files changed:
gyac/yacpki/algo.go
gyac/yacpki/cer.go
gyac/yacpki/cmd/yacertool/main.go
spec/format/cer-load.cddl [new file with mode: 0644]
spec/format/cer.texi
spec/format/hashed-data.cddl [new file with mode: 0644]
spec/format/hashed-data.texi [moved from spec/format/hashed.texi with 70% similarity]
spec/format/index.texi
spec/format/private-key.cddl [new file with mode: 0644]
spec/format/private-key.texi [new file with mode: 0644]
spec/format/prv.texi [deleted file]
spec/format/signed-data-sig-tbs.cddl [new file with mode: 0644]
spec/format/signed-data.cddl [new file with mode: 0644]
spec/format/signed-data.texi [new file with mode: 0644]
spec/format/signed.texi [deleted file]

index 0507478933f404d05895f584688de8eaf37fa2b9baa81673e28d55ccb6aa971e..96ece9cb9685bec525940378673e4766d7129bc6218124d31256d71e1d6d7043 100644 (file)
@@ -11,6 +11,7 @@ import (
 
 const (
        AlgoStreebog256  = "streebog256"
+       AlgoStreebog512  = "streebog512"
        AlgoGOST3410256A = "gost3410-256A"
        AlgoGOST3410256B = "gost3410-256B"
        AlgoGOST3410256C = "gost3410-256C"
@@ -20,6 +21,16 @@ const (
        AlgoGOST3410512C = "gost3410-512C"
 )
 
+var HashToNew = map[string]func() hash.Hash{
+       AlgoStreebog256: gost34112012256.New,
+       AlgoStreebog512: gost34112012512.New,
+}
+
+type AV struct {
+       A string `yac:"a"`
+       V []byte `yac:"v"`
+}
+
 func GOST3410CurveByName(name string) (curve *gost3410.Curve) {
        switch name {
        case AlgoGOST3410256A:
index 6a40913c79f02e98ad1f4da5b2377d355594b559432580be6be3849bae1f3e92..1d3991f40ae9ebed38ad563e2d718b7c87c373b1956b375f015e6b1d9dc6a664 100644 (file)
@@ -5,6 +5,7 @@ import (
        "crypto"
        "crypto/rand"
        "errors"
+       "hash"
        "time"
 
        "github.com/google/uuid"
@@ -13,28 +14,26 @@ import (
        "go.cypherpunks.su/yac/gyac"
 )
 
-type AV struct {
-       A string `yac:"a"`
-       V []byte `yac:"v"`
-}
-
 type SignedDataLoad struct {
        V any    `yac:"v"`
        T string `yac:"t"`
 }
 
-type SignedDataTBS struct {
-       V    any            `yac:"v"`
-       Load map[string]any `yac:"load"`
-       T    string         `yac:"t"`
-       KID  uuid.UUID      `yac:"kid"`
+type SigTBS struct {
+       Hashes map[string][]byte `yac:"hash,omitempty"`
+       SID    uuid.UUID         `yac:"sid"`
 }
 
 type Sig struct {
-       Load   map[string]any `yac:"load,omitempty"`
-       CerLoc []string       `yac:"cer-loc,omitempty"`
-       Sign   AV             `yac:"sign"`
-       KID    uuid.UUID      `yac:"kid"`
+       TBS    SigTBS   `yac:"tbs,omitempty"`
+       CerLoc []string `yac:"cer-loc,omitempty"`
+       Sign   AV       `yac:"sign"`
+}
+
+type SignedDataTBS struct {
+       V   any    `yac:"v"`
+       T   string `yac:"t"`
+       TBS SigTBS `yac:"tbs"`
 }
 
 type SignedData struct {
@@ -50,7 +49,7 @@ type CerLoad struct {
        Exp  []time.Time       `yac:"exp"`
        Crit []map[string]any  `yac:"crit,omitempty"`
        Pub  AV                `yac:"pub"`
-       KID  uuid.UUID         `yac:"kid"`
+       PKID uuid.UUID         `yac:"pkid"`
 }
 
 func (tbs *CerLoad) HasCA() (hasCA bool) {
@@ -62,7 +61,7 @@ func (tbs *CerLoad) HasCA() (hasCA bool) {
        return
 }
 
-func KIDFromPub(pub *AV) (kid uuid.UUID) {
+func PKIDFromPub(pub *AV) (kid uuid.UUID) {
        hasher := gost34112012256.New()
        hasher.Write(gyac.EncodeItem(nil, gyac.ItemFromGo(pub)))
        var err error
@@ -98,26 +97,61 @@ func (cer *CerLoad) CheckSignature(signed, signature []byte) (err error) {
 
 func (sd *SignedData) CheckSignatureFrom(parent *CerLoad) (err error) {
        sig := sd.Sigs[0]
-       if sig.KID != parent.KID {
-               err = errors.New("signer KID != parent KID")
+       if sig.TBS.SID != parent.PKID {
+               err = errors.New("sid != parent pkid")
+               return
+       }
+       if len(sig.TBS.Hashes) == 0 {
+               err = errors.New("sig.tbs misses hash")
+               return
+       }
+       if sig.TBS.Hashes[sd.Hashes[0]] == nil {
+               err = errors.New("sig.tbs misses specified hash")
+               return
+       }
+       hashNew, ok := HashToNew[sd.Hashes[0]]
+       if !ok {
+               err = errors.New("unsupported hash")
+               return
+       }
+       hasher := hashNew()
+       hasher.Write(gyac.EncodeItem(nil, gyac.ItemFromGo(sd.Load.V)))
+       if !bytes.Equal(sig.TBS.Hashes[sd.Hashes[0]], hasher.Sum(nil)) {
+               err = errors.New("hash differs")
                return
        }
-       tbs := SignedDataTBS{T: sd.Load.T, V: sd.Load.V, KID: parent.KID}
+       tbs := SignedDataTBS{T: sd.Load.T, V: sd.Load.V, TBS: sig.TBS}
        return parent.CheckSignature(
                gyac.EncodeItem(nil, gyac.ItemFromGo(tbs)),
                sig.Sign.V,
        )
 }
 
-func (sd *SignedData) SignWith(parent *CerLoad, prv crypto.Signer) (err error) {
-       tbs := SignedDataTBS{T: sd.Load.T, V: sd.Load.V, KID: parent.KID}
-       hasher := HasherByKeyAlgo(parent.Pub.A)
-       hasher.Write(gyac.EncodeItem(nil, gyac.ItemFromGo(tbs)))
-       sig := Sig{KID: parent.KID, Sign: AV{A: parent.Pub.A}}
+func (sd *SignedData) PrehashedSignWith(parent *CerLoad, prv crypto.Signer) (err error) {
+       var hashAlgo string
+       var hashNew func() hash.Hash
+       switch parent.Pub.A {
+       case AlgoGOST3410256A, AlgoGOST3410256B, AlgoGOST3410256C, AlgoGOST3410256D, AlgoGOST3410512A, AlgoGOST3410512B, AlgoGOST3410512C:
+               hashAlgo = AlgoStreebog256
+               hashNew = gost34112012256.New
+       default:
+               return errors.New("unknown hash algorithm for public key")
+       }
+       hasher := hashNew()
+       hasher.Write(gyac.EncodeItem(nil, gyac.ItemFromGo(sd.Load.V)))
+       sig := Sig{TBS: SigTBS{
+               SID:    parent.PKID,
+               Hashes: map[string][]byte{hashAlgo: hasher.Sum(nil)},
+       }}
+       sdTBS := SignedDataTBS{T: sd.Load.T, V: sd.Load.V, TBS: sig.TBS}
+       hasher = HasherByKeyAlgo(parent.Pub.A)
+       hasher.Write(gyac.EncodeItem(nil, gyac.ItemFromGo(sdTBS)))
+       sig.Sign.A = parent.Pub.A
        sig.Sign.V, err = prv.Sign(rand.Reader, hasher.Sum(nil), nil)
        if err != nil {
                return
        }
+       sd.Hashes = append(sd.Hashes, hashAlgo)
        sd.Sigs = append(sd.Sigs, sig)
        return
 }
@@ -133,15 +167,21 @@ func CerParse(data []byte) (sd *SignedData, tail []byte, err error) {
                err = errors.New("SignedData: not \"cer\" type")
                return
        }
-       if len(sd.Sigs) == 0 {
-               err = errors.New("SignedData: no \"sigs\"")
+       if len(sd.Sigs) != 1 {
+               err = errors.New("SignedData: wrong number of sigs")
                return
        }
-       var tbs CerLoad
-       err = gyac.MapToStruct(&tbs, sd.Load.V.(map[string]any))
-       if err != nil {
+       if len(sd.Hashes) != 1 {
+               err = errors.New("SignedData: wrong number of hashes")
                return
        }
-       sd.Load.V = &tbs
+       {
+               var load CerLoad
+               err = gyac.MapToStruct(&load, sd.Load.V.(map[string]any))
+               if err != nil {
+                       return
+               }
+               sd.Load.V = &load
+       }
        return
 }
index ade183adb0904caa7b3ff1a21915f063da5d4f0874281e2ee66cd0c35c963d83..5cd371a9955323132d6aacaec803ada7ca8a70c81ffc907bd530c0fff4fdc693 100644 (file)
@@ -113,7 +113,7 @@ func main() {
                }
                cerLoad := sd.Load.V.(*yacpki.CerLoad)
                sig := sd.Sigs[0]
-               if sig.KID != cerLoad.KID && !caCerLoad.HasCA() {
+               if sig.TBS.SID != cerLoad.PKID && !caCerLoad.HasCA() {
                        log.Fatal("no \"ca\" KU met in CA")
                }
                err = sd.CheckSignatureFrom(caCerLoad)
@@ -172,7 +172,7 @@ func main() {
                Subj: subj,
                Pub:  yacpki.AV{A: *algo, V: pub.RawBE()},
        }
-       cerLoad.KID = yacpki.KIDFromPub(&cerLoad.Pub)
+       cerLoad.PKID = yacpki.PKIDFromPub(&cerLoad.Pub)
        if caPrv == nil {
                caPrv = prv
                caCerLoad = &cerLoad
@@ -182,7 +182,7 @@ func main() {
                }
        }
        sd := yacpki.SignedData{Load: yacpki.SignedDataLoad{T: "cer", V: cerLoad}}
-       err = sd.SignWith(caCerLoad, caPrv)
+       err = sd.PrehashedSignWith(caCerLoad, caPrv)
        if err != nil {
                log.Fatal(err)
        }
diff --git a/spec/format/cer-load.cddl b/spec/format/cer-load.cddl
new file mode 100644 (file)
index 0000000..83e0515
--- /dev/null
@@ -0,0 +1,15 @@
+ai = text ; algorithm identifier
+av = {a: ai, v: bytes}
+
+certificate-load = {
+    ? ku: [+ ku],
+    exp: validity,
+    pub: av,
+    sub: {text => text}, ; subject
+    ? crit: [+ {t: text, * text => any}],
+    pkid: uuid, ; public key identifier
+    * text => any
+}
+
+ku = "ca" / "dh" / "sig" / text
+validity = [since: tai64, till: tai64]
index 674b0fde3dd221bcb5c701d1ff5834116acfdf1b9ba60434a9058c5df1a20a3b..851ca4dd32097b4abb5abfef664d1c5625fef8e45e9996dd769da58ad158a042 100644 (file)
@@ -1,32 +1,22 @@
-@node Certificate
-@cindex Certificate
-@section Certificate format
+@node cer
+@cindex cer
+@section cer format
 
-Certificate is the @ref{SignedData} structure with @code{/load.t} equals
-to @code{cer} and following @code{/load.v} content (keys are sorted as they
-will be encoded):
+Certificate is the prehashed @ref{signed-data} structure with
+@code{/load/t} equals to @code{cer}. @code{/load/v} must contain:
 
-@verbatim
-{
-    ?"ku": ["...", ...], # "dh", "sig", "ca", ...
-    "exp": [TAI64, TAI64],
-    "kid": UUID(signer's public key-based UUID),
-    "pub": {"a": "ID", "v": BIN(marshalled public key)},
-    "sub": {"entity": "...", ...},
-    ?"crit": [{"t": "ID", ...}, ...],
-}
-@end verbatim
+@verbatiminclude format/cer-load.cddl
 
-@code{kid} is a hash calculated over the @code{pub} field and used to
+@code{pkid} is a hash calculated over the @code{pub} field and used to
 form UUIDv4. But it may be formed another way, no limitations. Depending
-on @code{pub.a}, that may be different hash like Streebog-256 or
+on @code{pub/a}, that may be different hash like Streebog-256 or
 SHAKE-128 (let's stop using SHA-2!).
 
 @code{sub} is a subject name. Its values are UTF-8 strings. Currently no
 constraints on what fields must be present.
 
 @code{exp}iration period @strong{must} contain TAI64 datetime, without
-any nanoseconds part.
+nanoseconds part.
 
 @code{ku} (key usage) contains supposed usage contexts like being CA
 (@code{ca}), or using it solely for either signing (@code{sig}), or
@@ -41,26 +31,36 @@ It @strong{must} be absent if empty. Extension is a map with expected
 @code{t}ype field containing the identifier of the extension. Other
 extension's keys are defined by its type.
 
+There @strong{must} be single signature and single hash used during
+prehashing.
+
 Example minimal certificate may look like:
 
 @verbatim
 {
+    "hash": ["streebog256"]
     "load": {
         "t": "cer",
         "v": {
             "exp": [TAI64, TAI64],
-            "kid": UUID(SKID),
-            "pub": {"a": "gost3410-256A", "v": BIN(...)},
+            "pub": {"a": "gost3410-256A", "v": 'pubkey'},
             "sub": {"CN": "Test", "O": "Testers"},
+            "pkid": UUID(hash(pub)),
         },
     },
-    "sigs": [{"kid": UUID(AKID), "sign": {"a": "gost3410-256A", "v": BIN(...)}}],
+    "sigs": [{
+        "tbs": {
+            "sid": UUID(signer's pkid),
+            "hash": {"streebog256": 'hash value'},
+        },
+        "sign": {"a": "gost3410-256A", "v": 'signature'},
+    }],
 }
 @end verbatim
 
-@node Certificate-GOST3410
-@subsection Certificate with GOST R 34.10-2012
+@node cer-gost3410
+@subsection cer with GOST R 34.10-2012
 
 Same rules of serialisation must be used as with
-@ref{SignedData-GOST3410, SignedData}. KID's UUIDv4 is advised to be
-calculated by using Streebog-256 hash over the encoded "pub" field.
+@ref{signed-data-gost3410, signed-data-gost3410}. @code{pkid} and
+@code{cid} should be calculated using Streebog-256 hash.
diff --git a/spec/format/hashed-data.cddl b/spec/format/hashed-data.cddl
new file mode 100644 (file)
index 0000000..5d0c8b1
--- /dev/null
@@ -0,0 +1,8 @@
+ai = text ; algorithm identifier
+
+hashed-data = {
+    a: [+ ai],
+    t: text, ; type of the content
+    v: bytes / text / blob / map / list, ; content itself
+    hash: [+ bytes], ; hash values
+}
similarity index 70%
rename from spec/format/hashed.texi
rename to spec/format/hashed-data.texi
index 7509779dcdcb28b09b7e6fdbf7169cd33e21df3524664f0179fbe2f4570388e5..f39966d7baee09aeb783020bccfe1baeb0e8dac17520849df3653f9daedeb0f8 100644 (file)
@@ -1,17 +1,10 @@
-@node HashedData
-@cindex HashedData
-@section HashedData format
+@node hashed-data
+@cindex hashed-data
+@section hashed-data format
 
 Integrity protected container, analogue to CMS'es DigestedData structure.
 
-@verbatim
-{
-    "a": ["ID", ..."],
-    "t": "ID",
-    "v": BIN/STR or BLOB or MAP/LIST,
-    "hash": [BIN(hash value), ...],
-}
-@end verbatim
+@verbatiminclude format/hashed-data.cddl
 
 @code{/a} tells what algorithms will be used to hash the data.
 
index 6780942f767e6153215fb29cee7c502f48bdb6ec12618394ec3df4fad0ff177e..9ac3a7453b831acf1b52aefb39bda2ee44eba31c8b838430ce4c1763e4cf46a7 100644 (file)
@@ -2,8 +2,10 @@
 @unnumbered Formats
 
 Here are some suggested formats.
+They are written in
+@url{https://datatracker.ietf.org/doc/html/rfc8610, CDDL}-like format.
 
-@include format/prv.texi
-@include format/signed.texi
+@include format/private-key.texi
+@include format/signed-data.texi
 @include format/cer.texi
-@include format/hashed.texi
+@include format/hashed-data.texi
diff --git a/spec/format/private-key.cddl b/spec/format/private-key.cddl
new file mode 100644 (file)
index 0000000..30c292e
--- /dev/null
@@ -0,0 +1,4 @@
+private-key = {
+    a: text, ; algorithm identifier
+    v: bytes, ; private key's value
+}
diff --git a/spec/format/private-key.texi b/spec/format/private-key.texi
new file mode 100644 (file)
index 0000000..c55f442
--- /dev/null
@@ -0,0 +1,12 @@
+@node private-key
+@cindex private-key
+@section private-key format
+
+Private key is stored in trivial map:
+
+@verbatiminclude format/private-key.cddl
+
+@node private-key-gost3410
+@subsection private-key with GOST R 34.10-2012
+
+Big-endian private key representation must be used.
diff --git a/spec/format/prv.texi b/spec/format/prv.texi
deleted file mode 100644 (file)
index c4192db..0000000
+++ /dev/null
@@ -1,17 +0,0 @@
-@node PrivateKey
-@cindex PrivateKey
-@section PrivateKey format
-
-Private key is stored in trivial map:
-
-@verbatim
-{
-    "a": "ID",
-    "v": BIN(private key's value),
-}
-@end verbatim
-
-@node PrivateKey-GOST3410
-@subsection PrivateKey with GOST R 34.10-2012
-
-Big-endian private key representation must be used.
diff --git a/spec/format/signed-data-sig-tbs.cddl b/spec/format/signed-data-sig-tbs.cddl
new file mode 100644 (file)
index 0000000..231733b
--- /dev/null
@@ -0,0 +1,5 @@
+sig-tbs = {
+    t: text, ; = /load/t
+    v: '' / any, ; empty string if prehashed, /load/v otherwise
+    tbs: map, ; = /sigs/?/tbs
+}
diff --git a/spec/format/signed-data.cddl b/spec/format/signed-data.cddl
new file mode 100644 (file)
index 0000000..0d75261
--- /dev/null
@@ -0,0 +1,28 @@
+ai = text ; algorithm identifier
+av = {a: ai, v: bytes}
+
+signed-data = {
+    ? hash: [+ ai], ; when using prehashing
+    load: {
+        t: text,
+        ? v: bytes / text / blob / map / list,
+    },
+    sigs: [+ sig],
+    ? certs: [+ cer],
+}
+
+cer = signed-data ; with /load/t = cer
+
+sig = {
+    tbs: {
+        sid: uuid, ; signer's public key id
+        ? hash: {ai => bytes}, ; when using prehashing
+        ? when: tai64 / tai64n,
+        * text => any
+    },
+    sign: av,
+    ? cer-loc: [+ url],
+    * text => any
+}
+
+url = text
diff --git a/spec/format/signed-data.texi b/spec/format/signed-data.texi
new file mode 100644 (file)
index 0000000..92367cd
--- /dev/null
@@ -0,0 +1,39 @@
+@node signed-data
+@cindex signed-data
+@section signed-data format
+
+That resembles @url{https://datatracker.ietf.org/doc/html/rfc5652, CMS}
+(PKCS#7) ASN.1-based format.
+
+@verbatiminclude format/signed-data.cddl
+
+Signature is created by signing the following encoded MAP:
+
+@verbatiminclude format/signed-data-sig-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
+MAP or LIST, then its encoded binary representation is hashed. If it is
+BIN/STR or BLOB, then its binary contents are hashed. So signature will
+stay the same even if data is converted from BIN to BLOB.
+
+If @code{/load/v} is absent, then it is detached and must be provided as
+a binary data from outside.
+
+@code{/sigs/*/tbs/when} is optional signing time.
+
+Additional values that must be protected (covered by signature) are
+placed in @code{/sigs/*/tbs} map. Non-protected (informational) fields
+are placed outside it.
+
+@code{/certs} are optionally provided @ref{cer, certificates} to
+help creating the whole verification chain. They are placed outside
+@code{/sigs}, because some of them may be shared among signers.
+
+@node signed-data-gost3410
+@subsection signed-data 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
+in @code{BE(X)||BE(Y)} format. Signature is in @code{BE(S)||BE(R)}
+format.
diff --git a/spec/format/signed.texi b/spec/format/signed.texi
deleted file mode 100644 (file)
index 40601b8..0000000
+++ /dev/null
@@ -1,110 +0,0 @@
-@node SignedData
-@cindex SignedData
-@section SignedData format
-
-Signing of arbitrary data can be done in two ways: either with
-pre-hashing the data, or signing it directly. That resembles
-@url{https://datatracker.ietf.org/doc/html/rfc5652, CMS} (PKCS#7)
-ASN.1-based format.
-
-@table @asis
-
-@item Pre-hashed version:
-
-@verbatim
-{
-    "hash": ["ID", ...],
-    "load": {
-        "t": "ID",
-        ?"v": BIN/STR or BLOB or MAP/LIST,
-    },
-    "sigs": [
-        {
-            "kid": UUID(signer's public key-based UUID),
-            "load": {
-                "hash": {"ID": BIN(one of hashes value), ...},
-                ?"when": TAI64*,
-            },
-            "sign": {"a": "ID", "v": BIN(signature value)},
-            ?"cer-loc": ["URL", ...],
-        },
-        ...
-    ],
-    ?"certs": [...],
-}
-@end verbatim
-
-@code{/hash} tells what algorithms will be used to hash the data of
-@code{/load.v}. Signature is created by signing the following encoded MAP:
-
-@verbatim
-{
-    "t": "/load.t value",
-    "v": BIN(""),
-    "kid": UUID(/sigs.?.kid value)
-    "load": MAP(/sigs.?.load value),
-}
-@end verbatim
-
-If @code{/load.v} is either a MAP or LIST, then its encoded binary
-representation is hashed. If it is BIN/STR or BLOB, then its binary
-contents are hashed. So signature will stay the same even if data is
-converted from BIN to BLOB.
-
-@item Plain version:
-
-@verbatim
-{
-    "load": {
-        "t": "ID",
-        ?"v": BIN/STR or MAP/LIST,
-    },
-    "sigs": [
-        {
-            "kid": UUID(signer's public key-based UUID),
-            ?"load": {
-                ?"when": TAI64*,
-            },
-            "sign": {"a": "ID", "v": BIN(signature value)},
-            ?"cer-loc": ["URL", ...],
-        },
-        ...
-    ],
-    ?"certs": [...],
-}
-@end verbatim
-
-If @code{/sigs.?.load} is empty, then it @strong{must not} be present.
-Signature is created by signing the following encoded MAP:
-
-@verbatim
-{
-    "t": "/load.t value",
-    "v": /load.v's value,
-    "kid": UUID(/sigs.?.kid value)
-    "load": MAP(/sigs.?.load value or empty MAP),
-}
-@end verbatim
-
-@end table
-
-If @code{/load.v} is absent, then it is detached and must be provided as
-a binary data from outside.
-
-@code{/sigs.*.load.when} is optional signing time.
-
-Additional values that must be protected (covered by signature) are
-placed in @code{/sigs.*.load} map. Non-protected (informational) fields
-are placed outside it.
-
-@code{/cers} are optionally provided @ref{Certificate, certificates} to
-help creating the whole verification chain. They are placed outside
-@code{/sigs}, because some of them may be shared among signers.
-
-@node SignedData-GOST3410
-@subsection SignedData 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
-in @code{BE(X)||BE(Y)} format. Signature is in @code{BE(S)||BE(R)}
-format.