certdeck

package module
v0.1.1 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Dec 11, 2024 License: MIT Imports: 19 Imported by: 1

README

Cert Deck

go get github.com/a-novel-kit/certdeck

GitHub Actions Workflow Status codecov

GitHub repo file or directory count GitHub code size in bytes

Coverage graph

A x509 certificates management library.

Signer

store := newStore()

rootSigner := certdeck.NewSigner(&certdeck.SignerConfig{
	SerialStore: store,
})

rootKey, err := rsa.GenerateKey(rand.Reader, 8192)
rootKeyHash := certdeck.HashRSA(&rootKey.PublicKey)

rootCert, err := rootSigner.Sign(context.Background(), rootKey, rootKeyHash, &certdeck.Template{
	Exp: time.Hour,
	Name: pkix.Name{
		Country:       []string{"FR"},
		Organization:  []string{"A Novel Kit"},
		Locality:      []string{"Paris"},
		Province:      []string{""},
		StreetAddress: []string{"1 rue de la Paix"},
		PostalCode:    []string{"75000"},
	},
	IPAddresses: certdeck.IPLocalHost,
	DNSNames:    []string{"localhost"},
})

intermediateSigner := certdeck.NewSigner(&certdeck.SignerConfig{
	SerialStore: store,
	IssuerChain: []*x509.Certificate{rootCert},
	IssuerKey:   rootKey,
})

intermediateKey, err := ecdsa.GenerateKey(elliptic.P521(), rand.Reader)
intermediateKeyHash := certdeck.HashECDSA(&intermediateKey.PublicKey)

intermediateCert, err := intermediateSigner.Sign(
	context.Background(), intermediateKey.Public(), intermediateKeyHash,
	&certdeck.Template{
		Exp: time.Hour,
		Name: pkix.Name{
			Country:       []string{"FR"},
			Organization:  []string{"A Novel Kit"},
			Locality:      []string{"Paris"},
			Province:      []string{""},
			StreetAddress: []string{"1 rue de la Paix"},
			PostalCode:    []string{"75000"},
		},
		IPAddresses: certdeck.IPLocalHost,
		DNSNames:    []string{"localhost"},
	},
)

leafSigner := certdeck.NewSigner(&certdeck.SignerConfig{
	SerialStore: store,
	IssuerChain: []*x509.Certificate{intermediateCert, rootCert},
	IssuerKey:   intermediateKey,
})

leafKey, _, err := ed25519.GenerateKey(rand.Reader)
leafKeyHash := certdeck.HashED25519(&leafKey)

leafCert, err := leafSigner.Sign(
	context.Background(), leafKey, leafKeyHash,
	&certdeck.Template{
		Exp: time.Hour,
		Name: pkix.Name{
			Country:       []string{"FR"},
			Organization:  []string{"A Novel Kit"},
			Locality:      []string{"Paris"},
			Province:      []string{""},
			StreetAddress: []string{"1 rue de la Paix"},
			PostalCode:    []string{"75000"},
		},
		IPAddresses: certdeck.IPLocalHost,
		DNSNames:    []string{"localhost"},
		LeafOnly:    true,
	},
)

The Signer interface requires you to provide a store for serial numbers. More information in the Store section.

Generating certs

The signer interface uses smart presets, to help you sign valid certificates for web with minimal configuration.

signer := certdeck.NewSigner(&certdeck.SignerConfig{
	SerialStore: store,
	IssuerChain: caChain,
	IssuerKey:   caKey,
})

The only 3 parameters you need to initialize a signer are

  • Store: a persistent database of assigned serial numbers, to prevent duplicates
  • IssuerChain: the chain of certificates used to sign issued certificates
  • IssuerKey: the private key used to sign issued certificates, which must match that of the first certificate in the issuer chain

Once you have this set, you can call the sign method to issue new certificates. This method wraps the standard library with some default configuration, so you can focus on what is required to generate a certificate valid for the web.

