[]any{},
map[string]any{},
gyac.MakeBlob(123, []byte{}),
- uuid.MustParse("00000000-0000-0000-0000-000000000000"),
+ uuid.Nil,
&gyac.Raw{
T: gyac.AtomTAI64,
V: []byte("\x00\x00\x00\x00\x00\x00\x00\x00"),
buf = gyac.AtomBlobEncode(buf, 123)
buf = gyac.AtomBinEncode(buf, []byte{})
}
- buf = gyac.AtomUUIDEncode(buf,
- uuid.MustParse("00000000-0000-0000-0000-000000000000"))
+ buf = gyac.AtomUUIDEncode(buf, uuid.Nil)
buf = gyac.AtomRawEncode(buf, &gyac.Raw{
T: gyac.AtomTAI64,
V: []byte("\x00\x00\x00\x00\x00\x00\x00\x00"),
T byte
}
+func (i *Item) Typ() ItemType {
+ return ItemType(i.T)
+}
+
type Raw struct {
V []byte
T AtomType
err = ErrNotEnough
return
}
- if ItemType(item.T) == ItemBin {
+ if item.Typ() == ItemBin {
item.V = buf[1+ll : 1+ll+l]
} else {
s := unsafe.String(unsafe.SliceData(buf[1+ll:]), l)
err = ErrLenNonMinimal
return
}
- if ItemType(item.T) == ItemUInt {
+ if item.Typ() == ItemUInt {
item.V = v
} else {
item.V = -1 - int64(v)
}
buf = buf[off:]
tail = buf
- switch ItemType(item.T) {
+ switch item.Typ() {
case ItemList:
var sub *Item
var v []*Item
}
func EncodeItem(buf []byte, item *Item) []byte {
- switch ItemType(item.T) {
+ switch item.Typ() {
case ItemNIL:
return AtomNILEncode(buf)
case ItemBool:
var empty bool
item := ItemFromGo(fv.Interface())
switch ItemType(item.T) {
+ case ItemNIL:
+ empty = true
case ItemList:
if len(item.V.([]*Item)) == 0 {
empty = true
if err != nil {
return
}
- if ItemType(item.T) != ItemMap {
+ if item.Typ() != ItemMap {
err = errors.New("non-map")
return
}
package yacpki
import (
+ "bytes"
"hash"
"log"
+ "github.com/google/uuid"
"go.cypherpunks.su/gogost/v6/gost3410"
"go.cypherpunks.su/gogost/v6/gost34112012256"
"go.cypherpunks.su/gogost/v6/gost34112012512"
+ "go.cypherpunks.su/yac/gyac"
+ "go.cypherpunks.su/yac/gyac/yacpki/utils"
)
const (
V []byte `yac:"v"`
}
+func (av *AV) Id() (id uuid.UUID) {
+ var hasher hash.Hash
+ switch av.A {
+ case AlgoGOST3410256A, AlgoGOST3410256B, AlgoGOST3410256C, AlgoGOST3410256D, AlgoGOST3410512A, AlgoGOST3410512B, AlgoGOST3410512C:
+ hasher = gost34112012256.New()
+ default:
+ panic("unsupported algorithm")
+ }
+ utils.MustWrite(hasher, gyac.EncodeItem(nil, gyac.ItemFromGo(av)))
+ id, err := uuid.NewRandomFromReader(bytes.NewReader(hasher.Sum(nil)))
+ if err != nil {
+ panic(err)
+ }
+ return id
+}
+
func GOST3410CurveByName(name string) (curve *gost3410.Curve) {
switch name {
case AlgoGOST3410256A:
curve = gost3410.CurveIdtc26gost341012512paramSetB()
case AlgoGOST3410512C:
curve = gost3410.CurveIdtc26gost341012512paramSetC()
+ default:
+ log.Fatal("unknown curve")
}
return
}
case AlgoGOST3410512A, AlgoGOST3410512B, AlgoGOST3410512C:
return gost34112012512.New()
default:
- log.Fatal("unsupported CA algorithm")
+ log.Fatal("unsupported algorithm")
}
return nil
}
package yacpki
import (
- "bytes"
"crypto"
- "crypto/rand"
"errors"
- "hash"
+ "fmt"
"time"
"github.com/google/uuid"
"go.cypherpunks.su/gogost/v6/gost3410"
- "go.cypherpunks.su/gogost/v6/gost34112012256"
"go.cypherpunks.su/yac/gyac"
+ "go.cypherpunks.su/yac/gyac/yacpki/utils"
)
-type SignedDataLoad struct {
- V any `yac:"v"`
- T string `yac:"t"`
-}
-
-type SigTBS struct {
- Hashes map[string][]byte `yac:"hash,omitempty"`
- SID uuid.UUID `yac:"sid"`
-}
+const (
+ KUCA = "ca"
+ KUSign = "sig"
+)
-type Sig struct {
- TBS SigTBS `yac:"tbs,omitempty"`
- CerLoc []string `yac:"cer-loc,omitempty"`
- Sign AV `yac:"sign"`
+type Pub struct {
+ A string `yac:"a"`
+ V []byte `yac:"v"`
+ Id uuid.UUID `yac:"id"`
}
-type SignedDataTBS struct {
- V any `yac:"v"`
- T string `yac:"t"`
- TBS SigTBS `yac:"tbs"`
+type CerLoad struct {
+ KU *map[string]*struct{} `yac:"ku,omitempty"`
+ Subj map[string]string `yac:"sub"`
+ Crit *[]map[string]any `yac:"crit,omitempty"`
+ Pub []Pub `yac:"pub"`
}
-type SignedData struct {
- Hashes []string `yac:"hash,omitempty"`
- Load SignedDataLoad `yac:"load"`
- Sigs []Sig `yac:"sigs"`
- Cers []SignedData `yac:"certs,omitempty"`
+func CerParse(data []byte) (sd *SignedData, tail []byte, err error) {
+ sd, tail, err = SignedDataParse(data)
+ if err != nil {
+ return
+ }
+ err = sd.CerParse()
+ return
}
-type CerLoad struct {
- KU []string `yac:"ku,omitempty"`
- Subj map[string]string `yac:"sub"`
- Exp []time.Time `yac:"exp"`
- Crit []map[string]any `yac:"crit,omitempty"`
- Pub AV `yac:"pub"`
- PKID uuid.UUID `yac:"pkid"`
+func (sd *SignedData) CerParse() error {
+ if sd.Load.T != "cer" {
+ return errors.New("CerParse: wrong load type")
+ }
+ if len(sd.Sigs) == 0 {
+ return errors.New("CerParse: missing sigs")
+ }
+ for _, sig := range sd.Sigs {
+ if sig.TBS.Hashes != nil {
+ return errors.New("CerParse: prehashed SignedData")
+ }
+ if sig.TBS.CID == nil {
+ return errors.New("CerParse: missing cid")
+ }
+ if sig.TBS.Exp == nil {
+ return errors.New("CerParse: missing exp")
+ }
+ }
+ var load CerLoad
+ var err error
+ if v, ok := sd.Load.V.(map[string]any); ok {
+ err = gyac.MapToStruct(&load, v)
+ } else {
+ err = errors.New("CerParse: wrong /load/v")
+ }
+ if err != nil {
+ return err
+ }
+ sd.Load.V = load
+ if load.KU != nil {
+ if len(*load.KU) == 0 {
+ return errors.New("CerParse: empty ku")
+ }
+ for _, v := range *load.KU {
+ if v != nil {
+ return errors.New("CerParse: non-nil ku value")
+ }
+ }
+ }
+ if len(load.Subj) == 0 {
+ return errors.New("CerParse: empty sub")
+ }
+ if load.Crit != nil {
+ if len(*load.Crit) == 0 {
+ return errors.New("CerParse: empty crit")
+ }
+ return errors.New("CerParse: currently no critical extensions are supported")
+ }
+ if len(load.Pub) == 0 {
+ return errors.New("CerParse: empty pub")
+ }
+ for _, pub := range load.Pub {
+ if len(pub.A) == 0 || len(pub.V) == 0 || pub.Id == uuid.Nil {
+ return errors.New("CerParse: non-filled pub")
+ }
+ }
+ return nil
}
-func (tbs *CerLoad) HasCA() (hasCA bool) {
- for _, ku := range tbs.KU {
- if ku == "ca" {
- hasCA = true
- }
+func (cer *CerLoad) CanSign() bool {
+ if cer.KU == nil {
+ return false
}
- return
+ if _, ok := (*cer.KU)[KUSign]; !ok {
+ return false
+ }
+ if len(cer.Pub) != 1 {
+ return false
+ }
+ return true
}
-func PKIDFromPub(pub *AV) (kid uuid.UUID) {
- hasher := gost34112012256.New()
- hasher.Write(gyac.EncodeItem(nil, gyac.ItemFromGo(pub)))
- var err error
- kid, err = uuid.NewRandomFromReader(bytes.NewReader(hasher.Sum(nil)))
+func (sd *SignedData) CerIssueWith(
+ parent *CerLoad,
+ prv crypto.Signer,
+ since, till time.Time,
+) error {
+ exp := []time.Time{since, till}
+ cid, err := uuid.NewV7()
if err != nil {
- panic(err)
+ return err
}
- return
+ return sd.SignWith(parent, prv, SigTBS{CID: &cid, Exp: &exp})
}
-var SigInvalid = errors.New("signature is invalid")
+var ErrSigInvalid = errors.New("signature is invalid")
func (cer *CerLoad) CheckSignature(signed, signature []byte) (err error) {
- switch cer.Pub.A {
+ if !cer.CanSign() {
+ err = errors.New("cer can not sign")
+ return
+ }
+ pub := cer.Pub[0]
+ switch pub.A {
case AlgoGOST3410256A, AlgoGOST3410256B, AlgoGOST3410256C, AlgoGOST3410256D, AlgoGOST3410512A, AlgoGOST3410512B, AlgoGOST3410512C:
- var pub *gost3410.PublicKey
- pub, err = gost3410.NewPublicKeyBE(GOST3410CurveByName(cer.Pub.A), cer.Pub.V)
+ var pk *gost3410.PublicKey
+ pk, err = gost3410.NewPublicKeyBE(GOST3410CurveByName(pub.A), pub.V)
if err != nil {
return
}
- hasher := HasherByKeyAlgo(cer.Pub.A)
- hasher.Write(signed)
+ hasher := HasherByKeyAlgo(pub.A)
+ utils.MustWrite(hasher, signed)
var valid bool
- valid, err = pub.VerifyDigest(hasher.Sum(nil), signature)
+ valid, err = pk.VerifyDigest(hasher.Sum(nil), signature)
if !valid {
- err = SigInvalid
+ err = ErrSigInvalid
}
default:
err = errors.New("unsupported signature algorithm")
return
}
-func (sd *SignedData) CheckSignatureFrom(parent *CerLoad) (err error) {
- sig := sd.Sigs[0]
- if sig.TBS.SID != parent.PKID {
- err = errors.New("sid != parent pkid")
- return
- }
- if len(sig.TBS.Hashes) == 0 {
- err = errors.New("sig.tbs misses hash")
- return
- }
- if sig.TBS.Hashes[sd.Hashes[0]] == nil {
- err = errors.New("sig.tbs misses specified hash")
+func (sd *SignedData) CerCheckSignatureFrom(parent *CerLoad) (err error) {
+ if len(sd.Sigs) != 1 {
+ err = errors.New("can verify only single signature")
return
}
- hashNew, ok := HashToNew[sd.Hashes[0]]
- if !ok {
- err = errors.New("unsupported hash")
+ if !parent.CanSign() {
+ err = errors.New("parent can not sign")
return
}
- hasher := hashNew()
- hasher.Write(gyac.EncodeItem(nil, gyac.ItemFromGo(sd.Load.V)))
- if !bytes.Equal(sig.TBS.Hashes[sd.Hashes[0]], hasher.Sum(nil)) {
- err = errors.New("hash differs")
+ sig := sd.Sigs[0]
+ if sig.TBS.SID != parent.Pub[0].Id {
+ err = errors.New("sid != parent pub id")
return
}
tbs := SignedDataTBS{T: sd.Load.T, V: sd.Load.V, TBS: sig.TBS}
)
}
-func (sd *SignedData) PrehashedSignWith(parent *CerLoad, prv crypto.Signer) (err error) {
- var hashAlgo string
- var hashNew func() hash.Hash
- switch parent.Pub.A {
- case AlgoGOST3410256A, AlgoGOST3410256B, AlgoGOST3410256C, AlgoGOST3410256D, AlgoGOST3410512A, AlgoGOST3410512B, AlgoGOST3410512C:
- hashAlgo = AlgoStreebog256
- hashNew = gost34112012256.New
- default:
- return errors.New("unknown hash algorithm for public key")
- }
- hasher := hashNew()
- hasher.Write(gyac.EncodeItem(nil, gyac.ItemFromGo(sd.Load.V)))
- sig := Sig{TBS: SigTBS{
- SID: parent.PKID,
- Hashes: map[string][]byte{hashAlgo: hasher.Sum(nil)},
- }}
- sdTBS := SignedDataTBS{T: sd.Load.T, V: sd.Load.V, TBS: sig.TBS}
- hasher = HasherByKeyAlgo(parent.Pub.A)
- hasher.Write(gyac.EncodeItem(nil, gyac.ItemFromGo(sdTBS)))
- sig.Sign.A = parent.Pub.A
- sig.Sign.V, err = prv.Sign(rand.Reader, hasher.Sum(nil), nil)
- if err != nil {
- return
+func (sd *SignedData) CerLoad() *CerLoad {
+ l, ok := sd.Load.V.(CerLoad)
+ if ok {
+ return &l
}
- sd.Hashes = append(sd.Hashes, hashAlgo)
- sd.Sigs = append(sd.Sigs, sig)
- return
+ return nil
}
-func CerParse(data []byte) (sd *SignedData, tail []byte, err error) {
- var s SignedData
- tail, err = gyac.DecodeToStruct(&s, data)
- if err != nil {
- return
+func (sd *SignedData) CerVerify(cers []*SignedData, t time.Time) (err error) {
+ {
+ exp := *(sd.Sigs[0].TBS.Exp)
+ if t.Before(exp[0]) || t.Equal(exp[0]) {
+ err = errors.New("cer is not active")
+ return
+ }
+ if t.After(exp[1]) || t.Equal(exp[1]) {
+ err = errors.New("cer is expired")
+ return
+ }
}
- sd = &s
- if sd.Load.T != "cer" {
- err = errors.New("SignedData: not \"cer\" type")
- return
+ sid := sd.Sigs[0].TBS.SID
+ if sid == sd.CerLoad().Pub[0].Id {
+ return sd.CerCheckSignatureFrom(sd.CerLoad())
}
- if len(sd.Sigs) != 1 {
- err = errors.New("SignedData: wrong number of sigs")
- return
+ idToCer := make(map[uuid.UUID]*SignedData, len(cers))
+ for _, cer := range cers {
+ cerLoad := cer.CerLoad()
+ if !cerLoad.CanSign() {
+ err = errors.New("cer can not sign")
+ return
+ }
+ if _, ok := (*cerLoad.KU)[KUCA]; !ok {
+ err = errors.New("cer can not ca")
+ return
+ }
+ idToCer[cerLoad.Pub[0].Id] = cer
}
- if len(sd.Hashes) != 1 {
- err = errors.New("SignedData: wrong number of hashes")
+ signer := idToCer[sid]
+ if signer == nil {
+ err = fmt.Errorf("no cer found for sid: %v", sd.Sigs[0].TBS.SID)
return
}
- {
- var load CerLoad
- err = gyac.MapToStruct(&load, sd.Load.V.(map[string]any))
- if err != nil {
- return
- }
- sd.Load.V = &load
+ err = sd.CerCheckSignatureFrom(signer.CerLoad())
+ if err != nil {
+ return
}
- return
+ return signer.CerVerify(cers, t)
}
TMPDIR=${TMPDIR:-/tmp}
-subj='{"CN": "CA", "C": "RU"}'
+subj="-subj CN=CA -subj C=RU"
test_expect_success "CA generation" "yacertool \
-algo gost3410-512C \
- -ku ca \
- -prv $TMPDIR/ca.prv -cer $TMPDIR/ca.cer \
- -subj '$subj'"
-
+ -ku ca -ku sig $subj \
+ -prv $TMPDIR/ca.prv -cer $TMPDIR/ca.cer"
test_expect_success "CA regeneration" "yacertool \
-algo gost3410-512C \
- -ku ca \
+ -ku ca -ku sig $subj \
-prv $TMPDIR/ca.prv -cer $TMPDIR/ca.cer \
- -reuse-key \
- -subj '$subj'"
+ -reuse-key"
test_expect_success "CA self-signature" "yacertool \
-ca-cer $TMPDIR/ca.cer \
-cer $TMPDIR/ca.cer \
-verify"
-subj='{"CN": "EE", "C": "RU"}'
+subj="-subj CN=SubCA -subj C=RU"
+test_expect_success "SubCA generation" "yacertool \
+ -ku ca -ku sig $subj \
+ -prv $TMPDIR/subca.prv -cer $TMPDIR/subca.cer \
+ -ca-cer $TMPDIR/ca.cer -ca-prv $TMPDIR/ca.prv"
+test_expect_success "SubCA signature" "yacertool \
+ -ca-cer $TMPDIR/ca.cer \
+ -cer $TMPDIR/subca.cer \
+ -verify"
+
+subj="-subj CN=EE -subj C=RU"
test_expect_success "EE generation" "yacertool \
- -algo gost3410-256A \
- -ca-prv $TMPDIR/ca.prv -ca-cer $TMPDIR/ca.cer \
- -prv $TMPDIR/ee.prv -cer $TMPDIR/ee.cer \
- -subj '$subj'"
+ -algo gost3410-256A $subj \
+ -ca-prv $TMPDIR/subca.prv -ca-cer $TMPDIR/subca.cer \
+ -prv $TMPDIR/ee.prv -cer $TMPDIR/ee.cer"
test_expect_success "EE chain" "yacertool \
-ca-cer $TMPDIR/ca.cer \
+ -ca-cer $TMPDIR/subca.cer \
-cer $TMPDIR/ee.cer \
-verify"
import (
"crypto"
"crypto/rand"
- "encoding/json"
+ "errors"
"flag"
- "fmt"
"io"
"log"
"os"
- "sort"
+ "strings"
"time"
"go.cypherpunks.su/gogost/v6/gost3410"
"go.cypherpunks.su/yac/gyac"
"go.cypherpunks.su/yac/gyac/yacpki"
+ "go.cypherpunks.su/yac/gyac/yacpki/utils"
)
-func MustReadFile(p string) []byte {
- data, err := os.ReadFile(p)
- if err != nil {
- panic(fmt.Errorf("read %s: %v", p, err))
- }
- return data
-}
-
func main() {
- kuMap := make(map[string]struct{})
+ ku := make(map[string]*struct{})
+ subj := make(map[string]string)
flag.Func(
"ku",
- "Optional key usage, may be specified multiple times",
+ "Optional key usage, can be specified multiple times",
+ func(v string) error {
+ ku[v] = nil
+ return nil
+ },
+ )
+ flag.Func(
+ "subj",
+ "Part of subject, key=value, can be specified multiple times",
+ func(v string) error {
+ s := strings.SplitN(v, "=", 2)
+ if len(s) != 2 {
+ return errors.New("invalid key=value")
+ }
+ subj[s[0]] = s[1]
+ return nil
+ },
+ )
+ var issuingCers []string
+ flag.Func(
+ "ca-cer",
+ "Add CA certificate to the chain",
func(v string) error {
- kuMap[v] = struct{}{}
+ issuingCers = append(issuingCers, v)
return nil
},
)
"Optional notBefore, \"2006-01-02 15:04:05\" format")
lifetime := flag.Uint("lifetime", 365,
"Lifetime of the certificate, days")
- subjRaw := flag.String("subj", `{"CN": "test"}`,
- "JSON map of the subject")
algo := flag.String("algo", "gost3410-256A", "Public key algorithm")
- issuingCer := flag.String("ca-cer", "",
- "Path to certificate file for issuing with")
issuingPrv := flag.String("ca-prv", "",
"Path to private key file for issuing with")
reuseKey := flag.Bool("reuse-key", false,
log.Fatal("no -cer is set")
}
- var ku []string
- for k := range kuMap {
- ku = append(ku, k)
- }
- kuMap = nil
- sort.Sort(gyac.ByLenFirst(ku))
-
- var subj map[string]string
- err := json.Unmarshal([]byte(*subjRaw), &subj)
- if err != nil {
- log.Fatalln("while parsing -subj:", err)
+ if !*verify && len(subj) == 0 {
+ log.Fatal("no -subj is set")
}
+ var err error
var since time.Time
if *sinceRaw == "" {
since = time.Now().UTC().Truncate(time.Second)
till := since.Add(time.Duration(*lifetime) * 24 * time.Hour)
var caPrv *gost3410.PrivateKey
- var caCerLoad *yacpki.CerLoad
- if *issuingCer != "" {
+ var caCers []*yacpki.SignedData
+ for _, issuingCer := range issuingCers {
var sd *yacpki.SignedData
- sd, _, err = yacpki.CerParse(MustReadFile(*issuingCer))
+ sd, _, err = yacpki.CerParse(utils.MustReadFile(issuingCer))
if err != nil {
log.Fatal(err)
}
- caCerLoad = sd.Load.V.(*yacpki.CerLoad)
- if !*verify {
- if *issuingPrv == "" {
- log.Fatal("no -issuing-key is set")
- }
- var signer crypto.Signer
- signer, err = yacpki.PrvParse(MustReadFile(*issuingPrv))
- if err != nil {
- log.Fatal(err)
- }
- caPrv = signer.(*gost3410.PrivateKey)
+ caCers = append(caCers, sd)
+ }
+ if len(caCers) > 0 && !*verify {
+ if *issuingPrv == "" {
+ log.Fatal("no -ca-key is set")
+ }
+ var signer crypto.Signer
+ signer, err = yacpki.PrvParse(utils.MustReadFile(*issuingPrv))
+ if err != nil {
+ log.Fatal(err)
}
+ caPrv = signer.(*gost3410.PrivateKey)
}
if *verify {
var sd *yacpki.SignedData
- sd, _, err = yacpki.CerParse(MustReadFile(*cerPath))
+ sd, _, err = yacpki.CerParse(utils.MustReadFile(*cerPath))
if err != nil {
log.Fatal(err)
}
- cerLoad := sd.Load.V.(*yacpki.CerLoad)
- sig := sd.Sigs[0]
- if sig.TBS.SID != cerLoad.PKID && !caCerLoad.HasCA() {
- log.Fatal("no \"ca\" KU met in CA")
- }
- err = sd.CheckSignatureFrom(caCerLoad)
+ err = sd.CerVerify(caCers, time.Now().UTC())
if err != nil {
- if err == yacpki.SigInvalid {
- os.Exit(1)
- }
log.Fatal(err)
}
return
var prv *gost3410.PrivateKey
if *reuseKey {
var signer crypto.Signer
- signer, err = yacpki.PrvParse(MustReadFile(*prvPath))
+ signer, err = yacpki.PrvParse(utils.MustReadFile(*prvPath))
if err != nil {
log.Fatal(err)
}
if err != nil {
log.Fatal(err)
}
- cerLoad := yacpki.CerLoad{
- KU: ku,
- Exp: []time.Time{since, till},
- Subj: subj,
- Pub: yacpki.AV{A: *algo, V: pub.RawBE()},
+ pubMap := yacpki.Pub{A: *algo, V: pub.RawBE()}
+ {
+ av := yacpki.AV{A: *algo, V: pub.RawBE()}
+ pubMap.Id = av.Id()
}
- cerLoad.PKID = yacpki.PKIDFromPub(&cerLoad.Pub)
+ cerLoad := yacpki.CerLoad{Subj: subj, Pub: []yacpki.Pub{pubMap}}
+ if len(ku) > 0 {
+ cerLoad.KU = &ku
+ }
+ var caCerLoad *yacpki.CerLoad
if caPrv == nil {
caPrv = prv
caCerLoad = &cerLoad
} else {
- if !caCerLoad.HasCA() {
- log.Fatal("no \"ca\" KU met in CA")
- }
+ caCerLoad = caCers[0].CerLoad()
}
sd := yacpki.SignedData{Load: yacpki.SignedDataLoad{T: "cer", V: cerLoad}}
- err = sd.PrehashedSignWith(caCerLoad, caPrv)
+ err = sd.CerIssueWith(caCerLoad, caPrv, since, till)
if err != nil {
log.Fatal(err)
}
+
err = os.WriteFile(*cerPath, gyac.EncodeItem(nil, gyac.ItemFromGo(sd)), 0o666)
if err != nil {
log.Fatal(err)
--- /dev/null
+package main
+
+import (
+ "bufio"
+ "bytes"
+ "crypto"
+ "flag"
+ "io"
+ "log"
+ "os"
+ "time"
+
+ "go.cypherpunks.su/yac/gyac"
+ "go.cypherpunks.su/yac/gyac/yacpki"
+ "go.cypherpunks.su/yac/gyac/yacpki/utils"
+)
+
+func main() {
+ prvPath := flag.String("prv", "", "Path to private key file")
+ cerPath := flag.String("cer", "", "Path to certificate file")
+ sdPath := flag.String("sd", "", "Path to signed-data 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 provided -cer with -ca-cer")
+
+ flag.Parse()
+ log.SetFlags(log.Lshortfile)
+
+ if *cerPath == "" {
+ log.Fatal("no -cer is set")
+ }
+ cer, _, err := yacpki.CerParse(utils.MustReadFile(*cerPath))
+ if err != nil {
+ log.Fatal(err)
+ }
+
+ var signer crypto.Signer
+ if !*verify {
+ if *prvPath == "" {
+ log.Fatal("no -prv is set")
+ }
+ signer, err = yacpki.PrvParse(utils.MustReadFile(*prvPath))
+ if err != nil {
+ log.Fatal(err)
+ }
+ }
+
+ hashNew := yacpki.HashToNew[*hashAlgo]
+ if hashNew == nil {
+ log.Fatal("unknown -hash specified")
+ }
+ hasher := hashNew()
+ _, err = io.Copy(hasher, bufio.NewReader(os.Stdin))
+ if err != nil {
+ log.Fatal(err)
+ }
+ if *verify {
+ var sd *yacpki.SignedData
+ sd, _, err = yacpki.SignedDataParse(utils.MustReadFile(*sdPath))
+ if err != nil {
+ log.Fatal(err)
+ }
+ if len(sd.Sigs) == 0 {
+ log.Fatal("no sigs")
+ }
+ sig := sd.Sigs[0]
+ if sd.Hashes == nil || sig.TBS.Hashes == nil {
+ log.Fatal("no hashes")
+ }
+ hashes := *sig.TBS.Hashes
+ hashExpect := hashes[*hashAlgo]
+ hashGot := hasher.Sum(nil)
+ if hashExpect == nil || !bytes.Equal(hashExpect, hashGot) {
+ log.Fatal("hash mismatch")
+ }
+ signer := cer.CerLoad()
+ if !signer.CanSign() {
+ log.Fatal("cer can not sign")
+ }
+ if sig.Sign.A != signer.Pub[0].A {
+ log.Fatal("differing signature algorithms")
+ }
+ err = sd.CerCheckSignatureFrom(signer)
+ if err != nil {
+ log.Fatal(err)
+ }
+ } else {
+ var sd yacpki.SignedData
+ sd.Load.T = *typ
+ sdHashes := []string{*hashAlgo}
+ sd.Hashes = &sdHashes
+ sigHashes := map[string][]byte{*hashAlgo: hasher.Sum(nil)}
+ when := time.Now().UTC().Truncate(1000 * time.Microsecond)
+ err = sd.SignWith(cer.CerLoad(), signer, yacpki.SigTBS{
+ Hashes: &sigHashes,
+ When: &when,
+ })
+ if err != nil {
+ log.Fatal(err)
+ }
+ err = os.WriteFile(*sdPath, gyac.EncodeItem(nil, gyac.ItemFromGo(sd)), 0o666)
+ if err != nil {
+ log.Fatal(err)
+ }
+ }
+}
--- /dev/null
+package yacpki
+
+import (
+ "crypto"
+ "crypto/rand"
+ "errors"
+ "time"
+
+ "github.com/google/uuid"
+ "go.cypherpunks.su/yac/gyac"
+ "go.cypherpunks.su/yac/gyac/yacpki/utils"
+)
+
+type SignedDataLoad struct {
+ V any `yac:"v,omitempty"`
+ T string `yac:"t"`
+}
+
+type SigTBS struct {
+ Hashes *map[string][]byte `yac:"hash,omitempty"`
+ CID *uuid.UUID `yac:"cid,omitempty"`
+ Exp *[]time.Time `yac:"exp,omitempty"`
+ When *time.Time `yac:"when,omitempty"`
+ SID uuid.UUID `yac:"sid"`
+}
+
+type Sig struct {
+ TBS SigTBS `yac:"tbs,omitempty"`
+ CerLoc *[]string `yac:"cer-loc,omitempty"`
+ Sign AV `yac:"sign"`
+}
+
+type SignedDataTBS struct {
+ V any `yac:"v"`
+ T string `yac:"t"`
+ TBS SigTBS `yac:"tbs"`
+}
+
+type SignedData struct {
+ Hashes *[]string `yac:"hash,omitempty"`
+ Cers *[]*SignedData `yac:"certs,omitempty"`
+ Load SignedDataLoad `yac:"load"`
+ Sigs []*Sig `yac:"sigs"`
+}
+
+func SignedDataParse(data []byte) (sd *SignedData, tail []byte, err error) {
+ var item *gyac.Item
+ item, tail, err = gyac.DecodeItem(data)
+ if err != nil {
+ return
+ }
+ sd, err = SignedDataParseItem(item)
+ return
+}
+
+func SignedDataParseItem(item *gyac.Item) (sd *SignedData, err error) {
+ if item.Typ() != gyac.ItemMap {
+ err = errors.New("SignedDataParse: non-map")
+ return
+ }
+ var _sd SignedData
+ err = gyac.MapToStruct(&_sd, item.ToGo().(map[string]any))
+ if err != nil {
+ return
+ }
+ sd = &_sd
+
+ if sd.Hashes != nil && len(*sd.Hashes) == 0 {
+ err = errors.New("SignedDataParse: empty /hash")
+ return
+ }
+ if sd.Cers != nil {
+ if len(*sd.Cers) == 0 {
+ err = errors.New("SignedDataParse: empty /certs")
+ return
+ }
+ for _, cer := range *sd.Cers {
+ err = cer.CerParse()
+ if err != nil {
+ return
+ }
+ }
+ }
+ for _, sig := range sd.Sigs {
+ if sig.CerLoc != nil && len(*sig.CerLoc) == 0 {
+ err = errors.New("SignedDataParse: empty cer-loc")
+ return
+ }
+ if sig.TBS.Hashes != nil && len(*sig.TBS.Hashes) == 0 {
+ err = errors.New("SignedDataParse: empty hash")
+ return
+ }
+ if sig.TBS.Exp != nil {
+ if len(*sig.TBS.Exp) != 2 {
+ err = errors.New("SignedDataParse: wrong exp len")
+ return
+ }
+ for _, t := range *sig.TBS.Exp {
+ if t.Nanosecond() != 0 {
+ err = errors.New("SignedDataParse: exp with nanoseconds")
+ return
+ }
+ }
+ }
+ if sd.Hashes != nil {
+ if sig.TBS.Hashes == nil {
+ err = errors.New("SignedDataParse: /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("SignedDataParse: /sigs: no hash")
+ return
+ }
+ }
+ }
+ return
+}
+
+func (sd *SignedData) SignWith(parent *CerLoad, prv crypto.Signer, sigTBS SigTBS) error {
+ if !parent.CanSign() {
+ return errors.New("parent can not sign")
+ }
+ sigTBS.SID = parent.Pub[0].Id
+ sdTBS := SignedDataTBS{T: sd.Load.T, V: sd.Load.V, TBS: sigTBS}
+ sig := Sig{TBS: sigTBS}
+ hasher := HasherByKeyAlgo(parent.Pub[0].A)
+ utils.MustWrite(hasher, gyac.EncodeItem(nil, gyac.ItemFromGo(sdTBS)))
+ sig.Sign.A = parent.Pub[0].A
+ var err error
+ sig.Sign.V, err = prv.Sign(rand.Reader, hasher.Sum(nil), nil)
+ if err != nil {
+ return err
+ }
+ sd.Sigs = append(sd.Sigs, &sig)
+ return nil
+}
--- /dev/null
+package utils
+
+import (
+ "fmt"
+ "io"
+ "os"
+)
+
+func MustWrite(w io.Writer, data []byte) {
+ n, err := w.Write(data)
+ if err != nil {
+ panic(err)
+ }
+ if n != len(data) {
+ panic("not full write")
+ }
+}
+
+func MustReadFile(p string) []byte {
+ data, err := os.ReadFile(p)
+ if err != nil {
+ panic(fmt.Errorf("read %s: %v", p, err))
+ }
+ return data
+}
ai = text ; algorithm identifier
av = {a: ai, v: bytes}
-certificate-load = {
- ? ku: [+ ku],
- exp: validity,
- pub: av,
+cer-load = {
+ ? ku: {+ ku => nil},
+ pub: [+ {av, id: uuid}],
sub: {text => text}, ; subject
? crit: [+ {t: text, * text => any}],
- pkid: uuid, ; public key identifier
* text => any
}
-ku = "ca" / "dh" / "sig" / text
-validity = [since: tai64, till: tai64]
+ku = "ca" / "sig" / "app-name" / text
--- /dev/null
+cer-sig-tbs = {
+ sid: uuid, ; signer's public key id
+ cid: uuid, ; certificate's id
+ exp: validity,
+ * text => any
+}
+
+validity = [since: tai64, till: tai64]
@cindex cer
@section cer format
-Certificate is the prehashed @ref{signed-data} structure with
-@code{/load/t} equals to @code{cer}. @code{/load/v} must contain:
+Certificate is the @ref{signed-data} structure. Its @code{/load/t}
+equals to @code{cer}. @code{/load/v} contains @code{cer-load}:
@verbatiminclude format/cer-load.cddl
-@code{pkid} is a hash calculated over the @code{pub} field and used to
-form UUIDv4. But it may be formed another way, no limitations. Depending
-on @code{pub/a}, that may be different hash like Streebog-256 or
-SHAKE-128 (let's stop using SHA-2!).
+@table @code
-@code{sub} is a subject name. Its values are UTF-8 strings. Currently no
-constraints on what fields must be present.
+@item sub
+Subject is a map of arbitrary strings. Currently no constraints on what
+fields must be present. Each application and usage context defines it on
+his own. But you may mimic X.509's subject with keys like "CN", "C", "O"
+and similar ones.
-@code{exp}iration period @strong{must} contain TAI64 datetime, without
-nanoseconds part.
+@item pub
+Certificate may contain multiple public keys.
-@code{ku} (key usage) contains supposed usage contexts like being CA
-(@code{ca}), or using it solely for either signing (@code{sig}), or
-key-agreement (@code{dh}) purposes. It @strong{must} be absent if empty.
-Its items @strong{must} be length-first sorted (like MAP) and unique.
-Possibly some additional usages like @code{email}, @code{ike},
-@code{codeSign} may be thought out.
+That is @strong{solely} intended for tasks requiring more than single
+key usage. For example @url{http://www.nncpgo.org, NNCP} uses one
+curve25519 for (DH) encryption, one curve25519 for online authentication
+and one ed25519 for signing purposes. All those three keys are used
+together. That certificate's key usage field must contain something like
+"nncp".
-Optional @code{crit} list may contain other critical extensions.
-Non-critical extensions may be placed just inside the map itself.
-It @strong{must} be absent if empty. Extension is a map with expected
-@code{t}ype field containing the identifier of the extension. Other
-extension's keys are defined by its type.
+If your keypair is intended for general purposes like signing of
+arbitrary data, then single public key @strong{should} be used, with a
+key usage like "sig".
-There @strong{must} be single signature and single hash used during
-prehashing.
+Each public key contain the key itself, its algorithm identifier and key
+identifier, that @strong{should} be generated as an UUIDv4 based on the
+hash of the key.
+
+@item ku
+Intended public key usage. Certificate @strong{must} be signed with the
+key having "ca" key usage, unless it is self-signed.
+Application-specific example with multiple public keys is described
+above.
+
+It is a map with NIL values, to force deterministic encoding of the
+list. It @strong{must} be absent if empty.
+
+@item crit
+Optional list of critical (in terms of X.509) extensions. Non-critical
+ones may be placed outside that map, directly in @code{cer-load}. It
+@strong{must} be absent if empty.
+
+Each extension has required "t" field with specified extension type. All
+other values are extension-specific.
+
+@end table
+
+signed-data's sig-tbs @strong{must} contain additional fields:
+
+@verbatiminclude format/cer-sig-tbs.cddl
+
+@table @code
+
+@item sid
+Signing public key identifier.
+
+@item cid
+Certificate's unique identifier. UUIDv7 is a good choice. But it may be
+UUIDv4, or any desired method of generation.
+
+@item exp
+Expiration period of the certificate. It @strong{must} contain TAI64
+datetime (no nanoseconds).
+
+@end table
Example minimal certificate may look like:
@verbatim
{
- "hash": ["streebog256"],
"load": {
"t": "cer",
"v": {
- "exp": [TAI64, TAI64],
- "pub": {"a": "gost3410-256A", "v": 'pubkey'},
+ "pub": [{"a": "gost3410-256A", "v": 'pubkey', "id": UUID(hash(pub))}],
"sub": {"CN": "Test", "O": "Testers"},
- "pkid": UUID(hash(pub)),
},
},
"sigs": [{
"tbs": {
+ "cid": UUID(certificate's id),
"sid": UUID(signer's pkid),
- "hash": {"streebog256": 'hash value'},
+ "exp": [TAI64, TAI64],
},
"sign": {"a": "gost3410-256A", "v": 'signature'},
}],
@subsection cer with GOST R 34.10-2012
Same rules of serialisation must be used as with
-@ref{signed-data-gost3410, signed-data-gost3410}. @code{pkid} and
-@code{cid} should be calculated using Streebog-256 hash.
+@ref{signed-data-gost3410, signed-data-gost3410}. Public key's
+identifier and and @code{cid} should be calculated using Streebog-256
+hash.
@cindex hashed-data
@section hashed-data format
-Integrity protected container, analogue to CMS'es DigestedData structure.
+Integrity protected container, CMS'es DigestedData analogue.
@verbatiminclude format/hashed-data.cddl
@code{/hash} contains the hash values for all corresponding @code{/a}
algorithms.
+
+@node hashed-data-gost3411
+@subsection hashed-data with GOST R 34.11-2012
+
+Streebog must be big-endian serialised.
+
+Following algorithm identifiers are acceptable:
+
+@verbatim
+streebog256
+streebog512
+@end verbatim
-private-key = {
- a: text, ; algorithm identifier
- v: bytes, ; private key's value
-}
+ai = text ; algorithm identifier
+av = {a: ai, v: bytes}
+
+private-key = av
@subsection private-key with GOST R 34.10-2012
Big-endian private key representation must be used.
+
+Following algorithm identifiers are acceptable:
+
+@verbatim
+gost3410-256A
+gost3410-256B
+gost3410-256C
+gost3410-256D
+gost3410-512A
+gost3410-512B
+gost3410-512C
+@end verbatim
sig-tbs = {
t: text, ; = /load/t
- v: '' / any, ; empty string if prehashed, /load/v otherwise
+ v: nil / any,
tbs: map, ; = /sigs/?/tbs
}
? certs: [+ cer],
}
-cer = signed-data ; with /load/t = cer
+cer = signed-data ; with /load/t = cer, /load/v = cer-load
sig = {
- tbs: {
- sid: uuid, ; signer's public key id
- ? hash: {ai => bytes}, ; when using prehashing
- ? when: tai64 / tai64n,
- * text => any
- },
+ tbs: sig-tbs,
sign: av,
? cer-loc: [+ url],
* text => any
}
url = text
+
+sig-tbs = {
+ sid: uuid, ; signer's public key id
+ ? hash: {ai => bytes}, ; when using prehashing
+ ? when: tai64 / tai64n,
+ * text => any
+}
function. Its digest must be big-endian serialised. Public key must be
in @code{BE(X)||BE(Y)} format. Signature is in @code{BE(S)||BE(R)}
format.
+
+Following algorithm identifiers are acceptable for the hash:
+
+@verbatim
+streebog256
+streebog512
+@end verbatim