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
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ As this project's primary aim is handling device certificate renewal, optional
features of the RFC have been omitted including:

* 4.3 - CMC
* 4.4 - Server side key generation
* 4.4 - server side key generation is supported partially, without the key encryption.
* 4.5 - CSR attributes

## Contributing
Expand Down
3 changes: 2 additions & 1 deletion cmd/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ func main() {
port := flag.Int("port", 8443, "Port to listen on")
certDuration := flag.Duration("cert-duration", time.Hour*24*365*3, "How long new certs should be valid for. e.g. such as '1.5h' or '2h45m'. 3 years is default")
clientCas := flag.String("client-cas", "", "PEM encoded list of device CA's to allow. The device must present a certificate signed by a CA in this list or the `ca-cert` to authenticate")
serverKeygen := flag.Bool("serverkeygen", false, "Whether server should enable server-side keygen (default disabled)")

for _, opt := range required {
flag.StringVar(&opt.value, opt.name, "", opt.help)
Expand Down Expand Up @@ -86,7 +87,7 @@ func main() {
log.Fatal().Err(err).Msg("Unable to create tls cert handler")
}

svcHandler := est.NewStaticServiceHandler(est.NewService(rootCerts, caCert, caKey, *certDuration))
svcHandler := est.NewStaticServiceHandler(est.NewService(rootCerts, caCert, caKey, *certDuration, *serverKeygen))

e := echo.New()
s := http.Server{
Expand Down
50 changes: 50 additions & 0 deletions http_handlers.go
Original file line number Diff line number Diff line change
@@ -1,10 +1,13 @@
package est

import (
"bytes"
"errors"
"fmt"
"io"
"mime/multipart"
"net/http"
"net/textproto"

"github.com/labstack/echo/v4"
)
Expand Down Expand Up @@ -73,6 +76,53 @@ func RegisterEchoHandlers(svcHandler ServiceHandler, e *echo.Echo) {
return c.Blob(http.StatusOK, "application/pkcs7-mime; smime-type=certs-only", bytes)
})

e.POST("/.well-known/est/serverkeygen", func(c echo.Context) error {
svc, err := svcHandler.GetService(c.Request().Context(), c.Request().TLS.ServerName)
if err != nil {
return c.String(http.StatusInternalServerError, err.Error())
}
if !svc.allowServerKeygen {
return c.String(http.StatusBadRequest, "this server does not allow server-side keygen")
}
reqbytes, err := validateRequest(svc, c)
if err != nil || reqbytes == nil { // validateRequest failed and sent the response
return err
}
peerCerts := c.Request().TLS.PeerCertificates
crt, pkey, err := svc.ServerKeygen(c.Request().Context(), reqbytes, peerCerts[0])
if err != nil {
if errors.Is(err, ErrEst) {
return c.String(http.StatusBadRequest, err.Error())
}
return c.String(http.StatusInternalServerError, err.Error())
}
mw := new(bytes.Buffer)
mpWriter := multipart.NewWriter(mw)

crtHeader := make(textproto.MIMEHeader)
crtHeader.Set("Content-Type", "application/pkcs7-mime; smime-type=certs-only")
crtHeader.Set("Content-Transfer-Encoding", "base64")
partC, _ := mpWriter.CreatePart(crtHeader)
_, err = partC.Write(crt)
if err != nil {
return c.String(http.StatusInternalServerError, err.Error())
}

keyHeader := make(textproto.MIMEHeader)
keyHeader.Set("Content-Type", "application/pkcs8")
keyHeader.Set("Content-Transfer-Encoding", "base64")
partK, _ := mpWriter.CreatePart(keyHeader)
_, err = partK.Write(pkey)
if err != nil {
return c.String(http.StatusInternalServerError, err.Error())
}
err = mpWriter.Close()
if err != nil {
return c.String(http.StatusInternalServerError, err.Error())
}
contentType := "multipart/mixed; boundary=" + mpWriter.Boundary()
return c.Blob(http.StatusOK, contentType, mw.Bytes())
})
}

// validateRequest checks that the client has provided a client cert (via mTLS)
Expand Down
61 changes: 58 additions & 3 deletions http_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@ import (
"crypto/x509"
"encoding/base64"
"io"
"mime"
"mime/multipart"
"net"
"net/http"
"net/http/httptest"
Expand Down Expand Up @@ -126,13 +128,14 @@ func TestSimpleEnrollRequiresValidCert(t *testing.T) {

kp := svc.createTlsKP(t, tc.ctx, "enrollRequiresValid")

url := tc.srv.URL + ".well-known/est/simpleenroll"
url := tc.srv.URL + "/.well-known/est/simpleenroll"
client := tc.srv.Client()
transport := client.Transport.(*http.Transport)
transport.TLSClientConfig.Certificates = []tls.Certificate{*kp}

_, err := client.Post(url, "application/pkcs10", bytes.NewBuffer([]byte{}))
require.NotNil(t, err)
res, err := client.Post(url, "application/pkcs10", bytes.NewBuffer([]byte{}))
require.GreaterOrEqual(t, res.StatusCode, 400, "Server accepted invalid")
require.Nil(t, err)
})
}

