From: Sergey Matveev Date: Thu, 23 Jan 2025 08:55:35 +0000 (+0300) Subject: Revised detached signatures X-Git-Url: http://www.git.cypherpunks.su/?a=commitdiff_plain;h=604f03fdf2636b0d2f40bfca9d24048649484f8e3c1a755b524b961c87ed3bd0;p=keks.git Revised detached signatures --- diff --git a/go/blob.go b/go/blob.go index 0d52c15..017624b 100644 --- a/go/blob.go +++ b/go/blob.go @@ -16,9 +16,13 @@ package keks import ( + "errors" "fmt" "io" "strings" + "unsafe" + + "go.cypherpunks.su/keks/types" ) type BlobChunked struct { @@ -53,3 +57,59 @@ type BlobReader struct { func (blob *BlobReader) String() string { return fmt.Sprintf("BLOB(%d, ?)", blob.ChunkLen) } + +// BlobDecoder decodes a BLOB from the reader. +type BlobDecoder struct { + d *Decoder + ChunkLen int64 + eof bool +} + +func NewBlobDecoder(r io.Reader, maxChunkLen int64) (*BlobDecoder, error) { + d := NewDecoderFromReader(r, &DecodeOpts{MaxStrLen: maxChunkLen}) + t, err := d.DecodeAtom() + if err != nil { + return nil, err + } + if t != types.Blob { + return nil, errors.New("not a BLOB") + } + chunkLen := d.blobChunkLens[0] + if chunkLen > maxChunkLen { + return nil, ErrLenTooBig + } + d.readBuf = make([]byte, max(chunkLen, 8)) + return &BlobDecoder{d: d, ChunkLen: chunkLen}, nil +} + +// Get the next chunk of the BLOB. Last one may be smaller size than the +// ChunkLen. When BLOB is finished, io.EOF is returned with nil chunk. +func (d *BlobDecoder) Next() (chunk []byte, err error) { + if d.eof { + return nil, io.EOF + } + var t types.Type + t, err = d.d.DecodeAtom() + if err != nil { + return + } + if t != types.Bin { + err = ErrBlobBadAtom + return + } + s := d.d.strs[len(d.d.strs)-1] + d.d.deTail() + d.d.strs = d.d.strs[:len(d.d.strs)-1] + chunk = unsafe.Slice(unsafe.StringData(s), len(s)) + if int64(len(s)) == d.ChunkLen { + return chunk, nil + } + if int64(len(s)) > d.ChunkLen { + return nil, ErrBlobBadChunkLen + } + d.eof = true + if len(s) == 0 { + return nil, io.EOF + } + return chunk, nil +} diff --git a/go/ctx.go b/go/ctx.go index 148a7b8..01e053b 100644 --- a/go/ctx.go +++ b/go/ctx.go @@ -75,6 +75,8 @@ type Decoder struct { blobChunkLens []int64 blobChunkses [][]string + + readBuf []byte // used only by BlobDecoder } // Initialise decoder that will read from b bytes. After the parse, diff --git a/go/getter.go b/go/getter.go index f34bf23..0c196a2 100644 --- a/go/getter.go +++ b/go/getter.go @@ -43,7 +43,15 @@ func (ctx *Decoder) getByte() (b byte, err error) { func (ctx *Decoder) getBytes(n int) (s string, err error) { var read int if ctx.B == nil { - buf := make([]byte, n) + var buf []byte + if ctx.readBuf == nil { + buf = make([]byte, n) + } else { + buf = ctx.readBuf[:n] + if len(ctx.readBuf) < n { + panic("readBuf is smaller") + } + } read, err = io.ReadFull(ctx.R, buf) ctx.Read += int64(read) if err != nil { diff --git a/go/pki/algo.go b/go/pki/algo.go index bced6af..695aae9 100644 --- a/go/pki/algo.go +++ b/go/pki/algo.go @@ -1,6 +1,7 @@ package pki import ( + "go.cypherpunks.su/keks" ed25519blake2b "go.cypherpunks.su/keks/pki/ed25519-blake2b" "go.cypherpunks.su/keks/pki/gost" sntrup4591761x25519 "go.cypherpunks.su/keks/pki/sntrup4591761-x25519" @@ -15,7 +16,7 @@ const ( BalloonBLAKE2bHKDF = "balloon-blake2b-hkdf" ChaCha20Poly1305 = "chacha20poly1305" - EncryptedMagic = "pki/encryptd" - HashedMagic = "pki/hashed" - PrvKeyMagic = "pki/prvkey" + EncryptedMagic = keks.Magic("pki/encryptd") + HashedMagic = keks.Magic("pki/hashed") + PrvKeyMagic = keks.Magic("pki/prvkey") ) diff --git a/go/pki/cer.go b/go/pki/cer.go index 1f99071..e5d38a0 100644 --- a/go/pki/cer.go +++ b/go/pki/cer.go @@ -19,6 +19,7 @@ import ( "crypto" "errors" "fmt" + "hash" "time" "github.com/google/uuid" @@ -32,7 +33,7 @@ const ( KUCA = "ca" // CA-capable key usage KUSig = "sig" // Signing-capable key usage KUKEM = "kem" // Key-encapsulation-mechanism key usage - CerMagic = "pki/cer" + CerMagic = keks.Magic("pki/cer") ) // Public key. @@ -51,16 +52,13 @@ type CerLoad struct { } // Parse Signed contents as CerLoad (certificate) and check its -// signatures necessary structure. sd.Load.V will hold the CerLoad in +// signatures necessary structure. signed.Load.V will hold the CerLoad in // case of success. -func (sd *Signed) CerParse() error { - if sd.Load.T != "cer" { +func (signed *Signed) CerParse() error { + if signed.Load.T != "cer" { return errors.New("CerParse: wrong load type") } - for _, sig := range sd.Sigs { - if sig.TBS.Hashes != nil { - return errors.New("CerParse: prehashed Signed") - } + for _, sig := range signed.Sigs { if sig.TBS.CID == nil { return errors.New("CerParse: missing cid") } @@ -68,9 +66,12 @@ func (sd *Signed) CerParse() error { return errors.New("CerParse: missing exp") } } + if signed.Load.V == nil { + return errors.New("CerParse: missing /load/v") + } var load CerLoad var err error - if v, ok := sd.Load.V.(map[string]any); ok { + if v, ok := (*signed.Load.V).(map[string]any); ok { err = keks.Map2Struct(&load, v) } else { err = errors.New("CerParse: wrong /load/v") @@ -78,7 +79,10 @@ func (sd *Signed) CerParse() error { if err != nil { return err } - sd.Load.V = load + { + loadAny := any(load) + signed.Load.V = &loadAny + } if load.KU != nil { if len(*load.KU) == 0 { return errors.New("CerParse: empty ku") @@ -110,7 +114,7 @@ func (sd *Signed) CerParse() error { } // Parse KEKS-encoded data as Signed with the CerLoad (certificate) contents. -func CerParse(data []byte) (sd *Signed, err error) { +func CerParse(data []byte) (signed *Signed, err error) { { var magic keks.Magic magic, data = keks.StripMagic(data) @@ -119,11 +123,11 @@ func CerParse(data []byte) (sd *Signed, err error) { return } } - sd, err = SignedParse(data) + signed, err = SignedParse(data) if err != nil { return } - err = sd.CerParse() + err = signed.CerParse() return } @@ -139,7 +143,7 @@ func (cer *CerLoad) Can(ku string) (yes bool) { // Sign the current Signed, having CerLoad payload with the provided // parent's CerLoad and prv key. Certificate's CID will be automatically // generated UUIDv7. since and till times must not have nanoseconds part. -func (sd *Signed) CerIssueWith( +func (signed *Signed) CerIssueWith( parent *CerLoad, prv crypto.Signer, since, till time.Time, @@ -149,7 +153,7 @@ func (sd *Signed) CerIssueWith( if err != nil { return err } - return sd.SignWith(parent, prv, SigTBS{CID: &cid, Exp: &exp}) + return signed.SignWith(parent, prv, SigTBS{CID: &cid, Exp: &exp}) } var ErrSigInvalid = errors.New("signature is invalid") @@ -180,10 +184,40 @@ func (cer *CerLoad) CheckSignature(signed, signature []byte) (err error) { return } +// 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) { + if !cer.Can(KUSig) || len(cer.Pub) != 1 { + err = errors.New("cer can not sign") + return + } + pub := cer.Pub[0] + var valid bool + switch pub.A { + case Ed25519BLAKE2b: + valid, err = ed25519blake2b.VerifyDigest(pub.V, digest, signature) + if !valid { + err = ErrSigInvalid + } + case GOST3410256A, GOST3410512C: + valid, err = gost.VerifyDigest(pub.A, pub.V, digest, signature) + if !valid { + err = ErrSigInvalid + } + default: + err = errors.New("unsupported signature algorithm") + } + return +} + // Verify Signed CerLoad certificate's signature with provided parent. +// If hasher is specified, then prehashed signature mode is used. // Currently only single signature can be verified. -func (sd *Signed) CerCheckSignatureFrom(parent *CerLoad) (err error) { - if len(sd.Sigs) != 1 { +func (signed *Signed) CerCheckSignatureFrom( + parent *CerLoad, + hasher *hash.Hash, +) (err error) { + if len(signed.Sigs) != 1 { err = errors.New("can verify only single signature") return } @@ -191,37 +225,48 @@ func (sd *Signed) CerCheckSignatureFrom(parent *CerLoad) (err error) { err = errors.New("parent can not sign") return } - sig := sd.Sigs[0] + sig := signed.Sigs[0] if sig.TBS.SID != parent.Pub[0].Id { err = errors.New("sid != parent pub id") return } - tbs := SignedTBS{T: sd.Load.T, V: sd.Load.V, TBS: sig.TBS} - buf, err := keks.EncodeBuf(tbs, nil) - if err != nil { - 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 { + return + } + return parent.CheckSignature(buf, sig.Sign.V) + } else { + tbs = SignedTBS{T: signed.Load.T, TBS: sig.TBS} + _, err = keks.Encode(*hasher, tbs, nil) + if err != nil { + return + } + return parent.CheckSignatureDigest((*hasher).Sum(nil), sig.Sign.V) } - return parent.CheckSignature(buf, sig.Sign.V) } // Get CerLoad from Signed. // Returns nil if Signed does not hold it (or it is not yet parsed). -func (sd *Signed) CerLoad() *CerLoad { - if sd.Load.T != "cer" { +func (signed *Signed) CerLoad() *CerLoad { + if signed.Load.T != "cer" || signed.Load.V == nil { return nil } - l, ok := sd.Load.V.(CerLoad) + l, ok := (*signed.Load.V).(CerLoad) if ok { return &l } return nil } -// Verify sd Signed CerLoad certificate against cers chain of +// Verify signed Signed CerLoad certificate against cers chain of // certificate authority ones at specified point of time t. -func (sd *Signed) CerVerify(cers []*Signed, t time.Time) (err error) { +func (signed *Signed) CerVerify(cers []*Signed, t time.Time) (err error) { { - exp := *(sd.Sigs[0].TBS.Exp) + exp := *(signed.Sigs[0].TBS.Exp) if t.Before(exp[0]) || t.Equal(exp[0]) { err = errors.New("cer is not active") return @@ -231,9 +276,9 @@ func (sd *Signed) CerVerify(cers []*Signed, t time.Time) (err error) { return } } - sid := sd.Sigs[0].TBS.SID - if sid == sd.CerLoad().Pub[0].Id { - return sd.CerCheckSignatureFrom(sd.CerLoad()) + sid := signed.Sigs[0].TBS.SID + if sid == signed.CerLoad().Pub[0].Id { + return signed.CerCheckSignatureFrom(signed.CerLoad(), nil) } idToCer := make(map[uuid.UUID]*Signed, len(cers)) for _, cer := range cers { @@ -250,10 +295,10 @@ func (sd *Signed) CerVerify(cers []*Signed, t time.Time) (err error) { } signer := idToCer[sid] if signer == nil { - err = fmt.Errorf("no cer found for sid: %v", sd.Sigs[0].TBS.SID) + err = fmt.Errorf("no cer found for sid: %v", signed.Sigs[0].TBS.SID) return } - err = sd.CerCheckSignatureFrom(signer.CerLoad()) + err = signed.CerCheckSignatureFrom(signer.CerLoad(), nil) if err != nil { return } diff --git a/go/pki/cmd/certool/basic.t b/go/pki/cmd/certool/basic.t index abe3ed4..20da835 100755 --- a/go/pki/cmd/certool/basic.t +++ b/go/pki/cmd/certool/basic.t @@ -14,27 +14,23 @@ test_expect_success "$caAlgo: CA load generation" "certool \ -ku ca -ku sig $subj \ -prv $TMPDIR/ca.prv -cer $TMPDIR/ca.cer" test_expect_success "$caAlgo: CA generation" "certool \ - -algo $caAlgo \ - -ku ca -ku sig $subj \ - -reuse-key \ - -prv $TMPDIR/ca.prv -cer $TMPDIR/ca.cer \ + -cer $TMPDIR/ca.cer \ -ca-prv $TMPDIR/ca.prv -ca-cer $TMPDIR/ca.cer" test_expect_success "$caAlgo: CA regeneration" "certool \ - -algo $caAlgo \ - -ku ca -ku sig $subj \ - -prv $TMPDIR/ca.prv -cer $TMPDIR/ca.cer \ - -ca-prv $TMPDIR/ca.prv -ca-cer $TMPDIR/ca.cer \ - -reuse-key" + -cer $TMPDIR/ca.cer \ + -ca-prv $TMPDIR/ca.prv -ca-cer $TMPDIR/ca.cer" test_expect_success "$caAlgo: CA self-signature" "certool \ -ca-cer $TMPDIR/ca.cer \ -cer $TMPDIR/ca.cer \ -verify" subj="-subj CN=SubCA -subj C=RU" -test_expect_success "$eeAlgo: SubCA generation" "certool \ +test_expect_success "$eeAlgo: SubCA load generation" "certool \ -algo $eeAlgo \ -ku ca -ku sig $subj \ - -prv $TMPDIR/subca.prv -cer $TMPDIR/subca.cer \ + -prv $TMPDIR/subca.prv -cer $TMPDIR/subca.cer" +test_expect_success "$eeAlgo: SubCA generation" "certool \ + -cer $TMPDIR/subca.cer \ -ca-cer $TMPDIR/ca.cer -ca-prv $TMPDIR/ca.prv" test_expect_success "$eeAlgo: SubCA signature" "certool \ -ca-cer $TMPDIR/ca.cer \ @@ -42,10 +38,12 @@ test_expect_success "$eeAlgo: SubCA signature" "certool \ -verify" subj="-subj CN=EE -subj C=RU" -test_expect_success "$eeAlgo: EE generation" "certool \ +test_expect_success "$eeAlgo: EE load generation" "certool \ -algo $eeAlgo $subj \ - -ca-prv $TMPDIR/subca.prv -ca-cer $TMPDIR/subca.cer \ -prv $TMPDIR/ee.prv -cer $TMPDIR/ee.cer" +test_expect_success "$eeAlgo: EE generation" "certool \ + -ca-prv $TMPDIR/subca.prv -ca-cer $TMPDIR/subca.cer \ + -cer $TMPDIR/ee.cer" test_expect_success "$eeAlgo: EE chain" "certool \ -ca-cer $TMPDIR/ca.cer \ -ca-cer $TMPDIR/subca.cer \ diff --git a/go/pki/cmd/certool/main.go b/go/pki/cmd/certool/main.go index 7b40f29..2b84822 100644 --- a/go/pki/cmd/certool/main.go +++ b/go/pki/cmd/certool/main.go @@ -20,8 +20,10 @@ import ( "crypto" "errors" "flag" + "fmt" "log" "os" + "sort" "strings" "time" @@ -34,6 +36,7 @@ import ( ) func main() { + flag.Usage = usage ku := make(map[string]*struct{}) subj := make(map[string]string) flag.Func( @@ -69,24 +72,33 @@ func main() { "Optional notBefore, \"2006-01-02 15:04:05\" format") lifetime := flag.Uint("lifetime", 365, "Lifetime of the certificate, days") - algo := flag.String("algo", pki.GOST3410256A, "Public key algorithm") + algo := flag.String("algo", pki.Ed25519BLAKE2b, "Public key algorithm") issuingPrv := flag.String("ca-prv", "", "Path to private key file for issuing with") - reuseKey := flag.Bool("reuse-key", false, - "Reuse the key, do not generate new one") prvPath := flag.String("prv", "", "Path to private key file") cerPath := flag.String("cer", "", "Path to certificate file") verify := flag.Bool("verify", false, "Verify provided -cer with -ca-cer") + doList := flag.Bool("list-algo", false, "List available algorithms") flag.Parse() log.SetFlags(log.Lshortfile) - if *cerPath == "" { - log.Fatal("no -cer is set") + if *doList { + algos := []string{ + pki.Ed25519BLAKE2b, + pki.GOST3410256A, + pki.GOST3410512C, + pki.SNTRUP4591761X25519, + } + sort.Strings(algos) + for _, s := range algos { + fmt.Println(s) + } + return } - if !*verify && len(subj) == 0 { - log.Fatal("no -subj is set") + if *cerPath == "" { + log.Fatal("no -cer is set") } var err error @@ -104,16 +116,16 @@ func main() { var caPrv crypto.Signer var caCers []*pki.Signed for _, issuingCer := range issuingCers { - var sd *pki.Signed - sd, err = pki.CerParse(utils.MustReadFile(issuingCer)) + var signed *pki.Signed + signed, err = pki.CerParse(utils.MustReadFile(issuingCer)) if err != nil { log.Fatal(err) } - caCers = append(caCers, sd) + caCers = append(caCers, signed) } if len(caCers) > 0 && !*verify { if *issuingPrv == "" { - log.Fatal("no -ca-key is set") + log.Fatal("no -ca-prv is set") } caPrv, _, err = pki.PrvParse(utils.MustReadFile(*issuingPrv)) if err != nil { @@ -122,30 +134,35 @@ func main() { } if *verify { - var sd *pki.Signed - sd, err = pki.CerParse(utils.MustReadFile(*cerPath)) + var signed *pki.Signed + signed, err = pki.CerParse(utils.MustReadFile(*cerPath)) if err != nil { log.Fatal(err) } - err = sd.CerVerify(caCers, time.Now().UTC()) + err = signed.CerVerify(caCers, time.Now().UTC()) if err != nil { log.Fatal(err) } return } - if *prvPath == "" { - log.Fatal("no -prv is set") - } - var prvRaw []byte - var pub []byte - if *reuseKey { - _, pub, err = pki.PrvParse(utils.MustReadFile(*prvPath)) + var cerLoad *pki.CerLoad + var signed *pki.Signed + if caPrv != nil { + signed, err = pki.CerParse(utils.MustReadFile(*cerPath)) if err != nil { log.Fatal(err) } + cerLoad = signed.CerLoad() } else { + if len(subj) == 0 { + log.Fatal("no -subj is set") + } + var pub []byte + if *prvPath == "" { + log.Fatal("no -prv is set") + } switch *algo { case pki.Ed25519BLAKE2b: _, prvRaw, pub, err = ed25519blake2b.NewKeypair() @@ -161,7 +178,7 @@ func main() { } { var buf bytes.Buffer - if _, err = keks.Encode(&buf, keks.Magic(pki.PrvKeyMagic), nil); err != nil { + if _, err = keks.Encode(&buf, pki.PrvKeyMagic, nil); err != nil { log.Fatal(err) } if _, err = keks.Encode(&buf, pki.AV{A: *algo, V: prvRaw}, nil); err != nil { @@ -172,36 +189,38 @@ func main() { log.Fatal(err) } } + { + pubMap := pki.Pub{A: *algo, V: pub} + { + av := pki.AV{A: *algo, V: pub} + pubMap.Id = av.Id() + } + cerLoad = &pki.CerLoad{Subj: subj, Pub: []pki.Pub{pubMap}} + } + if len(ku) > 0 { + cerLoad.KU = &ku + } } - pubMap := pki.Pub{A: *algo, V: pub} { - av := pki.AV{A: *algo, V: pub} - pubMap.Id = av.Id() - } - cerLoad := pki.CerLoad{Subj: subj, Pub: []pki.Pub{pubMap}} - if len(ku) > 0 { - cerLoad.KU = &ku + cerLoadAny := any(cerLoad) + signed = &pki.Signed{Load: pki.SignedLoad{T: "cer", V: &cerLoadAny}} } - var caCerLoad *pki.CerLoad - if caPrv == nil { - caCerLoad = &cerLoad - } else { - caCerLoad = caCers[0].CerLoad() - } - sd := pki.Signed{Load: pki.SignedLoad{T: "cer", V: cerLoad}} + if caPrv != nil { - if err = sd.CerIssueWith(caCerLoad, caPrv, since, till); err != nil { + if err = signed.CerIssueWith( + caCers[0].CerLoad(), caPrv, since, till, + ); err != nil { log.Fatal(err) } } { var buf bytes.Buffer - if _, err = keks.Encode(&buf, keks.Magic(pki.CerMagic), nil); err != nil { + if _, err = keks.Encode(&buf, pki.CerMagic, nil); err != nil { log.Fatal(err) } - if _, err = keks.Encode(&buf, sd, nil); err != nil { + if _, err = keks.Encode(&buf, signed, nil); err != nil { log.Fatal(err) } if err = os.WriteFile(*cerPath, buf.Bytes(), 0o666); err != nil { diff --git a/go/pki/cmd/certool/usage.go b/go/pki/cmd/certool/usage.go new file mode 100644 index 0000000..344fbd3 --- /dev/null +++ b/go/pki/cmd/certool/usage.go @@ -0,0 +1,37 @@ +// certool -- dealing with KEKS-encoded certificates utility +// 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 main + +import ( + "flag" + "fmt" + "os" +) + +func usage() { + fmt.Fprintf(os.Stderr, `Usage: + Generate certificate load: + certool -prv PRV -cer CER [-algo ALGO] [-ku KU ...] \ + -subj K=V [-subj K=V ...] + Sign certificate: + certool -cer CER -ca-prv CA-PRV -ca-cer CA-CER \ + [-lifetime DAYS] [-since DATE] + Verify certificate: + certool -verify -cer CER CA-PRV -ca-cer CA-CER0 [-ca-cer CA-CER1 ...] + +`) + flag.PrintDefaults() +} diff --git a/go/pki/cmd/enctool/main.go b/go/pki/cmd/enctool/main.go index d15b158..2e85721 100644 --- a/go/pki/cmd/enctool/main.go +++ b/go/pki/cmd/enctool/main.go @@ -86,6 +86,7 @@ func blake2b256() hash.Hash { 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") @@ -94,12 +95,12 @@ func main() { balloonP := flag.Int("balloon-p", 2, "Balloon's number of threads") doDecrypt := flag.Bool("d", false, "Decrypt") var pubs []*pki.Pub - flag.Func("pub", "Encrypt to, path to .cer", func(v string) error { - sd, err := pki.CerParse(utils.MustReadFile(v)) + flag.Func("cer", "Path to certificate to encrypt to", func(v string) error { + signed, err := pki.CerParse(utils.MustReadFile(v)) if err != nil { return err } - load := sd.CerLoad() + load := signed.CerLoad() if load.KU == nil { log.Println(v, "does not have key usages") } else { @@ -115,11 +116,12 @@ func main() { }) var prvs []*pki.AV flag.Func("prv", "Our private keys for decryption", func(v string) (err error) { + magic, data := keks.StripMagic(utils.MustReadFile(v)) + if magic == "" || magic != pki.PrvKeyMagic { + return errors.New("wrong magic") + } var av pki.AV - d := keks.NewDecoderFromBytes( - utils.MustReadFile(v), - &keks.DecodeOpts{MaxStrLen: 1 << 16}, - ) + d := keks.NewDecoderFromBytes(data, &keks.DecodeOpts{MaxStrLen: 1 << 16}) if err = d.DecodeStruct(&av); err != nil { return err } @@ -200,7 +202,7 @@ func main() { if len(*kem.Encap) != sntrup4591761.CiphertextSize+32 { log.Fatalln("invalid encap len") } - for prvIdx, prv := range prvs { + for _, prv := range prvs { if len(prv.V) != sntrup4591761.PrivateKeySize+32 { log.Fatalln("invalid private keys len") } @@ -234,9 +236,13 @@ func main() { log.Fatal(err) } { + pub := append( + ourSNTRUP[382:], + ourX25519.PublicKey().Bytes()..., + ) ikm := bytes.Join([][]byte{ encrypted.Bind[:], - *kem.Encap, pubs[prvIdx].V, + *kem.Encap, pub, keySNTRUP[:], keyX25519, }, []byte{}) kek := hkdf.Extract(blake2b256, @@ -372,7 +378,7 @@ func main() { } { var hdr bytes.Buffer - if _, err = keks.Encode(&hdr, keks.Magic(pki.EncryptedMagic), nil); err != nil { + if _, err = keks.Encode(&hdr, pki.EncryptedMagic, nil); err != nil { log.Fatal(err) } if _, err = keks.Encode(&hdr, &Encrypted{ diff --git a/go/pki/cmd/enctool/usage.go b/go/pki/cmd/enctool/usage.go new file mode 100644 index 0000000..b9744cd --- /dev/null +++ b/go/pki/cmd/enctool/usage.go @@ -0,0 +1,36 @@ +// enctool -- dealing with KEKS-encoded pki-encrypted utility +// 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 main + +import ( + "flag" + "fmt" + "os" +) + +func usage() { + fmt.Fprintf(os.Stderr, `Usage: + Encrypt to recipient: + enctool -cer CER [-include-to] [-bind UUID] DATA.encrypted + Encrypt on passphrase: + enctool -passwd PASSPHRASE [-bind UUID] DATA.encrypted + [-balloon-s X] [-balloon-t X] [-balloon-p X] + Decrypt by providing possible KEMs: + enctool -d [-passwd ...] [-prv PRV ...] DATA + +`) + flag.PrintDefaults() +} diff --git a/go/pki/cmd/sigtool/main.go b/go/pki/cmd/sigtool/main.go index 93cdf5f..614536e 100644 --- a/go/pki/cmd/sigtool/main.go +++ b/go/pki/cmd/sigtool/main.go @@ -18,8 +18,8 @@ package main import ( "bufio" "bytes" - "crypto" "flag" + "hash" "io" "log" "os" @@ -31,16 +31,20 @@ import ( "go.cypherpunks.su/keks/pki" pkihash "go.cypherpunks.su/keks/pki/hash" "go.cypherpunks.su/keks/pki/utils" + "go.cypherpunks.su/keks/types" ) +const BlobChunkLen = 128 * 1024 + func main() { + flag.Usage = usage prvPath := flag.String("prv", "", "Path to private key file") cerPath := flag.String("cer", "", "Path to certificate file") - sdPath := flag.String("sd", "", "Path to pki-signed 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 with provided -cer") - encryptedBindingHex := flag.String("encrypted-binding", "", "Set encrypted-binding") + typ := flag.String("type", "data", "Set/check the load type") + verify := flag.Bool("verify", false, "Do verification") + encryptedBindingHex := flag.String("encrypted-binding", "", + "Set/check encrypted-binding, UUID") + detached := flag.Bool("detached", false, "Detached data mode") flag.Parse() log.SetFlags(log.Lshortfile) @@ -62,7 +66,8 @@ func main() { log.Fatal(err) } - var signer crypto.Signer + var signer pki.Signer + var hasher hash.Hash if !*verify { if *prvPath == "" { log.Fatal("no -prv is set") @@ -71,71 +76,142 @@ func main() { if err != nil { log.Fatal(err) } + hasher = signer.NewHasher() } - hasher := pkihash.ByName(*hashAlgo) - if hasher == nil { - log.Fatal("unknown -hash specified") - } - _, err = io.Copy(hasher, bufio.NewReader(os.Stdin)) - if err != nil { - log.Fatal(err) - } + stdin := bufio.NewReaderSize(os.Stdin, BlobChunkLen) if *verify { - var sd *pki.Signed - sd, err = pki.SignedParse(utils.MustReadFile(*sdPath)) + decoder := keks.NewDecoderFromReader(stdin, nil) + var t types.Type + t, err = decoder.Parse() if err != nil { log.Fatal(err) } - if len(sd.Sigs) == 0 { - log.Fatal("no sigs") + if t != types.Magic || decoder.Iter().Magic() != pki.SignedMagic { + log.Fatal("wrong magic") } - sig := sd.Sigs[0] - if sd.Hashes == nil || sig.TBS.Hashes == nil { - log.Fatal("no hashes") + decoder = keks.NewDecoderFromReader(stdin, nil) + if _, err = decoder.Parse(); err != nil { + log.Fatal(err) } - hashes := *sig.TBS.Hashes - hashExpect := hashes[*hashAlgo] - hashGot := hasher.Sum(nil) - if hashExpect == nil || !bytes.Equal(hashExpect, hashGot) { - log.Fatal("hash mismatch") + var prehash pki.SignedPrehash + var signed pki.Signed + err = decoder.UnmarshalStruct(&prehash) + if err == nil && prehash.T == pki.SignedPrehashT { + if len(prehash.Sigs) == 0 { + log.Fatal("prehash: no sigs") + } + if len(prehash.Sigs) > 1 { + log.Fatal("prehash: currently only single signature support") + } + for algo := range prehash.Sigs { + hasher = pkihash.ByName(algo) + } + var blob *keks.BlobDecoder + blob, err = keks.NewBlobDecoder(stdin, 1<<32) + if err != nil { + log.Fatal(err) + } + var chunk []byte + mw := io.MultiWriter(hasher, os.Stdout) + for { + chunk, err = blob.Next() + if err != nil { + if err != io.EOF { + log.Fatal(err) + } + break + } + if _, err = io.Copy(mw, bytes.NewReader(chunk)); err != nil { + log.Fatal(err) + } + } + decoder = keks.NewDecoderFromReader(stdin, nil) + err = decoder.DecodeStruct(&signed) + } else { + err = decoder.UnmarshalStruct(&signed) } - signer := cer.CerLoad() - if !signer.Can(pki.KUSig) || len(signer.Pub) != 1 { - log.Fatal("cer can not sign") + if err != nil { + log.Fatal(err) + } + if err = pki.SignedValidate(&signed); err != nil { + log.Fatal(err) + } + if len(signed.Sigs) == 0 { + log.Fatal("no sigs") + } + if len(signed.Sigs) > 1 { + log.Fatal("prehash: currently only single signature support") } + sig := signed.Sigs[0] + signer := cer.CerLoad() if sig.Sign.A != signer.Pub[0].A { log.Fatal("differing signature algorithms") } - err = sd.CerCheckSignatureFrom(signer) - if err != nil { + if signed.Load.T != *typ { + log.Fatalln("differing load type:", signed.Load.T) + } + if encryptedBinding != uuid.Nil { + if sig.TBS.EncryptedBinding == nil { + log.Fatal("missing encrypted-binding") + } + if *sig.TBS.EncryptedBinding != encryptedBinding { + log.Fatalln("differing encrypted-binding:", *sig.TBS.EncryptedBinding) + } + } + if prehash.T == "" { + hasher = pkihash.ByName(sig.Sign.A) + if _, err = io.Copy(hasher, stdin); err != nil { + log.Fatal(err) + } + } + if err = signed.CerCheckSignatureFrom(signer, &hasher); err != nil { log.Fatal(err) } } else { - var sd pki.Signed - sd.Load.T = *typ - sdHashes := map[string]*struct{}{*hashAlgo: nil} - sd.Hashes = &sdHashes - sigHashes := map[string][]byte{*hashAlgo: hasher.Sum(nil)} - when := time.Now().UTC().Truncate(1000 * time.Microsecond) - sigTbs := pki.SigTBS{ - Hashes: &sigHashes, - When: &when, + if _, err = keks.Encode(os.Stdout, pki.SignedMagic, nil); err != nil { + log.Fatal(err) } + if *detached { + if _, err = io.Copy(hasher, 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}, + }, nil); err != nil { + log.Fatal(err) + } + pr, pw := io.Pipe() + br := keks.BlobReader{R: pr, ChunkLen: BlobChunkLen} + copyErr := make(chan error) + go func() { + _, e := io.Copy(io.MultiWriter(pw, hasher), stdin) + pw.Close() + copyErr <- e + }() + _, err = keks.Encode(os.Stdout, br, nil) + if err != nil { + log.Fatal(err) + } + if err = <-copyErr; err != nil { + log.Fatal(err) + } + } + var signed pki.Signed + signed.Load.T = *typ + when := time.Now().UTC().Truncate(1000 * time.Microsecond) + sigTbs := pki.SigTBS{When: &when} if encryptedBinding != uuid.Nil { sigTbs.EncryptedBinding = &encryptedBinding } - err = sd.SignWith(cer.CerLoad(), signer, sigTbs) - if err != nil { + if err = signed.SignPrehashedWith( + hasher, cer.CerLoad(), signer, sigTbs, + ); err != nil { log.Fatal(err) } - var data []byte - data, err = keks.EncodeBuf(sd, nil) - if err != nil { - log.Fatal(err) - } - err = os.WriteFile(*sdPath, data, 0o666) - if err != nil { + if _, err = keks.Encode(os.Stdout, signed, nil); err != nil { log.Fatal(err) } } diff --git a/go/pki/cmd/sigtool/usage.go b/go/pki/cmd/sigtool/usage.go new file mode 100644 index 0000000..c3ad3d6 --- /dev/null +++ b/go/pki/cmd/sigtool/usage.go @@ -0,0 +1,37 @@ +// sigtool -- dealing with KEKS-encoded pki-signed utility +// 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 main + +import ( + "flag" + "fmt" + "os" +) + +func usage() { + fmt.Fprintf(os.Stderr, `Usage: + sigtool -prv PRV -cer CER [-type TYPE] [bind] DATA.signed + sigtool -verify -cer CER [-type TYPE] [bind] DATA + sigtool -detached -prv PRV -cer CER [-type TYPE] [bind] DATA.signature + sigtool -detached -verify -cer CER [-type TYPE] [bind] <(cat DATA.signature DATA) + +"bind" is optional -encrypted-binding flag. +DATA.signed holds completely encapsulated DATA. +-detached mode keeps DATA completely separate. + +`) + flag.PrintDefaults() +} diff --git a/go/pki/ed25519-blake2b/ed25519-to-blake2b.patch b/go/pki/ed25519-blake2b/ed25519-to-blake2b.patch index f3cbc22..69bafad 100644 --- a/go/pki/ed25519-blake2b/ed25519-to-blake2b.patch +++ b/go/pki/ed25519-blake2b/ed25519-to-blake2b.patch @@ -2,7 +2,7 @@ +++ ed25519/ed25519.go 2024-12-03 11:07:51.892841000 +0300 @@ -20,11 +20,12 @@ "crypto" - "go.cypherpunks.su/gokeks/pki/ed25519-blake2b/edwards25519" + "go.cypherpunks.su/keks/pki/ed25519-blake2b/edwards25519" cryptorand "crypto/rand" - "crypto/sha512" "crypto/subtle" diff --git a/go/pki/ed25519-blake2b/kp.go b/go/pki/ed25519-blake2b/kp.go index 92ff449..13aa582 100644 --- a/go/pki/ed25519-blake2b/kp.go +++ b/go/pki/ed25519-blake2b/kp.go @@ -16,20 +16,19 @@ package ed25519blake2b import ( - "crypto" "crypto/rand" "go.cypherpunks.su/keks/pki/ed25519-blake2b/ed25519" ) -func NewKeypair() (signer crypto.Signer, prv, pub []byte, err error) { +func NewKeypair() (signer *Signer, prv, pub []byte, err error) { var prvEd ed25519.PrivateKey var pubEd ed25519.PublicKey pubEd, prvEd, err = ed25519.GenerateKey(rand.Reader) if err != nil { return } - signer = prvEd + signer = &Signer{prvEd} prv = prvEd.Seed() pub = pubEd[:] return diff --git a/go/pki/ed25519-blake2b/mk-from-go b/go/pki/ed25519-blake2b/mk-from-go index 666b053..b53ae56 100755 --- a/go/pki/ed25519-blake2b/mk-from-go +++ b/go/pki/ed25519-blake2b/mk-from-go @@ -4,7 +4,7 @@ # That script copies the library (tested on 1.23.3) and patches it to # use BLAKE2b hash. -modname=go.cypherpunks.su/gokeks/pki/ed25519-blake2b +modname=go.cypherpunks.su/keks/pki/ed25519-blake2b go mod init $modname dst=$PWD cd $(go env GOROOT)/src diff --git a/go/pki/ed25519-blake2b/prv.go b/go/pki/ed25519-blake2b/prv.go index c25e7e6..d9c62d8 100644 --- a/go/pki/ed25519-blake2b/prv.go +++ b/go/pki/ed25519-blake2b/prv.go @@ -18,17 +18,48 @@ package ed25519blake2b import ( "crypto" "errors" + "hash" + "io" "go.cypherpunks.su/keks/pki/ed25519-blake2b/ed25519" + "golang.org/x/crypto/blake2b" ) -func NewSigner(v []byte) (prv crypto.Signer, pub []byte, err error) { +type Signer struct { + Prv ed25519.PrivateKey +} + +func (s *Signer) Public() crypto.PublicKey { + return s.Prv.Public() +} + +func (s *Signer) Sign( + rand io.Reader, + 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}) +} + +func (s *Signer) NewHasher() hash.Hash { + h, err := blake2b.New512(nil) + if err != nil { + panic(err) + } + return h +} + +func NewSigner(v []byte) (prv *Signer, pub []byte, err error) { if len(v) != ed25519.SeedSize { err = errors.New("wrong ed25519 private key size") return } p := ed25519.NewKeyFromSeed(v) pub = p[ed25519.SeedSize:] - prv = p + prv = &Signer{p} return } diff --git a/go/pki/ed25519-blake2b/verify.go b/go/pki/ed25519-blake2b/verify.go index afec09e..662c7f4 100644 --- a/go/pki/ed25519-blake2b/verify.go +++ b/go/pki/ed25519-blake2b/verify.go @@ -16,6 +16,7 @@ package ed25519blake2b import ( + "crypto" "errors" "go.cypherpunks.su/keks/pki/ed25519-blake2b/ed25519" @@ -29,3 +30,25 @@ func Verify(pub, signed, signature []byte) (valid bool, err error) { valid = ed25519.Verify(ed25519.PublicKey(pub), signed, signature) return } + +func VerifyDigest(pub, digest, 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, + signature, + &ed25519.Options{Hash: crypto.BLAKE2b_512}, + ) + if err == nil { + valid = true + } else { + if err.Error() == "ed25519: invalid signature" { + valid = false + err = nil + } + } + return +} diff --git a/go/pki/gost/kp.go b/go/pki/gost/kp.go index a9836dc..b89c255 100644 --- a/go/pki/gost/kp.go +++ b/go/pki/gost/kp.go @@ -1,7 +1,6 @@ package gost import ( - "crypto" "crypto/rand" "errors" "io" @@ -9,7 +8,7 @@ import ( "go.cypherpunks.su/gogost/v6/gost3410" ) -func NewKeypair(algo string) (signer crypto.Signer, prv, pub []byte, err error) { +func NewKeypair(algo string) (signer *Signer, prv, pub []byte, err error) { curve := CurveByName(algo) if curve == nil { err = errors.New("unknown curve") diff --git a/go/pki/gost/signer.go b/go/pki/gost/signer.go index 2f7436b..b869631 100644 --- a/go/pki/gost/signer.go +++ b/go/pki/gost/signer.go @@ -36,7 +36,20 @@ func (s *Signer) Sign( return } -func NewSigner(a string, v []byte) (prv crypto.Signer, pub []byte, err error) { +func (s *Signer) SignDigest(rand io.Reader, dgst []byte) (signature []byte, err error) { + signature, err = s.Prv.SignDigest(dgst, rand) + if err != nil { + return + } + signature = append(signature[len(signature)/2:], signature[:len(signature)/2]...) + return +} + +func (s *Signer) NewHasher() hash.Hash { + return s.Hasher() +} + +func NewSigner(a string, v []byte) (prv *Signer, pub []byte, err error) { signer := Signer{} switch a { case GOST3410256A: diff --git a/go/pki/gost/verify.go b/go/pki/gost/verify.go index a307b55..6046b02 100644 --- a/go/pki/gost/verify.go +++ b/go/pki/gost/verify.go @@ -28,3 +28,14 @@ func Verify(algo string, pub, signed, signature []byte) (valid bool, err error) append(signature[len(signature)/2:], signature[:len(signature)/2]...)) return } + +func VerifyDigest(algo string, pub, digest, 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, + 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 c83e58f..fdde23b 100644 --- a/go/pki/hash/algo.go +++ b/go/pki/hash/algo.go @@ -21,6 +21,9 @@ import ( "go.cypherpunks.su/gogost/v6/gost34112012256" "go.cypherpunks.su/gogost/v6/gost34112012512" "golang.org/x/crypto/blake2b" + + ed25519blake2b "go.cypherpunks.su/keks/pki/ed25519-blake2b" + "go.cypherpunks.su/keks/pki/gost" ) const ( @@ -32,11 +35,11 @@ const ( func ByName(name string) hash.Hash { switch name { - case Streebog256: + case Streebog256, gost.GOST3410256A: return gost34112012256.New() - case Streebog512: + case Streebog512, gost.GOST3410512C: return gost34112012512.New() - case BLAKE2b: + case BLAKE2b, ed25519blake2b.Ed25519BLAKE2b: h, err := blake2b.New512(nil) if err != nil { panic(err) diff --git a/go/pki/prv.go b/go/pki/prv.go index fd8f885..94acb28 100644 --- a/go/pki/prv.go +++ b/go/pki/prv.go @@ -16,7 +16,6 @@ package pki import ( - "crypto" "errors" "fmt" @@ -26,7 +25,7 @@ import ( ) // Parse private key contained in AV KEKS-encoded structure. -func PrvParse(data []byte) (prv crypto.Signer, pub []byte, err error) { +func PrvParse(data []byte) (prv Signer, pub []byte, err error) { { var magic keks.Magic magic, data = keks.StripMagic(data) diff --git a/go/pki/signed.go b/go/pki/signed.go index 4de2cd8..922579f 100644 --- a/go/pki/signed.go +++ b/go/pki/signed.go @@ -19,6 +19,8 @@ import ( "crypto" "crypto/rand" "errors" + "hash" + "io" "time" "github.com/google/uuid" @@ -26,19 +28,31 @@ import ( "go.cypherpunks.su/keks" ) -const SignedMagic = "pki/signed" +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"` +} type SignedLoad struct { - V any `keks:"v,omitempty"` + V *any `keks:"v,omitempty"` T string `keks:"t"` } type SigTBS struct { - Hashes *map[string][]byte `keks:"hash,omitempty"` - CID *uuid.UUID `keks:"cid,omitempty"` - Exp *[]time.Time `keks:"exp,omitempty"` - When *time.Time `keks:"when,omitempty"` - SID uuid.UUID `keks:"sid"` + CID *uuid.UUID `keks:"cid,omitempty"` + Exp *[]time.Time `keks:"exp,omitempty"` + When *time.Time `keks:"when,omitempty"` + SID uuid.UUID `keks:"sid"` EncryptedBinding *uuid.UUID `keks:"encrypted-binding,omitempty"` } @@ -50,45 +64,36 @@ type Sig struct { } type SignedTBS struct { - V any `keks:"v"` + V any `keks:"v,omitempty"` T string `keks:"t"` TBS SigTBS `keks:"tbs"` } type Signed struct { - Hashes *map[string]*struct{} `keks:"hash,omitempty"` - Cers *[]*Signed `keks:"certs,omitempty"` - Load SignedLoad `keks:"load"` - Sigs []*Sig `keks:"sigs"` + Cers *[]*Signed `keks:"certs,omitempty"` + Load SignedLoad `keks:"load"` + Sigs []*Sig `keks:"sigs"` } // Validate parsed pki-signed structure. -func SignedValidate(sd *Signed) (err error) { - if sd.Hashes != nil && len(*sd.Hashes) == 0 { - err = errors.New("SignedParse: empty /hash") - return - } - if sd.Cers != nil { - if len(*sd.Cers) == 0 { +func SignedValidate(signed *Signed) (err error) { + if signed.Cers != nil { + if len(*signed.Cers) == 0 { err = errors.New("SignedParse: empty /certs") return } - for _, cer := range *sd.Cers { + for _, cer := range *signed.Cers { err = cer.CerParse() if err != nil { return } } } - for _, sig := range sd.Sigs { + for _, sig := range signed.Sigs { if sig.CerLoc != nil && len(*sig.CerLoc) == 0 { err = errors.New("SignedParse: empty cer-loc") return } - if sig.TBS.Hashes != nil && len(*sig.TBS.Hashes) == 0 { - err = errors.New("SignedParse: empty hash") - return - } if sig.TBS.Exp != nil { if len(*sig.TBS.Exp) != 2 { err = errors.New("SignedParse: wrong exp len") @@ -101,23 +106,6 @@ func SignedValidate(sd *Signed) (err error) { } } } - if sd.Hashes != nil { - if sig.TBS.Hashes == nil { - err = errors.New("SignedParse: /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("SignedParse: /sigs: no hash") - return - } - } } return } @@ -133,19 +121,19 @@ func SignedParse(data []byte) (*Signed, error) { } } d := keks.NewDecoderFromBytes(data, nil) - var sd Signed - err := d.DecodeStruct(&sd) + var signed Signed + err := d.DecodeStruct(&signed) if err != nil { return nil, err } - err = SignedValidate(&sd) - return &sd, err + err = SignedValidate(&signed) + return &signed, err } // Sign Signed's contents and sigTBS corresponding data with the // provided prv signer, having parent certificate. Signature is appended -// to the sd.Sigs. parent certificate must have "sig" key-usage. -func (sd *Signed) SignWith( +// to the signed.Sigs. parent certificate must have "sig" key-usage. +func (signed *Signed) SignWith( parent *CerLoad, prv crypto.Signer, sigTBS SigTBS, @@ -154,11 +142,11 @@ func (sd *Signed) SignWith( return errors.New("parent can not sign") } sigTBS.SID = parent.Pub[0].Id - sdTBS := SignedTBS{T: sd.Load.T, V: sd.Load.V, TBS: sigTBS} + 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(sdTBS, nil) + buf, err = keks.EncodeBuf(signedTBS, nil) if err != nil { return } @@ -166,6 +154,32 @@ func (sd *Signed) SignWith( if err != nil { return } - sd.Sigs = append(sd.Sigs, &sig) + 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") + } + 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)) + if err != nil { + return + } + signed.Sigs = append(signed.Sigs, &sig) return nil } diff --git a/spec/format/cer-load.cddl b/spec/format/cer-load.cddl index c9281fe..8e3e228 100644 --- a/spec/format/cer-load.cddl +++ b/spec/format/cer-load.cddl @@ -9,5 +9,5 @@ cer-load = { * text => any } -ku = "ca" / "sig" / "app-name" / text +ku = "ca" / "sig" / "kem" / "app-name" / text crit-ext-type = text diff --git a/spec/format/cer.texi b/spec/format/cer.texi index 1c9d9a9..b180ae0 100644 --- a/spec/format/cer.texi +++ b/spec/format/cer.texi @@ -8,6 +8,10 @@ Stored in a file, it should begin with "pki/cer" @ref{Magic, magic}. Its @code{/load/t} equals to @code{cer}. @code{/load/v} contains @code{cer-load}: +@verbatim +cer = pki-signed ; with /load/t = "cer", /load/v = cer-load +@end verbatim + @verbatiminclude format/cer-load.cddl @table @code diff --git a/spec/format/encrypted.cddl b/spec/format/encrypted.cddl index 829423c..0122491 100644 --- a/spec/format/encrypted.cddl +++ b/spec/format/encrypted.cddl @@ -28,7 +28,7 @@ kem-generic = { * text => any } -kem-balloon-blake2b = { +kem-balloon-blake2b-hkdf = { a: "balloon-blake2b-hkdf", cek: bytes, cost: { diff --git a/spec/format/private-key.texi b/spec/format/private-key.texi index 1933d1a..d41ce92 100644 --- a/spec/format/private-key.texi +++ b/spec/format/private-key.texi @@ -2,8 +2,6 @@ @cindex private-key @section private-key format -Private key is stored in trivial map: - @verbatiminclude format/private-key.cddl Stored in a file, it should begin with "pki/prvkey" @ref{Magic, magic}. @@ -13,7 +11,7 @@ Stored in a file, it should begin with "pki/prvkey" @ref{Magic, magic}. Big-endian private key representation must be used. -Following algorithm identifiers are acceptable: +Following algorithm identifiers are used: @code{gost3410-256A}, @code{gost3410-512C}. @node private-key-ed25519-blake2b diff --git a/spec/format/registry.texi b/spec/format/registry.texi index 4439102..ba42874 100644 --- a/spec/format/registry.texi +++ b/spec/format/registry.texi @@ -21,9 +21,7 @@ There is example registry of known algorithm identifiers. @item skein512 @code{@ref{pki-hashed-skein512}} @item streebog256, streebog512 - @code{@ref{cer-gost3410}}, - @code{@ref{pki-hashed-gost3411}}, - @code{@ref{pki-signed-gost3410}} + @code{@ref{pki-hashed-gost3411}} @item xxh3-128 @code{@ref{pki-hashed-xxh3-128}} @end table diff --git a/spec/format/signed-prehash.cddl b/spec/format/signed-prehash.cddl new file mode 100644 index 0000000..0d3c686 --- /dev/null +++ b/spec/format/signed-prehash.cddl @@ -0,0 +1,4 @@ +pki-signed-prehash = { + t: "prehash", + sigs: set, ; set of signature algorithm identifiers (/sigs/*/sign/a) +} diff --git a/spec/format/signed-tbs.cddl b/spec/format/signed-tbs.cddl index 137d8a6..94e8a04 100644 --- a/spec/format/signed-tbs.cddl +++ b/spec/format/signed-tbs.cddl @@ -1,5 +1,5 @@ pki-signed-tbs = { t: text, ; = /load/t - v: nil / any, + ? v: any, ; /load/v if it is supplied tbs: map, ; = /sigs/?/tbs } diff --git a/spec/format/signed.cddl b/spec/format/signed.cddl index 6579cb1..cb7077d 100644 --- a/spec/format/signed.cddl +++ b/spec/format/signed.cddl @@ -1,21 +1,17 @@ ai = text ; algorithm identifier -av = {a: ai, v: bytes} pki-signed = { - ? hash: set, ; when using prehashing load: { t: text, - ? v: bytes / text / blob / map / list, + ? v: bytes / text / map / list, }, sigs: [+ sig], ? certs: [+ cer], } -cer = pki-signed ; with /load/t = "cer", /load/v = cer-load - sig = { tbs: sig-tbs, - sign: av, + sign: {a: ai, v: bytes}, ? cer-loc: [+ url], * text => any } @@ -24,8 +20,7 @@ url = text sig-tbs = { sid: uuid, ; signer's public key id - ? hash: {ai => bytes}, ; when using prehashing - ? envelope-binding: uuid, + ? encrypted-binding: uuid, ? when: tai64 / tai64n, * text => any } diff --git a/spec/format/signed.texi b/spec/format/signed.texi index af5d1c9..52f413f 100644 --- a/spec/format/signed.texi +++ b/spec/format/signed.texi @@ -6,7 +6,7 @@ That resembles @url{https://datatracker.ietf.org/doc/html/rfc5652, CMS} (PKCS#7) ASN.1-based format. Stored in a file, it should begin with "pki/signed" @ref{Magic, magic}, -unless this is a @ref{cer, certificate}. +unless it is a @ref{cer, certificate}. @verbatiminclude format/signed.cddl @@ -14,14 +14,22 @@ Signature is created by signing the following encoded MAP: @verbatiminclude format/signed-tbs.cddl -If prehashing is used, then @code{/hash} tells what algorithms will be -used to hash the data of @code{/load/v}. If @code{/load/v} is either a -MAP or LIST, then its encoded binary representation is hashed. If it is -BIN/STR or BLOB, then its binary contents are hashed. So signature will -stay the same even if data is converted from BIN to BLOB. +If 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 +structure. You can provide it any way you wish, but for keeping that +detached data closely to the @code{pki-signed}, you should use the +following approach: -If @code{/load/v} is absent, then it is detached and must be provided as -a binary data from outside. +@verbatim +pki-signed-prehash || BLOB || 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. @code{/sigs/*/tbs/when} is optional signing time. @@ -35,7 +43,7 @@ help creating the whole verification chain. They are placed outside If signed data is also intended to be @ref{pki-encrypted, encrypted}, then @code{/sigs/*/tbs/encrypted-binding} should be set to -envelope's @code{/bind} value. +@ref{pki-encrypted, encrypted}'s @code{/bind} value. @node pki-signed-gost3410 @subsection pki-signed with GOST R 34.10-2012 @@ -45,9 +53,6 @@ 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. -Following algorithm identifiers should be used for the hash: -@code{streebog256}, @code{streebog512}. - Following algorithm identifiers are acceptable for the public key and signature: @code{gost3410-256A}, @code{gost3410-512C}. @@ -61,4 +66,7 @@ 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. + @code{ed25519-blake2b} algorithm identifier is used.