aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorJeremy Clerc <jclerc@google.com>2015-09-10 19:23:15 +0200
committerJeremy Clerc <jeremy@clerc.io>2015-09-10 19:23:15 +0200
commitf2487ba1c659998b5792efdf11fc86275ed5dcc9 (patch)
treee01290dec2dfa38f30b56e65a3a331e804d1ca6a
downloadeasypki-f2487ba1c659998b5792efdf11fc86275ed5dcc9.tar.xz
initial commit
-rw-r--r--.gitignore2
-rw-r--r--cmd/easyca/main.go165
-rw-r--r--pkg/easyca/easyca.go142
-rw-r--r--pkg/easyca/serial.go45
4 files changed, 354 insertions, 0 deletions
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..b72f9be
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,2 @@
+*~
+*.swp
diff --git a/cmd/easyca/main.go b/cmd/easyca/main.go
new file mode 100644
index 0000000..4a16172
--- /dev/null
+++ b/cmd/easyca/main.go
@@ -0,0 +1,165 @@
+package main
+
+import (
+ "crypto/x509"
+ "crypto/x509/pkix"
+ "log"
+ "net"
+ "os"
+ "path/filepath"
+ "strings"
+ "time"
+
+ "github.com/codegangsta/cli"
+ "github.com/jeremy-clerc/easyca/pkg/easyca"
+)
+
+// https://access.redhat.com/documentation/en-US/Red_Hat_Certificate_System/8.0/html/Admin_Guide/Standard_X.509_v3_Certificate_Extensions.html
+// B.3.8. keyUsage
+
+func initPki(c *cli.Context) {
+ log.Print("generating new pki structure")
+ err := os.MkdirAll(filepath.Join(c.GlobalString("root"), "private"), 0755)
+ if err != nil {
+ log.Fatalf("creating pki structure %v", err)
+ }
+}
+
+func createBundle(c *cli.Context) {
+ if !c.Args().Present() {
+ cli.ShowSubcommandHelp(c)
+ log.Fatalf("Usage: %v name (common name defaults to name, use --cn and "+
+ "different name if you need multiple certs for same cn)", c.Command.FullName())
+ }
+
+ var filename string
+ commonName := strings.Join(c.Args()[:], " ")
+
+ if len(c.String("filename")) > 0 {
+ filename = c.String("filename")
+ } else {
+ filename = strings.Replace(commonName, " ", "_", -1)
+ filename = strings.Replace(filename, "*", "wildcard", -1)
+ }
+
+ template := &x509.Certificate{
+ Subject: pkix.Name{
+ CommonName: commonName,
+ Organization: c.StringSlice("organization"),
+ Locality: c.StringSlice("locality"),
+ Country: c.StringSlice("country"),
+ Province: c.StringSlice("province"),
+ },
+ NotAfter: time.Now().AddDate(0, 0, c.Int("expire")),
+ }
+
+ if c.Bool("ca") {
+ template.IsCA = true
+ filename = "ca"
+ } else if c.Bool("client") {
+ template.ExtKeyUsage = append(template.ExtKeyUsage, x509.ExtKeyUsageClientAuth)
+ template.EmailAddresses = c.StringSlice("email")
+ } else {
+ // We default to server
+ template.ExtKeyUsage = append(template.ExtKeyUsage, x509.ExtKeyUsageServerAuth)
+
+ IPs := make([]net.IP, 0, len(c.StringSlice("ip")))
+ for _, ipStr := range c.StringSlice("ip") {
+ if i := net.ParseIP(ipStr); i != nil {
+ IPs = append(IPs, i)
+ }
+ }
+ template.IPAddresses = IPs
+ template.DNSNames = c.StringSlice("dns")
+ }
+ err := easyca.GenerateCertifcate(c.GlobalString("root"), filename, template)
+ if err != nil {
+ log.Fatal(err)
+ }
+}
+
+func parseArgs() {
+ app := cli.NewApp()
+ app.Name = "easypki"
+ app.Usage = "Manage pki"
+ app.Author = "Jeremy Clerc"
+ app.Email = "jeremy@clerc.io"
+ app.Version = "0.0.1"
+
+ app.Flags = []cli.Flag{
+ cli.StringFlag{
+ Name: "root",
+ Value: filepath.Join(os.Getenv("PWD"), "pki_auto_generated_dir"),
+ Usage: "path to pki root directory",
+ EnvVar: "PKI_ROOT",
+ },
+ }
+ app.Commands = []cli.Command{
+ {
+ Name: "init",
+ Usage: "create directory structure",
+ Action: initPki,
+ },
+ {
+ Name: "create",
+ Usage: "create COMMON NAME",
+ Description: "create private key + cert signed by CA",
+ Action: createBundle,
+ Flags: []cli.Flag{
+ cli.BoolFlag{
+ Name: "ca",
+ Usage: "certificate authority",
+ },
+ cli.BoolFlag{
+ Name: "client",
+ Usage: "generate a client certificate (default is server)",
+ },
+ cli.IntFlag{
+ Name: "expire",
+ Usage: "expiration limit in days",
+ Value: 365,
+ },
+ cli.StringFlag{
+ Name: "filename",
+ Usage: "filename for bundle, use when you generate multiple certs for same cn",
+ },
+ cli.StringSliceFlag{
+ Name: "organization",
+ EnvVar: "PKI_ORGANIZATION",
+ },
+ cli.StringSliceFlag{
+ Name: "locality",
+ EnvVar: "PKI_LOCALITY",
+ },
+ cli.StringSliceFlag{
+ Name: "country",
+ EnvVar: "PKI_COUNTRY",
+ Usage: "Country name, 2 letter code",
+ },
+ cli.StringSliceFlag{
+ Name: "province",
+ Usage: "province/state",
+ EnvVar: "PKI_PROVINCE",
+ },
+ cli.StringSliceFlag{
+ Name: "dns, d",
+ Usage: "dns alt names",
+ },
+ cli.StringSliceFlag{
+ Name: "ip, i",
+ Usage: "IP alt names",
+ },
+ cli.StringSliceFlag{
+ Name: "email, e",
+ Usage: "Email alt names",
+ },
+ },
+ },
+ }
+
+ app.Run(os.Args)
+}
+
+func main() {
+ parseArgs()
+}
diff --git a/pkg/easyca/easyca.go b/pkg/easyca/easyca.go
new file mode 100644
index 0000000..be77ae6
--- /dev/null
+++ b/pkg/easyca/easyca.go
@@ -0,0 +1,142 @@
+package easyca
+
+import (
+ "crypto/rand"
+ "crypto/rsa"
+ "crypto/sha1"
+ "crypto/x509"
+ "encoding/asn1"
+ "encoding/pem"
+ "fmt"
+ "io/ioutil"
+ "math/big"
+ "os"
+ "path/filepath"
+ "time"
+)
+
+func GeneratePrivateKey(path string) (*rsa.PrivateKey, error) {
+ keyFile, err := os.Create(path)
+ if err != nil {
+ return nil, fmt.Errorf("create %v: %v", path, err)
+ }
+ defer keyFile.Close()
+
+ key, err := rsa.GenerateKey(rand.Reader, 2048)
+ if err != nil {
+ return nil, fmt.Errorf("generate private key: %v", err)
+ }
+ err = pem.Encode(keyFile, &pem.Block{
+ Type: "RSA PRIVATE KEY",
+ Bytes: x509.MarshalPKCS1PrivateKey(key),
+ })
+ if err != nil {
+ return nil, fmt.Errorf("pem encode private key: %v", err)
+ }
+ return key, nil
+}
+
+func GenerateCertifcate(pkiroot, name string, template *x509.Certificate) error {
+ // TODO(jclerc): check that pki has been init
+
+ privateKeyPath := filepath.Join(pkiroot, "private", name+".key")
+ crtPath := filepath.Join(pkiroot, name+".crt")
+
+ var caCrt *x509.Certificate
+ var caKey *rsa.PrivateKey
+
+ if _, err := os.Stat(privateKeyPath); err == nil {
+ return fmt.Errorf("a key pair for %v already exists", name)
+ }
+
+ privateKey, err := GeneratePrivateKey(privateKeyPath)
+ if err != nil {
+ return fmt.Errorf("generate private key: %v", err)
+ }
+
+ publicKeyBytes, err := asn1.Marshal(*privateKey.Public().(*rsa.PublicKey))
+ if err != nil {
+ return fmt.Errorf("marshal public key: %v", err)
+ }
+ subjectKeyId := sha1.Sum(publicKeyBytes)
+ template.SubjectKeyId = subjectKeyId[:]
+
+ template.NotBefore = time.Now()
+ template.SignatureAlgorithm = x509.SHA256WithRSA
+ if template.IsCA {
+ serialNumberLimit := new(big.Int).Lsh(big.NewInt(1), 128)
+ serialNumber, err := rand.Int(rand.Reader, serialNumberLimit)
+ if err != nil {
+ return fmt.Errorf("failed to generate ca serial number: %s", err)
+ }
+ template.SerialNumber = serialNumber
+ template.KeyUsage = x509.KeyUsageCertSign | x509.KeyUsageCRLSign
+ template.BasicConstraintsValid = true
+ template.Issuer = template.Subject
+ template.AuthorityKeyId = template.SubjectKeyId
+
+ caCrt = template
+ caKey = privateKey
+ } else {
+ template.KeyUsage = x509.KeyUsageDigitalSignature | x509.KeyUsageKeyEncipherment
+ serialNumber, err := NextSerial(pkiroot)
+ if err != nil {
+ return fmt.Errorf("get next serial: %v", err)
+ }
+ template.SerialNumber = big.NewInt(serialNumber)
+
+ caCrt, caKey, err = GetCA(pkiroot)
+ if err != nil {
+ return fmt.Errorf("get ca: %v", err)
+ }
+ }
+
+ crt, err := x509.CreateCertificate(rand.Reader, template, caCrt, privateKey.Public(), caKey)
+ if err != nil {
+ return fmt.Errorf("create certificate: %v", err)
+ }
+
+ crtFile, err := os.Create(crtPath)
+ if err != nil {
+ return fmt.Errorf("create %v: %v", crtPath, err)
+ }
+ defer crtFile.Close()
+
+ err = pem.Encode(crtFile, &pem.Block{
+ Type: "CERTIFICATE",
+ Bytes: crt,
+ })
+ if err != nil {
+ return fmt.Errorf("pem encode crt: %v", err)
+ }
+
+ return nil
+}
+
+func GetCA(pkiroot string) (*x509.Certificate, *rsa.PrivateKey, error) {
+ caKeyBytes, err := ioutil.ReadFile(filepath.Join(pkiroot, "private", "ca.key"))
+ if err != nil {
+ return nil, nil, fmt.Errorf("read ca private key: %v", err)
+ }
+ p, _ := pem.Decode(caKeyBytes)
+ if p == nil {
+ return nil, nil, fmt.Errorf("pem decode did not found pem encoded ca private key")
+ }
+ caKey, err := x509.ParsePKCS1PrivateKey(p.Bytes)
+ if err != nil {
+ return nil, nil, fmt.Errorf("parse ca private key: %v", err)
+ }
+ caCrtBytes, err := ioutil.ReadFile(filepath.Join(pkiroot, "ca.crt"))
+ if err != nil {
+ return nil, nil, fmt.Errorf("read ca crt: %v", err)
+ }
+ p, _ = pem.Decode(caCrtBytes)
+ if p == nil {
+ return nil, nil, fmt.Errorf("pem decode did not found pem encoded ca cert")
+ }
+ caCrt, err := x509.ParseCertificate(p.Bytes)
+ if err != nil {
+ return nil, nil, fmt.Errorf("parse ca crt: %v", err)
+ }
+ return caCrt, caKey, nil
+}
diff --git a/pkg/easyca/serial.go b/pkg/easyca/serial.go
new file mode 100644
index 0000000..aded542
--- /dev/null
+++ b/pkg/easyca/serial.go
@@ -0,0 +1,45 @@
+package easyca
+
+import (
+ "fmt"
+ "io/ioutil"
+ "os"
+ "path/filepath"
+ "strconv"
+ "strings"
+)
+
+func NextSerial(pkiroot string) (int64, error) {
+ var serial int64
+ f, err := os.OpenFile(filepath.Join(pkiroot, "serial"), os.O_RDWR|os.O_CREATE, 0644)
+ if err != nil {
+ return 0, err
+ }
+ defer f.Close()
+ out, err := ioutil.ReadAll(f)
+ if err != nil {
+ return 0, err
+ }
+ if len(out) == 0 {
+ serial = 1
+ } else {
+ // If serial file is edited manually, it will probably get \n or \r\n
+ // We make sure to clean the unwanted characters
+ serial, err = strconv.ParseInt(strings.TrimSpace(string(out)), 10, 64)
+ if err != nil {
+ return 0, err
+ }
+ serial += 1
+ }
+
+ f.Seek(0, 0)
+ written, err := fmt.Fprint(f, serial)
+ if err != nil {
+ return 0, err
+ }
+ if written == 0 {
+ return 0, fmt.Errorf("wanted to write %s to serial file, no byte written", written)
+ }
+
+ return serial, nil
+}