Skip to content

Commit d6bf551

Browse files
authored
Merge pull request #1803 from smallstep/herman/fix-scep-vault-ra
Fix CA startup with Vault RA configuration
2 parents f4d506f + 1e5e267 commit d6bf551

File tree

9 files changed

+834
-35
lines changed

9 files changed

+834
-35
lines changed

authority/authority.go

Lines changed: 35 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -447,6 +447,7 @@ func (a *Authority) init() error {
447447
return err
448448
}
449449
a.rootX509Certs = append(a.rootX509Certs, resp.RootCertificate)
450+
a.intermediateX509Certs = append(a.intermediateX509Certs, resp.IntermediateCertificates...)
450451
}
451452
}
452453

@@ -695,32 +696,42 @@ func (a *Authority) init() error {
695696
options := &scep.Options{
696697
Roots: a.rootX509Certs,
697698
Intermediates: a.intermediateX509Certs,
698-
SignerCert: a.intermediateX509Certs[0],
699699
}
700-
if options.Signer, err = a.keyManager.CreateSigner(&kmsapi.CreateSignerRequest{
701-
SigningKey: a.config.IntermediateKey,
702-
Password: a.password,
703-
}); err != nil {
704-
return err
700+
701+
// intermediate certificates can be empty in RA mode
702+
if len(a.intermediateX509Certs) > 0 {
703+
options.SignerCert = a.intermediateX509Certs[0]
705704
}
706-
// TODO(hs): instead of creating the decrypter here, pass the
707-
// intermediate key + chain down to the SCEP authority,
708-
// and only instantiate it when required there. Is that possible?
709-
// Also with entering passwords?
710-
// TODO(hs): if moving the logic, try improving the logic for the
711-
// decrypter password too? Right now it needs to be entered multiple
712-
// times; I've observed it to be three times maximum, every time
713-
// the intermediate key is read.
714-
_, isRSA := options.Signer.Public().(*rsa.PublicKey)
715-
if km, ok := a.keyManager.(kmsapi.Decrypter); ok && isRSA {
716-
if decrypter, err := km.CreateDecrypter(&kmsapi.CreateDecrypterRequest{
717-
DecryptionKey: a.config.IntermediateKey,
718-
Password: a.password,
719-
}); err == nil {
720-
// only pass the decrypter down when it was successfully created,
721-
// meaning it's an RSA key, and `CreateDecrypter` did not fail.
722-
options.Decrypter = decrypter
723-
options.DecrypterCert = options.Intermediates[0]
705+
706+
// attempt to create the (default) SCEP signer if the intermediate
707+
// key is configured.
708+
if a.config.IntermediateKey != "" {
709+
if options.Signer, err = a.keyManager.CreateSigner(&kmsapi.CreateSignerRequest{
710+
SigningKey: a.config.IntermediateKey,
711+
Password: a.password,
712+
}); err != nil {
713+
return err
714+
}
715+
716+
// TODO(hs): instead of creating the decrypter here, pass the
717+
// intermediate key + chain down to the SCEP authority,
718+
// and only instantiate it when required there. Is that possible?
719+
// Also with entering passwords?
720+
// TODO(hs): if moving the logic, try improving the logic for the
721+
// decrypter password too? Right now it needs to be entered multiple
722+
// times; I've observed it to be three times maximum, every time
723+
// the intermediate key is read.
724+
_, isRSAKey := options.Signer.Public().(*rsa.PublicKey)
725+
if km, ok := a.keyManager.(kmsapi.Decrypter); ok && isRSAKey {
726+
if decrypter, err := km.CreateDecrypter(&kmsapi.CreateDecrypterRequest{
727+
DecryptionKey: a.config.IntermediateKey,
728+
Password: a.password,
729+
}); err == nil {
730+
// only pass the decrypter down when it was successfully created,
731+
// meaning it's an RSA key, and `CreateDecrypter` did not fail.
732+
options.Decrypter = decrypter
733+
options.DecrypterCert = options.Intermediates[0]
734+
}
724735
}
725736
}
726737

cas/apiv1/requests.go

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -116,7 +116,8 @@ type GetCertificateAuthorityRequest struct {
116116
// GetCertificateAuthorityResponse is the response that contains
117117
// the root certificate.
118118
type GetCertificateAuthorityResponse struct {
119-
RootCertificate *x509.Certificate
119+
RootCertificate *x509.Certificate
120+
IntermediateCertificates []*x509.Certificate
120121
}
121122

122123
// CreateKeyRequest is the request used to generate a new key using a KMS.

cas/vaultcas/vaultcas.go

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -165,7 +165,8 @@ func (v *VaultCAS) GetCertificateAuthority(*apiv1.GetCertificateAuthorityRequest
165165
}
166166

167167
return &apiv1.GetCertificateAuthorityResponse{
168-
RootCertificate: cert.root,
168+
RootCertificate: cert.root,
169+
IntermediateCertificates: cert.intermediates,
169170
}, nil
170171
}
171172

scep/options.go

Lines changed: 11 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -37,19 +37,21 @@ func (o *Options) Validate() error {
3737
switch {
3838
case len(o.Intermediates) == 0:
3939
return errors.New("no intermediate certificate available for SCEP authority")
40-
case o.Signer == nil:
41-
return errors.New("no signer available for SCEP authority")
4240
case o.SignerCert == nil:
4341
return errors.New("no signer certificate available for SCEP authority")
4442
}
4543

46-
// check if the signer (intermediate CA) certificate has the same public key as
47-
// the signer. According to the RFC it seems valid to have different keys for
48-
// the intermediate and the CA signing new certificates, so this might change
49-
// in the future.
50-
signerPublicKey := o.Signer.Public().(comparablePublicKey)
51-
if !signerPublicKey.Equal(o.SignerCert.PublicKey) {
52-
return errors.New("mismatch between signer certificate and public key")
44+
// the signer is optional, but if it's set, its public key must match the signer
45+
// certificate public key.
46+
if o.Signer != nil {
47+
// check if the signer (intermediate CA) certificate has the same public key as
48+
// the signer. According to the RFC it seems valid to have different keys for
49+
// the intermediate and the CA signing new certificates, so this might change
50+
// in the future.
51+
signerPublicKey := o.Signer.Public().(comparablePublicKey)
52+
if !signerPublicKey.Equal(o.SignerCert.PublicKey) {
53+
return errors.New("mismatch between signer certificate and public key")
54+
}
5355
}
5456

5557
// decrypter can be nil in case a signing only key is used; validation complete.
Lines changed: 273 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,273 @@
1+
package sceptest
2+
3+
import (
4+
"context"
5+
"crypto/rand"
6+
"crypto/rsa"
7+
"crypto/tls"
8+
"crypto/x509"
9+
"encoding/base64"
10+
"errors"
11+
"fmt"
12+
"io"
13+
"math/big"
14+
"net"
15+
"net/http"
16+
"net/url"
17+
"testing"
18+
"time"
19+
20+
"github.com/stretchr/testify/assert"
21+
"github.com/stretchr/testify/require"
22+
23+
"github.com/smallstep/pkcs7"
24+
"github.com/smallstep/scep"
25+
"go.step.sm/crypto/minica"
26+
"go.step.sm/crypto/x509util"
27+
28+
"github.com/smallstep/certificates/ca"
29+
"github.com/smallstep/certificates/cas/apiv1"
30+
)
31+
32+
func newCAClient(t *testing.T, caURL, rootFilepath string) *ca.Client {
33+
caClient, err := ca.NewClient(
34+
caURL,
35+
ca.WithRootFile(rootFilepath),
36+
)
37+
require.NoError(t, err)
38+
return caClient
39+
}
40+
41+
func requireHealthyCA(t *testing.T, caClient *ca.Client) {
42+
ctx := context.Background()
43+
healthResponse, err := caClient.HealthWithContext(ctx)
44+
require.NoError(t, err)
45+
if assert.NotNil(t, healthResponse) {
46+
require.Equal(t, "ok", healthResponse.Status)
47+
}
48+
}
49+
50+
// reservePort "reserves" a TCP port by opening a listener on a random
51+
// port and immediately closing it. The port can then be assumed to be
52+
// available for running a server on.
53+
func reservePort(t *testing.T) (host, port string) {
54+
t.Helper()
55+
l, err := net.Listen("tcp", ":0")
56+
require.NoError(t, err)
57+
58+
address := l.Addr().String()
59+
err = l.Close()
60+
require.NoError(t, err)
61+
62+
host, port, err = net.SplitHostPort(address)
63+
require.NoError(t, err)
64+
65+
return
66+
}
67+
68+
type client struct {
69+
caURL string
70+
caCert *x509.Certificate
71+
httpClient *http.Client
72+
}
73+
74+
func createSCEPClient(t *testing.T, caURL string, root *x509.Certificate) *client {
75+
t.Helper()
76+
trustedRoots := x509.NewCertPool()
77+
trustedRoots.AddCert(root)
78+
transport := http.DefaultTransport.(*http.Transport).Clone()
79+
transport.TLSClientConfig = &tls.Config{
80+
RootCAs: trustedRoots,
81+
}
82+
httpClient := &http.Client{
83+
Transport: transport,
84+
}
85+
return &client{
86+
caURL: caURL,
87+
httpClient: httpClient,
88+
}
89+
}
90+
91+
func (c *client) getCACert(t *testing.T) error {
92+
// return early if CA certificate already available
93+
if c.caCert != nil {
94+
return nil
95+
}
96+
97+
resp, err := c.httpClient.Get(fmt.Sprintf("%s?operation=GetCACert&message=test", c.caURL))
98+
if err != nil {
99+
return fmt.Errorf("failed get request: %w", err)
100+
}
101+
defer resp.Body.Close()
102+
103+
body, err := io.ReadAll(resp.Body)
104+
if err != nil {
105+
return fmt.Errorf("failed reading response body: %w", err)
106+
}
107+
108+
t.Log(string(body))
109+
110+
// SCEP CA/RA certificate selection. If there's only a single certificate, it will
111+
// be used as the CA certificate at all times. If there's multiple, the first certificate
112+
// is assumed to be the certificate of the recipient to encrypt messages to.
113+
switch ct := resp.Header.Get("Content-Type"); ct {
114+
case "application/x-x509-ca-cert":
115+
cert, err := x509.ParseCertificate(body)
116+
if err != nil {
117+
return fmt.Errorf("failed parsing response body: %w", err)
118+
}
119+
if _, ok := cert.PublicKey.(*rsa.PublicKey); !ok {
120+
return fmt.Errorf("certificate has unexpected public key type %T", cert.PublicKey)
121+
}
122+
c.caCert = cert
123+
case "application/x-x509-ca-ra-cert":
124+
certs, err := scep.CACerts(body)
125+
if err != nil {
126+
return fmt.Errorf("failed parsing response body: %w", err)
127+
}
128+
cert := certs[0]
129+
if _, ok := cert.PublicKey.(*rsa.PublicKey); !ok {
130+
return fmt.Errorf("certificate has unexpected public key type %T", cert.PublicKey)
131+
}
132+
c.caCert = cert
133+
default:
134+
return fmt.Errorf("unexpected content-type value %q", ct)
135+
}
136+
137+
return nil
138+
}
139+
140+
func (c *client) requestCertificate(t *testing.T, commonName string, sans []string) (*x509.Certificate, error) {
141+
if err := c.getCACert(t); err != nil {
142+
return nil, fmt.Errorf("failed getting CA certificate: %w", err)
143+
}
144+
145+
signer, err := rsa.GenerateKey(rand.Reader, 2048)
146+
if err != nil {
147+
return nil, fmt.Errorf("failed creating SCEP private key: %w", err)
148+
}
149+
150+
csr, err := x509util.CreateCertificateRequest(commonName, sans, signer)
151+
if err != nil {
152+
return nil, fmt.Errorf("failed creating CSR: %w", err)
153+
}
154+
155+
tmpl := &x509.Certificate{
156+
Subject: csr.Subject,
157+
PublicKey: signer.Public(),
158+
SerialNumber: big.NewInt(1),
159+
NotBefore: time.Now().Add(-1 * time.Hour),
160+
NotAfter: time.Now().Add(1 * time.Hour),
161+
DNSNames: csr.DNSNames,
162+
IPAddresses: csr.IPAddresses,
163+
EmailAddresses: csr.EmailAddresses,
164+
URIs: csr.URIs,
165+
}
166+
167+
selfSigned, err := x509.CreateCertificate(rand.Reader, tmpl, tmpl, signer.Public(), signer)
168+
if err != nil {
169+
return nil, fmt.Errorf("failed creating self signed certificate: %w", err)
170+
}
171+
selfSignedCertificate, err := x509.ParseCertificate(selfSigned)
172+
if err != nil {
173+
return nil, fmt.Errorf("failed parsing self signed certificate: %w", err)
174+
}
175+
176+
msgTmpl := &scep.PKIMessage{
177+
TransactionID: "test-1",
178+
MessageType: scep.PKCSReq,
179+
SenderNonce: []byte("test-nonce-1"),
180+
Recipients: []*x509.Certificate{c.caCert},
181+
SignerCert: selfSignedCertificate,
182+
SignerKey: signer,
183+
}
184+
185+
msg, err := scep.NewCSRRequest(csr, msgTmpl)
186+
if err != nil {
187+
return nil, fmt.Errorf("failed creating SCEP PKCSReq message: %w", err)
188+
}
189+
190+
t.Log(string(msg.Raw))
191+
192+
u, err := url.Parse(c.caURL)
193+
if err != nil {
194+
return nil, fmt.Errorf("failed parsing CA URL: %w", err)
195+
}
196+
197+
opURL := u.ResolveReference(&url.URL{RawQuery: fmt.Sprintf("operation=PKIOperation&message=%s", url.QueryEscape(base64.StdEncoding.EncodeToString(msg.Raw)))})
198+
resp, err := c.httpClient.Get(opURL.String())
199+
if err != nil {
200+
return nil, fmt.Errorf("failed get request: %w", err)
201+
}
202+
defer resp.Body.Close()
203+
204+
if ct := resp.Header.Get("Content-Type"); ct != "application/x-pki-message" {
205+
return nil, fmt.Errorf("received unexpected content type %q", ct)
206+
}
207+
208+
body, err := io.ReadAll(resp.Body)
209+
if err != nil {
210+
return nil, fmt.Errorf("failed reading response body: %w", err)
211+
}
212+
213+
t.Log(string(body))
214+
215+
signedData, err := pkcs7.Parse(body)
216+
if err != nil {
217+
return nil, fmt.Errorf("failed parsing response body: %w", err)
218+
}
219+
220+
// TODO: verify the signature?
221+
222+
p7, err := pkcs7.Parse(signedData.Content)
223+
if err != nil {
224+
return nil, fmt.Errorf("failed decrypting inner p7: %w", err)
225+
}
226+
227+
content, err := p7.Decrypt(selfSignedCertificate, signer)
228+
if err != nil {
229+
return nil, fmt.Errorf("failed decrypting response: %w", err)
230+
}
231+
232+
p7, err = pkcs7.Parse(content)
233+
if err != nil {
234+
return nil, fmt.Errorf("failed parsing p7 content: %w", err)
235+
}
236+
237+
cert := p7.Certificates[0]
238+
239+
return cert, nil
240+
}
241+
242+
type testCAS struct {
243+
ca *minica.CA
244+
}
245+
246+
func (c *testCAS) CreateCertificate(req *apiv1.CreateCertificateRequest) (*apiv1.CreateCertificateResponse, error) {
247+
cert, err := c.ca.SignCSR(req.CSR)
248+
if err != nil {
249+
return nil, fmt.Errorf("failed signing CSR: %w", err)
250+
}
251+
252+
return &apiv1.CreateCertificateResponse{
253+
Certificate: cert,
254+
CertificateChain: []*x509.Certificate{cert, c.ca.Intermediate},
255+
}, nil
256+
}
257+
func (c *testCAS) RenewCertificate(req *apiv1.RenewCertificateRequest) (*apiv1.RenewCertificateResponse, error) {
258+
return nil, errors.New("not implemented")
259+
}
260+
261+
func (c *testCAS) RevokeCertificate(req *apiv1.RevokeCertificateRequest) (*apiv1.RevokeCertificateResponse, error) {
262+
return nil, errors.New("not implemented")
263+
}
264+
265+
func (c *testCAS) GetCertificateAuthority(req *apiv1.GetCertificateAuthorityRequest) (*apiv1.GetCertificateAuthorityResponse, error) {
266+
return &apiv1.GetCertificateAuthorityResponse{
267+
RootCertificate: c.ca.Root,
268+
IntermediateCertificates: []*x509.Certificate{c.ca.Intermediate},
269+
}, nil
270+
}
271+
272+
var _ apiv1.CertificateAuthorityService = (*testCAS)(nil)
273+
var _ apiv1.CertificateAuthorityGetter = (*testCAS)(nil)

0 commit comments

Comments
 (0)