From f2487ba1c659998b5792efdf11fc86275ed5dcc9 Mon Sep 17 00:00:00 2001 From: Jeremy Clerc Date: Thu, 10 Sep 2015 19:23:15 +0200 Subject: initial commit --- .gitignore | 2 + cmd/easyca/main.go | 165 +++++++++++++++++++++++++++++++++++++++++++++++++++ pkg/easyca/easyca.go | 142 ++++++++++++++++++++++++++++++++++++++++++++ pkg/easyca/serial.go | 45 ++++++++++++++ 4 files changed, 354 insertions(+) create mode 100644 .gitignore create mode 100644 cmd/easyca/main.go create mode 100644 pkg/easyca/easyca.go create mode 100644 pkg/easyca/serial.go 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 +} -- cgit v1.3