From 039f72c3d59808d4b825a7b7be09060a9a6454a2 Mon Sep 17 00:00:00 2001 From: Gibheer Date: Sun, 28 May 2017 11:33:04 +0200 Subject: [PATCH] initial commit The basic server and client are working and it is possible to add, list, show, set and remove subjects. Locations are not yet written to the filesystem yet and need to be fixed. --- client.go | 54 +++++ cmd/pkiadm/main.go | 92 ++++++++ cmd/pkiadm/subject.go | 154 +++++++++++++ cmd/pkiadmd/certificate.go | 129 +++++++++++ cmd/pkiadmd/crypto.go | 11 + cmd/pkiadmd/csr.go | 103 +++++++++ cmd/pkiadmd/location.go | 71 ++++++ cmd/pkiadmd/main.go | 109 +++++++++ cmd/pkiadmd/private_key.go | 150 ++++++++++++ cmd/pkiadmd/public_key.go | 77 +++++++ cmd/pkiadmd/publickeytype_string.go | 16 ++ cmd/pkiadmd/resourcetype_string.go | 16 ++ cmd/pkiadmd/serial.go | 56 +++++ cmd/pkiadmd/server.go | 139 ++++++++++++ cmd/pkiadmd/storage.go | 340 ++++++++++++++++++++++++++++ cmd/pkiadmd/subject.go | 41 ++++ config.go | 46 ++++ pkiadm.json | 4 + resourcetype_string.go | 16 ++ subject.go | 62 +++++ transport.go | 50 ++++ 21 files changed, 1736 insertions(+) create mode 100644 client.go create mode 100644 cmd/pkiadm/main.go create mode 100644 cmd/pkiadm/subject.go create mode 100644 cmd/pkiadmd/certificate.go create mode 100644 cmd/pkiadmd/crypto.go create mode 100644 cmd/pkiadmd/csr.go create mode 100644 cmd/pkiadmd/location.go create mode 100644 cmd/pkiadmd/main.go create mode 100644 cmd/pkiadmd/private_key.go create mode 100644 cmd/pkiadmd/public_key.go create mode 100644 cmd/pkiadmd/publickeytype_string.go create mode 100644 cmd/pkiadmd/resourcetype_string.go create mode 100644 cmd/pkiadmd/serial.go create mode 100644 cmd/pkiadmd/server.go create mode 100644 cmd/pkiadmd/storage.go create mode 100644 cmd/pkiadmd/subject.go create mode 100644 config.go create mode 100644 pkiadm.json create mode 100644 resourcetype_string.go create mode 100644 subject.go create mode 100644 transport.go diff --git a/client.go b/client.go new file mode 100644 index 0000000..9944b25 --- /dev/null +++ b/client.go @@ -0,0 +1,54 @@ +package pkiadm + +import ( + "fmt" + "net/rpc" +) + +const ( + // ProtoIdent is the name of the resource container provided by the server. + ProtoIdent = "pkiadm" +) + +type ( + Client struct { + c *rpc.Client + } +) + +// Create a new Client instance using the provided configuration. +func NewClient(cfg Config) (*Client, error) { + conn, err := rpc.Dial("unix", cfg.Path) + if err != nil { + return nil, err + } + return &Client{conn}, nil +} + +// Close the client connection with the server. When the Connection is already +// closed, the returned error will be net.rpc.ErrShutdown. +func (c *Client) Close() error { + return c.c.Close() +} + +// Exec sends `cmd` to the server with the given input and evaluates the +// standard Result for error messages. +// When one is found, the error is returned. +func (c *Client) exec(cmd string, input interface{}) error { + result := &Result{} + if err := c.c.Call(fmt.Sprintf("%s.%s", ProtoIdent, cmd), input, result); err != nil { + return err + } + if result.HasError { + return result.Error + } + return nil +} + +// query can be used to call a function returning a result set. +func (c *Client) query(cmd string, input interface{}, result interface{}) error { + if err := c.c.Call(fmt.Sprintf("%s.%s", ProtoIdent, cmd), input, result); err != nil { + return err + } + return nil +} diff --git a/cmd/pkiadm/main.go b/cmd/pkiadm/main.go new file mode 100644 index 0000000..4faaae0 --- /dev/null +++ b/cmd/pkiadm/main.go @@ -0,0 +1,92 @@ +package main + +import ( + "fmt" + "os" + "text/tabwriter" + + "github.com/gibheer/pkiadm" +) + +func main() { + cfg, err := pkiadm.LoadConfig() + if err != nil { + fmt.Printf("could not load config: %s\n", err) + os.Exit(2) + } + + client, err := pkiadm.NewClient(*cfg) + if err != nil { + fmt.Printf("Could not open connection to server: %s\n", err) + os.Exit(2) + } + defer client.Close() + + if len(os.Args) == 1 { + printCommands() + os.Exit(0) + } + + cmd := os.Args[1] + args := os.Args[2:] + switch cmd { + case `create-subj`: + err = createSubject(args, client) + case `delete-subj`: + err = deleteSubject(args, client) + case `list-subj`: + err = listSubject(args, client) + case `set-subj`: + err = setSubject(args, client) + case `show-subj`: + err = showSubject(args, client) + // case `list`: + // err = listDescription(args, client) + // case `create-file`: + // err = createFile(args, client) + // case `list-files`: + // err = listFile(args, client) + // case `delete-file`: + // err = deleteFile(args, client) + // case `create-private-key`: + // err = createPrivateKey(args, client) + // case `get-private-key`: + // err = getPrivateKey(args, client) + // case `list-private-keys`: + // err = listPrivateKey(args, client) + // case `delete-private-key`: + // err = deletePrivateKey(args, client) + // case `create-public-key`: + // err = createPublicKey(args, client) + // case `list-public-keys`: + // err = listPublicKey(args, client) + // case `delete-public-key`: + // err = deletePublicKey(args, client) + default: + fmt.Printf("unknown subcommand '%s'\n", cmd) + printCommands() + os.Exit(0) + } + if err != nil { + fmt.Printf("received an error: %s\n", err) + os.Exit(1) + } +} + +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", "def-list", "list all registered definitions") + fmt.Fprintf(out, " %s\t%s\n", "create-file", "create a new file export") + fmt.Fprintf(out, " %s\t%s\n", "list-files", "list all file exports") + fmt.Fprintf(out, " %s\t%s\n", "delete-file", "delete a file export from the database and os") + fmt.Fprintf(out, " %s\t%s\n", "create-private-key", "create a new private key") + fmt.Fprintf(out, " %s\t%s\n", "list-private-keys", "list all private keys") + fmt.Fprintf(out, " %s\t%s\n", "get-private-key", "get information on a specific private key") + fmt.Fprintf(out, " %s\t%s\n", "delete-private-key", "delete a specific private key") + fmt.Fprintf(out, " %s\t%s\n", "create-public-key", "create a new public key") + fmt.Fprintf(out, " %s\t%s\n", "list-public-keys", "list all public keys") + fmt.Fprintf(out, " %s\t%s\n", "delete-public-key", "delete a specific public key") + out.Flush() +} diff --git a/cmd/pkiadm/subject.go b/cmd/pkiadm/subject.go new file mode 100644 index 0000000..91a63b2 --- /dev/null +++ b/cmd/pkiadm/subject.go @@ -0,0 +1,154 @@ +package main + +import ( + "fmt" + "os" + "strings" + "text/tabwriter" + + "github.com/gibheer/pkiadm" + "github.com/pkg/errors" + flag "github.com/spf13/pflag" +) + +func createSubject(args []string, client *pkiadm.Client) error { + fs := flag.NewFlagSet("pkiadm create-subj", flag.ExitOnError) + fs.Usage = func() { + fmt.Printf("Usage of %s:\n", "pkiadm create-subj") + fmt.Println(` +Create a new subject which can then be used in the CSR. All fields +are optional and can be provided multiple times to add multiple instances. +In most cases only the common name, organization name and the country is provided. +`) + fs.PrintDefaults() + } + subj := pkiadm.Subject{} + fs.StringVar(&subj.ID, "id", "", "the unique subject id") + setSubjectParams(&subj, fs) + fs.Parse(args) + + if subj.ID == "" { + return errors.New("no ID given") + } + + if err := client.CreateSubject(subj); err != nil { + fmt.Println("got an error") + return errors.Wrap(err, "could not create new subject") + } + return nil +} + +func setSubject(args []string, client *pkiadm.Client) error { + fs := flag.NewFlagSet("pkiadm set-subjprop", flag.ExitOnError) + subj := pkiadm.Subject{} + fs.StringVar(&subj.ID, "id", "", "set the ID to edit") + setSubjectParams(&subj, fs) + fs.Parse(args) + + fieldList := []string{} + for _, field := range []string{"common-name", "org", "org-unit", "locality", "province", "street", "code"} { + flag := fs.Lookup(field) + if flag.Changed { + fieldList = append(fieldList, field) + } + } + + if err := client.SetSubject(subj, fieldList); err != nil { + return err + } + return nil +} + +func deleteSubject(args []string, client *pkiadm.Client) error { + fs := flag.NewFlagSet("pkiadm delete-subj", flag.ExitOnError) + var ( + id = fs.String("id", "", "the id to delete") + ) + fs.Parse(args) + + if err := client.DeleteSubject(*id); err != nil { + return err + } + return nil +} + +func listSubject(args []string, client *pkiadm.Client) error { + fs := flag.NewFlagSet("pkiadm list-subj", flag.ExitOnError) + fs.Parse(args) + + res, err := client.ListSubject() + if err != nil { + return err + } + + if len(res) == 0 { + return nil + } + out := tabwriter.NewWriter(os.Stdout, 2, 2, 1, ' ', 0) + fmt.Fprintf(out, "ID\tserial\tcommon name\torganization\torg-unit\tlocality\tprovince\tstreet\tpostal\n") + for _, subj := range res { + fmt.Fprintf( + out, + "%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s\n", + subj.ID, + ReplaceEmpty(subj.Name.SerialNumber), + ReplaceEmpty(subj.Name.CommonName), + ReplaceEmpty(strings.Join(subj.Name.Organization, ", ")), + ReplaceEmpty(strings.Join(subj.Name.OrganizationalUnit, ", ")), + ReplaceEmpty(strings.Join(subj.Name.Locality, ", ")), + ReplaceEmpty(strings.Join(subj.Name.Province, ", ")), + ReplaceEmpty(strings.Join(subj.Name.StreetAddress, ", ")), + ReplaceEmpty(strings.Join(subj.Name.PostalCode, ", ")), + ) + } + out.Flush() + return nil +} + +// ShowSubject prints all fields of a subject. +func showSubject(args []string, client *pkiadm.Client) error { + fs := flag.NewFlagSet("pkiadm show-subj", flag.ExitOnError) + var ( + id = fs.String("id", "", "the identifier of the subject to show") + ) + fs.Parse(args) + + subj, err := client.ShowSubject(*id) + if err != nil { + return err + } + out := tabwriter.NewWriter(os.Stdout, 2, 2, 1, ' ', tabwriter.AlignRight) + fmt.Fprintf(out, "ID:\t%s\t\n", subj.ID) + fmt.Fprintf(out, "serial:\t%s\t\n", ReplaceEmpty(subj.Name.SerialNumber)) + fmt.Fprintf(out, "common name:\t%s\t\n", ReplaceEmpty(subj.Name.CommonName)) + fmt.Fprintf(out, "organization:\t%s\t\n", ReplaceEmpty(strings.Join(subj.Name.Organization, ", "))) + fmt.Fprintf(out, "org-unit:\t%s\t\n", ReplaceEmpty(strings.Join(subj.Name.OrganizationalUnit, ", "))) + fmt.Fprintf(out, "locality:\t%s\t\n", ReplaceEmpty(strings.Join(subj.Name.Locality, ", "))) + fmt.Fprintf(out, "province:\t%s\t\n", ReplaceEmpty(strings.Join(subj.Name.Province, ", "))) + fmt.Fprintf(out, "street:\t%s\t\n", ReplaceEmpty(strings.Join(subj.Name.StreetAddress, ", "))) + fmt.Fprintf(out, "postal code:\t%s\t\n", ReplaceEmpty(strings.Join(subj.Name.PostalCode, ", "))) + out.Flush() + return nil +} + +// ReplaceEmpty replaces an empty string with a dash sign to visually show, that +// the field is empty and not an empty string. +func ReplaceEmpty(in string) string { + if in != "" { + return in + } + return "-" +} + +// SetSubject adds the common flags for createSubject and setSubject. +func setSubjectParams(subj *pkiadm.Subject, fs *flag.FlagSet) { + fs.StringVar(&subj.Name.SerialNumber, "serial", "", "set a serial number for the subject") + fs.StringVar(&subj.Name.CommonName, "common-name", "", "set a unique and human understandable identifier for the subject") + fs.StringSliceVar(&subj.Name.Country, "country", []string{}, "set countries as short codes or long names") + fs.StringSliceVar(&subj.Name.Organization, "org", []string{}, "set the organization names") + fs.StringSliceVar(&subj.Name.OrganizationalUnit, "org-unit", []string{}, "set the sub division or organizational units") + fs.StringSliceVar(&subj.Name.Locality, "locality", []string{}, "set the city where the organization is located") + fs.StringSliceVar(&subj.Name.Province, "province", []string{}, "set the province, region or state of the organization") + fs.StringSliceVar(&subj.Name.StreetAddress, "street", []string{}, "set the street for the organization") + fs.StringSliceVar(&subj.Name.PostalCode, "code", []string{}, "set the postal code for the address") +} diff --git a/cmd/pkiadmd/certificate.go b/cmd/pkiadmd/certificate.go new file mode 100644 index 0000000..512083c --- /dev/null +++ b/cmd/pkiadmd/certificate.go @@ -0,0 +1,129 @@ +package main + +import ( + "encoding/pem" + "time" + + "github.com/gibheer/pki" +) + +type ( + Certificate struct { + ID string + + IsCA bool + Duration time.Duration + + PrivateKey ResourceName + Serial ResourceName + CSR ResourceName + CA ResourceName + + Data []byte + } +) + +func NewCertificate(id string, privateKey, serial, csr, ca ResourceName, isCA bool, duration time.Duration) (*Certificate, error) { + return &Certificate{ + ID: id, + PrivateKey: privateKey, + Serial: serial, + CSR: csr, + CA: ca, + IsCA: isCA, + Duration: duration, + }, nil +} + +// Return the unique ResourceName +func (c *Certificate) Name() ResourceName { + return ResourceName{c.ID, RTCertificate} +} + +// AddDependency registers a depending resource to be retuened by Dependencies() +// Refresh must trigger a rebuild of the resource. +func (c *Certificate) Refresh(lookup *Storage) error { + var ca *pki.Certificate + if !c.IsCA { + cert, err := lookup.GetCertificate(c.CA) + if err != nil { + return err + } + ca, err = cert.GetCertificate() + if err != nil { + return err + } + } + csrRes, err := lookup.GetCSR(c.CSR) + if err != nil { + return err + } + csr, err := csrRes.GetCSR() + if err != nil { + return err + } + pkRes, err := lookup.GetPrivateKey(c.PrivateKey) + if err != nil { + return err + } + pk, err := pkRes.GetKey() + if err != nil { + return err + } + serRes, err := lookup.GetSerial(c.Serial) + if err != nil { + return err + } + serial, err := serRes.Generate() + if err != nil { + return err + } + + // now we can start with the real interesting stuff + // TODO add key usage and that stuff + opts := pki.CertificateOptions{ + SerialNumber: serial, + NotBefore: time.Now(), + NotAfter: time.Now().Add(c.Duration), + IsCA: c.IsCA, + CALength: 0, // TODO make this an option + } + cert, err := csr.ToCertificate(pk, opts, ca) + if err != nil { + return err + } + block, err := cert.ToPem() + if err != nil { + return err + } + block.Headers = map[string]string{"ID": c.ID} + c.Data = pem.EncodeToMemory(&block) + return nil +} + +func (c *Certificate) GetCertificate() (*pki.Certificate, error) { + // TODO fix this, we must check if there is anything else + block, _ := pem.Decode(c.Data) + cert, err := pki.LoadCertificate(block.Bytes) + if err != nil { + return nil, err + } + return cert, nil +} + +// Return the PEM output of the contained resource. +func (c *Certificate) Pem() ([]byte, error) { return c.Data, nil } +func (c *Certificate) Checksum() []byte { return Hash(c.Data) } + +// DependsOn must return the resource names it is depending on. +func (c *Certificate) DependsOn() []ResourceName { + res := []ResourceName{ + c.PrivateKey, + c.Serial, + c.CSR, + } + if !c.IsCA { + res = append(res, c.CA) + } + return res +} diff --git a/cmd/pkiadmd/crypto.go b/cmd/pkiadmd/crypto.go new file mode 100644 index 0000000..e36a506 --- /dev/null +++ b/cmd/pkiadmd/crypto.go @@ -0,0 +1,11 @@ +package main + +import ( + "crypto/sha512" + "encoding/base64" +) + +func Hash(in []byte) []byte { + raw := sha512.Sum512(in) + return []byte(base64.StdEncoding.EncodeToString(raw[:])) +} diff --git a/cmd/pkiadmd/csr.go b/cmd/pkiadmd/csr.go new file mode 100644 index 0000000..38bf3d5 --- /dev/null +++ b/cmd/pkiadmd/csr.go @@ -0,0 +1,103 @@ +package main + +import ( + "encoding/pem" + "net" + + "github.com/gibheer/pki" +) + +type ( + CSR struct { + // ID is the unique identifier of the CSR. + ID string + + // The following options are used to generate the content of the CSR. + CommonName string + DNSNames []string + EmailAddresses []string + IPAddresses []net.IP + + // PrivateKey is needed to sign the certificate sign request. + PrivateKey ResourceName + Subject ResourceName + + // Data contains the pem representation of the CSR. + Data []byte + } +) + +// NewCSR creates a new CSR. +func NewCSR(id string, pk, subject ResourceName, commonName string, dnsNames []string, + emailAddresses []string, iPAddresses []net.IP) (*CSR, error) { + return &CSR{ + ID: id, + Subject: subject, + CommonName: commonName, + DNSNames: dnsNames, + EmailAddresses: emailAddresses, + IPAddresses: iPAddresses, + PrivateKey: pk, + }, nil +} + +// Return the unique ResourceName +func (c *CSR) Name() ResourceName { + return ResourceName{c.ID, RTCSR} +} + +// AddDependency registers a depending resource to be retuened by Dependencies() +// Refresh must trigger a rebuild of the resource. +func (c *CSR) Refresh(lookup *Storage) error { + pk, err := lookup.GetPrivateKey(c.PrivateKey) + if err != nil { + return err + } + key, err := pk.GetKey() + if err != nil { + return err + } + subjRes, err := lookup.GetSubject(c.Subject) + if err != nil { + return err + } + subject := subjRes.GetName() + subject.CommonName = c.CommonName + + opts := pki.CertificateData{ + Subject: subject, + DNSNames: c.DNSNames, + EmailAddresses: c.EmailAddresses, + IPAddresses: c.IPAddresses, + } + csr, err := opts.ToCertificateRequest(key) + if err != nil { + return err + } + block, err := csr.ToPem() + if err != nil { + return err + } + block.Headers = map[string]string{"ID": c.ID} + c.Data = pem.EncodeToMemory(&block) + return nil +} + +// 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) } + +// DependsOn must return the resource names it is depending on. +func (c *CSR) DependsOn() []ResourceName { + return []ResourceName{c.PrivateKey} +} + +func (c *CSR) GetCSR() (*pki.CertificateRequest, error) { + // TODO fix this, we must check if there is anything else + block, _ := pem.Decode(c.Data) + csr, err := pki.LoadCertificateSignRequest(block.Bytes) + if err != nil { + return nil, err + } + return csr, nil +} diff --git a/cmd/pkiadmd/location.go b/cmd/pkiadmd/location.go new file mode 100644 index 0000000..a5ae157 --- /dev/null +++ b/cmd/pkiadmd/location.go @@ -0,0 +1,71 @@ +package main + +import ( + "fmt" +) + +const ( + ENoPathGiven = Error("no path given") +) + +type ( + Location struct { + ID string + + Path string + Dependencies []ResourceName + } +) + +func NewLocation(id, path string, res []ResourceName) (*Location, error) { + if id == "" { + return nil, ENoIDGiven + } + if path == "" { + return nil, ENoPathGiven + } + l := &Location{ + ID: id, + Path: path, + Dependencies: res, + } + return l, nil +} + +func (l *Location) Name() ResourceName { + return ResourceName{l.ID, RTLocation} +} + +// Refresh writes all resources into the single file. +func (l *Location) Refresh(lookup *Storage) error { + raw := []byte{} + for _, rn := range l.DependsOn() { + r, err := lookup.Get(rn) + if err != nil { + return err + } + output, err := r.Pem() + if err != nil { + return err + } + raw = append(raw, output...) + } + // TODO write to file + fmt.Printf("found %d characters for file: %s\n", len(raw), l.Path) + return nil +} + +func (l *Location) DependsOn() []ResourceName { return l.Dependencies } + +// Pem is not used by location, as it does not contain any data. +func (l *Location) Pem() ([]byte, error) { return []byte{}, nil } + +// Checksum is not used by Location, as it does not contain any data. +func (l *Location) Checksum() []byte { return []byte{} } + +//func (l *Location) MarshalJSON() ([]byte, error) { +// return json.Marshal(*l) +//} +//func (l *Location) UnmarshalJSON(raw []byte) error { +// return json.Unmarshal(raw, l) +//} diff --git a/cmd/pkiadmd/main.go b/cmd/pkiadmd/main.go new file mode 100644 index 0000000..159e313 --- /dev/null +++ b/cmd/pkiadmd/main.go @@ -0,0 +1,109 @@ +package main + +import ( + "log" + "net" + "net/rpc" + "os" + "os/signal" + + "github.com/gibheer/pkiadm" +) + +const ( + RTPrivateKey ResourceType = iota + RTPublicKey + RTCSR + RTCertificate + RTLocation + RTSerial + RTSubject +) + +const ( + ENoIDGiven = Error("no ID given") + EUnknownType = Error("unknown type found") + ENotInitialized = Error("resource not initialized") + ENotFound = Error("resource not found") + EWrongType = Error("incompatible type found - please report error") + EAlreadyExist = Error("resource already exists") +) + +type ( + Resource interface { + // Return the unique ResourceName + Name() ResourceName + // AddDependency registers a depending resource to be retuened by Dependencies() + // Refresh must trigger a rebuild of the resource. + Refresh(*Storage) error + // Return the PEM output of the contained resource. + Pem() ([]byte, error) + Checksum() []byte + // DependsOn must return the resource names it is depending on. + DependsOn() []ResourceName + } + + ResourceName struct { + ID string + Type ResourceType + } + + ResourceType uint + + Error string +) + +func (e Error) Error() string { return string(e) } +func (r ResourceName) String() string { return r.Type.String() + "/" + r.ID } + +func main() { + os.Exit(_main()) +} + +func _main() int { + cfg, err := pkiadm.LoadConfig() + if err != nil { + log.Fatalf("could not load config: %s", err) + } + + addr, err := net.ResolveUnixAddr("unix", cfg.Path) + if err != nil { + log.Fatalf("could not parse unix path: %s", err) + } + + storage, err := NewStorage(cfg.Storage) + if err != nil { + log.Fatalf("error when loading: %s\n", err) + } + + server, err := NewServer(storage) + if err != nil { + log.Fatalf("error when loading server: %s\n", err) + } + + rpcServer := rpc.NewServer() + if err := rpcServer.RegisterName(pkiadm.ProtoIdent, server); err != nil { + log.Fatalf("could not bind rpc interface: %s\n", err) + } + + listener, err := net.ListenUnix("unix", addr) + if err != nil { + log.Fatalf("could not open listen socket: %s", err) + } + defer os.Remove(cfg.Path) + + // start signal listener + sigs := make(chan os.Signal, 1) + signal.Notify(sigs, os.Interrupt, os.Kill) + go func() { + s := <-sigs + log.Printf("initializing shutdown because of signal: %s", s) + listener.Close() + os.Remove(cfg.Path) + os.Exit(1) + }() + + rpcServer.Accept(listener) + + return 0 +} diff --git a/cmd/pkiadmd/private_key.go b/cmd/pkiadmd/private_key.go new file mode 100644 index 0000000..fffe8ff --- /dev/null +++ b/cmd/pkiadmd/private_key.go @@ -0,0 +1,150 @@ +package main + +import ( + "crypto/elliptic" + "encoding/pem" + "fmt" + + "github.com/gibheer/pki" +) + +const ( + PKTRSA PrivateKeyType = iota + PKTECDSA + PKTED25519 +) + +const ( + EWrongKeyLength = Error("key length for ecdsa must be one of 224, 256, 384 or 521") + ELengthOutOfBounds = Error("key length must be between 1024 and 32768") + EWrongKeyLengthED25519 = Error("ed25519 keys only support 256 length") +) + +type ( + PrivateKey struct { + ID string + PKType PrivateKeyType + Length uint + Key []byte + } + PrivateKeyType uint +) + +func NewPrivateKey(id string, pkType PrivateKeyType, length uint) (*PrivateKey, error) { + if id == "" { + return nil, ENoIDGiven + } + if err := verifyPK(pkType, length); err != nil { + return nil, err + } + pk := PrivateKey{ + ID: id, + PKType: pkType, + Length: length, + } + return &pk, nil +} + +func (p *PrivateKey) Name() ResourceName { + return ResourceName{p.ID, RTPrivateKey} +} + +func (p *PrivateKey) Checksum() []byte { + return Hash(p.Key) +} + +func (p *PrivateKey) Pem() ([]byte, error) { + return p.Key, nil +} + +func (p *PrivateKey) DependsOn() []ResourceName { + return []ResourceName{} +} + +func (p *PrivateKey) Refresh(_ *Storage) error { + var ( + key pki.PrivateKey + err error + ) + switch p.PKType { + case PKTRSA: + key, err = pki.NewPrivateKeyRsa(int(p.Length)) + case PKTED25519: + key, err = pki.NewPrivateKeyEd25519() + case PKTECDSA: + var curve elliptic.Curve + switch p.Length { + case 224: + curve = elliptic.P224() + case 256: + curve = elliptic.P256() + case 384: + curve = elliptic.P384() + case 521: + curve = elliptic.P521() + } + key, err = pki.NewPrivateKeyEcdsa(curve) + } + if err != nil { + return err + } + // set pem into the dump + block, err := key.ToPem() + if err != nil { + return err + } + block.Headers = map[string]string{"ID": p.ID} + p.Key = pem.EncodeToMemory(&block) + return nil +} + +func (p *PrivateKey) GetKey() (pki.PrivateKey, error) { + var ( + err error + key pki.PrivateKey + ) + block, _ := pem.Decode(p.Key) + switch block.Type { + case pki.PemLabelRsa: + key, err = pki.LoadPrivateKeyRsa(block.Bytes) + case pki.PemLabelEd25519: + key, err = pki.LoadPrivateKeyEd25519(block.Bytes) + case pki.PemLabelEcdsa: + key, err = pki.LoadPrivateKeyEcdsa(block.Bytes) + default: + return nil, fmt.Errorf("unknown private key type: %s - database corrupted", block.Type) + } + if err != nil { + return nil, err + } + return key, nil +} + +func verifyPK(pkType PrivateKeyType, length uint) error { + switch pkType { + case PKTRSA: + if length < 1024 || length > 32768 { + return ELengthOutOfBounds + } + case PKTECDSA: + switch length { + case 224, 256, 384, 521: + default: + return EWrongKeyLength + } + case PKTED25519: + if length != 256 { + return EWrongKeyLengthED25519 + } + default: + return EUnknownType + } + return nil +} + +//func (p *PrivateKey) MarshalJSON() ([]byte, error) { +// return json.Marshal(*p) +//} +//func (p *PrivateKey) UnmarshalJSON(raw []byte) error { +// return json.Unmarshal(raw, p) +//} diff --git a/cmd/pkiadmd/public_key.go b/cmd/pkiadmd/public_key.go new file mode 100644 index 0000000..9774a96 --- /dev/null +++ b/cmd/pkiadmd/public_key.go @@ -0,0 +1,77 @@ +package main + +import ( + "encoding/pem" +) + +const ( + PUTRSA PublicKeyType = iota + PUTECDSA + PUTED25519 +) + +type ( + PublicKey struct { + ID string + + PrivateKey ResourceName + Type PublicKeyType // mark the type of the public key + Key []byte + } + + PublicKeyType uint +) + +func NewPublicKey(id string, pk ResourceName) (*PublicKey, error) { + pub := PublicKey{ + ID: id, + PrivateKey: pk, + } + return &pub, nil +} + +func (p *PublicKey) Name() ResourceName { + return ResourceName{p.ID, RTPublicKey} +} + +func (p *PublicKey) Refresh(lookup *Storage) error { + r, err := lookup.Get(p.PrivateKey) + if err != nil { + return err + } + pk, ok := r.(*PrivateKey) + if !ok { + return EUnknownType + } + privateKey, err := pk.GetKey() + if err != nil { + return err + } + pubKey := privateKey.Public() + block, err := pubKey.ToPem() + if err != nil { + return err + } + block.Headers = map[string]string{"ID": p.ID, "TYPE": p.Type.String()} + p.Key = pem.EncodeToMemory(&block) + return nil +} + +func (p *PublicKey) DependsOn() []ResourceName { + return []ResourceName{p.PrivateKey} +} + +func (p *PublicKey) Pem() ([]byte, error) { + return p.Key, nil +} + +func (p *PublicKey) Checksum() []byte { + return Hash(p.Key) +} + +//func (p *PublicKey) MarshalJSON() ([]byte, error) { +// return json.Marshal(*p) +//} +//func (p *PublicKey) UnmarshalJSON(raw []byte) error { +// return json.Unmarshal(raw, p) +//} diff --git a/cmd/pkiadmd/publickeytype_string.go b/cmd/pkiadmd/publickeytype_string.go new file mode 100644 index 0000000..891eebd --- /dev/null +++ b/cmd/pkiadmd/publickeytype_string.go @@ -0,0 +1,16 @@ +// Code generated by "stringer -type PublicKeyType"; DO NOT EDIT + +package main + +import "fmt" + +const _PublicKeyType_name = "PUTRSAPUTECDSAPUTED25519" + +var _PublicKeyType_index = [...]uint8{0, 6, 14, 24} + +func (i PublicKeyType) String() string { + if i >= PublicKeyType(len(_PublicKeyType_index)-1) { + return fmt.Sprintf("PublicKeyType(%d)", i) + } + return _PublicKeyType_name[_PublicKeyType_index[i]:_PublicKeyType_index[i+1]] +} diff --git a/cmd/pkiadmd/resourcetype_string.go b/cmd/pkiadmd/resourcetype_string.go new file mode 100644 index 0000000..320a789 --- /dev/null +++ b/cmd/pkiadmd/resourcetype_string.go @@ -0,0 +1,16 @@ +// Code generated by "stringer -type ResourceType"; DO NOT EDIT + +package main + +import "fmt" + +const _ResourceType_name = "RTPrivateKeyRTPublicKeyRTCSRRTCertificateRTLocationRTSerialRTSubject" + +var _ResourceType_index = [...]uint8{0, 12, 23, 28, 41, 51, 59, 68} + +func (i ResourceType) String() string { + if i >= ResourceType(len(_ResourceType_index)-1) { + return fmt.Sprintf("ResourceType(%d)", i) + } + return _ResourceType_name[_ResourceType_index[i]:_ResourceType_index[i+1]] +} diff --git a/cmd/pkiadmd/serial.go b/cmd/pkiadmd/serial.go new file mode 100644 index 0000000..f2507bc --- /dev/null +++ b/cmd/pkiadmd/serial.go @@ -0,0 +1,56 @@ +package main + +import ( + "crypto/rand" + "math/big" +) + +const ( + ELengthTooSmall = Error("Length must not be smaller than 1") +) + +type ( + Serial struct { + ID string + Min int64 + Max int64 + UsedIDs map[int64]bool + } +) + +// NewSerial generates a new serial generator. +func NewSerial(id string, min, max int64) (*Serial, error) { + if max-min < 1 { + return nil, ELengthTooSmall + } + // TODO check maximum length for certificate serial + return &Serial{ID: id, Min: min, Max: max, UsedIDs: map[int64]bool{}}, nil +} + +// Return the unique ResourceName +func (s *Serial) Name() ResourceName { return ResourceName{s.ID, 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 + // pull their new ID themselves. + return nil +} + +// Return the PEM output of the contained resource. +func (s *Serial) Pem() ([]byte, error) { return []byte{}, nil } +func (s *Serial) Checksum() []byte { return []byte{} } + +// DependsOn must return the resource names it is depending on. +func (s *Serial) DependsOn() []ResourceName { return []ResourceName{} } + +// Generate generates a new serial number and stores it to avoid double +// assigning. +func (s *Serial) Generate() (*big.Int, error) { + val, err := rand.Int(rand.Reader, big.NewInt(s.Max-s.Min)) + if err != nil { + return big.NewInt(-1), err + } + return big.NewInt(val.Int64() + s.Min), nil +} diff --git a/cmd/pkiadmd/server.go b/cmd/pkiadmd/server.go new file mode 100644 index 0000000..a155e3f --- /dev/null +++ b/cmd/pkiadmd/server.go @@ -0,0 +1,139 @@ +package main + +import ( + "log" + "sync" + + "github.com/gibheer/pkiadm" +) + +type ( + Server struct { + storage *Storage + mu *sync.Mutex + } +) + +func NewServer(storage *Storage) (*Server, error) { + return &Server{storage, &sync.Mutex{}}, nil +} + +func (s *Server) lock() { + s.mu.Lock() +} +func (s *Server) unlock() { + s.mu.Unlock() +} +func (s *Server) store(res *pkiadm.Result) error { + if err := s.storage.store(); err != nil { + log.Printf("error when storing changes: %+v", err) + res.SetError(err, "could not save database") + } + return nil +} + +func (s *Server) CreateSubject(inSubj pkiadm.Subject, res *pkiadm.Result) error { + s.lock() + defer s.unlock() + + subj, err := NewSubject(inSubj.ID, inSubj.Name) + if err != nil { + res.SetError(err, "Could not create new subject '%s'", inSubj.ID) + return nil + } + if err := s.storage.AddSubject(subj); err != nil { + res.SetError(err, "Could not add subject '%s'", inSubj.ID) + return nil + } + return s.store(res) +} + +func (s *Server) SetSubject(changeset pkiadm.SubjectChange, res *pkiadm.Result) error { + s.lock() + defer s.unlock() + + subj, err := s.storage.GetSubject(ResourceName{ID: changeset.Subject.ID, Type: RTSubject}) + if err != nil { + res.SetError(err, "Could not find subject '%s'", changeset.Subject.ID) + return nil + } + changes := changeset.Subject.Name + for _, field := range changeset.FieldList { + switch field { + case "serial": + subj.Data.SerialNumber = changes.SerialNumber + case "common-name": + subj.Data.CommonName = changes.CommonName + case "country": + subj.Data.Country = changes.Country + case "org": + subj.Data.Organization = changes.Organization + case "org-unit": + subj.Data.OrganizationalUnit = changes.OrganizationalUnit + case "locality": + subj.Data.Locality = changes.Locality + case "province": + subj.Data.Province = changes.Province + case "street": + subj.Data.StreetAddress = changes.StreetAddress + case "code": + subj.Data.PostalCode = changes.PostalCode + } + } + if err := s.storage.Update(ResourceName{ID: subj.ID, Type: RTSubject}); err != nil { + res.SetError(err, "Could update resource '%s'", changeset.Subject.ID) + return nil + } + return s.store(res) +} + +func (s *Server) ListSubjects(filter pkiadm.Filter, res *pkiadm.ResultSubjects) error { + s.lock() + defer s.unlock() + + for _, subj := range s.storage.Subjects { + res.Subjects = append(res.Subjects, pkiadm.Subject{ + ID: subj.ID, + Name: subj.GetName(), + }) + } + return nil +} + +func (s *Server) DeleteSubject(inSubj pkiadm.ResourceName, res *pkiadm.Result) error { + s.lock() + defer s.unlock() + + subj, err := s.storage.Get(ResourceName{ID: inSubj.ID, Type: RTSubject}) + if err == ENotFound { + return nil + } else if err != nil { + res.SetError(err, "Could not find resource '%s'", inSubj) + return nil + } + if err := s.storage.Remove(subj); err != nil { + res.SetError(err, "Could not remove subject '%s'", inSubj) + return nil + } + return s.store(res) +} + +func (s *Server) ShowSubject(inSubj pkiadm.ResourceName, res *pkiadm.ResultSubjects) error { + s.lock() + defer s.unlock() + + subj, err := s.storage.GetSubject(ResourceName{ID: inSubj.ID, Type: RTSubject}) + if err == ENotFound { + return nil + } else if err != nil { + res.Result.SetError(err, "could not find resource '%s'", inSubj) + return nil + } + res.Subjects = []pkiadm.Subject{ + pkiadm.Subject{ + ID: subj.ID, + Name: subj.GetName(), + }, + } + return nil +} diff --git a/cmd/pkiadmd/storage.go b/cmd/pkiadmd/storage.go new file mode 100644 index 0000000..80f4bd5 --- /dev/null +++ b/cmd/pkiadmd/storage.go @@ -0,0 +1,340 @@ +package main + +import ( + "encoding/json" + "fmt" + "io/ioutil" + "log" + "os" + + "github.com/pkg/errors" +) + +type ( + // Storage is used to add and lookup resources and manages the dependency + // chain on an update. + Storage struct { + // path to the place where the storage should be stored. + path string + PrivateKeys map[string]*PrivateKey + PublicKeys map[string]*PublicKey + Locations map[string]*Location + Certificates map[string]*Certificate + CSRs map[string]*CSR + Serials map[string]*Serial + Subjects map[string]*Subject + // dependencies maps from a resource name to all resources which depend + // on it. + dependencies map[string]map[string]Resource + } +) + +// NewStorage builds a new storage instance and loads available data from the +// provided file path. +func NewStorage(path string) (*Storage, error) { + s := &Storage{ + path: path, + PrivateKeys: map[string]*PrivateKey{}, + PublicKeys: map[string]*PublicKey{}, + Locations: map[string]*Location{}, + Certificates: map[string]*Certificate{}, + CSRs: map[string]*CSR{}, + Serials: map[string]*Serial{}, + Subjects: map[string]*Subject{}, + dependencies: map[string]map[string]Resource{}, + } + if err := s.load(); err != nil { + return nil, err + } + return s, nil +} + +// load will restore the state from the file to the storage and overwrite +// already existing resources. +func (s *Storage) load() error { + raw, err := ioutil.ReadFile(s.path) + if os.IsNotExist(err) { + return nil + } + if err != nil { + return err + } + if err := json.Unmarshal(raw, s); err != nil { + return err + } + if err := s.refreshDependencies(); err != nil { + return err + } + return nil +} + +// refreshDependencies updates the inter resource dependencies. +func (s *Storage) refreshDependencies() error { + // TODO do something about broken dependencies + for _, se := range s.Serials { + _ = s.addDependency(se) // ignore errors here, as dependencies might be broken + } + for _, subj := range s.Subjects { + _ = s.addDependency(subj) + } + for _, pk := range s.PrivateKeys { + _ = s.addDependency(pk) + } + for _, pub := range s.PublicKeys { + _ = s.addDependency(pub) + } + for _, csr := range s.CSRs { + _ = s.addDependency(csr) + } + for _, cert := range s.Certificates { + _ = s.addDependency(cert) + } + for _, l := range s.Locations { + _ = s.addDependency(l) + } + return nil +} + +// addDependency adds a resource to the dependency graph. +func (s *Storage) addDependency(r Resource) error { + for _, rn := range r.DependsOn() { + _, err := s.Get(rn) + if err != nil { + return Error(fmt.Sprintf("problem with dependency '%s': %s", rn, err)) + } + deps, found := s.dependencies[rn.String()] + if !found { + s.dependencies[rn.String()] = map[string]Resource{r.Name().String(): r} + } else { + deps[r.Name().String()] = r + } + } + return nil +} + +// store writes the content of the storage to the disk in json format. +func (s *Storage) store() error { + raw, err := json.MarshalIndent(s, "", " ") + if err != nil { + log.Printf("could not marshal data: %s", err) + return err + } + if err := ioutil.WriteFile(s.path, raw, 0600); err != nil { + log.Printf("could not write to file '%s': %s", s.path, err) + return err + } + return nil +} + +// AddSerial adds a serial to the storage and refreshes the dependencies. +func (s *Storage) AddSerial(se *Serial) error { + if err := se.Refresh(s); err != nil { + return err + } + s.Serials[se.Name().ID] = se + return s.addDependency(se) +} + +// AddSubject adds a new subject to the storage and refreshes the dependencies. +func (s *Storage) AddSubject(se *Subject) error { + if _, found := s.Subjects[se.Name().ID]; found { + return EAlreadyExist + } + if err := se.Refresh(s); err != nil { + return err + } + s.Subjects[se.Name().ID] = se + return s.addDependency(se) +} + +// AddPrivateKey adds a private key to the storage and refreshes the dependencies. +func (s *Storage) AddPrivateKey(pk *PrivateKey) error { + if err := pk.Refresh(s); err != nil { + return err + } + s.PrivateKeys[pk.Name().ID] = pk + return s.addDependency(pk) +} + +// AddPublicKey adds a public key to the storage and refreshes the dependencies. +func (s *Storage) AddPublicKey(pub *PublicKey) error { + if err := pub.Refresh(s); err != nil { + return err + } + s.PublicKeys[pub.Name().ID] = pub + return s.addDependency(pub) +} + +// AddCertificate adds a certificate to the storage and refreshes the dependencies. +func (s *Storage) AddCertificate(cert *Certificate) error { + if err := cert.Refresh(s); err != nil { + return err + } + s.Certificates[cert.Name().ID] = cert + return s.addDependency(cert) +} + +// AddCSR adds a CSR to the storage and refreshes the dependencies. +func (s *Storage) AddCSR(csr *CSR) error { + if err := csr.Refresh(s); err != nil { + return err + } + s.CSRs[csr.Name().ID] = csr + return s.addDependency(csr) +} + +// AddLocation adds a location to the storage and refreshes the dependencies. +func (s *Storage) AddLocation(l *Location) error { + if err := l.Refresh(s); err != nil { + return err + } + s.Locations[l.Name().ID] = l + return s.addDependency(l) +} + +// Get figures out the resource to the ResourceName if available. +func (s *Storage) Get(r ResourceName) (Resource, error) { + if r.ID == "" { + return nil, ENoIDGiven + } + switch r.Type { + case RTSerial: + return s.GetSerial(r) + case RTSubject: + return s.GetSubject(r) + case RTPrivateKey: + return s.GetPrivateKey(r) + case RTPublicKey: + return s.GetPublicKey(r) + case RTCSR: + return s.GetCSR(r) + case RTCertificate: + return s.GetCertificate(r) + case RTLocation: + return s.GetLocation(r) + default: + return nil, EUnknownType + } +} + +// GetSerial returns the Serial matching the ResourceName. +func (s *Storage) GetSerial(r ResourceName) (*Serial, error) { + if se, found := s.Serials[r.ID]; found { + return se, nil + } + return nil, errors.Wrapf(ENotFound, "no serial with id '%s' found", r) +} + +// GetSubject returns the Subject matching the ResourceName. +func (s *Storage) GetSubject(r ResourceName) (*Subject, error) { + if se, found := s.Subjects[r.ID]; found { + return se, nil + } + return nil, errors.Wrapf(ENotFound, "no subject with id '%s' found", r) +} + +// GetPrivateKey returns the PrivateKey to the ResourceName. +func (s *Storage) GetPrivateKey(r ResourceName) (*PrivateKey, error) { + if pk, found := s.PrivateKeys[r.ID]; found { + return pk, nil + } + return nil, errors.Wrapf(ENotFound, "no private key with id '%s' found", r) +} + +// GetPublicKey returns the PublicKey to the ResourceName. +func (s *Storage) GetPublicKey(r ResourceName) (*PublicKey, error) { + if res, found := s.PublicKeys[r.ID]; found { + return res, nil + } + return nil, errors.Wrapf(ENotFound, "no public key with id '%s' found", r) +} + +// GetCSR returns the CSR to the CSR. +func (s *Storage) GetCSR(r ResourceName) (*CSR, error) { + if res, found := s.CSRs[r.ID]; found { + return res, nil + } + return nil, errors.Wrapf(ENotFound, "no CSR with id '%s' found", r) +} + +// GetCertificate returns the Certificate matching the ResourceName. +func (s *Storage) GetCertificate(r ResourceName) (*Certificate, error) { + if res, found := s.Certificates[r.ID]; found { + return res, nil + } + return nil, errors.Wrapf(ENotFound, "no certificate with id '%s' found", r) +} + +// GetLocation returns the Location matching the ResourceName. +func (s *Storage) GetLocation(r ResourceName) (*Location, error) { + if res, found := s.Locations[r.ID]; found { + return res, nil + } + return nil, errors.Wrapf(ENotFound, "no location 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 + switch r.Name().Type { + case RTSerial: + delete(s.Serials, r.Name().ID) + case RTSubject: + delete(s.Subjects, r.Name().ID) + case RTPrivateKey: + delete(s.PrivateKeys, r.Name().ID) + case RTPublicKey: + delete(s.PublicKeys, r.Name().ID) + case RTCSR: + delete(s.CSRs, r.Name().ID) + case RTCertificate: + delete(s.Certificates, r.Name().ID) + case RTLocation: + delete(s.Locations, r.Name().ID) + default: + return EUnknownType + } + for _, rn := range r.DependsOn() { + if deps, found := s.dependencies[rn.String()]; found { + delete(deps, r.Name().String()) + } + } + return nil +} + +// Update sends a refresh through all resources depending on the one given. +func (s *Storage) Update(rn ResourceName) error { + r, err := s.Get(rn) + if err != nil { + return err + } + updateOrder := []Resource{r} + checkList := map[string]bool{rn.String(): true} + depsToCheck := []Resource{} + for _, nextDep := range s.dependencies[rn.String()] { + depsToCheck = append(depsToCheck, nextDep) + } + + var dep Resource + for { + if len(depsToCheck) == 0 { + break + } + dep, depsToCheck = depsToCheck[0], depsToCheck[1:] + if _, found := checkList[dep.Name().String()]; found { + continue + } + updateOrder = append(updateOrder, dep) + checkList[dep.Name().String()] = true + for _, nextDep := range s.dependencies[dep.Name().String()] { + depsToCheck = append(depsToCheck, nextDep) + } + } + + for _, dep := range updateOrder { + if err := dep.Refresh(s); err != nil { + return err + } + } + return nil +} diff --git a/cmd/pkiadmd/subject.go b/cmd/pkiadmd/subject.go new file mode 100644 index 0000000..1c8953f --- /dev/null +++ b/cmd/pkiadmd/subject.go @@ -0,0 +1,41 @@ +package main + +import ( + "crypto/x509/pkix" +) + +type ( + Subject struct { + ID string + Data pkix.Name + } +) + +// Create a new subject resource. The commonName of the name is not used at the +// moment. +func NewSubject(id string, name pkix.Name) (*Subject, error) { + return &Subject{ + ID: id, + Data: name, + }, nil +} + +// Return the unique ResourceName +func (sub *Subject) Name() ResourceName { return ResourceName{sub.ID, 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 } + +// Return the PEM output of the contained resource. +func (sub *Subject) Pem() ([]byte, error) { return []byte{}, nil } +func (sub *Subject) Checksum() []byte { return []byte{} } + +// DependsOn must return the resource names it is depending on. +func (sub *Subject) DependsOn() []ResourceName { return []ResourceName{} } + +// GetName returns the stored name definition. +func (sub *Subject) GetName() pkix.Name { + return sub.Data +} diff --git a/config.go b/config.go new file mode 100644 index 0000000..d116bed --- /dev/null +++ b/config.go @@ -0,0 +1,46 @@ +package pkiadm + +import ( + "encoding/json" + "fmt" + "io/ioutil" + "os" +) + +var configLookupPath = []string{ + "config.json", + "pkiadm.json", + "/etc/pkiadm.json", +} + +type ( + Config struct { + Path string // path to the unix socket + Storage string // path to the storage location + } +) + +func LoadConfig() (*Config, error) { + for _, path := range configLookupPath { + if _, err := os.Stat(path); os.IsNotExist(err) { + continue + } + return tryToLoadConfig(path) + } + return nil, fmt.Errorf("no config file found") +} + +// tryToLoadConfig loads the config and tries to parse the file. When this +// doesn't work out, the error is returned. +func tryToLoadConfig(path string) (*Config, error) { + var cfg *Config + raw, err := ioutil.ReadFile(path) + if err != nil { + return nil, err + } + + if err := json.Unmarshal(raw, &cfg); err != nil { + return nil, err + } + return cfg, nil +} diff --git a/pkiadm.json b/pkiadm.json new file mode 100644 index 0000000..503e7ba --- /dev/null +++ b/pkiadm.json @@ -0,0 +1,4 @@ +{ + "Path": "pkiadm.sock", + "Storage": "pkiadm.db" +} diff --git a/resourcetype_string.go b/resourcetype_string.go new file mode 100644 index 0000000..bc05ab1 --- /dev/null +++ b/resourcetype_string.go @@ -0,0 +1,16 @@ +// Code generated by "stringer -type ResourceType"; DO NOT EDIT + +package pkiadm + +import "fmt" + +const _ResourceType_name = "RTPrivateKeyRTPublicKeyRTCSRRTCertificateRTLocationRTSerialRTSubject" + +var _ResourceType_index = [...]uint8{0, 12, 23, 28, 41, 51, 59, 68} + +func (i ResourceType) String() string { + if i >= ResourceType(len(_ResourceType_index)-1) { + return fmt.Sprintf("ResourceType(%d)", i) + } + return _ResourceType_name[_ResourceType_index[i]:_ResourceType_index[i+1]] +} diff --git a/subject.go b/subject.go new file mode 100644 index 0000000..ef7143e --- /dev/null +++ b/subject.go @@ -0,0 +1,62 @@ +package pkiadm + +import ( + "crypto/x509/pkix" +) + +type ( + Subject struct { + ID string + Name pkix.Name + } + // SubjectChange is a struct containing the fields that were changed. + SubjectChange struct { + Subject Subject + FieldList []string // The list of fields that were changed. + } + + ResultSubjects struct { + Result Result + Subjects []Subject + } +) + +func (c *Client) CreateSubject(subj Subject) error { + return c.exec("CreateSubject", subj) +} + +func (c *Client) DeleteSubject(id string) error { + subj := ResourceName{ID: id, Type: RTSubject} + return c.exec("DeleteSubject", subj) +} + +func (c *Client) SetSubject(subj Subject, fieldList []string) error { + changeset := SubjectChange{subj, fieldList} + return c.exec("SetSubject", changeset) +} + +func (c *Client) ShowSubject(id string) (Subject, error) { + subj := ResourceName{ID: id, Type: RTSubject} + result := &ResultSubjects{} + if err := c.query("ShowSubject", subj, result); err != nil { + return Subject{}, nil + } + if result.Result.HasError { + return Subject{}, result.Result.Error + } + for _, subject := range result.Subjects { + return subject, nil + } + return Subject{}, nil +} + +func (c *Client) ListSubject() ([]Subject, error) { + result := &ResultSubjects{} + if err := c.query("ListSubjects", Filter{}, result); err != nil { + return []Subject{}, err + } + if result.Result.HasError { + return []Subject{}, result.Result.Error + } + return result.Subjects, nil +} diff --git a/transport.go b/transport.go new file mode 100644 index 0000000..ec7689e --- /dev/null +++ b/transport.go @@ -0,0 +1,50 @@ +package pkiadm + +import ( + "fmt" +) + +// Result is a struct to send error messages from the server to the client. +type Result struct { + // TODO make field private to avoid accidental write + // HasError is true when an error was hit + HasError bool + // Error contains a list of errors, which can be used to add more details. + Error Error + // A message with more detailed information can be provided. + Message string +} + +type Error string + +func (e Error) Error() string { return string(e) } +func (r *Result) SetError(err error, msg string, args ...interface{}) { + r.HasError = true + r.Error = Error(err.Error()) + if len(args) > 0 { + r.Message = fmt.Sprintf(msg, args) + } else { + r.Message = msg + } +} + +// TODO documentation and cleanup +const ( + RTPrivateKey ResourceType = iota + RTPublicKey + RTCSR + RTCertificate + RTLocation + RTSerial + RTSubject +) + +type ResourceName struct { + ID string + Type ResourceType +} +type ResourceType uint + +func (r ResourceName) String() string { return r.Type.String() + "/" + r.ID } + +type Filter struct{}