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:
Mariano Cano 2021-07-19 19:28:06 -07:00
parent dd9850ce4c
commit 8fb5340dc9
7 changed files with 399 additions and 85 deletions

View file

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

View file

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

View file

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

View file

@ -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()),
) )

View file

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

View file

@ -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
View file

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