From f696f5ca0efa3ee2dede9bc65ad2725844058affcef53eaeed4df74852e4df68 Mon Sep 17 00:00:00 2001 From: Sergey Matveev Date: Mon, 6 Oct 2025 13:08:10 +0300 Subject: [PATCH] Directories with keys --- go/cm/cmd/cmenctool/main.go | 107 ++++++------ go/cm/cmd/cmenctool/multirecipient.t | 2 +- .../cmenctool/{missing-from.t => no-from.t} | 2 +- go/cm/cmd/cmenctool/no-id.t | 21 +++ go/cm/cmd/cmenctool/{missing-to.t => no-to.t} | 4 +- go/cm/cmd/cmenctool/prv-encrypted.t | 2 +- go/cm/cmd/cmenctool/pub.t | 8 +- go/cm/cmd/cmenctool/sender-auth.t | 7 +- go/cm/cmd/cmenctool/usage.go | 19 +- go/cm/cmd/cmkeytool/certification.t | 49 +++--- go/cm/cmd/cmkeytool/kem-generation.t | 3 +- go/cm/cmd/cmkeytool/main.go | 154 +++++++++------- go/cm/cmd/cmkeytool/usage.go | 9 +- go/cm/cmd/cmsigtool/basic.t | 33 ++-- go/cm/cmd/cmsigtool/main.go | 164 +++++++++--------- go/cm/cmd/cmsigtool/usage.go | 13 +- go/cm/enc/enc.go | 8 +- go/cm/sign/.gitignore | 1 + go/cm/sign/prv.go | 10 +- go/cm/sign/pub.go | 28 +-- go/cm/sign/schema.go | 14 ++ go/cm/sign/signed.go | 5 +- go/cm/sign/storage.go | 48 +++++ go/cm/utils/mk-bin | 1 + spec/Integrity | 3 +- spec/cm/prv/index | 5 +- tcl/schemas/prv.tcl | 8 + tcl/schemas/signed.tcl | 1 + 28 files changed, 437 insertions(+), 292 deletions(-) rename go/cm/cmd/cmenctool/{missing-from.t => no-from.t} (94%) create mode 100755 go/cm/cmd/cmenctool/no-id.t rename go/cm/cmd/cmenctool/{missing-to.t => no-to.t} (80%) create mode 100644 go/cm/sign/storage.go create mode 100644 tcl/schemas/prv.tcl diff --git a/go/cm/cmd/cmenctool/main.go b/go/cm/cmd/cmenctool/main.go index 7efd191..ebde3ea 100644 --- a/go/cm/cmd/cmenctool/main.go +++ b/go/cm/cmd/cmenctool/main.go @@ -57,16 +57,14 @@ import ( "go.cypherpunks.su/keks/types" ) -const ( - FdPubR = 4 - FdPrvR = 8 - - X25519KeyLen = 32 -) +const X25519KeyLen = 32 var ( - PrvDir = flag.String("prvs", "prvs", "Path to directory with private keys") - PubDir = flag.String("pubs", "pubs", "Path to directory with public keys") + PrvDir = flag.String("prvs", os.Getenv("CM_PRVS"), + "Path to directory with private keys") + PubDirPth = flag.String("pubs", os.Getenv("CM_PUBS"), + "Path to directory with public keys") + PubDir sign.PubStorageDir ) func blake2bHash() hash.Hash { @@ -77,17 +75,7 @@ func blake2bHash() hash.Hash { return h } -func pubParse(pth string) (pubData *sign.PubData, err error) { - var data []byte - data, err = os.ReadFile(pth) - if err != nil { - return - } - var signed *sign.Signed - signed, _, err = sign.PubParse(data) - if err != nil { - return - } +func pubDataFromSigned(signed *sign.Signed) (pubData *sign.PubData, err error) { pubData = signed.PubData() if !pubData.Can(sign.KUKEM) { err = fmt.Errorf("does not have %s key usage", sign.KUKEM) @@ -99,17 +87,18 @@ func pubParse(pth string) (pubData *sign.PubData, err error) { return } -func findPub(id []byte) (pub *sign.PubData, err error) { - pth := path.Join( - *PubDir, strings.ToUpper(hex.EncodeToString(id)), - ) - if _, err = os.Stat(pth); err != nil { - if errors.Is(err, fs.ErrNotExist) { - err = nil - } +func pubParse(pth string) (pubData *sign.PubData, err error) { + var data []byte + data, err = os.ReadFile(pth) + if err != nil { + return + } + var signed *sign.Signed + signed, _, err = sign.PubParse(data) + if err != nil { return } - return pubParse(pth) + return pubDataFromSigned(signed) } func findPrv(id []byte) (av *cm.AV, err error) { @@ -141,7 +130,7 @@ func findPrv(id []byte) (av *cm.AV, err error) { if v, err = d.Decode(); err != nil { return } - if err = schema.Check("av", sign.PubSchemas, v); err != nil { + if err = schema.Check("prv", sign.PrvSchemas, v); err != nil { return } } @@ -162,6 +151,7 @@ func main() { `Assume that hexadecimal "from" value if missing`) noFrom := flag.Bool("no-from", false, `Do not include "from" field in KEMs`) noTo := flag.Bool("no-to", false, `Do not include "to" field in KEMs`) + noId := flag.Bool("no-id", false, `Do not include "id" field`) passphrase := flag.Bool("p", false, "Use passphrase") balloonS := flag.Int("balloon-s", 1<<17, "Balloon's space cost") balloonT := flag.Int("balloon-t", 4, "Balloon's time cost") @@ -170,18 +160,19 @@ func main() { parallel := flag.Int("parallel", cmhash.DefaultNumCPU, "Parallel cryptors") noblob := flag.Bool("embed", false, "Include payload into container") fromPth := flag.String("from", "", "Path to sender's public key for authentication") + flag.Parse() + + PubDir.Dir = *PubDirPth var pubs []cm.AV var pubIds [][]byte - flag.Func("to", "Path to recipient's public key", func(v string) error { - pubData, err := pubParse(v) + for _, pth := range flag.Args() { + pubData, err := pubParse(pth) if err != nil { - log.Fatalln(v, ":", err) + log.Fatalln(pth, ":", err) } pubs = append(pubs, pubData.Pub[0]) pubIds = append(pubIds, pubData.Id) - return nil - }) - flag.Parse() + } var err error var assumeTo []byte @@ -283,6 +274,10 @@ func main() { if len(encrypted.KEM) == 0 { log.Fatalln("no KEMs") } + var id uuid.UUID + if encrypted.Id != nil { + id = *encrypted.Id + } for kemIdx, kem := range encrypted.KEM { switch kem.A { case cmballoon.BalloonBLAKE2bHKDF: @@ -295,7 +290,7 @@ func main() { if err != nil { log.Fatal(err) } - cek, err = cmballoon.Decapsulate(kem, encrypted.Id[:], passwd) + cek, err = cmballoon.Decapsulate(kem, id[:], passwd) if err != nil { log.Print(err) continue @@ -341,13 +336,18 @@ func main() { if bytes.Equal(kem.From, bytes.Repeat([]byte{0}, 32)) { kem.From = assumeFrom } - from, err = findPub(kem.From) + var signed *sign.Signed + signed, err = PubDir.Get(kem.From) if err != nil { log.Fatalln("from:", err) } - if from == nil { + if signed == nil { log.Fatalln(kemIdx, kem.A, "can not find public key") } + from, err = pubDataFromSigned(signed) + if err != nil { + log.Fatalln("from:", err) + } } var ourSNTRUP sntrup761kem.PrivateKey ourSNTRUP, err = scheme.UnmarshalBinaryPrivateKey( @@ -433,9 +433,7 @@ func main() { kek, err = hkdf.Expand( blake2bHash, prk, - string(append( - []byte(cmenc.SNTRUP761X25519Info), - encrypted.Id[:]...)), + string(append([]byte(cmenc.SNTRUP761X25519Info), id[:]...)), chacha20poly1305.KeySize) if err != nil { log.Fatal(err) @@ -487,13 +485,18 @@ func main() { if bytes.Equal(kem.From, bytes.Repeat([]byte{0}, 32)) { kem.From = assumeFrom } - from, err = findPub(kem.From) + var signed *sign.Signed + signed, err = PubDir.Get(kem.From) if err != nil { log.Fatalln("from:", err) } - if from == nil { + if signed == nil { log.Fatalln(kemIdx, kem.A, "can not find public key") } + from, err = pubDataFromSigned(signed) + if err != nil { + log.Fatalln("from:", err) + } } var ourMcEliece *mceliece6960119.PrivateKey ourMcEliece, err = mceliece6960119.UnmarshalBinaryPrivateKey( @@ -525,7 +528,7 @@ func main() { keyMcEliece, string(append( []byte(cmenc.ClassicMcEliece6960119X25519DecapInfo), - encrypted.Id[:]...)), + id[:]...)), chacha20poly1305.KeySize+chacha20poly1305.NonceSizeX, ) if err != nil { @@ -611,7 +614,7 @@ func main() { prk, string(append( []byte(cmenc.ClassicMcEliece6960119X25519Info), - encrypted.Id[:]...)), + id[:]...)), chacha20poly1305.KeySize, ) if err != nil { @@ -649,10 +652,12 @@ func main() { } } else { var id uuid.UUID - if *setId == "" { - id, err = uuid.NewRandom() - } else { - id, err = uuid.Parse(*setId) + if !*noId { + if *setId == "" { + id, err = uuid.NewRandom() + } else { + id, err = uuid.Parse(*setId) + } } if err != nil { log.Fatal(err) @@ -960,10 +965,12 @@ func main() { log.Fatal(err) } enc := cmenc.Encrypted{ - Id: id, KEM: kems, DEM: cmenc.DEM{A: chapoly.DEMAlgo}, } + if !*noId { + enc.Id = &id + } if *noblob { var buf bytes.Buffer if _, err = chapoly.Seal(&buf, os.Stdin, cek, *parallel); err != nil { diff --git a/go/cm/cmd/cmenctool/multirecipient.t b/go/cm/cmd/cmenctool/multirecipient.t index 560da9c..fd17432 100755 --- a/go/cm/cmd/cmenctool/multirecipient.t +++ b/go/cm/cmd/cmenctool/multirecipient.t @@ -11,7 +11,7 @@ test_expect_success "1: pub generation" "cmkeytool \ -algo sntrup761-x25519 -ku kem -sub N=1 5>enc.1.pub 9>enc.1.prv" test_expect_success "encrypting" " - cmenctool -to enc.0.pub -to enc.1.pub enc.enc" + cmenctool enc.0.pub enc.1.pub enc.enc" pubId=$(kekspp -v -p /data/id enc.enc" + -from s.pub -prvs prvs -no-from r.pub enc.enc" test_expect_success "$algo: decrypting fails" " ! cmenctool -d -prvs prvs -pubs pubs enc.data.got" test_expect_success "$algo: decrypting" " diff --git a/go/cm/cmd/cmenctool/no-id.t b/go/cm/cmd/cmenctool/no-id.t new file mode 100755 index 0000000..14aedcb --- /dev/null +++ b/go/cm/cmd/cmenctool/no-id.t @@ -0,0 +1,21 @@ +#!/bin/sh + +test_description="Check workability with missing \"id\"" +. $SHARNESS_TEST_SRCDIR/sharness.sh + +dd if=/dev/urandom of=enc.data bs=300K count=1 2>/dev/null + +for algo in sntrup761-x25519 mceliece6960119-x25519 ; do + +test_expect_success "$algo: pub generation" "cmkeytool \ + -algo $algo -ku kem -sub N=0 5>enc.pub 9>enc.prv" +test_expect_success "$algo: encrypting" "cmenctool -no-id enc.pub enc.enc" +pubId=$(kekspp -v -p /data/id enc.data.got" +test_expect_success "$algo: comparing" "test_cmp enc.data enc.data.got" + +done + +test_done diff --git a/go/cm/cmd/cmenctool/missing-to.t b/go/cm/cmd/cmenctool/no-to.t similarity index 80% rename from go/cm/cmd/cmenctool/missing-to.t rename to go/cm/cmd/cmenctool/no-to.t index 38a27f1..cc33dc7 100755 --- a/go/cm/cmd/cmenctool/missing-to.t +++ b/go/cm/cmd/cmenctool/no-to.t @@ -8,8 +8,8 @@ dd if=/dev/urandom of=enc.data bs=300K count=1 2>/dev/null for algo in sntrup761-x25519 mceliece6960119-x25519 ; do test_expect_success "$algo: pub generation" "cmkeytool \ - -algo sntrup761-x25519 -ku kem -sub N=0 5>enc.pub 9>enc.prv" -test_expect_success "$algo: encrypting" "cmenctool -no-to -to enc.pub enc.enc" + -algo $algo -ku kem -sub N=0 5>enc.pub 9>enc.prv" +test_expect_success "$algo: encrypting" "cmenctool -no-to enc.pub enc.enc" pubId=$(kekspp -v -p /data/id enc.prv.enc" test_expect_success "$algo: data encrypting" " - cmenctool -to enc.pub enc.enc" + cmenctool enc.pub enc.enc" ln -s enc.prv.enc $(kekspp -v -p /data/id enc.data.got" diff --git a/go/cm/cmd/cmenctool/pub.t b/go/cm/cmd/cmenctool/pub.t index ab24e3b..df47c34 100755 --- a/go/cm/cmd/cmenctool/pub.t +++ b/go/cm/cmd/cmenctool/pub.t @@ -18,7 +18,7 @@ test_expect_success "$algo: pub generation" " id1=$(kekspp -v -p /data/id enc.enc" + cmenctool enc.$algo0.pub enc.$algo1.pub enc.enc" ln -s enc.$algo0.prv $id0 ln -s enc.$algo1.prv $id1 @@ -27,8 +27,8 @@ test_expect_success "any: decrypting" " test_expect_success "comparing" "test_cmp enc.data enc.data.got" rm $id1 -test_expect_success "$algo0: decrypting" " - cmenctool -d -prvs . enc.data.got" +CM_PRVS=$(realpath .) test_expect_success "$algo0: decrypting" " + cmenctool -d enc.data.got" test_expect_success "$algo0: comparing" "test_cmp enc.data enc.data.got" rm $id0 @@ -40,7 +40,7 @@ test_expect_success "$algo1: comparing" "test_cmp enc.data enc.data.got" export CM_PASSPHRASE=$(dd if=/dev/urandom bs=32 count=1 2>/dev/null | xxd -p) test_expect_success "encrypting also with passphrase" " - cmenctool $balloonparams -p -to enc.$algo0.pub -to enc.$algo1.pub enc.enc" + cmenctool $balloonparams -p enc.$algo0.pub enc.$algo1.pub enc.enc" test_expect_success "any: decrypting" " cmenctool -d -prvs . enc.data.got" test_expect_success "comparing" "test_cmp enc.data enc.data.got" diff --git a/go/cm/cmd/cmenctool/sender-auth.t b/go/cm/cmd/cmenctool/sender-auth.t index c9079b3..694f882 100755 --- a/go/cm/cmd/cmenctool/sender-auth.t +++ b/go/cm/cmd/cmenctool/sender-auth.t @@ -17,13 +17,14 @@ ln -fs ../r.prv prvs/$(kekspp -v -p /data/id enc.enc" + -from s.pub -prvs prvs r.pub enc.enc" +export CM_PRVS=$(realpath prvs) CM_PUBS=$(realpath pubs) test_expect_success "$algo: decrypting" " - cmenctool -d -prvs prvs -pubs pubs enc.data.got" + cmenctool -d enc.data.got" test_expect_success "$algo: comparing" "test_cmp enc.data enc.data.got" ln -fs ../r.pub pubs/$(kekspp -v -p /data/id enc.data.got" + ! cmenctool -d enc.data.got" done diff --git a/go/cm/cmd/cmenctool/usage.go b/go/cm/cmd/cmenctool/usage.go index c2d43d9..69ee662 100644 --- a/go/cm/cmd/cmenctool/usage.go +++ b/go/cm/cmd/cmenctool/usage.go @@ -24,17 +24,22 @@ import ( func usage() { fmt.Fprintf(os.Stderr, `Usage: Encrypt to recipient(s): - cmenctool [-no-to] -to PUB0 [-to PUB1 ...] DATA.encrypted + cmenctool [-no-to] [-no-from] [-from PUB] [-prvs dir] \ + PUB0 [PUB1 ...] DATA.encrypted Encrypt on passphrase: cmenctool -p [-balloon-s X] [-balloon-t X] [-balloon-p X] DATA.encrypted Decrypt by providing possible KEMs and/or passphrase(s): - cmenctool -d [-p] -prvs prvs/dir DATA - With sender authentication: - cmenctool [...] -from PUB -prvs prvs/dir [...] - cmenctool [...] -d -pubs pubs/dir [...] + cmenctool -d [-p] [-prvs dir] [-pubs dir] DATA + +PUB is a path to public key files. +-prvs/-pubs directory must contain key files with uppercase hexadecimal +corresponding public key's id used as the filename. They can be set by +CM_PRVS and CM_PUBS environment variables. + +If -from is specified, then sender authentication will be enabled and +-prvs must be set to find corresponding private key. -pubs must be set +on recipient side in that case. -{prvs,pubs}/dir must contain key files with uppercase hexadecimal -corresponding public key's id used as the filename. `) flag.PrintDefaults() } diff --git a/go/cm/cmd/cmkeytool/certification.t b/go/cm/cmd/cmkeytool/certification.t index 7804502..503ca34 100755 --- a/go/cm/cmd/cmkeytool/certification.t +++ b/go/cm/cmd/cmkeytool/certification.t @@ -3,55 +3,50 @@ test_description="Check certification" . $SHARNESS_TEST_SRCDIR/sharness.sh +mkdir prvs pubs + echo "gost3410-512C gost3410-256A ed25519-blake2b ed25519-blake2b slh-dsa-shake-256s slh-dsa-shake-256s" | while read caAlgo eeAlgo ; do +unset CM_PRVS CM_PUBS sub="-sub CN=CA -sub C=RU" test_expect_success "$caAlgo: CA load generation" "cmkeytool \ -algo $caAlgo -ku sig $sub \ 5>ca.$caAlgo.pub 9>ca.$caAlgo.prv" -test_expect_success "$caAlgo: CA generation" "cmkeytool \ - 4ca.$caAlgo.pub.certified" +ln -fs ../ca.$caAlgo.prv prvs/$(kekspp -v -p /pub-id ca.$caAlgo.pub.certified" mv ca.$caAlgo.pub.certified ca.$caAlgo.pub -test_expect_success "$caAlgo: CA regeneration" "cmkeytool \ - 4ca.$caAlgo.pub.certified" +test_expect_success "$caAlgo: CA regeneration" "cmkeytool -c \ + ca.$caAlgo.pub ca.$caAlgo.pub >ca.$caAlgo.pub.certified" mv ca.$caAlgo.pub.certified ca.$caAlgo.pub -test_expect_success "$caAlgo: CA self-signature" " - cmkeytool -verify 4subca.$eeAlgo.pub 9>subca.$eeAlgo.prv" -test_expect_success "$eeAlgo: SubCA generation" "cmkeytool \ - 4subca.$eeAlgo.pub.certified" +test_expect_success "$eeAlgo: SubCA generation" "cmkeytool -c \ + subca.$eeAlgo.pub ca.$caAlgo.pub >subca.$eeAlgo.pub.certified" mv subca.$eeAlgo.pub.certified subca.$eeAlgo.pub -test_expect_success "$eeAlgo: SubCA signature" " - cmkeytool -verify 4ee.$eeAlgo.pub 9>ee.$eeAlgo.prv" -test_expect_success "$eeAlgo: EE generation" "cmkeytool \ - 4ee.$eeAlgo.pub.certified" +test_expect_success "$eeAlgo: EE generation" "cmkeytool -c \ + ee.$eeAlgo.pub subca.$eeAlgo.pub >ee.$eeAlgo.pub.certified" mv ee.$eeAlgo.pub.certified ee.$eeAlgo.pub -test_expect_success "$eeAlgo: EE chain" " - cat ca.$caAlgo.pub subca.$eeAlgo.pub | - cmkeytool -verify 4<&0 kem.$algo.pub 9>kem.$algo.prv" diff --git a/go/cm/cmd/cmkeytool/main.go b/go/cm/cmd/cmkeytool/main.go index 8481ce9..f21f9fd 100644 --- a/go/cm/cmd/cmkeytool/main.go +++ b/go/cm/cmd/cmkeytool/main.go @@ -17,6 +17,7 @@ package main import ( "bytes" + "encoding/hex" "errors" "flag" "fmt" @@ -24,6 +25,7 @@ import ( "io" "log" "os" + "path" "sort" "strings" "time" @@ -40,21 +42,10 @@ import ( ) const ( - FdPubR = 4 FdPubW = 5 - FdPrvR = 8 FdPrvW = 9 ) -func mustReadAll(r io.ReadCloser) []byte { - data, err := io.ReadAll(r) - if err != nil { - log.Fatal(err) - } - r.Close() - return data -} - func main() { flag.Usage = usage ku := make(map[string]*struct{}) @@ -85,9 +76,13 @@ func main() { "Lifetime of the certification, days") algo := flag.String("algo", ed25519blake2b.Ed25519BLAKE2b, "Public key algorithm") - verify := flag.Bool("verify", false, "Verify provided -pub with -ca-pub") + certify := flag.Bool("c", false, "Certify public key") + verify := flag.Bool("v", false, "Verify provided -pub with -ca-pub") + prvsDir := flag.String("prvs", os.Getenv("CM_PRVS"), + "Path to directory with private keys") + pubsDirPth := flag.String("pubs", os.Getenv("CM_PUBS"), + "Path to directory with public keys") doList := flag.Bool("list", false, "List available algorithms") - flag.Parse() log.SetFlags(log.Lshortfile) @@ -107,58 +102,77 @@ func main() { return } - fdPubR := os.NewFile(FdPubR, "pub-in") fdPubW := os.NewFile(FdPubW, "pub-out") - fdPrvR := os.NewFile(FdPrvR, "prv-in") fdPrvW := os.NewFile(FdPrvW, "prv-out") - var doCertify bool - if len(sub) == 0 && !*verify { - doCertify = true - } - + pubsDir := sign.PubStorageDir{Dir: *pubsDirPth} + var caPrv sign.Iface + var caPub *sign.Signed var err error - var since time.Time - if *sinceRaw == "" { - since = time.Now().UTC().Truncate(time.Second) - } else { - since, err = time.Parse(time.DateTime, *sinceRaw) + if *certify { + if (len(sub) > 0) || *verify { + log.Fatal("-c can not be used with -sub or -v") + } + if flag.NArg() != 2 { + log.Fatal("must specify PUB and CA.PUB") + } + var data []byte + data, err = os.ReadFile(flag.Arg(1)) if err != nil { - log.Fatalln("while parsing -since:", err) + log.Fatal(err) + } + caPub, _, err = sign.PubParse(data) + if err != nil { + log.Fatal(err) + } + data, err = os.ReadFile(path.Join( + *prvsDir, strings.ToUpper(hex.EncodeToString(caPub.PubData().Id)), + )) + if err != nil { + log.Fatal(err) + } + caPrv, _, err = sign.PrvParse(data) + if err != nil { + log.Fatal(err) } } - till := since.Add(time.Duration(*lifetime) * 24 * time.Hour) - - var caPrv sign.Iface - var caPubs []*sign.Signed - if doCertify || *verify { - data := mustReadAll(fdPubR) - for len(data) > 0 { - var signed *sign.Signed - signed, data, err = sign.PubParse(data) - if err != nil { - log.Fatal(err) - } - caPubs = append(caPubs, signed) + if *verify { + if (len(sub) > 0) || *certify { + log.Fatal("-v can not be used with -sub or -c") + } + if flag.NArg() != 1 { + log.Fatal("must specify PUB") } } - if doCertify { - caPrv, _, err = sign.PrvParse(mustReadAll(fdPrvR)) + var signed *sign.Signed + if *certify || *verify { + var data []byte + data, err = os.ReadFile(flag.Arg(0)) + if err != nil { + log.Fatal(err) + } + signed, _, err = sign.PubParse(data) if err != nil { log.Fatal(err) } } + if !*certify && !*verify && len(sub) == 0 { + log.Fatal("must specify one of -v, -c, -sub") + } - var signed *sign.Signed - if doCertify || *verify { - signed, _, err = sign.PubParse(mustReadAll(os.Stdin)) + var since time.Time + if *sinceRaw == "" { + since = time.Now().UTC().Truncate(time.Second) + } else { + since, err = time.Parse(time.DateTime, *sinceRaw) if err != nil { - log.Fatal(err) + log.Fatalln("while parsing -since:", err) } } + till := since.Add(time.Duration(*lifetime) * 24 * time.Hour) if *verify { - err = signed.CertificationVerify(caPubs, time.Now().UTC()) + err = signed.CertificationVerify(&pubsDir, time.Now().UTC()) if err != nil { log.Fatal(err) } @@ -167,7 +181,7 @@ func main() { var prvRaw []byte var pubData map[string]any - if doCertify { + if *certify { pubData = (*signed.Data).(map[string]any) } else { var pub []byte @@ -188,18 +202,7 @@ func main() { if err != nil { log.Fatal(err) } - { - var buf bytes.Buffer - if _, err = keks.Encode(&buf, sign.PrvMagic, nil); err != nil { - log.Fatal(err) - } - if _, err = keks.Encode(&buf, cm.AV{A: *algo, V: prvRaw}, nil); err != nil { - log.Fatal(err) - } - if _, err = io.Copy(fdPrvW, &buf); err != nil { - log.Fatal(err) - } - } + var pubId []byte { pubData = map[string]any{ "sub": sub, @@ -220,7 +223,8 @@ func main() { if err != nil { log.Fatal(err) } - pubData["id"] = hasher.Sum(nil) + pubId = hasher.Sum(nil) + pubData["id"] = pubId if err != nil { log.Fatal(err) } @@ -228,6 +232,22 @@ func main() { if len(ku) > 0 { pubData["ku"] = ku } + { + var buf bytes.Buffer + if _, err = keks.Encode(&buf, sign.PrvMagic, nil); err != nil { + log.Fatal(err) + } + if _, err = keks.Encode( + &buf, + sign.Prv{A: *algo, V: prvRaw, PubId: pubId}, + nil, + ); err != nil { + log.Fatal(err) + } + if _, err = io.Copy(fdPrvW, &buf); err != nil { + log.Fatal(err) + } + } } { @@ -235,10 +255,8 @@ func main() { signed = &sign.Signed{TBS: sign.SDTBS{T: "pub"}, Data: &pubLoadAny} } - if doCertify { - if err = signed.CertifyWith( - caPubs[0].PubData(), caPrv, since, till, - ); err != nil { + if *certify { + if err = signed.CertifyWith(caPub.PubData(), caPrv, since, till); err != nil { log.Fatal(err) } } @@ -251,7 +269,13 @@ func main() { if _, err = keks.Encode(&buf, signed, nil); err != nil { log.Fatal(err) } - if _, err = io.Copy(fdPubW, &buf); err != nil { + var dst io.Writer + if *certify { + dst = os.Stdout + } else { + dst = fdPubW + } + if _, err = io.Copy(dst, &buf); err != nil { log.Fatal(err) } } diff --git a/go/cm/cmd/cmkeytool/usage.go b/go/cm/cmd/cmkeytool/usage.go index 22a7bde..1237d3b 100644 --- a/go/cm/cmd/cmkeytool/usage.go +++ b/go/cm/cmd/cmkeytool/usage.go @@ -26,9 +26,14 @@ func usage() { Generate public key load: cmkeytool -sub K=V [-sub K=V ...] [-algo ALGO] [-ku KU ...] 5>PUB 9>PRV Certify public key: - cmkeytool [-lifetime DAYS] [-since DATE] 4PUB.certified + cmkeytool -c [-lifetime DAYS] [-since DATE] [-prvs dir] PUB CA.PUB >PUB.cer Verify certification: - cmkeytool -verify 4sign.$keyalgo.pub 9>sign.$keyalgo.prv" +ln -s ../sign.$keyalgo.prv prvs/$(kekspp -v -p /pub-id /dev/null encTo=$(dd if=/dev/urandom bs=32 count=1 2>/dev/null | xxd -c 0 -p) badEncTo=$(dd if=/dev/urandom bs=32 count=1 2>/dev/null | xxd -c 0 -p) @@ -21,36 +25,37 @@ badEncTo="-encrypted-to $badEncTo" for merkle in "" "-merkle" ; do +unset CM_PUBS algo=${keyalgo}${merkle} test_expect_success "$algo: signing" " - cmsigtool $merkle -t $typ $encTo \ - 4sign.$algo.sig" +test_expect_success "$algo: verifying, no key" " + ! cmsigtool -v -t $typ sign.data.got" +ln -fs ../sign.$keyalgo.pub pubs/$(kekspp -v -p /pub-id sign.data.got" + cmsigtool -v -t $typ sign.data.got" test_expect_success "$algo: comparing" \ "test_cmp sign.$keyalgo.data sign.data.got" test_expect_success "$algo: differing type" " - ! cmsigtool -v 4/dev/null" + ! cmsigtool -v /dev/null" test_expect_success "$algo: good encTo" " - ! cmsigtool -v $encTo 4/dev/null" + ! cmsigtool -v $encTo /dev/null" test_expect_success "$algo: bad encTo" " - ! cmsigtool -v $badEncTo 4/dev/null" + ! cmsigtool -v $badEncTo /dev/null" test_expect_success "$algo: detached signing" " cmsigtool -d $merkle -t $typ \ - 4sign.$algo.detached.sig" + sign.$keyalgo.pub sign.$algo.detached.sig" test_expect_success "$algo: detached verifying" " - cat sign.$algo.detached.sig sign.$keyalgo.data | - cmsigtool -d -v -t $typ 4/dev/null" + ! cmsigtool -d -v /dev/null" test_expect_success "$algo: good encTo" "! cmsigtool -d \ - -v $encTo 4/dev/null" + -v $encTo /dev/null" test_expect_success "$algo: bad encTo" "! cmsigtool -d \ - -v $badEncTo 4/dev/null" + -v $badEncTo /dev/null" done diff --git a/go/cm/cmd/cmsigtool/main.go b/go/cm/cmd/cmsigtool/main.go index c0ef13d..23f7579 100644 --- a/go/cm/cmd/cmsigtool/main.go +++ b/go/cm/cmd/cmsigtool/main.go @@ -24,6 +24,8 @@ import ( "io" "log" "os" + "path" + "strings" "time" "go.cypherpunks.su/keks" @@ -35,26 +37,17 @@ import ( "go.cypherpunks.su/keks/types" ) -const ( - FdPubR = 4 - FdPrvR = 8 -) - const BlobChunkLen = 128 * 1024 -func mustReadAll(r io.Reader) []byte { - data, err := io.ReadAll(r) - if err != nil { - log.Fatal(err) - } - return data -} - func main() { log.SetFlags(log.Lshortfile) flag.Usage = usage typ := flag.String("t", "data", "Set/check the load type") verify := flag.Bool("v", false, "Do verification") + prvDir := flag.String("prvs", os.Getenv("CM_PRVS"), + "Path to directory with private keys") + pubDirPth := flag.String("pubs", os.Getenv("CM_PUBS"), + "Path to directory with public keys") var encryptedTo [][]byte flag.Func("encrypted-to", "Set/check encrypted-to, hex", func(v string) error { to, err := hex.DecodeString(v) @@ -69,22 +62,39 @@ func main() { doMerkle := flag.Bool("merkle", false, "Use Merkle-tree based hasher") flag.Parse() - fdPubR := os.NewFile(FdPubR, "pub-in") - fdPrvR := os.NewFile(FdPrvR, "prv-in") - - var pubs []*sign.PubData + pubsDir := sign.PubStorageDir{Dir: *pubDirPth} + var pub *sign.PubData + var signer sign.Iface var err error - { - data := mustReadAll(fdPubR) - for len(data) > 0 { - var signed *sign.Signed - signed, data, err = sign.PubParse(data) - if err != nil { - log.Fatal(err) - } - pubs = append(pubs, signed.PubData()) + if !*verify { + if flag.NArg() != 1 { + log.Fatal("no PUB specified") + } + var data []byte + data, err = os.ReadFile(flag.Arg(0)) + if err != nil { + log.Fatal(err) + } + var signed *sign.Signed + signed, _, err = sign.PubParse(data) + if err != nil { + log.Fatal(err) + } + pub = signed.PubData() + data, err = os.ReadFile(path.Join( + *prvDir, strings.ToUpper(hex.EncodeToString(pub.Id)), + )) + if err != nil { + log.Fatal(err) + } + data, err = cmballoon.PossibleInteractiveDecrypt(data) + if err != nil { + log.Fatal(err) + } + signer, _, err = sign.PrvParse(data) + if err != nil { + log.Fatal(err) } - fdPubR.Close() } stdin := bufio.NewReaderSize(os.Stdin, BlobChunkLen) @@ -160,74 +170,59 @@ func main() { } for _, sig := range signed.Sigs { sid := sig.TBS["sid"].([]byte) - var signerFound bool - for _, pub := range pubs { - if !bytes.Equal(sid, pub.Id) { - continue - } - signerFound = true - var sigTBS *sign.SigTBS - sigTBS, err = sig.TBSGet() + { + var p *sign.Signed + p, err = pubsDir.Get(sid) if err != nil { log.Fatal(err) } - if len(encryptedTo) > 0 { - if len(sigTBS.EncryptedTo) == 0 { - log.Fatalln(hex.EncodeToString(sid), "missing encrypted-to") - } - found := false - for _, their := range sigTBS.EncryptedTo { - for _, our := range encryptedTo { - if bytes.Equal(our, their) { - found = true - break - } + if p == nil { + log.Fatalln(hex.EncodeToString(sid), "can not find signer") + } + pub = p.PubData() + } + var sigTBS *sign.SigTBS + sigTBS, err = sig.TBSGet() + if err != nil { + log.Fatal(err) + } + if len(encryptedTo) > 0 { + if len(sigTBS.EncryptedTo) == 0 { + log.Fatalln(hex.EncodeToString(sid), "missing encrypted-to") + } + found := false + for _, their := range sigTBS.EncryptedTo { + for _, our := range encryptedTo { + if bytes.Equal(our, their) { + found = true + break } } - if !found { - log.Fatalln( - hex.EncodeToString(sid), - "corresponding encrypted-to not found") - } } - var hasher hash.Hash - if prehash.T == "" { - hasher = cmhash.ByName(sig.Sign.A) - if _, err = io.Copy(hasher, stdin); err != nil { - log.Fatal(hex.EncodeToString(sid), err) - } - } else { - var found bool - hasher, found = hashers[sig.Sign.A] - if !found { - log.Fatalln(hex.EncodeToString(sid), "no hasher in prehash") - } + if !found { + log.Fatalln( + hex.EncodeToString(sid), + "corresponding encrypted-to not found") + } + } + var hasher hash.Hash + if prehash.T == "" { + hasher = cmhash.ByName(sig.Sign.A) + if _, err = io.Copy(hasher, stdin); err != nil { + log.Fatal(hex.EncodeToString(sid), err) } - if err = signed.CertificationCheckSignatureFrom( - pub, &hasher, - ); err != nil { - log.Fatalln(hex.EncodeToString(sid), err) + } else { + var found bool + hasher, found = hashers[sig.Sign.A] + if !found { + log.Fatalln(hex.EncodeToString(sid), "no hasher in prehash") } - break } - if !signerFound { - log.Fatalln(hex.EncodeToString(sid), "can not find signer") + if err = signed.CertificationCheckSignatureFrom(pub, &hasher); err != nil { + log.Fatalln(hex.EncodeToString(sid), err) } } } else { - var signer sign.Iface - { - prvRaw := mustReadAll(fdPrvR) - fdPrvR.Close() - prvRaw, err = cmballoon.PossibleInteractiveDecrypt(prvRaw) - if err != nil { - log.Fatal(err) - } - signer, _, err = sign.PrvParse(prvRaw) - } - if err != nil { - log.Fatal(err) - } if *doMerkle { err = signer.SetMode(mode.Merkle) } else { @@ -236,7 +231,6 @@ func main() { if err != nil { log.Fatal(err) } - if _, err = keks.Encode(os.Stdout, sign.SignedMagic, nil); err != nil { log.Fatal(err) } @@ -276,7 +270,7 @@ func main() { if len(encryptedTo) > 0 { tbs["encrypted-to"] = encryptedTo } - if err = signed.SignWith(pubs[0], signer, tbs); err != nil { + if err = signed.SignWith(pub, signer, tbs); err != nil { log.Fatal(err) } if _, err = keks.Encode(os.Stdout, signed, nil); err != nil { diff --git a/go/cm/cmd/cmsigtool/usage.go b/go/cm/cmd/cmsigtool/usage.go index 1903226..a0c89d7 100644 --- a/go/cm/cmd/cmsigtool/usage.go +++ b/go/cm/cmd/cmsigtool/usage.go @@ -23,14 +23,19 @@ import ( func usage() { fmt.Fprintf(os.Stderr, `Usage: - cmsigtool [-t TYPE] 4DATA.signed - cmsigtool -v [-t TYPE] 4DATA - cmsigtool -d [-t TYPE] 4DATA.signature - cmsigtool -d -v [-t TYPE] 4DATA.signed + cmsigtool -d [-t TYPE] [-prvs dir] PUB DATA.signature + cmsigtool -v [-t TYPE] [-pubs dir] DATA + cmsigtool -d -v [-t TYPE] [-pubs dir] <<(cat DATA.signature DATA) DATA.signed holds completely encapsulated DATA. -d(etached) mode keeps DATA completely separate. +PUB is a path to public key files. +-prvs/-pubs directory must contain key files with uppercase hexadecimal +corresponding public key's id used as the filename. They can be set by +CM_PRVS and CM_PUBS environment variables. + `) flag.PrintDefaults() } diff --git a/go/cm/enc/enc.go b/go/cm/enc/enc.go index effe612..cdf2326 100644 --- a/go/cm/enc/enc.go +++ b/go/cm/enc/enc.go @@ -3,8 +3,8 @@ package encrypted import "github.com/google/uuid" type Encrypted struct { - DEM DEM `keks:"dem"` - KEM []KEM `keks:"kem"` - Payload []byte `keks:"payload,omitempty"` - Id uuid.UUID `keks:"id"` + DEM DEM `keks:"dem"` + KEM []KEM `keks:"kem"` + Payload []byte `keks:"payload,omitempty"` + Id *uuid.UUID `keks:"id,omitempty"` } diff --git a/go/cm/sign/.gitignore b/go/cm/sign/.gitignore index a530887..cf6a0c0 100644 --- a/go/cm/sign/.gitignore +++ b/go/cm/sign/.gitignore @@ -1,2 +1,3 @@ +/prv.schema.keks /pub.schema.keks /signed.schema.keks diff --git a/go/cm/sign/prv.go b/go/cm/sign/prv.go index 6b7ee06..baf0f75 100644 --- a/go/cm/sign/prv.go +++ b/go/cm/sign/prv.go @@ -29,7 +29,13 @@ import ( const PrvMagic = keks.Magic("cm/prv") -// Parse private key contained in AV KEKS-encoded structure. +type Prv struct { + A string `keks:"a"` + V []byte `keks:"v"` + PubId []byte `keks:"pub-id,omitempty"` +} + +// Parse private key contained in KEKS-encoded structure. func PrvParse(data []byte) (prv Iface, pub []byte, err error) { { var magic keks.Magic @@ -46,7 +52,7 @@ func PrvParse(data []byte) (prv Iface, pub []byte, err error) { if err != nil { return } - err = schema.Check("av", PubSchemas, v) + err = schema.Check("prv", PrvSchemas, v) if err != nil { return } diff --git a/go/cm/sign/pub.go b/go/cm/sign/pub.go index fedc13d..5e8cb01 100644 --- a/go/cm/sign/pub.go +++ b/go/cm/sign/pub.go @@ -17,6 +17,7 @@ package sign import ( "bytes" + "encoding/hex" "errors" "fmt" "hash" @@ -270,9 +271,9 @@ func (signed *Signed) PubData() *PubData { return &pubData } -// Verify signed Signed PubData certification against pubs chain of +// Verify signed Signed PubData certification against pubs storage of // public keys at specified point of time t. -func (signed *Signed) CertificationVerify(pubs []*Signed, t time.Time) (err error) { +func (signed *Signed) CertificationVerify(pubs PubStorage, t time.Time) (err error) { if len(signed.Sigs) == 0 { return errors.New("no sigs") } @@ -297,22 +298,21 @@ func (signed *Signed) CertificationVerify(pubs []*Signed, t time.Time) (err erro if bytes.Equal(sid, signed.PubData().Id) { return signed.CertificationCheckSignatureFrom(signed.PubData(), nil) } - type FPR [FPRLen]byte - idToPub := make(map[FPR]*Signed, len(pubs)) - for _, cer := range pubs { - pubData := cer.PubData() - if !pubData.Can(KUSig) || len(pubData.Pub) != 1 { - err = errors.New("pub can not sign") - return - } - idToPub[FPR(pubData.Id)] = cer + var signer *Signed + signer, err = pubs.Get(sid) + if err != nil { + return } - signer := idToPub[FPR(sid)] if signer == nil { - err = fmt.Errorf("no pub found for sid: %v", sigTBS.SID) + err = fmt.Errorf("no pub found for sid: %s", hex.EncodeToString(sid)) + return + } + pubData := signer.PubData() + if !pubData.Can(KUSig) || len(pubData.Pub) != 1 { + err = errors.New("pub can not sign") return } - err = signed.CertificationCheckSignatureFrom(signer.PubData(), nil) + err = signed.CertificationCheckSignatureFrom(pubData, nil) if err != nil { return } diff --git a/go/cm/sign/schema.go b/go/cm/sign/schema.go index 776a5e7..9a06418 100644 --- a/go/cm/sign/schema.go +++ b/go/cm/sign/schema.go @@ -25,11 +25,15 @@ import ( //go:embed signed.schema.keks var SignedSchemasRaw []byte +//go:embed prv.schema.keks +var PrvSchemasRaw []byte + //go:embed pub.schema.keks var PubSchemasRaw []byte var ( SignedSchemas map[string][][]any + PrvSchemas map[string][][]any PubSchemas map[string][][]any ) @@ -45,6 +49,16 @@ func init() { panic(err) } + magic, PrvSchemasRaw = keks.StripMagic(PrvSchemasRaw) + if magic != schema.Magic { + panic("wrong magic in prv.schema.keks") + } + if err := keks.NewDecoderFromBytes( + PrvSchemasRaw, nil, + ).DecodeStruct(&PrvSchemas); err != nil { + panic(err) + } + magic, PubSchemasRaw = keks.StripMagic(PubSchemasRaw) if magic != schema.Magic { panic("wrong magic in pub.schema.keks") diff --git a/go/cm/sign/signed.go b/go/cm/sign/signed.go index 9997d84..e972530 100644 --- a/go/cm/sign/signed.go +++ b/go/cm/sign/signed.go @@ -33,7 +33,8 @@ import ( const SignedMagic = keks.Magic("cm/signed") type SDTBS struct { - T string `keks:"t"` + T string `keks:"t"` + Id *uuid.UUID `keks:"id,omitempty"` } type SigTBS struct { @@ -54,7 +55,7 @@ type Sig struct { } type Signed struct { - TBS SDTBS `keks:"tbs"` + TBS SDTBS `keks:"tbs"` Data *any `keks:"data,omitempty"` Pubs *[]*Signed `keks:"pubs,omitempty"` Sigs []*Sig `keks:"sigs,omitempty"` diff --git a/go/cm/sign/storage.go b/go/cm/sign/storage.go new file mode 100644 index 0000000..d1efcb2 --- /dev/null +++ b/go/cm/sign/storage.go @@ -0,0 +1,48 @@ +// KEKS/CM -- KEKS-encoded cryptographic messages +// 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 sign + +import ( + "encoding/hex" + "errors" + "io/fs" + "os" + "path" + "strings" +) + +type PubStorage interface { + Get(id []byte) (*Signed, error) +} + +type PubStorageDir struct { + Dir string +} + +func (s *PubStorageDir) Get(id []byte) (signed *Signed, err error) { + var data []byte + data, err = os.ReadFile(path.Join( + s.Dir, strings.ToUpper(hex.EncodeToString(id)), + )) + if err != nil { + if errors.Is(err, fs.ErrNotExist) { + return nil, nil + } + return nil, err + } + signed, _, err = PubParse(data) + return signed, err +} diff --git a/go/cm/utils/mk-bin b/go/cm/utils/mk-bin index 52b34dd..d2cebd0 100755 --- a/go/cm/utils/mk-bin +++ b/go/cm/utils/mk-bin @@ -11,6 +11,7 @@ cd "$root/.." [ -d vendor ] && mod_vendor="-mod=vendor" || redo-ifchange \ enc/encrypted.schema.keks \ hash/prehash.schema.keks \ + sign/prv.schema.keks \ sign/pub.schema.keks \ sign/signed.schema.keks mkdir -p bin diff --git a/spec/Integrity b/spec/Integrity index c73d166..b22e18a 100644 --- a/spec/Integrity +++ b/spec/Integrity @@ -12,5 +12,6 @@ Metalink4 file contains its OpenSSH signature. => PUBKEY-CM.pub => PUBKEY-CM.pub.asc + $ ln -s PUBKEY-CM.pub $(kekspp -v -p /data/id 0} {# algorithm identifier} + {field v {bin}} + {field pub-id {with fpr} optional} +} + +schema-include fpr.tcl diff --git a/tcl/schemas/signed.tcl b/tcl/schemas/signed.tcl index 0edbb4a..27eedf4 100644 --- a/tcl/schemas/signed.tcl +++ b/tcl/schemas/signed.tcl @@ -12,6 +12,7 @@ signed { tbs { {field . {map}} {field t {str} >0} {# type of the data we sign} + {field id {hexlet} optional} } sig { -- 2.51.0