Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add AuthMethod to Vault issuer #103

Merged
merged 2 commits into from
Dec 22, 2019
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion issuer.go
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ type CertConfig struct {
// KeyGenerator is used to create new private keys
// for CSR requests. If not defined, defaults to ECDSA P256.
// Only ECDSA and RSA keys are supported.
// This is guaranteed to be privided in Issue calls.
// This is guaranteed to be provided in Issue calls.
KeyGenerator KeyGenerator
}

Expand Down
18 changes: 18 additions & 0 deletions issuers/vault/types.go
Original file line number Diff line number Diff line change
@@ -1,10 +1,28 @@
package vault

import (
"context"
"strings"
"time"

"github.com/hashicorp/vault/api"
)

// AuthMethod defines the interface required to implement
// custom authentication against the Vault server.
type AuthMethod interface {
GetToken(context.Context, *api.Client) (string, error)
}

// ConstantToken implements AuthMethod with a constant token
type ConstantToken string

// GetToken returns the token
func (c ConstantToken) GetToken(context.Context, *api.Client) (string, error) {
return string(c), nil
}


// https://www.vaultproject.io/api/secret/pki/index.html#parameters-14
type csrOpts struct {
CSR string `json:"csr"`
Expand Down
33 changes: 27 additions & 6 deletions issuers/vault/vault.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,19 +18,26 @@ import (
// Issuer implements the Issuer interface with a
// Hashicorp Vault PKI Secrets Engine backend.
//
// URL, Token and Role are required.
// URL, Role and AuthMethod are required.
type Issuer struct {
// URL is the URL of the Vault instance.
URL *url.URL
// Role is the Vault Role that should be used
// when issuing certificates.
Role string

// Token is the Vault secret token that should be used
// when issuing certificates.
//
// Deprecated: use AuthMethod instead.
Token string
// AuthMethod configures the method used for authenticating
// against the Vault server.
AuthMethod AuthMethod

// Mount is the name under which the PKI secrets engine
// is mounted. Defaults to `pki`
Mount string
// Role is the Vault Role that should be used
// when issuing certificates.
Role string
// TLSConfig allows configuration of the TLS config
// used when connecting to the Vault server.
TLSConfig *tls.Config
Expand Down Expand Up @@ -64,10 +71,12 @@ type Issuer struct {

// FromClient returns an Issuer using the provided Vault API client.
// Any changes to the issuers properties (such as setting the TTL or adding Other SANS)
// must be done before using it. The client must have its token configured.
// must be done before using it. The Issuer will default to using
// the token already defined in the client for authentication.
func FromClient(v *api.Client, role string) *Issuer {
return &Issuer{
Role: role,
AuthMethod: ConstantToken(v.Token()),
cli: v,
}
}
Expand All @@ -86,7 +95,6 @@ func (v *Issuer) connect(ctx context.Context) error {
return err
}

v.cli.SetToken(v.Token)
return nil
}

Expand All @@ -100,6 +108,12 @@ func (v *Issuer) Issue(ctx context.Context, commonName string, conf *certify.Cer
}
}

// Convert Token to AuthMethod, if no AuthMethod set,
// for backwards compatibility.
if v.AuthMethod == nil {
v.AuthMethod = ConstantToken(v.Token)
}

csrPEM, keyPEM, err := csr.FromCertConfig(commonName, conf)
if err != nil {
return nil, err
Expand Down Expand Up @@ -147,6 +161,13 @@ func (v Issuer) signCSR(ctx context.Context, opts csrOpts) (*api.Secret, error)
pkiMountName = v.Mount
}

// Update token immediately before making the request
token, err := v.AuthMethod.GetToken(ctx, v.cli)
if err != nil {
return nil, err
}
v.cli.SetToken(token)

r := v.cli.NewRequest("PUT", "/v1/"+pkiMountName+"/sign/"+v.Role)
if err := r.SetJSONBody(opts); err != nil {
return nil, err
Expand Down
74 changes: 67 additions & 7 deletions issuers/vault/vault_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,8 @@ import (
"github.com/johanbrandhorst/certify/issuers/vault/proto"
)

//go:generate protoc --go_out=plugins=grpc:./ ./proto/test.proto

type otherName struct {
TypeID asn1.ObjectIdentifier
Value string `asn1:"explicit,utf8"`
Expand Down Expand Up @@ -57,16 +59,14 @@ func getOtherNames(cert *x509.Certificate) (otherNames []otherName, err error) {
return otherNames, nil
}

//go:generate protoc --go_out=plugins=grpc:./ ./proto/test.proto

var _ = Describe("Vault Issuer", func() {
var iss certify.Issuer
var conf *certify.CertConfig

BeforeEach(func() {
iss = &vault.Issuer{
URL: vaultTLSConf.URL,
Token: vaultTLSConf.Token,
AuthMethod: vault.ConstantToken(vaultTLSConf.Token),
Role: vaultTLSConf.Role,
TLSConfig: &tls.Config{
RootCAs: vaultTLSConf.CertPool,
Expand Down Expand Up @@ -106,11 +106,36 @@ var _ = Describe("Vault Issuer", func() {
}))
})

Context("with no explicit AuthMethod set", func() {
It("still works", func() {
cn := "somename.com"

tlsCert, err := iss.Issue(context.Background(), cn, conf)
Expect(err).NotTo(HaveOccurred())

Expect(tlsCert.Leaf).NotTo(BeNil(), "tlsCert.Leaf should be populated by Issue to track expiry")
Expect(tlsCert.Leaf.Subject.CommonName).To(Equal(cn))

// Check that chain is included
Expect(tlsCert.Certificate).To(HaveLen(2))
caCert, err := x509.ParseCertificate(tlsCert.Certificate[1])
Expect(err).NotTo(HaveOccurred())
Expect(caCert.Subject.SerialNumber).To(Equal(tlsCert.Leaf.Issuer.SerialNumber))

Expect(tlsCert.Leaf.NotBefore).To(BeTemporally("<", time.Now()))
Expect(tlsCert.Leaf.NotAfter).To(BeTemporally("~", time.Now().Add(iss.(*vault.Issuer).TimeToLive), 5*time.Second))
Expect(getOtherNames(tlsCert.Leaf)).To(ConsistOf(otherName{
TypeID: asn1.ObjectIdentifier([]int{1, 3, 6, 1, 4, 1, 311, 20, 2, 3}),
Value: "devops@nope.com",
}))
})
})

Context("with URI SANs", func() {
BeforeEach(func() {
iss = &vault.Issuer{
URL: vaultTLSConf.URL,
Token: vaultTLSConf.Token,
AuthMethod: vault.ConstantToken(vaultTLSConf.Token),
Role: vaultTLSConf.RoleURISANs,
TLSConfig: &tls.Config{
RootCAs: vaultTLSConf.CertPool,
Expand Down Expand Up @@ -148,7 +173,7 @@ var _ = Describe("Vault Issuer", func() {
BeforeEach(func() {
iss = &vault.Issuer{
URL: vaultTLSConf.URL,
Token: vaultTLSConf.Token,
AuthMethod: vault.ConstantToken(vaultTLSConf.Token),
Mount: altMount,
Role: vaultTLSConf.Role,
TLSConfig: &tls.Config{
Expand Down Expand Up @@ -252,7 +277,7 @@ var _ = Describe("Vault HTTP Issuer", func() {
BeforeEach(func() {
iss = &vault.Issuer{
URL: vaultConf.URL,
Token: vaultConf.Token,
AuthMethod: vault.ConstantToken(vaultTLSConf.Token),
Role: vaultConf.Role,
TimeToLive: time.Minute * 10,
// Format is "<type_id>;utf8:<value>", where type_id
Expand Down Expand Up @@ -388,6 +413,41 @@ var _ = Describe("Using a pre-created client", func() {
})
})

var _ = Describe("When an AuthMethod is not explicitly set", func() {
It("still works", func() {
iss := &vault.Issuer{
URL: vaultTLSConf.URL,
Token: vaultTLSConf.Token,
Role: vaultTLSConf.Role,
TLSConfig: &tls.Config{
RootCAs: vaultTLSConf.CertPool,
},
TimeToLive: time.Minute * 10,
}
conf := &certify.CertConfig{
KeyGenerator: keyGeneratorFunc(func() (crypto.PrivateKey, error) {
return ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
}),
}
cn := "somename.com"

tlsCert, err := iss.Issue(context.Background(), cn, conf)
Expect(err).NotTo(HaveOccurred())

Expect(tlsCert.Leaf).NotTo(BeNil(), "tlsCert.Leaf should be populated by Issue to track expiry")
Expect(tlsCert.Leaf.Subject.CommonName).To(Equal(cn))

// Check that chain is included
Expect(tlsCert.Certificate).To(HaveLen(2))
caCert, err := x509.ParseCertificate(tlsCert.Certificate[1])
Expect(err).NotTo(HaveOccurred())
Expect(caCert.Subject.SerialNumber).To(Equal(tlsCert.Leaf.Issuer.SerialNumber))

Expect(tlsCert.Leaf.NotBefore).To(BeTemporally("<", time.Now()))
Expect(tlsCert.Leaf.NotAfter).To(BeTemporally("~", time.Now().Add(iss.TimeToLive), 5*time.Second))
})
})

type keyGeneratorFunc func() (crypto.PrivateKey, error)

func (kgf keyGeneratorFunc) Generate() (crypto.PrivateKey, error) {
Expand Down Expand Up @@ -424,7 +484,7 @@ var _ = Describe("gRPC Test", func() {
CommonName: "Certify",
Issuer: &vault.Issuer{
URL: vaultTLSConf.URL,
Token: vaultTLSConf.Token,
AuthMethod: vault.ConstantToken(vaultTLSConf.Token),
Role: vaultTLSConf.Role,
TLSConfig: &tls.Config{
RootCAs: vaultTLSConf.CertPool,
Expand Down