Expand Down Expand Up @@ -214,6 +217,58 @@ func TestSimpleReEnroll(t *testing.T) {
})
}

func TestServerKeygen(t *testing.T) {
WithEstServer(t, func(tc testClient) {
cn := random.String(10)
kp := tc.svc.createTlsKP(t, tc.ctx, cn)
rc, data := tc.POST(t, "/.well-known/est/serverkeygen", []byte{}, kp)
require.Equal(t, 400, rc, string(data))
require.Equal(t, "The CSR could not be decoded: asn1: syntax error: sequence truncated", string(data))

var boundary string
var extract responseChecker = func(t *testing.T, res *http.Response) {
mediaType, params, err := mime.ParseMediaType(res.Header.Get("Content-Type"))
boundary = params["boundary"]
require.Equal(t, "multipart/mixed", mediaType)
require.Nil(t, err)
} // ugly but boundary is in header but response is already read by POST()

_, csr := createB64CsrDer(t, cn)
rc, buf := tc.POST(t, "/.well-known/est/serverkeygen", csr, kp, extract)
require.Equal(t, 200, rc, buf)

// multipart structure
mpr := multipart.NewReader(bytes.NewBuffer(buf), boundary)

// certificate part
part, err := mpr.NextPart()
require.Nil(t, err, string(data))
require.Equal(t, "application/pkcs7-mime; smime-type=certs-only", part.Header.Get("Content-Type"), "Wrong Content type for first part")
bytebuf, err := io.ReadAll(part)
require.Nil(t, err)
cert, err := base64.StdEncoding.DecodeString(string(bytebuf))
require.Nil(t, err)
p7c, err := pkcs7.Parse(cert)
require.Nil(t, err)
require.Equal(t, 1, len(p7c.Certificates), "wrong amout of certs")

// private key part
part, err = mpr.NextPart()
require.Nil(t, err, string(data))
require.Equal(t, "application/pkcs8", part.Header.Get("Content-Type"), "Wrong Content type for first part")
bytebuf, err = io.ReadAll(part)
require.Nil(t, err)
keyb, err := base64.StdEncoding.DecodeString(string(bytebuf))
require.Nil(t, err)
_, err = x509.ParsePKCS8PrivateKey(keyb)
require.Nil(t, err)

// test rejecting when service not allow serverkeygen
rc, buf = tc.POST(t, "/.well-known/est/serverkeygen", csr, kp, extract)
require.Equal(t, 200, rc, buf)
})
}

