Skip to content

Commit 293af49

Browse files
authored
Merge pull request #38 from mobe1/features/include_tlsunique
Include challenge password attribute if required by EST server
2 parents a0d54bb + dae7d89 commit 293af49

File tree

7 files changed

+227
-209
lines changed

7 files changed

+227
-209
lines changed

README.md

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -82,6 +82,19 @@ Using a configuration file, we can enroll with a private key resident on a
8282
hardware module, such as a hardware security module (HSM) or a Trusted Platform
8383
Module 2.0 (TPM) device. Refer to the documentation for more details.
8484

85+
### Enforcing proof of possession
86+
87+
If one would like to enforce the PoP as defined in [RFC 7030 section 3.5](https://www.rfc-editor.org/rfc/rfc7030#section-3.5), we have to pass the signing key to EST client so it can include the challenge password in the CSR and then sign it once again.
88+
89+
user@host:$ estclient enroll -server localhost:8443 -explicit anchor.pem -csr csr.pem -signingkey key.pem -out cert.pem
90+
91+
Reminder,
92+
93+
* the `tls-unique` value mentioned in RFC 7030 is specific to TLS v1.2
94+
* the options `-key` and `-signingkey` are not necessarily the same
95+
* `-key` is used for mTLS purpose like during an initial enroll
96+
* `-signingkey` is the key signing the CSR and therefore the one to be enrolled
97+
8598
### Enrolling with a server-generated private key
8699

87100
If we're unable or unwilling to create our own private key, the EST server can

client.go

Lines changed: 101 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ package est
1818
import (
1919
"bytes"
2020
"context"
21+
"crypto/rand"
2122
"crypto/tls"
2223
"crypto/x509"
2324
"errors"
@@ -26,10 +27,17 @@ import (
2627
"io/ioutil"
2728
"mime"
2829
"mime/multipart"
30+
"net"
2931
"net/http"
3032
"strconv"
3133
"strings"
3234
"time"
35+
36+
"github.com/smallstep/scep/x509util"
37+
)
38+
39+
const (
40+
tcpProtocol string = "tcp"
3341
)
3442

3543
// Client is an EST client implementing the Enrollment over Secure Transport
@@ -70,6 +78,11 @@ type Client struct {
7078
// Trusted Platform Module (TPM) or other hardware device.
7179
PrivateKey interface{}
7280

81+
// SigningKey is an optional private key to use for signing CSRs during initial enrollment.
82+
//
83+
// If not set, the challenge password field will not be included in the CSR.
84+
SigningKey interface{}
85+
7386
// AdditionalHeaders are additional HTTP headers to include with the
7487
// request to the EST server.
7588
AdditionalHeaders map[string]string
@@ -111,7 +124,12 @@ func (c *Client) CACerts(ctx context.Context) ([]*x509.Certificate, error) {
111124
return nil, err
112125
}
113126

114-
resp, err := c.makeHTTPClient().Do(req)
127+
httpc, _, err := c.makeHTTPClient()
128+
if err != nil {
129+
return nil, err
130+
}
131+
132+
resp, err := httpc.Do(req)
115133
if err != nil {
116134
return nil, fmt.Errorf("failed to execute HTTP request: %w", err)
117135
}
@@ -135,7 +153,12 @@ func (c *Client) CSRAttrs(ctx context.Context) (CSRAttrs, error) {
135153
return CSRAttrs{}, err
136154
}
137155

138-
resp, err := c.makeHTTPClient().Do(req)
156+
httpc, _, err := c.makeHTTPClient()
157+
if err != nil {
158+
return CSRAttrs{}, err
159+
}
160+
161+
resp, err := httpc.Do(req)
139162
if err != nil {
140163
return CSRAttrs{}, fmt.Errorf("failed to execute HTTP request: %w", err)
141164
}
@@ -177,19 +200,31 @@ func (c *Client) Reenroll(ctx context.Context, r *x509.CertificateRequest) (*x50
177200

178201
// Enroll requests a new certificate.
179202
func (c *Client) enrollCommon(ctx context.Context, r *x509.CertificateRequest, renew bool) (*x509.Certificate, error) {
180-
reqBody := ioutil.NopCloser(bytes.NewBuffer(base64Encode(r.Raw)))
181-
182203
var endpoint = enrollEndpoint
183204
if renew {
184205
endpoint = reenrollEndpoint
185206
}
186207

208+
httpc, tlsUnique64, err := c.makeHTTPClient()
209+
if err != nil {
210+
return nil, err
211+
}
212+
213+
crBs := r.Raw
214+
if tlsUnique64 != "" && c.SigningKey != nil {
215+
crBs, err = c.addChallengePassword(r.Raw, tlsUnique64)
216+
if err != nil {
217+
return nil, err
218+
}
219+
}
220+
221+
reqBody := ioutil.NopCloser(bytes.NewBuffer(base64Encode(crBs)))
187222
req, err := c.newRequest(ctx, http.MethodPost, endpoint, mimeTypePKCS10, encodingTypeBase64, mimeTypePKCS7, reqBody)
188223
if err != nil {
189224
return nil, err
190225
}
191226

192-
resp, err := c.makeHTTPClient().Do(req)
227+
resp, err := httpc.Do(req)
193228
if err != nil {
194229
return nil, fmt.Errorf("failed to execute HTTP request: %w", err)
195230
}
@@ -216,7 +251,12 @@ func (c *Client) ServerKeyGen(ctx context.Context, r *x509.CertificateRequest) (
216251
return nil, nil, err
217252
}
218253

219-
resp, err := c.makeHTTPClient().Do(req)
254+
httpc, _, err := c.makeHTTPClient()
255+
if err != nil {
256+
return nil, nil, err
257+
}
258+
259+
resp, err := httpc.Do(req)
220260
if err != nil {
221261
return nil, nil, fmt.Errorf("failed to execute HTTP request: %w", err)
222262
}
@@ -342,7 +382,12 @@ func (c *Client) TPMEnroll(
342382
return nil, nil, nil, err
343383
}
344384

345-
resp, err := c.makeHTTPClient().Do(req)
385+
httpc, _, err := c.makeHTTPClient()
386+
if err != nil {
387+
return nil, nil, nil, err
388+
}
389+
390+
resp, err := httpc.Do(req)
346391
if err != nil {
347392
return nil, nil, nil, fmt.Errorf("failed to execute HTTP request: %w", err)
348393
}
@@ -447,6 +492,26 @@ func (c *Client) newRequest(
447492
return req, err
448493
}
449494

495+
// addChallengePassword returns a new CSR based on the input csr.
496+
// The challenge password corresponds to the tls-unique value base64 encoded (only in TLS 1.2)
497+
func (c *Client) addChallengePassword(csr []byte, challengePassword string) ([]byte, error) {
498+
if challengePassword == "" {
499+
return csr, nil
500+
}
501+
502+
stdCsr, err := x509.ParseCertificateRequest(csr)
503+
if err != nil {
504+
return nil, err
505+
}
506+
507+
cr := x509util.CertificateRequest{
508+
CertificateRequest: *stdCsr,
509+
ChallengePassword: challengePassword,
510+
}
511+
crBs, err := x509util.CreateCertificateRequest(rand.Reader, &cr, c.SigningKey)
512+
return crBs, err
513+
}
514+
450515
// checkResponseError returns nil if the HTTP response status code is 200 OK,
451516
// otherwise it returns an error object implementing est.Error. In order to
452517
// parse the Retry-After header and return a value, note that 202 Accepted
@@ -533,8 +598,12 @@ func (c *Client) uri(endpoint string) string {
533598
}
534599

535600
// makeHTTPClient makes and configures an HTTP client for connecting to an
536-
// EST server.
537-
func (c *Client) makeHTTPClient() *http.Client {
601+
// EST server. It also returns the corresponding TLS-unique value base64 encoded which could be required by EST server.
602+
//
603+
// RFC 7030 - section 3.5 recommends including it in the CSR challenge password field.
604+
//
605+
// The value could be nil with respect to TLS version. More details at https://pkg.go.dev/crypto/tls#ConnectionState.TLSUnique
606+
func (c *Client) makeHTTPClient() (*http.Client, string, error) {
538607
var rootCAs *x509.CertPool
539608
if c.ExplicitAnchor != nil {
540609
rootCAs = c.ExplicitAnchor
@@ -550,14 +619,32 @@ func (c *Client) makeHTTPClient() *http.Client {
550619
}
551620
}
552621

553-
return &http.Client{
622+
tlsClientConfig := &tls.Config{
623+
RootCAs: rootCAs,
624+
Certificates: tlsCerts,
625+
InsecureSkipVerify: c.InsecureSkipVerify,
626+
}
627+
628+
conn, err := tls.Dial(tcpProtocol, c.Host, tlsClientConfig)
629+
if err != nil {
630+
return nil, "", err
631+
}
632+
633+
var tlsUnique64 string
634+
if tlsu := conn.ConnectionState().TLSUnique; tlsu != nil {
635+
tlsUnique64 = string(base64Encode(tlsu))
636+
} else {
637+
tlsUnique64 = ""
638+
}
639+
640+
httpc := &http.Client{
554641
Transport: &http.Transport{
555-
TLSClientConfig: &tls.Config{
556-
RootCAs: rootCAs,
557-
Certificates: tlsCerts,
558-
InsecureSkipVerify: c.InsecureSkipVerify,
642+
DialTLSContext: func(ctx context.Context, network, addr string) (net.Conn, error) {
643+
return conn, nil
559644
},
560645
DisableKeepAlives: c.DisableKeepAlives,
561646
},
562647
}
648+
649+
return httpc, tlsUnique64, nil
563650
}

cmd/estclient/commands.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -176,6 +176,7 @@ func init() {
176176
passwordFlag,
177177
separatorFlag,
178178
serverFlag,
179+
signingKeyFlag,
179180
usernameFlag,
180181
timeoutFlag,
181182
},

cmd/estclient/config.go

Lines changed: 25 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,7 @@ type config struct {
5151
Password string `json:"password"`
5252
Explicit string `json:"explicit_anchor"`
5353
Implicit string `json:"implicit_anchor"`
54+
SigningKey *privateKey `json:"signing_key,omitempty"`
5455
PrivateKey *privateKey `json:"private_key,omitempty"`
5556
Certificates string `json:"client_certificates"`
5657
certificates []*x509.Certificate
@@ -63,6 +64,7 @@ type config struct {
6364
implicitAnchor *x509.CertPool
6465
insecure bool
6566
openPrivateKey interface{}
67+
openSigningKey interface{}
6668
separator string
6769
timeout time.Duration
6870
}
@@ -148,6 +150,7 @@ func (cfg *config) MakeClient() (*est.Client, error) {
148150
ImplicitAnchor: cfg.implicitAnchor,
149151
HostHeader: cfg.HostHeader,
150152
PrivateKey: cfg.openPrivateKey,
153+
SigningKey: cfg.openSigningKey,
151154
Certificates: cfg.certificates,
152155
Username: cfg.Username,
153156
Password: cfg.Password,
@@ -166,10 +169,14 @@ func (cfg *config) MakeClient() (*est.Client, error) {
166169
// nil, the private key from the configuration will be used.
167170
func (cfg *config) GenerateCSR(key interface{}) (*x509.CertificateRequest, error) {
168171
if key == nil {
169-
if cfg.openPrivateKey == nil {
172+
switch {
173+
case cfg.openSigningKey != nil:
174+
key = cfg.openSigningKey
175+
case cfg.openPrivateKey != nil:
176+
key = cfg.openPrivateKey
177+
default:
170178
return nil, errNoPrivateKey
171179
}
172-
key = cfg.openPrivateKey
173180
}
174181

175182
tmpl, err := cfg.CSRTemplate()
@@ -629,6 +636,22 @@ func newConfig(set *flag.FlagSet) (config, error) {
629636
cfg.closeFuncs = append(cfg.closeFuncs, closeFunc)
630637
}
631638

639+
// Process signing key. Note that a signing key located in a file is the
640+
// only type which can be specified at the command line.
641+
if filename, ok := cfg.flags[signingKeyFlag]; ok {
642+
cfg.SigningKey = &privateKey{Path: fullPath(wd, filename)}
643+
}
644+
645+
if cfg.SigningKey != nil {
646+
signingkey, closeFunc, err := cfg.SigningKey.Get(cfg.baseDir)
647+
if err != nil {
648+
return config{}, fmt.Errorf("failed to get signing key: %v", err)
649+
}
650+
651+
cfg.openSigningKey = signingkey
652+
cfg.closeFuncs = append(cfg.closeFuncs, closeFunc)
653+
}
654+
632655
return cfg, nil
633656
}
634657

cmd/estclient/flags.go

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -89,6 +89,7 @@ const (
8989
separatorFlag = "separator"
9090
serialNumberFlag = "sn"
9191
serverFlag = "server"
92+
signingKeyFlag = "signingkey"
9293
streetAddressFlag = "street"
9394
timeoutFlag = "timeout"
9495
tpmFlag = "tpm"
@@ -240,6 +241,11 @@ var optDefs = map[string]option{
240241
desc: "server host and port",
241242
defaultValue: "",
242243
},
244+
signingKeyFlag: {
245+
argFmt: pathFmt,
246+
desc: "CSR signing key",
247+
defaultValue: "",
248+
},
243249
usernameFlag: {
244250
argFmt: stringFmt,
245251
defaultLabel: "none",

go.mod

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -5,18 +5,19 @@ go 1.22.1
55
require (
66
github.com/ThalesIgnite/crypto11 v1.2.1
77
github.com/globalsign/pemfile v1.0.0
8-
github.com/globalsign/tpmkeys v1.0.3
8+
github.com/globalsign/tpmkeys v1.0.4-0.20250806150134-4b00bd5e66cb
99
github.com/go-chi/chi v4.1.2+incompatible
10-
github.com/google/go-tpm v0.9.5
10+
github.com/google/go-tpm v0.9.6
11+
github.com/smallstep/scep v0.0.0-20250318231241-a25cabb69492
1112
go.mozilla.org/pkcs7 v0.0.0-20200128120323-432b2356ecb1
12-
golang.org/x/crypto v0.31.0
13+
golang.org/x/crypto v0.33.0
1314
golang.org/x/time v0.0.0-20210220033141-f8bda1e9f3ba
1415
)
1516

1617
require (
1718
github.com/miekg/pkcs11 v1.0.3-0.20190429190417-a667d056470f // indirect
18-
github.com/pkg/errors v0.8.1 // indirect
19+
github.com/pkg/errors v0.9.1 // indirect
1920
github.com/thales-e-security/pool v0.0.1 // indirect
20-
golang.org/x/sys v0.28.0 // indirect
21-
golang.org/x/term v0.27.0 // indirect
21+
golang.org/x/sys v0.30.0 // indirect
22+
golang.org/x/term v0.29.0 // indirect
2223
)

0 commit comments

Comments
 (0)