From 298f162d430a11a0db06952b9539af9bfcf1ce51 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thilo=20Karra=C3=9F?= Date: Fri, 4 Nov 2022 19:38:34 +0100 Subject: [PATCH] Port from heep; initial chekin --- .gitignore | 1 + cert.go | 208 +++++++++++++++++++++++++++++++++++++++++++++++++++ certifier.go | 64 ++++++++++++++++ go.mod | 10 +++ go.sum | 13 ++++ grpc.go | 55 ++++++++++++++ key.go | 62 +++++++++++++++ 7 files changed, 413 insertions(+) create mode 100644 .gitignore create mode 100644 cert.go create mode 100644 certifier.go create mode 100644 go.mod create mode 100644 go.sum create mode 100644 grpc.go create mode 100644 key.go diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..9f11b75 --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +.idea/ diff --git a/cert.go b/cert.go new file mode 100644 index 0000000..cf2180e --- /dev/null +++ b/cert.go @@ -0,0 +1,208 @@ +package certifer + +import ( + "bytes" + "crypto/aes" + "crypto/cipher" + "crypto/sha1" + "crypto/sha256" + "crypto/x509" + "crypto/x509/pkix" + "encoding/binary" + "encoding/gob" + "encoding/pem" + "fmt" + "math/big" + "math/rand" + "net" + "os" + "time" +) + +type Cert struct { + x *certifier + Template *x509.Certificate + Bytes []byte + Key *KeyParameters + Parent *Cert + CA *Cert +} + +func (x *certifier) NewCA() (*Cert, error) { + ca := &x509.Certificate{ + SerialNumber: big.NewInt(1337), + Subject: pkix.Name{ + Organization: []string{x.Orga}, + OrganizationalUnit: []string{"ca"}, + CommonName: "root", + }, + NotBefore: time.Now(), + NotAfter: time.Now().Add(time.Hour * 24 * 365), + IsCA: true, + KeyUsage: x509.KeyUsageCertSign | x509.KeyUsageDigitalSignature, + ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageClientAuth, x509.ExtKeyUsageServerAuth}, + BasicConstraintsValid: true, + // up to this point this should be the bare minimum for a Parent cert + } + + pk := createKeyPair() + + // create certificate. + caBytes, err := x509.CreateCertificate(getRandom(), ca, ca, &pk.PublicKey, pk) + if err != nil { + return nil, err + } + + return &Cert{ + Template: ca, + Bytes: caBytes, + Key: NewKeyParameters(pk), + }, nil +} + +func (c *Cert) NewCert(cn string) (*Cert, error) { + cert := &x509.Certificate{ + SerialNumber: big.NewInt(1337 + int64(rand.Uint64())), + Subject: pkix.Name{ + Organization: []string{c.Template.Subject.Organization[0]}, + OrganizationalUnit: []string{c.x.Thing}, + CommonName: cn, + }, + // DNSNames: []string{"bloq", "localhost"}, + IPAddresses: []net.IP{net.IPv4(127, 0, 0, 1), net.IPv6loopback}, + NotBefore: time.Now(), + NotAfter: time.Now().AddDate(1, 0, 0), + // SubjectKeyId: []byte{1, 2, 3, 4, 6}, + ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageClientAuth, x509.ExtKeyUsageServerAuth}, + KeyUsage: x509.KeyUsageDigitalSignature, + } + + pk := createKeyPair() + + hash := sha1.Sum(pk.PublicKey.X.Bytes()) + cert.SubjectKeyId = hash[:] + + // caBytes, err := x509.CreateCertificate(r, ca, ca, &pk.PublicKey, pk) + certBytes, err := x509.CreateCertificate(getRandom(), cert, c.Template, &pk.PublicKey, c.Key.Key()) + if err != nil { + return nil, err + } + + ca := c.CA + if ca == nil { + ca = c + } + return &Cert{ + Template: cert, + Bytes: certBytes, + Key: NewKeyParameters(pk), + Parent: c, + CA: ca, + }, nil +} + +func (c *Cert) CertAsPem() []byte { + p := pem.EncodeToMemory(&pem.Block{ + Type: "CERTIFICATE", + Bytes: c.Bytes, + }) + return p +} + +func (c *Cert) KeyAsPem() ([]byte, error) { + sec, err := x509.MarshalECPrivateKey(c.Key.Key()) + if err != nil { + return nil, err + } + buf := pem.EncodeToMemory(&pem.Block{ + Type: "EC PRIVATE KEY", + Bytes: sec, + }) + return buf, nil +} + +// WithoutKeys returns a copy of this Cert tree without any private keys. +// Note: The result is NOT a deep copy of the original cert tree. Certificates, Templates and +// byte slices will still point to the original data, so be careful and do not modify! +func (c *Cert) WithoutKeys() *Cert { + ret := c.WithoutParentKeys() + ret.Key = nil + return ret +} + +// WithoutParentKeys retruns a copy of this Cert tree without keys of the parent and ca. +// Note: The result is NOT a deep copy of the original cert tree. Certificates, Templates and +// byte slices will still point to the original data, so be careful and do not modify! +func (c *Cert) WithoutParentKeys() *Cert { + p := c.Parent + if p != nil { + p = p.WithoutKeys() + } + ca := c.CA + if ca != nil { + ca.WithoutKeys() + } + return &Cert{ + Template: c.Template, + Bytes: c.Bytes, + Key: c.Key, + Parent: p, + CA: ca, + } +} + +func (c *Cert) Export(includeKey bool) ([]byte, error) { + exportCert := c + if !includeKey { + exportCert = c.WithoutParentKeys() + } + + buf := &bytes.Buffer{} + tGob := gob.NewEncoder(buf) + if err := tGob.Encode(exportCert); err != nil { + return nil, err + } + blob := buf.Bytes() + bloblen := uint32(len(blob)) + padding := (aes.BlockSize - ((len(blob) + 4) % aes.BlockSize)) % aes.BlockSize + paddedBlob := make([]byte, len(blob)+4+padding) + binary.BigEndian.PutUint32(paddedBlob[0:4], bloblen) + copy(paddedBlob[4:], blob) + //for i := 0; i < padding; i++ { + // use padding spaces. Nul bytes would confuse the json unmarshaller + //paddedBlob[len(paddedBlob)+i] = ' ' + //} + tKeydata := sha256.Sum256([]byte(c.x.key)) // gives 32 bytes, which is a multiple of the block size + tIVdata := tKeydata[:aes.BlockSize] + block, err := aes.NewCipher(tKeydata[:]) + if err != nil { + return nil, err + } + cbc := cipher.NewCBCEncrypter(block, tIVdata) + cbc.CryptBlocks(paddedBlob, paddedBlob) + return paddedBlob, nil +} + +func (c *Cert) ExportToFile(includeKey bool, filename string) error { + cBlob, err := c.Export(includeKey) + if err != nil { + return err + } + if err := os.WriteFile(filename, cBlob, 0600); err != nil { + return err + } + return nil +} + +func (c *Cert) String() string { + ret := "" + if c.Parent != nil { + ret += fmt.Sprintf("%v->", c.Parent) + } + key := "" + if c.Key != nil { + key = "(*)" + } + ret += fmt.Sprintf("[%v%v]", c.Template.Subject, key) + return ret +} diff --git a/certifier.go b/certifier.go new file mode 100644 index 0000000..94ecc23 --- /dev/null +++ b/certifier.go @@ -0,0 +1,64 @@ +package certifer + +import ( + "bytes" + "crypto/aes" + "crypto/cipher" + "crypto/sha256" + "encoding/binary" + "encoding/gob" + "os" +) + +type certifier struct { + key string + + // head / thing / bla ( + // orga / unit / cn + // "abc"/ "ca" / "ca" + + // Orga will be the Organization of newly created CAs + Orga string + + // Thing is the OrgaUnit of newly created Certs + Thing string +} + +func New(key, orga, thing string) *certifier { + return &certifier{ + key: key, + Orga: orga, + Thing: thing, + } +} + +func (x *certifier) Import(data []byte) (*Cert, error) { + blob := make([]byte, len(data)) + copy(blob, data) + + tKeydata := sha256.Sum256([]byte(x.key)) // gives 32 bytes, which is a multiple of the block size + tIVdata := tKeydata[:aes.BlockSize] + block, err := aes.NewCipher(tKeydata[:]) + if err != nil { + return nil, err + } + cbc := cipher.NewCBCDecrypter(block, tIVdata) + cbc.CryptBlocks(blob, blob) + bloblen := binary.BigEndian.Uint32(blob[0:4]) + buf := bytes.NewBuffer(blob[4 : 4+bloblen]) + // should be plain gob now + cert := &Cert{} + tGob := gob.NewDecoder(buf) + if err := tGob.Decode(cert); err != nil { + return nil, err + } + return cert, nil +} + +func (x *certifier) ImportFromFile(filename string) (*Cert, error) { + blob, err := os.ReadFile(filename) + if err != nil { + return nil, err + } + return x.Import(blob) +} diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..6dc4a09 --- /dev/null +++ b/go.mod @@ -0,0 +1,10 @@ +module udico.de/heep/cert + +go 1.19 + +require google.golang.org/grpc v1.50.1 + +require ( + github.com/golang/protobuf v1.5.2 // indirect + google.golang.org/protobuf v1.27.1 // indirect +) diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..771bdca --- /dev/null +++ b/go.sum @@ -0,0 +1,13 @@ +github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= +github.com/golang/protobuf v1.5.2 h1:ROPKBNFfQgOUMifHyP+KYbvpjbdoFNs+aK7DXlji0Tw= +github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= +github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.6 h1:BKbKCqvP6I+rmFHt06ZmyQtvB8xAkWdhFyr0ZUNZcxQ= +golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 h1:go1bK/D/BFZV2I8cIQd1NKEZ+0owSTG1fDTci4IqFcE= +google.golang.org/grpc v1.50.1 h1:DS/BukOZWp8s6p4Dt/tOaJaTQyPyOoCcrjroHuCeLzY= +google.golang.org/grpc v1.50.1/go.mod h1:ZgQEeidpAuNRZ8iRrlBKXZQP1ghovWIVhdJRyCDK+GI= +google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= +google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= +google.golang.org/protobuf v1.27.1 h1:SnqbnDw1V7RiZcXPx5MEeqPv2s79L9i7BJUlG/+RurQ= +google.golang.org/protobuf v1.27.1/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= diff --git a/grpc.go b/grpc.go new file mode 100644 index 0000000..ec09dfa --- /dev/null +++ b/grpc.go @@ -0,0 +1,55 @@ +package certifer + +import ( + "crypto/tls" + "crypto/x509" + "errors" + "google.golang.org/grpc/credentials" +) + +func (c *Cert) GrpcServerConfig() (credentials.TransportCredentials, error) { + if c.CA == nil { + return nil, errors.New("security blob contains no CA") + } + certPool := x509.NewCertPool() + if !certPool.AppendCertsFromPEM(c.CA.CertAsPem()) { + return nil, errors.New("cannot add CA to pool") + } + + tCertPem := c.CertAsPem() + tKeyPem, _ := c.KeyAsPem() + tCert, err := tls.X509KeyPair(tCertPem, tKeyPem) + if err != nil { + return nil, err + } + config := &tls.Config{ + Certificates: []tls.Certificate{tCert}, + ClientAuth: tls.RequireAndVerifyClientCert, + ClientCAs: certPool, + } + creds := credentials.NewTLS(config) + return creds, nil +} + +func (c *Cert) GrpcClientConfig() (credentials.TransportCredentials, error) { + if c.CA == nil { + return nil, errors.New("security blob contains no CA") + } + certPool := x509.NewCertPool() + if !certPool.AppendCertsFromPEM(c.CA.CertAsPem()) { + return nil, errors.New("cannot add CA to pool") + } + + tCertPem := c.CertAsPem() + tKeyPem, _ := c.KeyAsPem() + tCert, err := tls.X509KeyPair(tCertPem, tKeyPem) + if err != nil { + return nil, err + } + config := &tls.Config{ + Certificates: []tls.Certificate{tCert}, + RootCAs: certPool, + } + creds := credentials.NewTLS(config) + return creds, nil +} diff --git a/key.go b/key.go new file mode 100644 index 0000000..b86f94b --- /dev/null +++ b/key.go @@ -0,0 +1,62 @@ +package certifer + +import ( + "crypto/ecdsa" + "crypto/elliptic" + "math/big" + "math/rand" + "time" +) + +const maxCreateKeyRounds = 3 + +var ecdsacurve = elliptic.P256() + +// KeyParameters hold the ecdsa curve parameters of private (and public, despite these are redundand values) keys. +type KeyParameters struct { + D, X, Y *big.Int +} + +// NewKeyParameters extracts key information from a given ecdsa private key and returns a KeyParameters instance. +func NewKeyParameters(key *ecdsa.PrivateKey) *KeyParameters { + return &KeyParameters{ + D: key.D, + X: key.X, + Y: key.Y, + } +} + +// Key creates a new ecdsa.PrivateKey from the given KeyParameters +func (k *KeyParameters) Key() *ecdsa.PrivateKey { + priv := new(ecdsa.PrivateKey) + priv.PublicKey.Curve = ecdsacurve + priv.D = k.D + priv.PublicKey.X, priv.PublicKey.Y = k.X, k.Y + return priv +} + +func createKeyPair() *ecdsa.PrivateKey { + // generate key pair + var pk *ecdsa.PrivateKey + var err error + for i := 0; i < 3; i++ { + r := getRandom() + pk, err = ecdsa.GenerateKey(ecdsacurve, r) + if err != nil { + //log.WithError(err).Error("cannot create key pair") + continue + } + return pk + } + panic(err) +} + +var theRand *rand.Rand = nil + +func getRandom() *rand.Rand { + if theRand == nil { + rnd := rand.NewSource(time.Now().Unix()) + theRand = rand.New(rnd) + } + return theRand +}