From 7ec68ca83dd421b4747b19b80ef8ce7238f37f6efd47c8f130e1ef4acb71c663 Mon Sep 17 00:00:00 2001 From: Sergey Matveev Date: Sat, 12 Oct 2024 17:11:35 +0300 Subject: [PATCH] Yet another revised certificate format --- gyac/cmd/test-vector-anys/main.go | 2 +- gyac/cmd/test-vector-manual/main.go | 3 +- gyac/dec.go | 10 +- gyac/enc.go | 2 +- gyac/reflect.go | 4 +- gyac/yacpki/algo.go | 24 ++- gyac/yacpki/cer.go | 278 +++++++++++++++------------ gyac/yacpki/cmd/yacertool/basic.t | 33 ++-- gyac/yacpki/cmd/yacertool/main.go | 124 ++++++------ gyac/yacpki/cmd/yacsdtool/main.go | 106 ++++++++++ gyac/yacpki/signed-data.go | 143 ++++++++++++++ gyac/yacpki/utils/utils.go | 25 +++ spec/format/cer-load.cddl | 11 +- spec/format/cer-sig-tbs.cddl | 8 + spec/format/cer.texi | 96 ++++++--- spec/format/hashed-data.texi | 14 +- spec/format/private-key.cddl | 8 +- spec/format/private-key.texi | 12 ++ spec/format/signed-data-sig-tbs.cddl | 2 +- spec/format/signed-data.cddl | 16 +- spec/format/signed-data.texi | 7 + 21 files changed, 671 insertions(+), 257 deletions(-) create mode 100644 gyac/yacpki/cmd/yacsdtool/main.go create mode 100644 gyac/yacpki/signed-data.go create mode 100644 gyac/yacpki/utils/utils.go create mode 100644 spec/format/cer-sig-tbs.cddl diff --git a/gyac/cmd/test-vector-anys/main.go b/gyac/cmd/test-vector-anys/main.go index 92a2cde..0f63d6b 100644 --- a/gyac/cmd/test-vector-anys/main.go +++ b/gyac/cmd/test-vector-anys/main.go @@ -57,7 +57,7 @@ func main() { []any{}, map[string]any{}, gyac.MakeBlob(123, []byte{}), - uuid.MustParse("00000000-0000-0000-0000-000000000000"), + uuid.Nil, &gyac.Raw{ T: gyac.AtomTAI64, V: []byte("\x00\x00\x00\x00\x00\x00\x00\x00"), diff --git a/gyac/cmd/test-vector-manual/main.go b/gyac/cmd/test-vector-manual/main.go index b053753..aa0048b 100644 --- a/gyac/cmd/test-vector-manual/main.go +++ b/gyac/cmd/test-vector-manual/main.go @@ -169,8 +169,7 @@ func main() { buf = gyac.AtomBlobEncode(buf, 123) buf = gyac.AtomBinEncode(buf, []byte{}) } - buf = gyac.AtomUUIDEncode(buf, - uuid.MustParse("00000000-0000-0000-0000-000000000000")) + buf = gyac.AtomUUIDEncode(buf, uuid.Nil) buf = gyac.AtomRawEncode(buf, &gyac.Raw{ T: gyac.AtomTAI64, V: []byte("\x00\x00\x00\x00\x00\x00\x00\x00"), diff --git a/gyac/dec.go b/gyac/dec.go index ef552b8..8f41f71 100644 --- a/gyac/dec.go +++ b/gyac/dec.go @@ -51,6 +51,10 @@ type Item struct { T byte } +func (i *Item) Typ() ItemType { + return ItemType(i.T) +} + type Raw struct { V []byte T AtomType @@ -121,7 +125,7 @@ func AtomDecode(buf []byte) (item *Item, off int, err error) { err = ErrNotEnough return } - if ItemType(item.T) == ItemBin { + if item.Typ() == ItemBin { item.V = buf[1+ll : 1+ll+l] } else { s := unsafe.String(unsafe.SliceData(buf[1+ll:]), l) @@ -172,7 +176,7 @@ func AtomDecode(buf []byte) (item *Item, off int, err error) { err = ErrLenNonMinimal return } - if ItemType(item.T) == ItemUInt { + if item.Typ() == ItemUInt { item.V = v } else { item.V = -1 - int64(v) @@ -293,7 +297,7 @@ func DecodeItem(buf []byte) (item *Item, tail []byte, err error) { } buf = buf[off:] tail = buf - switch ItemType(item.T) { + switch item.Typ() { case ItemList: var sub *Item var v []*Item diff --git a/gyac/enc.go b/gyac/enc.go index 8dca15a..b59d8de 100644 --- a/gyac/enc.go +++ b/gyac/enc.go @@ -273,7 +273,7 @@ func AtomRawEncode(buf []byte, raw *Raw) []byte { } func EncodeItem(buf []byte, item *Item) []byte { - switch ItemType(item.T) { + switch item.Typ() { case ItemNIL: return AtomNILEncode(buf) case ItemBool: diff --git a/gyac/reflect.go b/gyac/reflect.go index d4f9514..3d21c3f 100644 --- a/gyac/reflect.go +++ b/gyac/reflect.go @@ -152,6 +152,8 @@ func ItemFromGo(v any) *Item { var empty bool item := ItemFromGo(fv.Interface()) switch ItemType(item.T) { + case ItemNIL: + empty = true case ItemList: if len(item.V.([]*Item)) == 0 { empty = true @@ -231,7 +233,7 @@ func DecodeToStruct(dst any, raw []byte) (tail []byte, err error) { if err != nil { return } - if ItemType(item.T) != ItemMap { + if item.Typ() != ItemMap { err = errors.New("non-map") return } diff --git a/gyac/yacpki/algo.go b/gyac/yacpki/algo.go index 96ece9c..99997a0 100644 --- a/gyac/yacpki/algo.go +++ b/gyac/yacpki/algo.go @@ -1,12 +1,16 @@ package yacpki import ( + "bytes" "hash" "log" + "github.com/google/uuid" "go.cypherpunks.su/gogost/v6/gost3410" "go.cypherpunks.su/gogost/v6/gost34112012256" "go.cypherpunks.su/gogost/v6/gost34112012512" + "go.cypherpunks.su/yac/gyac" + "go.cypherpunks.su/yac/gyac/yacpki/utils" ) const ( @@ -31,6 +35,22 @@ type AV struct { V []byte `yac:"v"` } +func (av *AV) Id() (id uuid.UUID) { + var hasher hash.Hash + switch av.A { + case AlgoGOST3410256A, AlgoGOST3410256B, AlgoGOST3410256C, AlgoGOST3410256D, AlgoGOST3410512A, AlgoGOST3410512B, AlgoGOST3410512C: + hasher = gost34112012256.New() + default: + panic("unsupported algorithm") + } + utils.MustWrite(hasher, gyac.EncodeItem(nil, gyac.ItemFromGo(av))) + id, err := uuid.NewRandomFromReader(bytes.NewReader(hasher.Sum(nil))) + if err != nil { + panic(err) + } + return id +} + func GOST3410CurveByName(name string) (curve *gost3410.Curve) { switch name { case AlgoGOST3410256A: @@ -47,6 +67,8 @@ func GOST3410CurveByName(name string) (curve *gost3410.Curve) { curve = gost3410.CurveIdtc26gost341012512paramSetB() case AlgoGOST3410512C: curve = gost3410.CurveIdtc26gost341012512paramSetC() + default: + log.Fatal("unknown curve") } return } @@ -58,7 +80,7 @@ func HasherByKeyAlgo(a string) hash.Hash { case AlgoGOST3410512A, AlgoGOST3410512B, AlgoGOST3410512C: return gost34112012512.New() default: - log.Fatal("unsupported CA algorithm") + log.Fatal("unsupported algorithm") } return nil } diff --git a/gyac/yacpki/cer.go b/gyac/yacpki/cer.go index 1d3991f..c3675c8 100644 --- a/gyac/yacpki/cer.go +++ b/gyac/yacpki/cer.go @@ -1,93 +1,150 @@ package yacpki import ( - "bytes" "crypto" - "crypto/rand" "errors" - "hash" + "fmt" "time" "github.com/google/uuid" "go.cypherpunks.su/gogost/v6/gost3410" - "go.cypherpunks.su/gogost/v6/gost34112012256" "go.cypherpunks.su/yac/gyac" + "go.cypherpunks.su/yac/gyac/yacpki/utils" ) -type SignedDataLoad struct { - V any `yac:"v"` - T string `yac:"t"` -} - -type SigTBS struct { - Hashes map[string][]byte `yac:"hash,omitempty"` - SID uuid.UUID `yac:"sid"` -} +const ( + KUCA = "ca" + KUSign = "sig" +) -type Sig struct { - TBS SigTBS `yac:"tbs,omitempty"` - CerLoc []string `yac:"cer-loc,omitempty"` - Sign AV `yac:"sign"` +type Pub struct { + A string `yac:"a"` + V []byte `yac:"v"` + Id uuid.UUID `yac:"id"` } -type SignedDataTBS struct { - V any `yac:"v"` - T string `yac:"t"` - TBS SigTBS `yac:"tbs"` +type CerLoad struct { + KU *map[string]*struct{} `yac:"ku,omitempty"` + Subj map[string]string `yac:"sub"` + Crit *[]map[string]any `yac:"crit,omitempty"` + Pub []Pub `yac:"pub"` } -type SignedData struct { - Hashes []string `yac:"hash,omitempty"` - Load SignedDataLoad `yac:"load"` - Sigs []Sig `yac:"sigs"` - Cers []SignedData `yac:"certs,omitempty"` +func CerParse(data []byte) (sd *SignedData, tail []byte, err error) { + sd, tail, err = SignedDataParse(data) + if err != nil { + return + } + err = sd.CerParse() + return } -type CerLoad struct { - KU []string `yac:"ku,omitempty"` - Subj map[string]string `yac:"sub"` - Exp []time.Time `yac:"exp"` - Crit []map[string]any `yac:"crit,omitempty"` - Pub AV `yac:"pub"` - PKID uuid.UUID `yac:"pkid"` +func (sd *SignedData) CerParse() error { + if sd.Load.T != "cer" { + return errors.New("CerParse: wrong load type") + } + if len(sd.Sigs) == 0 { + return errors.New("CerParse: missing sigs") + } + for _, sig := range sd.Sigs { + if sig.TBS.Hashes != nil { + return errors.New("CerParse: prehashed SignedData") + } + if sig.TBS.CID == nil { + return errors.New("CerParse: missing cid") + } + if sig.TBS.Exp == nil { + return errors.New("CerParse: missing exp") + } + } + var load CerLoad + var err error + if v, ok := sd.Load.V.(map[string]any); ok { + err = gyac.MapToStruct(&load, v) + } else { + err = errors.New("CerParse: wrong /load/v") + } + if err != nil { + return err + } + sd.Load.V = load + if load.KU != nil { + if len(*load.KU) == 0 { + return errors.New("CerParse: empty ku") + } + for _, v := range *load.KU { + if v != nil { + return errors.New("CerParse: non-nil ku value") + } + } + } + if len(load.Subj) == 0 { + return errors.New("CerParse: empty sub") + } + if load.Crit != nil { + if len(*load.Crit) == 0 { + return errors.New("CerParse: empty crit") + } + return errors.New("CerParse: currently no critical extensions are supported") + } + if len(load.Pub) == 0 { + return errors.New("CerParse: empty pub") + } + for _, pub := range load.Pub { + if len(pub.A) == 0 || len(pub.V) == 0 || pub.Id == uuid.Nil { + return errors.New("CerParse: non-filled pub") + } + } + return nil } -func (tbs *CerLoad) HasCA() (hasCA bool) { - for _, ku := range tbs.KU { - if ku == "ca" { - hasCA = true - } +func (cer *CerLoad) CanSign() bool { + if cer.KU == nil { + return false } - return + if _, ok := (*cer.KU)[KUSign]; !ok { + return false + } + if len(cer.Pub) != 1 { + return false + } + return true } -func PKIDFromPub(pub *AV) (kid uuid.UUID) { - hasher := gost34112012256.New() - hasher.Write(gyac.EncodeItem(nil, gyac.ItemFromGo(pub))) - var err error - kid, err = uuid.NewRandomFromReader(bytes.NewReader(hasher.Sum(nil))) +func (sd *SignedData) CerIssueWith( + parent *CerLoad, + prv crypto.Signer, + since, till time.Time, +) error { + exp := []time.Time{since, till} + cid, err := uuid.NewV7() if err != nil { - panic(err) + return err } - return + return sd.SignWith(parent, prv, SigTBS{CID: &cid, Exp: &exp}) } -var SigInvalid = errors.New("signature is invalid") +var ErrSigInvalid = errors.New("signature is invalid") func (cer *CerLoad) CheckSignature(signed, signature []byte) (err error) { - switch cer.Pub.A { + if !cer.CanSign() { + err = errors.New("cer can not sign") + return + } + pub := cer.Pub[0] + switch pub.A { case AlgoGOST3410256A, AlgoGOST3410256B, AlgoGOST3410256C, AlgoGOST3410256D, AlgoGOST3410512A, AlgoGOST3410512B, AlgoGOST3410512C: - var pub *gost3410.PublicKey - pub, err = gost3410.NewPublicKeyBE(GOST3410CurveByName(cer.Pub.A), cer.Pub.V) + var pk *gost3410.PublicKey + pk, err = gost3410.NewPublicKeyBE(GOST3410CurveByName(pub.A), pub.V) if err != nil { return } - hasher := HasherByKeyAlgo(cer.Pub.A) - hasher.Write(signed) + hasher := HasherByKeyAlgo(pub.A) + utils.MustWrite(hasher, signed) var valid bool - valid, err = pub.VerifyDigest(hasher.Sum(nil), signature) + valid, err = pk.VerifyDigest(hasher.Sum(nil), signature) if !valid { - err = SigInvalid + err = ErrSigInvalid } default: err = errors.New("unsupported signature algorithm") @@ -95,29 +152,18 @@ func (cer *CerLoad) CheckSignature(signed, signature []byte) (err error) { return } -func (sd *SignedData) CheckSignatureFrom(parent *CerLoad) (err error) { - sig := sd.Sigs[0] - 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") +func (sd *SignedData) CerCheckSignatureFrom(parent *CerLoad) (err error) { + if len(sd.Sigs) != 1 { + err = errors.New("can verify only single signature") return } - hashNew, ok := HashToNew[sd.Hashes[0]] - if !ok { - err = errors.New("unsupported hash") + if !parent.CanSign() { + err = errors.New("parent can not sign") 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") + sig := sd.Sigs[0] + if sig.TBS.SID != parent.Pub[0].Id { + err = errors.New("sid != parent pub id") return } tbs := SignedDataTBS{T: sd.Load.T, V: sd.Load.V, TBS: sig.TBS} @@ -127,61 +173,51 @@ func (sd *SignedData) CheckSignatureFrom(parent *CerLoad) (err error) { ) } -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 +func (sd *SignedData) CerLoad() *CerLoad { + l, ok := sd.Load.V.(CerLoad) + if ok { + return &l } - sd.Hashes = append(sd.Hashes, hashAlgo) - sd.Sigs = append(sd.Sigs, sig) - return + return nil } -func CerParse(data []byte) (sd *SignedData, tail []byte, err error) { - var s SignedData - tail, err = gyac.DecodeToStruct(&s, data) - if err != nil { - return +func (sd *SignedData) CerVerify(cers []*SignedData, t time.Time) (err error) { + { + exp := *(sd.Sigs[0].TBS.Exp) + if t.Before(exp[0]) || t.Equal(exp[0]) { + err = errors.New("cer is not active") + return + } + if t.After(exp[1]) || t.Equal(exp[1]) { + err = errors.New("cer is expired") + return + } } - sd = &s - if sd.Load.T != "cer" { - err = errors.New("SignedData: not \"cer\" type") - return + sid := sd.Sigs[0].TBS.SID + if sid == sd.CerLoad().Pub[0].Id { + return sd.CerCheckSignatureFrom(sd.CerLoad()) } - if len(sd.Sigs) != 1 { - err = errors.New("SignedData: wrong number of sigs") - return + idToCer := make(map[uuid.UUID]*SignedData, len(cers)) + for _, cer := range cers { + cerLoad := cer.CerLoad() + if !cerLoad.CanSign() { + err = errors.New("cer can not sign") + return + } + if _, ok := (*cerLoad.KU)[KUCA]; !ok { + err = errors.New("cer can not ca") + return + } + idToCer[cerLoad.Pub[0].Id] = cer } - if len(sd.Hashes) != 1 { - err = errors.New("SignedData: wrong number of hashes") + signer := idToCer[sid] + if signer == nil { + err = fmt.Errorf("no cer found for sid: %v", sd.Sigs[0].TBS.SID) return } - { - var load CerLoad - err = gyac.MapToStruct(&load, sd.Load.V.(map[string]any)) - if err != nil { - return - } - sd.Load.V = &load + err = sd.CerCheckSignatureFrom(signer.CerLoad()) + if err != nil { + return } - return + return signer.CerVerify(cers, t) } diff --git a/gyac/yacpki/cmd/yacertool/basic.t b/gyac/yacpki/cmd/yacertool/basic.t index a4ea1ca..0c93675 100755 --- a/gyac/yacpki/cmd/yacertool/basic.t +++ b/gyac/yacpki/cmd/yacertool/basic.t @@ -6,32 +6,39 @@ test_description="Check that basic functionality works" TMPDIR=${TMPDIR:-/tmp} -subj='{"CN": "CA", "C": "RU"}' +subj="-subj CN=CA -subj C=RU" test_expect_success "CA generation" "yacertool \ -algo gost3410-512C \ - -ku ca \ - -prv $TMPDIR/ca.prv -cer $TMPDIR/ca.cer \ - -subj '$subj'" - + -ku ca -ku sig $subj \ + -prv $TMPDIR/ca.prv -cer $TMPDIR/ca.cer" test_expect_success "CA regeneration" "yacertool \ -algo gost3410-512C \ - -ku ca \ + -ku ca -ku sig $subj \ -prv $TMPDIR/ca.prv -cer $TMPDIR/ca.cer \ - -reuse-key \ - -subj '$subj'" + -reuse-key" test_expect_success "CA self-signature" "yacertool \ -ca-cer $TMPDIR/ca.cer \ -cer $TMPDIR/ca.cer \ -verify" -subj='{"CN": "EE", "C": "RU"}' +subj="-subj CN=SubCA -subj C=RU" +test_expect_success "SubCA generation" "yacertool \ + -ku ca -ku sig $subj \ + -prv $TMPDIR/subca.prv -cer $TMPDIR/subca.cer \ + -ca-cer $TMPDIR/ca.cer -ca-prv $TMPDIR/ca.prv" +test_expect_success "SubCA signature" "yacertool \ + -ca-cer $TMPDIR/ca.cer \ + -cer $TMPDIR/subca.cer \ + -verify" + +subj="-subj CN=EE -subj C=RU" test_expect_success "EE generation" "yacertool \ - -algo gost3410-256A \ - -ca-prv $TMPDIR/ca.prv -ca-cer $TMPDIR/ca.cer \ - -prv $TMPDIR/ee.prv -cer $TMPDIR/ee.cer \ - -subj '$subj'" + -algo gost3410-256A $subj \ + -ca-prv $TMPDIR/subca.prv -ca-cer $TMPDIR/subca.cer \ + -prv $TMPDIR/ee.prv -cer $TMPDIR/ee.cer" test_expect_success "EE chain" "yacertool \ -ca-cer $TMPDIR/ca.cer \ + -ca-cer $TMPDIR/subca.cer \ -cer $TMPDIR/ee.cer \ -verify" diff --git a/gyac/yacpki/cmd/yacertool/main.go b/gyac/yacpki/cmd/yacertool/main.go index 5cd371a..a32fb3c 100644 --- a/gyac/yacpki/cmd/yacertool/main.go +++ b/gyac/yacpki/cmd/yacertool/main.go @@ -3,35 +3,49 @@ package main import ( "crypto" "crypto/rand" - "encoding/json" + "errors" "flag" - "fmt" "io" "log" "os" - "sort" + "strings" "time" "go.cypherpunks.su/gogost/v6/gost3410" "go.cypherpunks.su/yac/gyac" "go.cypherpunks.su/yac/gyac/yacpki" + "go.cypherpunks.su/yac/gyac/yacpki/utils" ) -func MustReadFile(p string) []byte { - data, err := os.ReadFile(p) - if err != nil { - panic(fmt.Errorf("read %s: %v", p, err)) - } - return data -} - func main() { - kuMap := make(map[string]struct{}) + ku := make(map[string]*struct{}) + subj := make(map[string]string) flag.Func( "ku", - "Optional key usage, may be specified multiple times", + "Optional key usage, can be specified multiple times", + func(v string) error { + ku[v] = nil + return nil + }, + ) + flag.Func( + "subj", + "Part of subject, key=value, can be specified multiple times", + func(v string) error { + s := strings.SplitN(v, "=", 2) + if len(s) != 2 { + return errors.New("invalid key=value") + } + subj[s[0]] = s[1] + return nil + }, + ) + var issuingCers []string + flag.Func( + "ca-cer", + "Add CA certificate to the chain", func(v string) error { - kuMap[v] = struct{}{} + issuingCers = append(issuingCers, v) return nil }, ) @@ -39,11 +53,7 @@ func main() { "Optional notBefore, \"2006-01-02 15:04:05\" format") lifetime := flag.Uint("lifetime", 365, "Lifetime of the certificate, days") - subjRaw := flag.String("subj", `{"CN": "test"}`, - "JSON map of the subject") algo := flag.String("algo", "gost3410-256A", "Public key algorithm") - issuingCer := flag.String("ca-cer", "", - "Path to certificate file for issuing with") issuingPrv := flag.String("ca-prv", "", "Path to private key file for issuing with") reuseKey := flag.Bool("reuse-key", false, @@ -59,19 +69,11 @@ func main() { log.Fatal("no -cer is set") } - var ku []string - for k := range kuMap { - ku = append(ku, k) - } - kuMap = nil - sort.Sort(gyac.ByLenFirst(ku)) - - var subj map[string]string - err := json.Unmarshal([]byte(*subjRaw), &subj) - if err != nil { - log.Fatalln("while parsing -subj:", err) + if !*verify && len(subj) == 0 { + log.Fatal("no -subj is set") } + var err error var since time.Time if *sinceRaw == "" { since = time.Now().UTC().Truncate(time.Second) @@ -84,43 +86,35 @@ func main() { till := since.Add(time.Duration(*lifetime) * 24 * time.Hour) var caPrv *gost3410.PrivateKey - var caCerLoad *yacpki.CerLoad - if *issuingCer != "" { + var caCers []*yacpki.SignedData + for _, issuingCer := range issuingCers { var sd *yacpki.SignedData - sd, _, err = yacpki.CerParse(MustReadFile(*issuingCer)) + sd, _, err = yacpki.CerParse(utils.MustReadFile(issuingCer)) if err != nil { log.Fatal(err) } - caCerLoad = sd.Load.V.(*yacpki.CerLoad) - if !*verify { - if *issuingPrv == "" { - log.Fatal("no -issuing-key is set") - } - var signer crypto.Signer - signer, err = yacpki.PrvParse(MustReadFile(*issuingPrv)) - if err != nil { - log.Fatal(err) - } - caPrv = signer.(*gost3410.PrivateKey) + caCers = append(caCers, sd) + } + if len(caCers) > 0 && !*verify { + if *issuingPrv == "" { + log.Fatal("no -ca-key is set") + } + var signer crypto.Signer + signer, err = yacpki.PrvParse(utils.MustReadFile(*issuingPrv)) + if err != nil { + log.Fatal(err) } + caPrv = signer.(*gost3410.PrivateKey) } if *verify { var sd *yacpki.SignedData - sd, _, err = yacpki.CerParse(MustReadFile(*cerPath)) + sd, _, err = yacpki.CerParse(utils.MustReadFile(*cerPath)) if err != nil { log.Fatal(err) } - cerLoad := sd.Load.V.(*yacpki.CerLoad) - sig := sd.Sigs[0] - if sig.TBS.SID != cerLoad.PKID && !caCerLoad.HasCA() { - log.Fatal("no \"ca\" KU met in CA") - } - err = sd.CheckSignatureFrom(caCerLoad) + err = sd.CerVerify(caCers, time.Now().UTC()) if err != nil { - if err == yacpki.SigInvalid { - os.Exit(1) - } log.Fatal(err) } return @@ -137,7 +131,7 @@ func main() { var prv *gost3410.PrivateKey if *reuseKey { var signer crypto.Signer - signer, err = yacpki.PrvParse(MustReadFile(*prvPath)) + signer, err = yacpki.PrvParse(utils.MustReadFile(*prvPath)) if err != nil { log.Fatal(err) } @@ -166,26 +160,28 @@ func main() { if err != nil { log.Fatal(err) } - cerLoad := yacpki.CerLoad{ - KU: ku, - Exp: []time.Time{since, till}, - Subj: subj, - Pub: yacpki.AV{A: *algo, V: pub.RawBE()}, + pubMap := yacpki.Pub{A: *algo, V: pub.RawBE()} + { + av := yacpki.AV{A: *algo, V: pub.RawBE()} + pubMap.Id = av.Id() } - cerLoad.PKID = yacpki.PKIDFromPub(&cerLoad.Pub) + cerLoad := yacpki.CerLoad{Subj: subj, Pub: []yacpki.Pub{pubMap}} + if len(ku) > 0 { + cerLoad.KU = &ku + } + var caCerLoad *yacpki.CerLoad if caPrv == nil { caPrv = prv caCerLoad = &cerLoad } else { - if !caCerLoad.HasCA() { - log.Fatal("no \"ca\" KU met in CA") - } + caCerLoad = caCers[0].CerLoad() } sd := yacpki.SignedData{Load: yacpki.SignedDataLoad{T: "cer", V: cerLoad}} - err = sd.PrehashedSignWith(caCerLoad, caPrv) + err = sd.CerIssueWith(caCerLoad, caPrv, since, till) if err != nil { log.Fatal(err) } + err = os.WriteFile(*cerPath, gyac.EncodeItem(nil, gyac.ItemFromGo(sd)), 0o666) if err != nil { log.Fatal(err) diff --git a/gyac/yacpki/cmd/yacsdtool/main.go b/gyac/yacpki/cmd/yacsdtool/main.go new file mode 100644 index 0000000..7473cb4 --- /dev/null +++ b/gyac/yacpki/cmd/yacsdtool/main.go @@ -0,0 +1,106 @@ +package main + +import ( + "bufio" + "bytes" + "crypto" + "flag" + "io" + "log" + "os" + "time" + + "go.cypherpunks.su/yac/gyac" + "go.cypherpunks.su/yac/gyac/yacpki" + "go.cypherpunks.su/yac/gyac/yacpki/utils" +) + +func main() { + prvPath := flag.String("prv", "", "Path to private key file") + cerPath := flag.String("cer", "", "Path to certificate file") + sdPath := flag.String("sd", "", "Path to signed-data file") + typ := flag.String("type", "data", "Type of the content, /load/t value") + hashAlgo := flag.String("hash", "", "Algorithm identifier of the hash to use") + verify := flag.Bool("verify", false, "Verify provided -cer with -ca-cer") + + flag.Parse() + log.SetFlags(log.Lshortfile) + + if *cerPath == "" { + log.Fatal("no -cer is set") + } + cer, _, err := yacpki.CerParse(utils.MustReadFile(*cerPath)) + if err != nil { + log.Fatal(err) + } + + var signer crypto.Signer + if !*verify { + if *prvPath == "" { + log.Fatal("no -prv is set") + } + signer, err = yacpki.PrvParse(utils.MustReadFile(*prvPath)) + if err != nil { + log.Fatal(err) + } + } + + hashNew := yacpki.HashToNew[*hashAlgo] + if hashNew == nil { + log.Fatal("unknown -hash specified") + } + hasher := hashNew() + _, err = io.Copy(hasher, bufio.NewReader(os.Stdin)) + if err != nil { + log.Fatal(err) + } + if *verify { + var sd *yacpki.SignedData + sd, _, err = yacpki.SignedDataParse(utils.MustReadFile(*sdPath)) + if err != nil { + log.Fatal(err) + } + if len(sd.Sigs) == 0 { + log.Fatal("no sigs") + } + sig := sd.Sigs[0] + if sd.Hashes == nil || sig.TBS.Hashes == nil { + log.Fatal("no hashes") + } + hashes := *sig.TBS.Hashes + hashExpect := hashes[*hashAlgo] + hashGot := hasher.Sum(nil) + if hashExpect == nil || !bytes.Equal(hashExpect, hashGot) { + log.Fatal("hash mismatch") + } + signer := cer.CerLoad() + if !signer.CanSign() { + log.Fatal("cer can not sign") + } + if sig.Sign.A != signer.Pub[0].A { + log.Fatal("differing signature algorithms") + } + err = sd.CerCheckSignatureFrom(signer) + if err != nil { + log.Fatal(err) + } + } else { + var sd yacpki.SignedData + sd.Load.T = *typ + sdHashes := []string{*hashAlgo} + sd.Hashes = &sdHashes + sigHashes := map[string][]byte{*hashAlgo: hasher.Sum(nil)} + when := time.Now().UTC().Truncate(1000 * time.Microsecond) + err = sd.SignWith(cer.CerLoad(), signer, yacpki.SigTBS{ + Hashes: &sigHashes, + When: &when, + }) + if err != nil { + log.Fatal(err) + } + err = os.WriteFile(*sdPath, gyac.EncodeItem(nil, gyac.ItemFromGo(sd)), 0o666) + if err != nil { + log.Fatal(err) + } + } +} diff --git a/gyac/yacpki/signed-data.go b/gyac/yacpki/signed-data.go new file mode 100644 index 0000000..7a69ff5 --- /dev/null +++ b/gyac/yacpki/signed-data.go @@ -0,0 +1,143 @@ +package yacpki + +import ( + "crypto" + "crypto/rand" + "errors" + "time" + + "github.com/google/uuid" + "go.cypherpunks.su/yac/gyac" + "go.cypherpunks.su/yac/gyac/yacpki/utils" +) + +type SignedDataLoad struct { + V any `yac:"v,omitempty"` + T string `yac:"t"` +} + +type SigTBS struct { + Hashes *map[string][]byte `yac:"hash,omitempty"` + CID *uuid.UUID `yac:"cid,omitempty"` + Exp *[]time.Time `yac:"exp,omitempty"` + When *time.Time `yac:"when,omitempty"` + SID uuid.UUID `yac:"sid"` +} + +type Sig struct { + 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 { + Hashes *[]string `yac:"hash,omitempty"` + Cers *[]*SignedData `yac:"certs,omitempty"` + Load SignedDataLoad `yac:"load"` + Sigs []*Sig `yac:"sigs"` +} + +func SignedDataParse(data []byte) (sd *SignedData, tail []byte, err error) { + var item *gyac.Item + item, tail, err = gyac.DecodeItem(data) + if err != nil { + return + } + sd, err = SignedDataParseItem(item) + return +} + +func SignedDataParseItem(item *gyac.Item) (sd *SignedData, err error) { + if item.Typ() != gyac.ItemMap { + err = errors.New("SignedDataParse: non-map") + return + } + var _sd SignedData + err = gyac.MapToStruct(&_sd, item.ToGo().(map[string]any)) + if err != nil { + return + } + sd = &_sd + + if sd.Hashes != nil && len(*sd.Hashes) == 0 { + err = errors.New("SignedDataParse: empty /hash") + return + } + if sd.Cers != nil { + if len(*sd.Cers) == 0 { + err = errors.New("SignedDataParse: empty /certs") + return + } + for _, cer := range *sd.Cers { + err = cer.CerParse() + if err != nil { + return + } + } + } + for _, sig := range sd.Sigs { + if sig.CerLoc != nil && len(*sig.CerLoc) == 0 { + err = errors.New("SignedDataParse: empty cer-loc") + return + } + if sig.TBS.Hashes != nil && len(*sig.TBS.Hashes) == 0 { + err = errors.New("SignedDataParse: empty hash") + return + } + if sig.TBS.Exp != nil { + if len(*sig.TBS.Exp) != 2 { + err = errors.New("SignedDataParse: wrong exp len") + return + } + for _, t := range *sig.TBS.Exp { + if t.Nanosecond() != 0 { + err = errors.New("SignedDataParse: exp with nanoseconds") + return + } + } + } + if sd.Hashes != nil { + if sig.TBS.Hashes == nil { + err = errors.New("SignedDataParse: /sigs: no hash") + return + } + var exists bool + for _, ai := range *sd.Hashes { + if _, ok := (*sig.TBS.Hashes)[ai]; ok { + exists = true + break + } + } + if !exists { + err = errors.New("SignedDataParse: /sigs: no hash") + return + } + } + } + return +} + +func (sd *SignedData) SignWith(parent *CerLoad, prv crypto.Signer, sigTBS SigTBS) error { + if !parent.CanSign() { + return errors.New("parent can not sign") + } + sigTBS.SID = parent.Pub[0].Id + sdTBS := SignedDataTBS{T: sd.Load.T, V: sd.Load.V, TBS: sigTBS} + sig := Sig{TBS: sigTBS} + hasher := HasherByKeyAlgo(parent.Pub[0].A) + utils.MustWrite(hasher, gyac.EncodeItem(nil, gyac.ItemFromGo(sdTBS))) + sig.Sign.A = parent.Pub[0].A + var err error + sig.Sign.V, err = prv.Sign(rand.Reader, hasher.Sum(nil), nil) + if err != nil { + return err + } + sd.Sigs = append(sd.Sigs, &sig) + return nil +} diff --git a/gyac/yacpki/utils/utils.go b/gyac/yacpki/utils/utils.go new file mode 100644 index 0000000..5390719 --- /dev/null +++ b/gyac/yacpki/utils/utils.go @@ -0,0 +1,25 @@ +package utils + +import ( + "fmt" + "io" + "os" +) + +func MustWrite(w io.Writer, data []byte) { + n, err := w.Write(data) + if err != nil { + panic(err) + } + if n != len(data) { + panic("not full write") + } +} + +func MustReadFile(p string) []byte { + data, err := os.ReadFile(p) + if err != nil { + panic(fmt.Errorf("read %s: %v", p, err)) + } + return data +} diff --git a/spec/format/cer-load.cddl b/spec/format/cer-load.cddl index 83e0515..801f0e0 100644 --- a/spec/format/cer-load.cddl +++ b/spec/format/cer-load.cddl @@ -1,15 +1,12 @@ ai = text ; algorithm identifier av = {a: ai, v: bytes} -certificate-load = { - ? ku: [+ ku], - exp: validity, - pub: av, +cer-load = { + ? ku: {+ ku => nil}, + pub: [+ {av, id: uuid}], 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] +ku = "ca" / "sig" / "app-name" / text diff --git a/spec/format/cer-sig-tbs.cddl b/spec/format/cer-sig-tbs.cddl new file mode 100644 index 0000000..2034d50 --- /dev/null +++ b/spec/format/cer-sig-tbs.cddl @@ -0,0 +1,8 @@ +cer-sig-tbs = { + sid: uuid, ; signer's public key id + cid: uuid, ; certificate's id + exp: validity, + * text => any +} + +validity = [since: tai64, till: tai64] diff --git a/spec/format/cer.texi b/spec/format/cer.texi index e630845..843d388 100644 --- a/spec/format/cer.texi +++ b/spec/format/cer.texi @@ -2,56 +2,91 @@ @cindex cer @section cer format -Certificate is the prehashed @ref{signed-data} structure with -@code{/load/t} equals to @code{cer}. @code{/load/v} must contain: +Certificate is the @ref{signed-data} structure. Its @code{/load/t} +equals to @code{cer}. @code{/load/v} contains @code{cer-load}: @verbatiminclude format/cer-load.cddl -@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 -SHAKE-128 (let's stop using SHA-2!). +@table @code -@code{sub} is a subject name. Its values are UTF-8 strings. Currently no -constraints on what fields must be present. +@item sub +Subject is a map of arbitrary strings. Currently no constraints on what +fields must be present. Each application and usage context defines it on +his own. But you may mimic X.509's subject with keys like "CN", "C", "O" +and similar ones. -@code{exp}iration period @strong{must} contain TAI64 datetime, without -nanoseconds part. +@item pub +Certificate may contain multiple public keys. -@code{ku} (key usage) contains supposed usage contexts like being CA -(@code{ca}), or using it solely for either signing (@code{sig}), or -key-agreement (@code{dh}) purposes. It @strong{must} be absent if empty. -Its items @strong{must} be length-first sorted (like MAP) and unique. -Possibly some additional usages like @code{email}, @code{ike}, -@code{codeSign} may be thought out. +That is @strong{solely} intended for tasks requiring more than single +key usage. For example @url{http://www.nncpgo.org, NNCP} uses one +curve25519 for (DH) encryption, one curve25519 for online authentication +and one ed25519 for signing purposes. All those three keys are used +together. That certificate's key usage field must contain something like +"nncp". -Optional @code{crit} list may contain other critical extensions. -Non-critical extensions may be placed just inside the map itself. -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. +If your keypair is intended for general purposes like signing of +arbitrary data, then single public key @strong{should} be used, with a +key usage like "sig". -There @strong{must} be single signature and single hash used during -prehashing. +Each public key contain the key itself, its algorithm identifier and key +identifier, that @strong{should} be generated as an UUIDv4 based on the +hash of the key. + +@item ku +Intended public key usage. Certificate @strong{must} be signed with the +key having "ca" key usage, unless it is self-signed. +Application-specific example with multiple public keys is described +above. + +It is a map with NIL values, to force deterministic encoding of the +list. It @strong{must} be absent if empty. + +@item crit +Optional list of critical (in terms of X.509) extensions. Non-critical +ones may be placed outside that map, directly in @code{cer-load}. It +@strong{must} be absent if empty. + +Each extension has required "t" field with specified extension type. All +other values are extension-specific. + +@end table + +signed-data's sig-tbs @strong{must} contain additional fields: + +@verbatiminclude format/cer-sig-tbs.cddl + +@table @code + +@item sid +Signing public key identifier. + +@item cid +Certificate's unique identifier. UUIDv7 is a good choice. But it may be +UUIDv4, or any desired method of generation. + +@item exp +Expiration period of the certificate. It @strong{must} contain TAI64 +datetime (no nanoseconds). + +@end table Example minimal certificate may look like: @verbatim { - "hash": ["streebog256"], "load": { "t": "cer", "v": { - "exp": [TAI64, TAI64], - "pub": {"a": "gost3410-256A", "v": 'pubkey'}, + "pub": [{"a": "gost3410-256A", "v": 'pubkey', "id": UUID(hash(pub))}], "sub": {"CN": "Test", "O": "Testers"}, - "pkid": UUID(hash(pub)), }, }, "sigs": [{ "tbs": { + "cid": UUID(certificate's id), "sid": UUID(signer's pkid), - "hash": {"streebog256": 'hash value'}, + "exp": [TAI64, TAI64], }, "sign": {"a": "gost3410-256A", "v": 'signature'}, }], @@ -62,5 +97,6 @@ Example minimal certificate may look like: @subsection cer with GOST R 34.10-2012 Same rules of serialisation must be used as with -@ref{signed-data-gost3410, signed-data-gost3410}. @code{pkid} and -@code{cid} should be calculated using Streebog-256 hash. +@ref{signed-data-gost3410, signed-data-gost3410}. Public key's +identifier and and @code{cid} should be calculated using Streebog-256 +hash. diff --git a/spec/format/hashed-data.texi b/spec/format/hashed-data.texi index f39966d..ad16189 100644 --- a/spec/format/hashed-data.texi +++ b/spec/format/hashed-data.texi @@ -2,7 +2,7 @@ @cindex hashed-data @section hashed-data format -Integrity protected container, analogue to CMS'es DigestedData structure. +Integrity protected container, CMS'es DigestedData analogue. @verbatiminclude format/hashed-data.cddl @@ -17,3 +17,15 @@ converted from BIN to BLOB. @code{/hash} contains the hash values for all corresponding @code{/a} algorithms. + +@node hashed-data-gost3411 +@subsection hashed-data with GOST R 34.11-2012 + +Streebog must be big-endian serialised. + +Following algorithm identifiers are acceptable: + +@verbatim +streebog256 +streebog512 +@end verbatim diff --git a/spec/format/private-key.cddl b/spec/format/private-key.cddl index 30c292e..e441adf 100644 --- a/spec/format/private-key.cddl +++ b/spec/format/private-key.cddl @@ -1,4 +1,4 @@ -private-key = { - a: text, ; algorithm identifier - v: bytes, ; private key's value -} +ai = text ; algorithm identifier +av = {a: ai, v: bytes} + +private-key = av diff --git a/spec/format/private-key.texi b/spec/format/private-key.texi index c55f442..64199c5 100644 --- a/spec/format/private-key.texi +++ b/spec/format/private-key.texi @@ -10,3 +10,15 @@ Private key is stored in trivial map: @subsection private-key with GOST R 34.10-2012 Big-endian private key representation must be used. + +Following algorithm identifiers are acceptable: + +@verbatim +gost3410-256A +gost3410-256B +gost3410-256C +gost3410-256D +gost3410-512A +gost3410-512B +gost3410-512C +@end verbatim diff --git a/spec/format/signed-data-sig-tbs.cddl b/spec/format/signed-data-sig-tbs.cddl index 231733b..f6fb011 100644 --- a/spec/format/signed-data-sig-tbs.cddl +++ b/spec/format/signed-data-sig-tbs.cddl @@ -1,5 +1,5 @@ sig-tbs = { t: text, ; = /load/t - v: '' / any, ; empty string if prehashed, /load/v otherwise + v: nil / any, tbs: map, ; = /sigs/?/tbs } diff --git a/spec/format/signed-data.cddl b/spec/format/signed-data.cddl index 0d75261..356f913 100644 --- a/spec/format/signed-data.cddl +++ b/spec/format/signed-data.cddl @@ -11,18 +11,20 @@ signed-data = { ? certs: [+ cer], } -cer = signed-data ; with /load/t = cer +cer = signed-data ; with /load/t = cer, /load/v = cer-load sig = { - tbs: { - sid: uuid, ; signer's public key id - ? hash: {ai => bytes}, ; when using prehashing - ? when: tai64 / tai64n, - * text => any - }, + tbs: sig-tbs, sign: av, ? cer-loc: [+ url], * text => any } url = text + +sig-tbs = { + sid: uuid, ; signer's public key id + ? hash: {ai => bytes}, ; when using prehashing + ? when: tai64 / tai64n, + * text => any +} diff --git a/spec/format/signed-data.texi b/spec/format/signed-data.texi index 92367cd..7583d90 100644 --- a/spec/format/signed-data.texi +++ b/spec/format/signed-data.texi @@ -37,3 +37,10 @@ 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. + +Following algorithm identifiers are acceptable for the hash: + +@verbatim +streebog256 +streebog512 +@end verbatim -- 2.50.0