add refresh

This adds the facilities to refresh resources automatically.
This commit is contained in:
Gibheer 2017-10-06 14:25:02 +02:00
parent bc410d2d10
commit 89e7ac94f5
14 changed files with 300 additions and 44 deletions

View File

@ -88,9 +88,9 @@ func listCertificate(args []string, client *pkiadm.Client) error {
return nil
}
out := tabwriter.NewWriter(os.Stdout, 2, 2, 1, ' ', tabwriter.AlignRight)
fmt.Fprintf(out, "%s\t%s\t%s\t%s\t%s\t%s\t%s\t\n", "id", "private", "csr", "ca", "serial", "created", "duration", "self-signed")
fmt.Fprintf(out, "%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s\t\n", "id", "private", "csr", "ca", "serial", "created", "duration", "self-signed")
for _, cert := range certs {
fmt.Fprintf(out, "%s\t%s\t%s\t%s\t%s\t%s\t%t\t\n", cert.ID, cert.PrivateKey.ID, cert.CSR.ID, cert.CA.ID, cert.Serial.ID, cert.Created, cert.Duration, cert.IsCA)
fmt.Fprintf(out, "%s\t%s\t%s\t%s\t%s\t%s\t%s\t%t\t\n", cert.ID, cert.PrivateKey.ID, cert.CSR.ID, cert.CA.ID, cert.Serial.ID, cert.Created, cert.Duration, cert.IsCA)
}
out.Flush()

View File

