Use a token at start time to configure linkedca.
Instead of using `step-ca login` we will use a new token provided as a flag to configure and start linkedca. Certificates will be kept in memory and refreshed automatically.
This commit is contained in:
parent
dd9850ce4c
commit
8fb5340dc9
7 changed files with 399 additions and 85 deletions
|
@ -7,6 +7,7 @@ import (
|
||||||
"crypto/x509"
|
"crypto/x509"
|
||||||
"encoding/hex"
|
"encoding/hex"
|
||||||
"log"
|
"log"
|
||||||
|
"strings"
|
||||||
"sync"
|
"sync"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
@ -40,6 +41,7 @@ type Authority struct {
|
||||||
db db.AuthDB
|
db db.AuthDB
|
||||||
adminDB admin.DB
|
adminDB admin.DB
|
||||||
templates *templates.Templates
|
templates *templates.Templates
|
||||||
|
linkedCAToken string
|
||||||
|
|
||||||
// X509 CA
|
// X509 CA
|
||||||
x509CAService cas.CertificateAuthorityService
|
x509CAService cas.CertificateAuthorityService
|
||||||
|
@ -442,17 +444,24 @@ func (a *Authority) init() error {
|
||||||
// Initialize step-ca Admin Database if it's not already initialized using
|
// Initialize step-ca Admin Database if it's not already initialized using
|
||||||
// WithAdminDB.
|
// WithAdminDB.
|
||||||
if a.adminDB == nil {
|
if a.adminDB == nil {
|
||||||
if a.config.AuthorityConfig.AuthorityID == "" {
|
if a.linkedCAToken == "" {
|
||||||
// Check if AuthConfig already exists
|
// Check if AuthConfig already exists
|
||||||
a.adminDB, err = adminDBNosql.New(a.db.(nosql.DB), admin.DefaultAuthorityID)
|
a.adminDB, err = adminDBNosql.New(a.db.(nosql.DB), admin.DefaultAuthorityID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
a.adminDB, err = createLinkedCAClient(a.config.AuthorityConfig.AuthorityID, "localhost:6040")
|
// Use the linkedca client as the admindb.
|
||||||
|
client, err := newLinkedCAClient(a.linkedCAToken)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
// If authorityId is configured make sure it matches the one in the token
|
||||||
|
if id := a.config.AuthorityConfig.AuthorityID; id != "" && !strings.EqualFold(id, client.authorityID) {
|
||||||
|
return errors.New("error initializing linkedca: token authority and configured authority do not match")
|
||||||
|
}
|
||||||
|
client.Run()
|
||||||
|
a.adminDB = client
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -534,6 +543,9 @@ func (a *Authority) CloseForReload() {
|
||||||
if err := a.keyManager.Close(); err != nil {
|
if err := a.keyManager.Close(); err != nil {
|
||||||
log.Printf("error closing the key manager: %v", err)
|
log.Printf("error closing the key manager: %v", err)
|
||||||
}
|
}
|
||||||
|
if client, ok := a.adminDB.(*linkedCaClient); ok {
|
||||||
|
client.Stop()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// requiresDecrypter returns whether the Authority
|
// requiresDecrypter returns whether the Authority
|
||||||
|
|
|
@ -2,59 +2,126 @@ package authority
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"crypto"
|
||||||
|
"crypto/sha256"
|
||||||
"crypto/tls"
|
"crypto/tls"
|
||||||
"crypto/x509"
|
"crypto/x509"
|
||||||
"io/ioutil"
|
"encoding/hex"
|
||||||
"path/filepath"
|
"encoding/pem"
|
||||||
|
"fmt"
|
||||||
|
"net/url"
|
||||||
|
"regexp"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
"github.com/pkg/errors"
|
"github.com/pkg/errors"
|
||||||
"github.com/smallstep/certificates/errs"
|
"github.com/smallstep/certificates/errs"
|
||||||
"go.step.sm/cli-utils/config"
|
"go.step.sm/crypto/jose"
|
||||||
|
"go.step.sm/crypto/keyutil"
|
||||||
|
"go.step.sm/crypto/tlsutil"
|
||||||
|
"go.step.sm/crypto/x509util"
|
||||||
"go.step.sm/linkedca"
|
"go.step.sm/linkedca"
|
||||||
"google.golang.org/grpc"
|
"google.golang.org/grpc"
|
||||||
"google.golang.org/grpc/credentials"
|
"google.golang.org/grpc/credentials"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
const uuidPattern = "^[a-fA-F0-9]{8}-[a-fA-F0-9]{4}-[a-fA-F0-9]{4}-[a-fA-F0-9]{4}-[a-fA-F0-9]{12}$"
|
||||||
|
|
||||||
type linkedCaClient struct {
|
type linkedCaClient struct {
|
||||||
|
renewer *tlsutil.Renewer
|
||||||
client linkedca.MajordomoClient
|
client linkedca.MajordomoClient
|
||||||
authorityID string
|
authorityID string
|
||||||
}
|
}
|
||||||
|
|
||||||
func createLinkedCAClient(authorityID, endpoint string) (*linkedCaClient, error) {
|
type linkedCAClaims struct {
|
||||||
base := filepath.Join(config.StepPath(), "linkedca")
|
jose.Claims
|
||||||
rootFile := filepath.Join(base, "root_ca.crt")
|
SANs []string `json:"sans"`
|
||||||
certFile := filepath.Join(base, "linkedca.crt")
|
SHA string `json:"sha"`
|
||||||
keyFile := filepath.Join(base, "linkedca.key")
|
|
||||||
|
|
||||||
b, err := ioutil.ReadFile(rootFile)
|
|
||||||
if err != nil {
|
|
||||||
return nil, errors.Wrap(err, "error reading linkedca root")
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func newLinkedCAClient(token string) (*linkedCaClient, error) {
|
||||||
|
tok, err := jose.ParseSigned(token)
|
||||||
|
if err != nil {
|
||||||
|
return nil, errors.Wrap(err, "error parsing token")
|
||||||
|
}
|
||||||
|
|
||||||
|
var claims linkedCAClaims
|
||||||
|
if err := tok.UnsafeClaimsWithoutVerification(&claims); err != nil {
|
||||||
|
return nil, errors.Wrap(err, "error parsing token")
|
||||||
|
}
|
||||||
|
// Validate claims
|
||||||
|
if len(claims.Audience) != 1 {
|
||||||
|
return nil, errors.New("error parsing token: invalid aud claim")
|
||||||
|
}
|
||||||
|
if claims.SHA == "" {
|
||||||
|
return nil, errors.New("error parsing token: invalid sha claim")
|
||||||
|
}
|
||||||
|
// Get linkedCA endpoint from audience.
|
||||||
|
u, err := url.Parse(claims.Audience[0])
|
||||||
|
if err != nil {
|
||||||
|
return nil, errors.New("error parsing token: invalid aud claim")
|
||||||
|
}
|
||||||
|
// Get authority from SANs
|
||||||
|
authority, err := getAuthority(claims.SANs)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create csr to login with
|
||||||
|
signer, err := keyutil.GenerateDefaultSigner()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
csr, err := x509util.CreateCertificateRequest(claims.Subject, claims.SANs, signer)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get and verify root certificate
|
||||||
|
root, err := getRootCertificate(u.Host, claims.SHA)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
pool := x509.NewCertPool()
|
pool := x509.NewCertPool()
|
||||||
if !pool.AppendCertsFromPEM(b) {
|
pool.AddCert(root)
|
||||||
return nil, errors.Errorf("error reading %s: no certificates were found", rootFile)
|
|
||||||
|
// Login with majordomo and get certificates
|
||||||
|
cert, tlsConfig, err := login(authority, token, csr, signer, u.Host, pool)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
conn, err := grpc.Dial(endpoint, grpc.WithTransportCredentials(credentials.NewTLS(&tls.Config{
|
// Start TLS renewer and set the GetClientCertificate callback to it.
|
||||||
RootCAs: pool,
|
renewer, err := tlsutil.NewRenewer(cert, tlsConfig, func() (*tls.Certificate, *tls.Config, error) {
|
||||||
GetClientCertificate: func(*tls.CertificateRequestInfo) (*tls.Certificate, error) {
|
return login(authority, token, csr, signer, u.Host, pool)
|
||||||
cert, err := tls.LoadX509KeyPair(certFile, keyFile)
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, errors.Wrap(err, "error reading linkedca certificate")
|
return nil, err
|
||||||
}
|
}
|
||||||
return &cert, nil
|
tlsConfig.GetClientCertificate = renewer.GetClientCertificate
|
||||||
},
|
|
||||||
})))
|
// Start mTLS client
|
||||||
|
conn, err := grpc.Dial(u.Host, grpc.WithTransportCredentials(credentials.NewTLS(tlsConfig)))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, errors.Wrapf(err, "error connecting %s", endpoint)
|
return nil, errors.Wrapf(err, "error connecting %s", u.Host)
|
||||||
}
|
}
|
||||||
|
|
||||||
return &linkedCaClient{
|
return &linkedCaClient{
|
||||||
|
renewer: renewer,
|
||||||
client: linkedca.NewMajordomoClient(conn),
|
client: linkedca.NewMajordomoClient(conn),
|
||||||
authorityID: authorityID,
|
authorityID: authority,
|
||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (c *linkedCaClient) Run() {
|
||||||
|
c.renewer.Run()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *linkedCaClient) Stop() {
|
||||||
|
c.renewer.Stop()
|
||||||
|
}
|
||||||
|
|
||||||
func (c *linkedCaClient) CreateProvisioner(ctx context.Context, prov *linkedca.Provisioner) error {
|
func (c *linkedCaClient) CreateProvisioner(ctx context.Context, prov *linkedca.Provisioner) error {
|
||||||
resp, err := c.client.CreateProvisioner(ctx, &linkedca.CreateProvisionerRequest{
|
resp, err := c.client.CreateProvisioner(ctx, &linkedca.CreateProvisionerRequest{
|
||||||
Type: prov.Type,
|
Type: prov.Type,
|
||||||
|
@ -169,3 +236,154 @@ func (c *linkedCaClient) DeleteAdmin(ctx context.Context, id string) error {
|
||||||
})
|
})
|
||||||
return errors.Wrap(err, "error deleting admin")
|
return errors.Wrap(err, "error deleting admin")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func getAuthority(sans []string) (string, error) {
|
||||||
|
for _, s := range sans {
|
||||||
|
if strings.HasPrefix(s, "urn:smallstep:authority:") {
|
||||||
|
if regexp.MustCompile(uuidPattern).MatchString(s[24:]) {
|
||||||
|
return s[24:], nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return "", fmt.Errorf("error parsing token: invalid sans claim")
|
||||||
|
}
|
||||||
|
|
||||||
|
// getRootCertificate creates an insecure majordomo client and returns the
|
||||||
|
// verified root certificate.
|
||||||
|
func getRootCertificate(endpoint, fingerprint string) (*x509.Certificate, error) {
|
||||||
|
ctx, cancel := context.WithCancel(context.Background())
|
||||||
|
defer cancel()
|
||||||
|
|
||||||
|
conn, err := grpc.DialContext(ctx, endpoint, grpc.WithTransportCredentials(credentials.NewTLS(&tls.Config{
|
||||||
|
InsecureSkipVerify: true,
|
||||||
|
})))
|
||||||
|
if err != nil {
|
||||||
|
return nil, errors.Wrapf(err, "error connecting %s", endpoint)
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx, cancel = context.WithTimeout(context.Background(), 15*time.Second)
|
||||||
|
defer cancel()
|
||||||
|
|
||||||
|
client := linkedca.NewMajordomoClient(conn)
|
||||||
|
resp, err := client.GetRootCertificate(ctx, &linkedca.GetRootCertificateRequest{
|
||||||
|
Fingerprint: fingerprint,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("error getting root certificate: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
var block *pem.Block
|
||||||
|
b := []byte(resp.PemCertificate)
|
||||||
|
for len(b) > 0 {
|
||||||
|
block, b = pem.Decode(b)
|
||||||
|
if block == nil {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
if block.Type != "CERTIFICATE" || len(block.Headers) != 0 {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
cert, err := x509.ParseCertificate(block.Bytes)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("error parsing certificate: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// verify the sha256
|
||||||
|
sum := sha256.Sum256(cert.Raw)
|
||||||
|
if !strings.EqualFold(fingerprint, hex.EncodeToString(sum[:])) {
|
||||||
|
return nil, fmt.Errorf("error verifying certificate: SHA256 fingerprint does not match")
|
||||||
|
}
|
||||||
|
|
||||||
|
return cert, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil, fmt.Errorf("error getting root certificate: certificate not found")
|
||||||
|
}
|
||||||
|
|
||||||
|
// login creates a new majordomo client with just the root ca pool and returns
|
||||||
|
// the signed certificate and tls configuration.
|
||||||
|
func login(authority, token string, csr *x509.CertificateRequest, signer crypto.PrivateKey, endpoint string, rootCAs *x509.CertPool) (*tls.Certificate, *tls.Config, error) {
|
||||||
|
// Connect to majordomo
|
||||||
|
ctx, cancel := context.WithCancel(context.Background())
|
||||||
|
defer cancel()
|
||||||
|
|
||||||
|
conn, err := grpc.DialContext(ctx, endpoint, grpc.WithTransportCredentials(credentials.NewTLS(&tls.Config{
|
||||||
|
RootCAs: rootCAs,
|
||||||
|
})))
|
||||||
|
if err != nil {
|
||||||
|
return nil, nil, errors.Wrapf(err, "error connecting %s", endpoint)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Login to get the signed certificate
|
||||||
|
ctx, cancel = context.WithTimeout(context.Background(), 15*time.Second)
|
||||||
|
defer cancel()
|
||||||
|
|
||||||
|
client := linkedca.NewMajordomoClient(conn)
|
||||||
|
resp, err := client.Login(ctx, &linkedca.LoginRequest{
|
||||||
|
AuthorityId: authority,
|
||||||
|
Token: token,
|
||||||
|
PemCertificateRequest: string(pem.EncodeToMemory(&pem.Block{
|
||||||
|
Type: "CERTIFICATE REQUEST",
|
||||||
|
Bytes: csr.Raw,
|
||||||
|
})),
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return nil, nil, errors.Wrapf(err, "error logging in %s", endpoint)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Parse login response
|
||||||
|
var block *pem.Block
|
||||||
|
var bundle []*x509.Certificate
|
||||||
|
rest := []byte(resp.PemCertificateChain)
|
||||||
|
for {
|
||||||
|
block, rest = pem.Decode(rest)
|
||||||
|
if block == nil {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
if block.Type != "CERTIFICATE" {
|
||||||
|
return nil, nil, errors.New("error decoding login response: pemCertificateChain is not a certificate bundle")
|
||||||
|
}
|
||||||
|
crt, err := x509.ParseCertificate(block.Bytes)
|
||||||
|
if err != nil {
|
||||||
|
return nil, nil, errors.Wrap(err, "error parsing login response")
|
||||||
|
}
|
||||||
|
bundle = append(bundle, crt)
|
||||||
|
}
|
||||||
|
if len(bundle) == 0 {
|
||||||
|
return nil, nil, errors.New("error decoding login response: pemCertificateChain should not be empty")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Build tls.Certificate with PemCertificate and intermediates in the
|
||||||
|
// PemCertificateChain
|
||||||
|
cert := &tls.Certificate{
|
||||||
|
PrivateKey: signer,
|
||||||
|
}
|
||||||
|
rest = []byte(resp.PemCertificate)
|
||||||
|
for {
|
||||||
|
block, rest = pem.Decode(rest)
|
||||||
|
if block == nil {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
if block.Type == "CERTIFICATE" {
|
||||||
|
leaf, err := x509.ParseCertificate(block.Bytes)
|
||||||
|
if err != nil {
|
||||||
|
return nil, nil, errors.Wrap(err, "error parsing pemCertificate")
|
||||||
|
}
|
||||||
|
cert.Certificate = append(cert.Certificate, block.Bytes)
|
||||||
|
cert.Leaf = leaf
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add intermediates to the tls.Certificate
|
||||||
|
last := len(bundle) - 1
|
||||||
|
for i := 0; i < last; i++ {
|
||||||
|
cert.Certificate = append(cert.Certificate, bundle[i].Raw)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add root to the pool if it's not there yet
|
||||||
|
rootCAs.AddCert(bundle[last])
|
||||||
|
|
||||||
|
return cert, &tls.Config{
|
||||||
|
RootCAs: rootCAs,
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
|
@ -196,6 +196,15 @@ func WithAdminDB(db admin.DB) Option {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// WithLinkedCAToken is an option to set the authentication token used to enable
|
||||||
|
// linked ca.
|
||||||
|
func WithLinkedCAToken(token string) Option {
|
||||||
|
return func(a *Authority) error {
|
||||||
|
a.linkedCAToken = token
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func readCertificateBundle(pemCerts []byte) ([]*x509.Certificate, error) {
|
func readCertificateBundle(pemCerts []byte) ([]*x509.Certificate, error) {
|
||||||
var block *pem.Block
|
var block *pem.Block
|
||||||
var certs []*x509.Certificate
|
var certs []*x509.Certificate
|
||||||
|
|
13
ca/ca.go
13
ca/ca.go
|
@ -30,6 +30,7 @@ import (
|
||||||
|
|
||||||
type options struct {
|
type options struct {
|
||||||
configFile string
|
configFile string
|
||||||
|
linkedCAToken string
|
||||||
password []byte
|
password []byte
|
||||||
issuerPassword []byte
|
issuerPassword []byte
|
||||||
database db.AuthDB
|
database db.AuthDB
|
||||||
|
@ -75,6 +76,13 @@ func WithDatabase(db db.AuthDB) Option {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// WithLinkedCAToken sets the token used to authenticate with the linkedca.
|
||||||
|
func WithLinkedCAToken(token string) Option {
|
||||||
|
return func(o *options) {
|
||||||
|
o.linkedCAToken = token
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// CA is the type used to build the complete certificate authority. It builds
|
// CA is the type used to build the complete certificate authority. It builds
|
||||||
// the HTTP server, set ups the middlewares and the HTTP handlers.
|
// the HTTP server, set ups the middlewares and the HTTP handlers.
|
||||||
type CA struct {
|
type CA struct {
|
||||||
|
@ -111,6 +119,10 @@ func (ca *CA) Init(config *config.Config) (*CA, error) {
|
||||||
}
|
}
|
||||||
|
|
||||||
var opts []authority.Option
|
var opts []authority.Option
|
||||||
|
if ca.opts.linkedCAToken != "" {
|
||||||
|
opts = append(opts, authority.WithLinkedCAToken(ca.opts.linkedCAToken))
|
||||||
|
}
|
||||||
|
|
||||||
if ca.opts.database != nil {
|
if ca.opts.database != nil {
|
||||||
opts = append(opts, authority.WithDatabase(ca.opts.database))
|
opts = append(opts, authority.WithDatabase(ca.opts.database))
|
||||||
}
|
}
|
||||||
|
@ -326,6 +338,7 @@ func (ca *CA) Reload() error {
|
||||||
newCA, err := New(config,
|
newCA, err := New(config,
|
||||||
WithPassword(ca.opts.password),
|
WithPassword(ca.opts.password),
|
||||||
WithIssuerPassword(ca.opts.issuerPassword),
|
WithIssuerPassword(ca.opts.issuerPassword),
|
||||||
|
WithLinkedCAToken(ca.opts.linkedCAToken),
|
||||||
WithConfigFile(ca.opts.configFile),
|
WithConfigFile(ca.opts.configFile),
|
||||||
WithDatabase(ca.auth.GetDatabase()),
|
WithDatabase(ca.auth.GetDatabase()),
|
||||||
)
|
)
|
||||||
|
|
|
@ -38,6 +38,10 @@ certificate issuer private key used in the RA mode.`,
|
||||||
Name: "resolver",
|
Name: "resolver",
|
||||||
Usage: "address of a DNS resolver to be used instead of the default.",
|
Usage: "address of a DNS resolver to be used instead of the default.",
|
||||||
},
|
},
|
||||||
|
cli.StringFlag{
|
||||||
|
Name: "token",
|
||||||
|
Usage: "token used to enable the linked ca.",
|
||||||
|
},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -46,6 +50,7 @@ func appAction(ctx *cli.Context) error {
|
||||||
passFile := ctx.String("password-file")
|
passFile := ctx.String("password-file")
|
||||||
issuerPassFile := ctx.String("issuer-password-file")
|
issuerPassFile := ctx.String("issuer-password-file")
|
||||||
resolver := ctx.String("resolver")
|
resolver := ctx.String("resolver")
|
||||||
|
token := ctx.String("token")
|
||||||
|
|
||||||
// If zero cmd line args show help, if >1 cmd line args show error.
|
// If zero cmd line args show help, if >1 cmd line args show error.
|
||||||
if ctx.NArg() == 0 {
|
if ctx.NArg() == 0 {
|
||||||
|
@ -88,7 +93,8 @@ func appAction(ctx *cli.Context) error {
|
||||||
srv, err := ca.New(config,
|
srv, err := ca.New(config,
|
||||||
ca.WithConfigFile(configFile),
|
ca.WithConfigFile(configFile),
|
||||||
ca.WithPassword(password),
|
ca.WithPassword(password),
|
||||||
ca.WithIssuerPassword(issuerPassword))
|
ca.WithIssuerPassword(issuerPassword),
|
||||||
|
ca.WithLinkedCAToken(token))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
fatal(err)
|
fatal(err)
|
||||||
}
|
}
|
||||||
|
|
|
@ -2,13 +2,18 @@ package commands
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"crypto/sha256"
|
||||||
"crypto/tls"
|
"crypto/tls"
|
||||||
"crypto/x509"
|
"crypto/x509"
|
||||||
|
"encoding/hex"
|
||||||
"encoding/pem"
|
"encoding/pem"
|
||||||
|
"fmt"
|
||||||
"io/ioutil"
|
"io/ioutil"
|
||||||
|
"net/url"
|
||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"regexp"
|
"regexp"
|
||||||
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/pkg/errors"
|
"github.com/pkg/errors"
|
||||||
|
@ -32,13 +37,14 @@ const uuidPattern = "^[a-fA-F0-9]{8}-[a-fA-F0-9]{4}-[a-fA-F0-9]{4}-[a-fA-F0-9]{4
|
||||||
type linkedCAClaims struct {
|
type linkedCAClaims struct {
|
||||||
jose.Claims
|
jose.Claims
|
||||||
SANs []string `json:"sans"`
|
SANs []string `json:"sans"`
|
||||||
|
SHA string `json:"sha"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func init() {
|
func init() {
|
||||||
command.Register(cli.Command{
|
command.Register(cli.Command{
|
||||||
Name: "login",
|
Name: "login",
|
||||||
Usage: "create the certificates to authorize your Linked CA instance",
|
Usage: "create the certificates to authorize your Linked CA instance",
|
||||||
UsageText: `**step-ca login** <authority> **--token*=<token>
|
UsageText: `**step-ca login** **--token*=<token>
|
||||||
[**--linkedca**=<endpoint>] [**--root**=<file>]`,
|
[**--linkedca**=<endpoint>] [**--root**=<file>]`,
|
||||||
Action: loginAction,
|
Action: loginAction,
|
||||||
Description: `**step-ca login** ...
|
Description: `**step-ca login** ...
|
||||||
|
@ -50,16 +56,7 @@ func init() {
|
||||||
Flags: []cli.Flag{
|
Flags: []cli.Flag{
|
||||||
cli.StringFlag{
|
cli.StringFlag{
|
||||||
Name: "token",
|
Name: "token",
|
||||||
Usage: "The one-time <token> used to authenticate with the Linked CA in order to create the initial credentials",
|
Usage: "The <token> used to authenticate with the Linked CA in order to create the initial credentials",
|
||||||
},
|
|
||||||
cli.StringFlag{
|
|
||||||
Name: "linkedca",
|
|
||||||
Usage: "The linkedca <endpoint> to connect to.",
|
|
||||||
Value: loginEndpoint,
|
|
||||||
},
|
|
||||||
cli.StringFlag{
|
|
||||||
Name: "root",
|
|
||||||
Usage: "The root certificate <file> used to authenticate with the linkedca endpoint.",
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
@ -70,18 +67,9 @@ func loginAction(ctx *cli.Context) error {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
args := ctx.Args()
|
|
||||||
authority := args[0]
|
|
||||||
token := ctx.String("token")
|
token := ctx.String("token")
|
||||||
endpoint := ctx.String("linkedca")
|
if token == "" {
|
||||||
rx := regexp.MustCompile(uuidPattern)
|
|
||||||
switch {
|
|
||||||
case !rx.MatchString(authority):
|
|
||||||
return errors.Errorf("positional argument %s is not a valid uuid", authority)
|
|
||||||
case token == "":
|
|
||||||
return errs.RequiredFlag(ctx, "token")
|
return errs.RequiredFlag(ctx, "token")
|
||||||
case endpoint == "":
|
|
||||||
return errs.RequiredFlag(ctx, "linkedca")
|
|
||||||
}
|
}
|
||||||
|
|
||||||
var claims linkedCAClaims
|
var claims linkedCAClaims
|
||||||
|
@ -90,9 +78,43 @@ func loginAction(ctx *cli.Context) error {
|
||||||
return errors.Wrap(err, "error parsing token")
|
return errors.Wrap(err, "error parsing token")
|
||||||
}
|
}
|
||||||
if err := tok.UnsafeClaimsWithoutVerification(&claims); err != nil {
|
if err := tok.UnsafeClaimsWithoutVerification(&claims); err != nil {
|
||||||
return errors.Wrap(err, "error parsing payload")
|
return errors.Wrap(err, "error parsing token")
|
||||||
|
}
|
||||||
|
if len(claims.Audience) != 0 {
|
||||||
|
return errors.Wrap(err, "error parsing token: invalid aud claim")
|
||||||
|
}
|
||||||
|
u, err := url.Parse(claims.Audience[0])
|
||||||
|
if err != nil {
|
||||||
|
return errors.Wrap(err, "error parsing token: invalid aud claim")
|
||||||
|
}
|
||||||
|
if claims.SHA == "" {
|
||||||
|
return errors.Wrap(err, "error parsing token: invalid sha claim")
|
||||||
|
}
|
||||||
|
authority, err := getAuthority(claims.SANs)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Get and verify root certificate
|
||||||
|
root, err := getRootCertificate(u.Host, claims.SHA)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
pool := x509.NewCertPool()
|
||||||
|
pool.AddCert(root)
|
||||||
|
|
||||||
|
gctx, cancel := context.WithCancel(context.Background())
|
||||||
|
defer cancel()
|
||||||
|
|
||||||
|
conn, err := grpc.DialContext(gctx, u.Host, grpc.WithTransportCredentials(credentials.NewTLS(&tls.Config{
|
||||||
|
RootCAs: pool,
|
||||||
|
})))
|
||||||
|
if err != nil {
|
||||||
|
return errors.Wrapf(err, "error connecting %s", u.Host)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create csr
|
||||||
signer, err := keyutil.GenerateDefaultSigner()
|
signer, err := keyutil.GenerateDefaultSigner()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
|
@ -107,33 +129,7 @@ func loginAction(ctx *cli.Context) error {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
var options []grpc.DialOption
|
// Perform login and get signed certificate
|
||||||
if root := ctx.String("root"); root != "" {
|
|
||||||
b, err := ioutil.ReadFile(root)
|
|
||||||
if err != nil {
|
|
||||||
return errors.Wrap(err, "error reading file")
|
|
||||||
}
|
|
||||||
|
|
||||||
pool := x509.NewCertPool()
|
|
||||||
if !pool.AppendCertsFromPEM(b) {
|
|
||||||
return errors.Errorf("error reading %s: no certificates were found", root)
|
|
||||||
}
|
|
||||||
|
|
||||||
options = append(options, grpc.WithTransportCredentials(credentials.NewTLS(&tls.Config{
|
|
||||||
RootCAs: pool,
|
|
||||||
})))
|
|
||||||
} else {
|
|
||||||
options = append(options, grpc.WithTransportCredentials(credentials.NewTLS(&tls.Config{})))
|
|
||||||
}
|
|
||||||
|
|
||||||
gctx, cancel := context.WithTimeout(context.Background(), 15*time.Second)
|
|
||||||
defer cancel()
|
|
||||||
|
|
||||||
conn, err := grpc.DialContext(gctx, endpoint, options...)
|
|
||||||
if err != nil {
|
|
||||||
return errors.Wrapf(err, "error connecting %s", endpoint)
|
|
||||||
}
|
|
||||||
|
|
||||||
client := linkedca.NewMajordomoClient(conn)
|
client := linkedca.NewMajordomoClient(conn)
|
||||||
gctx, cancel = context.WithTimeout(context.Background(), 15*time.Second)
|
gctx, cancel = context.WithTimeout(context.Background(), 15*time.Second)
|
||||||
defer cancel()
|
defer cancel()
|
||||||
|
@ -180,6 +176,67 @@ func loginAction(ctx *cli.Context) error {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func getAuthority(sans []string) (string, error) {
|
||||||
|
for _, s := range sans {
|
||||||
|
if strings.HasPrefix(s, "urn:smallstep:authority:") {
|
||||||
|
if regexp.MustCompile(uuidPattern).MatchString(s[24:]) {
|
||||||
|
return s[24:], nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return "", fmt.Errorf("error parsing token: invalid sans claim")
|
||||||
|
}
|
||||||
|
|
||||||
|
func getRootCertificate(endpoint, fingerprint string) (*x509.Certificate, error) {
|
||||||
|
ctx, cancel := context.WithCancel(context.Background())
|
||||||
|
defer cancel()
|
||||||
|
|
||||||
|
conn, err := grpc.DialContext(ctx, endpoint, grpc.WithTransportCredentials(credentials.NewTLS(&tls.Config{
|
||||||
|
InsecureSkipVerify: true,
|
||||||
|
})))
|
||||||
|
if err != nil {
|
||||||
|
return nil, errors.Wrapf(err, "error connecting %s", endpoint)
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx, cancel = context.WithTimeout(context.Background(), 15*time.Second)
|
||||||
|
defer cancel()
|
||||||
|
|
||||||
|
client := linkedca.NewMajordomoClient(conn)
|
||||||
|
resp, err := client.GetRootCertificate(ctx, &linkedca.GetRootCertificateRequest{
|
||||||
|
Fingerprint: fingerprint,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("error getting root certificate: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
var block *pem.Block
|
||||||
|
b := []byte(resp.PemCertificate)
|
||||||
|
for len(b) > 0 {
|
||||||
|
block, b = pem.Decode(b)
|
||||||
|
if block == nil {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
if block.Type != "CERTIFICATE" || len(block.Headers) != 0 {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
cert, err := x509.ParseCertificate(block.Bytes)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("error parsing certificate: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// verify the sha256
|
||||||
|
sum := sha256.Sum256(cert.Raw)
|
||||||
|
if !strings.EqualFold(fingerprint, hex.EncodeToString(sum[:])) {
|
||||||
|
return nil, fmt.Errorf("error verifying certificate: SHA256 fingerprint does not match")
|
||||||
|
}
|
||||||
|
|
||||||
|
return cert, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil, fmt.Errorf("error getting root certificate: certificate not found")
|
||||||
|
}
|
||||||
|
|
||||||
func parseLoginResponse(resp *linkedca.LoginResponse) ([]byte, []byte, error) {
|
func parseLoginResponse(resp *linkedca.LoginResponse) ([]byte, []byte, error) {
|
||||||
var block *pem.Block
|
var block *pem.Block
|
||||||
var bundle []*x509.Certificate
|
var bundle []*x509.Certificate
|
||||||
|
|
3
go.mod
3
go.mod
|
@ -40,9 +40,8 @@ require (
|
||||||
)
|
)
|
||||||
|
|
||||||
// replace github.com/smallstep/nosql => ../nosql
|
// replace github.com/smallstep/nosql => ../nosql
|
||||||
|
|
||||||
//replace go.step.sm/crypto => ../crypto
|
//replace go.step.sm/crypto => ../crypto
|
||||||
|
|
||||||
//replace go.step.sm/cli-utils => ../cli-utils
|
//replace go.step.sm/cli-utils => ../cli-utils
|
||||||
|
replace go.step.sm/linkedca => ../linkedca
|
||||||
|
|
||||||
replace go.mozilla.org/pkcs7 v0.0.0-20200128120323-432b2356ecb1 => github.com/omorsi/pkcs7 v0.0.0-20210217142924-a7b80a2a8568
|
replace go.mozilla.org/pkcs7 v0.0.0-20200128120323-432b2356ecb1 => github.com/omorsi/pkcs7 v0.0.0-20210217142924-a7b80a2a8568
|
||||||
|
|
Loading…
Reference in a new issue