cert, err := signer.Sign(context.Background(), key, keyHash, &certdeck.Template{
	// How long the certificate will be valid for. Default is 1 year.
	Exp: time.Hour,
	// Information about the certificate owner.
	Name: pkix.Name{
		Country:       []string{"FR"},
		Organization:  []string{"A Novel Kit"},
		Locality:      []string{"Paris"},
		Province:      []string{""},
		StreetAddress: []string{"1 rue de la Paix"},
		PostalCode:    []string{"75000"},
	},
	// The IP addresses the certificate is valid for.
	IPAddresses: certdeck.IPLocalHost,
	// The DNS names the certificate is valid for.
	DNSNames:    []string{"localhost"},
})

The Signer interface is thread-safe, so one instance should be shared across your application.

Certificate keys

To generate a certificate, you must also create a private/public key pair for it. The private key is used to generate signatures, or issue descendant certificates. The public key can be shared, and the certificate is used to validate it.

X509 only supports the following key types:

  • RSA
  • ECDSA
  • ED25519

Go crypto library already provides generators for those keys. However, another field you must provide is a key ID. This ID can be randomly generated, or derived from the public key. This package provides methods for the second option:

Key type Method
RSA certdeck.HashRSA
ECDSA certdeck.HashECDSA
ED25519 certdeck.HashED25519
Leaf only

By default, the generated certificates can be used to issue their own children. While this is useful to build chains, you should disable this if your certificate is only intended for key validation.

cert, err := signer.Sign(context.Background(), key, keyHash, &certdeck.Template{
	// ... other fields
	LeafOnly: true,
})
Self Signed

You can become your own root, by simply omitting the IssuerChain and IssuerKey fields, when initializing the signer.

rootSigner := certdeck.NewSigner(&certdeck.SignerConfig{
	SerialStore: store,
})

If using self-signed certificates, make sure they are added to the root system pool of the target machine, when verifying the issued certificates.

Update the issuer chain

You can update the certificates used by a signer, when new ones are available for example:

signer.Rotate(caChain, caKey)

Store

For security reason, you should provide a way to ensure uniqueness of serial numbers among the certificates from a given authority. Serial number should be unique even across revoked / expired certificates.

The best way to do this is to keep track of the serial numbers in a persistent database.

The Store is a simple interface, with a single method:

type SerialStore interface {
	Insert(ctx context.Context, serial *big.Int) error
}

The Insert method saves a new serial number in its database. If the number is already present, it MUST return the certdeck.ErrAlreadyExists error.

Below is an example with an in-memory, volatile store. You should build your own store with a persistent database instead.

type MemoryStore struct {
	serials map[string]bool
}

func (m *MemoryStore) Insert(ctx context.Context, serial *big.Int) error {
	if m.serials == nil {
		m.serials = make(map[string]bool)
	}

	serialStr := serial.String()
	if m.serials[serialStr] {
		return certdeck.ErrAlreadyExists
	}

	m.serials[serialStr] = true
	return nil
}

Collection

This package provides a Collection interface, to manage collections of certificates.

collection := certdeck.NewCollection(time.Hour)

provider := providers.NewHTTPS(&providers.HTTPSProviderConfig{
	ID: "my-website",
	CertsReq: func() (*http.Request, error) {
		return http.NewRequest(http.MethodGet, "https://my-website.com/certs", nil)
	},
	KeyReq: func() (*http.Request, error) {
		return http.NewRequest(http.MethodGet, "https://my-website.com/key", nil)
	},
})

data, err := collection.Get(provider)

certs := data.Certificates()
key := data.Key()

The argument of a collection is a duration, that indicates ho long values will be cached before being fetched again from the provider.

The returned value is a row, that returns the certificate chain and the private signature key, in both parsed and raw PEM formats.

Method Description
Certificates() []*x509.Certificate Returns the certificate chain, with the first certificate being the leaf.
Key() crypto.Signer Returns the private key used to sign the leaf certificate.
CertificatesPEM() [][]byte Returns the certificate chain, with the first certificate being the leaf, in PEM format.
KeyPEM() []byte Returns the private key used to sign the leaf certificate, in PEM format.
Default providers

This package provides the following default providers:

File provider

The easier use case is to load certificates from files. First, you need to link your files to your Go code using a filesystem.

Given the following file tree.

- pkg
    - certs
        - files.go
        - 20241012.crt
        - 20241012.key
        - 20241010.crt
        - 20241010.key

The content of files.go should be:

