const (
AlgoStreebog256 = "streebog256"
+ AlgoStreebog512 = "streebog512"
AlgoGOST3410256A = "gost3410-256A"
AlgoGOST3410256B = "gost3410-256B"
AlgoGOST3410256C = "gost3410-256C"
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:
"crypto"
"crypto/rand"
"errors"
+ "hash"
"time"
"github.com/google/uuid"
"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 {
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) {
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
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
}
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
}
}
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)
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
}
}
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)
}
--- /dev/null
+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]
-@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
@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.
--- /dev/null
+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
+}
-@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.
@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
--- /dev/null
+private-key = {
+ a: text, ; algorithm identifier
+ v: bytes, ; private key's value
+}
--- /dev/null
+@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.
+++ /dev/null
-@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.
--- /dev/null
+sig-tbs = {
+ t: text, ; = /load/t
+ v: '' / any, ; empty string if prehashed, /load/v otherwise
+ tbs: map, ; = /sigs/?/tbs
+}
--- /dev/null
+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
--- /dev/null
+@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.
+++ /dev/null
-@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.