const (
Ed25519BLAKE2b = ed25519blake2b.Ed25519BLAKE2b
+ Ed25519PhBLAKE2b = ed25519blake2b.Ed25519PhBLAKE2b
GOST3410256A = gost.GOST3410256A
GOST3410512C = gost.GOST3410512C
SNTRUP4591761X25519 = sntrup4591761x25519.SNTRUP4591761X25519
package pki
import (
- "crypto"
+ "bytes"
"errors"
"fmt"
"hash"
"go.cypherpunks.su/keks"
ed25519blake2b "go.cypherpunks.su/keks/pki/ed25519-blake2b"
"go.cypherpunks.su/keks/pki/gost"
+ "go.cypherpunks.su/keks/pki/sign"
)
const (
CerMagic = keks.Magic("pki/cer")
)
+var (
+ ErrSigInvalid = errors.New("signature is invalid")
+ ErrBadSigAlgo = errors.New("bad signature algo")
+)
+
// Public key.
type Pub struct {
A string `keks:"a"`
// generated UUIDv7. since and till times must not have nanoseconds part.
func (signed *Signed) CerIssueWith(
parent *CerLoad,
- prv crypto.Signer,
+ prv sign.Iface,
since, till time.Time,
) error {
exp := []time.Time{since, till}
return signed.SignWith(parent, prv, SigTBS{CID: &cid, Exp: &exp})
}
-var ErrSigInvalid = errors.New("signature is invalid")
-
// Verify signature of signed data. ErrSigInvalid will be returned in
// case of invalid signature.
-func (cer *CerLoad) CheckSignature(signed, signature []byte) (err error) {
+func (cer *CerLoad) CheckSignature(algo string, signed, signature []byte) (err error) {
if !cer.Can(KUSig) || len(cer.Pub) != 1 {
err = errors.New("cer can not sign")
return
var valid bool
switch pub.A {
case Ed25519BLAKE2b:
+ if algo != Ed25519BLAKE2b {
+ return ErrBadSigAlgo
+ }
valid, err = ed25519blake2b.Verify(pub.V, signed, signature)
if !valid {
err = ErrSigInvalid
}
case GOST3410256A, GOST3410512C:
+ if algo != pub.A {
+ return ErrBadSigAlgo
+ }
valid, err = gost.Verify(pub.A, pub.V, signed, signature)
if !valid {
err = ErrSigInvalid
// Verify signature of signed data, by providing prehashed data.
// ErrSigInvalid will be returned in case of invalid signature.
-func (cer *CerLoad) CheckSignatureDigest(digest, signature []byte) (err error) {
+func (cer *CerLoad) CheckSignaturePrehash(
+ algo string,
+ prehash, signature []byte,
+) (err error) {
if !cer.Can(KUSig) || len(cer.Pub) != 1 {
err = errors.New("cer can not sign")
return
var valid bool
switch pub.A {
case Ed25519BLAKE2b:
- valid, err = ed25519blake2b.VerifyDigest(pub.V, digest, signature)
+ if algo != Ed25519PhBLAKE2b {
+ return ErrBadSigAlgo
+ }
+ valid, err = ed25519blake2b.VerifyPrehash(pub.V, prehash, signature)
if !valid {
err = ErrSigInvalid
}
case GOST3410256A, GOST3410512C:
- valid, err = gost.VerifyDigest(pub.A, pub.V, digest, signature)
+ if algo != pub.A {
+ return ErrBadSigAlgo
+ }
+ valid, err = gost.VerifyPrehash(pub.A, pub.V, prehash, signature)
if !valid {
err = ErrSigInvalid
}
}
// Verify Signed CerLoad certificate's signature with provided parent.
-// If hasher is specified, then prehashed signature mode is used.
+// If prehasher is specified, then prehashed signature mode is used.
// Currently only single signature can be verified.
func (signed *Signed) CerCheckSignatureFrom(
parent *CerLoad,
- hasher *hash.Hash,
+ prehasher *hash.Hash,
) (err error) {
if len(signed.Sigs) != 1 {
err = errors.New("can verify only single signature")
err = errors.New("sid != parent pub id")
return
}
- var tbs SignedTBS
- if hasher == nil {
- tbs = SignedTBS{T: signed.Load.T, V: signed.Load.V, TBS: sig.TBS}
- var buf []byte
- buf, err = keks.EncodeBuf(tbs, nil)
- if err != nil {
+ if prehasher == nil {
+ var tbs bytes.Buffer
+ if _, err = keks.Encode(&tbs, signed.Load, nil); err != nil {
+ return
+ }
+ if _, err = keks.Encode(&tbs, sig.TBS, nil); err != nil {
return
}
- return parent.CheckSignature(buf, sig.Sign.V)
+ return parent.CheckSignature(sig.Sign.A, tbs.Bytes(), sig.Sign.V)
} else {
- tbs = SignedTBS{T: signed.Load.T, TBS: sig.TBS}
- _, err = keks.Encode(*hasher, tbs, nil)
- if err != nil {
+ if _, err = keks.Encode(*prehasher, signed.Load, nil); err != nil {
+ return
+ }
+ if _, err = keks.Encode(*prehasher, sig.TBS, nil); err != nil {
return
}
- return parent.CheckSignatureDigest((*hasher).Sum(nil), sig.Sign.V)
+ return parent.CheckSignaturePrehash(sig.Sign.A, (*prehasher).Sum(nil), sig.Sign.V)
}
}
import (
"bytes"
- "crypto"
"errors"
"flag"
"fmt"
"go.cypherpunks.su/keks/pki"
ed25519blake2b "go.cypherpunks.su/keks/pki/ed25519-blake2b"
"go.cypherpunks.su/keks/pki/gost"
+ "go.cypherpunks.su/keks/pki/sign"
sntrup4591761x25519 "go.cypherpunks.su/keks/pki/sntrup4591761-x25519"
"go.cypherpunks.su/keks/pki/utils"
)
}
till := since.Add(time.Duration(*lifetime) * 24 * time.Hour)
- var caPrv crypto.Signer
+ var caPrv sign.Iface
var caCers []*pki.Signed
for _, issuingCer := range issuingCers {
var signed *pki.Signed
"io"
"os"
+ "go.cypherpunks.su/keks"
"golang.org/x/crypto/chacha20poly1305"
)
out := make([]byte, len(ChaPolyPad)+ChaPolyChunkLen+ciph.Overhead())
br := bufio.NewReaderSize(os.Stdin, ChaPolyChunkLen)
bw := bufio.NewWriterSize(os.Stdout, len(out))
+ pr, pw := io.Pipe()
+ blobErr := make(chan error)
+ go func() {
+ _, e := keks.BlobEncode(bw, int64(len(out)), pr)
+ blobErr <- e
+ }()
nonce := make([]byte, ciph.NonceSize())
var n int
var eof bool
if n != ChaPolyChunkLen {
nonce[len(nonce)-1] = 0x01
}
- _, err = bw.Write(ciph.Seal(out[:0], nonce, in[:len(ChaPolyPad)+n], nil))
+ _, err = pw.Write(ciph.Seal(out[:0], nonce, in[:len(ChaPolyPad)+n], nil))
if err != nil {
return err
}
}
+ if err = pw.Close(); err != nil {
+ return err
+ }
+ if err = <-blobErr; err != nil {
+ return err
+ }
return bw.Flush()
}
return err
}
in := make([]byte, len(ChaPolyPad)+ChaPolyChunkLen+ciph.Overhead())
+ pr, pw := io.Pipe()
+ blobErr := make(chan error)
+ go func() {
+ d, e := keks.NewBlobDecoder(
+ bufio.NewReaderSize(os.Stdin, len(in)),
+ int64(len(in)),
+ )
+ defer func() {
+ pw.Close()
+ blobErr <- e
+ }()
+ if e != nil {
+ return
+ }
+ var chunk []byte
+ for {
+ chunk, e = d.Next()
+ if e != nil {
+ if e == io.EOF {
+ e = nil
+ break
+ }
+ return
+ }
+ if _, e = pw.Write(chunk); e != nil {
+ return
+ }
+ }
+ }()
out := make([]byte, len(ChaPolyPad)+ChaPolyChunkLen)
var n int
var eof bool
var chunk []byte
- br := bufio.NewReaderSize(os.Stdin, len(in))
bw := bufio.NewWriterSize(os.Stdout, ChaPolyChunkLen)
nonce := make([]byte, ciph.NonceSize())
for !eof {
- n, err = io.ReadFull(br, in)
+ n, err = io.ReadFull(pr, in)
if err != nil {
if err != io.ErrUnexpectedEOF {
return err
return err
}
}
+ if err = <-blobErr; err != nil {
+ return err
+ }
return bw.Flush()
}
"crypto/rand"
"errors"
"flag"
+ "fmt"
"hash"
"io"
"log"
"golang.org/x/crypto/blake2b"
"golang.org/x/crypto/chacha20poly1305"
"golang.org/x/crypto/hkdf"
+ "golang.org/x/term"
"go.cypherpunks.su/keks"
"go.cypherpunks.su/keks/pki"
return h
}
+func readPasswd(prompt string) (passwd []byte) {
+ tty, err := os.OpenFile("/dev/tty", os.O_RDWR, 0)
+ if err != nil {
+ log.Fatal(err)
+ }
+ defer tty.Close()
+ tty.WriteString(prompt)
+ passwd, err = term.ReadPassword(int(tty.Fd()))
+ if err != nil {
+ log.Fatalln(err)
+ }
+ // taken from age/cmd/age/tui.go:clearLine
+ const (
+ CUI = "\033[" // Control Sequence Introducer
+ CPL = CUI + "F" // Cursor Previous Line
+ EL = CUI + "K" // Erase in Line
+ )
+ fmt.Fprintf(tty, "\r\n"+CPL+EL)
+ return
+}
+
func main() {
log.SetFlags(log.Lshortfile)
flag.Usage = usage
setBind := flag.String("bind", "", "Set that /bind instead of autogeneration")
includeTo := flag.Bool("include-to", false, `Include "to" field in KEMs`)
- passwd := flag.String("passwd", "", "Passphrase")
+ passphrase := flag.Bool("p", false, "Use passphrase")
balloonS := flag.Int("balloon-s", 1<<16, "Balloon's space cost")
balloonT := flag.Int("balloon-t", 3, "Balloon's time cost")
balloonP := flag.Int("balloon-p", 2, "Balloon's number of threads")
for kemIdx, kem := range encrypted.KEM {
switch kem.A {
case pki.BalloonBLAKE2bHKDF:
- if *passwd == "" {
+ if !*passphrase {
log.Println(kemIdx, kem.A, "skipping because no -passwd")
continue
}
if kem.Cost == nil {
log.Fatalln("missing cost")
}
+ passwd := readPasswd("Passphrase:")
{
kek := hkdf.Extract(blake2b256, balloon.H(blake2b256,
- []byte(*passwd),
+ passwd,
append(encrypted.Bind[:], *kem.Salt...),
int(kem.Cost.S), int(kem.Cost.T), int(kem.Cost.P),
), []byte(BalloonHKDFSalt))
var cekp []byte
cekp, err = kemChaPolyOpen(kek, kem.CEK, chacha20poly1305.KeySize)
if err != nil {
- log.Println(kemIdx, kem.A, err, "skipping")
+ log.Println(kemIdx, kem.A, err, ", skipping")
continue
}
cek = cekp
var cekp []byte
cekp, err = kemChaPolyOpen(kek, kem.CEK, chacha20poly1305.KeySize)
if err != nil {
- log.Println(kemIdx, kem.A, err, "skipping")
+ log.Println(kemIdx, kem.A, err, ", skipping")
continue
}
cek = cekp
if err != nil {
log.Fatal(err)
}
- if *passwd != "" {
+ if *passphrase {
+ passwd := readPasswd("Passphrase:")
+ {
+ confirm := readPasswd("Confirm:")
+ if !bytes.Equal(passwd, confirm) {
+ log.Fatal("passphrases do not match")
+ }
+ }
salt := make([]byte, BalloonSaltLen)
if _, err = io.ReadFull(rand.Reader, salt); err != nil {
log.Fatal(err)
}
{
kek := hkdf.Extract(blake2b256, balloon.H(blake2b256,
- []byte(*passwd),
+ passwd,
append(binding[:], salt...),
*balloonS, *balloonT, *balloonP,
), []byte(BalloonHKDFSalt))
Encrypt to recipient:
enctool -cer CER [-include-to] [-bind UUID] <DATA >DATA.encrypted
Encrypt on passphrase:
- enctool -passwd PASSPHRASE [-bind UUID] <DATA >DATA.encrypted
+ enctool -p [-bind UUID] <DATA >DATA.encrypted
[-balloon-s X] [-balloon-t X] [-balloon-p X]
Decrypt by providing possible KEMs:
- enctool -d [-passwd ...] [-prv PRV ...] <DATA.encrypted >DATA
+ enctool -d [-p] [-prv PRV ...] <DATA.encrypted >DATA
`)
flag.PrintDefaults()
--- /dev/null
+#!/bin/sh
+
+test_description="TODO"
+. $SHARNESS_TEST_SRCDIR/sharness.sh
+
+TMPDIR=${TMPDIR:-/tmp}
+
+echo "gost3410-512C
+gost3410-256A
+ed25519-blake2b" | while read algo ; do
+
+subj="-subj what=ever"
+typ="some-different-type"
+test_expect_success "$algo: cer generation" "certool \
+ -algo $algo -ku sig $subj \
+ -prv $TMPDIR/sign.prv -cer $TMPDIR/sign.cer"
+dd if=/dev/urandom of=$TMPDIR/sign.data bs=300K count=1 2>/dev/null
+bind="-encrypted-binding $(uuidgen)"
+badBind="-encrypted-binding $(uuidgen)"
+
+test_expect_success "$algo: signing" "sigtool \
+ -prv $TMPDIR/sign.prv -cer $TMPDIR/sign.cer -type $typ \
+ $bind <$TMPDIR/sign.data >$TMPDIR/sign.sig"
+test_expect_success "$algo: verifying" "sigtool \
+ -verify -cer $TMPDIR/sign.cer -type $typ \
+ <$TMPDIR/sign.sig >$TMPDIR/sign.data.got"
+test_expect_success "$algo: comparing" \
+ "test_cmp $TMPDIR/sign.data $TMPDIR/sign.data.got"
+test_expect_success "$algo: differing type" "! sigtool \
+ -verify -cer $TMPDIR/sign.cer <$TMPDIR/sign.sig >/dev/null"
+test_expect_success "$algo: good bind" "! sigtool \
+ -verify -cer $TMPDIR/sign.cer $bind <$TMPDIR/sign.sig >/dev/null"
+test_expect_success "$algo: bad bind" "! sigtool \
+ -verify -cer $TMPDIR/sign.cer $badBind <$TMPDIR/sign.sig >/dev/null"
+
+test_expect_success "$algo: detached signing" "sigtool -detached \
+ -prv $TMPDIR/sign.prv -cer $TMPDIR/sign.cer -type $typ \
+ <$TMPDIR/sign.data >$TMPDIR/sign.sig"
+test_expect_success "$algo: detached verifying" \
+ "cat $TMPDIR/sign.sig $TMPDIR/sign.data |
+ sigtool -detached -verify -cer $TMPDIR/sign.cer -type $typ"
+test_expect_success "$algo: differing type" "! sigtool -detached \
+ -verify -cer $TMPDIR/sign.cer <$TMPDIR/sign.sig >/dev/null"
+test_expect_success "$algo: good bind" "! sigtool -detached \
+ -verify -cer $TMPDIR/sign.cer $bind <$TMPDIR/sign.sig >/dev/null"
+test_expect_success "$algo: bad bind" "! sigtool -detached \
+ -verify -cer $TMPDIR/sign.cer $badBind <$TMPDIR/sign.sig >/dev/null"
+
+done
+
+test_done
"go.cypherpunks.su/keks"
"go.cypherpunks.su/keks/pki"
pkihash "go.cypherpunks.su/keks/pki/hash"
+ "go.cypherpunks.su/keks/pki/sign"
"go.cypherpunks.su/keks/pki/utils"
"go.cypherpunks.su/keks/types"
)
log.Fatal(err)
}
- var signer pki.Signer
- var hasher hash.Hash
- if !*verify {
- if *prvPath == "" {
- log.Fatal("no -prv is set")
- }
- signer, _, err = pki.PrvParse(utils.MustReadFile(*prvPath))
- if err != nil {
- log.Fatal(err)
- }
- hasher = signer.NewHasher()
- }
-
stdin := bufio.NewReaderSize(os.Stdin, BlobChunkLen)
if *verify {
decoder := keks.NewDecoderFromReader(stdin, nil)
var prehash pki.SignedPrehash
var signed pki.Signed
err = decoder.UnmarshalStruct(&prehash)
- if err == nil && prehash.T == pki.SignedPrehashT {
+ var hasher hash.Hash
+ if err == nil && prehash.T == sign.PrehashT {
if len(prehash.Sigs) == 0 {
log.Fatal("prehash: no sigs")
}
for algo := range prehash.Sigs {
hasher = pkihash.ByName(algo)
}
+ if hasher == nil {
+ log.Fatal("prehash: unsupported algorithm")
+ }
var blob *keks.BlobDecoder
blob, err = keks.NewBlobDecoder(stdin, 1<<32)
if err != nil {
}
sig := signed.Sigs[0]
signer := cer.CerLoad()
- if sig.Sign.A != signer.Pub[0].A {
- log.Fatal("differing signature algorithms")
- }
if signed.Load.T != *typ {
log.Fatalln("differing load type:", signed.Load.T)
}
log.Fatal(err)
}
} else {
+ if *prvPath == "" {
+ log.Fatal("no -prv is set")
+ }
+ var signer sign.Iface
+ signer, _, err = pki.PrvParse(utils.MustReadFile(*prvPath))
+ if err != nil {
+ log.Fatal(err)
+ }
+ if err = signer.SetMode(sign.ModePrehash); err != nil {
+ log.Fatal(err)
+ }
+
if _, err = keks.Encode(os.Stdout, pki.SignedMagic, nil); err != nil {
log.Fatal(err)
}
if *detached {
- if _, err = io.Copy(hasher, stdin); err != nil {
+ if _, err = io.Copy(*signer.Prehasher(), stdin); err != nil {
log.Fatal(err)
}
} else {
if _, err = keks.Encode(os.Stdout, pki.SignedPrehash{
- T: pki.SignedPrehashT,
- Sigs: map[string]*struct{}{cer.CerLoad().Pub[0].A: nil},
+ T: sign.PrehashT,
+ Sigs: map[string]*struct{}{signer.Algo(): nil},
}, nil); err != nil {
log.Fatal(err)
}
br := keks.BlobReader{R: pr, ChunkLen: BlobChunkLen}
copyErr := make(chan error)
go func() {
- _, e := io.Copy(io.MultiWriter(pw, hasher), stdin)
+ _, e := io.Copy(io.MultiWriter(pw, *signer.Prehasher()), stdin)
pw.Close()
copyErr <- e
}()
}
var signed pki.Signed
signed.Load.T = *typ
- when := time.Now().UTC().Truncate(1000 * time.Microsecond)
+ when := time.Now().UTC().Truncate(time.Millisecond)
sigTbs := pki.SigTBS{When: &when}
if encryptedBinding != uuid.Nil {
sigTbs.EncryptedBinding = &encryptedBinding
}
- if err = signed.SignPrehashedWith(
- hasher, cer.CerLoad(), signer, sigTbs,
- ); err != nil {
+ if err = signed.SignWith(cer.CerLoad(), signer, sigTbs); err != nil {
log.Fatal(err)
}
if _, err = keks.Encode(os.Stdout, signed, nil); err != nil {
package ed25519blake2b
-const Ed25519BLAKE2b = "ed25519-blake2b"
+const (
+ Ed25519BLAKE2b = "ed25519-blake2b"
+ Ed25519PhBLAKE2b = "ed25519ph-blake2b"
+)
if err != nil {
return
}
- signer = &Signer{prvEd}
+ signer = &Signer{Prv: prvEd}
prv = prvEd.Seed()
pub = pubEd[:]
return
"go.cypherpunks.su/keks/pki/ed25519-blake2b/ed25519"
"golang.org/x/crypto/blake2b"
+
+ "go.cypherpunks.su/keks/pki/sign"
)
type Signer struct {
- Prv ed25519.PrivateKey
+ Prv ed25519.PrivateKey
+ mode sign.Mode
+ prehasher *hash.Hash
+}
+
+func (s *Signer) SetMode(m sign.Mode) error {
+ switch m {
+ case sign.ModePure:
+ s.mode = m
+ return nil
+ case sign.ModePrehash:
+ s.mode = m
+ h, err := blake2b.New512(nil)
+ if err != nil {
+ return err
+ }
+ s.prehasher = &h
+ return nil
+ default:
+ return errors.New("unsupported mode")
+ }
+}
+
+func (s *Signer) Mode() sign.Mode {
+ return s.mode
+}
+
+func (s *Signer) Algo() string {
+ switch s.mode {
+ case sign.ModePure:
+ return Ed25519BLAKE2b
+ case sign.ModePrehash:
+ return Ed25519PhBLAKE2b
+ }
+ return ""
}
func (s *Signer) Public() crypto.PublicKey {
msg []byte,
opts crypto.SignerOpts,
) (signature []byte, err error) {
- return s.Prv.Sign(rand, msg, opts)
-}
-
-func (s *Signer) SignDigest(rand io.Reader, dgst []byte) (signature []byte, err error) {
- return s.Prv.Sign(rand, dgst, &ed25519.Options{Hash: crypto.BLAKE2b_512})
+ switch s.mode {
+ case sign.ModePure:
+ return s.Prv.Sign(rand, msg, opts)
+ case sign.ModePrehash:
+ return s.Prv.Sign(rand, msg, &ed25519.Options{Hash: crypto.BLAKE2b_512})
+ default:
+ panic("unsupported mode")
+ }
}
-func (s *Signer) NewHasher() hash.Hash {
- h, err := blake2b.New512(nil)
- if err != nil {
- panic(err)
- }
- return h
+func (s *Signer) Prehasher() *hash.Hash {
+ return s.prehasher
}
func NewSigner(v []byte) (prv *Signer, pub []byte, err error) {
}
p := ed25519.NewKeyFromSeed(v)
pub = p[ed25519.SeedSize:]
- prv = &Signer{p}
+ prv = &Signer{Prv: p}
return
}
return
}
-func VerifyDigest(pub, digest, signature []byte) (valid bool, err error) {
+func VerifyPrehash(pub, hsh, signature []byte) (valid bool, err error) {
if len(pub) != ed25519.PublicKeySize {
err = errors.New("invalid ed25519 public key size")
return
}
err = ed25519.VerifyWithOptions(
ed25519.PublicKey(pub),
- digest,
+ hsh,
signature,
&ed25519.Options{Hash: crypto.BLAKE2b_512},
)
+// GoKEKS/PKI -- PKI-related capabilities based on KEKS encoded formats
+// Copyright (C) 2024-2025 Sergey Matveev <stargrave@stargrave.org>
+//
+// 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 <http://www.gnu.org/licenses/>.
+
package gost
import (
+// GoKEKS/PKI -- PKI-related capabilities based on KEKS encoded formats
+// Copyright (C) 2024-2025 Sergey Matveev <stargrave@stargrave.org>
+//
+// 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 <http://www.gnu.org/licenses/>.
+
package gost
import (
if err != nil {
return
}
- signer, pub, err = NewSigner(algo, pk.RawBE())
+ signer, pub, err = NewSigner(pk.RawBE())
return
}
+// GoKEKS/PKI -- PKI-related capabilities based on KEKS encoded formats
+// Copyright (C) 2024-2025 Sergey Matveev <stargrave@stargrave.org>
+//
+// 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 <http://www.gnu.org/licenses/>.
+
package gost
import (
"go.cypherpunks.su/gogost/v6/gost3410"
"go.cypherpunks.su/gogost/v6/gost34112012256"
"go.cypherpunks.su/gogost/v6/gost34112012512"
+
+ "go.cypherpunks.su/keks/pki/sign"
)
type Signer struct {
- Prv *gost3410.PrivateKey
- Hasher func() hash.Hash
+ mode sign.Mode
+ Prv *gost3410.PrivateKey
+ NewHasher func() hash.Hash
+ prehasher *hash.Hash
+}
+
+func (s *Signer) SetMode(m sign.Mode) error {
+ switch m {
+ case sign.ModePure:
+ s.mode = m
+ return nil
+ case sign.ModePrehash:
+ s.mode = m
+ p := s.NewHasher()
+ s.prehasher = &p
+ return nil
+ default:
+ return errors.New("unsupported mode")
+ }
+}
+
+func (s *Signer) Mode() sign.Mode {
+ return s.mode
+}
+
+func (s *Signer) Prehasher() *hash.Hash {
+ return s.prehasher
+}
+
+func (s *Signer) Algo() string {
+ switch s.Prv.C.PointSize() {
+ case 32:
+ return GOST3410256A
+ case 64:
+ return GOST3410512C
+ }
+ return ""
}
func (s *Signer) Public() crypto.PublicKey {
msg []byte,
opts crypto.SignerOpts,
) (signature []byte, err error) {
- h := s.Hasher()
- h.Write(msg)
- dgst := h.Sum(nil)
- signature, err = s.Prv.Sign(rand, dgst, opts)
- if err != nil {
- return
+ var hsh []byte
+ switch s.mode {
+ case sign.ModePure:
+ h := s.NewHasher()
+ h.Write(msg)
+ hsh = h.Sum(nil)
+ case sign.ModePrehash:
+ hsh = msg
+ default:
+ panic("unsupported mode")
}
- signature = append(signature[len(signature)/2:], signature[:len(signature)/2]...)
- return
-}
-
-func (s *Signer) SignDigest(rand io.Reader, dgst []byte) (signature []byte, err error) {
- signature, err = s.Prv.SignDigest(dgst, rand)
+ signature, err = s.Prv.SignDigest(hsh, rand)
if err != nil {
return
}
return
}
-func (s *Signer) NewHasher() hash.Hash {
- return s.Hasher()
-}
-
-func NewSigner(a string, v []byte) (prv *Signer, pub []byte, err error) {
+func NewSigner(v []byte) (prv *Signer, pub []byte, err error) {
signer := Signer{}
- switch a {
- case GOST3410256A:
- signer.Hasher = gost34112012256.New
+ switch len(v) {
+ case 32:
+ signer.NewHasher = gost34112012256.New
signer.Prv, err = gost3410.NewPrivateKeyBE(
gost3410.CurveIdtc26gost341012256paramSetA(), v,
)
- case GOST3410512C:
- signer.Hasher = gost34112012512.New
+ case 64:
+ signer.NewHasher = gost34112012512.New
signer.Prv, err = gost3410.NewPrivateKeyBE(
gost3410.CurveIdtc26gost341012512paramSetC(), v,
)
+// GoKEKS/PKI -- PKI-related capabilities based on KEKS encoded formats
+// Copyright (C) 2024-2025 Sergey Matveev <stargrave@stargrave.org>
+//
+// 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 <http://www.gnu.org/licenses/>.
+
package gost
import (
return
}
-func VerifyDigest(algo string, pub, digest, signature []byte) (valid bool, err error) {
+func VerifyPrehash(algo string, pub, hsh, signature []byte) (valid bool, err error) {
var pk *gost3410.PublicKey
pk, err = gost3410.NewPublicKeyBE(CurveByName(algo), pub)
if err != nil {
return
}
- valid, err = pk.VerifyDigest(digest,
+ valid, err = pk.VerifyDigest(hsh,
append(signature[len(signature)/2:], signature[:len(signature)/2]...))
return
}
return gost34112012256.New()
case Streebog512, gost.GOST3410512C:
return gost34112012512.New()
- case BLAKE2b, ed25519blake2b.Ed25519BLAKE2b:
+ case BLAKE2b, ed25519blake2b.Ed25519BLAKE2b, ed25519blake2b.Ed25519PhBLAKE2b:
h, err := blake2b.New512(nil)
if err != nil {
panic(err)
"go.cypherpunks.su/keks"
ed25519blake2b "go.cypherpunks.su/keks/pki/ed25519-blake2b"
"go.cypherpunks.su/keks/pki/gost"
+ "go.cypherpunks.su/keks/pki/sign"
)
// Parse private key contained in AV KEKS-encoded structure.
-func PrvParse(data []byte) (prv Signer, pub []byte, err error) {
+func PrvParse(data []byte) (prv sign.Iface, pub []byte, err error) {
{
var magic keks.Magic
magic, data = keks.StripMagic(data)
case Ed25519BLAKE2b:
prv, pub, err = ed25519blake2b.NewSigner(av.V)
case GOST3410256A, GOST3410512C:
- prv, pub, err = gost.NewSigner(av.A, av.V)
+ prv, pub, err = gost.NewSigner(av.V)
default:
err = fmt.Errorf("unknown private key algo: %s", av.A)
}
--- /dev/null
+package sign
+
+import (
+ "crypto"
+ "hash"
+)
+
+type Iface interface {
+ crypto.Signer
+ SetMode(Mode) error
+ Prehasher() *hash.Hash
+ Algo() string
+ Mode() Mode
+}
--- /dev/null
+package sign
+
+type Mode int
+
+const (
+ ModePure Mode = 0
+ ModePrehash = iota
+
+ PrehashT = "prehash"
+)
package pki
import (
+ "bytes"
"crypto"
"crypto/rand"
"errors"
- "hash"
- "io"
"time"
"github.com/google/uuid"
"go.cypherpunks.su/keks"
+ "go.cypherpunks.su/keks/pki/sign"
)
const SignedMagic = keks.Magic("pki/signed")
-type Signer interface {
- crypto.Signer
- SignDigest(io.Reader, []byte) ([]byte, error)
- NewHasher() hash.Hash
-}
-
-const SignedPrehashT = "prehash"
-
type SignedPrehash struct {
T string `keks:"t"`
Sigs map[string]*struct{} `keks:"sigs"`
Sign AV `keks:"sign"`
}
-type SignedTBS struct {
- V any `keks:"v,omitempty"`
- T string `keks:"t"`
- TBS SigTBS `keks:"tbs"`
-}
-
type Signed struct {
Cers *[]*Signed `keks:"certs,omitempty"`
Load SignedLoad `keks:"load"`
// Sign Signed's contents and sigTBS corresponding data with the
// provided prv signer, having parent certificate. Signature is appended
// to the signed.Sigs. parent certificate must have "sig" key-usage.
-func (signed *Signed) SignWith(
- parent *CerLoad,
- prv crypto.Signer,
- sigTBS SigTBS,
-) (err error) {
+func (signed *Signed) SignWith(parent *CerLoad, prv sign.Iface, sigTBS SigTBS) (err error) {
if !parent.Can(KUSig) || len(parent.Pub) != 1 {
return errors.New("parent can not sign")
}
sigTBS.SID = parent.Pub[0].Id
- signedTBS := SignedTBS{T: signed.Load.T, V: signed.Load.V, TBS: sigTBS}
- sig := Sig{TBS: sigTBS}
- sig.Sign.A = parent.Pub[0].A
- var buf []byte
- buf, err = keks.EncodeBuf(signedTBS, nil)
- if err != nil {
- return
- }
- sig.Sign.V, err = prv.Sign(rand.Reader, buf, crypto.Hash(0))
- if err != nil {
- return
- }
- signed.Sigs = append(signed.Sigs, &sig)
- return nil
-}
-
-// Same as SignWith, but use prehashed mode.
-func (signed *Signed) SignPrehashedWith(
- hasher hash.Hash,
- parent *CerLoad,
- prv Signer,
- sigTBS SigTBS,
-) (err error) {
- if !parent.Can(KUSig) || len(parent.Pub) != 1 {
- return errors.New("parent can not sign")
+ var tbs []byte
+ if prv.Mode() == sign.ModePure {
+ var b bytes.Buffer
+ if _, err = keks.Encode(&b, signed.Load, nil); err != nil {
+ return
+ }
+ if _, err = keks.Encode(&b, sigTBS, nil); err != nil {
+ return
+ }
+ tbs = b.Bytes()
+ } else {
+ if _, err = keks.Encode(*prv.Prehasher(), signed.Load, nil); err != nil {
+ return
+ }
+ if _, err = keks.Encode(*prv.Prehasher(), sigTBS, nil); err != nil {
+ return
+ }
+ tbs = (*prv.Prehasher()).Sum(nil)
}
- sigTBS.SID = parent.Pub[0].Id
- signedTBS := SignedTBS{T: signed.Load.T, TBS: sigTBS}
sig := Sig{TBS: sigTBS}
- sig.Sign.A = parent.Pub[0].A
- _, err = keks.Encode(hasher, signedTBS, nil)
- if err != nil {
- return
- }
- sig.Sign.V, err = prv.SignDigest(rand.Reader, hasher.Sum(nil))
+ sig.Sign.A = prv.Algo()
+ sig.Sign.V, err = prv.Sign(rand.Reader, tbs, crypto.Hash(0))
if err != nil {
return
}
@node cer-gost3410
@subsection cer with GOST R 34.10-2012
-Same rules of serialisation must be used as with
-@code{@ref{pki-signed-gost3410}}. Public key's
-identifier and @code{cid} should be calculated
-using big-endian Streebog-256 hash.
+GOST R 34.10-2012 must be used with Streebog (GOST R 34.11-2012) hash
+function. Its digest must be big-endian serialised. Public key must be
+in @code{BE(X)||BE(Y)} format.
+
+Algorithm identifiers for the public key: @code{gost3410-256A},
+@code{gost3410-512C}.
+
+Public key's identifier and @code{cid} should be calculated using
+big-endian Streebog-256 hash.
@node cer-ed25519-blake2b
@subsection cer with Ed25519-BLAKE2b
@code{@ref{pki-signed-ed25519-blake2b}}.
Public key's identifier and @code{cid} should be calculated
using BLAKE2b hash with 128 or 256 bit output length specified.
+Algorithm identifiers for the public key: @code{ed25519ph-blake2b},
@node cer-sntrup4591761-x25519
@subsection cer with SNTRUP4591761-X25519
+++ /dev/null
-pki-signed-tbs = {
- t: text, ; = /load/t
- ? v: any, ; /load/v if it is supplied
- tbs: map, ; = /sigs/?/tbs
-}
@verbatiminclude format/signed.cddl
-Signature is created by signing the following encoded MAP:
+Signature is created by signing the:
-@verbatiminclude format/signed-tbs.cddl
+@verbatim
+[detached-data] || /load || sig-tbs
+@end verbatim
If no @code{/load/v} is provided, then the data is detached from the
@code{pki-signed} structure itself and it is fed into hasher before that
following approach:
@verbatim
-pki-signed-prehash || BLOB || pki-signed
+pki-signed-prehash || BLOB(detached-data) || pki-signed
@end verbatim
@verbatiminclude format/signed-prehash.cddl
With @code{pki-signed-prehash} you initialise your hashers used during
signing process and fed BLOB's contents (not the encoded BLOB itself!)
-into the them. Finally you add @code{pki-signed-tbs} without @code{v}
-field to them.
+into the them.
@code{/sigs/*/tbs/when} is optional signing time.
@subsection pki-signed with GOST R 34.10-2012
GOST R 34.10-2012 must be used with Streebog (GOST R 34.11-2012) hash
-function. Its digest must be big-endian serialised. Public key must be
-in @code{BE(X)||BE(Y)} format. Signature is in @code{BE(R)||BE(S)}
-format.
+function. Its digest must be big-endian serialised. Signature is in
+@code{BE(R)||BE(S)} format.
-Following algorithm identifiers are acceptable for the public key and
-signature: @code{gost3410-256A}, @code{gost3410-512C}.
+Algorithm identifiers for the signature: @code{gost3410-256A},
+@code{gost3410-512C}.
@node pki-signed-ed25519-blake2b
@subsection pki-signed with Ed25519-BLAKE2b
Strict @url{https://zips.z.cash/zip-0215, ZIP-0215} validation rules
should be used while verifying the signature.
-PureEdDSA @strong{must} be used when no detached data exists.
-HashEdDSA @strong{must} be used otherwise, using BLAKE2b-512 as a hash.
+PureEdDSA @strong{must} be used when no detached data exists and
+@code{ed25519-blake2b} algorithm identifier is used for signature.
-@code{ed25519-blake2b} algorithm identifier is used.
+HashEdDSA @strong{must} be used otherwise, using BLAKE2b-512 as a hash,
+using @code{ed25519ph-blake2b} algorithm identifier for signature.