From 5ceea8b7821646f4e43b7d6c37845c6501c402b8 Mon Sep 17 00:00:00 2001 From: Jeremy Clerc Date: Sun, 13 Sep 2015 19:50:13 +0200 Subject: rename easyca to easypki --- README.md | 18 +- cmd/easyca/main.go | 229 ---------------------- cmd/easypki/main.go | 229 ++++++++++++++++++++++ pkg/easyca/easyca.go | 466 --------------------------------------------- pkg/easyca/easyca_test.go | 144 -------------- pkg/easypki/easyca.go | 466 +++++++++++++++++++++++++++++++++++++++++++++ pkg/easypki/easyca_test.go | 144 ++++++++++++++ 7 files changed, 848 insertions(+), 848 deletions(-) delete mode 100644 cmd/easyca/main.go create mode 100644 cmd/easypki/main.go delete mode 100644 pkg/easyca/easyca.go delete mode 100644 pkg/easyca/easyca_test.go create mode 100644 pkg/easypki/easyca.go create mode 100644 pkg/easypki/easyca_test.go diff --git a/README.md b/README.md index 949465d..1de1bd3 100644 --- a/README.md +++ b/README.md @@ -1,13 +1,13 @@ -easyca +easypki ====== -Easyca attempts to make managing a Certificate Authority very easy. +Easypki attempts to make managing a Certificate Authority very easy. Serial, index, etc, are formatted in a way to be compatible with openssl, -so you can use openssl for commands not implemented by easyca. +so you can use openssl for commands not implemented by easypki. # Usage -Easyca usage is straighforward: +Easypki usage is straighforward: 1. Init the directory you will use 2. Create the CA @@ -25,28 +25,28 @@ export PKI_PROVINCE="New York" ``` Before being able to create you certificates, you need to `init` the root directory. -It creates files and directories required by easyca. +It creates files and directories required by easypki. ``` mkdir $PKI_ROOT -easyca init +easypki init ``` Args passed to create make the Common Name, here: "Umbrella Corp Global Authority" ``` -easyca create --ca Umbrella Corp Global Authority +easypki create --ca Umbrella Corp Global Authority ``` Then you can choose between server and client certificate, by default server is implied, to generate a client certificate add `--client` Generate a wildcard certificate for your web apps: ``` -easyca create --dns "*.umbrella.com" *.umbrella.com +easypki create --dns "*.umbrella.com" *.umbrella.com ``` Another example, a certificate for wiki and www: ``` -easyca create --dns "www.umbrella.com" --dns "wiki.umbrella.com" www.umbrella.com +easypki create --dns "www.umbrella.com" --dns "wiki.umbrella.com" www.umbrella.com ``` For more info about available flags, checkout out the help `-h` diff --git a/cmd/easyca/main.go b/cmd/easyca/main.go deleted file mode 100644 index 5efa271..0000000 --- a/cmd/easyca/main.go +++ /dev/null @@ -1,229 +0,0 @@ -// Copyright 2015 Google Inc. -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -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") - if err := easyca.GeneratePKIStructure(c.GlobalString("root")); err != nil { - log.Fatalf("generate 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()) - } - - commonName := strings.Join(c.Args()[:], " ") - var filename string - if filename = c.String("filename"); len(filename) == 0 { - filename = strings.Replace(commonName, " ", "_", -1) - filename = strings.Replace(filename, "*", "wildcard", -1) - } - - subject := pkix.Name{CommonName: commonName} - if str := c.String("organization"); len(str) > 0 { - subject.Organization = []string{str} - } - if str := c.String("locality"); len(str) > 0 { - subject.Locality = []string{str} - } - if str := c.String("country"); len(str) > 0 { - subject.Country = []string{str} - } - if str := c.String("province"); len(str) > 0 { - subject.Province = []string{str} - } - if str := c.String("organizational-unit"); len(str) > 0 { - subject.OrganizationalUnit = []string{str} - } - - template := &x509.Certificate{ - Subject: subject, - 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 revoke(c *cli.Context) { - if !c.Args().Present() { - cli.ShowSubcommandHelp(c) - log.Fatalf("Usage: %v path/to/cert.crt", c.Command.FullName()) - } - crtPath := c.Args().First() - crt, err := easyca.GetCertificate(crtPath) - if err != nil { - log.Fatalf("get certificate (%v): %v", crtPath, err) - } - err = easyca.RevokeSerial(c.GlobalString("root"), crt.SerialNumber) - if err != nil { - log.Fatalf("revoke serial %X: %v", crt.SerialNumber, err) - } -} - -func gencrl(c *cli.Context) { - if err := easyca.GenCRL(c.GlobalString("root"), c.Int("expire")); err != nil { - log.Fatalf("general crl: %v", 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", - Description: "create directory structure", - Action: initPki, - }, - { - Name: "revoke", - Usage: "revoke path/to/cert", - Description: "revoke certificate", - Action: revoke, - }, - { - Name: "gencrl", - Description: "generate certificate revocation list", - Action: gencrl, - Flags: []cli.Flag{ - cli.IntFlag{ - Name: "expire", - Usage: "expiration limit in days", - Value: 30, - }, - }, - }, - { - 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.StringFlag{ - Name: "organization", - EnvVar: "PKI_ORGANIZATION", - }, - cli.StringFlag{ - Name: "organizational-unit", - EnvVar: "PKI_ORGANIZATIONAL_UNIT", - }, - cli.StringFlag{ - Name: "locality", - EnvVar: "PKI_LOCALITY", - }, - cli.StringFlag{ - Name: "country", - EnvVar: "PKI_COUNTRY", - Usage: "Country name, 2 letter code", - }, - cli.StringFlag{ - 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/cmd/easypki/main.go b/cmd/easypki/main.go new file mode 100644 index 0000000..6f55311 --- /dev/null +++ b/cmd/easypki/main.go @@ -0,0 +1,229 @@ +// Copyright 2015 Google Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package main + +import ( + "crypto/x509" + "crypto/x509/pkix" + "log" + "net" + "os" + "path/filepath" + "strings" + "time" + + "github.com/codegangsta/cli" + "github.com/jeremy-clerc/easypki/pkg/easypki" +) + +// 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") + if err := easypki.GeneratePKIStructure(c.GlobalString("root")); err != nil { + log.Fatalf("generate 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()) + } + + commonName := strings.Join(c.Args()[:], " ") + var filename string + if filename = c.String("filename"); len(filename) == 0 { + filename = strings.Replace(commonName, " ", "_", -1) + filename = strings.Replace(filename, "*", "wildcard", -1) + } + + subject := pkix.Name{CommonName: commonName} + if str := c.String("organization"); len(str) > 0 { + subject.Organization = []string{str} + } + if str := c.String("locality"); len(str) > 0 { + subject.Locality = []string{str} + } + if str := c.String("country"); len(str) > 0 { + subject.Country = []string{str} + } + if str := c.String("province"); len(str) > 0 { + subject.Province = []string{str} + } + if str := c.String("organizational-unit"); len(str) > 0 { + subject.OrganizationalUnit = []string{str} + } + + template := &x509.Certificate{ + Subject: subject, + 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 := easypki.GenerateCertifcate(c.GlobalString("root"), filename, template) + if err != nil { + log.Fatal(err) + } +} +func revoke(c *cli.Context) { + if !c.Args().Present() { + cli.ShowSubcommandHelp(c) + log.Fatalf("Usage: %v path/to/cert.crt", c.Command.FullName()) + } + crtPath := c.Args().First() + crt, err := easypki.GetCertificate(crtPath) + if err != nil { + log.Fatalf("get certificate (%v): %v", crtPath, err) + } + err = easypki.RevokeSerial(c.GlobalString("root"), crt.SerialNumber) + if err != nil { + log.Fatalf("revoke serial %X: %v", crt.SerialNumber, err) + } +} + +func gencrl(c *cli.Context) { + if err := easypki.GenCRL(c.GlobalString("root"), c.Int("expire")); err != nil { + log.Fatalf("general crl: %v", 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", + Description: "create directory structure", + Action: initPki, + }, + { + Name: "revoke", + Usage: "revoke path/to/cert", + Description: "revoke certificate", + Action: revoke, + }, + { + Name: "gencrl", + Description: "generate certificate revocation list", + Action: gencrl, + Flags: []cli.Flag{ + cli.IntFlag{ + Name: "expire", + Usage: "expiration limit in days", + Value: 30, + }, + }, + }, + { + 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.StringFlag{ + Name: "organization", + EnvVar: "PKI_ORGANIZATION", + }, + cli.StringFlag{ + Name: "organizational-unit", + EnvVar: "PKI_ORGANIZATIONAL_UNIT", + }, + cli.StringFlag{ + Name: "locality", + EnvVar: "PKI_LOCALITY", + }, + cli.StringFlag{ + Name: "country", + EnvVar: "PKI_COUNTRY", + Usage: "Country name, 2 letter code", + }, + cli.StringFlag{ + 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 deleted file mode 100644 index 93c0662..0000000 --- a/pkg/easyca/easyca.go +++ /dev/null @@ -1,466 +0,0 @@ -// Copyright 2015 Google Inc. -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -package easyca - -import ( - "bufio" - "crypto/rand" - "crypto/rsa" - "crypto/sha1" - "crypto/x509" - "crypto/x509/pkix" - "encoding/asn1" - "encoding/pem" - "fmt" - "io/ioutil" - "math/big" - "os" - "path/filepath" - "regexp" - "strings" - "time" -) - -var ( - // Index format - // 0 full string - // 1 Valid/Revoked/Expired - // 2 Expiration date - // 3 Revocation date - // 4 Serial - // 5 Filename - // 6 Subject - indexRegexp = regexp.MustCompile("^(V|R|E)\t([0-9]{12}Z)\t([0-9]{12}Z)?\t([0-9a-fA-F]{2,})\t([^\t]+)\t(.+)") -) - -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 - - var crtPath string - privateKeyPath := filepath.Join(pkiroot, "private", name+".key") - if name == "ca" { - crtPath = filepath.Join(pkiroot, name+".crt") - } else { - crtPath = filepath.Join(pkiroot, "issued", 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 := NextNumber(pkiroot, "serial") - if err != nil { - return fmt.Errorf("get next serial: %v", err) - } - template.SerialNumber = 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) - } - - // I do not think we have to write the ca.crt in the index - if !template.IsCA { - WriteIndex(pkiroot, name, template) - if err != nil { - return fmt.Errorf("write index: %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 -} - -func GenCRL(pkiroot string, expire int) error { - var revokedCerts []pkix.RevokedCertificate - f, err := os.Open(filepath.Join(pkiroot, "index.txt")) - if err != nil { - return err - } - defer f.Close() - - scanner := bufio.NewScanner(f) - for scanner.Scan() { - matches := indexRegexp.FindStringSubmatch(scanner.Text()) - if len(matches) != 7 { - return fmt.Errorf("wrong line format %v elems: %v, %v", len(matches), matches, scanner.Text()) - } - if matches[1] != "R" { - continue - } - - crt, err := GetCertificate(filepath.Join(pkiroot, "issued", matches[5])) - if err != nil { - return fmt.Errorf("get certificate %v: %v", matches[5], err) - } - - matchedSerial := big.NewInt(0) - fmt.Sscanf(matches[4], "%X", matchedSerial) - if matchedSerial.Cmp(crt.SerialNumber) != 0 { - return fmt.Errorf("serial in index does not match revoked certificate: %v", matches[0]) - } - revocationTime, err := time.Parse("060102150405", strings.TrimSuffix(matches[3], "Z")) - if err != nil { - return fmt.Errorf("parse revocation time: %v", err) - } - revokedCerts = append(revokedCerts, pkix.RevokedCertificate{ - SerialNumber: crt.SerialNumber, - RevocationTime: revocationTime, - Extensions: crt.Extensions, - }) - } - caCrt, caKey, err := GetCA(pkiroot) - if err != nil { - return fmt.Errorf("get ca: %v", err) - } - crl, err := caCrt.CreateCRL(rand.Reader, caKey, revokedCerts, time.Now(), time.Now().AddDate(0, 0, expire)) - if err != nil { - return fmt.Errorf("create crl: %v", err) - } - // I do no see where we can pass it to CreateCRL, differs from openssl - crlNumber, err := NextNumber(pkiroot, "crlnumber") - if err != nil { - return fmt.Errorf("get next serial: %v", err) - } - - serialHexa := fmt.Sprintf("%X", crlNumber) - if len(serialHexa)%2 == 1 { - serialHexa = "0" + serialHexa - } - - crlPath := filepath.Join(pkiroot, "crl-"+serialHexa+".pem") - crlFile, err := os.Create(crlPath) - if err != nil { - return fmt.Errorf("create %v: %v", crlPath, err) - } - defer crlFile.Close() - - err = pem.Encode(crlFile, &pem.Block{ - Type: "X509 CRL", - Bytes: crl, - }) - if err != nil { - return fmt.Errorf("pem encode crt: %v", err) - } - - return nil -} - -func RevokeSerial(pkiroot string, serial *big.Int) error { - f, err := os.OpenFile(filepath.Join(pkiroot, "index.txt"), os.O_RDWR, 0644) - if err != nil { - return err - } - defer f.Close() - - var lines []string - scanner := bufio.NewScanner(f) - for scanner.Scan() { - matches := indexRegexp.FindStringSubmatch(scanner.Text()) - if len(matches) != 7 { - return fmt.Errorf("wrong line format") - } - matchedSerial := big.NewInt(0) - fmt.Sscanf(matches[4], "%X", matchedSerial) - if matchedSerial.Cmp(serial) == 0 { - if matches[1] == "R" { - return fmt.Errorf("certificate already revoked") - } else if matches[1] == "E" { - return fmt.Errorf("certificate already expired") - } - - lines = append(lines, fmt.Sprintf("R\t%v\t%vZ\t%v\t%v\t%v", - matches[2], - time.Now().UTC().Format("060102150405"), - matches[4], - matches[5], - matches[6])) - } else { - lines = append(lines, matches[0]) - } - } - - f.Truncate(0) - f.Seek(0, 0) - - for _, line := range lines { - n, err := fmt.Fprintln(f, line) - if err != nil { - return fmt.Errorf("write line: %v", err) - } - if n == 0 { - return fmt.Errorf("supposed to write [%v], written 0 bytes", line) - } - } - return nil -} - -func GetCertificate(path string) (*x509.Certificate, error) { - crtBytes, err := ioutil.ReadFile(path) - if err != nil { - return nil, fmt.Errorf("read crt: %v", err) - } - p, _ := pem.Decode(crtBytes) - if p == nil { - return nil, fmt.Errorf("pem decode did not found pem encoded cert") - } - crt, err := x509.ParseCertificate(p.Bytes) - if err != nil { - return nil, fmt.Errorf("parse crt: %v", err) - } - - return crt, nil -} - -func WriteIndex(pkiroot, filename string, crt *x509.Certificate) error { - f, err := os.OpenFile(filepath.Join(pkiroot, "index.txt"), os.O_WRONLY|os.O_APPEND, 0644) - if err != nil { - return err - } - defer f.Close() - - serialOutput := fmt.Sprintf("%X", crt.SerialNumber) - // For compatibility with openssl we need an even length - if len(serialOutput)%2 == 1 { - serialOutput = "0" + serialOutput - } - - // Date format: yymmddHHMMSSZ - // E|R|VExpiry[RevocationDate]SerialfilenameSubjectDN - var subject string - if strs := crt.Subject.Country; len(strs) == 1 { - subject += "/C=" + strs[0] - } - if strs := crt.Subject.Organization; len(strs) == 1 { - subject += "/O=" + strs[0] - } - if strs := crt.Subject.OrganizationalUnit; len(strs) == 1 { - subject += "/OU=" + strs[0] - } - if strs := crt.Subject.Locality; len(strs) == 1 { - subject += "/L=" + strs[0] - } - if strs := crt.Subject.Province; len(strs) == 1 { - subject += "/ST=" + strs[0] - } - subject += "/CN=" + crt.Subject.CommonName - - n, err := fmt.Fprintf(f, "V\t%vZ\t\t%v\t%v.crt\t%v\n", - crt.NotAfter.UTC().Format("060102150405"), - serialOutput, - filename, - subject) - if err != nil { - return err - } - if n == 0 { - return fmt.Errorf("written 0 bytes in index file") - } - return nil -} - -// |-ca.crt -// |-crlnumber -// |-index.txt -// |-index.txt.attr -// |-serial -// |-issued/ -// |- name.crt -// |-private -// |- ca.key -// |- name.key -func GeneratePKIStructure(pkiroot string) error { - - for _, dir := range []string{"private", "issued"} { - err := os.Mkdir(filepath.Join(pkiroot, dir), 0755) - if err != nil { - return fmt.Errorf("creating dir %v: %v", dir, err) - } - } - - files := []struct { - Name string - Content string - File *os.File - }{ - {Name: "serial", Content: "01"}, - {Name: "crlnumber", Content: "01"}, - {Name: "index.txt", Content: ""}, - {Name: "index.txt.attr", Content: "unique_subject = no"}, - } - for _, f := range files { - // if using := here i get needs identifier, hm ?, needs to declare err before - var err error - f.File, err = os.Create(filepath.Join(pkiroot, f.Name)) - if err != nil { - return fmt.Errorf("create %v: %v", f.Name, err) - } - defer f.File.Close() - - if len(f.Content) == 0 { - continue - } - - n, err := fmt.Fprintln(f.File, f.Content) - if err != nil { - return fmt.Errorf("write %v: %v", f.Name, err) - } - if n == 0 { - return fmt.Errorf("write %v, written 0 bytes", f.Name) - } - } - - return nil -} - -func NextNumber(pkiroot, name string) (*big.Int, error) { - serial := big.NewInt(0) - - f, err := os.OpenFile(filepath.Join(pkiroot, name), os.O_RDWR, 0644) - if err != nil { - return nil, err - } - defer f.Close() - - n, err := fmt.Fscanf(f, "%X\n", serial) - if err != nil { - return nil, err - } - if n != 1 { - return nil, fmt.Errorf("supposed to read 1 element, read: %v", n) - } - - next := big.NewInt(1) - next.Add(serial, next) - output := fmt.Sprintf("%X", next) - // For compatibility with openssl we need an even length - if len(output)%2 == 1 { - output = "0" + output - } - f.Truncate(0) - f.Seek(0, 0) - - n, err = fmt.Fprintln(f, output) - if err != nil { - return nil, err - } - if n == 0 { - return nil, fmt.Errorf("supposed to write 1 element, written: %v", n) - } - - return serial, nil -} diff --git a/pkg/easyca/easyca_test.go b/pkg/easyca/easyca_test.go deleted file mode 100644 index 176b5fb..0000000 --- a/pkg/easyca/easyca_test.go +++ /dev/null @@ -1,144 +0,0 @@ -// Copyright 2015 Google Inc. -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -package easyca - -import ( - "io/ioutil" - "math/big" - "math/rand" - "os" - "path/filepath" - "testing" - "time" -) - -func TestGeneratePKIStructure(t *testing.T) { - rand.Seed(time.Now().UnixNano()) - - pkiroot, err := ioutil.TempDir("", "gotestpki") - if err != nil { - t.Fatalf("failed to create temp dir: %v", err) - } - - if err := GeneratePKIStructure(pkiroot); err != nil { - t.Fatalf("%v", err) - } - - // We should check the minimum content also.. - toCheck := []struct { - Name string - Dir bool - Content string - }{ - {"private", true, ""}, - {"issued", true, ""}, - {"serial", false, "01\n"}, - {"crlnumber", false, "01\n"}, - {"index.txt", false, ""}, - {"index.txt.attr", false, "unique_subject = no\n"}, - } - - for _, name := range toCheck { - fd, err := os.Stat(filepath.Join(pkiroot, name.Name)) - if err != nil { - t.Errorf("%v: %v", name.Name, err) - } - if name.Dir && !fd.IsDir() { - t.Errorf("%v supposed to be a directory", name.Name) - } - if len(name.Content) > 0 { - f, err := os.Open(filepath.Join(pkiroot, name.Name)) - if err != nil { - t.Fatalf("failed open %v: %v", name.Name, err) - } - defer f.Close() - bytes, err := ioutil.ReadAll(f) - if err != nil { - t.Fatalf("failed read %v: %v", name.Name, err) - } - if string(bytes) != name.Content { - t.Fatalf("%v content expected %v, got: %v", name.Name, name.Content, string(bytes)) - } - } - } - if err := os.RemoveAll(pkiroot); err != nil { - t.Logf("failed cleaning tmp dir %v: %v", pkiroot, err) - } -} - -func TestNextNumber(t *testing.T) { - pkiroot, err := ioutil.TempDir("", "gotestpki") - if err != nil { - t.Fatalf("failed to create temp dir: %v", err) - } - - if err := GeneratePKIStructure(pkiroot); err != nil { - t.Fatalf("generate pki structure: %v", err) - } - - n, err := NextNumber(pkiroot, "serial") - if err != nil { - t.Fatal("failed get next serial number: %v", err) - } - if big.NewInt(1).Cmp(n) != 0 { - t.Fatalf("after init serial is supposed to be 1, value is: %v", n) - } - // File content is now 02 - f, err := os.Open(filepath.Join(pkiroot, "serial")) - if err != nil { - t.Fatalf("failed open serial: %v", err) - } - defer f.Close() - bytes, err := ioutil.ReadAll(f) - if err != nil { - t.Fatalf("failed read serial: %v", err) - } - if string(bytes) != "02\n" { - t.Fatalf("serial content expected 02, got: %v", string(bytes)) - } -} - -func TestLargeNextNumber(t *testing.T) { - pkiroot, err := ioutil.TempDir("", "gotestpki") - if err != nil { - t.Fatalf("failed to create temp dir: %v", err) - } - - if err := GeneratePKIStructure(pkiroot); err != nil { - t.Fatalf("generate pki structure: %v", err) - } - - for { - n, err := NextNumber(pkiroot, "serial") - if err != nil { - t.Fatal("failed get next serial number: %v", err) - } - if big.NewInt(255).Cmp(n) == 0 { - break - } - } - f, err := os.Open(filepath.Join(pkiroot, "serial")) - if err != nil { - t.Fatalf("failed open serial: %v", err) - } - defer f.Close() - bytes, err := ioutil.ReadAll(f) - if err != nil { - t.Fatalf("failed read serial: %v", err) - } - if string(bytes) != "0100\n" { - t.Fatalf("serial content expected 0100, got: %v", string(bytes)) - } -} diff --git a/pkg/easypki/easyca.go b/pkg/easypki/easyca.go new file mode 100644 index 0000000..0c908d2 --- /dev/null +++ b/pkg/easypki/easyca.go @@ -0,0 +1,466 @@ +// Copyright 2015 Google Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package easypki + +import ( + "bufio" + "crypto/rand" + "crypto/rsa" + "crypto/sha1" + "crypto/x509" + "crypto/x509/pkix" + "encoding/asn1" + "encoding/pem" + "fmt" + "io/ioutil" + "math/big" + "os" + "path/filepath" + "regexp" + "strings" + "time" +) + +var ( + // Index format + // 0 full string + // 1 Valid/Revoked/Expired + // 2 Expiration date + // 3 Revocation date + // 4 Serial + // 5 Filename + // 6 Subject + indexRegexp = regexp.MustCompile("^(V|R|E)\t([0-9]{12}Z)\t([0-9]{12}Z)?\t([0-9a-fA-F]{2,})\t([^\t]+)\t(.+)") +) + +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 + + var crtPath string + privateKeyPath := filepath.Join(pkiroot, "private", name+".key") + if name == "ca" { + crtPath = filepath.Join(pkiroot, name+".crt") + } else { + crtPath = filepath.Join(pkiroot, "issued", 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 := NextNumber(pkiroot, "serial") + if err != nil { + return fmt.Errorf("get next serial: %v", err) + } + template.SerialNumber = 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) + } + + // I do not think we have to write the ca.crt in the index + if !template.IsCA { + WriteIndex(pkiroot, name, template) + if err != nil { + return fmt.Errorf("write index: %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 +} + +func GenCRL(pkiroot string, expire int) error { + var revokedCerts []pkix.RevokedCertificate + f, err := os.Open(filepath.Join(pkiroot, "index.txt")) + if err != nil { + return err + } + defer f.Close() + + scanner := bufio.NewScanner(f) + for scanner.Scan() { + matches := indexRegexp.FindStringSubmatch(scanner.Text()) + if len(matches) != 7 { + return fmt.Errorf("wrong line format %v elems: %v, %v", len(matches), matches, scanner.Text()) + } + if matches[1] != "R" { + continue + } + + crt, err := GetCertificate(filepath.Join(pkiroot, "issued", matches[5])) + if err != nil { + return fmt.Errorf("get certificate %v: %v", matches[5], err) + } + + matchedSerial := big.NewInt(0) + fmt.Sscanf(matches[4], "%X", matchedSerial) + if matchedSerial.Cmp(crt.SerialNumber) != 0 { + return fmt.Errorf("serial in index does not match revoked certificate: %v", matches[0]) + } + revocationTime, err := time.Parse("060102150405", strings.TrimSuffix(matches[3], "Z")) + if err != nil { + return fmt.Errorf("parse revocation time: %v", err) + } + revokedCerts = append(revokedCerts, pkix.RevokedCertificate{ + SerialNumber: crt.SerialNumber, + RevocationTime: revocationTime, + Extensions: crt.Extensions, + }) + } + caCrt, caKey, err := GetCA(pkiroot) + if err != nil { + return fmt.Errorf("get ca: %v", err) + } + crl, err := caCrt.CreateCRL(rand.Reader, caKey, revokedCerts, time.Now(), time.Now().AddDate(0, 0, expire)) + if err != nil { + return fmt.Errorf("create crl: %v", err) + } + // I do no see where we can pass it to CreateCRL, differs from openssl + crlNumber, err := NextNumber(pkiroot, "crlnumber") + if err != nil { + return fmt.Errorf("get next serial: %v", err) + } + + serialHexa := fmt.Sprintf("%X", crlNumber) + if len(serialHexa)%2 == 1 { + serialHexa = "0" + serialHexa + } + + crlPath := filepath.Join(pkiroot, "crl-"+serialHexa+".pem") + crlFile, err := os.Create(crlPath) + if err != nil { + return fmt.Errorf("create %v: %v", crlPath, err) + } + defer crlFile.Close() + + err = pem.Encode(crlFile, &pem.Block{ + Type: "X509 CRL", + Bytes: crl, + }) + if err != nil { + return fmt.Errorf("pem encode crt: %v", err) + } + + return nil +} + +func RevokeSerial(pkiroot string, serial *big.Int) error { + f, err := os.OpenFile(filepath.Join(pkiroot, "index.txt"), os.O_RDWR, 0644) + if err != nil { + return err + } + defer f.Close() + + var lines []string + scanner := bufio.NewScanner(f) + for scanner.Scan() { + matches := indexRegexp.FindStringSubmatch(scanner.Text()) + if len(matches) != 7 { + return fmt.Errorf("wrong line format") + } + matchedSerial := big.NewInt(0) + fmt.Sscanf(matches[4], "%X", matchedSerial) + if matchedSerial.Cmp(serial) == 0 { + if matches[1] == "R" { + return fmt.Errorf("certificate already revoked") + } else if matches[1] == "E" { + return fmt.Errorf("certificate already expired") + } + + lines = append(lines, fmt.Sprintf("R\t%v\t%vZ\t%v\t%v\t%v", + matches[2], + time.Now().UTC().Format("060102150405"), + matches[4], + matches[5], + matches[6])) + } else { + lines = append(lines, matches[0]) + } + } + + f.Truncate(0) + f.Seek(0, 0) + + for _, line := range lines { + n, err := fmt.Fprintln(f, line) + if err != nil { + return fmt.Errorf("write line: %v", err) + } + if n == 0 { + return fmt.Errorf("supposed to write [%v], written 0 bytes", line) + } + } + return nil +} + +func GetCertificate(path string) (*x509.Certificate, error) { + crtBytes, err := ioutil.ReadFile(path) + if err != nil { + return nil, fmt.Errorf("read crt: %v", err) + } + p, _ := pem.Decode(crtBytes) + if p == nil { + return nil, fmt.Errorf("pem decode did not found pem encoded cert") + } + crt, err := x509.ParseCertificate(p.Bytes) + if err != nil { + return nil, fmt.Errorf("parse crt: %v", err) + } + + return crt, nil +} + +func WriteIndex(pkiroot, filename string, crt *x509.Certificate) error { + f, err := os.OpenFile(filepath.Join(pkiroot, "index.txt"), os.O_WRONLY|os.O_APPEND, 0644) + if err != nil { + return err + } + defer f.Close() + + serialOutput := fmt.Sprintf("%X", crt.SerialNumber) + // For compatibility with openssl we need an even length + if len(serialOutput)%2 == 1 { + serialOutput = "0" + serialOutput + } + + // Date format: yymmddHHMMSSZ + // E|R|VExpiry[RevocationDate]SerialfilenameSubjectDN + var subject string + if strs := crt.Subject.Country; len(strs) == 1 { + subject += "/C=" + strs[0] + } + if strs := crt.Subject.Organization; len(strs) == 1 { + subject += "/O=" + strs[0] + } + if strs := crt.Subject.OrganizationalUnit; len(strs) == 1 { + subject += "/OU=" + strs[0] + } + if strs := crt.Subject.Locality; len(strs) == 1 { + subject += "/L=" + strs[0] + } + if strs := crt.Subject.Province; len(strs) == 1 { + subject += "/ST=" + strs[0] + } + subject += "/CN=" + crt.Subject.CommonName + + n, err := fmt.Fprintf(f, "V\t%vZ\t\t%v\t%v.crt\t%v\n", + crt.NotAfter.UTC().Format("060102150405"), + serialOutput, + filename, + subject) + if err != nil { + return err + } + if n == 0 { + return fmt.Errorf("written 0 bytes in index file") + } + return nil +} + +// |-ca.crt +// |-crlnumber +// |-index.txt +// |-index.txt.attr +// |-serial +// |-issued/ +// |- name.crt +// |-private +// |- ca.key +// |- name.key +func GeneratePKIStructure(pkiroot string) error { + + for _, dir := range []string{"private", "issued"} { + err := os.Mkdir(filepath.Join(pkiroot, dir), 0755) + if err != nil { + return fmt.Errorf("creating dir %v: %v", dir, err) + } + } + + files := []struct { + Name string + Content string + File *os.File + }{ + {Name: "serial", Content: "01"}, + {Name: "crlnumber", Content: "01"}, + {Name: "index.txt", Content: ""}, + {Name: "index.txt.attr", Content: "unique_subject = no"}, + } + for _, f := range files { + // if using := here i get needs identifier, hm ?, needs to declare err before + var err error + f.File, err = os.Create(filepath.Join(pkiroot, f.Name)) + if err != nil { + return fmt.Errorf("create %v: %v", f.Name, err) + } + defer f.File.Close() + + if len(f.Content) == 0 { + continue + } + + n, err := fmt.Fprintln(f.File, f.Content) + if err != nil { + return fmt.Errorf("write %v: %v", f.Name, err) + } + if n == 0 { + return fmt.Errorf("write %v, written 0 bytes", f.Name) + } + } + + return nil +} + +func NextNumber(pkiroot, name string) (*big.Int, error) { + serial := big.NewInt(0) + + f, err := os.OpenFile(filepath.Join(pkiroot, name), os.O_RDWR, 0644) + if err != nil { + return nil, err + } + defer f.Close() + + n, err := fmt.Fscanf(f, "%X\n", serial) + if err != nil { + return nil, err + } + if n != 1 { + return nil, fmt.Errorf("supposed to read 1 element, read: %v", n) + } + + next := big.NewInt(1) + next.Add(serial, next) + output := fmt.Sprintf("%X", next) + // For compatibility with openssl we need an even length + if len(output)%2 == 1 { + output = "0" + output + } + f.Truncate(0) + f.Seek(0, 0) + + n, err = fmt.Fprintln(f, output) + if err != nil { + return nil, err + } + if n == 0 { + return nil, fmt.Errorf("supposed to write 1 element, written: %v", n) + } + + return serial, nil +} diff --git a/pkg/easypki/easyca_test.go b/pkg/easypki/easyca_test.go new file mode 100644 index 0000000..ece603a --- /dev/null +++ b/pkg/easypki/easyca_test.go @@ -0,0 +1,144 @@ +// Copyright 2015 Google Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package easypki + +import ( + "io/ioutil" + "math/big" + "math/rand" + "os" + "path/filepath" + "testing" + "time" +) + +func TestGeneratePKIStructure(t *testing.T) { + rand.Seed(time.Now().UnixNano()) + + pkiroot, err := ioutil.TempDir("", "gotestpki") + if err != nil { + t.Fatalf("failed to create temp dir: %v", err) + } + + if err := GeneratePKIStructure(pkiroot); err != nil { + t.Fatalf("%v", err) + } + + // We should check the minimum content also.. + toCheck := []struct { + Name string + Dir bool + Content string + }{ + {"private", true, ""}, + {"issued", true, ""}, + {"serial", false, "01\n"}, + {"crlnumber", false, "01\n"}, + {"index.txt", false, ""}, + {"index.txt.attr", false, "unique_subject = no\n"}, + } + + for _, name := range toCheck { + fd, err := os.Stat(filepath.Join(pkiroot, name.Name)) + if err != nil { + t.Errorf("%v: %v", name.Name, err) + } + if name.Dir && !fd.IsDir() { + t.Errorf("%v supposed to be a directory", name.Name) + } + if len(name.Content) > 0 { + f, err := os.Open(filepath.Join(pkiroot, name.Name)) + if err != nil { + t.Fatalf("failed open %v: %v", name.Name, err) + } + defer f.Close() + bytes, err := ioutil.ReadAll(f) + if err != nil { + t.Fatalf("failed read %v: %v", name.Name, err) + } + if string(bytes) != name.Content { + t.Fatalf("%v content expected %v, got: %v", name.Name, name.Content, string(bytes)) + } + } + } + if err := os.RemoveAll(pkiroot); err != nil { + t.Logf("failed cleaning tmp dir %v: %v", pkiroot, err) + } +} + +func TestNextNumber(t *testing.T) { + pkiroot, err := ioutil.TempDir("", "gotestpki") + if err != nil { + t.Fatalf("failed to create temp dir: %v", err) + } + + if err := GeneratePKIStructure(pkiroot); err != nil { + t.Fatalf("generate pki structure: %v", err) + } + + n, err := NextNumber(pkiroot, "serial") + if err != nil { + t.Fatal("failed get next serial number: %v", err) + } + if big.NewInt(1).Cmp(n) != 0 { + t.Fatalf("after init serial is supposed to be 1, value is: %v", n) + } + // File content is now 02 + f, err := os.Open(filepath.Join(pkiroot, "serial")) + if err != nil { + t.Fatalf("failed open serial: %v", err) + } + defer f.Close() + bytes, err := ioutil.ReadAll(f) + if err != nil { + t.Fatalf("failed read serial: %v", err) + } + if string(bytes) != "02\n" { + t.Fatalf("serial content expected 02, got: %v", string(bytes)) + } +} + +func TestLargeNextNumber(t *testing.T) { + pkiroot, err := ioutil.TempDir("", "gotestpki") + if err != nil { + t.Fatalf("failed to create temp dir: %v", err) + } + + if err := GeneratePKIStructure(pkiroot); err != nil { + t.Fatalf("generate pki structure: %v", err) + } + + for { + n, err := NextNumber(pkiroot, "serial") + if err != nil { + t.Fatal("failed get next serial number: %v", err) + } + if big.NewInt(255).Cmp(n) == 0 { + break + } + } + f, err := os.Open(filepath.Join(pkiroot, "serial")) + if err != nil { + t.Fatalf("failed open serial: %v", err) + } + defer f.Close() + bytes, err := ioutil.ReadAll(f) + if err != nil { + t.Fatalf("failed read serial: %v", err) + } + if string(bytes) != "0100\n" { + t.Fatalf("serial content expected 0100, got: %v", string(bytes)) + } +} -- cgit v1.3