func (s Service) createTlsKP(t *testing.T, ctx context.Context, cn string) *tls.Certificate {
key, csrBytes := createB64CsrDer(t, cn)
bytes, err := s.Enroll(ctx, csrBytes)
Expand Down
59 changes: 51 additions & 8 deletions service.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,9 @@ import (
"bytes"
"context"
"crypto"
"crypto/ecdsa"
"crypto/rand"
"crypto/rsa"
"crypto/x509"
"crypto/x509/pkix"
"encoding/asn1"
Expand Down Expand Up @@ -88,7 +90,7 @@ func NewStaticServiceHandler(svc Service) ServiceHandler {
// Optional APIs are not implemented including:
//
// 4.3 - cmc
// 4.4 - server side key generation
// 4.4 - server side key generation is supported partially, without the key encryption.
// 4.5 - CSR attributes
type Service struct {
// Root CAs for a Factory
Expand All @@ -97,17 +99,19 @@ type Service struct {
ca *x509.Certificate
key crypto.Signer

certDuration time.Duration
allowServerKeygen bool
certDuration time.Duration
}

// NewService creates an EST7030 API for a Factory
func NewService(rootCa []*x509.Certificate, ca *x509.Certificate, key crypto.Signer, certDuration time.Duration) Service {
func NewService(rootCa []*x509.Certificate, ca *x509.Certificate, key crypto.Signer, certDuration time.Duration, allowServerKeygen bool) Service {
return Service{
rootCa: rootCa,
ca: ca,
key: key,

certDuration: certDuration,
allowServerKeygen: allowServerKeygen,
certDuration: certDuration,
}
}

Expand Down Expand Up @@ -182,6 +186,49 @@ func (s Service) ReEnroll(ctx context.Context, csrBytes []byte, curCert *x509.Ce
return s.signCsr(ctx, csr)
}

// ServerKeygen performs EST7030 enrollment operation with server generated key as per
// https://www.rfc-editor.org/rfc/rfc7030.html#section-4.1.1
// Errors can be generic errors or of the type EstError.
// server won't use additional encryption independent from TLS.
func (s Service) ServerKeygen(ctx context.Context, csrBytes []byte, curCert *x509.Certificate) ([]byte, []byte, error) {
if !s.allowServerKeygen {
return nil, nil, fmt.Errorf("server not allow serverside keygen")
}
originalCsr, err := s.loadCsr(ctx, csrBytes)
if err != nil {
return nil, nil, err
}
var newCertKey crypto.Signer
switch capub := s.ca.PublicKey.(type) {
case *rsa.PublicKey:
newCertKey, err = rsa.GenerateKey(rand.Reader, 2048)
case *ecdsa.PublicKey:
newCertKey, err = ecdsa.GenerateKey(capub.Curve, rand.Reader)
default:
return nil, nil, fmt.Errorf("nullkeytype")
}

if err != nil {
return nil, nil, err
}
// Re-key CSR, only add things that signCsr needs.
// Make a new variable because Golang does not support modifying the CSR object fields directly.
newCsrTemplate := x509.CertificateRequest{
SignatureAlgorithm: s.ca.SignatureAlgorithm,
RawSubject: originalCsr.RawSubject,
PublicKey: newCertKey.Public(),
Extensions: originalCsr.Extensions,
}
newCertKeyByte, _ := x509.MarshalPKCS8PrivateKey(newCertKey)

cert, err := s.signCsr(ctx, &newCsrTemplate)
if err != nil {
return nil, nil, err
}

return cert, []byte(base64.StdEncoding.EncodeToString(newCertKeyByte)), nil
}

// loadCsr parses the certifcate signing request based on rules of
// https://www.rfc-editor.org/rfc/rfc7030.html#section-4.2.1
// - content is a base64 encoded certificate signing request
Expand Down Expand Up @@ -251,11 +298,7 @@ func (s Service) signCsr(ctx context.Context, csr *x509.CertificateRequest) ([]b
NotBefore: now,
NotAfter: notAfter,
RawSubject: csr.RawSubject,
Signature: csr.Signature,
SignatureAlgorithm: csr.SignatureAlgorithm,
PublicKeyAlgorithm: csr.PublicKeyAlgorithm,
Issuer: s.ca.Subject,
PublicKey: csr.PublicKey,
BasicConstraintsValid: true,
IsCA: false,
KeyUsage: ku,
Expand Down
33 changes: 31 additions & 2 deletions service_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -71,7 +71,7 @@ func createService(t *testing.T) Service {
cert, err := x509.ParseCertificate(der)
require.Nil(t, err)

return Service{[]*x509.Certificate{cert}, cert, key, time.Hour * 24}
return Service{[]*x509.Certificate{cert}, cert, key, true, time.Hour * 24}
}

func TestService_CA(t *testing.T) {
Expand Down Expand Up @@ -118,7 +118,7 @@ func TestService_signCsr(t *testing.T) {

cn := random.String(12)
s := createService(t)
_, csrBytes := createB64CsrDer(t, cn)
key, csrBytes := createB64CsrDer(t, cn)
csr, err := s.loadCsr(ctx, csrBytes)
require.Nil(t, err)

Expand All @@ -130,6 +130,8 @@ func TestService_signCsr(t *testing.T) {
require.Nil(t, err)
cert := p7.Certificates[0]
require.Equal(t, cn, cert.Subject.CommonName)
certpub := cert.PublicKey.(*ecdsa.PublicKey)
require.True(t, certpub.Equal(key.Public()), "cert have different public key from request CSR")

// X509 Key usage must be: DigitalSignature
csr.Extensions[0].Value = []byte{3, 2, 7, 120}
Expand Down Expand Up @@ -196,3 +198,30 @@ func TestService_ReEnroll(t *testing.T) {
cert := p7.Certificates[0]
require.Equal(t, cn, cert.Subject.CommonName)
}

func TestService_ServerKeygen(t *testing.T) {
log := InitLogger("")
ctx := CtxWithLog(context.TODO(), log)

s := createService(t)
cn := random.String(12)
_, csrBytes := createB64CsrDer(t, cn)
bytes, key, err := s.ServerKeygen(ctx, csrBytes, nil)
require.Nil(t, err)
require.NotNil(t, key)

bytes, err = base64.StdEncoding.DecodeString(string(bytes))
require.Nil(t, err)
p7, err := pkcs7.Parse(bytes)
require.Nil(t, err)
cert := p7.Certificates[0]
require.Equal(t, cn, cert.Subject.CommonName)
key, err = base64.StdEncoding.DecodeString(string(key))
require.Nil(t, err)
_, err = x509.ParsePKCS8PrivateKey(key)
require.Nil(t, err)

s.allowServerKeygen = false
_, _, err = s.ServerKeygen(ctx, csrBytes, nil)
require.NotNil(t, err)
}