From 5a3a082921976b9622f1a0fec6f13f47773f5f7f182a30579da36d543a83c690 Mon Sep 17 00:00:00 2001 From: Sergey Matveev Date: Sat, 30 Nov 2024 17:54:44 +0300 Subject: [PATCH] Add various suggested hash algorithms and Ed25519-BLAKE2b scheme --- gyac/yacpki/algo.go | 59 ++----- gyac/yacpki/cer.go | 23 ++- gyac/yacpki/cmd/yacertool/basic.t | 29 ++-- gyac/yacpki/cmd/yacertool/main.go | 102 ++++++++---- gyac/yacpki/ed25519-blake2b/.gitignore | 4 + gyac/yacpki/ed25519-blake2b/clean | 3 + .../ed25519-blake2b/ed25519-to-blake2b.patch | 153 ++++++++++++++++++ gyac/yacpki/ed25519-blake2b/mk-from-go | 30 ++++ gyac/yacpki/gost.go | 47 ++++++ gyac/yacpki/prv.go | 37 ++--- gyac/yacpki/signed-data.go | 16 +- spec/format/cer.texi | 19 ++- spec/format/hashed-data.texi | 52 +++++- spec/format/index.texi | 1 + spec/format/private-key.texi | 19 +-- spec/format/registry.texi | 66 ++++++++ spec/format/signed-data.texi | 21 ++- spec/index.texi | 3 +- spec/registry.texi | 77 --------- 19 files changed, 537 insertions(+), 224 deletions(-) create mode 100644 gyac/yacpki/ed25519-blake2b/.gitignore create mode 100755 gyac/yacpki/ed25519-blake2b/clean create mode 100644 gyac/yacpki/ed25519-blake2b/ed25519-to-blake2b.patch create mode 100755 gyac/yacpki/ed25519-blake2b/mk-from-go create mode 100644 gyac/yacpki/gost.go create mode 100644 spec/format/registry.texi delete mode 100644 spec/registry.texi diff --git a/gyac/yacpki/algo.go b/gyac/yacpki/algo.go index 99997a0..cd70703 100644 --- a/gyac/yacpki/algo.go +++ b/gyac/yacpki/algo.go @@ -3,26 +3,21 @@ 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" + "golang.org/x/crypto/blake2b" ) const ( - AlgoStreebog256 = "streebog256" - AlgoStreebog512 = "streebog512" - AlgoGOST3410256A = "gost3410-256A" - AlgoGOST3410256B = "gost3410-256B" - AlgoGOST3410256C = "gost3410-256C" - AlgoGOST3410256D = "gost3410-256D" - AlgoGOST3410512A = "gost3410-512A" - AlgoGOST3410512B = "gost3410-512B" - AlgoGOST3410512C = "gost3410-512C" + AlgoEd25519BLAKE2b = "ed25519-blake2b" + AlgoGOST3410256A = "gost3410-256A" + AlgoGOST3410512C = "gost3410-512C" + AlgoStreebog256 = "streebog256" + AlgoStreebog512 = "streebog512" ) var HashToNew = map[string]func() hash.Hash{ @@ -38,7 +33,13 @@ type AV struct { func (av *AV) Id() (id uuid.UUID) { var hasher hash.Hash switch av.A { - case AlgoGOST3410256A, AlgoGOST3410256B, AlgoGOST3410256C, AlgoGOST3410256D, AlgoGOST3410512A, AlgoGOST3410512B, AlgoGOST3410512C: + case AlgoEd25519BLAKE2b: + var err error + hasher, err = blake2b.New256(nil) + if err != nil { + panic(err) + } + case AlgoGOST3410256A, AlgoGOST3410512C: hasher = gost34112012256.New() default: panic("unsupported algorithm") @@ -50,37 +51,3 @@ func (av *AV) Id() (id uuid.UUID) { } return id } - -func GOST3410CurveByName(name string) (curve *gost3410.Curve) { - switch name { - case AlgoGOST3410256A: - curve = gost3410.CurveIdtc26gost341012256paramSetA() - case AlgoGOST3410256B: - curve = gost3410.CurveIdtc26gost341012256paramSetB() - case AlgoGOST3410256C: - curve = gost3410.CurveIdtc26gost341012256paramSetC() - case AlgoGOST3410256D: - curve = gost3410.CurveIdtc26gost341012256paramSetD() - case AlgoGOST3410512A: - curve = gost3410.CurveIdtc26gost341012512paramSetA() - case AlgoGOST3410512B: - curve = gost3410.CurveIdtc26gost341012512paramSetB() - case AlgoGOST3410512C: - curve = gost3410.CurveIdtc26gost341012512paramSetC() - default: - log.Fatal("unknown curve") - } - return -} - -func HasherByKeyAlgo(a string) hash.Hash { - switch a { - case AlgoGOST3410256A, AlgoGOST3410256B, AlgoGOST3410256C, AlgoGOST3410256D: - return gost34112012256.New() - case AlgoGOST3410512A, AlgoGOST3410512B, AlgoGOST3410512C: - return gost34112012512.New() - default: - log.Fatal("unsupported algorithm") - } - return nil -} diff --git a/gyac/yacpki/cer.go b/gyac/yacpki/cer.go index a1c7e2e..54f5cec 100644 --- a/gyac/yacpki/cer.go +++ b/gyac/yacpki/cer.go @@ -4,11 +4,15 @@ import ( "crypto" "errors" "fmt" + "hash" "time" "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/ed25519-blake2b/ed25519" "go.cypherpunks.su/yac/gyac/yacpki/utils" ) @@ -130,13 +134,28 @@ func (cer *CerLoad) CheckSignature(signed, signature []byte) (err error) { } pub := cer.Pub[0] switch pub.A { - case AlgoGOST3410256A, AlgoGOST3410256B, AlgoGOST3410256C, AlgoGOST3410256D, AlgoGOST3410512A, AlgoGOST3410512B, AlgoGOST3410512C: + case AlgoEd25519BLAKE2b: + if len(pub.V) != ed25519.PublicKeySize { + err = errors.New("invalid ed25519 public key size") + return + } + if !ed25519.Verify(ed25519.PublicKey(pub.V), signed, signature) { + err = errors.New("invalid ed25519 signature") + return + } + case AlgoGOST3410256A, AlgoGOST3410512C: var pk *gost3410.PublicKey pk, err = gost3410.NewPublicKeyBE(GOST3410CurveByName(pub.A), pub.V) if err != nil { return } - hasher := HasherByKeyAlgo(pub.A) + var hasher hash.Hash + switch pub.A { + case AlgoGOST3410256A: + hasher = gost34112012256.New() + case AlgoGOST3410512C: + hasher = gost34112012512.New() + } utils.MustWrite(hasher, signed) hsh := hasher.Sum(nil) var valid bool diff --git a/gyac/yacpki/cmd/yacertool/basic.t b/gyac/yacpki/cmd/yacertool/basic.t index 0c93675..fccc5d3 100755 --- a/gyac/yacpki/cmd/yacertool/basic.t +++ b/gyac/yacpki/cmd/yacertool/basic.t @@ -1,45 +1,52 @@ #!/bin/sh testname=`basename "$0"` -test_description="Check that basic functionality works" +test_description="Check that basic GOST-related functionality works" . $SHARNESS_TEST_SRCDIR/sharness.sh TMPDIR=${TMPDIR:-/tmp} + +echo "gost3410-512C gost3410-256A +ed25519-blake2b ed25519-blake2b" | while read caAlgo eeAlgo ; do + subj="-subj CN=CA -subj C=RU" -test_expect_success "CA generation" "yacertool \ - -algo gost3410-512C \ +test_expect_success "$caAlgo: CA generation" "yacertool \ + -algo $caAlgo \ -ku ca -ku sig $subj \ -prv $TMPDIR/ca.prv -cer $TMPDIR/ca.cer" -test_expect_success "CA regeneration" "yacertool \ - -algo gost3410-512C \ +test_expect_success "$caAlgo: CA regeneration" "yacertool \ + -algo $caAlgo \ -ku ca -ku sig $subj \ -prv $TMPDIR/ca.prv -cer $TMPDIR/ca.cer \ -reuse-key" -test_expect_success "CA self-signature" "yacertool \ +test_expect_success "$caAlgo: CA self-signature" "yacertool \ -ca-cer $TMPDIR/ca.cer \ -cer $TMPDIR/ca.cer \ -verify" subj="-subj CN=SubCA -subj C=RU" -test_expect_success "SubCA generation" "yacertool \ +test_expect_success "$eeAlgo: SubCA generation" "yacertool \ + -algo $eeAlgo \ -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 \ +test_expect_success "$eeAlgo: 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 $subj \ +test_expect_success "$eeAlgo: EE generation" "yacertool \ + -algo $eeAlgo $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 \ +test_expect_success "$eeAlgo: EE chain" "yacertool \ -ca-cer $TMPDIR/ca.cer \ -ca-cer $TMPDIR/subca.cer \ -cer $TMPDIR/ee.cer \ -verify" +done + test_done diff --git a/gyac/yacpki/cmd/yacertool/main.go b/gyac/yacpki/cmd/yacertool/main.go index a32fb3c..0546c7b 100644 --- a/gyac/yacpki/cmd/yacertool/main.go +++ b/gyac/yacpki/cmd/yacertool/main.go @@ -14,6 +14,7 @@ import ( "go.cypherpunks.su/gogost/v6/gost3410" "go.cypherpunks.su/yac/gyac" "go.cypherpunks.su/yac/gyac/yacpki" + "go.cypherpunks.su/yac/gyac/yacpki/ed25519-blake2b/ed25519" "go.cypherpunks.su/yac/gyac/yacpki/utils" ) @@ -85,7 +86,7 @@ func main() { } till := since.Add(time.Duration(*lifetime) * 24 * time.Hour) - var caPrv *gost3410.PrivateKey + var caPrv crypto.Signer var caCers []*yacpki.SignedData for _, issuingCer := range issuingCers { var sd *yacpki.SignedData @@ -99,12 +100,10 @@ func main() { if *issuingPrv == "" { log.Fatal("no -ca-key is set") } - var signer crypto.Signer - signer, err = yacpki.PrvParse(utils.MustReadFile(*issuingPrv)) + caPrv, err = yacpki.PrvParse(utils.MustReadFile(*issuingPrv)) if err != nil { log.Fatal(err) } - caPrv = signer.(*gost3410.PrivateKey) } if *verify { @@ -124,45 +123,80 @@ func main() { log.Fatal("no -prv is set") } - curve := yacpki.GOST3410CurveByName(*algo) - if curve == nil { - log.Fatal("unknown -algo specified") - } - var prv *gost3410.PrivateKey - if *reuseKey { - var signer crypto.Signer - signer, err = yacpki.PrvParse(utils.MustReadFile(*prvPath)) - if err != nil { - log.Fatal(err) - } - prv = signer.(*gost3410.PrivateKey) - if prv.C.Name != curve.Name { - log.Fatal("-algo is not same with private key") + var prv crypto.Signer + var pubRaw []byte + switch *algo { + case yacpki.AlgoEd25519BLAKE2b: + if *reuseKey { + prv, err = yacpki.PrvParse(utils.MustReadFile(*prvPath)) + if err != nil { + log.Fatal(err) + } + prvEd25519 := prv.(ed25519.PrivateKey) + pubRaw = prvEd25519[ed25519.SeedSize:] + } else { + var prvEd25519 ed25519.PrivateKey + var pubEd25519 ed25519.PublicKey + pubEd25519, prvEd25519, err = ed25519.GenerateKey(rand.Reader) + if err != nil { + log.Fatal(err) + } + prv = prvEd25519 + pubRaw = pubEd25519[:] + err = os.WriteFile(*prvPath, gyac.EncodeItem(nil, + gyac.ItemFromGo(yacpki.AV{A: *algo, V: prvEd25519.Seed()})), 0o600) + if err != nil { + log.Fatal(err) + } } - } else { - prvRaw := make([]byte, curve.PointSize()) - if _, err = io.ReadFull(rand.Reader, prvRaw); err != nil { - log.Fatal(err) + default: // GOST + curve := yacpki.GOST3410CurveByName(*algo) + if curve == nil { + log.Fatal("unknown -algo specified") } - prv, err = gost3410.NewPrivateKeyBE(curve, prvRaw) - if err != nil { - log.Fatal(err) + var signer *yacpki.GOSTSigner + if *reuseKey { + prv, err = yacpki.PrvParse(utils.MustReadFile(*prvPath)) + if err != nil { + log.Fatal(err) + } + signer = prv.(*yacpki.GOSTSigner) + if signer.Prv.C.Name != curve.Name { + log.Fatal("-algo is not same with private key") + } + } else { + prvRaw := make([]byte, curve.PointSize()) + if _, err = io.ReadFull(rand.Reader, prvRaw); err != nil { + log.Fatal(err) + } + var prvKey *gost3410.PrivateKey + prvKey, err = gost3410.NewPrivateKeyBE(curve, prvRaw) + if err != nil { + log.Fatal(err) + } + raw := gyac.EncodeItem(nil, + gyac.ItemFromGo(yacpki.AV{A: *algo, V: prvKey.RawBE()})) + prv, err = yacpki.PrvParse(raw) + if err != nil { + log.Fatal(err) + } + err = os.WriteFile(*prvPath, raw, 0o600) + if err != nil { + log.Fatal(err) + } + signer = prv.(*yacpki.GOSTSigner) } - err = os.WriteFile(*prvPath, gyac.EncodeItem(nil, - gyac.ItemFromGo(yacpki.AV{A: *algo, V: prv.RawBE()})), 0o600) + var pub *gost3410.PublicKey + pub, err = signer.Prv.PublicKey() if err != nil { log.Fatal(err) } + pubRaw = pub.RawBE() } - var pub *gost3410.PublicKey - pub, err = prv.PublicKey() - if err != nil { - log.Fatal(err) - } - pubMap := yacpki.Pub{A: *algo, V: pub.RawBE()} + pubMap := yacpki.Pub{A: *algo, V: pubRaw} { - av := yacpki.AV{A: *algo, V: pub.RawBE()} + av := yacpki.AV{A: *algo, V: pubRaw} pubMap.Id = av.Id() } cerLoad := yacpki.CerLoad{Subj: subj, Pub: []yacpki.Pub{pubMap}} diff --git a/gyac/yacpki/ed25519-blake2b/.gitignore b/gyac/yacpki/ed25519-blake2b/.gitignore new file mode 100644 index 0000000..ca24282 --- /dev/null +++ b/gyac/yacpki/ed25519-blake2b/.gitignore @@ -0,0 +1,4 @@ +byteorder/ +ed25519/ +edwards25519/ +go.mod diff --git a/gyac/yacpki/ed25519-blake2b/clean b/gyac/yacpki/ed25519-blake2b/clean new file mode 100755 index 0000000..24504f8 --- /dev/null +++ b/gyac/yacpki/ed25519-blake2b/clean @@ -0,0 +1,3 @@ +#!/bin/sh -e + +exec rm -rf byteorder ed25519 edwards25519 go.mod diff --git a/gyac/yacpki/ed25519-blake2b/ed25519-to-blake2b.patch b/gyac/yacpki/ed25519-blake2b/ed25519-to-blake2b.patch new file mode 100644 index 0000000..859d327 --- /dev/null +++ b/gyac/yacpki/ed25519-blake2b/ed25519-to-blake2b.patch @@ -0,0 +1,153 @@ +--- ed25519/ed25519.go 2024-12-03 10:59:27.811011000 +0300 ++++ ed25519/ed25519.go 2024-12-03 11:07:51.892841000 +0300 +@@ -20,11 +20,12 @@ + "crypto" + "go.cypherpunks.su/yac/gyac/yacpki/ed25519-blake2b/edwards25519" + cryptorand "crypto/rand" +- "crypto/sha512" + "crypto/subtle" + "errors" + "io" + "strconv" ++ ++ "golang.org/x/crypto/blake2b" + ) + + const ( +@@ -81,13 +82,13 @@ + + // Sign signs the given message with priv. rand is ignored and can be nil. + // +-// If opts.HashFunc() is [crypto.SHA512], the pre-hashed variant Ed25519ph is used +-// and message is expected to be a SHA-512 hash, otherwise opts.HashFunc() must ++// If opts.HashFunc() is [crypto.BLAKE2b_512], the pre-hashed variant Ed25519ph is used ++// and message is expected to be a BLAKE2b-512 hash, otherwise opts.HashFunc() must + // be [crypto.Hash](0) and the message must not be hashed, as Ed25519 performs two + // passes over messages to be signed. + // + // A value of type [Options] can be used as opts, or crypto.Hash(0) or +-// crypto.SHA512 directly to select plain Ed25519 or Ed25519ph, respectively. ++// crypto.BLAKE2b_512 directly to select plain Ed25519 or Ed25519ph, respectively. + func (priv PrivateKey) Sign(rand io.Reader, message []byte, opts crypto.SignerOpts) (signature []byte, err error) { + hash := opts.HashFunc() + context := "" +@@ -95,8 +96,8 @@ + context = opts.Context + } + switch { +- case hash == crypto.SHA512: // Ed25519ph +- if l := len(message); l != sha512.Size { ++ case hash == crypto.BLAKE2b_512: // Ed25519ph ++ if l := len(message); l != blake2b.Size { + return nil, errors.New("ed25519: bad Ed25519ph message hash length: " + strconv.Itoa(l)) + } + if l := len(context); l > 255 { +@@ -122,7 +123,7 @@ + // Options can be used with [PrivateKey.Sign] or [VerifyWithOptions] + // to select Ed25519 variants. + type Options struct { +- // Hash can be zero for regular Ed25519, or crypto.SHA512 for Ed25519ph. ++ // Hash can be zero for regular Ed25519, or crypto.BLAKE2b_512 for Ed25519ph. + Hash crypto.Hash + + // Context, if not empty, selects Ed25519ctx or provides the context string +@@ -171,7 +172,7 @@ + panic("ed25519: bad seed length: " + strconv.Itoa(l)) + } + +- h := sha512.Sum512(seed) ++ h := blake2b.Sum512(seed) + s, err := edwards25519.NewScalar().SetBytesWithClamping(h[:32]) + if err != nil { + panic("ed25519: internal error: setting scalar failed") +@@ -213,14 +214,17 @@ + } + seed, publicKey := privateKey[:SeedSize], privateKey[SeedSize:] + +- h := sha512.Sum512(seed) ++ h := blake2b.Sum512(seed) + s, err := edwards25519.NewScalar().SetBytesWithClamping(h[:32]) + if err != nil { + panic("ed25519: internal error: setting scalar failed") + } + prefix := h[32:] + +- mh := sha512.New() ++ mh, err := blake2b.New512(nil) ++ if err != nil { ++ panic(err) ++ } + if domPrefix != domPrefixPure { + mh.Write([]byte(domPrefix)) + mh.Write([]byte{byte(len(context))}) +@@ -228,7 +232,7 @@ + } + mh.Write(prefix) + mh.Write(message) +- messageDigest := make([]byte, 0, sha512.Size) ++ messageDigest := make([]byte, 0, blake2b.Size) + messageDigest = mh.Sum(messageDigest) + r, err := edwards25519.NewScalar().SetUniformBytes(messageDigest) + if err != nil { +@@ -237,7 +241,10 @@ + + R := (&edwards25519.Point{}).ScalarBaseMult(r) + +- kh := sha512.New() ++ kh, err := blake2b.New512(nil) ++ if err != nil { ++ panic(err) ++ } + if domPrefix != domPrefixPure { + kh.Write([]byte(domPrefix)) + kh.Write([]byte{byte(len(context))}) +@@ -246,7 +253,7 @@ + kh.Write(R.Bytes()) + kh.Write(publicKey) + kh.Write(message) +- hramDigest := make([]byte, 0, sha512.Size) ++ hramDigest := make([]byte, 0, blake2b.Size) + hramDigest = kh.Sum(hramDigest) + k, err := edwards25519.NewScalar().SetUniformBytes(hramDigest) + if err != nil { +@@ -272,7 +279,7 @@ + // publicKey. A valid signature is indicated by returning a nil error. It will + // panic if len(publicKey) is not [PublicKeySize]. + // +-// If opts.Hash is [crypto.SHA512], the pre-hashed variant Ed25519ph is used and ++// If opts.Hash is [crypto.BLAKE2b_512], the pre-hashed variant Ed25519ph is used and + // message is expected to be a SHA-512 hash, otherwise opts.Hash must be + // [crypto.Hash](0) and the message must not be hashed, as Ed25519 performs two + // passes over messages to be signed. +@@ -281,8 +288,8 @@ + // channels, or if an attacker has control of part of the inputs. + func VerifyWithOptions(publicKey PublicKey, message, sig []byte, opts *Options) error { + switch { +- case opts.Hash == crypto.SHA512: // Ed25519ph +- if l := len(message); l != sha512.Size { ++ case opts.Hash == crypto.BLAKE2b_512: // Ed25519ph ++ if l := len(message); l != blake2b.Size { + return errors.New("ed25519: bad Ed25519ph message hash length: " + strconv.Itoa(l)) + } + if l := len(opts.Context); l > 255 { +@@ -324,7 +331,10 @@ + return false + } + +- kh := sha512.New() ++ kh, err := blake2b.New512(nil) ++ if err != nil { ++ panic(err) ++ } + if domPrefix != domPrefixPure { + kh.Write([]byte(domPrefix)) + kh.Write([]byte{byte(len(context))}) +@@ -333,7 +343,7 @@ + kh.Write(sig[:32]) + kh.Write(publicKey) + kh.Write(message) +- hramDigest := make([]byte, 0, sha512.Size) ++ hramDigest := make([]byte, 0, blake2b.Size) + hramDigest = kh.Sum(hramDigest) + k, err := edwards25519.NewScalar().SetUniformBytes(hramDigest) + if err != nil { diff --git a/gyac/yacpki/ed25519-blake2b/mk-from-go b/gyac/yacpki/ed25519-blake2b/mk-from-go new file mode 100755 index 0000000..1ab8cce --- /dev/null +++ b/gyac/yacpki/ed25519-blake2b/mk-from-go @@ -0,0 +1,30 @@ +#!/bin/sh -e +# Unfortunately native crypto/ed25519 is not flexible enough and does +# not give ability to use different hash in Ed25519. +# That script copies the library (tested on 1.23.3) and patches it to +# use BLAKE2b hash. + +modname=go.cypherpunks.su/yac/gyac/yacpki/ed25519-blake2b +go mod init $modname +dst=$PWD +cd $(go env GOROOT)/src +cp -r crypto/ed25519 crypto/internal/edwards25519 internal/byteorder $dst +cd $dst + +cd ed25519 +rm -r testdata +rm *_test.go +cd .. + +cd edwards25519 +rm doc.go *_test.go +cd .. + +find . -name "*.go" -exec perl -i -npe 's#^(\s+)".*/?internal/(.*)"#$1"'$modname'/$2"#' {} + + +cd edwards25519/field/_asm +go mod edit -module $modname/edwards25519/field/_asm +perl -i -npe "s#crypto/internal#$modname#" fe_amd64_asm.go + +cd $dst +patch