aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--.gitignore1
-rw-r--r--README.md20
-rw-r--r--cmd/easypki/main.go150
-rw-r--r--pkg/certificate/certificate.go58
-rw-r--r--pkg/easypki/easyca.go518
-rw-r--r--pkg/easypki/easyca_test.go144
-rw-r--r--pkg/easypki/easypki.go147
-rw-r--r--pkg/easypki/easypki_test.go162
-rw-r--r--pkg/easypki/template.go74
-rw-r--r--pkg/store/local.go383
-rw-r--r--pkg/store/store.go65
11 files changed, 1002 insertions, 720 deletions
diff --git a/.gitignore b/.gitignore
index b72f9be..0047d7e 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,2 +1,3 @@
*~
+**/main
*.swp
diff --git a/README.md b/README.md
index 4ee2fa6..bfd7f38 100644
--- a/README.md
+++ b/README.md
@@ -56,6 +56,26 @@ For more info about available flags, checkout out the help `-h`
You will find the generated cert in `issued` and private key in `private`
+# API
+
+For the latest API:
+
+```
+import "gopkg.in/google/easypki.v1"
+```
+
+## Legacy API
+
+API below pkg/ has been rewritten to allow extensibility in terms of PKI
+storage and better readability.
+
+If you used the legacy API that was only writing files to disk, a tag has been
+applied so you can still import it:
+
+```
+import "gopkg.in/google/easypki.v0"
+```
+
# Disclaimer
This is not an official Google product
diff --git a/cmd/easypki/main.go b/cmd/easypki/main.go
index 0564c6a..f4b73ea 100644
--- a/cmd/easypki/main.go
+++ b/cmd/easypki/main.go
@@ -12,6 +12,7 @@
// See the License for the specific language governing permissions and
// limitations under the License.
+// Command easypki provides a simple client to manage a local PKI.
package main
import (
@@ -20,25 +21,28 @@ import (
"log"
"net"
"os"
+ "path"
"path/filepath"
"strings"
"time"
+ "encoding/pem"
+
"github.com/codegangsta/cli"
+ "github.com/google/easypki/pkg/certificate"
"github.com/google/easypki/pkg/easypki"
+ "github.com/google/easypki/pkg/store"
)
-// 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
+const (
+ defaultCAName = "ca"
+)
-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)
- }
+type router struct {
+ PKI *easypki.EasyPKI
}
-func createBundle(c *cli.Context) {
+func (r *router) create(c *cli.Context) {
if !c.Args().Present() {
cli.ShowSubcommandHelp(c)
log.Fatalf("Usage: %v name (common name defaults to name, use --cn and "+
@@ -53,35 +57,41 @@ func createBundle(c *cli.Context) {
}
subject := pkix.Name{CommonName: commonName}
- if str := c.String("organization"); len(str) > 0 {
+ if str := c.String("organization"); str != "" {
subject.Organization = []string{str}
}
- if str := c.String("locality"); len(str) > 0 {
+ if str := c.String("locality"); str != "" {
subject.Locality = []string{str}
}
- if str := c.String("country"); len(str) > 0 {
+ if str := c.String("country"); str != "" {
subject.Country = []string{str}
}
- if str := c.String("province"); len(str) > 0 {
+ if str := c.String("province"); str != "" {
subject.Province = []string{str}
}
- if str := c.String("organizational-unit"); len(str) > 0 {
+ if str := c.String("organizational-unit"); str != "" {
subject.OrganizationalUnit = []string{str}
}
template := &x509.Certificate{
- Subject: subject,
- NotAfter: time.Now().AddDate(0, 0, c.Int("expire")),
+ Subject: subject,
+ NotAfter: time.Now().AddDate(0, 0, c.Int("expire")),
+ MaxPathLen: c.Int("max-path-len"),
}
- intCA := c.Bool("intermediate")
+ var signer *certificate.Bundle
+ isRootCa := c.Bool("ca")
+ if !isRootCa {
+ var err error
+ signer, err = r.PKI.GetCA(c.String("ca-name"))
+ if err != nil {
+ log.Fatal(err)
+ }
+ }
- if intCA || c.Bool("ca") {
+ isIntCA := c.Bool("intermediate")
+ if isIntCA || isRootCa {
template.IsCA = true
-
- if !intCA {
- filename = "ca"
- }
} else if c.Bool("client") {
template.EmailAddresses = c.StringSlice("email")
} else {
@@ -95,85 +105,102 @@ func createBundle(c *cli.Context) {
template.IPAddresses = IPs
template.DNSNames = c.StringSlice("dns")
}
- err := easypki.GenerateCertificate(&easypki.GenerationRequest{
- PKIRoot: c.GlobalString("root"),
+
+ req := &easypki.Request{
Name: filename,
Template: template,
- MaxPathLen: c.Int("max-path-len"),
- IsIntermediateCA: intCA,
IsClientCertificate: c.Bool("client"),
- })
- if err != nil {
+ PrivateKeySize: c.Int("private-key-size"),
+ }
+ if err := r.PKI.Sign(signer, req); err != nil {
log.Fatal(err)
}
}
-func revoke(c *cli.Context) {
+
+func (r *router) 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)
+
+ for _, p := range c.Args() {
+ name := strings.TrimSuffix(path.Base(p), ".crt")
+ ca := path.Base(strings.TrimSuffix(path.Dir(p), store.LocalCertsDir))
+ bundle, err := r.PKI.GetBundle(ca, name)
+ if err != nil {
+ log.Fatalf("Failed fetching certificate %v under CA %v: %v", name, ca, err)
+ }
+ err = r.PKI.Revoke(ca, bundle.Cert)
+ if err != nil {
+ log.Fatalf("Failed revoking certificate %v under CA %v: %v", name, ca, 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 (r *router) crl(c *cli.Context) {
+ ca := c.String("ca-name")
+ crl, err := r.PKI.CRL(ca, time.Now().AddDate(0, 0, c.Int("expire")))
+ if err != nil {
+ log.Fatalf("Failed generating CRL for CA %v: %v", ca, err)
+ }
+ err = pem.Encode(os.Stdout, &pem.Block{
+ Type: "X509 CRL",
+ Bytes: crl,
+ })
+ if err != nil {
+ log.Fatalf("Failed writing PEM formated CRL to stdout: %v", err)
}
}
-func parseArgs() {
+func (r *router) run() {
app := cli.NewApp()
app.Name = "easypki"
app.Usage = "Manage pki"
app.Author = "Jeremy Clerc"
app.Email = "jeremy@clerc.io"
- app.Version = "0.1.1"
+ app.Version = "0.2.0"
+
+ caNameFlag := cli.StringFlag{
+ Name: "ca-name",
+ Usage: "Specify a different CA name to use an intermediate CA.",
+ Value: defaultCAName,
+ }
+ local := r.PKI.Store.(*store.Local)
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",
+ Name: "root",
+ Value: filepath.Join(os.Getenv("PWD"), "pki_auto_generated_dir"),
+ Usage: "path to pki root directory",
+ EnvVar: "PKI_ROOT",
+ Destination: &local.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,
+ Usage: "revoke path/to/ca-name/certs/cert path/to/ca-name/certs/cert2",
+ Description: "Revoke the given certificates",
+ Action: r.revoke,
},
{
- Name: "gencrl",
+ Name: "crl",
Description: "generate certificate revocation list",
- Action: gencrl,
+ Action: r.crl,
Flags: []cli.Flag{
cli.IntFlag{
Name: "expire",
Usage: "expiration limit in days",
- Value: 30,
+ Value: 7,
},
+ caNameFlag,
},
},
{
Name: "create",
Usage: "create COMMON NAME",
Description: "create private key + cert signed by CA",
- Action: createBundle,
+ Action: r.create,
Flags: []cli.Flag{
cli.BoolFlag{
Name: "ca",
@@ -183,6 +210,7 @@ func parseArgs() {
Name: "intermediate",
Usage: "intermediate certificate authority; implies --ca",
},
+ caNameFlag,
cli.IntFlag{
Name: "max-path-len",
Usage: "intermediate maximum path length",
@@ -197,6 +225,11 @@ func parseArgs() {
Usage: "expiration limit in days",
Value: 365,
},
+ cli.IntFlag{
+ Name: "private-key-size",
+ Usage: "size of the private key (default: 2048)",
+ Value: 2048,
+ },
cli.StringFlag{
Name: "filename",
Usage: "filename for bundle, use when you generate multiple certs for same cn",
@@ -243,5 +276,6 @@ func parseArgs() {
}
func main() {
- parseArgs()
+ r := router{PKI: &easypki.EasyPKI{Store: &store.Local{}}}
+ r.run()
}
diff --git a/pkg/certificate/certificate.go b/pkg/certificate/certificate.go
new file mode 100644
index 0000000..187fb1c
--- /dev/null
+++ b/pkg/certificate/certificate.go
@@ -0,0 +1,58 @@
+// 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 certificate provide helpers to manipulate certificates.
+package certificate
+
+import (
+ "crypto/rsa"
+ "crypto/x509"
+ "fmt"
+)
+
+// Bundle represents a pair of private key and certificate.
+type Bundle struct {
+ Name string
+ Key *rsa.PrivateKey
+ Cert *x509.Certificate
+}
+
+// Raw returns the raw bytes for the private key and certificate.
+func (b *Bundle) Raw() ([]byte, []byte) {
+ return x509.MarshalPKCS1PrivateKey(b.Key), b.Cert.Raw
+}
+
+// RawToBundle creates a bundle from the name and bytes given for a private key
+// and a certificate.
+func RawToBundle(name string, key []byte, cert []byte) (*Bundle, error) {
+ k, err := x509.ParsePKCS1PrivateKey(key)
+ if err != nil {
+ return nil, fmt.Errorf("failed parsing private key from PEM bytes: %v", err)
+ }
+ c, err := x509.ParseCertificate(cert)
+ if err != nil {
+ return nil, fmt.Errorf("failed parsing certificate from PEM bytes: %v", err)
+ }
+ return &Bundle{Name: name, Key: k, Cert: c}, nil
+}
+
+// State represents a certificate state (Valid, Expired, Revoked).
+type State int
+
+// Certificate states.
+const (
+ Valid State = iota
+ Revoked
+ Expired
+)
diff --git a/pkg/easypki/easyca.go b/pkg/easypki/easyca.go
deleted file mode 100644
index dbb6f0e..0000000
--- a/pkg/easypki/easyca.go
+++ /dev/null
@@ -1,518 +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 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
-}
-
-// GenerationRequest is a struct for providing configuration to
-// GenerateCertificate when actioning a certification generation request.
-type GenerationRequest struct {
- PKIRoot string
- Name string
- Template *x509.Certificate
- MaxPathLen int
- IsIntermediateCA bool
- IsClientCertificate bool
-}
-
-// GenerateCertificate is a function for helping to generate new x509
-// certificates and keys from the GenerationRequest. This function renders the
-// content out to the filesystem.
-func GenerateCertificate(genReq *GenerationRequest) error {
- // TODO(jclerc): check that pki has been init
-
- var crtPath string
- privateKeyPath := filepath.Join(genReq.PKIRoot, "private", genReq.Name+".key")
- if genReq.Name == "ca" {
- crtPath = filepath.Join(genReq.PKIRoot, genReq.Name+".crt")
- } else {
- crtPath = filepath.Join(genReq.PKIRoot, "issued", genReq.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", genReq.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)
- genReq.Template.SubjectKeyId = subjectKeyId[:]
-
- genReq.Template.NotBefore = time.Now()
- genReq.Template.SignatureAlgorithm = x509.SHA256WithRSA
-
- // Non-intermediate Certificate Authority
- if genReq.Template.IsCA && !genReq.IsIntermediateCA {
- // Random Serial
- 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)
- }
- genReq.Template.SerialNumber = serialNumber
-
- // Root certificate can self-sign
- genReq.Template.Issuer = genReq.Template.Subject
- genReq.Template.AuthorityKeyId = genReq.Template.SubjectKeyId
-
- // Use the generated certificate template and private key (self-signing)
- caCrt = genReq.Template
- caKey = privateKey
- }
-
- // Intermediate-only Certificate Authority
- if genReq.Template.IsCA && genReq.IsIntermediateCA {
- genReq.Template.ExtKeyUsage = []x509.ExtKeyUsage{
- x509.ExtKeyUsageClientAuth,
- x509.ExtKeyUsageServerAuth,
- }
- }
-
- // Either type of Certificate Authority (intermediate, root, etc.)
- if genReq.Template.IsCA || genReq.IsIntermediateCA {
- genReq.Template.KeyUsage = x509.KeyUsageDigitalSignature | x509.KeyUsageCertSign | x509.KeyUsageCRLSign
- genReq.Template.BasicConstraintsValid = true
-
- // Enforce Maximum Path Length
- if genReq.MaxPathLen >= 0 {
- genReq.Template.MaxPathLen = genReq.MaxPathLen
- genReq.Template.MaxPathLenZero = true // doesn't force to zero
- }
- }
-
- // Any leaf: intermediate CAs, client/server certificates, signed by a root
- if !genReq.Template.IsCA || genReq.IsIntermediateCA {
- serialNumber, err := NextNumber(genReq.PKIRoot, "serial")
- if err != nil {
- return fmt.Errorf("get next serial: %v", err)
- }
- genReq.Template.SerialNumber = serialNumber
-
- caCrt, caKey, err = GetCA(genReq.PKIRoot)
- if err != nil {
- return fmt.Errorf("get ca: %v", err)
- }
- }
-
- // Should cover only client/server (All non-CA, i.e. doesn't include intermediates)
- if !genReq.Template.IsCA {
- if !genReq.IsClientCertificate {
- genReq.Template.ExtKeyUsage = append(genReq.Template.ExtKeyUsage, x509.ExtKeyUsageServerAuth)
- }
- // Clients can only use ClientAuth
- genReq.Template.ExtKeyUsage = append(genReq.Template.ExtKeyUsage, x509.ExtKeyUsageClientAuth)
-
- // set the usage for non-CA certificates
- genReq.Template.KeyUsage = x509.KeyUsageDigitalSignature | x509.KeyUsageKeyEncipherment
- }
-
- crt, err := x509.CreateCertificate(rand.Reader, genReq.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 !genReq.Template.IsCA {
- WriteIndex(genReq.PKIRoot, genReq.Name, genReq.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|V<tab>Expiry<tab>[RevocationDate]<tab>Serial<tab>filename<tab>SubjectDN
- 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
deleted file mode 100644
index ece603a..0000000
--- a/pkg/easypki/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 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))
- }
-}
diff --git a/pkg/easypki/easypki.go b/pkg/easypki/easypki.go
new file mode 100644
index 0000000..be30748
--- /dev/null
+++ b/pkg/easypki/easypki.go
@@ -0,0 +1,147 @@
+// 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 provides helpers to manage a Public Key Infrastructure.
+package easypki
+
+import (
+ "crypto/rand"
+ "crypto/rsa"
+ "crypto/x509"
+ "errors"
+ "fmt"
+ "time"
+
+ "github.com/google/easypki/pkg/certificate"
+ "github.com/google/easypki/pkg/store"
+)
+
+const (
+ defaultPrivateKeySize = 2048
+)
+
+// Signing errors.
+var (
+ ErrCannotSelfSignNonCA = errors.New("cannot self sign non CA request")
+ ErrMaxPathLenReached = errors.New("max path len reached")
+)
+
+// Request is a struct for providing configuration to
+// GenerateCertificate when actioning a certification generation request.
+type Request struct {
+ Name string
+ IsClientCertificate bool
+ PrivateKeySize int
+ Template *x509.Certificate
+}
+
+// EasyPKI wraps helpers to handle a Public Key Infrastructure.
+type EasyPKI struct {
+ Store store.Store
+}
+
+// GetCA fetches and returns the named Certificate Authrority bundle
+// from the store.
+func (e *EasyPKI) GetCA(name string) (*certificate.Bundle, error) {
+ return e.GetBundle(name, name)
+}
+
+// GetBundle fetches and returns a certificate bundle from the store.
+func (e *EasyPKI) GetBundle(caName, name string) (*certificate.Bundle, error) {
+ k, c, err := e.Store.Fetch(caName, name)
+ if err != nil {
+ return nil, fmt.Errorf("failed fetching bundle %v within CA %v: %v", name, caName, err)
+ }
+
+ return certificate.RawToBundle(name, k, c)
+}
+
+// Sign signs a generated certificate bundle based on the given request with
+// the given signer.
+func (e *EasyPKI) Sign(signer *certificate.Bundle, req *Request) error {
+ if !req.Template.IsCA && signer == nil {
+ return ErrCannotSelfSignNonCA
+ }
+ if req.Template.IsCA && signer != nil && signer.Cert.MaxPathLen == 0 {
+ return ErrMaxPathLenReached
+ }
+
+ if req.PrivateKeySize == 0 {
+ req.PrivateKeySize = defaultPrivateKeySize
+ }
+ privateKey, err := rsa.GenerateKey(rand.Reader, req.PrivateKeySize)
+ if err != nil {
+ return fmt.Errorf("failed generating private key: %v", err)
+ }
+ publicKey := privateKey.Public()
+
+ if err := defaultTemplate(req, publicKey); err != nil {
+ return fmt.Errorf("failed updating generation request: %v", err)
+ }
+
+ if req.Template.IsCA {
+ var intermediateCA bool
+ if signer != nil {
+ intermediateCA = true
+ if signer.Cert.MaxPathLen > 0 {
+ req.Template.MaxPathLen = signer.Cert.MaxPathLen - 1
+ }
+ }
+ if err := caTemplate(req, intermediateCA); err != nil {
+ return fmt.Errorf("failed updating generation request for CA: %v", err)
+ }
+ if !intermediateCA {
+ // Use the generated certificate template and private key (self-signing).
+ signer = &certificate.Bundle{Name: req.Name, Cert: req.Template, Key: privateKey}
+ }
+ } else {
+ nonCATemplate(req)
+ }
+
+ rawCert, err := x509.CreateCertificate(rand.Reader, req.Template, signer.Cert, publicKey, signer.Key)
+ if err != nil {
+ return fmt.Errorf("failed creating and signing certificate: %v", err)
+ }
+
+ if err := e.Store.Add(signer.Name, req.Name, req.Template.IsCA, x509.MarshalPKCS1PrivateKey(privateKey), rawCert); err != nil {
+ return fmt.Errorf("failed saving generated bundle: %v", err)
+ }
+ return nil
+}
+
+// Revoke revokes the given certificate from the store.
+func (e *EasyPKI) Revoke(caName string, cert *x509.Certificate) error {
+ if err := e.Store.Update(caName, cert.SerialNumber, certificate.Revoked); err != nil {
+ return fmt.Errorf("failed revoking certificate: %v", err)
+ }
+ return nil
+}
+
+// CRL builds a CRL for a given CA based on the revoked certs.
+func (e *EasyPKI) CRL(caName string, expire time.Time) ([]byte, error) {
+ revoked, err := e.Store.Revoked(caName)
+ if err != nil {
+ return nil, fmt.Errorf("failed retrieving revoked certificates for %v: %v", caName, err)
+ }
+ ca, err := e.GetCA(caName)
+ if err != nil {
+ return nil, fmt.Errorf("failed retrieving CA bundle %v: %v", caName, err)
+ }
+
+ crl, err := ca.Cert.CreateCRL(rand.Reader, ca.Key, revoked, time.Now(), expire)
+ if err != nil {
+ return nil, fmt.Errorf("failed creating crl for %v: %v", caName, err)
+ }
+ return crl, nil
+}
diff --git a/pkg/easypki/easypki_test.go b/pkg/easypki/easypki_test.go
new file mode 100644
index 0000000..3c839dc
--- /dev/null
+++ b/pkg/easypki/easypki_test.go
@@ -0,0 +1,162 @@
+// 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 (
+ "crypto/x509"
+ "crypto/x509/pkix"
+ "io/ioutil"
+ "net"
+ "testing"
+ "time"
+
+ "reflect"
+
+ "github.com/google/easypki/pkg/store"
+)
+
+func TestE2E(t *testing.T) {
+ root, err := ioutil.TempDir("", "testeasypki")
+ if err != nil {
+ t.Fatalf("failed creating temporary directory: %v", err)
+ }
+ //defer os.RemoveAll(root)
+
+ pki := &EasyPKI{Store: &store.Local{Root: root}}
+
+ commonSubject := pkix.Name{
+ Organization: []string{"Acme Inc."},
+ OrganizationalUnit: []string{"IT"},
+ Locality: []string{"Agloe"},
+ Country: []string{"US"},
+ Province: []string{"New York"},
+ }
+
+ caRequest := &Request{
+ Name: "Root_CA",
+ Template: &x509.Certificate{
+ Subject: commonSubject,
+ NotAfter: time.Now().AddDate(0, 0, 30),
+ MaxPathLen: 1,
+ IsCA: true,
+ },
+ }
+ caRequest.Template.Subject.CommonName = "Root CA"
+ if err := pki.Sign(nil, caRequest); err != nil {
+ t.Fatalf("Sign(nil, %v): got error: %v != expected nil", caRequest, err)
+ }
+ rootCA, err := pki.GetCA(caRequest.Name)
+ if err != nil {
+ t.Fatalf("GetCA(%v): got error %v != expect nil", caRequest.Name, err)
+ }
+
+ cliRequest := &Request{
+ Name: "bob@acme.org",
+ Template: &x509.Certificate{
+ Subject: commonSubject,
+ NotAfter: time.Now().AddDate(0, 0, 30),
+ EmailAddresses: []string{"bob@acme.org"},
+ },
+ IsClientCertificate: true,
+ }
+ cliRequest.Template.Subject.CommonName = "bob@acme.org"
+ if err := pki.Sign(rootCA, cliRequest); err != nil {
+ t.Fatalf("Sign(%v, %v): go error: %v != expected nil", rootCA, cliRequest, err)
+ }
+ cli, err := pki.GetBundle(caRequest.Name, cliRequest.Name)
+ if err != nil {
+ t.Fatalf("GetBundle(%v, %v): go error %v != expected nil", caRequest.Name, cliRequest.Name, err)
+ }
+
+ expectedExtKeyUsage := []x509.ExtKeyUsage{x509.ExtKeyUsageClientAuth}
+ if !reflect.DeepEqual(cli.Cert.ExtKeyUsage, expectedExtKeyUsage) {
+ t.Errorf("Client ExtKeyUsage: got %v != expected %v", cli.Cert.ExtKeyUsage, expectedExtKeyUsage)
+ }
+
+ if err := pki.Sign(nil, cliRequest); err != ErrCannotSelfSignNonCA {
+ t.Errorf("Sign(nil, %v): got error %v != expected %v", cliRequest, err, ErrCannotSelfSignNonCA)
+ }
+
+ intRequest := &Request{
+ Name: "Intermediate_CA",
+ Template: &x509.Certificate{
+ Subject: commonSubject,
+ NotAfter: time.Now().AddDate(0, 0, 30),
+ IsCA: true,
+ },
+ }
+ intRequest.Template.Subject.CommonName = "Intermediate CA"
+ if err := pki.Sign(rootCA, intRequest); err != nil {
+ t.Fatalf("Sign(%v, %v): go error: %v != expected nil", rootCA, intRequest, err)
+ }
+ intCA, err := pki.GetCA(intRequest.Name)
+ if err != nil {
+ t.Fatalf("GetCA(%v): got error %v != expect nil", intRequest.Name, err)
+ }
+
+ srvRequest := &Request{
+ Name: "wiki.acme.org",
+ Template: &x509.Certificate{
+ Subject: commonSubject,
+ NotAfter: time.Now().AddDate(0, 0, 30),
+ DNSNames: []string{"wiki.acme.org"},
+ IPAddresses: []net.IP{net.ParseIP("10.10.10.10")},
+ },
+ PrivateKeySize: 4096,
+ }
+ srvRequest.Template.Subject.CommonName = "wiki.acme.org"
+ if err := pki.Sign(intCA, srvRequest); err != nil {
+ t.Fatalf("Sign(%v, %v): go error: %v != expected nil", intCA, srvRequest, err)
+
+ }
+ srv, err := pki.GetBundle(intRequest.Name, srvRequest.Name)
+ if err != nil {
+ t.Fatalf("GetBundle(%v, %v): go error %v != expected nil", intRequest.Name, srvRequest.Name, err)
+ }
+
+ if err := pki.Revoke(intRequest.Name, srv.Cert); err != nil {
+ t.Fatalf("Revoke(%v, %v): got error: %v != expected nil", intRequest.Name, srv.Cert, err)
+ }
+ expire := time.Now().Add(time.Hour * 24)
+ crlBytes, err := pki.CRL(intRequest.Name, expire)
+ if err != nil {
+ t.Fatalf("CRL(%v, %v): got error %v != expected nil", intRequest.Name, expire, err)
+ }
+
+ crl, err := x509.ParseCRL(crlBytes)
+ if err != nil {
+ t.Fatalf("ParseCRL(%v): got error %v != expected nil", crlBytes, err)
+ }
+ if len(crl.TBSCertList.RevokedCertificates) != 1 {
+ t.Fatalf("CRL does not have 1 revoked certificate: %v", crl)
+ }
+ if srv.Cert.SerialNumber.Cmp(crl.TBSCertList.RevokedCertificates[0].SerialNumber) != 0 {
+ t.Fatalf("Server certificate serial number %v != revoked server certificate serial %v",
+ srv.Cert.SerialNumber, crl.TBSCertList.RevokedCertificates[0].SerialNumber)
+ }
+
+ tooDeepReq := &Request{
+ Name: "Deep_Intermediate_CA",
+ Template: &x509.Certificate{
+ Subject: commonSubject,
+ NotAfter: time.Now().AddDate(0, 0, 30),
+ IsCA: true,
+ },
+ }
+ tooDeepReq.Template.Subject.CommonName = "Deep Intermediate CA"
+ if err := pki.Sign(intCA, tooDeepReq); err != ErrMaxPathLenReached {
+ t.Errorf("Sign(%v, %v): got error %v != expected %v", intCA, tooDeepReq, err, ErrMaxPathLenReached)
+ }
+}
diff --git a/pkg/easypki/template.go b/pkg/easypki/template.go
new file mode 100644
index 0000000..0a87cdc
--- /dev/null
+++ b/pkg/easypki/template.go
@@ -0,0 +1,74 @@
+// 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 (
+ "crypto"
+ "crypto/rand"
+ "crypto/rsa"
+ "crypto/sha1"
+ "crypto/x509"
+ "encoding/asn1"
+ "fmt"
+ "math/big"
+ "time"
+)
+
+func defaultTemplate(genReq *Request, publicKey crypto.PublicKey) error {
+ publicKeyBytes, err := asn1.Marshal(*publicKey.(*rsa.PublicKey))
+ if err != nil {
+ return fmt.Errorf("failed marshaling public key: %v", err)
+ }
+ subjectKeyID := sha1.Sum(publicKeyBytes)
+ genReq.Template.SubjectKeyId = subjectKeyID[:]
+
+ // Random serial number.
+ snLimit := new(big.Int).Lsh(big.NewInt(1), 128)
+ sn, err := rand.Int(rand.Reader, snLimit)
+ if err != nil {
+ return fmt.Errorf("failed generating serial number: %s", err)
+ }
+ genReq.Template.SerialNumber = sn
+
+ genReq.Template.NotBefore = time.Now()
+ genReq.Template.SignatureAlgorithm = x509.SHA256WithRSA
+ return nil
+}
+
+func caTemplate(genReq *Request, intermediateCA bool) error {
+ genReq.Template.KeyUsage = x509.KeyUsageDigitalSignature | x509.KeyUsageCertSign | x509.KeyUsageCRLSign
+ genReq.Template.BasicConstraintsValid = true
+ genReq.Template.MaxPathLenZero = true
+
+ if intermediateCA {
+ return nil
+ }
+
+ // Root certificate can self-sign.
+ genReq.Template.Issuer = genReq.Template.Subject
+ genReq.Template.AuthorityKeyId = genReq.Template.SubjectKeyId
+ return nil
+}
+
+func nonCATemplate(genReq *Request) {
+ if !genReq.IsClientCertificate {
+ genReq.Template.ExtKeyUsage = append(genReq.Template.ExtKeyUsage, x509.ExtKeyUsageServerAuth)
+ }
+ // Clients can only use ClientAuth
+ genReq.Template.ExtKeyUsage = append(genReq.Template.ExtKeyUsage, x509.ExtKeyUsageClientAuth)
+
+ // set the usage for non-CA certificates
+ genReq.Template.KeyUsage = x509.KeyUsageDigitalSignature | x509.KeyUsageKeyEncipherment
+}
diff --git a/pkg/store/local.go b/pkg/store/local.go
new file mode 100644
index 0000000..11208a1
--- /dev/null
+++ b/pkg/store/local.go
@@ -0,0 +1,383 @@
+// 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 store
+
+import (
+ "bufio"
+ "crypto/x509/pkix"
+ "encoding/pem"
+ "fmt"
+ "io/ioutil"
+ "math/big"
+ "os"
+ "path/filepath"
+ "regexp"
+ "strings"
+ "time"
+
+ "crypto/x509"
+
+ "github.com/google/easypki/pkg/certificate"
+)
+
+// Predifined directory names.
+const (
+ LocalCertsDir = "certs"
+ LocalKeysDir = "keys"
+ LocalCrlsDir = "crls"
+)
+
+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(.+)")
+)
+
+// Local lets us store a Certificate Authority on the local filesystem.
+//
+// The structure used makes it compatible with openssl.
+type Local struct {
+ Root string
+}
+
+// path returns private and public key path.
+func (l *Local) path(caName, name string) (priv string, cert string) {
+ priv = filepath.Join(l.Root, caName, LocalKeysDir, name+".key")
+ cert = filepath.Join(l.Root, caName, LocalCertsDir, name+".crt")
+ return
+}
+
+// Exists checks if a certificate or private key already exist on the local
+// filesystem for a given name.
+func (l *Local) Exists(caName, name string) bool {
+ privPath, certPath := l.path(caName, name)
+ if _, err := os.Stat(privPath); err == nil {
+ return true
+ }
+ if _, err := os.Stat(certPath); err == nil {
+ return true
+ }
+ return false
+}
+
+// Fetch fetchs the private key and certificate for a given name signed by caName.
+func (l *Local) Fetch(caName, name string) ([]byte, []byte, error) {
+ filepath.Join(l.Root, caName)
+
+ keyPath, certPath := l.path(caName, name)
+ k, err := readPEM(keyPath)
+ if err != nil {
+ return nil, nil, fmt.Errorf("failed reading CA private key from file %v: %v", keyPath, err)
+ }
+ c, err := readPEM(certPath)
+ if err != nil {
+ return nil, nil, fmt.Errorf("failed reading CA cert from file %v: %v", certPath, err)
+ }
+ return k, c, nil
+}
+
+func readPEM(path string) ([]byte, error) {
+ bytes, err := ioutil.ReadFile(path)
+ if err != nil {
+ return nil, fmt.Errorf("failed reading %v: %v", path, err)
+ }
+ p, _ := pem.Decode(bytes)
+ if p == nil {
+ return nil, fmt.Errorf("no PEM data found for certificate")
+ }
+ return p.Bytes, nil
+}
+
+// Add adds the given bundle to the local filesystem.
+func (l *Local) Add(caName, name string, isCa bool, key, cert []byte) error {
+ if l.Exists(caName, name) {
+ return fmt.Errorf("a bundle already exists for the name %v within CA %v", name, caName)
+ }
+ if err := l.writeBundle(caName, name, isCa, key, cert); err != nil {
+ return fmt.Errorf("failed writing bundle %v within CA %v to the local filesystem: %v", name, caName, err)
+ }
+ if err := l.updateIndex(caName, name, cert); err != nil {
+ return fmt.Errorf("failed updating CA %v index: %v", caName, err)
+ }
+ return nil
+}
+
+// writeBundle encodes in PEM format the bundle private key and
+// certificate and stores them on the local filesystem.
+func (l *Local) writeBundle(caName, name string, isCa bool, key, cert []byte) error {
+ caDir := filepath.Join(l.Root, caName)
+ if _, err := os.Stat(caDir); err != nil {
+ if err := InitCADir(caDir); err != nil {
+ return fmt.Errorf("root directory for CA %v does not exist and cannot be created: %v", caDir, err)
+ }
+ }
+ keyPath, certPath := l.path(caName, name)
+ if err := encodeAndWrite(keyPath, "RSA PRIVATE KEY", key); err != nil {
+ return fmt.Errorf("failed encoding and writing private key file: %v", err)
+ }
+ if err := encodeAndWrite(certPath, "CERTIFICATE", cert); err != nil {
+ return fmt.Errorf("failed encoding and writing cert file: %v", err)
+ }
+
+ if isCa && name != caName {
+ intCaDir := filepath.Join(l.Root, name)
+ if err := InitCADir(intCaDir); err != nil {
+ return fmt.Errorf("root directory for CA %v does not exist and cannot be created: %v", intCaDir, err)
+ }
+ kp, cp := l.path(name, name)
+ if err := os.Link(keyPath, kp); err != nil {
+ return fmt.Errorf("failed creating hard link from %v to %v: %v", keyPath, kp, err)
+ }
+ if err := os.Link(certPath, cp); err != nil {
+ return fmt.Errorf("failed creating hard link from %v to %v: %v", certPath, cp, err)
+ }
+ }
+ return nil
+}
+
+func encodeAndWrite(path, pemType string, data []byte) error {
+ f, err := os.Create(path)
+ if err != nil {
+ return err
+ }
+ defer f.Close()
+
+ return pem.Encode(f, &pem.Block{
+ Type: pemType,
+ Bytes: data,
+ })
+}
+
+// updateIndex appends a line to the index.txt with few information about the
+// given the certificate.
+func (l *Local) updateIndex(caName, name string, rawCert []byte) error {
+ f, err := os.OpenFile(filepath.Join(l.Root, caName, "index.txt"), os.O_WRONLY|os.O_APPEND, 0644)
+ if err != nil {
+ return err
+ }
+ defer f.Close()
+
+ cert, err := x509.ParseCertificate(rawCert)
+ if err != nil {
+ return fmt.Errorf("failed parsing raw certificate %v: %v", name, err)
+ }
+
+ sn := fmt.Sprintf("%X", cert.SerialNumber)
+ // For compatibility with openssl we need an even length.
+ if len(sn)%2 == 1 {
+ sn = "0" + sn
+ }
+
+ // Date format: yymmddHHMMSSZ
+ // E|R|V<tab>Expiry<tab>[RevocationDate]<tab>Serial<tab>filename<tab>SubjectDN
+ var subject string
+ if strs := cert.Subject.Country; len(strs) == 1 {
+ subject += "/C=" + strs[0]
+ }
+ if strs := cert.Subject.Organization; len(strs) == 1 {
+ subject += "/O=" + strs[0]
+ }
+ if strs := cert.Subject.OrganizationalUnit; len(strs) == 1 {
+ subject += "/OU=" + strs[0]
+ }
+ if strs := cert.Subject.Locality; len(strs) == 1 {
+ subject += "/L=" + strs[0]
+ }
+ if strs := cert.Subject.Province; len(strs) == 1 {
+ subject += "/ST=" + strs[0]
+ }
+ subject += "/CN=" + cert.Subject.CommonName
+
+ n, err := fmt.Fprintf(f, "V\t%vZ\t\t%v\t%v.crt\t%v\n",
+ cert.NotAfter.UTC().Format("060102150405"),
+ sn,
+ name,
+ subject)
+ if err != nil {
+ return err
+ }
+ if n == 0 {
+ return fmt.Errorf("written 0 bytes in index file")
+ }
+ return nil
+}
+
+// Update updates the state of a given certificate in the index.txt.
+func (l *Local) Update(caName string, sn *big.Int, st certificate.State) error {
+ f, err := os.OpenFile(filepath.Join(l.Root, caName, "index.txt"), os.O_RDWR, 0644)
+ if err != nil {
+ return err
+ }
+ defer f.Close()
+
+ var state string
+ switch st {
+ case certificate.Valid:
+ state = "V"
+ case certificate.Revoked:
+ state = "R"
+ case certificate.Expired:
+ state = "E"
+ default:
+ return fmt.Errorf("unhandled certificate state: %v", st)
+ }
+
+ var lines []string
+ scanner := bufio.NewScanner(f)
+ for scanner.Scan() {
+ matches := indexRegexp.FindStringSubmatch(scanner.Text())
+ if len(matches) != 7 {
+ return fmt.Errorf("line [%v] is incorrectly formated", scanner.Text())
+ }
+
+ matchedSerial := big.NewInt(0)
+ fmt.Sscanf(matches[4], "%X", matchedSerial)
+ if matchedSerial.Cmp(sn) == 0 {
+ if matches[1] == state {
+ return nil
+ }
+
+ lines = append(lines, fmt.Sprintf("%v\t%v\t%vZ\t%v\t%v\t%v",
+ state,
+ 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("failed writing line [%v]: %v", line, err)
+ }
+ if n == 0 {
+ return fmt.Errorf("failed writing line [%v]: written 0 bytes", line)
+ }
+ }
+ return nil
+}
+
+// Revoked returns a list of revoked certificates.
+func (l *Local) Revoked(caName string) ([]pkix.RevokedCertificate, error) {
+ index, err := os.Open(filepath.Join(l.Root, caName, "index.txt"))
+ if err != nil {
+ return nil, err
+ }
+ defer index.Close()
+
+ var revokedCerts []pkix.RevokedCertificate
+ scanner := bufio.NewScanner(index)
+ for scanner.Scan() {
+ matches := indexRegexp.FindStringSubmatch(scanner.Text())
+ if len(matches) != 7 {
+ return nil, fmt.Errorf("line [%v] is incorrectly formated", scanner.Text())
+ }
+ if matches[1] != "R" {
+ continue
+ }
+
+ sn := big.NewInt(0)
+ fmt.Sscanf(matches[4], "%X", sn)
+ t, err := time.Parse("060102150405", strings.TrimSuffix(matches[3], "Z"))
+ if err != nil {
+ return nil, fmt.Errorf("failed parsing revocation time %v: %v", matches[3], err)
+ }
+ revokedCerts = append(revokedCerts, pkix.RevokedCertificate{
+ SerialNumber: sn,
+ RevocationTime: t,
+ })
+ }
+ return revokedCerts, nil
+}
+
+// InitCADir creates the basic structure of a CA subdirectory.
+//
+// |- crlnumber
+// |- index.txt
+// |- index.txt.attr
+// |- serial
+// |- certs/
+// |- ca.crt
+// |- name.crt
+// |- keys/
+// |- ca.key
+// |- name.key
+func InitCADir(path string) error {
+ if _, err := os.Stat(path); err == nil {
+ return nil
+ }
+ if err := os.Mkdir(path, 0755); err != nil {
+ return fmt.Errorf("failed creating CA root directory %v: %v", path, err)
+ }
+ dirs := map[string]os.FileMode{
+ filepath.Join(path, LocalCrlsDir): 0700,
+ filepath.Join(path, LocalCertsDir): 0755,
+ filepath.Join(path, LocalKeysDir): 0700,
+ }
+ for d, m := range dirs {
+ if err := os.Mkdir(d, m); err != nil {
+ return fmt.Errorf("failed creating directory %v: %v", d, err)
+ }
+ }
+
+ files := []struct {
+ Name string
+ Content string
+ }{
+ {Name: "serial", Content: "01"},
+ {Name: "crlnumber", Content: "01"},
+ {Name: "index.txt", Content: ""},
+ {Name: "index.txt.attr", Content: "unique_subject = no"},
+ }
+ for _, f := range files {
+ err := func(path, content string) error {
+ fh, err := os.Create(path)
+ if err != nil {
+ return fmt.Errorf("failed creating file %v: %v", path, err)
+ }
+ defer fh.Close()
+
+ if content == "" {
+ return nil
+ }
+
+ n, err := fmt.Fprintln(fh, content)
+ if err != nil {
+ return fmt.Errorf("failed wrinting %v in %v: %v", content, path, err)
+ }
+ if n == 0 {
+ return fmt.Errorf("failed writing %v in %v: 0 bytes written", content, path)
+ }
+ return nil
+ }(filepath.Join(path, f.Name), f.Content)
+ if err != nil {
+ return err
+ }
+ }
+ return nil
+}
diff --git a/pkg/store/store.go b/pkg/store/store.go
new file mode 100644
index 0000000..2b311d9
--- /dev/null
+++ b/pkg/store/store.go
@@ -0,0 +1,65 @@
+// 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 store provides different methods to store a Public Key Infrastructure.
+package store
+
+import (
+ "crypto/x509/pkix"
+ "math/big"
+
+ "github.com/google/easypki/pkg/certificate"
+)
+
+// Store reprents a way to store a Certificate Authority.
+type Store interface {
+ // Add adds a newly signed certificate bundle to the store.
+ //
+ // Args:
+ // The CA name, if the certificate was signed with an intermediate CA.
+ // The certificate bundle name.
+ // Is the bundle to add an intermediate CA.
+ // The raw private key.
+ // The raw certificate.
+ //
+ // Returns an error if it failed to store the bundle.
+ Add(string, string, bool, []byte, []byte) error
+
+ // Fetch fetches a certificate bundle from the store.
+ //
+ // Args:
+ // The CA name, if the certificate was signed with an intermediate CA.
+ // The name of the certificate bundle.
+ //
+ // Returns the raw private key and certificate respectively or an error.
+ Fetch(string, string) ([]byte, []byte, error)
+
+ // Update updates the state of a certificate. (Valid, Revoked, Expired)
+ //
+ // Args:
+ // The CA name, if the certificate was signed with an intermediate CA.
+ // The serial of the certificate to update.
+ // The new state.
+ //
+ // Returns an error if the update failed.
+ Update(string, *big.Int, certificate.State) error
+
+ // Revoked returns a list of revoked certificates for a given CA.
+ //
+ // Args:
+ // The CA name, if it is for an intermediate CA.
+ //
+ // Returns a list of revoked certificate or an error.
+ Revoked(string) ([]pkix.RevokedCertificate, error)
+}