Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Include challenge password attribute if required by EST server #38

Open
wants to merge 2 commits into
base: master
Choose a base branch
from
Open
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
9 changes: 9 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -115,3 +115,12 @@ use that chain to reenroll:
Note that when we omit the `-csr` option when reenrolling, the EST client
automatically generates a CSR for us by copying the subject field and subject
alternative name extension from the certificate we're renewing.

### EST client samples

Basic and common operations are available in the samples directory.

The EST client is expecting `root`and `intermediate` certificates of an EST server.
Because it can also be configured in a way to establish an unsecure communication, those certificates could be omitted (for testing purposes only).

In any case, sample can be changed to meet user requirements : Include CA certificates, change user credentials, enroll a different CSR etc.
111 changes: 86 additions & 25 deletions client.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,12 +18,12 @@ package est
import (
"bytes"
"context"
"crypto/rand"
"crypto/tls"
"crypto/x509"
"errors"
"fmt"
"io"
"io/ioutil"
"mime"
"mime/multipart"
"net/http"
Expand Down Expand Up @@ -96,6 +96,15 @@ type Client struct {
// verified in some out-of-band manner before any further EST operations with
// that server are performed.
InsecureSkipVerify bool

// httpc handles all EST http requests/responses
httpc *http.Client

// TLS-unique channel binding value is computed during the TLS handshake.
// RFC 7030 - section 3.5 recommends including it in the CSR.
//
// The value could be nil with respect to TLS version. More details at https://pkg.go.dev/crypto/tls#ConnectionState.TLSUnique
tlsUnique []byte
}

// Client constants.
Expand All @@ -111,7 +120,11 @@ func (c *Client) CACerts(ctx context.Context) ([]*x509.Certificate, error) {
return nil, err
}

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

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

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

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

return readCSRAttrsResponse(resp.Body)
}
attributes, err := readCSRAttrsResponse(resp.Body)

challengeRequired := false
for _, oid := range attributes.OIDs {
if oid.Equal(oidChallengePassword) {
c.tlsUnique = resp.TLS.TLSUnique
challengeRequired = true
}
}
if !challengeRequired {
c.tlsUnique = nil
}

// Enroll requests a new certificate.
func (c *Client) Enroll(ctx context.Context, r *x509.CertificateRequest) (*x509.Certificate, error) {
return c.enrollCommon(ctx, r, false)
return attributes, err
}