//go:embed *.crt *.key
var CertsFS embed.FS

You can then create a file provider:

provider, err := providers.NewFile(&providers.FileProviderConfig{
	ID: "local",
	FS: CertsFS,
})

When loading files, they are sorted by creation date. The most recent one is used as the leaf certificate, and other are appended in a chain. The key returned is the one of the leaf.

You can customize the behavior of the file provider:

provider, err := providers.NewFile(&providers.FileProviderConfig{
	ID: "local",
	FS: CertsFS,

	// Customize the file pattern used to match certificates.
	CertsPattern: regexp.MustCompile(`\.crt$`)
	// Customize the file pattern used to match keys.
	KeysPattern:  regexp.MustCompile(`\.key$`)
	
	// Custom ordering of cert files. The first one is the leaf, then certificates must be sorted in order.
	SortCerts: providers.SortCreatedAt,
	// Custom ordering of key files. The first one is the key of the leaf, and is the only one actually parsed.
	SortKeys: providers.SortCreatedAt,
})
HTTPS provider

This provider fetches certificates from a remote server. It requires a function to create a request for the certificates and the key.

provider, err := providers.NewHTTPS(&providers.HTTPSProviderConfig{
	ID: "my-website",
	CertsReq: func() (*http.Request, error) {
		return http.NewRequest(http.MethodGet, "https://my-website.com/certs", nil)
	},
	KeyReq: func() (*http.Request, error) {
		return http.NewRequest(http.MethodGet, "https://my-website.com/key", nil)
	},
})

Documentation

Index

Constants

View Source
const SerialGenerationMaxRetries = 5

Variables

View Source
var (
	ErrCertMismatch    = errors.New("certificate chains are not semantically equal")
	ErrCertKeyMismatch = errors.New("public key mismatch")
)
View Source
var ErrAlreadyExists = errors.New("serial number already exists")
View Source
var ErrUnsupportedKeyFormat = errors.New("unsupported CertKey format")
View Source
var IPLocalHost = []net.IP{net.IPv4(127, 0, 0, 1), net.IPv6loopback}

Functions

func Base64ToCerts

func Base64ToCerts(data []string) ([]*x509.Certificate, error)

func CertsToBase64

func CertsToBase64(certificates ...*x509.Certificate) []string

func CertsToDER

func CertsToDER(certificates ...*x509.Certificate) [][]byte

func CertsToDERInline

func CertsToDERInline(certificates ...*x509.Certificate) []byte

func CertsToPEM

func CertsToPEM(certificates ...*x509.Certificate) [][]byte

func CertsToPEMInline

func CertsToPEMInline(certificates ...*x509.Certificate) []byte

func DERInlineToCerts

func DERInlineToCerts(data []byte) ([]*x509.Certificate, error)

func DERToCerts

func DERToCerts(data [][]byte) ([]*x509.Certificate, error)

func DERToKey

func DERToKey(data []byte) (crypto.Signer, error)

func GenerateSerial

func GenerateSerial() (*big.Int, error)

GenerateSerial generates a random serial number for a certificate.

func GenerateSerialWithStore

func GenerateSerialWithStore(ctx context.Context, store SerialStore, maxRetries int) (*big.Int, error)

func HashECDSA

func HashECDSA(src *ecdsa.PublicKey) []byte

func HashED25519

func HashED25519(src *ed25519.PublicKey) []byte

func HashRSA

func HashRSA(src *rsa.PublicKey) []byte

func KeyToDER

func KeyToDER(key any) ([]byte, error)

func KeyToPEM

func KeyToPEM(key any) ([]byte, error)

func Match

func Match(chain1, chain2 []*x509.Certificate) error

Match checks if two certificate chains are semantically equal.

func MatchKey

func MatchKey(keyPub interface{}, certs []*x509.Certificate) error

MatchKey checks if the public key matches the certificate.

func PEMInlineToCerts

func PEMInlineToCerts(data []byte) ([]*x509.Certificate, error)

func PEMOrDERToCerts

func PEMOrDERToCerts(data [][]byte) ([]*x509.Certificate, error)

func PEMOrDerToKey

func PEMOrDerToKey(data []byte) (crypto.Signer, error)

func PEMToCerts

