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

Embed SCTs in issued certificates #507

Merged
merged 4 commits into from
Apr 13, 2022
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.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
26 changes: 22 additions & 4 deletions cmd/app/serve.go
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,8 @@ import (
"strings"
"time"

ctclient "github.com/google/certificate-transparency-go/client"
"github.com/google/certificate-transparency-go/jsonclient"
"github.com/pkg/errors"
"github.com/prometheus/client_golang/prometheus/promhttp"
"github.com/sigstore/fulcio/pkg/api"
Expand All @@ -34,10 +36,10 @@ import (
"github.com/sigstore/fulcio/pkg/ca/kmsca"
"github.com/sigstore/fulcio/pkg/ca/x509ca"
"github.com/sigstore/fulcio/pkg/config"
"github.com/sigstore/fulcio/pkg/ctl"
"github.com/sigstore/fulcio/pkg/log"
"github.com/spf13/cobra"
"github.com/spf13/viper"
"go.uber.org/zap"
)

const serveCmdEnvPrefix = "FULCIO_SERVE"
Expand Down Expand Up @@ -73,6 +75,15 @@ func newServeCmd() *cobra.Command {
return cmd
}

// Adaptor for logging with the CT log
type logAdaptor struct {
logger *zap.SugaredLogger
}

func (la logAdaptor) Printf(s string, args ...interface{}) {
la.logger.Infof(s, args...)
}

func runServeCmd(cmd *cobra.Command, args []string) {
// If a config file is provided, modify the viper config to locate and read it
if err := checkServeCmdConfigFile(); err != nil {
Expand Down Expand Up @@ -181,10 +192,17 @@ func runServeCmd(cmd *cobra.Command, args []string) {
host, port := viper.GetString("host"), viper.GetString("port")
log.Logger.Infof("%s:%s", host, port)

var ctClient ctl.Client
var ctClient *ctclient.LogClient
if logURL := viper.GetString("ct-log-url"); logURL != "" {
ctClient = ctl.New(logURL)
ctClient = ctl.WithLogging(ctClient, log.Logger)
ctClient, err = ctclient.New(logURL,
&http.Client{Timeout: 30 * time.Second},
jsonclient.Options{
Logger: logAdaptor{logger: log.Logger},
// TODO: Add public key from CT Log for verification.
bobcallaway marked this conversation as resolved.
Show resolved Hide resolved
})
if err != nil {
log.Logger.Fatal(err)
}
}

var handler http.Handler
Expand Down
6 changes: 3 additions & 3 deletions config/logid.sh
Original file line number Diff line number Diff line change
Expand Up @@ -16,11 +16,11 @@


function get_log_id() {
curl -s --retry-connrefused --retry 10 http://trillian-log-server:8090/metrics |grep "^quota_acquired_tokens{spec=\"trees"|head -1|awk ' { print $1 } '|sed -e 's/[^0-9]*//g' > /tmp/logid
curl -s --retry-connrefused --retry 10 http://trillian-log-server:8095/metrics |grep "^quota_acquired_tokens{spec=\"trees"|head -1|awk ' { print $1 } '|sed -e 's/[^0-9]*//g' > /tmp/logid
}

function create_log () {
/go/bin/createtree -admin_server trillian-log-server:8091 > /tmp/logid
/go/bin/createtree -admin_server trillian-log-server:8096 > /tmp/logid
echo -n "Created log ID " && cat /tmp/logid
}

Expand Down Expand Up @@ -48,5 +48,5 @@ if ! [[ -s /etc/config/ct_server.cfg ]]; then
else
echo " found."
configid=`cat /etc/config/ct_server.cfg|grep log_id|awk ' { print $2 } '`
echo "Exisiting configuration uses log ID $configid, exiting"
echo "Existing configuration uses log ID $configid, exiting"
fi
2 changes: 1 addition & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ require (
github.com/coreos/go-oidc/v3 v3.1.0
github.com/fsnotify/fsnotify v1.5.1
github.com/go-chi/chi v4.1.2+incompatible
github.com/google/certificate-transparency-go v1.1.2
github.com/google/go-cmp v0.5.7
github.com/hashicorp/golang-lru v0.5.4
github.com/magiconair/properties v1.8.6
Expand All @@ -19,7 +20,6 @@ require (
github.com/sigstore/sigstore v1.2.1-0.20220401110139-0e610e39782f
github.com/spf13/cobra v1.4.0
github.com/spf13/viper v1.10.1
github.com/stretchr/testify v1.7.1
go.step.sm/crypto v0.16.1
go.uber.org/multierr v1.7.0 // indirect
go.uber.org/zap v1.21.0
Expand Down
485 changes: 482 additions & 3 deletions go.sum

Large diffs are not rendered by default.

59 changes: 18 additions & 41 deletions pkg/api/api_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -25,12 +25,10 @@ import (
"crypto/x509"
"crypto/x509/pkix"
"encoding/asn1"
"encoding/base64"
"encoding/json"
"encoding/pem"
"errors"
"fmt"
"io"
"net/http"
"net/http/httptest"
"net/url"
Expand All @@ -39,18 +37,17 @@ import (
"testing"
"time"

ctclient "github.com/google/certificate-transparency-go/client"
"github.com/google/certificate-transparency-go/jsonclient"
"github.com/sigstore/fulcio/pkg/ca"
"github.com/sigstore/fulcio/pkg/ca/ephemeralca"
"github.com/sigstore/fulcio/pkg/challenges"
"github.com/sigstore/fulcio/pkg/config"
"github.com/sigstore/fulcio/pkg/ctl"
"gopkg.in/square/go-jose.v2"
"gopkg.in/square/go-jose.v2/jwt"
)

// base64 encoded placeholder for SCT
const (
testSCT = "ZXhhbXBsZXNjdAo="
expectedNoRootMessage = "{\"code\":500,\"message\":\"error communicating with CA backend\"}\n"
)

Expand Down Expand Up @@ -626,44 +623,17 @@ func newOIDCIssuer(t *testing.T) (jose.Signer, string) {
return signer, *testIssuer
}

// This is private in pkg/ctl, so making a copy here.
type certChain struct {
Chain []string `json:"chain"`
}

func fakeCTLogServer(t *testing.T) *httptest.Server {
return httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
defer r.Body.Close()
body, err := io.ReadAll(r.Body)
if err != nil {
t.Fatalf("No body")
}
var chain certChain
json.Unmarshal(body, &chain)
if len(chain.Chain) != 2 {
t.Fatalf("did not get expected chain for input, wanted 2 entries, got %d", len(chain.Chain))
}
// Just make sure we can decode it.
for _, chainEntry := range chain.Chain {
_, err := base64.StdEncoding.DecodeString(chainEntry)
if err != nil {
t.Fatalf("failed to decode incoming chain entry: %v", err)
}
}

// Create a fake response.
resp := &ctl.CertChainResponse{
SctVersion: 1,
ID: "testid",
Timestamp: time.Now().Unix(),
}
responseBytes, err := json.Marshal(&resp)
if err != nil {
t.Fatalf("failed to marshal response: %v", err)
}
w.WriteHeader(http.StatusOK)
w.Header().Set("SCT", testSCT)
fmt.Fprint(w, string(responseBytes))
addJSONResp := `{
"sct_version":0,
"id":"KHYaGJAn++880NYaAY12sFBXKcenQRvMvfYE9F1CYVM=",
"timestamp":1337,
"extensions":"",
"signature":"BAMARjBEAiAIc21J5ZbdKZHw5wLxCP+MhBEsV5+nfvGyakOIv6FOvAIgWYMZb6Pw///uiNM7QTg2Of1OqmK1GbeGuEl9VJN8v8c="
}`
fmt.Fprint(w, string(addJSONResp))
}))
}

Expand All @@ -681,7 +651,13 @@ func createCA(cfg *config.FulcioConfig, t *testing.T) (*ephemeralca.EphemeralCA,
}

// Create a test HTTP server to host our API.
h := New(ctl.New(ctlogServer.URL), eca)
ctClient, err := ctclient.New(ctlogServer.URL,
&http.Client{Timeout: 30 * time.Second},
jsonclient.Options{})
if err != nil {
t.Fatalf("error creating CT client: %v", err)
}
h := New(ctClient, eca)
server := httptest.NewServer(http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) {
ctx := r.Context()
// For each request, infuse context with our snapshot of the FulcioConfig.
Expand All @@ -690,6 +666,7 @@ func createCA(cfg *config.FulcioConfig, t *testing.T) (*ephemeralca.EphemeralCA,
h.ServeHTTP(rw, r.WithContext(ctx))
}))
t.Cleanup(server.Close)
t.Cleanup(ctlogServer.Close)

return eca, server.URL
}
Expand Down
47 changes: 39 additions & 8 deletions pkg/api/ca.go
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ import (
"strings"

"github.com/coreos/go-oidc/v3/oidc"
ctclient "github.com/google/certificate-transparency-go/client"
certauth "github.com/sigstore/fulcio/pkg/ca"
"github.com/sigstore/fulcio/pkg/challenges"
"github.com/sigstore/fulcio/pkg/config"
Expand Down Expand Up @@ -56,14 +57,14 @@ const (
)

type api struct {
ct ctl.Client
ct *ctclient.LogClient
ca certauth.CertificateAuthority

*http.ServeMux
}

// New creates a new http.Handler for serving the Fulcio API.
func New(ct ctl.Client, ca certauth.CertificateAuthority) http.Handler {
func New(ct *ctclient.LogClient, ca certauth.CertificateAuthority) http.Handler {
var a api
a.ServeMux = http.NewServeMux()
a.HandleFunc(signingCertPath, a.signingCert)
Expand Down Expand Up @@ -177,8 +178,8 @@ func (a *api) signingCert(w http.ResponseWriter, req *http.Request) {

var csc *certauth.CodeSigningCertificate
var sctBytes []byte
// TODO: prefer embedding SCT if possible
if _, ok := a.ca.(certauth.EmbeddedSCTCA); !ok {
// For CAs that do not support embedded SCTs or if the CT log is not configured
if sctCa, ok := a.ca.(certauth.EmbeddedSCTCA); !ok || a.ct == nil {
// currently configured CA doesn't support pre-certificate flow required to embed SCT in final certificate
csc, err = a.ca.CreateCertificate(ctx, subject)
if err != nil {
Expand All @@ -192,21 +193,49 @@ func (a *api) signingCert(w http.ResponseWriter, req *http.Request) {
return
}

// Submit to CTL
// submit to CTL
if a.ct != nil {
sct, err := a.ct.AddChain(csc)
sct, err := a.ct.AddChain(ctx, ctl.BuildCTChain(csc.FinalCertificate, csc.FinalChain))
if err != nil {
handleFulcioAPIError(w, req, http.StatusInternalServerError, err, failedToEnterCertInCTL)
return
}
sctBytes, err = json.Marshal(sct)
// convert to AddChainResponse because Cosign expects this struct.
addChainResp, err := ctl.ToAddChainResponse(sct)
if err != nil {
handleFulcioAPIError(w, req, http.StatusInternalServerError, err, failedToMarshalSCT)
return
}
sctBytes, err = json.Marshal(addChainResp)
if err != nil {
handleFulcioAPIError(w, req, http.StatusInternalServerError, err, failedToMarshalSCT)
return
}
} else {
logger.Info("Skipping CT log upload.")
}
} else {
precert, err := sctCa.CreatePrecertificate(ctx, subject)
if err != nil {
// if the error was due to invalid input in the request, return HTTP 400
if _, ok := err.(certauth.ValidationError); ok {
handleFulcioAPIError(w, req, http.StatusBadRequest, err, err.Error())
return
}
// otherwise return a 500 error to reflect that it is a transient server issue that the client can't resolve
handleFulcioAPIError(w, req, http.StatusInternalServerError, err, genericCAError)
}
// submit precertificate and chain to CT log
sct, err := a.ct.AddPreChain(ctx, ctl.BuildCTChain(precert.PreCert, precert.CertChain))
if err != nil {
handleFulcioAPIError(w, req, http.StatusInternalServerError, err, failedToEnterCertInCTL)
return
}
csc, err = sctCa.IssueFinalCertificate(ctx, precert, sct)
if err != nil {
handleFulcioAPIError(w, req, http.StatusInternalServerError, err, genericCAError)
return
}
}

metricNewEntries.Inc()
Expand Down Expand Up @@ -235,7 +264,9 @@ func (a *api) signingCert(w http.ResponseWriter, req *http.Request) {
}

// Set the SCT and Content-Type headers, and then respond with a 201 Created.
w.Header().Add("SCT", base64.StdEncoding.EncodeToString(sctBytes))
if len(sctBytes) != 0 {
w.Header().Add("SCT", base64.StdEncoding.EncodeToString(sctBytes))
}
w.Header().Add("Content-Type", "application/pem-certificate-chain")
w.WriteHeader(http.StatusCreated)
// Write the PEM encoded certificate chain to the response body.
Expand Down
18 changes: 13 additions & 5 deletions pkg/ca/interface.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,9 +18,11 @@ package ca
import (
"bytes"
"context"
"crypto"
"crypto/x509"
"strings"

ct "github.com/google/certificate-transparency-go"
"github.com/sigstore/fulcio/pkg/challenges"
"github.com/sigstore/sigstore/pkg/cryptoutils"
)
Expand All @@ -33,9 +35,17 @@ type CodeSigningCertificate struct {
finalChainPEM []byte
}

// CodeSigningPreCertificate holds a precertificate and chain.
type CodeSigningPreCertificate struct {
// Subject contains information about the OIDC identity of the caller.
Subject *challenges.ChallengeResult
// PreCert contains the precertificate. Not a valid certificate due to a critical poison extension.
PreCert *x509.Certificate
// CertChain contains the certificate chain to verify the precertificate.
CertChain []*x509.Certificate
// PrivateKey contains the signing key used to sign the precertificate. Will be used to sign the certificate.
// Included in case the signing key is rotated in between precertificate generation and final issuance.
PrivateKey crypto.Signer
}

func CreateCSCFromPEM(subject *challenges.ChallengeResult, cert string, chain []string) (*CodeSigningCertificate, error) {
Expand Down Expand Up @@ -83,9 +93,6 @@ func CreateCSCFromDER(subject *challenges.ChallengeResult, cert []byte, chain []
buf := bytes.Buffer{}
for _, chainCert := range c.FinalChain {
buf.Write(cryptoutils.PEMEncode(cryptoutils.CertificatePEMType, chainCert.Raw))
if chainCert.Raw[len(chainCert.Raw)-1] != '\n' {
buf.WriteRune('\n')
}
}
c.finalChainPEM = buf.Bytes()
return c, nil
Expand All @@ -107,15 +114,16 @@ func (c *CodeSigningCertificate) ChainPEM() ([]byte, error) {
return c.finalChainPEM, err
}

// CertificateAuthority only returns the SCT in detached format
// CertificateAuthority implements certificate creation with a detached SCT and fetching the CA trust bundle.
type CertificateAuthority interface {
CreateCertificate(ctx context.Context, challenge *challenges.ChallengeResult) (*CodeSigningCertificate, error)
Root(ctx context.Context) ([]byte, error)
}

// EmbeddedSCTCA implements precertificate and certificate issuance. Certificates will contain an embedded SCT.
type EmbeddedSCTCA interface {
CreatePrecertificate(ctx context.Context, challenge *challenges.ChallengeResult) (*CodeSigningPreCertificate, error)
IssueFinalCertificate(ctx context.Context, precert *CodeSigningPreCertificate) (*CodeSigningCertificate, error)
IssueFinalCertificate(ctx context.Context, precert *CodeSigningPreCertificate, sct *ct.SignedCertificateTimestamp) (*CodeSigningCertificate, error)
}

// ValidationError indicates that there is an issue with the content in the HTTP Request that
Expand Down