From: Sergey Matveev Date: Thu, 5 Jun 2025 09:46:54 +0000 (+0300) Subject: Support of encrypted private signing keys X-Git-Url: http://www.git.cypherpunks.su/?a=commitdiff_plain;h=6258b201cfa515703a234a0ae01fa7f35b4ea95f871fd2ff16afa4216d90fefd;p=keks.git Support of encrypted private signing keys --- diff --git a/go/cm/cmd/cmenctool/main.go b/go/cm/cmd/cmenctool/main.go index 76bc585..0f0022c 100644 --- a/go/cm/cmd/cmenctool/main.go +++ b/go/cm/cmd/cmenctool/main.go @@ -24,7 +24,6 @@ import ( "crypto/sha3" "errors" "flag" - "fmt" "hash" "io" "log" @@ -35,7 +34,6 @@ import ( "go.cypherpunks.su/balloon/v3" "golang.org/x/crypto/blake2b" "golang.org/x/crypto/chacha20poly1305" - "golang.org/x/term" "go.cypherpunks.su/keks" "go.cypherpunks.su/keks/cm" @@ -69,93 +67,33 @@ func blake2bHash() hash.Hash { return h } -func readPasswd(prompt string) (passwd []byte) { - if raw := os.Getenv("CMENCTOOL_PASSPHRASE"); raw != "" { - return []byte(raw) - } - 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())) +func parsePrv(data []byte) (av cm.AV, tail []byte, err error) { + data, err = cmballoon.PossibleInteractiveDecrypt(data) if err != nil { - log.Fatalln(err) + return } - // 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 parsePrv(data []byte) (av cm.AV, tail []byte, err error) { - var magic keks.Magic - magic, data = keks.StripMagic(data) - switch magic { - case sign.PrvMagic: - case cmenc.Magic: - var encrypted cmenc.Encrypted - var v any - { - d := keks.NewDecoderFromBytes(data, nil) - v, err = d.Decode() - if err != nil { - return - } - err = schema.Check("encrypted", cmenc.EncryptedSchemas, v) - if err != nil { - return - } - err = d.UnmarshalStruct(&encrypted) - if err != nil { - return - } - } - if encrypted.DEM.A != chapoly.DEMAlgo { - err = errors.New("unsupported prv encryption DEM") - return - } - if len(encrypted.KEM) != 1 || - encrypted.KEM[0].A != cmballoon.BalloonBLAKE2bHKDF || - len(encrypted.Payload) == 0 { - err = errors.New("wrong prv encryption KEM") - return - } - v = v.(map[string]any)["kem"].([]any)[0] - err = schema.Check("kem-balloon-blake2b-hkdf", cmenc.EncryptedSchemas, v) - if err != nil { + { + var magic keks.Magic + magic, data = keks.StripMagic(data) + if magic != sign.PrvMagic { + err = errors.New("wrong magic") return } - - passwd := readPasswd("Passphrase for private key:") - var cek []byte - cek, err = cmballoon.Decapsulate(encrypted.KEM[0], encrypted.Id[:], passwd) - if err != nil { + } + sign.PrvParse(data) + d := keks.NewDecoderFromBytes(data, &keks.DecodeOpts{MaxStrLen: 1 << 16}) + { + var v any + if v, err = d.Decode(); err != nil { return } - var buf bytes.Buffer - _, err = chapoly.Open(&buf, bytes.NewReader(encrypted.Payload), cek, 1) - if err != nil { + if err = schema.Check("av", sign.PubSchemas, v); err != nil { return } - data = buf.Bytes() - magic, data = keks.StripMagic(data) - if magic == sign.PrvMagic { - break - } - fallthrough - default: - err = errors.New("wrong magic") + } + if err = d.UnmarshalStruct(&av); err != nil { return } - sign.PrvParse(data) - d := keks.NewDecoderFromBytes(data, &keks.DecodeOpts{MaxStrLen: 1 << 16}) - err = d.DecodeStruct(&av) tail = d.B return } @@ -275,7 +213,11 @@ func main() { log.Println(kemIdx, kem.A, "skipping because no -passwd") continue } - passwd := readPasswd("Passphrase for " + strconv.Itoa(kemIdx) + " KEM:") + var passwd []byte + passwd, err = cmenc.ReadPasswd("Passphrase for " + strconv.Itoa(kemIdx) + " KEM:") + if err != nil { + log.Fatal(err) + } cek, err = cmballoon.Decapsulate(kem, encrypted.Id[:], passwd) if err != nil { log.Print(err) @@ -542,9 +484,17 @@ func main() { cek = make([]byte, chapoly.CEKLen) rand.Read(cek) if *passphrase { - passwd := readPasswd("Passphrase:") + var passwd []byte + passwd, err = cmenc.ReadPasswd("Passphrase:") + if err != nil { + log.Fatal(err) + } { - confirm := readPasswd("Confirm:") + var confirm []byte + confirm, err = cmenc.ReadPasswd("Confirm:") + if err != nil { + log.Fatal(err) + } if !bytes.Equal(passwd, confirm) { log.Fatal("passphrases do not match") } diff --git a/go/cm/cmd/cmenctool/passphrase.t b/go/cm/cmd/cmenctool/passphrase.t index 9ef833f..d1b27ca 100755 --- a/go/cm/cmd/cmenctool/passphrase.t +++ b/go/cm/cmd/cmenctool/passphrase.t @@ -6,7 +6,7 @@ test_description="Check passphrase encryption" TMPDIR=${TMPDIR:-/tmp} dd if=/dev/urandom of=$TMPDIR/enc.data bs=300K count=1 2>/dev/null -export CMENCTOOL_PASSPHRASE=$(dd if=/dev/urandom bs=32 count=1 2>/dev/null | xxd -p) +export CM_PASSPHRASE=$(dd if=/dev/urandom bs=32 count=1 2>/dev/null | xxd -p) balloonparams="-balloon-s 123 -balloon-t 2" test_expect_success "encrypting" "cmenctool $balloonparams -p \ <$TMPDIR/enc.data >$TMPDIR/enc.enc" diff --git a/go/cm/cmd/cmenctool/prv-encrypted.t b/go/cm/cmd/cmenctool/prv-encrypted.t index 48ad4e1..e14b25b 100755 --- a/go/cm/cmd/cmenctool/prv-encrypted.t +++ b/go/cm/cmd/cmenctool/prv-encrypted.t @@ -7,7 +7,7 @@ TMPDIR=${TMPDIR:-/tmp} cmkeytool -algo sntrup761-x25519 -ku kem -sub A=KEY 5>$TMPDIR/enc.pub 9>$TMPDIR/enc.prv dd if=/dev/urandom of=$TMPDIR/enc.data bs=12K count=1 2>/dev/null -export CMENCTOOL_PASSPHRASE=$(dd if=/dev/urandom bs=32 count=1 2>/dev/null | xxd -p) +export CM_PASSPHRASE=$(dd if=/dev/urandom bs=32 count=1 2>/dev/null | xxd -p) balloonparams="-balloon-s 123 -balloon-t 2" test_expect_success "key encrypting" "cmenctool -p -embed $balloonparams \ <$TMPDIR/enc.prv >$TMPDIR/enc.prv.enc" diff --git a/go/cm/cmd/cmenctool/pub.t b/go/cm/cmd/cmenctool/pub.t index c1bc7b8..322a30c 100755 --- a/go/cm/cmd/cmenctool/pub.t +++ b/go/cm/cmd/cmenctool/pub.t @@ -39,7 +39,7 @@ test_expect_success "$algo1: decrypting" "cmenctool -d \ test_expect_success "$algo1: comparing" \ "test_cmp $TMPDIR/enc.data $TMPDIR/enc.data.got" -export CMENCTOOL_PASSPHRASE=$(dd if=/dev/urandom bs=32 count=1 2>/dev/null | xxd -p) +export CM_PASSPHRASE=$(dd if=/dev/urandom bs=32 count=1 2>/dev/null | xxd -p) test_expect_success "encrypting also with passphrase" " cat $TMPDIR/enc.$algo0.pub $TMPDIR/enc.$algo1.pub | cmenctool $balloonparams -p 4<&0 <$TMPDIR/enc.data >$TMPDIR/enc.enc" diff --git a/go/cm/cmd/cmsigtool/main.go b/go/cm/cmd/cmsigtool/main.go index e57c650..81a48b8 100644 --- a/go/cm/cmd/cmsigtool/main.go +++ b/go/cm/cmd/cmsigtool/main.go @@ -27,6 +27,7 @@ import ( "time" "go.cypherpunks.su/keks" + cmballoon "go.cypherpunks.su/keks/cm/enc/balloon" cmhash "go.cypherpunks.su/keks/cm/hash" "go.cypherpunks.su/keks/cm/sign" "go.cypherpunks.su/keks/cm/sign/mode" @@ -215,8 +216,15 @@ func main() { } } else { var signer sign.Iface - signer, _, err = sign.PrvParse(mustReadAll(fdPrvR)) - fdPrvR.Close() + { + 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) } diff --git a/go/cm/enc/balloon/possible.go b/go/cm/enc/balloon/possible.go new file mode 100644 index 0000000..0c9f638 --- /dev/null +++ b/go/cm/enc/balloon/possible.go @@ -0,0 +1,81 @@ +// GoKEKS/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 balloon + +import ( + "bytes" + "errors" + + "go.cypherpunks.su/keks" + cmenc "go.cypherpunks.su/keks/cm/enc" + "go.cypherpunks.su/keks/cm/enc/chapoly" + "go.cypherpunks.su/keks/schema" +) + +// Possibly interactively decrypt passphrase-encrypted data. +func PossibleInteractiveDecrypt(in []byte) (out []byte, err error) { + var magic keks.Magic + magic, out = keks.StripMagic(in) + if magic != cmenc.Magic { + return in, nil + } + var encrypted cmenc.Encrypted + var v any + { + d := keks.NewDecoderFromBytes(out, nil) + v, err = d.Decode() + if err != nil { + return + } + err = schema.Check("encrypted", cmenc.EncryptedSchemas, v) + if err != nil { + return + } + err = d.UnmarshalStruct(&encrypted) + if err != nil { + return + } + } + if encrypted.DEM.A != chapoly.DEMAlgo { + err = errors.New("unsupported encryption DEM") + return + } + if len(encrypted.KEM) != 1 || + encrypted.KEM[0].A != BalloonBLAKE2bHKDF || + len(encrypted.Payload) == 0 { + err = errors.New("wrong encryption KEM") + return + } + v = v.(map[string]any)["kem"].([]any)[0] + err = schema.Check("kem-balloon-blake2b-hkdf", cmenc.EncryptedSchemas, v) + if err != nil { + return + } + var passwd []byte + passwd, err = cmenc.ReadPasswd("Passphrase:") + if err != nil { + return + } + var cek []byte + cek, err = Decapsulate(encrypted.KEM[0], encrypted.Id[:], passwd) + if err != nil { + return + } + var buf bytes.Buffer + _, err = chapoly.Open(&buf, bytes.NewReader(encrypted.Payload), cek, 1) + out = buf.Bytes() + return +} diff --git a/go/cm/enc/passwd.go b/go/cm/enc/passwd.go new file mode 100644 index 0000000..167287e --- /dev/null +++ b/go/cm/enc/passwd.go @@ -0,0 +1,48 @@ +// GoKEKS/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 encrypted + +import ( + "fmt" + "os" + + "golang.org/x/term" +) + +func ReadPasswd(prompt string) (passwd []byte, err error) { + if raw := os.Getenv("CM_PASSPHRASE"); raw != "" { + return []byte(raw), nil + } + var tty *os.File + tty, err = os.OpenFile("/dev/tty", os.O_RDWR, 0) + if err != nil { + return + } + defer tty.Close() + tty.WriteString(prompt) + passwd, err = term.ReadPassword(int(tty.Fd())) + if err != nil { + return + } + // 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 +}