func PEMToCerts(data [][]byte) ([]*x509.Certificate, error)

func PEMToKey

func PEMToKey(data []byte) (crypto.Signer, error)

Types

type CertsProvider

type CertsProvider interface {
	// ID is a unique identifier for the data cached by this updater.
	ID() string
	// Retrieve returns the updated data.
	Retrieve() (CollectionRow, error)
}

type Collection

type Collection interface {
	// Get returns the collection of certificates and private CertKey for the given updater.
	Get(updater CertsProvider) (CollectionRow, error)
}

func NewCollection

func NewCollection(cacheDuration time.Duration) Collection

type CollectionRow

type CollectionRow interface {
	// Certificates returns the underlying certificates chain.
	Certificates() []*x509.Certificate
	// Key returns the underlying private CertKey, that corresponds to the public CertKey of the first certificate in
	// the chain.
	Key() crypto.Signer

	// CertificatesPEM returns the PEM encoded collection of certificates.
	CertificatesPEM() [][]byte
	// KeyPEM returns the PEM encoded private CertKey.
	KeyPEM() []byte
}

type CollectionRowBase

type CollectionRowBase struct {
	Certs   []*x509.Certificate
	CertKey crypto.Signer

	CertsPEM   [][]byte
	CertKeyPEM []byte
}

func (*CollectionRowBase) Certificates

func (row *CollectionRowBase) Certificates() []*x509.Certificate

func (*CollectionRowBase) CertificatesPEM

func (row *CollectionRowBase) CertificatesPEM() [][]byte

func (*CollectionRowBase) Fill

func (row *CollectionRowBase) Fill() error

func (*CollectionRowBase) Key

func (row *CollectionRowBase) Key() crypto.Signer

func (*CollectionRowBase) KeyPEM

func (row *CollectionRowBase) KeyPEM() []byte

type SerialStore

type SerialStore interface {
	// Insert a new serial number in the store. If the serial number is already taken, this must return
	// ErrAlreadyExists.
	Insert(ctx context.Context, serial *big.Int) error
}

SerialStore keeps track of used serial numbers.

type Signer

type Signer interface {
	// Sign a CertKey with a template, returning the certificate.
	//
	// Pub CertKey is the CertKey of the certificate that will be issued. It must be one of the following supported types:
	//  - *rsa.PublicKey
	//  - *ecdsa.PublicKey
	//  - ed25519.PublicKey
	//
	// KeyID must be a random, unique identifier for the certificate. It can be derived from the public CertKey.
	// Depending on the type of your public CertKey, you can use any of the provided hashers in this package:
	//  - HashRSA
	//  - HashECDSA
	//  - HashED25519
	Sign(ctx context.Context, key any, keyID []byte, template *Template) (*x509.Certificate, error)
	// Rotate updates the issuer chain and the CertKey used to sign the certificates.
	Rotate(issuers []*x509.Certificate, issuerKey crypto.Signer)
}

func NewSigner

func NewSigner(config *SignerConfig) Signer

type SignerConfig

type SignerConfig struct {
	// SerialStore keeps track of used serial numbers.
	SerialStore SerialStore
	// IssuerChain is a list of certificates that will be used to sign the certificate.
	IssuerChain []*x509.Certificate
	// IssuerKey is the CertKey that will be used to sign the certificate. It must be the CertKey of the first
	// certificate in the IssuerChain list.
	//
	// The public CertKey of the IssuerKey must be of a supported type:
	//  - *rsa.PublicKey
	//  - *ecdsa.PublicKey
	//  - ed25519.PublicKey
	IssuerKey crypto.Signer
}

type Template

type Template struct {
	// Exp sets the expiration time of the certificate.
	//
	// It is set to 365 days by default.
	Exp time.Duration

	// Name is the subject of the certificate.
	Name pkix.Name

	// IPAddresses is a list of IP addresses that the certificate is valid for.
	IPAddresses []net.IP

	// DNSNames is a list of DNS names that the certificate is valid for.
	DNSNames []string

	// LeafOnly revokes the ability of the issued certificate to sign other certificates.
	LeafOnly bool
}

Directories

Path Synopsis
internal

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL
JackTT - Gopher 🇻🇳