Skip to content

Commit

Permalink
* Add RootCert method to client + tests
Browse files Browse the repository at this point in the history
* Add tests for exercising STL
* Fix sigstore#289
* Validates that the new STL tests failed unless backporting sigstore#288

Signed-off-by: Ville Aikas <vaikas@chainguard.dev>
  • Loading branch information
vaikas committed Dec 21, 2021
1 parent 18b4650 commit 4baf9bf
Show file tree
Hide file tree
Showing 3 changed files with 116 additions and 5 deletions.
88 changes: 84 additions & 4 deletions pkg/api/api_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,8 +22,11 @@ import (
"crypto/rsa"
"crypto/sha256"
"crypto/x509"
"encoding/base64"
"encoding/json"
"encoding/pem"
"fmt"
"io"
"net/http"
"net/http/httptest"
"net/url"
Expand All @@ -33,10 +36,14 @@ import (

"github.com/sigstore/fulcio/pkg/ca/ephemeralca"
"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="

func TestAPI(t *testing.T) {
signer, issuer := newOIDCIssuer(t)

Expand Down Expand Up @@ -74,6 +81,11 @@ func TestAPI(t *testing.T) {
t.Fatalf("ephemeralca.NewEphemeralCA() = %v", err)
}

ctlogServer := fakeCTLogServer(t)
if ctlogServer == nil {
t.Fatalf("Failed to create the fake ctlog server")
}
ctlogURL := ctlogServer.URL
// Create a test HTTP server to host our API.
h := NewHandler()
server := httptest.NewServer(http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) {
Expand All @@ -83,6 +95,7 @@ func TestAPI(t *testing.T) {

// Decorate the context with our CA for testing.
ctx = WithCA(ctx, eca)
ctx = WithCTLogURL(ctx, ctlogURL)

h.ServeHTTP(rw, r.WithContext(ctx))
}))
Expand Down Expand Up @@ -122,12 +135,38 @@ func TestAPI(t *testing.T) {
t.Fatalf("SigningCert() = %v", err)
}

// We shouldn't have an SCT because we didn't decorate context
// with a ct-log-url
if string(resp.SCT) != "" {
t.Errorf("Unexpected SCT: %s", resp.SCT)
if string(resp.SCT) == "" {
t.Error("Did not get SCT")
}

// Check that we get the CA root back as well.
root, err := client.RootCert()
if err != nil {
t.Fatal("Failed to get Root", err)
}
if root == nil {
t.Fatal("Got nil root back")
}
if len(root.ChainPEM) == 0 {
t.Fatal("Got back empty chain")
}
block, rest := pem.Decode(root.ChainPEM)
if block == nil {
t.Fatal("Did not find PEM data")
}
if len(rest) != 0 {
t.Fatal("Got more than bargained for, should only have one cert")
}
if block.Type != "CERTIFICATE" {
t.Fatalf("Unexpected root type, expected CERTIFICATE, got %s", block.Type)
}
rootCert, err := x509.ParseCertificate(block.Bytes)
if err != nil {
t.Fatalf("Failed to parse the received root cert: %v", err)
}
if !rootCert.Equal(eca.RootCA) {
t.Errorf("Root CA does not match, wanted %+v got %+v", eca.RootCA, rootCert)
}
// TODO(mattmoor): What interesting checks can we perform on
// the other return values?
}
Expand Down Expand Up @@ -188,3 +227,44 @@ 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) != 1 {
t.Fatalf("Did not get expected chain for input, wanted 1 entry, 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))
}))
}
2 changes: 1 addition & 1 deletion pkg/api/ca.go
Original file line number Diff line number Diff line change
Expand Up @@ -207,7 +207,7 @@ func 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", string(sctBytes))
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
31 changes: 31 additions & 0 deletions pkg/api/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,10 @@ type CertificateResponse struct {
SCT []byte
}

type RootResponse struct {
ChainPEM []byte
}

// SigstorePublicServerURL is the URL of Sigstore's public Fulcio service.
const SigstorePublicServerURL = "https://fulcio.sigstore.dev"

Expand All @@ -42,6 +46,8 @@ type Client interface {
// SigningCert sends the provided CertificateRequest to the /api/v1/signingCert
// endpoint of a Fulcio API, authenticated with the provided bearer token.
SigningCert(cr CertificateRequest, token string) (*CertificateResponse, error)
// RootCert sends a request to get the current CA used by Fulcio.
RootCert() (*RootResponse, error)
}

// ClientOption is a functional option for customizing static signatures.
Expand Down Expand Up @@ -112,6 +118,9 @@ func (c *client) SigningCert(cr CertificateRequest, token string) (*CertificateR

// Split the cert and the chain
certBlock, chainPem := pem.Decode(body)
if certBlock == nil {
return nil, errors.New("did not find a cert from Fulcio")
}
certPem := pem.EncodeToMemory(certBlock)
return &CertificateResponse{
CertPEM: certPem,
Expand All @@ -120,6 +129,28 @@ func (c *client) SigningCert(cr CertificateRequest, token string) (*CertificateR
}, nil
}

func (c *client) RootCert() (*RootResponse, error) {
// Construct the API endpoint for this handler
endpoint := *c.baseURL
endpoint.Path = path.Join(endpoint.Path, rootCertPath)

resp, err := http.Get(endpoint.String())
if err != nil {
return nil, err
}
defer resp.Body.Close()
body, err := io.ReadAll(resp.Body)

if err != nil {
return nil, err
}

if resp.StatusCode != http.StatusOK {
return nil, errors.New(string(body))
}
return &RootResponse{ChainPEM: body}, nil
}

type clientOptions struct {
UserAgent string
Timeout time.Duration
Expand Down

0 comments on commit 4baf9bf

Please sign in to comment.