From d5a937763d19801648e9e0c0aed415722b4db7b79394e235780a8a66905aea65 Mon Sep 17 00:00:00 2001 From: Sergey Matveev Date: Wed, 9 Oct 2024 12:00:00 +0300 Subject: [PATCH] Prehashed certificate and CDDLs --- gyac/yacpki/algo.go | 11 ++ gyac/yacpki/cer.go | 100 +++++++++++----- gyac/yacpki/cmd/yacertool/main.go | 6 +- spec/format/cer-load.cddl | 15 +++ spec/format/cer.texi | 52 ++++----- spec/format/hashed-data.cddl | 8 ++ spec/format/{hashed.texi => hashed-data.texi} | 15 +-- spec/format/index.texi | 8 +- spec/format/private-key.cddl | 4 + spec/format/private-key.texi | 12 ++ spec/format/prv.texi | 17 --- spec/format/signed-data-sig-tbs.cddl | 5 + spec/format/signed-data.cddl | 28 +++++ spec/format/signed-data.texi | 39 +++++++ spec/format/signed.texi | 110 ------------------ 15 files changed, 230 insertions(+), 200 deletions(-) create mode 100644 spec/format/cer-load.cddl create mode 100644 spec/format/hashed-data.cddl rename spec/format/{hashed.texi => hashed-data.texi} (70%) create mode 100644 spec/format/private-key.cddl create mode 100644 spec/format/private-key.texi delete mode 100644 spec/format/prv.texi create mode 100644 spec/format/signed-data-sig-tbs.cddl create mode 100644 spec/format/signed-data.cddl create mode 100644 spec/format/signed-data.texi delete mode 100644 spec/format/signed.texi diff --git a/gyac/yacpki/algo.go b/gyac/yacpki/algo.go index 0507478..96ece9c 100644 --- a/gyac/yacpki/algo.go +++ b/gyac/yacpki/algo.go @@ -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: diff --git a/gyac/yacpki/cer.go b/gyac/yacpki/cer.go index 6a40913..1d3991f 100644 --- a/gyac/yacpki/cer.go +++ b/gyac/yacpki/cer.go @@ -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 } diff --git a/gyac/yacpki/cmd/yacertool/main.go b/gyac/yacpki/cmd/yacertool/main.go index ade183a..5cd371a 100644 --- a/gyac/yacpki/cmd/yacertool/main.go +++ b/gyac/yacpki/cmd/yacertool/main.go @@ -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 index 0000000..83e0515 --- /dev/null +++ b/spec/format/cer-load.cddl @@ -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] diff --git a/spec/format/cer.texi b/spec/format/cer.texi index 674b0fd..851ca4d 100644 --- a/spec/format/cer.texi +++ b/spec/format/cer.texi @@ -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 index 0000000..5d0c8b1 --- /dev/null +++ b/spec/format/hashed-data.cddl @@ -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 +} diff --git a/spec/format/hashed.texi b/spec/format/hashed-data.texi similarity index 70% rename from spec/format/hashed.texi rename to spec/format/hashed-data.texi index 7509779..f39966d 100644 --- a/spec/format/hashed.texi +++ b/spec/format/hashed-data.texi @@ -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. diff --git a/spec/format/index.texi b/spec/format/index.texi index 6780942..9ac3a74 100644 --- a/spec/format/index.texi +++ b/spec/format/index.texi @@ -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 index 0000000..30c292e --- /dev/null +++ b/spec/format/private-key.cddl @@ -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 index 0000000..c55f442 --- /dev/null +++ b/spec/format/private-key.texi @@ -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 index c4192db..0000000 --- a/spec/format/prv.texi +++ /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 index 0000000..231733b --- /dev/null +++ b/spec/format/signed-data-sig-tbs.cddl @@ -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 index 0000000..0d75261 --- /dev/null +++ b/spec/format/signed-data.cddl @@ -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 index 0000000..92367cd --- /dev/null +++ b/spec/format/signed-data.texi @@ -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 index 40601b8..0000000 --- a/spec/format/signed.texi +++ /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. -- 2.50.0