@ -3,6 +3,7 @@ package main
import (
"fmt"
"os"
"sort"
"text/tabwriter"
"github.com/gibheer/pkiadm"
@ -185,6 +186,7 @@ func list(args []string, c *pkiadm.Client) error {
if err != nil {
return err
}
sort.Sort(resources)
out := tabwriter.NewWriter(os.Stdout, 0, 4, 1, ' ', 0)
fmt.Fprintf(out, "%s\t%s\t\n", "type", "id")
for _, res := range resources {

View File

@ -22,6 +22,7 @@ type (
ID string
Type pkiadm.CAType
Certificate pkiadm.ResourceName
Interval Interval
}
)
@ -87,12 +88,18 @@ func (ca *CA) Name() pkiadm.ResourceName {
return pkiadm.ResourceName{ca.ID, pkiadm.RTCA}
}
// AddDependency registers a depending resource to be retuened by Dependencies()
// Refresh must trigger a rebuild of the resource.
// Refresh must trigger a rebuild of the resource. In this case, this is a NOOP.
func (ca *CA) Refresh(*Storage) error {
return nil
}
// RefreshInterval returns the dates and interval settings which are used to
// decide when to trigger a refresh for the resource.
// For CAs, this is a NOOP, as the underlying cert needs the refresh.
func (ca *CA) RefreshInterval() Interval {
return NoInterval
}
// Return the PEM output of the contained resource.
func (ca *CA) Pem() ([]byte, error) { return []byte{}, nil }
func (ca *CA) Checksum() []byte { return []byte{} }

View File

@ -18,6 +18,8 @@ type (
ID string
IsCA bool
Interval Interval
// TODO remove obsolete field - got replaced with interval
Duration time.Duration
Created time.Time
@ -31,6 +33,11 @@ type (
)
func NewCertificate(id string, privateKey, serial, csr, ca pkiadm.ResourceName, selfSign bool, duration time.Duration) (*Certificate, error) {
if id == "" {
return nil, ENoIDGiven
}
return &Certificate{
ID: id,
PrivateKey: privateKey,
@ -39,6 +46,10 @@ func NewCertificate(id string, privateKey, serial, csr, ca pkiadm.ResourceName,
CA: ca,
IsCA: selfSign,
Duration: duration,
Interval: Interval{
Created: time.Now(),
RefreshAfter: duration,
},
}, nil
}
@ -86,10 +97,16 @@ func (c *Certificate) Refresh(lookup *Storage) error {
return err
}
c.Data = pem.EncodeToMemory(&block)
// TODO remove obsolete field
c.Created = time.Now()
c.Interval.LastRefresh = time.Now()
return nil
}
func (c *Certificate) RefreshInterval() Interval {
return c.Interval
}
func (c *Certificate) GetCertificate() (*pki.Certificate, error) {
// TODO fix this, we must check if there is anything else
block, _ := pem.Decode(c.Data)
@ -156,6 +173,7 @@ func (s *Server) SetCertificate(changeset pkiadm.CertificateChange, res *pkiadm.
switch field {
case "duration":
cert.Duration = change.Duration
cert.Interval.RefreshAfter = change.Duration
case "private":
cert.PrivateKey = change.PrivateKey
case "csr":

View File

@ -4,6 +4,7 @@ import (
"encoding/pem"
"fmt"
"net"
"time"
"github.com/gibheer/pki"
"github.com/gibheer/pkiadm"
@ -14,6 +15,9 @@ type (
// ID is the unique identifier of the CSR.
ID string
// Interval represents the refresh timing information.
Interval Interval
// The following options are used to generate the content of the CSR.
DNSNames []string
EmailAddresses []string
@ -30,7 +34,8 @@ type (
// NewCSR creates a new CSR.
func NewCSR(id string, pk, subject pkiadm.ResourceName, dnsNames []string,
emailAddresses []string, iPAddresses []net.IP) (*CSR, error) {
emailAddresses []string, iPAddresses []net.IP, refreshAfter time.Duration,
invalidAfter time.Duration) (*CSR, error) {
return &CSR{
ID: id,
Subject: subject,
@ -38,6 +43,11 @@ func NewCSR(id string, pk, subject pkiadm.ResourceName, dnsNames []string,
EmailAddresses: emailAddresses,
IPAddresses: iPAddresses,
PrivateKey: pk,
Interval: Interval{
Created: time.Now(),
RefreshAfter: refreshAfter,
InvalidAfter: invalidAfter,
},
}, nil
}
@ -78,9 +88,14 @@ func (c *CSR) Refresh(lookup *Storage) error {
return err
}
c.Data = pem.EncodeToMemory(&block)
c.Interval.LastRefresh = time.Now()
return nil
}
func (c *CSR) RefreshInterval() Interval {
return c.Interval
}
// Return the PEM output of the contained resource.
func (c *CSR) Pem() ([]byte, error) { return c.Data, nil }
func (c *CSR) Checksum() []byte { return Hash(c.Data) }
@ -111,6 +126,7 @@ func (s *Server) CreateCSR(inCSR pkiadm.CSR, res *pkiadm.Result) error {
inCSR.DNSNames,
inCSR.EmailAddresses,
inCSR.IPAddresses,
0, 0,
)
if err != nil {
res.SetError(err, "Could not create new private key '%s'", inCSR.ID)

View File

@ -6,6 +6,7 @@ import (
"log"
"os"
"os/exec"
"time"
"github.com/gibheer/pkiadm"
)
@ -23,10 +24,13 @@ type (
Path string
Dependencies []pkiadm.ResourceName
Interval Interval
}
)
func NewLocation(id, path, preCom, postCom string, res []pkiadm.ResourceName) (*Location, error) {
func NewLocation(id, path, preCom, postCom string, res []pkiadm.ResourceName,
interval Interval) (*Location, error) {
if id == "" {
return nil, ENoIDGiven
}
@ -37,6 +41,7 @@ func NewLocation(id, path, preCom, postCom string, res []pkiadm.ResourceName) (*
ID: id,
Path: path,
Dependencies: res,
Interval: interval,
}
return l, nil
}
@ -78,9 +83,14 @@ func (l *Location) Refresh(lookup *Storage) error {
return err
}
}
l.Interval.LastRefresh = time.Now()
return nil
}
func (l *Location) RefreshInterval() Interval {
return l.Interval
}
func (l *Location) DependsOn() []pkiadm.ResourceName { return l.Dependencies }
// Pem is not used by location, as it does not contain any data.
@ -97,7 +107,7 @@ func (s *Server) CreateLocation(inLoc pkiadm.Location, res *pkiadm.Result) error
for _, dep := range inLoc.Dependencies {
deps = append(deps, pkiadm.ResourceName{ID: dep.ID, Type: dep.Type})
}
loc, err := NewLocation(inLoc.ID, inLoc.Path, inLoc.PreCommand, inLoc.PostCommand, deps)
loc, err := NewLocation(inLoc.ID, inLoc.Path, inLoc.PreCommand, inLoc.PostCommand, deps, NoInterval)
if err != nil {
res.SetError(err, "Could not create location '%s'", inLoc.ID)
return nil

View File

@ -6,6 +6,7 @@ import (
"net/rpc"
"os"
"os/signal"
"time"
"github.com/gibheer/pkiadm"
)
@ -19,20 +20,42 @@ const (
EAlreadyExist = Error("resource already exists")
)
var (
NoInterval = Interval{}
)
type (
Resource interface {
// Return the unique ResourceName
Name() pkiadm.ResourceName
// AddDependency registers a depending resource to be retuened by Dependencies()
// Refresh must trigger a rebuild of the resource.
Refresh(*Storage) error
// RefreshInterval returns the dates and interval settings which are used to
// decide when to trigger a refresh for the resource.
RefreshInterval() Interval
// Return the PEM output of the contained resource.
Pem() ([]byte, error)
// Return the checksum of the PEM content.
Checksum() []byte
// DependsOn must return the resource names it is depending on.
DependsOn() []pkiadm.ResourceName
}
Interval struct {
// Created states the time, the resource was created.
Created time.Time
// LastRefresh is the time, when the resource was last refreshed.
LastRefresh time.Time
// RefreshAfter is the duration after which the refresh of the resource
// is triggered.
RefreshAfter time.Duration
// InvalidAfter is the duration after which this resource becomes invalid.
// The decision when a resource becomes invalid is based on the created time
// and the duration. When the refresh duration is less than the invalid
// duration, then the resource will never be invalid.
InvalidAfter time.Duration
}
Error string
)

View File

@ -4,6 +4,7 @@ import (
"crypto/elliptic"
"encoding/pem"
"fmt"
"time"
"github.com/gibheer/pki"
"github.com/gibheer/pkiadm"
@ -17,14 +18,15 @@ const (
type (
PrivateKey struct {
ID string
PKType pkiadm.PrivateKeyType
Bits uint
Key []byte
ID string
PKType pkiadm.PrivateKeyType
Bits uint
Key []byte
Interval Interval
}
)
func NewPrivateKey(id string, pkType pkiadm.PrivateKeyType, bits uint) (*PrivateKey, error) {
func NewPrivateKey(id string, pkType pkiadm.PrivateKeyType, bits uint, interval Interval) (*PrivateKey, error) {
if id == "" {
return nil, ENoIDGiven
}
@ -32,9 +34,10 @@ func NewPrivateKey(id string, pkType pkiadm.PrivateKeyType, bits uint) (*Private
return nil, err
}
pk := PrivateKey{
ID: id,
PKType: pkType,
Bits: bits,
ID: id,
PKType: pkType,
Bits: bits,
Interval: interval,
}
return &pk, nil
}
@ -88,9 +91,16 @@ func (p *PrivateKey) Refresh(_ *Storage) error {
return err
}
p.Key = pem.EncodeToMemory(&block)
p.Interval.LastRefresh = time.Now()
return nil
}
// RefreshInterval returns the dates and interval settings which are used to
// decide when to trigger a refresh for the resource.
func (p *PrivateKey) RefreshInterval() Interval {
return p.Interval
}
func (p *PrivateKey) GetKey() (pki.PrivateKey, error) {
var (
err error
@ -139,7 +149,7 @@ func (s *Server) CreatePrivateKey(inPk pkiadm.PrivateKey, res *pkiadm.Result) er
s.lock()
defer s.unlock()
pk, err := NewPrivateKey(inPk.ID, inPk.Type, inPk.Bits)
pk, err := NewPrivateKey(inPk.ID, inPk.Type, inPk.Bits, NoInterval)
if err != nil {
res.SetError(err, "Could not create new private key '%s'", inPk.ID)
return nil

View File

@ -3,6 +3,7 @@ package main
import (
"encoding/pem"
"fmt"
"time"
"github.com/gibheer/pkiadm"
)
@ -14,13 +15,21 @@ type (
PrivateKey pkiadm.ResourceName
Type pkiadm.PrivateKeyType // mark the type of the public key
Key []byte
Interval Interval
}
)
func NewPublicKey(id string, pk pkiadm.ResourceName) (*PublicKey, error) {
func NewPublicKey(id string, pk pkiadm.ResourceName, refreshAfter time.Duration,
invalidAfter time.Duration) (*PublicKey, error) {
pub := PublicKey{
ID: id,
PrivateKey: pk,
Interval: Interval{
Created: time.Now(),
RefreshAfter: refreshAfter,
InvalidAfter: invalidAfter,
},
}
return &pub, nil
}
@ -45,9 +54,14 @@ func (p *PublicKey) Refresh(lookup *Storage) error {
return err
}
p.Key = pem.EncodeToMemory(&block)
p.Interval.LastRefresh = time.Now()
return nil
}
func (p *PublicKey) RefreshInterval() Interval {
return p.Interval
}
func (p *PublicKey) DependsOn() []pkiadm.ResourceName {
return []pkiadm.ResourceName{p.PrivateKey}
}
@ -64,7 +78,7 @@ func (s *Server) CreatePublicKey(inPub pkiadm.PublicKey, res *pkiadm.Result) err
s.lock()
defer s.unlock()
pub, err := NewPublicKey(inPub.ID, inPub.PrivateKey)
pub, err := NewPublicKey(inPub.ID, inPub.PrivateKey, 0, 0)
if err != nil {
res.SetError(err, "Could not create public key '%s'", inPub.ID)
return nil

View File

@ -33,7 +33,6 @@ func NewSerial(id string, min, max int64) (*Serial, error) {
// Return the unique ResourceName
func (s *Serial) Name() pkiadm.ResourceName { return pkiadm.ResourceName{s.ID, pkiadm.RTSerial} }
// AddDependency registers a depending resource to be retuened by Dependencies()
// Refresh must trigger a rebuild of the resource.
func (s *Serial) Refresh(*Storage) error {
// This is a NOOP, because there is nothing to refresh. Depending resources
@ -41,6 +40,11 @@ func (s *Serial) Refresh(*Storage) error {
return nil
}
// RefreshInterval is a NOOP here, as serials can't be refreshed.
func (s *Serial) RefreshInterval() Interval {
return NoInterval
}
// Return the PEM output of the contained resource.
func (s *Serial) Pem() ([]byte, error) { return []byte{}, nil }
func (s *Serial) Checksum() []byte { return []byte{} }

View File

@ -33,26 +33,10 @@ func (s *Server) store(res *pkiadm.Result) error {
}
func (s *Server) List(filter pkiadm.Filter, result *pkiadm.ResultResource) error {
for _, res := range s.storage.PrivateKeys {
result.Resources = append(result.Resources, res.Name())
}
for _, res := range s.storage.PublicKeys {
result.Resources = append(result.Resources, res.Name())
}
for _, res := range s.storage.Locations {
result.Resources = append(result.Resources, res.Name())
}
for _, res := range s.storage.Certificates {
result.Resources = append(result.Resources, res.Name())
}
for _, res := range s.storage.CSRs {
result.Resources = append(result.Resources, res.Name())
}
for _, res := range s.storage.Serials {
result.Resources = append(result.Resources, res.Name())
}
for _, res := range s.storage.Subjects {
result.Resources = append(result.Resources, res.Name())
resources := s.storage.List()
result.Resources = make([]pkiadm.ResourceName, len(resources))
for i, res := range resources {
result.Resources[i] = res.Name()
}
return nil
}

View File

@ -6,6 +6,8 @@ import (
"io/ioutil"
"log"
"os"
"sort"
"time"
"github.com/gibheer/pkiadm"
"github.com/pkg/errors"
@ -28,6 +30,19 @@ type (
// dependencies maps from a resource name to all resources which depend
// on it.
dependencies map[string]map[string]Resource
// refresh order contains all resources in the order they need to be
// refreshed next.
refreshOrder RefreshList
refreshTimer *time.Timer
}
// RefreshList is a list of resources
RefreshList []RefreshSet
// RefreshSet contains the vital information to decide, when to refresh
// the specified resource.
RefreshSet struct {
Name pkiadm.ResourceName
Interval Interval
}
)
@ -68,6 +83,7 @@ func (s *Storage) load() error {
if err := s.refreshDependencies(); err != nil {
return err
}
s.scanForRefresh()
return nil
}
@ -101,6 +117,69 @@ func (s *Storage) refreshDependencies() error {
return nil
}
// scanForRefresh updates the list of resources that need an update.
func (s *Storage) scanForRefresh() {
if s.refreshTimer != nil {
s.refreshTimer.Stop()
}
refList := RefreshList{}
for _, res := range s.PrivateKeys {
refList.Add(res)
}
for _, res := range s.PublicKeys {
refList.Add(res)
}
for _, res := range s.CSRs {
refList.Add(res)
}
for _, res := range s.Certificates {
refList.Add(res)
}
for _, res := range s.Locations {
refList.Add(res)
}
sort.Sort(refList)
if len(refList) == 0 {
log.Println("nothing found to refresh, looking again in 24h")
s.refreshTimer = time.AfterFunc(24*time.Hour, s.scanForRefresh)
return
}
s.refreshOrder = refList
duration := refList[0].Interval.LastRefresh.
Add(refList[0].Interval.RefreshAfter).
Sub(time.Now())
if duration <= 5*time.Second {
duration = 5 * time.Second
}
log.Printf("next refresh planned for '%s' in %s", refList[0].Name, duration)
s.refreshTimer = time.AfterFunc(
duration,
s.refresh,
)
}
func (s *Storage) refresh() {
if len(s.refreshOrder) == 0 {
return
}
resName := s.refreshOrder[0].Name
res, err := s.Get(resName)
if err != nil {
// the resource doesn't exist anymore, so just rescan
log.Printf("resource to refresh has gone away: %s", resName)
goto rescan
}
if err := res.Refresh(s); err != nil {
log.Printf("error refreshing resource '%s': %s", res.Name(), err)
}
if err := s.store(); err != nil {
log.Printf("could not update resources: %s", err)
}
rescan:
log.Printf("rescanning for new entries")
s.scanForRefresh()
}
// addDependency adds a resource to the dependency graph.
func (s *Storage) addDependency(r Resource) error {
for _, rn := range r.DependsOn() {
@ -138,6 +217,7 @@ func (s *Storage) AddSerial(se *Serial) error {
return err
}
s.Serials[se.Name().ID] = se
s.scanForRefresh()
return s.addDependency(se)
}
@ -150,6 +230,7 @@ func (s *Storage) AddSubject(se *Subject) error {
return err
}
s.Subjects[se.Name().ID] = se
s.scanForRefresh()
return s.addDependency(se)
}
@ -159,6 +240,7 @@ func (s *Storage) AddPrivateKey(pk *PrivateKey) error {
return err
}
s.PrivateKeys[pk.Name().ID] = pk
s.scanForRefresh()
return s.addDependency(pk)
}
@ -168,6 +250,7 @@ func (s *Storage) AddPublicKey(pub *PublicKey) error {
return err
}
s.PublicKeys[pub.Name().ID] = pub
s.scanForRefresh()
return s.addDependency(pub)
}
@ -177,6 +260,7 @@ func (s *Storage) AddCertificate(cert *Certificate) error {
return err
}
s.Certificates[cert.Name().ID] = cert
s.scanForRefresh()
return s.addDependency(cert)
}
@ -186,6 +270,7 @@ func (s *Storage) AddCSR(csr *CSR) error {
return err
}
s.CSRs[csr.Name().ID] = csr
s.scanForRefresh()
return s.addDependency(csr)
}
@ -195,6 +280,7 @@ func (s *Storage) AddLocation(l *Location) error {
return err
}
s.Locations[l.Name().ID] = l
s.scanForRefresh()
return s.addDependency(l)
}
@ -203,6 +289,7 @@ func (s *Storage) AddCA(ca *CA) error {
return err
}
s.CAs[ca.Name().ID] = ca
s.scanForRefresh()
return s.addDependency(ca)
}
@ -326,6 +413,7 @@ func (s *Storage) Remove(r Resource) error {
delete(deps, r.Name().String())
}
}
s.scanForRefresh()
return nil
}
@ -364,5 +452,65 @@ func (s *Storage) Update(rn pkiadm.ResourceName) error {
return err
}
}
s.scanForRefresh()
return nil
}
// List returns all currently registered resources.
func (s *Storage) List() []Resource {
resources := []Resource{}
for _, res := range s.PrivateKeys {
resources = append(resources, res)
}
for _, res := range s.PublicKeys {
resources = append(resources, res)
}
for _, res := range s.Locations {
resources = append(resources, res)
}
for _, res := range s.Certificates {
resources = append(resources, res)
}
for _, res := range s.CSRs {
resources = append(resources, res)
}
for _, res := range s.Serials {
resources = append(resources, res)
}
for _, res := range s.Subjects {
resources = append(resources, res)
}
return resources
}
// Add adds a resource to the refreshList when it should be refreshed.
func (refList *RefreshList) Add(res Resource) {
refSet := RefreshSet{
Name: res.Name(),
Interval: res.RefreshInterval(),
}
if refSet.Interval.RefreshAfter <= 0 {
return
}
newRefList := append(*refList, refSet)
*refList = newRefList
}
// Len is the number of elements in the collection.
func (refList RefreshList) Len() int { return len(refList) }
// Less reports whether the element with
// index i should sort before the element with index j.
func (refList RefreshList) Less(i, j int) bool {
return 0 > refList[i].Interval.LastRefresh.Add(
refList[i].Interval.RefreshAfter,
).Sub(
refList[j].Interval.LastRefresh.Add(
refList[j].Interval.RefreshAfter),
)
}
// Swap swaps the elements with indexes i and j.
func (refList RefreshList) Swap(i, j int) {
refList[i], refList[j] = refList[j], refList[i]
}

View File

@ -3,14 +3,16 @@ package main
import (
"crypto/x509/pkix"
"fmt"
"time"
"github.com/gibheer/pkiadm"
)
type (
Subject struct {
ID string
Data pkix.Name
ID string
Data pkix.Name
Created time.Time
}
)
@ -26,11 +28,14 @@ func NewSubject(id string, name pkix.Name) (*Subject, error) {
// Return the unique ResourceName
func (sub *Subject) Name() pkiadm.ResourceName { return pkiadm.ResourceName{sub.ID, pkiadm.RTSubject} }
// AddDependency registers a depending resource to be retuened by Dependencies()
// Refresh must trigger a rebuild of the resource.
// This is a NOOP as it does not have any dependencies.
func (sub *Subject) Refresh(_ *Storage) error { return nil }
// RefreshInterval returns the dates and interval settings which are used to
// decide when to trigger a refresh for the resource.
func (sub *Subject) RefreshInterval() Interval { return Interval{Created: sub.Created} }
// Return the PEM output of the contained resource.
func (sub *Subject) Pem() ([]byte, error) { return []byte{}, nil }
func (sub *Subject) Checksum() []byte { return []byte{} }

View File

@ -49,6 +49,21 @@ type ResourceType uint
func (r ResourceName) String() string { return r.Type.String() + "/" + r.ID }
type ResourceNameList []ResourceName
func (r ResourceNameList) Len() int {
return len(r)
}
func (r ResourceNameList) Less(i, j int) bool {
if r[i].Type != r[j].Type {
return r[i].Type < r[j].Type
}
return r[i].ID < r[j].ID
}
func (r ResourceNameList) Swap(i, j int) {
r[i], r[j] = r[j], r[i]
}
type Filter struct{}
type ResultResource struct {
@ -56,7 +71,7 @@ type ResultResource struct {
Resources []ResourceName
}
func (c *Client) List() ([]ResourceName, error) {
func (c *Client) List() (ResourceNameList, error) {
result := ResultResource{}
if err := c.query("List", Filter{}, &result); err != nil {
return []ResourceName{}, err