hashicorp/packer · error

verify Fulcio certificate chain: %w

Error message

verify Fulcio certificate chain: %w

What it means

verifyKeylessCertificate wraps sigstore-go's VerifyLeafCertificate, which validates that the Fulcio-issued short-lived certificate in a keyless attestation chains to a trusted Fulcio root within the configured TrustedMaterial. Failure means the leaf certificate's chain, validity window, or issuing root does not match the trusted root (the embedded public Sigstore root by default, or a custom trusted_root.json).

Source

Thrown at internal/attestation/sign_keyless.go:55

var newKeylessBundle = sigstoregosign.Bundle

var newKeylessRekor = func(baseURL string) sigstoregosign.Transparency {
	return sigstoregosign.NewRekor(&sigstoregosign.RekorOptions{BaseURL: baseURL})
}

var loadKeylessTrustedMaterial = func(cfg BackendConfig) (sigstoreroot.TrustedMaterial, error) {
	trustedRootPath := strings.TrimSpace(cfg.TrustedRootPath)
	if trustedRootPath == "" {
		return sigstoreroot.FetchTrustedRoot()
	}

	return sigstoreroot.NewTrustedRootFromPath(trustedRootPath)
}

var verifyKeylessCertificate = func(certificate *x509.Certificate, trustedMaterial sigstoreroot.TrustedMaterial, expectedIdentity, expectedOIDCIssuer, trustedRootPath string) error {
	chains, err := sigstoreverify.VerifyLeafCertificate(time.Now().UTC(), certificate, trustedMaterial)
	if err != nil {
		return fmt.Errorf("verify Fulcio certificate chain: %w", err)
	}

	// When using the public Sigstore root (no custom trusted root configured),
	// require a valid SCT so certificates issued outside a public CT log are rejected.
	if strings.TrimSpace(trustedRootPath) == "" {
		if err := sigstoreverify.VerifySignedCertificateTimestamp(chains, 1, trustedMaterial); err != nil {
			return fmt.Errorf("verify Fulcio certificate SCT: %w", err)
		}
	}

	summary, err := fulciocertificate.SummarizeCertificate(certificate)
	if err != nil {
		return fmt.Errorf("summarize Fulcio certificate: %w", err)
	}

	identity, err := sigstoreverify.NewShortCertificateIdentity(expectedOIDCIssuer, "", expectedIdentity, "")
	if err != nil {
		return fmt.Errorf("build keyless identity policy: %w", err)

View on GitHub (pinned to eb36e3c3e4)

Solutions

  1. Update to a current trusted root: clear trusted_root_path so FetchTrustedRoot() pulls the latest public Sigstore root, or re-download trusted_root.json ('sigstore fetch-trusted-root' / TUF client).
  2. If using a custom trusted_root_path, ensure it contains the Fulcio CA (and intermediates) that issued the signer's certificate.
  3. Check clock skew on the verifying machine (NTP); certificates are short-lived and 'now' must fall inside the validity window or the timestamp/chain checks fail.
  4. Confirm the attestation bundle is intact and was produced against the same Sigstore ecosystem (public Fulcio vs. a private/enterprise instance — roots must match).
  5. Read the wrapped inner error from sigstore-go; it distinguishes expired cert, unknown issuer, and chain-building failures, which point to different fixes.

Example fix

// before: stale custom trusted root missing current Fulcio CA
// trusted_root_path = "old_trusted_root.json"
// after: refresh or omit to use the fetched public root
// trusted_root_path = ""  // fetches current Sigstore trusted root via TUF
Defensive patterns

Strategy: try-catch

Validate before calling

// pre-check: refresh trusted root and confirm clock sanity before verifying
func precheckKeylessEnv(trustedRootPath string) error {
	if strings.TrimSpace(trustedRootPath) == "" {
		if _, err := sigstoreroot.FetchTrustedRoot(); err != nil {
			return fmt.Errorf("cannot fetch current Sigstore trusted root: %w", err)
		}
	} else if _, err := sigstoreroot.NewTrustedRootFromPath(trustedRootPath); err != nil {
		return fmt.Errorf("cannot load trusted root %s: %w", trustedRootPath, err)
	}
	if sk := time.Until(time.Now().UTC().Truncate(time.Hour)); sk > time.Hour || sk < -time.Hour {
		return fmt.Errorf("system clock appears skewed; sync NTP before verifying")
	}
	return nil
}

Type guard

func isChainError(err error) bool {
	return err != nil && strings.Contains(err.Error(), "verify Fulcio certificate chain:")
}

Try / catch

err := keylessVerifier.Verify(ctx, payloadType, payload, sig)
if err != nil {
	if strings.Contains(err.Error(), "verify Fulcio certificate chain") {
		// refresh trusted root once, then surface actionable guidance
		return fmt.Errorf("keyless verification failed: Fulcio cert does not chain to configured trusted root; refresh trusted_root or re-sign the artifact: %w", err)
	}
	return err
}

Prevention

When it happens

Trigger: keylessVerifier.Verify is invoked (via newKeylessVerifier/newKeylessVerifierForEnvelope) and sigstoreverify.VerifyLeafCertificate(time.Now(), cert, trustedMaterial) rejects the certificate: untrusted root, expired/not-yet-valid cert, wrong chain, or the trusted root omits the Fulcio CA that issued the cert.

Common situations: Verifying an old attestation whose Fulcio cert has expired (typically valid ~10 minutes) — chain trust relies on the recorded Rekor entry/trusted root; using a custom trusted_root.json that lacks the CA which issued the certificate; a stale embedded trusted root that predates a Fulcio CA rotation; system clock skew making the cert appear not-yet-valid or expired.

Understand the failure class

Related errors


AI-assisted analysis of hashicorp/packer@eb36e3c3e4 (2026-09-05). Data as JSON: /api/errors/15f5489f262626bb. Report an issue: GitHub.