From e9a2cfe295488fc315d7baa17ab8943b0164b321742ec673ccd7132fd1790388 Mon Sep 17 00:00:00 2001 From: Sergey Matveev Date: Fri, 24 Jan 2025 11:55:22 +0300 Subject: [PATCH] Properly prehashed signatures --- go/pki/algo.go | 1 + go/pki/cer.go | 62 ++++++++----- go/pki/cmd/certool/main.go | 4 +- go/pki/cmd/enctool/chapoly.go | 50 ++++++++++- go/pki/cmd/enctool/main.go | 45 ++++++++-- go/pki/cmd/enctool/usage.go | 4 +- go/pki/cmd/sigtool/basic.t | 51 +++++++++++ go/pki/cmd/sigtool/main.go | 49 +++++----- go/pki/ed25519-blake2b/algo.go | 5 +- go/pki/ed25519-blake2b/kp.go | 2 +- go/pki/ed25519-blake2b/{prv.go => signer.go} | 61 ++++++++++--- go/pki/ed25519-blake2b/verify.go | 4 +- go/pki/gost/gost.go | 15 ++++ go/pki/gost/kp.go | 17 +++- go/pki/gost/signer.go | 95 +++++++++++++++----- go/pki/gost/verify.go | 19 +++- go/pki/hash/algo.go | 2 +- go/pki/prv.go | 5 +- go/pki/sign/iface.go | 14 +++ go/pki/sign/mode.go | 10 +++ go/pki/signed.go | 77 +++++----------- spec/format/cer.texi | 14 ++- spec/format/signed-tbs.cddl | 5 -- spec/format/signed.texi | 27 +++--- 24 files changed, 455 insertions(+), 183 deletions(-) create mode 100755 go/pki/cmd/sigtool/basic.t rename go/pki/ed25519-blake2b/{prv.go => signer.go} (60%) create mode 100644 go/pki/sign/iface.go create mode 100644 go/pki/sign/mode.go delete mode 100644 spec/format/signed-tbs.cddl diff --git a/go/pki/algo.go b/go/pki/algo.go index 695aae9..ec129e0 100644 --- a/go/pki/algo.go +++ b/go/pki/algo.go @@ -9,6 +9,7 @@ import ( const ( Ed25519BLAKE2b = ed25519blake2b.Ed25519BLAKE2b + Ed25519PhBLAKE2b = ed25519blake2b.Ed25519PhBLAKE2b GOST3410256A = gost.GOST3410256A GOST3410512C = gost.GOST3410512C SNTRUP4591761X25519 = sntrup4591761x25519.SNTRUP4591761X25519 diff --git a/go/pki/cer.go b/go/pki/cer.go index e5d38a0..e6a3491 100644 --- a/go/pki/cer.go +++ b/go/pki/cer.go @@ -16,7 +16,7 @@ package pki import ( - "crypto" + "bytes" "errors" "fmt" "hash" @@ -27,6 +27,7 @@ import ( "go.cypherpunks.su/keks" ed25519blake2b "go.cypherpunks.su/keks/pki/ed25519-blake2b" "go.cypherpunks.su/keks/pki/gost" + "go.cypherpunks.su/keks/pki/sign" ) const ( @@ -36,6 +37,11 @@ const ( CerMagic = keks.Magic("pki/cer") ) +var ( + ErrSigInvalid = errors.New("signature is invalid") + ErrBadSigAlgo = errors.New("bad signature algo") +) + // Public key. type Pub struct { A string `keks:"a"` @@ -145,7 +151,7 @@ func (cer *CerLoad) Can(ku string) (yes bool) { // generated UUIDv7. since and till times must not have nanoseconds part. func (signed *Signed) CerIssueWith( parent *CerLoad, - prv crypto.Signer, + prv sign.Iface, since, till time.Time, ) error { exp := []time.Time{since, till} @@ -156,11 +162,9 @@ func (signed *Signed) CerIssueWith( return signed.SignWith(parent, prv, SigTBS{CID: &cid, Exp: &exp}) } -var ErrSigInvalid = errors.New("signature is invalid") - // Verify signature of signed data. ErrSigInvalid will be returned in // case of invalid signature. -func (cer *CerLoad) CheckSignature(signed, signature []byte) (err error) { +func (cer *CerLoad) CheckSignature(algo string, signed, signature []byte) (err error) { if !cer.Can(KUSig) || len(cer.Pub) != 1 { err = errors.New("cer can not sign") return @@ -169,11 +173,17 @@ func (cer *CerLoad) CheckSignature(signed, signature []byte) (err error) { var valid bool switch pub.A { case Ed25519BLAKE2b: + if algo != Ed25519BLAKE2b { + return ErrBadSigAlgo + } valid, err = ed25519blake2b.Verify(pub.V, signed, signature) if !valid { err = ErrSigInvalid } case GOST3410256A, GOST3410512C: + if algo != pub.A { + return ErrBadSigAlgo + } valid, err = gost.Verify(pub.A, pub.V, signed, signature) if !valid { err = ErrSigInvalid @@ -186,7 +196,10 @@ func (cer *CerLoad) CheckSignature(signed, signature []byte) (err error) { // Verify signature of signed data, by providing prehashed data. // ErrSigInvalid will be returned in case of invalid signature. -func (cer *CerLoad) CheckSignatureDigest(digest, signature []byte) (err error) { +func (cer *CerLoad) CheckSignaturePrehash( + algo string, + prehash, signature []byte, +) (err error) { if !cer.Can(KUSig) || len(cer.Pub) != 1 { err = errors.New("cer can not sign") return @@ -195,12 +208,18 @@ func (cer *CerLoad) CheckSignatureDigest(digest, signature []byte) (err error) { var valid bool switch pub.A { case Ed25519BLAKE2b: - valid, err = ed25519blake2b.VerifyDigest(pub.V, digest, signature) + if algo != Ed25519PhBLAKE2b { + return ErrBadSigAlgo + } + valid, err = ed25519blake2b.VerifyPrehash(pub.V, prehash, signature) if !valid { err = ErrSigInvalid } case GOST3410256A, GOST3410512C: - valid, err = gost.VerifyDigest(pub.A, pub.V, digest, signature) + if algo != pub.A { + return ErrBadSigAlgo + } + valid, err = gost.VerifyPrehash(pub.A, pub.V, prehash, signature) if !valid { err = ErrSigInvalid } @@ -211,11 +230,11 @@ func (cer *CerLoad) CheckSignatureDigest(digest, signature []byte) (err error) { } // Verify Signed CerLoad certificate's signature with provided parent. -// If hasher is specified, then prehashed signature mode is used. +// If prehasher is specified, then prehashed signature mode is used. // Currently only single signature can be verified. func (signed *Signed) CerCheckSignatureFrom( parent *CerLoad, - hasher *hash.Hash, + prehasher *hash.Hash, ) (err error) { if len(signed.Sigs) != 1 { err = errors.New("can verify only single signature") @@ -230,22 +249,23 @@ func (signed *Signed) CerCheckSignatureFrom( err = errors.New("sid != parent pub id") return } - var tbs SignedTBS - if hasher == nil { - tbs = SignedTBS{T: signed.Load.T, V: signed.Load.V, TBS: sig.TBS} - var buf []byte - buf, err = keks.EncodeBuf(tbs, nil) - if err != nil { + if prehasher == nil { + var tbs bytes.Buffer + if _, err = keks.Encode(&tbs, signed.Load, nil); err != nil { + return + } + if _, err = keks.Encode(&tbs, sig.TBS, nil); err != nil { return } - return parent.CheckSignature(buf, sig.Sign.V) + return parent.CheckSignature(sig.Sign.A, tbs.Bytes(), sig.Sign.V) } else { - tbs = SignedTBS{T: signed.Load.T, TBS: sig.TBS} - _, err = keks.Encode(*hasher, tbs, nil) - if err != nil { + if _, err = keks.Encode(*prehasher, signed.Load, nil); err != nil { + return + } + if _, err = keks.Encode(*prehasher, sig.TBS, nil); err != nil { return } - return parent.CheckSignatureDigest((*hasher).Sum(nil), sig.Sign.V) + return parent.CheckSignaturePrehash(sig.Sign.A, (*prehasher).Sum(nil), sig.Sign.V) } } diff --git a/go/pki/cmd/certool/main.go b/go/pki/cmd/certool/main.go index 2b84822..c437c73 100644 --- a/go/pki/cmd/certool/main.go +++ b/go/pki/cmd/certool/main.go @@ -17,7 +17,6 @@ package main import ( "bytes" - "crypto" "errors" "flag" "fmt" @@ -31,6 +30,7 @@ import ( "go.cypherpunks.su/keks/pki" ed25519blake2b "go.cypherpunks.su/keks/pki/ed25519-blake2b" "go.cypherpunks.su/keks/pki/gost" + "go.cypherpunks.su/keks/pki/sign" sntrup4591761x25519 "go.cypherpunks.su/keks/pki/sntrup4591761-x25519" "go.cypherpunks.su/keks/pki/utils" ) @@ -113,7 +113,7 @@ func main() { } till := since.Add(time.Duration(*lifetime) * 24 * time.Hour) - var caPrv crypto.Signer + var caPrv sign.Iface var caCers []*pki.Signed for _, issuingCer := range issuingCers { var signed *pki.Signed diff --git a/go/pki/cmd/enctool/chapoly.go b/go/pki/cmd/enctool/chapoly.go index 7c745ab..a8aa955 100644 --- a/go/pki/cmd/enctool/chapoly.go +++ b/go/pki/cmd/enctool/chapoly.go @@ -22,6 +22,7 @@ import ( "io" "os" + "go.cypherpunks.su/keks" "golang.org/x/crypto/chacha20poly1305" ) @@ -47,6 +48,12 @@ func demChaPolySeal(cek []byte) error { out := make([]byte, len(ChaPolyPad)+ChaPolyChunkLen+ciph.Overhead()) br := bufio.NewReaderSize(os.Stdin, ChaPolyChunkLen) bw := bufio.NewWriterSize(os.Stdout, len(out)) + pr, pw := io.Pipe() + blobErr := make(chan error) + go func() { + _, e := keks.BlobEncode(bw, int64(len(out)), pr) + blobErr <- e + }() nonce := make([]byte, ciph.NonceSize()) var n int var eof bool @@ -62,11 +69,17 @@ func demChaPolySeal(cek []byte) error { if n != ChaPolyChunkLen { nonce[len(nonce)-1] = 0x01 } - _, err = bw.Write(ciph.Seal(out[:0], nonce, in[:len(ChaPolyPad)+n], nil)) + _, err = pw.Write(ciph.Seal(out[:0], nonce, in[:len(ChaPolyPad)+n], nil)) if err != nil { return err } } + if err = pw.Close(); err != nil { + return err + } + if err = <-blobErr; err != nil { + return err + } return bw.Flush() } @@ -76,15 +89,43 @@ func demChaPolyOpen(cek []byte) error { return err } in := make([]byte, len(ChaPolyPad)+ChaPolyChunkLen+ciph.Overhead()) + pr, pw := io.Pipe() + blobErr := make(chan error) + go func() { + d, e := keks.NewBlobDecoder( + bufio.NewReaderSize(os.Stdin, len(in)), + int64(len(in)), + ) + defer func() { + pw.Close() + blobErr <- e + }() + if e != nil { + return + } + var chunk []byte + for { + chunk, e = d.Next() + if e != nil { + if e == io.EOF { + e = nil + break + } + return + } + if _, e = pw.Write(chunk); e != nil { + return + } + } + }() out := make([]byte, len(ChaPolyPad)+ChaPolyChunkLen) var n int var eof bool var chunk []byte - br := bufio.NewReaderSize(os.Stdin, len(in)) bw := bufio.NewWriterSize(os.Stdout, ChaPolyChunkLen) nonce := make([]byte, ciph.NonceSize()) for !eof { - n, err = io.ReadFull(br, in) + n, err = io.ReadFull(pr, in) if err != nil { if err != io.ErrUnexpectedEOF { return err @@ -106,6 +147,9 @@ func demChaPolyOpen(cek []byte) error { return err } } + if err = <-blobErr; err != nil { + return err + } return bw.Flush() } diff --git a/go/pki/cmd/enctool/main.go b/go/pki/cmd/enctool/main.go index 2e85721..25a2b31 100644 --- a/go/pki/cmd/enctool/main.go +++ b/go/pki/cmd/enctool/main.go @@ -21,6 +21,7 @@ import ( "crypto/rand" "errors" "flag" + "fmt" "hash" "io" "log" @@ -32,6 +33,7 @@ import ( "golang.org/x/crypto/blake2b" "golang.org/x/crypto/chacha20poly1305" "golang.org/x/crypto/hkdf" + "golang.org/x/term" "go.cypherpunks.su/keks" "go.cypherpunks.su/keks/pki" @@ -84,12 +86,33 @@ func blake2b256() hash.Hash { return h } +func readPasswd(prompt string) (passwd []byte) { + tty, err := os.OpenFile("/dev/tty", os.O_RDWR, 0) + if err != nil { + log.Fatal(err) + } + defer tty.Close() + tty.WriteString(prompt) + passwd, err = term.ReadPassword(int(tty.Fd())) + if err != nil { + log.Fatalln(err) + } + // taken from age/cmd/age/tui.go:clearLine + const ( + CUI = "\033[" // Control Sequence Introducer + CPL = CUI + "F" // Cursor Previous Line + EL = CUI + "K" // Erase in Line + ) + fmt.Fprintf(tty, "\r\n"+CPL+EL) + return +} + func main() { log.SetFlags(log.Lshortfile) flag.Usage = usage setBind := flag.String("bind", "", "Set that /bind instead of autogeneration") includeTo := flag.Bool("include-to", false, `Include "to" field in KEMs`) - passwd := flag.String("passwd", "", "Passphrase") + passphrase := flag.Bool("p", false, "Use passphrase") balloonS := flag.Int("balloon-s", 1<<16, "Balloon's space cost") balloonT := flag.Int("balloon-t", 3, "Balloon's time cost") balloonP := flag.Int("balloon-p", 2, "Balloon's number of threads") @@ -167,7 +190,7 @@ func main() { for kemIdx, kem := range encrypted.KEM { switch kem.A { case pki.BalloonBLAKE2bHKDF: - if *passwd == "" { + if !*passphrase { log.Println(kemIdx, kem.A, "skipping because no -passwd") continue } @@ -177,16 +200,17 @@ func main() { if kem.Cost == nil { log.Fatalln("missing cost") } + passwd := readPasswd("Passphrase:") { kek := hkdf.Extract(blake2b256, balloon.H(blake2b256, - []byte(*passwd), + passwd, append(encrypted.Bind[:], *kem.Salt...), int(kem.Cost.S), int(kem.Cost.T), int(kem.Cost.P), ), []byte(BalloonHKDFSalt)) var cekp []byte cekp, err = kemChaPolyOpen(kek, kem.CEK, chacha20poly1305.KeySize) if err != nil { - log.Println(kemIdx, kem.A, err, "skipping") + log.Println(kemIdx, kem.A, err, ", skipping") continue } cek = cekp @@ -250,7 +274,7 @@ func main() { var cekp []byte cekp, err = kemChaPolyOpen(kek, kem.CEK, chacha20poly1305.KeySize) if err != nil { - log.Println(kemIdx, kem.A, err, "skipping") + log.Println(kemIdx, kem.A, err, ", skipping") continue } cek = cekp @@ -287,7 +311,14 @@ func main() { if err != nil { log.Fatal(err) } - if *passwd != "" { + if *passphrase { + passwd := readPasswd("Passphrase:") + { + confirm := readPasswd("Confirm:") + if !bytes.Equal(passwd, confirm) { + log.Fatal("passphrases do not match") + } + } salt := make([]byte, BalloonSaltLen) if _, err = io.ReadFull(rand.Reader, salt); err != nil { log.Fatal(err) @@ -303,7 +334,7 @@ func main() { } { kek := hkdf.Extract(blake2b256, balloon.H(blake2b256, - []byte(*passwd), + passwd, append(binding[:], salt...), *balloonS, *balloonT, *balloonP, ), []byte(BalloonHKDFSalt)) diff --git a/go/pki/cmd/enctool/usage.go b/go/pki/cmd/enctool/usage.go index b9744cd..360254e 100644 --- a/go/pki/cmd/enctool/usage.go +++ b/go/pki/cmd/enctool/usage.go @@ -26,10 +26,10 @@ func usage() { Encrypt to recipient: enctool -cer CER [-include-to] [-bind UUID] DATA.encrypted Encrypt on passphrase: - enctool -passwd PASSPHRASE [-bind UUID] DATA.encrypted + enctool -p [-bind UUID] DATA.encrypted [-balloon-s X] [-balloon-t X] [-balloon-p X] Decrypt by providing possible KEMs: - enctool -d [-passwd ...] [-prv PRV ...] DATA + enctool -d [-p] [-prv PRV ...] DATA `) flag.PrintDefaults() diff --git a/go/pki/cmd/sigtool/basic.t b/go/pki/cmd/sigtool/basic.t new file mode 100755 index 0000000..399c049 --- /dev/null +++ b/go/pki/cmd/sigtool/basic.t @@ -0,0 +1,51 @@ +#!/bin/sh + +test_description="TODO" +. $SHARNESS_TEST_SRCDIR/sharness.sh + +TMPDIR=${TMPDIR:-/tmp} + +echo "gost3410-512C +gost3410-256A +ed25519-blake2b" | while read algo ; do + +subj="-subj what=ever" +typ="some-different-type" +test_expect_success "$algo: cer generation" "certool \ + -algo $algo -ku sig $subj \ + -prv $TMPDIR/sign.prv -cer $TMPDIR/sign.cer" +dd if=/dev/urandom of=$TMPDIR/sign.data bs=300K count=1 2>/dev/null +bind="-encrypted-binding $(uuidgen)" +badBind="-encrypted-binding $(uuidgen)" + +test_expect_success "$algo: signing" "sigtool \ + -prv $TMPDIR/sign.prv -cer $TMPDIR/sign.cer -type $typ \ + $bind <$TMPDIR/sign.data >$TMPDIR/sign.sig" +test_expect_success "$algo: verifying" "sigtool \ + -verify -cer $TMPDIR/sign.cer -type $typ \ + <$TMPDIR/sign.sig >$TMPDIR/sign.data.got" +test_expect_success "$algo: comparing" \ + "test_cmp $TMPDIR/sign.data $TMPDIR/sign.data.got" +test_expect_success "$algo: differing type" "! sigtool \ + -verify -cer $TMPDIR/sign.cer <$TMPDIR/sign.sig >/dev/null" +test_expect_success "$algo: good bind" "! sigtool \ + -verify -cer $TMPDIR/sign.cer $bind <$TMPDIR/sign.sig >/dev/null" +test_expect_success "$algo: bad bind" "! sigtool \ + -verify -cer $TMPDIR/sign.cer $badBind <$TMPDIR/sign.sig >/dev/null" + +test_expect_success "$algo: detached signing" "sigtool -detached \ + -prv $TMPDIR/sign.prv -cer $TMPDIR/sign.cer -type $typ \ + <$TMPDIR/sign.data >$TMPDIR/sign.sig" +test_expect_success "$algo: detached verifying" \ + "cat $TMPDIR/sign.sig $TMPDIR/sign.data | + sigtool -detached -verify -cer $TMPDIR/sign.cer -type $typ" +test_expect_success "$algo: differing type" "! sigtool -detached \ + -verify -cer $TMPDIR/sign.cer <$TMPDIR/sign.sig >/dev/null" +test_expect_success "$algo: good bind" "! sigtool -detached \ + -verify -cer $TMPDIR/sign.cer $bind <$TMPDIR/sign.sig >/dev/null" +test_expect_success "$algo: bad bind" "! sigtool -detached \ + -verify -cer $TMPDIR/sign.cer $badBind <$TMPDIR/sign.sig >/dev/null" + +done + +test_done diff --git a/go/pki/cmd/sigtool/main.go b/go/pki/cmd/sigtool/main.go index 614536e..c8077e2 100644 --- a/go/pki/cmd/sigtool/main.go +++ b/go/pki/cmd/sigtool/main.go @@ -30,6 +30,7 @@ import ( "go.cypherpunks.su/keks" "go.cypherpunks.su/keks/pki" pkihash "go.cypherpunks.su/keks/pki/hash" + "go.cypherpunks.su/keks/pki/sign" "go.cypherpunks.su/keks/pki/utils" "go.cypherpunks.su/keks/types" ) @@ -66,19 +67,6 @@ func main() { log.Fatal(err) } - var signer pki.Signer - var hasher hash.Hash - if !*verify { - if *prvPath == "" { - log.Fatal("no -prv is set") - } - signer, _, err = pki.PrvParse(utils.MustReadFile(*prvPath)) - if err != nil { - log.Fatal(err) - } - hasher = signer.NewHasher() - } - stdin := bufio.NewReaderSize(os.Stdin, BlobChunkLen) if *verify { decoder := keks.NewDecoderFromReader(stdin, nil) @@ -97,7 +85,8 @@ func main() { var prehash pki.SignedPrehash var signed pki.Signed err = decoder.UnmarshalStruct(&prehash) - if err == nil && prehash.T == pki.SignedPrehashT { + var hasher hash.Hash + if err == nil && prehash.T == sign.PrehashT { if len(prehash.Sigs) == 0 { log.Fatal("prehash: no sigs") } @@ -107,6 +96,9 @@ func main() { for algo := range prehash.Sigs { hasher = pkihash.ByName(algo) } + if hasher == nil { + log.Fatal("prehash: unsupported algorithm") + } var blob *keks.BlobDecoder blob, err = keks.NewBlobDecoder(stdin, 1<<32) if err != nil { @@ -145,9 +137,6 @@ func main() { } sig := signed.Sigs[0] signer := cer.CerLoad() - if sig.Sign.A != signer.Pub[0].A { - log.Fatal("differing signature algorithms") - } if signed.Load.T != *typ { log.Fatalln("differing load type:", signed.Load.T) } @@ -169,17 +158,29 @@ func main() { log.Fatal(err) } } else { + if *prvPath == "" { + log.Fatal("no -prv is set") + } + var signer sign.Iface + signer, _, err = pki.PrvParse(utils.MustReadFile(*prvPath)) + if err != nil { + log.Fatal(err) + } + if err = signer.SetMode(sign.ModePrehash); err != nil { + log.Fatal(err) + } + if _, err = keks.Encode(os.Stdout, pki.SignedMagic, nil); err != nil { log.Fatal(err) } if *detached { - if _, err = io.Copy(hasher, stdin); err != nil { + if _, err = io.Copy(*signer.Prehasher(), stdin); err != nil { log.Fatal(err) } } else { if _, err = keks.Encode(os.Stdout, pki.SignedPrehash{ - T: pki.SignedPrehashT, - Sigs: map[string]*struct{}{cer.CerLoad().Pub[0].A: nil}, + T: sign.PrehashT, + Sigs: map[string]*struct{}{signer.Algo(): nil}, }, nil); err != nil { log.Fatal(err) } @@ -187,7 +188,7 @@ func main() { br := keks.BlobReader{R: pr, ChunkLen: BlobChunkLen} copyErr := make(chan error) go func() { - _, e := io.Copy(io.MultiWriter(pw, hasher), stdin) + _, e := io.Copy(io.MultiWriter(pw, *signer.Prehasher()), stdin) pw.Close() copyErr <- e }() @@ -201,14 +202,12 @@ func main() { } var signed pki.Signed signed.Load.T = *typ - when := time.Now().UTC().Truncate(1000 * time.Microsecond) + when := time.Now().UTC().Truncate(time.Millisecond) sigTbs := pki.SigTBS{When: &when} if encryptedBinding != uuid.Nil { sigTbs.EncryptedBinding = &encryptedBinding } - if err = signed.SignPrehashedWith( - hasher, cer.CerLoad(), signer, sigTbs, - ); err != nil { + if err = signed.SignWith(cer.CerLoad(), signer, sigTbs); err != nil { log.Fatal(err) } if _, err = keks.Encode(os.Stdout, signed, nil); err != nil { diff --git a/go/pki/ed25519-blake2b/algo.go b/go/pki/ed25519-blake2b/algo.go index 2bacaf6..f3a68bc 100644 --- a/go/pki/ed25519-blake2b/algo.go +++ b/go/pki/ed25519-blake2b/algo.go @@ -1,3 +1,6 @@ package ed25519blake2b -const Ed25519BLAKE2b = "ed25519-blake2b" +const ( + Ed25519BLAKE2b = "ed25519-blake2b" + Ed25519PhBLAKE2b = "ed25519ph-blake2b" +) diff --git a/go/pki/ed25519-blake2b/kp.go b/go/pki/ed25519-blake2b/kp.go index 13aa582..12a51fa 100644 --- a/go/pki/ed25519-blake2b/kp.go +++ b/go/pki/ed25519-blake2b/kp.go @@ -28,7 +28,7 @@ func NewKeypair() (signer *Signer, prv, pub []byte, err error) { if err != nil { return } - signer = &Signer{prvEd} + signer = &Signer{Prv: prvEd} prv = prvEd.Seed() pub = pubEd[:] return diff --git a/go/pki/ed25519-blake2b/prv.go b/go/pki/ed25519-blake2b/signer.go similarity index 60% rename from go/pki/ed25519-blake2b/prv.go rename to go/pki/ed25519-blake2b/signer.go index d9c62d8..d2da760 100644 --- a/go/pki/ed25519-blake2b/prv.go +++ b/go/pki/ed25519-blake2b/signer.go @@ -23,10 +23,46 @@ import ( "go.cypherpunks.su/keks/pki/ed25519-blake2b/ed25519" "golang.org/x/crypto/blake2b" + + "go.cypherpunks.su/keks/pki/sign" ) type Signer struct { - Prv ed25519.PrivateKey + Prv ed25519.PrivateKey + mode sign.Mode + prehasher *hash.Hash +} + +func (s *Signer) SetMode(m sign.Mode) error { + switch m { + case sign.ModePure: + s.mode = m + return nil + case sign.ModePrehash: + s.mode = m + h, err := blake2b.New512(nil) + if err != nil { + return err + } + s.prehasher = &h + return nil + default: + return errors.New("unsupported mode") + } +} + +func (s *Signer) Mode() sign.Mode { + return s.mode +} + +func (s *Signer) Algo() string { + switch s.mode { + case sign.ModePure: + return Ed25519BLAKE2b + case sign.ModePrehash: + return Ed25519PhBLAKE2b + } + return "" } func (s *Signer) Public() crypto.PublicKey { @@ -38,19 +74,18 @@ func (s *Signer) Sign( msg []byte, opts crypto.SignerOpts, ) (signature []byte, err error) { - return s.Prv.Sign(rand, msg, opts) -} - -func (s *Signer) SignDigest(rand io.Reader, dgst []byte) (signature []byte, err error) { - return s.Prv.Sign(rand, dgst, &ed25519.Options{Hash: crypto.BLAKE2b_512}) + switch s.mode { + case sign.ModePure: + return s.Prv.Sign(rand, msg, opts) + case sign.ModePrehash: + return s.Prv.Sign(rand, msg, &ed25519.Options{Hash: crypto.BLAKE2b_512}) + default: + panic("unsupported mode") + } } -func (s *Signer) NewHasher() hash.Hash { - h, err := blake2b.New512(nil) - if err != nil { - panic(err) - } - return h +func (s *Signer) Prehasher() *hash.Hash { + return s.prehasher } func NewSigner(v []byte) (prv *Signer, pub []byte, err error) { @@ -60,6 +95,6 @@ func NewSigner(v []byte) (prv *Signer, pub []byte, err error) { } p := ed25519.NewKeyFromSeed(v) pub = p[ed25519.SeedSize:] - prv = &Signer{p} + prv = &Signer{Prv: p} return } diff --git a/go/pki/ed25519-blake2b/verify.go b/go/pki/ed25519-blake2b/verify.go index 662c7f4..e7d1ef5 100644 --- a/go/pki/ed25519-blake2b/verify.go +++ b/go/pki/ed25519-blake2b/verify.go @@ -31,14 +31,14 @@ func Verify(pub, signed, signature []byte) (valid bool, err error) { return } -func VerifyDigest(pub, digest, signature []byte) (valid bool, err error) { +func VerifyPrehash(pub, hsh, signature []byte) (valid bool, err error) { if len(pub) != ed25519.PublicKeySize { err = errors.New("invalid ed25519 public key size") return } err = ed25519.VerifyWithOptions( ed25519.PublicKey(pub), - digest, + hsh, signature, &ed25519.Options{Hash: crypto.BLAKE2b_512}, ) diff --git a/go/pki/gost/gost.go b/go/pki/gost/gost.go index bffbd62..dbd743d 100644 --- a/go/pki/gost/gost.go +++ b/go/pki/gost/gost.go @@ -1,3 +1,18 @@ +// GoKEKS/PKI -- PKI-related capabilities based on KEKS encoded formats +// Copyright (C) 2024-2025 Sergey Matveev +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Lesser General Public License as +// published by the Free Software Foundation, version 3 of the License. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Lesser General Public License for more details. +// +// You should have received a copy of the GNU Lesser General Public +// License along with this program. If not, see . + package gost import ( diff --git a/go/pki/gost/kp.go b/go/pki/gost/kp.go index b89c255..59db966 100644 --- a/go/pki/gost/kp.go +++ b/go/pki/gost/kp.go @@ -1,3 +1,18 @@ +// GoKEKS/PKI -- PKI-related capabilities based on KEKS encoded formats +// Copyright (C) 2024-2025 Sergey Matveev +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Lesser General Public License as +// published by the Free Software Foundation, version 3 of the License. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Lesser General Public License for more details. +// +// You should have received a copy of the GNU Lesser General Public +// License along with this program. If not, see . + package gost import ( @@ -24,6 +39,6 @@ func NewKeypair(algo string) (signer *Signer, prv, pub []byte, err error) { if err != nil { return } - signer, pub, err = NewSigner(algo, pk.RawBE()) + signer, pub, err = NewSigner(pk.RawBE()) return } diff --git a/go/pki/gost/signer.go b/go/pki/gost/signer.go index b869631..547b44e 100644 --- a/go/pki/gost/signer.go +++ b/go/pki/gost/signer.go @@ -1,3 +1,18 @@ +// GoKEKS/PKI -- PKI-related capabilities based on KEKS encoded formats +// Copyright (C) 2024-2025 Sergey Matveev +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Lesser General Public License as +// published by the Free Software Foundation, version 3 of the License. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Lesser General Public License for more details. +// +// You should have received a copy of the GNU Lesser General Public +// License along with this program. If not, see . + package gost import ( @@ -9,11 +24,48 @@ import ( "go.cypherpunks.su/gogost/v6/gost3410" "go.cypherpunks.su/gogost/v6/gost34112012256" "go.cypherpunks.su/gogost/v6/gost34112012512" + + "go.cypherpunks.su/keks/pki/sign" ) type Signer struct { - Prv *gost3410.PrivateKey - Hasher func() hash.Hash + mode sign.Mode + Prv *gost3410.PrivateKey + NewHasher func() hash.Hash + prehasher *hash.Hash +} + +func (s *Signer) SetMode(m sign.Mode) error { + switch m { + case sign.ModePure: + s.mode = m + return nil + case sign.ModePrehash: + s.mode = m + p := s.NewHasher() + s.prehasher = &p + return nil + default: + return errors.New("unsupported mode") + } +} + +func (s *Signer) Mode() sign.Mode { + return s.mode +} + +func (s *Signer) Prehasher() *hash.Hash { + return s.prehasher +} + +func (s *Signer) Algo() string { + switch s.Prv.C.PointSize() { + case 32: + return GOST3410256A + case 64: + return GOST3410512C + } + return "" } func (s *Signer) Public() crypto.PublicKey { @@ -25,19 +77,18 @@ func (s *Signer) Sign( msg []byte, opts crypto.SignerOpts, ) (signature []byte, err error) { - h := s.Hasher() - h.Write(msg) - dgst := h.Sum(nil) - signature, err = s.Prv.Sign(rand, dgst, opts) - if err != nil { - return + var hsh []byte + switch s.mode { + case sign.ModePure: + h := s.NewHasher() + h.Write(msg) + hsh = h.Sum(nil) + case sign.ModePrehash: + hsh = msg + default: + panic("unsupported mode") } - signature = append(signature[len(signature)/2:], signature[:len(signature)/2]...) - return -} - -func (s *Signer) SignDigest(rand io.Reader, dgst []byte) (signature []byte, err error) { - signature, err = s.Prv.SignDigest(dgst, rand) + signature, err = s.Prv.SignDigest(hsh, rand) if err != nil { return } @@ -45,20 +96,16 @@ func (s *Signer) SignDigest(rand io.Reader, dgst []byte) (signature []byte, err return } -func (s *Signer) NewHasher() hash.Hash { - return s.Hasher() -} - -func NewSigner(a string, v []byte) (prv *Signer, pub []byte, err error) { +func NewSigner(v []byte) (prv *Signer, pub []byte, err error) { signer := Signer{} - switch a { - case GOST3410256A: - signer.Hasher = gost34112012256.New + switch len(v) { + case 32: + signer.NewHasher = gost34112012256.New signer.Prv, err = gost3410.NewPrivateKeyBE( gost3410.CurveIdtc26gost341012256paramSetA(), v, ) - case GOST3410512C: - signer.Hasher = gost34112012512.New + case 64: + signer.NewHasher = gost34112012512.New signer.Prv, err = gost3410.NewPrivateKeyBE( gost3410.CurveIdtc26gost341012512paramSetC(), v, ) diff --git a/go/pki/gost/verify.go b/go/pki/gost/verify.go index 6046b02..f914899 100644 --- a/go/pki/gost/verify.go +++ b/go/pki/gost/verify.go @@ -1,3 +1,18 @@ +// GoKEKS/PKI -- PKI-related capabilities based on KEKS encoded formats +// Copyright (C) 2024-2025 Sergey Matveev +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Lesser General Public License as +// published by the Free Software Foundation, version 3 of the License. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Lesser General Public License for more details. +// +// You should have received a copy of the GNU Lesser General Public +// License along with this program. If not, see . + package gost import ( @@ -29,13 +44,13 @@ func Verify(algo string, pub, signed, signature []byte) (valid bool, err error) return } -func VerifyDigest(algo string, pub, digest, signature []byte) (valid bool, err error) { +func VerifyPrehash(algo string, pub, hsh, signature []byte) (valid bool, err error) { var pk *gost3410.PublicKey pk, err = gost3410.NewPublicKeyBE(CurveByName(algo), pub) if err != nil { return } - valid, err = pk.VerifyDigest(digest, + valid, err = pk.VerifyDigest(hsh, append(signature[len(signature)/2:], signature[:len(signature)/2]...)) return } diff --git a/go/pki/hash/algo.go b/go/pki/hash/algo.go index fdde23b..5350d32 100644 --- a/go/pki/hash/algo.go +++ b/go/pki/hash/algo.go @@ -39,7 +39,7 @@ func ByName(name string) hash.Hash { return gost34112012256.New() case Streebog512, gost.GOST3410512C: return gost34112012512.New() - case BLAKE2b, ed25519blake2b.Ed25519BLAKE2b: + case BLAKE2b, ed25519blake2b.Ed25519BLAKE2b, ed25519blake2b.Ed25519PhBLAKE2b: h, err := blake2b.New512(nil) if err != nil { panic(err) diff --git a/go/pki/prv.go b/go/pki/prv.go index 94acb28..fe9b9ba 100644 --- a/go/pki/prv.go +++ b/go/pki/prv.go @@ -22,10 +22,11 @@ import ( "go.cypherpunks.su/keks" ed25519blake2b "go.cypherpunks.su/keks/pki/ed25519-blake2b" "go.cypherpunks.su/keks/pki/gost" + "go.cypherpunks.su/keks/pki/sign" ) // Parse private key contained in AV KEKS-encoded structure. -func PrvParse(data []byte) (prv Signer, pub []byte, err error) { +func PrvParse(data []byte) (prv sign.Iface, pub []byte, err error) { { var magic keks.Magic magic, data = keks.StripMagic(data) @@ -47,7 +48,7 @@ func PrvParse(data []byte) (prv Signer, pub []byte, err error) { case Ed25519BLAKE2b: prv, pub, err = ed25519blake2b.NewSigner(av.V) case GOST3410256A, GOST3410512C: - prv, pub, err = gost.NewSigner(av.A, av.V) + prv, pub, err = gost.NewSigner(av.V) default: err = fmt.Errorf("unknown private key algo: %s", av.A) } diff --git a/go/pki/sign/iface.go b/go/pki/sign/iface.go new file mode 100644 index 0000000..b8b5ca1 --- /dev/null +++ b/go/pki/sign/iface.go @@ -0,0 +1,14 @@ +package sign + +import ( + "crypto" + "hash" +) + +type Iface interface { + crypto.Signer + SetMode(Mode) error + Prehasher() *hash.Hash + Algo() string + Mode() Mode +} diff --git a/go/pki/sign/mode.go b/go/pki/sign/mode.go new file mode 100644 index 0000000..773a4a3 --- /dev/null +++ b/go/pki/sign/mode.go @@ -0,0 +1,10 @@ +package sign + +type Mode int + +const ( + ModePure Mode = 0 + ModePrehash = iota + + PrehashT = "prehash" +) diff --git a/go/pki/signed.go b/go/pki/signed.go index 922579f..d6d070e 100644 --- a/go/pki/signed.go +++ b/go/pki/signed.go @@ -16,28 +16,20 @@ package pki import ( + "bytes" "crypto" "crypto/rand" "errors" - "hash" - "io" "time" "github.com/google/uuid" "go.cypherpunks.su/keks" + "go.cypherpunks.su/keks/pki/sign" ) const SignedMagic = keks.Magic("pki/signed") -type Signer interface { - crypto.Signer - SignDigest(io.Reader, []byte) ([]byte, error) - NewHasher() hash.Hash -} - -const SignedPrehashT = "prehash" - type SignedPrehash struct { T string `keks:"t"` Sigs map[string]*struct{} `keks:"sigs"` @@ -63,12 +55,6 @@ type Sig struct { Sign AV `keks:"sign"` } -type SignedTBS struct { - V any `keks:"v,omitempty"` - T string `keks:"t"` - TBS SigTBS `keks:"tbs"` -} - type Signed struct { Cers *[]*Signed `keks:"certs,omitempty"` Load SignedLoad `keks:"load"` @@ -133,50 +119,33 @@ func SignedParse(data []byte) (*Signed, error) { // Sign Signed's contents and sigTBS corresponding data with the // provided prv signer, having parent certificate. Signature is appended // to the signed.Sigs. parent certificate must have "sig" key-usage. -func (signed *Signed) SignWith( - parent *CerLoad, - prv crypto.Signer, - sigTBS SigTBS, -) (err error) { +func (signed *Signed) SignWith(parent *CerLoad, prv sign.Iface, sigTBS SigTBS) (err error) { if !parent.Can(KUSig) || len(parent.Pub) != 1 { return errors.New("parent can not sign") } sigTBS.SID = parent.Pub[0].Id - signedTBS := SignedTBS{T: signed.Load.T, V: signed.Load.V, TBS: sigTBS} - sig := Sig{TBS: sigTBS} - sig.Sign.A = parent.Pub[0].A - var buf []byte - buf, err = keks.EncodeBuf(signedTBS, nil) - if err != nil { - return - } - sig.Sign.V, err = prv.Sign(rand.Reader, buf, crypto.Hash(0)) - if err != nil { - return - } - signed.Sigs = append(signed.Sigs, &sig) - return nil -} - -// Same as SignWith, but use prehashed mode. -func (signed *Signed) SignPrehashedWith( - hasher hash.Hash, - parent *CerLoad, - prv Signer, - sigTBS SigTBS, -) (err error) { - if !parent.Can(KUSig) || len(parent.Pub) != 1 { - return errors.New("parent can not sign") + var tbs []byte + if prv.Mode() == sign.ModePure { + var b bytes.Buffer + if _, err = keks.Encode(&b, signed.Load, nil); err != nil { + return + } + if _, err = keks.Encode(&b, sigTBS, nil); err != nil { + return + } + tbs = b.Bytes() + } else { + if _, err = keks.Encode(*prv.Prehasher(), signed.Load, nil); err != nil { + return + } + if _, err = keks.Encode(*prv.Prehasher(), sigTBS, nil); err != nil { + return + } + tbs = (*prv.Prehasher()).Sum(nil) } - sigTBS.SID = parent.Pub[0].Id - signedTBS := SignedTBS{T: signed.Load.T, TBS: sigTBS} sig := Sig{TBS: sigTBS} - sig.Sign.A = parent.Pub[0].A - _, err = keks.Encode(hasher, signedTBS, nil) - if err != nil { - return - } - sig.Sign.V, err = prv.SignDigest(rand.Reader, hasher.Sum(nil)) + sig.Sign.A = prv.Algo() + sig.Sign.V, err = prv.Sign(rand.Reader, tbs, crypto.Hash(0)) if err != nil { return } diff --git a/spec/format/cer.texi b/spec/format/cer.texi index b180ae0..d63ef01 100644 --- a/spec/format/cer.texi +++ b/spec/format/cer.texi @@ -97,10 +97,15 @@ Example minimal certificate may look like: @node cer-gost3410 @subsection cer with GOST R 34.10-2012 -Same rules of serialisation must be used as with -@code{@ref{pki-signed-gost3410}}. Public key's -identifier and @code{cid} should be calculated -using big-endian Streebog-256 hash. +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. + +Algorithm identifiers for the public key: @code{gost3410-256A}, +@code{gost3410-512C}. + +Public key's identifier and @code{cid} should be calculated using +big-endian Streebog-256 hash. @node cer-ed25519-blake2b @subsection cer with Ed25519-BLAKE2b @@ -109,6 +114,7 @@ Same calculation and serialisation rules must be used as with @code{@ref{pki-signed-ed25519-blake2b}}. Public key's identifier and @code{cid} should be calculated using BLAKE2b hash with 128 or 256 bit output length specified. +Algorithm identifiers for the public key: @code{ed25519ph-blake2b}, @node cer-sntrup4591761-x25519 @subsection cer with SNTRUP4591761-X25519 diff --git a/spec/format/signed-tbs.cddl b/spec/format/signed-tbs.cddl deleted file mode 100644 index 94e8a04..0000000 --- a/spec/format/signed-tbs.cddl +++ /dev/null @@ -1,5 +0,0 @@ -pki-signed-tbs = { - t: text, ; = /load/t - ? v: any, ; /load/v if it is supplied - tbs: map, ; = /sigs/?/tbs -} diff --git a/spec/format/signed.texi b/spec/format/signed.texi index 52f413f..e02409d 100644 --- a/spec/format/signed.texi +++ b/spec/format/signed.texi @@ -10,9 +10,11 @@ unless it is a @ref{cer, certificate}. @verbatiminclude format/signed.cddl -Signature is created by signing the following encoded MAP: +Signature is created by signing the: -@verbatiminclude format/signed-tbs.cddl +@verbatim +[detached-data] || /load || sig-tbs +@end verbatim If no @code{/load/v} is provided, then the data is detached from the @code{pki-signed} structure itself and it is fed into hasher before that @@ -21,15 +23,14 @@ detached data closely to the @code{pki-signed}, you should use the following approach: @verbatim -pki-signed-prehash || BLOB || pki-signed +pki-signed-prehash || BLOB(detached-data) || pki-signed @end verbatim @verbatiminclude format/signed-prehash.cddl With @code{pki-signed-prehash} you initialise your hashers used during signing process and fed BLOB's contents (not the encoded BLOB itself!) -into the them. Finally you add @code{pki-signed-tbs} without @code{v} -field to them. +into the them. @code{/sigs/*/tbs/when} is optional signing time. @@ -49,12 +50,11 @@ then @code{/sigs/*/tbs/encrypted-binding} should be set to @subsection pki-signed 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(R)||BE(S)} -format. +function. Its digest must be big-endian serialised. Signature is in +@code{BE(R)||BE(S)} format. -Following algorithm identifiers are acceptable for the public key and -signature: @code{gost3410-256A}, @code{gost3410-512C}. +Algorithm identifiers for the signature: @code{gost3410-256A}, +@code{gost3410-512C}. @node pki-signed-ed25519-blake2b @subsection pki-signed with Ed25519-BLAKE2b @@ -66,7 +66,8 @@ But BLAKE2b is used instead of SHA2-512 hash. Strict @url{https://zips.z.cash/zip-0215, ZIP-0215} validation rules should be used while verifying the signature. -PureEdDSA @strong{must} be used when no detached data exists. -HashEdDSA @strong{must} be used otherwise, using BLAKE2b-512 as a hash. +PureEdDSA @strong{must} be used when no detached data exists and +@code{ed25519-blake2b} algorithm identifier is used for signature. -@code{ed25519-blake2b} algorithm identifier is used. +HashEdDSA @strong{must} be used otherwise, using BLAKE2b-512 as a hash, +using @code{ed25519ph-blake2b} algorithm identifier for signature. -- 2.48.1