Skip to content
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
13 changes: 13 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,19 @@ Using a configuration file, we can enroll with a private key resident on a
hardware module, such as a hardware security module (HSM) or a Trusted Platform
Module 2.0 (TPM) device. Refer to the documentation for more details.

### Enforcing proof of possession

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.

user@host:$ estclient enroll -server localhost:8443 -explicit anchor.pem -csr csr.pem -signingkey key.pem -out cert.pem

Reminder,

* the `tls-unique` value mentioned in RFC 7030 is specific to TLS v1.2
* the options `-key` and `-signingkey` are not necessarily the same
* `-key` is used for mTLS purpose like during an initial enroll
* `-signingkey` is the key signing the CSR and therefore the one to be enrolled

### Enrolling with a server-generated private key

If we're unable or unwilling to create our own private key, the EST server can
Expand Down
115 changes: 101 additions & 14 deletions client.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ package est
import (
"bytes"
"context"
"crypto/rand"
"crypto/tls"
"crypto/x509"
"errors"
Expand All @@ -26,10 +27,17 @@ import (
"io/ioutil"
"mime"
"mime/multipart"
"net"
"net/http"
"strconv"
"strings"
"time"

"github.com/smallstep/scep/x509util"
)

const (
tcpProtocol string = "tcp"
)

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

// SigningKey is an optional private key to use for signing CSRs during initial enrollment.
//
// If not set, the challenge password field will not be included in the CSR.
SigningKey interface{}

// AdditionalHeaders are additional HTTP headers to include with the
// request to the EST server.
AdditionalHeaders map[string]string
Expand Down Expand Up @@ -111,7 +124,12 @@ func (c *Client) CACerts(ctx context.Context) ([]*x509.Certificate, error) {
return nil, err
}

resp, err := c.makeHTTPClient().Do(req)
httpc, _, err := c.makeHTTPClient()
if err != nil {
return nil, err
}

resp, err := httpc.Do(req)
if err != nil {
return nil, fmt.Errorf("failed to execute HTTP request: %w", err)
}
Expand All @@ -135,7 +153,12 @@ func (c *Client) CSRAttrs(ctx context.Context) (CSRAttrs, error) {
return CSRAttrs{}, err
}

resp, err := c.makeHTTPClient().Do(req)
httpc, _, err := c.makeHTTPClient()
if err != nil {
return CSRAttrs{}, err
}

resp, err := httpc.Do(req)
if err != nil {
return CSRAttrs{}, fmt.Errorf("failed to execute HTTP request: %w", err)
}
Expand Down Expand Up @@ -177,19 +200,31 @@ func (c *Client) Reenroll(ctx context.Context, r *x509.CertificateRequest) (*x50

// Enroll requests a new certificate.
func (c *Client) enrollCommon(ctx context.Context, r *x509.CertificateRequest, renew bool) (*x509.Certificate, error) {
reqBody := ioutil.NopCloser(bytes.NewBuffer(base64Encode(r.Raw)))

var endpoint = enrollEndpoint
if renew {
endpoint = reenrollEndpoint
}

httpc, tlsUnique64, err := c.makeHTTPClient()
if err != nil {
return nil, err
}

crBs := r.Raw
if tlsUnique64 != "" && c.SigningKey != nil {
crBs, err = c.addChallengePassword(r.Raw, tlsUnique64)
if err != nil {
return nil, err
}
}

reqBody := ioutil.NopCloser(bytes.NewBuffer(base64Encode(crBs)))
req, err := c.newRequest(ctx, http.MethodPost, endpoint, mimeTypePKCS10, encodingTypeBase64, mimeTypePKCS7, reqBody)
if err != nil {
return nil, err
}

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

resp, err := c.makeHTTPClient().Do(req)
httpc, _, err := c.makeHTTPClient()
if err != nil {
return nil, nil, err
}

resp, err := httpc.Do(req)
if err != nil {
return nil, nil, fmt.Errorf("failed to execute HTTP request: %w", err)
}
Expand Down Expand Up @@ -342,7 +382,12 @@ func (c *Client) TPMEnroll(
return nil, nil, nil, err
}

resp, err := c.makeHTTPClient().Do(req)
httpc, _, err := c.makeHTTPClient()
if err != nil {
return nil, nil, nil, err
}

resp, err := httpc.Do(req)
if err != nil {
return nil, nil, nil, fmt.Errorf("failed to execute HTTP request: %w", err)
}
Expand Down Expand Up @@ -447,6 +492,26 @@ func (c *Client) newRequest(
return req, err
}

// addChallengePassword returns a new CSR based on the input csr.
// The challenge password corresponds to the tls-unique value base64 encoded (only in TLS 1.2)
func (c *Client) addChallengePassword(csr []byte, challengePassword string) ([]byte, error) {
if challengePassword == "" {
return csr, nil
}

stdCsr, err := x509.ParseCertificateRequest(csr)
if err != nil {
return nil, err
}

cr := x509util.CertificateRequest{
CertificateRequest: *stdCsr,
ChallengePassword: challengePassword,
}
crBs, err := x509util.CreateCertificateRequest(rand.Reader, &cr, c.SigningKey)
return crBs, err
}

// checkResponseError returns nil if the HTTP response status code is 200 OK,
// otherwise it returns an error object implementing est.Error. In order to
// parse the Retry-After header and return a value, note that 202 Accepted
Expand Down Expand Up @@ -533,8 +598,12 @@ func (c *Client) uri(endpoint string) string {
}

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

return &http.Client{
tlsClientConfig := &tls.Config{
RootCAs: rootCAs,
Certificates: tlsCerts,
InsecureSkipVerify: c.InsecureSkipVerify,
}

conn, err := tls.Dial(tcpProtocol, c.Host, tlsClientConfig)
if err != nil {
return nil, "", err
}

var tlsUnique64 string
if tlsu := conn.ConnectionState().TLSUnique; tlsu != nil {
tlsUnique64 = string(base64Encode(tlsu))
} else {
tlsUnique64 = ""
}

httpc := &http.Client{
Transport: &http.Transport{
TLSClientConfig: &tls.Config{
RootCAs: rootCAs,
Certificates: tlsCerts,
InsecureSkipVerify: c.InsecureSkipVerify,
DialTLSContext: func(ctx context.Context, network, addr string) (net.Conn, error) {
return conn, nil
},
DisableKeepAlives: c.DisableKeepAlives,
},
}

return httpc, tlsUnique64, nil
}
1 change: 1 addition & 0 deletions cmd/estclient/commands.go
Original file line number Diff line number Diff line change
Expand Up @@ -176,6 +176,7 @@ func init() {
passwordFlag,
separatorFlag,
serverFlag,
signingKeyFlag,
usernameFlag,
timeoutFlag,
},
Expand Down
27 changes: 25 additions & 2 deletions cmd/estclient/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@ type config struct {
Password string `json:"password"`
Explicit string `json:"explicit_anchor"`
Implicit string `json:"implicit_anchor"`
SigningKey *privateKey `json:"signing_key,omitempty"`
PrivateKey *privateKey `json:"private_key,omitempty"`
Certificates string `json:"client_certificates"`
certificates []*x509.Certificate
Expand All @@ -63,6 +64,7 @@ type config struct {
implicitAnchor *x509.CertPool
insecure bool
openPrivateKey interface{}
openSigningKey interface{}
separator string
timeout time.Duration
}
Expand Down Expand Up @@ -148,6 +150,7 @@ func (cfg *config) MakeClient() (*est.Client, error) {
ImplicitAnchor: cfg.implicitAnchor,
HostHeader: cfg.HostHeader,
PrivateKey: cfg.openPrivateKey,
SigningKey: cfg.openSigningKey,
Certificates: cfg.certificates,
Username: cfg.Username,
Password: cfg.Password,
Expand All @@ -166,10 +169,14 @@ func (cfg *config) MakeClient() (*est.Client, error) {
// nil, the private key from the configuration will be used.
func (cfg *config) GenerateCSR(key interface{}) (*x509.CertificateRequest, error) {
if key == nil {
if cfg.openPrivateKey == nil {
switch {
case cfg.openSigningKey != nil:
key = cfg.openSigningKey
case cfg.openPrivateKey != nil:
key = cfg.openPrivateKey
default:
return nil, errNoPrivateKey
}
key = cfg.openPrivateKey
}

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

// Process signing key. Note that a signing key located in a file is the
// only type which can be specified at the command line.
if filename, ok := cfg.flags[signingKeyFlag]; ok {
cfg.SigningKey = &privateKey{Path: fullPath(wd, filename)}
}

if cfg.SigningKey != nil {
signingkey, closeFunc, err := cfg.SigningKey.Get(cfg.baseDir)
if err != nil {
return config{}, fmt.Errorf("failed to get signing key: %v", err)
}

cfg.openSigningKey = signingkey
cfg.closeFuncs = append(cfg.closeFuncs, closeFunc)
}

return cfg, nil
}

Expand Down
6 changes: 6 additions & 0 deletions cmd/estclient/flags.go
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,7 @@ const (
separatorFlag = "separator"
serialNumberFlag = "sn"
serverFlag = "server"
signingKeyFlag = "signingkey"
streetAddressFlag = "street"
timeoutFlag = "timeout"
tpmFlag = "tpm"
Expand Down Expand Up @@ -240,6 +241,11 @@ var optDefs = map[string]option{
desc: "server host and port",
defaultValue: "",
},
signingKeyFlag: {
argFmt: pathFmt,
desc: "CSR signing key",
defaultValue: "",
},
usernameFlag: {
argFmt: stringFmt,
defaultLabel: "none",
Expand Down
13 changes: 7 additions & 6 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -5,18 +5,19 @@ go 1.22.1
require (
github.com/ThalesIgnite/crypto11 v1.2.1
github.com/globalsign/pemfile v1.0.0
github.com/globalsign/tpmkeys v1.0.3
github.com/globalsign/tpmkeys v1.0.4-0.20250806150134-4b00bd5e66cb
github.com/go-chi/chi v4.1.2+incompatible
github.com/google/go-tpm v0.9.5
github.com/google/go-tpm v0.9.6
github.com/smallstep/scep v0.0.0-20250318231241-a25cabb69492
go.mozilla.org/pkcs7 v0.0.0-20200128120323-432b2356ecb1
golang.org/x/crypto v0.31.0
golang.org/x/crypto v0.33.0
golang.org/x/time v0.0.0-20210220033141-f8bda1e9f3ba
)

require (
github.com/miekg/pkcs11 v1.0.3-0.20190429190417-a667d056470f // indirect
github.com/pkg/errors v0.8.1 // indirect
github.com/pkg/errors v0.9.1 // indirect
github.com/thales-e-security/pool v0.0.1 // indirect
golang.org/x/sys v0.28.0 // indirect
golang.org/x/term v0.27.0 // indirect
golang.org/x/sys v0.30.0 // indirect
golang.org/x/term v0.29.0 // indirect
)
Loading