From 41a7bd003be9345161bf9bf89dd0f1e6df013b38 Mon Sep 17 00:00:00 2001 From: Colleen Murphy Date: Tue, 2 Jul 2024 10:28:56 -0700 Subject: [PATCH] Add v1 Fulcio endpoint to prober There is an SLO set up for the /api/v1/signingCert Fulcio endpoint[1], but it is currently reporting "No SLO status data" because the prober was never testing that endpoint. This lead to an outage that went undetected by the monitoring system. Cosign uses the legacy certificate request endpoint in its Fulcio client[2][3]. This means that the v1 endpoint is likely the most used and therefore an important health indicator. This change adds the v1 endpoint to the prober test, which should populate Prometheus with data which should activate the SLO. [1] https://github.com/sigstore/scaffolding/blob/8f7aa097e54eabcecbc671818f9eb5f0e723e54b/terraform/gcp/modules/monitoring/fulcio/slo.tf#L79-L83 [2] https://github.com/sigstore/cosign/blob/79db196e2d97e7dfc4d8201ef829d4ce906605a7/cmd/cosign/cli/fulcio/fulcio.go#L32 [3] https://github.com/sigstore/fulcio/blob/07b19da442b418ebcf072ac65a7abb25f0e3d5c8/pkg/api/client.go#L60 Signed-off-by: Colleen Murphy --- cmd/prober/prober.go | 7 ++- cmd/prober/write.go | 114 +++++++++++++++++++++++++++++++++++++++++-- 2 files changed, 116 insertions(+), 5 deletions(-) diff --git a/cmd/prober/prober.go b/cmd/prober/prober.go index 4a23e7c42..10522a48e 100644 --- a/cmd/prober/prober.go +++ b/cmd/prober/prober.go @@ -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 diff --git a/cmd/prober/write.go b/cmd/prober/write.go index 0cabaae63..5acadc731 100644 --- a/cmd/prober/write.go +++ b/cmd/prober/write.go @@ -24,6 +24,7 @@ import ( "crypto/x509" "encoding/hex" "encoding/json" + "encoding/pem" "fmt" "io" "net/http" @@ -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) { @@ -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) + 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") @@ -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"` } @@ -314,3 +416,7 @@ type PublicKeyRequest struct { type PublicKey struct { Content string `json:"content"` } + +type PublicKeyLegacy struct { + Content []byte `json:"content"` +}