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

Add v1 Fulcio endpoint to prober #1160

Merged
merged 1 commit into from
Jul 2, 2024
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
7 changes: 6 additions & 1 deletion cmd/prober/prober.go
Original file line number Diff line number Diff line change
Expand Up @@ -238,7 +238,12 @@ func runProbers(ctx context.Context, freq int, runOnce bool, fulcioGrpcClient fu
cert, err := fulcioWriteEndpoint(ctx, priv)
if err != nil {
hasErr = true
Logger.Errorf("error running fulcio write prober: %v", err)
Logger.Errorf("error running fulcio v2 write prober: %v", err)
}
_, err = fulcioWriteLegacyEndpoint(ctx, priv)
if err != nil {
hasErr = true
Logger.Errorf("error running fulcio v1 write prober: %v", err)
}
if err := rekorWriteEndpoint(ctx, cert, priv); err != nil {
hasErr = true
Expand Down
114 changes: 110 additions & 4 deletions cmd/prober/write.go
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ import (
"crypto/x509"
"encoding/hex"
"encoding/json"
"encoding/pem"
"fmt"
"io"
"net/http"
Expand Down Expand Up @@ -52,8 +53,9 @@ const (
defaultOIDCIssuer = "https://oauth2.sigstore.dev/auth"
defaultOIDCClientID = "sigstore"

fulcioEndpoint = "/api/v2/signingCert"
rekorEndpoint = "/api/v1/log/entries"
fulcioEndpoint = "/api/v2/signingCert"
fulcioLegacyEndpoint = "/api/v1/signingCert"
rekorEndpoint = "/api/v1/log/entries"
)

func setHeaders(req *retryablehttp.Request, token string) {
Expand All @@ -68,8 +70,75 @@ func setHeaders(req *retryablehttp.Request, token string) {
req.Header.Set("X-Cloud-Trace-Context", uuid.Must(uuid.NewV7()).String())
}

// fulcioWriteEndpoint tests the only write endpoint for Fulcio
// which is "/api/v2/signingCert", which requests a cert from Fulcio
// fulcioWriteLegacyEndpoint tests the /api/v1/signingCert write endpoint for Fulcio.
func fulcioWriteLegacyEndpoint(ctx context.Context, priv *ecdsa.PrivateKey) (*x509.Certificate, error) {
if !all.Enabled(ctx) {
return nil, fmt.Errorf("no auth provider for fulcio is enabled")
}
tok, err := providers.Provide(ctx, "sigstore")
if err != nil {
return nil, fmt.Errorf("getting provider: %w", err)
}
b, err := legacyCertificateRequest(ctx, tok, priv)
if err != nil {
return nil, fmt.Errorf("certificate response: %w", err)
}

// Construct the API endpoint for this handler
endpoint := fulcioLegacyEndpoint
hostPath := fulcioURL + endpoint

req, err := retryablehttp.NewRequest(http.MethodPost, hostPath, bytes.NewBuffer(b))
if err != nil {
return nil, fmt.Errorf("new request: %w", err)
}

setHeaders(req, tok)

t := time.Now()
resp, err := retryableClient.Do(req)
latency := time.Since(t).Milliseconds()
if err != nil {
Logger.Errorf("error requesting cert: %v", err)
return nil, err
}
defer resp.Body.Close()

if resp.StatusCode != http.StatusCreated {
body, _ := io.ReadAll(resp.Body)
return nil, fmt.Errorf("invalid status code '%s' when requesting a cert from Fulcio with body '%s'", resp.Status, string(body))
}

responseBody, err := io.ReadAll(resp.Body)
if err != nil {
Logger.Errorf("error reading response from Fulcio: %v", err)
return nil, err
}
certBlock, chainPEM := pem.Decode(responseBody)
if certBlock == nil || chainPEM == nil {
Logger.Errorf("did not find expected certificates")
}
intermediateBlock, rootPEM := pem.Decode(chainPEM)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just so this can support probing custom instances with any number of intermediates, we might want to continually decode until pem.Decode() parses the root and rest is empty, or use UnmarshalCertificatesFromPEM(chainPEM). We could add a configuration flag for how many intermediates are expected?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I wrote it this way to be consistent with the v2 test which expects exactly 3:

if len(fulcioResp.CertificatesWithSct.CertificateChain.Certificates) != 3 {
Logger.Errorf("unexpected number of certificates, got %d, expected 3",
len(fulcioResp.CertificatesWithSct.CertificateChain.Certificates))
return nil, err
}

I think making it configurable would be a separate scope.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah fair point, then this LGTM!

if intermediateBlock == nil || rootPEM == nil {
Logger.Errorf("did not find expected certificate chain in response from Fulcio")
}
certPEM := pem.EncodeToMemory(certBlock)
cert, err := cryptoutils.UnmarshalCertificatesFromPEM(certPEM)
if err != nil {
Logger.Errorf("error unmarshalling leaf certificate from Fulcio: %v", err)
return nil, err
}
if len(cert) != 1 {
Logger.Errorf("unexpected number of certificates after unmarshalling got %d, expected 1", len(cert))
return nil, err
}

// Export data to prometheus
exportDataToPrometheus(resp, fulcioURL, endpoint, POST, latency)
return cert[0], nil
}

// fulcioWriteEndpoint tests the /api/v2/signingCert write endpoint for Fulcio.
func fulcioWriteEndpoint(ctx context.Context, priv *ecdsa.PrivateKey) (*x509.Certificate, error) {
if !all.Enabled(ctx) {
return nil, fmt.Errorf("no auth provider for fulcio is enabled")
Expand Down Expand Up @@ -290,10 +359,43 @@ func certificateRequest(_ context.Context, idToken string, priv *ecdsa.PrivateKe
return json.Marshal(req)
}

func legacyCertificateRequest(_ context.Context, idToken string, priv *ecdsa.PrivateKey) ([]byte, error) {
pubBytesPEM, err := cryptoutils.MarshalPublicKeyToPEM(priv.Public())
if err != nil {
return nil, err
}

tok, err := oauthflow.OIDConnect(defaultOIDCIssuer, defaultOIDCClientID, "", "", &oauthflow.StaticTokenGetter{RawToken: idToken})
if err != nil {
return nil, err
}

// Sign the email address as part of the request
h := sha256.Sum256([]byte(tok.Subject))
proof, err := ecdsa.SignASN1(rand.Reader, priv, h[:])
if err != nil {
return nil, err
}

req := SigningCertificateRequestLegacy{
PublicKey: PublicKeyLegacy{
Content: pubBytesPEM,
},
SignedEmailAddress: proof,
}

return json.Marshal(req)
}

type SigningCertificateRequest struct {
PublicKeyRequest PublicKeyRequest `json:"publicKeyRequest"`
}

type SigningCertificateRequestLegacy struct {
PublicKey PublicKeyLegacy `json:"publicKey"`
SignedEmailAddress []byte `json:"signedEmailAddress"`
}

type SigningCertificateResponse struct {
CertificatesWithSct SignedCertificateEmbeddedSct `json:"signedCertificateEmbeddedSct"`
}
Expand All @@ -314,3 +416,7 @@ type PublicKeyRequest struct {
type PublicKey struct {
Content string `json:"content"`
}

type PublicKeyLegacy struct {
Content []byte `json:"content"`
}
Loading