From 70515f6fa6e4490b7ef9149342b45c60b794ebd4 Mon Sep 17 00:00:00 2001 From: Gibheer Date: Fri, 14 Jul 2017 20:41:03 +0200 Subject: [PATCH] add basic CA support This is the intial CA support, but not yet connected with the certificate signing. --- ca.go | 88 +++++++++++++++++++++++++++ cmd/pkiadm/ca.go | 96 +++++++++++++++++++++++++++++ cmd/pkiadm/main.go | 15 +++++ cmd/pkiadmd/ca.go | 135 +++++++++++++++++++++++++++++++++++++++++ cmd/pkiadmd/storage.go | 25 ++++++++ transport.go | 1 + 6 files changed, 360 insertions(+) create mode 100644 ca.go create mode 100644 cmd/pkiadm/ca.go create mode 100644 cmd/pkiadmd/ca.go diff --git a/ca.go b/ca.go new file mode 100644 index 0000000..907f830 --- /dev/null +++ b/ca.go @@ -0,0 +1,88 @@ +package pkiadm + +import ( + "strings" +) + +const ( + CALocal CAType = iota + CALetsEncrypt + CAUnknown +) + +type ( + CAType uint + + CA struct { + ID string + Type CAType + Certificate ResourceName + } + ResultCA struct { + Result Result + CAs []CA + } + CAChange struct { + CA CA + FieldList []string + } +) + +// CreateCA sends a RPC request to create a new private key. +func (c *Client) CreateCA(pk CA) error { + return c.exec("CreateCA", pk) +} +func (c *Client) SetCA(pk CA, fieldList []string) error { + changeset := CAChange{pk, fieldList} + return c.exec("SetCA", changeset) +} +func (c *Client) DeleteCA(id string) error { + pk := ResourceName{ID: id, Type: RTCA} + return c.exec("DeleteCA", pk) +} +func (c *Client) ListCA() ([]CA, error) { + result := &ResultCA{} + if err := c.query("ListCA", Filter{}, result); err != nil { + return []CA{}, err + } + if result.Result.HasError { + return []CA{}, result.Result.Error + } + return result.CAs, nil +} +func (c *Client) ShowCA(id string) (CA, error) { + ca := ResourceName{ID: id, Type: RTCA} + result := &ResultCA{} + if err := c.query("ShowCA", ca, result); err != nil { + return CA{}, err + } + if result.Result.HasError { + return CA{}, result.Result.Error + } + for _, privateKey := range result.CAs { + return privateKey, nil + } + return CA{}, nil +} + +func (ct CAType) String() string { + switch ct { + case CALocal: + return "local" + case CALetsEncrypt: + return "LetsEncrypt" + default: + return "unknown" + } +} + +func StringToCAType(in string) CAType { + switch strings.ToLower(in) { + case "local": + return CALocal + case "letsencrypt": + return CALetsEncrypt + default: + return CAUnknown + } +} diff --git a/cmd/pkiadm/ca.go b/cmd/pkiadm/ca.go new file mode 100644 index 0000000..5978f10 --- /dev/null +++ b/cmd/pkiadm/ca.go @@ -0,0 +1,96 @@ +package main + +import ( + "fmt" + "os" + "text/tabwriter" + + "github.com/gibheer/pkiadm" + "github.com/pkg/errors" + flag "github.com/spf13/pflag" +) + +func createCA(args []string, client *pkiadm.Client) error { + fs := flag.NewFlagSet("pkiadm create-public", flag.ExitOnError) + id := fs.String("id", "", "the id to set for the CA") + ct := fs.String("type", "local", "the type of CA to create (local, LetsEncrypt)") + cert := fs.String("certificate", "", "the id of the certificate to use for CA creation") + fs.Parse(args) + + caType := pkiadm.StringToCAType(*ct) + if caType == pkiadm.CAUnknown { + return errors.New("unknown ca type") + } + caName := pkiadm.ResourceName{ID: *cert, Type: pkiadm.RTCertificate} + if err := client.CreateCA( + pkiadm.CA{ID: *id, Type: caType, Certificate: caName}, + ); err != nil { + return errors.Wrap(err, "Could not create CA") + } + return nil +} +func setCA(args []string, client *pkiadm.Client) error { + fs := flag.NewFlagSet("pkiadm set-public", flag.ExitOnError) + id := fs.String("id", "", "the id of the CA to change") + pk := fs.String("private-key", "", "the id of the new private key to use for CA generation") + fs.Parse(args) + + if !fs.Lookup("private-key").Changed { + return nil + } + caName := pkiadm.ResourceName{ID: *pk, Type: pkiadm.RTPrivateKey} + if err := client.SetCA( + pkiadm.CA{ID: *id, Certificate: caName}, + []string{"private-key"}, + ); err != nil { + return errors.Wrap(err, "Could not change CA") + } + return nil +} +func deleteCA(args []string, client *pkiadm.Client) error { + fs := flag.NewFlagSet("pkiadm delete-public", flag.ExitOnError) + id := fs.String("id", "", "the id of the CA to delete") + fs.Parse(args) + + if err := client.DeleteCA(*id); err != nil { + return errors.Wrap(err, "Could not delete CA") + } + return nil +} +func listCA(args []string, client *pkiadm.Client) error { + fs := flag.NewFlagSet("list-private", flag.ExitOnError) + fs.Parse(args) + + cas, err := client.ListCA() + if err != nil { + return err + } + + if len(cas) == 0 { + return nil + } + out := tabwriter.NewWriter(os.Stdout, 2, 2, 1, ' ', tabwriter.AlignRight) + fmt.Fprintf(out, "%s\t%s\t%s\t\n", "id", "type", "certificate") + for _, ca := range cas { + fmt.Fprintf(out, "%s\t%s\t%s\t\n", ca.ID, ca.Type.String(), ca.Certificate.ID) + } + out.Flush() + + return nil +} +func showCA(args []string, client *pkiadm.Client) error { + fs := flag.NewFlagSet("show-private", flag.ExitOnError) + var id = fs.String("id", "", "set the id of the private key to show") + fs.Parse(args) + + ca, err := client.ShowCA(*id) + if err != nil { + return err + } + out := tabwriter.NewWriter(os.Stdout, 2, 2, 1, ' ', tabwriter.AlignRight) + fmt.Fprintf(out, "ID:\t%s\t\n", ca.ID) + fmt.Fprintf(out, "type:\t%s\t\n", ca.Type.String()) + fmt.Fprintf(out, "certificate:\t%s\t\n", ca.Certificate.ID) + out.Flush() + return nil +} diff --git a/cmd/pkiadm/main.go b/cmd/pkiadm/main.go index 0b6ea8f..4138992 100644 --- a/cmd/pkiadm/main.go +++ b/cmd/pkiadm/main.go @@ -43,6 +43,16 @@ func main() { err = setSerial(args, client) case `show-serial`: err = showSerial(args, client) + case `create-ca`: + err = createCA(args, client) + case `delete-ca`: + err = deleteCA(args, client) + case `list-ca`: + err = listCA(args, client) + case `set-ca`: + err = setCA(args, client) + case `show-ca`: + err = showCA(args, client) case `create-subj`: err = createSubject(args, client) case `delete-subj`: @@ -118,6 +128,7 @@ func printCommands() { fmt.Println(`Usage: pkiadm [options] where subcommand is one of:`) out := tabwriter.NewWriter(os.Stdout, 0, 4, 1, ' ', 0) + fmt.Fprintf(out, " %s\t%s\n", "create-ca", "create a new CA") fmt.Fprintf(out, " %s\t%s\n", "create-cert", "create a new certificate") fmt.Fprintf(out, " %s\t%s\n", "create-csr", "create a new certificate sign request") fmt.Fprintf(out, " %s\t%s\n", "create-location", "create a new file export") @@ -126,6 +137,7 @@ where subcommand is one of:`) fmt.Fprintf(out, " %s\t%s\n", "create-serial", "") fmt.Fprintf(out, " %s\t%s\n", "create-subj", "") + fmt.Fprintf(out, " %s\t%s\n", "delete-ca", "delete a CA") fmt.Fprintf(out, " %s\t%s\n", "delete-cert", "") fmt.Fprintf(out, " %s\t%s\n", "delete-csr", "") fmt.Fprintf(out, " %s\t%s\n", "delete-location", "") @@ -135,6 +147,7 @@ where subcommand is one of:`) fmt.Fprintf(out, " %s\t%s\n", "delete-subj", "") fmt.Fprintf(out, " %s\t%s\n", "list", "") + fmt.Fprintf(out, " %s\t%s\n", "list-ca", "list all available CAs") fmt.Fprintf(out, " %s\t%s\n", "list-cert", "list all available certificates") fmt.Fprintf(out, " %s\t%s\n", "list-csr", "list all available certificate sign requests") fmt.Fprintf(out, " %s\t%s\n", "list-location", "list all file exports") @@ -143,6 +156,7 @@ where subcommand is one of:`) fmt.Fprintf(out, " %s\t%s\n", "list-serial", "") fmt.Fprintf(out, " %s\t%s\n", "list-subj", "") + fmt.Fprintf(out, " %s\t%s\n", "set-ca", "change attributes of a CA") fmt.Fprintf(out, " %s\t%s\n", "set-cert", "change attributes of a certificate") fmt.Fprintf(out, " %s\t%s\n", "set-csr", "change attributes of a certificate sign request") fmt.Fprintf(out, " %s\t%s\n", "set-location", "change attributes of a location") @@ -151,6 +165,7 @@ where subcommand is one of:`) fmt.Fprintf(out, " %s\t%s\n", "set-serial", "") fmt.Fprintf(out, " %s\t%s\n", "set-subj", "") + fmt.Fprintf(out, " %s\t%s\n", "show-ca", "") fmt.Fprintf(out, " %s\t%s\n", "show-cert", "") fmt.Fprintf(out, " %s\t%s\n", "show-csr", "") fmt.Fprintf(out, " %s\t%s\n", "show-location", "") diff --git a/cmd/pkiadmd/ca.go b/cmd/pkiadmd/ca.go new file mode 100644 index 0000000..0f043ac --- /dev/null +++ b/cmd/pkiadmd/ca.go @@ -0,0 +1,135 @@ +package main + +import ( + "github.com/gibheer/pkiadm" +) + +type ( + // CA is an instance that can sign certificates. When a certificate needs an + // update, the given CSR is signed by the CA. + // A CA can be responsible for multiple certificates to sign. + CA struct { + ID string + Type pkiadm.CAType + Certificate pkiadm.ResourceName + } +) + +func NewCA(id string, caType pkiadm.CAType, cert pkiadm.ResourceName) (*CA, error) { + ca := &CA{ + ID: id, + Type: caType, + Certificate: cert, + } + return ca, nil +} + +// Return the unique ResourceName +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. +func (ca *CA) Refresh(*Storage) error { + return nil +} + +// Return the PEM output of the contained resource. +func (ca *CA) Pem() ([]byte, error) { return []byte{}, nil } +func (ca *CA) Checksum() []byte { return []byte{} } + +// DependsOn must return the resource names it is depending on. +func (ca *CA) DependsOn() []pkiadm.ResourceName { + return []pkiadm.ResourceName{ + ca.Certificate, + } +} + +func (ca *CA) Sign(csr *CSR) (*Certificate, error) { + return nil, nil +} + +func (s *Server) CreateCA(inCA pkiadm.CA, res *pkiadm.Result) error { + s.lock() + defer s.unlock() + + ca, err := NewCA(inCA.ID, inCA.Type, inCA.Certificate) + if err != nil { + res.SetError(err, "could not create CA '%s'", inCA.ID) + return nil + } + if err := s.storage.AddCA(ca); err != nil { + res.SetError(err, "could not add CA '%s'", inCA.ID) + return nil + } + return s.store(res) +} + +func (s *Server) SetCA(change pkiadm.CAChange, res *pkiadm.Result) error { + s.lock() + defer s.unlock() + + ca, err := s.storage.GetCA(pkiadm.ResourceName{ID: change.CA.ID, Type: pkiadm.RTCA}) + if err != nil { + res.SetError(err, "could not find CA '%s'", change.CA.ID) + return nil + } + for _, field := range change.FieldList { + switch field { + case "type": + ca.Type = change.CA.Type + case "certificate": + ca.Certificate = change.CA.Certificate + } + } + return s.store(res) +} + +func (s *Server) DeleteCA(inCA pkiadm.CA, res *pkiadm.Result) error { + s.lock() + defer s.unlock() + + ca, err := s.storage.GetCA(pkiadm.ResourceName{inCA.ID, pkiadm.RTCA}) + if err != nil { + res.SetError(err, "Could not find ca '%s'", ca.ID) + return nil + } + + if err := s.storage.Remove(ca); err != nil { + res.SetError(err, "Could not remove ca '%s'", ca.ID) + return nil + } + return s.store(res) +} + +func (s *Server) ShowCA(inCA pkiadm.CA, res *pkiadm.ResultCA) error { + s.lock() + defer s.unlock() + + ca, err := s.storage.GetCA(pkiadm.ResourceName{ID: inCA.ID, Type: pkiadm.RTCA}) + if err != nil { + res.Result.SetError(err, "Could not find private key '%s'", inCA.ID) + return nil + } + res.CAs = []pkiadm.CA{pkiadm.CA{ + ID: ca.ID, + Type: ca.Type, + Certificate: ca.Certificate, + }} + return nil +} + +func (s *Server) ListCA(filter pkiadm.Filter, res *pkiadm.ResultCA) error { + s.lock() + defer s.unlock() + + for _, ca := range s.storage.CAs { + res.CAs = append(res.CAs, pkiadm.CA{ + ID: ca.ID, + Type: ca.Type, + Certificate: ca.Certificate, + }) + } + return nil +} diff --git a/cmd/pkiadmd/storage.go b/cmd/pkiadmd/storage.go index 6d84219..b3e2015 100644 --- a/cmd/pkiadmd/storage.go +++ b/cmd/pkiadmd/storage.go @@ -24,6 +24,7 @@ type ( CSRs map[string]*CSR Serials map[string]*Serial Subjects map[string]*Subject + CAs map[string]*CA // dependencies maps from a resource name to all resources which depend // on it. dependencies map[string]map[string]Resource @@ -42,6 +43,7 @@ func NewStorage(path string) (*Storage, error) { CSRs: map[string]*CSR{}, Serials: map[string]*Serial{}, Subjects: map[string]*Subject{}, + CAs: map[string]*CA{}, dependencies: map[string]map[string]Resource{}, } if err := s.load(); err != nil { @@ -93,6 +95,9 @@ func (s *Storage) refreshDependencies() error { for _, l := range s.Locations { _ = s.addDependency(l) } + for _, ca := range s.CAs { + _ = s.addDependency(ca) + } return nil } @@ -193,6 +198,14 @@ func (s *Storage) AddLocation(l *Location) error { return s.addDependency(l) } +func (s *Storage) AddCA(ca *CA) error { + if err := ca.Refresh(s); err != nil { + return err + } + s.CAs[ca.Name().ID] = ca + return s.addDependency(ca) +} + // Get figures out the resource to the ResourceName if available. func (s *Storage) Get(r pkiadm.ResourceName) (Resource, error) { if r.ID == "" { @@ -213,6 +226,8 @@ func (s *Storage) Get(r pkiadm.ResourceName) (Resource, error) { return s.GetCertificate(r) case pkiadm.RTLocation: return s.GetLocation(r) + case pkiadm.RTCA: + return s.GetCA(r) default: return nil, EUnknownType } @@ -274,6 +289,14 @@ func (s *Storage) GetLocation(r pkiadm.ResourceName) (*Location, error) { return nil, errors.Wrapf(ENotFound, "no location with id '%s' found", r) } +// GetCA returns the CA matching the resource name. +func (s *Storage) GetCA(r pkiadm.ResourceName) (*CA, error) { + if res, found := s.CAs[r.ID]; found { + return res, nil + } + return nil, errors.Wrapf(ENotFound, "no CA with id '%s' found", r) +} + // Remove takes a resource and removes it from the system. func (s *Storage) Remove(r Resource) error { // TODO implement unable to remove when having dependencies @@ -292,6 +315,8 @@ func (s *Storage) Remove(r Resource) error { delete(s.Certificates, r.Name().ID) case pkiadm.RTLocation: delete(s.Locations, r.Name().ID) + case pkiadm.RTCA: + delete(s.CAs, r.Name().ID) default: return EUnknownType } diff --git a/transport.go b/transport.go index a40203a..64abb25 100644 --- a/transport.go +++ b/transport.go @@ -38,6 +38,7 @@ const ( RTSerial RTSubject RTUnknown + RTCA ) type ResourceName struct {