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.
This commit is contained in:
Gibheer 2017-05-28 11:33:04 +02:00
commit 039f72c3d5
21 changed files with 1736 additions and 0 deletions

54
client.go Normal file
View File

@ -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
}

92
cmd/pkiadm/main.go Normal file
View File

@ -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 <subcommand> [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()
}

154
cmd/pkiadm/subject.go Normal file
View File

@ -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")
}

129
cmd/pkiadmd/certificate.go Normal file
View File

@ -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
}

11
cmd/pkiadmd/crypto.go Normal file
View File

@ -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[:]))
}

103
cmd/pkiadmd/csr.go Normal file
View File

@ -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
}

71
cmd/pkiadmd/location.go Normal file
View File

@ -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)
//}

109
cmd/pkiadmd/main.go Normal file
View File

@ -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
}

150
cmd/pkiadmd/private_key.go Normal file
View File

@ -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)
//}

77
cmd/pkiadmd/public_key.go Normal file
View File

@ -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)
//}

View File

@ -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]]
}

View File

@ -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]]
}

56
cmd/pkiadmd/serial.go Normal file
View File

@ -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
}

139
cmd/pkiadmd/server.go Normal file
View File

@ -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
}

340
cmd/pkiadmd/storage.go Normal file
View File

@ -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
}

41
cmd/pkiadmd/subject.go Normal file
View File

@ -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
}

46
config.go Normal file
View File

@ -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
}

4
pkiadm.json Normal file
View File

@ -0,0 +1,4 @@
{
"Path": "pkiadm.sock",
"Storage": "pkiadm.db"
}

16
resourcetype_string.go Normal file
View File

@ -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]]
}

62
subject.go Normal file
View File

@ -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
}

50
transport.go Normal file
View File

@ -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{}