// Reenroll renews an existing certificate.
func (c *Client) Reenroll(ctx context.Context, r *x509.CertificateRequest) (*x509.Certificate, error) {
return c.enrollCommon(ctx, r, true)
// Enroll requests a new certificate based on the csr der-encoded.
func (c *Client) Enroll(ctx context.Context, csr []byte) (*x509.Certificate, error) {
return c.enrollCommon(ctx, csr, false)
}

// 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)))
// Reenroll renews an existing certificate based on the csr der-encoded.
func (c *Client) Reenroll(ctx context.Context, csr []byte) (*x509.Certificate, error) {
return c.enrollCommon(ctx, csr, true)
}

func (c *Client) enrollCommon(ctx context.Context, csr []byte, renew bool) (*x509.Certificate, error) {
var reqBody io.ReadCloser
var endpoint = enrollEndpoint

if renew {
endpoint = reenrollEndpoint
c.makeHTTPClient()
}

// Re-evaluate the TLS-unique value
c.CSRAttrs(ctx)
if c.tlsUnique != nil {
crBs, err := c.addTlsUnique(csr)
if err != nil {
return nil, err
}
reqBody = io.NopCloser(bytes.NewBuffer(base64Encode(crBs)))
} else {
reqBody = io.NopCloser(bytes.NewBuffer(base64Encode(csr)))
}

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 := c.httpc.Do(req)
if err != nil {
return nil, fmt.Errorf("failed to execute HTTP request: %w", err)
}
Expand All @@ -206,17 +248,32 @@ func (c *Client) enrollCommon(ctx context.Context, r *x509.CertificateRequest, r
return readCertResponse(resp.Body)
}

// ServerKeyGen requests a new certificate and a server-generated private key.
func (c *Client) ServerKeyGen(ctx context.Context, r *x509.CertificateRequest) (*x509.Certificate, []byte, error) {
reqBody := ioutil.NopCloser(bytes.NewBuffer(base64Encode(r.Raw)))
// Creates and returns a new CSR where the challenge password attribute contains the Tls-unique value.
func (c *Client) addTlsUnique(csr []byte) ([]byte, error) {
standardLibCsr, _ := x509.ParseCertificateRequest(csr)
cr := CertificateRequest{
CertificateRequest: *standardLibCsr,
ChallengePassword: string(base64Encode(c.tlsUnique)),
}
crBs, err := CreateCertificateRequest(rand.Reader, &cr, c.PrivateKey)
return crBs, err
}

// ServerKeyGen requests a new certificate and a server-generated private key based on the csr der-encoded.
func (c *Client) ServerKeyGen(ctx context.Context, csr []byte) (*x509.Certificate, []byte, error) {
reqBody := io.NopCloser(bytes.NewBuffer(base64Encode(csr)))

req, err := c.newRequest(ctx, http.MethodPost, serverkeygenEndpoint,
mimeTypePKCS10, encodingTypeBase64, mimeTypeMultipart, reqBody)
if err != nil {
return nil, nil, err
}

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

resp, err := c.httpc.Do(req)
if err != nil {
return nil, nil, fmt.Errorf("failed to execute HTTP request: %w", err)
}
Expand Down Expand Up @@ -260,7 +317,7 @@ func (c *Client) ServerKeyGen(ctx context.Context, r *x509.CertificateRequest) (
// body.
if ce := part.Header.Get(transferEncodingHeader); ce == "" {
return nil, nil, fmt.Errorf("missing %s header", transferEncodingHeader)
} else if strings.ToUpper(ce) != strings.ToUpper(encodingTypeBase64) {
} else if !strings.EqualFold(ce, encodingTypeBase64) {
return nil, nil, fmt.Errorf("unexpected %s: %s", transferEncodingHeader, ce)
}

Expand Down Expand Up @@ -342,15 +399,19 @@ func (c *Client) TPMEnroll(
return nil, nil, nil, err
}

reqBody := ioutil.NopCloser(buf)
reqBody := io.NopCloser(buf)

req, err := c.newRequest(ctx, http.MethodPost, tpmenrollEndpoint,
contentType, "", mimeTypeMultipart, reqBody)
if err != nil {
return nil, nil, nil, err
}

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

resp, err := c.httpc.Do(req)
if err != nil {
return nil, nil, nil, fmt.Errorf("failed to execute HTTP request: %w", err)
}
Expand Down Expand Up @@ -480,7 +541,7 @@ func checkResponseError(r *http.Response) error {
if err == nil || r.Header.Get(contentTypeHeader) == "" {
switch mediaType {
case "", mimeTypeTextPlain, mimeTypeJSON, mimeTypeProblemJSON:
data, err := ioutil.ReadAll(r.Body)
data, err := io.ReadAll(r.Body)
if err != nil {
return err
}
Expand Down Expand Up @@ -542,7 +603,7 @@ 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 {
func (c *Client) makeHTTPClient() {
var rootCAs *x509.CertPool
if c.ExplicitAnchor != nil {
rootCAs = c.ExplicitAnchor
Expand All @@ -558,7 +619,7 @@ func (c *Client) makeHTTPClient() *http.Client {
}
}

return &http.Client{
c.httpc = &http.Client{
Transport: &http.Transport{
TLSClientConfig: &tls.Config{
RootCAs: rootCAs,
Expand Down
6 changes: 3 additions & 3 deletions cmd/estclient/enroll.go
Original file line number Diff line number Diff line change
Expand Up @@ -129,11 +129,11 @@ func enrollCommon(w io.Writer, set *flag.FlagSet, renew, keygen bool) error {
var cert *x509.Certificate
var key []byte
if renew {
cert, err = client.Reenroll(ctx, csr)
cert, err = client.Reenroll(ctx, csr.Raw)
} else if keygen {
cert, key, err = client.ServerKeyGen(ctx, csr)
cert, key, err = client.ServerKeyGen(ctx, csr.Raw)
} else {
cert, err = client.Enroll(ctx, csr)
cert, err = client.Enroll(ctx, csr.Raw)
}
if err != nil {
return fmt.Errorf("failed to get certificate: %v", err)
Expand Down
3 changes: 1 addition & 2 deletions common.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,6 @@ package est
import (
"encoding/asn1"
"io"
"io/ioutil"
)

// URI constants.
Expand Down Expand Up @@ -75,6 +74,6 @@ var (
// consumeAndClose discards any remaining data in the io.ReadCloser and then
// closes it.
func consumeAndClose(rc io.ReadCloser) {
io.Copy(ioutil.Discard, rc)
io.Copy(io.Discard, rc)
rc.Close()
}
Loading
Loading