diff options
| author | Jeremy Clerc <jclerc@google.com> | 2017-02-11 00:13:54 +0100 |
|---|---|---|
| committer | Jeremy Clerc <jclerc@google.com> | 2017-02-12 23:54:16 +0100 |
| commit | 06ee1171dee17245e71bb0ddd742c7f95f9bd2cb (patch) | |
| tree | 64767087217188af49e4c3788188ce6568198fa7 /pkg/store | |
| parent | c42a84ae556034b9fe2f9710603b1c10e8c5588f (diff) | |
| download | easypki-1.0.0.tar.xz | |
Refactor the all API for cleanup and extensibility.v1.0.0
API now has a store interface so one could choose to store the different
files in a database for example.
Diffstat (limited to 'pkg/store')
| -rw-r--r-- | pkg/store/local.go | 383 | ||||
| -rw-r--r-- | pkg/store/store.go | 65 |
2 files changed, 448 insertions, 0 deletions
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) +} |
