Skip to content

Commit 92a409a

Browse files
fix!: use Fulcio certificate instead of public key and upgrade Sigstore Cosign from v2 to v3 (#1726)
On-behalf-of: Gerald Morrison (SAP) <[email protected]> <!-- markdownlint-disable MD041 --> This PR introduces two breaking changes: ## Breaking change 1: Correct Sigstore bundle content in signatures OCM now bundles the short‑lived Fulcio certificate instead of just a raw public key into signatures when using keyless signing. To preserve backwards compatibility for existing components, we introduced a new signing algorithm `sigstore-v2` and retain the legacy `sigstore` algorithm: - `sigstore`: Stores only the public key in the Rekor entry. Existing signatures remain verifiable. - `sigstore-v2`: Stores the Fulcio certificate in the Rekor entry, conforming to the Sigstore Bundle spec. Recommended algorithm to be used for new signatures. ### OCM CLI Examples Create a new signature using `sigstore-v2`: `ocm sign componentversion --signature mysig --algorithm sigstore-v2 --keyless <component-version>` Verify a signature (correct algorithm is determined from signature name `mysig`) `ocm verify componentversion --signature mysig --keyless <component-version>` ## Breaking change 2: Cosign v3 changes OIDC token handling in GitHub Actions GitHub no longer auto‑injects an ID token when `SIGSTORE_ID_TOKEN` is missing; workflows must explicitly request and export it. Typical CI error if `SIGSTORE_ID_TOKEN` is missing: `getting ID token: executing OIDC flow: failed to start browser` ### Migration on GitHub Actions Add `id-token: write` permission and explicitly request an OIDC ID token with audience `sigstore`, then export it as `SIGSTORE_ID_TOKEN` prior to any `ocm sign componentversion --keyless` steps. Below you find a snippet from an example workflow step that gets an OIDC token and exports it to the GitHub env for further use. ```yaml - name: Acquire GitHub OIDC ID token shell: bash run: | TOKEN=$(curl -sSf \ -H "Authorization: Bearer $ACTIONS_ID_TOKEN_REQUEST_TOKEN" \ "${ACTIONS_ID_TOKEN_REQUEST_URL}&audience=sigstore" \ | jq -r '.value') echo "::add-mask::$TOKEN" echo "SIGSTORE_ID_TOKEN=$TOKEN" >> "$GITHUB_ENV" ``` ### Other CI platforms are not affected This problem is specific to GitHub Actions. Other CI/CD platforms have always required explicit handling of OIDC tokens and environment variables for Sigstore keyless flows. Therefore, no migration is needed outside GitHub Actions. ## Recommended path forward - Continue to verify existing `sigstore` signatures — they remain valid. - For new signatures, use `sigstore-v2` to ensure Sigstore Bundle compliance. - On GitHub Actions, add explicit OIDC token handling and export `SIGSTORE_ID_TOKEN` as shown above. ## References - Sigstore OIDC Documentation: https://docs.sigstore.dev/cosign/oidc/ - Cosign v3 Release Notes: https://github.com/sigstore/cosign/releases/tag/v3.0.0 --------- Signed-off-by: Gerald Morrison (SAP) <[email protected]> Co-authored-by: Gerald Morrison (SAP) <[email protected]>
1 parent e408963 commit 92a409a

File tree

11 files changed

+441
-60
lines changed

11 files changed

+441
-60
lines changed

.github/config/wordlist.txt

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -308,4 +308,7 @@ yaml
308308
yitsushi
309309
yml
310310
yyyy
311-
jsonNormalisation
311+
jsonNormalisation
312+
rekor
313+
oidc
314+
fulcio

api/tech/signing/handlers/init.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
package handlers
22

33
import (
4-
_ "github.com/sigstore/cosign/v2/pkg/providers/all"
4+
_ "github.com/sigstore/cosign/v3/pkg/providers/all"
55
_ "ocm.software/ocm/api/tech/signing/handlers/rsa"
66
_ "ocm.software/ocm/api/tech/signing/handlers/rsa-pss"
77
_ "ocm.software/ocm/api/tech/signing/handlers/rsa-pss-signingservice"

api/tech/signing/handlers/sigstore/attr/attr.go

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,12 @@ func (a AttributeType) Name() string {
4545
func (a AttributeType) Description() string {
4646
return `
4747
*sigstore config* Configuration to use for sigstore based signing.
48+
49+
Configuration applies to <code>sigstore</code> (legacy) and <code>sigstore-v2</code> signing algorithms.
50+
The algorithms affect how signatures are stored in Rekor:
51+
- <code>sigstore</code>: stores only public key in Rekor entry (non-compliant Sigstore Bundle spec).
52+
- <code>sigstore-v2</code>: stores Fulcio certificate in Rekor entry (compliant Sigstore Bundle spec).
53+
4854
The following fields are used.
4955
- *<code>fulcioURL</code>* *string* default is https://fulcio.sigstore.dev
5056
- *<code>rekorURL</code>* *string* default is https://rekor.sigstore.dev

api/tech/signing/handlers/sigstore/handler.go

Lines changed: 92 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -14,9 +14,9 @@ import (
1414
"github.com/go-openapi/strfmt"
1515
"github.com/go-openapi/swag/conv"
1616
"github.com/mandelsoft/goutils/errors"
17-
"github.com/sigstore/cosign/v2/cmd/cosign/cli/fulcio"
18-
"github.com/sigstore/cosign/v2/cmd/cosign/cli/options"
19-
"github.com/sigstore/cosign/v2/pkg/cosign"
17+
"github.com/sigstore/cosign/v3/cmd/cosign/cli/fulcio"
18+
"github.com/sigstore/cosign/v3/cmd/cosign/cli/options"
19+
"github.com/sigstore/cosign/v3/pkg/cosign"
2020
"github.com/sigstore/rekor/pkg/client"
2121
"github.com/sigstore/rekor/pkg/generated/client/entries"
2222
"github.com/sigstore/rekor/pkg/generated/models"
@@ -30,32 +30,47 @@ import (
3030
"ocm.software/ocm/api/tech/signing/handlers/sigstore/attr"
3131
)
3232

33-
// Algorithm defines the type for the RSA PKCS #1 v1.5 signature algorithm.
34-
const Algorithm = "sigstore"
33+
/*
34+
Algorithm defines the type for the Sigstore signature algorithm:
35+
- "sigstore" uses only the public key in the Rekor entry (legacy).
36+
- "sigstore-v2" uses the Fulcio certificate in the Rekor entry,
37+
as required by the Sigstore Bundle specification.
38+
- "sigstore-v2" is the recommended algorithm to use.
39+
*/
40+
const (
41+
Algorithm = "sigstore"
42+
AlgorithmV2 = "sigstore-v2"
43+
)
3544

3645
// MediaType defines the media type for a plain RSA signature.
3746
const MediaType = "application/vnd.ocm.signature.sigstore"
3847

39-
// SignaturePEMBlockAlgorithmHeader defines the header in a signature pem block where the signature algorithm is defined.
40-
const SignaturePEMBlockAlgorithmHeader = "Algorithm"
41-
4248
func init() {
43-
signing.DefaultHandlerRegistry().RegisterSigner(Algorithm, Handler{})
49+
// Register both algorithms for signature creation.
50+
// "sigstore" for backwards compatibility, "sigstore-v2" for correct sigstore bundle implementation.
51+
signing.DefaultHandlerRegistry().RegisterSigner(Algorithm, Handler{algorithm: Algorithm})
52+
signing.DefaultHandlerRegistry().RegisterSigner(AlgorithmV2, Handler{algorithm: AlgorithmV2})
4453
}
4554

4655
// Handler is a signatures.Signer compatible struct to sign using sigstore
4756
// and a signatures.Verifier compatible struct to verify using sigstore.
48-
type Handler struct{}
57+
// Uses "algorithm" field to distinguish between old "sigstore" and new "sigstore-v2" flows.
58+
type Handler struct {
59+
algorithm string
60+
}
4961

5062
// Algorithm specifies the name of the signing algorithm.
5163
func (h Handler) Algorithm() string {
52-
return Algorithm
64+
return h.algorithm
5365
}
5466

5567
// Sign implements the signing functionality.
68+
// Since "sigstore" algorithm is not compliant with Sigstore bundle spec, we introduce "sigstore-v2".
69+
// We use the algorithm name to decide if old "sigstore" or new "sigstore-v2" flow is used to
70+
// guarantee backwards compatibility.
5671
func (h Handler) Sign(cctx credentials.Context, digest string, sctx signing.SigningContext) (*signing.Signature, error) {
5772
hash := sctx.GetHash()
58-
// exit immediately if hash alg is not SHA-256, rekor doesn't currently support other hash functions
73+
// exit immediately if hash alg is not SHA-256, Rekor doesn't currently support other hash functions
5974
if hash != crypto.SHA256 {
6075
return nil, fmt.Errorf("cannot sign using sigstore. rekor only supports SHA-256 digests: %s provided", hash.String())
6176
}
@@ -113,32 +128,39 @@ func (h Handler) Sign(cctx credentials.Context, digest string, sctx signing.Sign
113128
return nil, fmt.Errorf("failed to verify signed certificate timestamp: %w", err)
114129
}
115130

116-
// get the public key from the signing key pair
117-
pub, err := fs.PublicKey()
118-
if err != nil {
119-
return nil, fmt.Errorf("failed to get public key for signing: %w", err)
120-
}
121-
122-
// marshal the public key bytes
123-
publicKeyBytes, err := x509.MarshalPKIXPublicKey(pub)
124-
if err != nil {
125-
return nil, fmt.Errorf("failed to marshal public key for signing: %w", err)
126-
}
127-
128-
// encode the public key to pem format
129-
publicKey := pem.EncodeToMemory(&pem.Block{
130-
Bytes: publicKeyBytes,
131-
Type: "PUBLIC KEY",
132-
})
133-
134131
// init the rekor client
135132
rekorClient, err := client.GetRekorClient(cfg.RekorURL)
136133
if err != nil {
137134
return nil, fmt.Errorf("failed to create rekor client: %w", err)
138135
}
139136

140-
// create a rekor hashed entry
141-
hashedEntry := prepareRekorEntry(digest, sig, publicKey)
137+
// decide which public material to use for rekor entry
138+
// old "sigstore" flow uses only raw public key
139+
// new "sigstore-v2" flow uses Fulcio certificate
140+
var rekorPublicMaterial []byte
141+
if h.Algorithm() == AlgorithmV2 {
142+
// Since v3 the Fulcio certificate fs.Cert is already in PEM format
143+
rekorPublicMaterial = fs.Cert
144+
} else {
145+
// get the public key from the signing key pair
146+
pub, err := fs.PublicKey()
147+
if err != nil {
148+
return nil, fmt.Errorf("failed to get public key for signing: %w", err)
149+
}
150+
// marshal the public key bytes
151+
publicKeyBytes, err := x509.MarshalPKIXPublicKey(pub)
152+
if err != nil {
153+
return nil, fmt.Errorf("failed to marshal public key for signing: %w", err)
154+
}
155+
// encode the public key to pem format
156+
rekorPublicMaterial = pem.EncodeToMemory(&pem.Block{
157+
Bytes: publicKeyBytes,
158+
Type: "PUBLIC KEY",
159+
})
160+
}
161+
162+
// create a Rekor hashed entry
163+
hashedEntry := prepareRekorEntry(digest, sig, rekorPublicMaterial)
142164

143165
// validate the rekor entry before submission
144166
if _, err := hashedEntry.Canonicalize(ctx); err != nil {
@@ -169,10 +191,11 @@ func (h Handler) Sign(cctx credentials.Context, digest string, sctx signing.Sign
169191
}
170192

171193
// store the rekor response in the signature value
194+
// depending on used algorithm, either "sigstore" or "sigstore-v2"
172195
return &signing.Signature{
173196
Value: base64.StdEncoding.EncodeToString(data),
174197
MediaType: MediaType,
175-
Algorithm: Algorithm,
198+
Algorithm: h.Algorithm(),
176199
Issuer: "",
177200
}, nil
178201
}
@@ -238,18 +261,13 @@ func (h Handler) Verify(digest string, sig *signing.Signature, sctx signing.Sign
238261
return fmt.Errorf("failed to decode rekor public key: %w", err)
239262
}
240263

241-
block, _ := pem.Decode(rekorPublicKeyRaw)
242-
if block == nil {
243-
return fmt.Errorf("failed to decode public key: %w", err)
244-
}
245-
246-
rekorPublicKey, err := x509.ParsePKIXPublicKey(block.Bytes)
264+
rekorPublicKey, err := extractECDSAPublicKey(rekorPublicKeyRaw)
247265
if err != nil {
248-
return fmt.Errorf("failed to parse public key: %w", err)
266+
return err
249267
}
250268

251269
// verify signature
252-
if err := ecdsa.VerifyASN1(rekorPublicKey.(*ecdsa.PublicKey), rawDigest, rekorSignature); !err {
270+
if ok := ecdsa.VerifyASN1(rekorPublicKey, rawDigest, rekorSignature); !ok {
253271
return errors.New("could not verify signature using public key")
254272
}
255273

@@ -270,8 +288,7 @@ func loadVerifier(ctx context.Context) (signature.Verifier, error) {
270288
for _, pubKey := range publicKeys.Keys {
271289
return signature.LoadVerifier(pubKey.PubKey, crypto.SHA256)
272290
}
273-
274-
return nil, nil
291+
return nil, errors.New("no Rekor public key found")
275292
}
276293

277294
// based on: https://github.com/sigstore/cosign/blob/ff648d5fb4ed6d0d1c16eaaceff970411fa969e3/pkg/cosign/tlog.go#L233
@@ -295,3 +312,35 @@ func prepareRekorEntry(digest string, sig, publicKey []byte) hashedrekord_v001.V
295312
},
296313
}
297314
}
315+
316+
// extractECDSAPublicKey extracts an ECDSA public key from PEM encoded bytes.
317+
// Fulcio’s current production setup for keyless flows only presents ECDSA public keys.
318+
func extractECDSAPublicKey(pubKeyBytes []byte) (*ecdsa.PublicKey, error) {
319+
block, _ := pem.Decode(pubKeyBytes)
320+
if block == nil {
321+
return nil, fmt.Errorf("no PEM block found in Fulcio public key")
322+
}
323+
var rawPub any
324+
switch block.Type {
325+
case "PUBLIC KEY":
326+
result, err := x509.ParsePKIXPublicKey(block.Bytes)
327+
if err != nil {
328+
return nil, fmt.Errorf("failed to parse public key: %w", err)
329+
}
330+
rawPub = result
331+
case "CERTIFICATE":
332+
cert, err := x509.ParseCertificate(block.Bytes)
333+
if err != nil {
334+
return nil, fmt.Errorf("failed to parse Fulcio certificate: %w", err)
335+
}
336+
rawPub = cert.PublicKey
337+
default:
338+
return nil, fmt.Errorf("unsupported PEM block type: %s", block.Type)
339+
}
340+
// cast to ecdsa.PublicKey as we use this in the verification
341+
pubKey, ok := rawPub.(*ecdsa.PublicKey)
342+
if !ok {
343+
return nil, fmt.Errorf("unexpected public key type: %T", rawPub)
344+
}
345+
return pubKey, nil
346+
}

0 commit comments

Comments
 (0)