Skip to content
Merged
Show file tree
Hide file tree
Changes from 12 commits
Commits
Show all changes
19 commits
Select commit Hold shift + click to select a range
a35b479
feat: Upgrade Sigstore from v2.6.1 to v3.0.2
morrison-sap Dec 5, 2025
0d6519f
fix: resolve merge conflicts for sigstore v3 upgrade
morrison-sap Jan 9, 2026
fa31f2f
correct format
morrison-sap Jan 9, 2026
9ae5880
add doc after generate
morrison-sap Jan 9, 2026
6051825
change verify to digest both v2 and v2 signature
morrison-sap Jan 12, 2026
cb3a749
add testing and doc changes
morrison-sap Jan 19, 2026
46c1aab
add comments about offline testing capability
morrison-sap Jan 19, 2026
1ecc235
resolve conflicts
morrison-sap Jan 19, 2026
653645a
docs: regenerate CLI docs and update sources for Sigstore. Register s…
morrison-sap Jan 19, 2026
0e84335
docs: regenerate CLI docs from source after sigstore-v3 handler changes
morrison-sap Jan 19, 2026
4f42f3d
sigstore: register sigstore-v3; update attribute description and test…
morrison-sap Jan 19, 2026
94e8d7b
Merge branch 'main' into feat/upgrade-sigstore-v3
morri-son Jan 20, 2026
9c4e4ef
add some more meaningful comments.
morrison-sap Jan 20, 2026
0ec5e07
add even more meaningful comments.
morrison-sap Jan 20, 2026
b84e26f
Clearly separate actions for public keys for different algorithms.
morrison-sap Jan 20, 2026
31894bb
Merge branch 'main' into feat/upgrade-sigstore-v3
morri-son Jan 20, 2026
1e27f20
correct tests for algorithm and check assertion for defined errors.
morrison-sap Jan 21, 2026
b9622ff
added test for wrong digest/signature and handler registration.
morrison-sap Jan 21, 2026
ec180ec
deduplicate type assertion
morrison-sap Jan 21, 2026
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
5 changes: 4 additions & 1 deletion .github/config/wordlist.txt
Original file line number Diff line number Diff line change
Expand Up @@ -308,4 +308,7 @@ yaml
yitsushi
yml
yyyy
jsonNormalisation
jsonNormalisation
rekor
oidc
fulcio
2 changes: 1 addition & 1 deletion api/tech/signing/handlers/init.go
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
package handlers

import (
_ "github.com/sigstore/cosign/v2/pkg/providers/all"
_ "github.com/sigstore/cosign/v3/pkg/providers/all"
_ "ocm.software/ocm/api/tech/signing/handlers/rsa"
_ "ocm.software/ocm/api/tech/signing/handlers/rsa-pss"
_ "ocm.software/ocm/api/tech/signing/handlers/rsa-pss-signingservice"
Expand Down
6 changes: 6 additions & 0 deletions api/tech/signing/handlers/sigstore/attr/attr.go
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,12 @@ func (a AttributeType) Name() string {
func (a AttributeType) Description() string {
return `
*sigstore config* Configuration to use for sigstore based signing.

Configuration applies to <code>sigstore</code> (legacy) and <code>sigstore-v2</code> signing algorithms.
The difference between the algorithms is transparent to configuration but affects how signatures are stored in Rekor:
- <code>sigstore</code>: stores only the public key in the Rekor entry (non-compliant Sigstore Bundle specification).
- <code>sigstore-v2</code>: stores the Fulcio certificate in the Rekor entry (compliant Sigstore Bundle specification).

The following fields are used.
- *<code>fulcioURL</code>* *string* default is https://fulcio.sigstore.dev
- *<code>rekorURL</code>* *string* default is https://rekor.sigstore.dev
Expand Down
101 changes: 77 additions & 24 deletions api/tech/signing/handlers/sigstore/handler.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,9 +14,9 @@ import (
"github.com/go-openapi/strfmt"
"github.com/go-openapi/swag/conv"
"github.com/mandelsoft/goutils/errors"
"github.com/sigstore/cosign/v2/cmd/cosign/cli/fulcio"
"github.com/sigstore/cosign/v2/cmd/cosign/cli/options"
"github.com/sigstore/cosign/v2/pkg/cosign"
"github.com/sigstore/cosign/v3/cmd/cosign/cli/fulcio"
"github.com/sigstore/cosign/v3/cmd/cosign/cli/options"
"github.com/sigstore/cosign/v3/pkg/cosign"
"github.com/sigstore/rekor/pkg/client"
"github.com/sigstore/rekor/pkg/generated/client/entries"
"github.com/sigstore/rekor/pkg/generated/models"
Expand All @@ -30,32 +30,47 @@ import (
"ocm.software/ocm/api/tech/signing/handlers/sigstore/attr"
)

// Algorithm defines the type for the RSA PKCS #1 v1.5 signature algorithm.
const Algorithm = "sigstore"
/*
Algorithm defines the type for the Sigstore signature algorithm:
- "sigstore" uses only the public key in the Rekor entry (legacy).
- "sigstore-v2" uses the Fulcio certificate in the Rekor entry,
as required by the Sigstore Bundle specification.
- "sigstore-v2" is the recommended algorithm to use.
*/
const (
Algorithm = "sigstore"
AlgorithmV2 = "sigstore-v2"
)

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

// SignaturePEMBlockAlgorithmHeader defines the header in a signature pem block where the signature algorithm is defined.
const SignaturePEMBlockAlgorithmHeader = "Algorithm"

func init() {
signing.DefaultHandlerRegistry().RegisterSigner(Algorithm, Handler{})
// Register both algorithms for signature creation.
// "sigstore" for backwards compatibility, "sigstore-v2" for correct sigstore bundle implementation.
signing.DefaultHandlerRegistry().RegisterSigner(Algorithm, Handler{algorithm: Algorithm})
signing.DefaultHandlerRegistry().RegisterSigner(AlgorithmV2, Handler{algorithm: AlgorithmV2})
}

// Handler is a signatures.Signer compatible struct to sign using sigstore
// and a signatures.Verifier compatible struct to verify using sigstore.
type Handler struct{}
// Uses "algorithm" field to distinguish between old "sigstore" and new "sigstore-v2" flows.
type Handler struct {
algorithm string
}

// Algorithm specifies the name of the signing algorithm.
func (h Handler) Algorithm() string {
return Algorithm
return h.algorithm
}

// Sign implements the signing functionality.
// Since the "sigstore" algorithm has a buggy implementation, we introduce "sigstore-v2".
// We use the algorithm name to decide if old "sigstore" or new "sigstore-v2" flow is used to
// guarantee backwards compatibility.
func (h Handler) Sign(cctx credentials.Context, digest string, sctx signing.SigningContext) (*signing.Signature, error) {
hash := sctx.GetHash()
// exit immediately if hash alg is not SHA-256, rekor doesn't currently support other hash functions
// exit immediately if hash alg is not SHA-256, Rekor doesn't currently support other hash functions
if hash != crypto.SHA256 {
return nil, fmt.Errorf("cannot sign using sigstore. rekor only supports SHA-256 digests: %s provided", hash.String())
}
Expand Down Expand Up @@ -137,8 +152,17 @@ func (h Handler) Sign(cctx credentials.Context, digest string, sctx signing.Sign
return nil, fmt.Errorf("failed to create rekor client: %w", err)
}

// decide which public material to use for rekor entry
// old "sigstore" flow uses only public key
// new "sigstore-v2" flow uses the fulcio certificate
// Since v3 the Fulcio certificate fs.Cert is already in PEM format
rekorPublicMaterial := publicKey
if h.Algorithm() == AlgorithmV2 {
rekorPublicMaterial = fs.Cert
}

// create a rekor hashed entry
hashedEntry := prepareRekorEntry(digest, sig, publicKey)
hashedEntry := prepareRekorEntry(digest, sig, rekorPublicMaterial)

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

// store the rekor response in the signature value
// depending on the used algorithm, either "sigstore" or "sigstore-v2"
return &signing.Signature{
Value: base64.StdEncoding.EncodeToString(data),
MediaType: MediaType,
Algorithm: Algorithm,
Algorithm: h.Algorithm(),
Issuer: "",
}, nil
}
Expand Down Expand Up @@ -238,18 +263,13 @@ func (h Handler) Verify(digest string, sig *signing.Signature, sctx signing.Sign
return fmt.Errorf("failed to decode rekor public key: %w", err)
}

block, _ := pem.Decode(rekorPublicKeyRaw)
if block == nil {
return fmt.Errorf("failed to decode public key: %w", err)
}

rekorPublicKey, err := x509.ParsePKIXPublicKey(block.Bytes)
rekorPublicKey, err := extractECDSAPublicKey(rekorPublicKeyRaw)
if err != nil {
return fmt.Errorf("failed to parse public key: %w", err)
return err
}

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

Expand All @@ -270,8 +290,8 @@ func loadVerifier(ctx context.Context) (signature.Verifier, error) {
for _, pubKey := range publicKeys.Keys {
return signature.LoadVerifier(pubKey.PubKey, crypto.SHA256)
}

return nil, nil
// in rare case no public keys are found
return nil, errors.New("no Rekor public key found")
}

// based on: https://github.com/sigstore/cosign/blob/ff648d5fb4ed6d0d1c16eaaceff970411fa969e3/pkg/cosign/tlog.go#L233
Expand All @@ -295,3 +315,36 @@ func prepareRekorEntry(digest string, sig, publicKey []byte) hashedrekord_v001.V
},
}
}

func extractECDSAPublicKey(pubKeyBytes []byte) (*ecdsa.PublicKey, error) {
block, _ := pem.Decode(pubKeyBytes)
if block == nil {
return nil, fmt.Errorf("no PEM block found in Fulcio public key")
}
switch block.Type {
case "PUBLIC KEY":
result, err := x509.ParsePKIXPublicKey(block.Bytes)
if err != nil {
return nil, fmt.Errorf("failed to parse public key: %w", err)
}
// cast to ecdsa.PublicKey as we use this in the verification
pub, ok := result.(*ecdsa.PublicKey)
if !ok {
return nil, fmt.Errorf("unexpected public key type: %T", result)
}
return pub, nil
case "CERTIFICATE":
cert, err := x509.ParseCertificate(block.Bytes)
if err != nil {
return nil, fmt.Errorf("failed to parse Fulcio certificate: %w", err)
}
// cast to ecdsa.PublicKey as we use this in the verification
pub, ok := cert.PublicKey.(*ecdsa.PublicKey)
if !ok {
return nil, fmt.Errorf("unexpected certificate public key type: %T", cert.PublicKey)
}
return pub, nil
default:
return nil, fmt.Errorf("unsupported PEM block type: %s", block.Type)
}
}
172 changes: 172 additions & 0 deletions api/tech/signing/handlers/sigstore/handler_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,172 @@
package sigstore

import (
"crypto/ecdsa"
"crypto/elliptic"
"crypto/rand"
"crypto/x509"
"crypto/x509/pkix"
"encoding/pem"
"math/big"
"os"
"path/filepath"
"testing"

"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"gopkg.in/yaml.v3"

"ocm.software/ocm/api/tech/signing"
)

// Helper function to load data from component descriptor
func loadTestData(t *testing.T, filename string) []byte {
path := filepath.Join("testdata", filename)
data, err := os.ReadFile(path)
require.NoError(t, err, "failed to load test data: %s", filename)
return data
}

// Helper to extract signature from descriptor YAML
func getSignatureByName(t *testing.T, descriptorYAML []byte, name string) (digest, sigValue string) {
var descriptor map[string]any
err := yaml.Unmarshal(descriptorYAML, &descriptor)
require.NoError(t, err)

sigs, ok := descriptor["signatures"].([]any)
require.True(t, ok)

for _, s := range sigs {
sig := s.(map[string]any)
if sig["name"].(string) == name {
digest = sig["digest"].(map[string]any)["value"].(string)
sigValue = sig["signature"].(map[string]any)["value"].(string)
return
}
}
t.Fatalf("signature %s not found", name)
return
}

// ============================================================================
// Pure Function Tests (No Network, No OIDC flows)
// ============================================================================

// Test extracting public key from PEM format
func TestExtractECDSAPublicKey_FromPEMPublicKey(t *testing.T) {
privKey, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
require.NoError(t, err)

pubKeyBytes, err := x509.MarshalPKIXPublicKey(&privKey.PublicKey)
require.NoError(t, err)

pemData := pem.EncodeToMemory(&pem.Block{
Type: "PUBLIC KEY",
Bytes: pubKeyBytes,
})
require.NotNil(t, pemData)

_, err = extractECDSAPublicKey(pemData)

assert.NoError(t, err, "extractECDSAPublicKey should successfully parse valid PUBLIC KEY PEM block")
}

// Test extracting public key from certificate
func TestExtractECDSAPublicKey_FromPEMCertificate(t *testing.T) {
privKey, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
require.NoError(t, err)

certTemplate := &x509.Certificate{
SerialNumber: big.NewInt(1),
Subject: pkix.Name{
CommonName: "test",
},
}

certDER, err := x509.CreateCertificate(rand.Reader, certTemplate, certTemplate, &privKey.PublicKey, privKey)
require.NoError(t, err)

certPEM := pem.EncodeToMemory(&pem.Block{
Type: "CERTIFICATE",
Bytes: certDER,
})

_, err = extractECDSAPublicKey(certPEM)

assert.NoError(t, err, "extractECDSAPublicKey should successfully extract public key from CERTIFICATE PEM block")
}

// Test error handling for invalid PEM
func TestExtractECDSAPublicKey_InvalidPEM(t *testing.T) {
invalidPEM := []byte("not a valid pem block")

_, err := extractECDSAPublicKey(invalidPEM)

assert.Error(t, err)
}

// Test error handling for malformed certificate
func TestExtractECDSAPublicKey_MalformedCertificate(t *testing.T) {
malformedCert := pem.EncodeToMemory(&pem.Block{
Type: "CERTIFICATE",
Bytes: []byte("invalid certificate data"),
})

_, err := extractECDSAPublicKey(malformedCert)

assert.Error(t, err)
}

// Test error handling for unsupported PEM type
func TestExtractECDSAPublicKey_UnsupportedPEMType(t *testing.T) {
unsupportedPEM := pem.EncodeToMemory(&pem.Block{
Type: "UNSUPPORTED",
Bytes: []byte("some data"),
})

_, err := extractECDSAPublicKey(unsupportedPEM)

assert.Error(t, err)
}

// ============================================================================
// Verify signatures with both Sigstore algorithms (backwards compatibility)
// These tests work OFFLINE because:
// - Rekor public keys are embedded in cosign library
// - All verification data is in the self-contained Sigstore bundle
// ============================================================================

// Test verifying legacy "sigstore" signature (created with old OCM CLI up to version v0.35.x)
// This ensures backwards compatibility - old signatures can be verified with new code
func TestVerify_LegacySignature(t *testing.T) {
descriptorYAML := loadTestData(t, "component-descriptor-signed.yaml")
digest, sigValue := getSignatureByName(t, descriptorYAML, "sigstore-legacy")

handler := Handler{algorithm: Algorithm}

err := handler.Verify(digest, &signing.Signature{
Value: sigValue,
MediaType: MediaType,
Algorithm: Algorithm,
}, nil)

assert.NoError(t, err, "legacy signature verification with new code should succeed")
}

// Test verifying "sigstore-v2" signature (created with new OCM CLI with fix)
// This tests the corrected implementation with Fulcio certificate in Rekor bundle
func TestVerify_V3Signature(t *testing.T) {

descriptorYAML := loadTestData(t, "component-descriptor-signed.yaml")
digest, sigValue := getSignatureByName(t, descriptorYAML, "sigstore-recommended")

handler := Handler{algorithm: AlgorithmV2}

err := handler.Verify(digest, &signing.Signature{
Value: sigValue,
MediaType: MediaType,
Algorithm: AlgorithmV2,
}, nil)

assert.NoError(t, err, "v3 signature verification should succeed")
}
Loading
Loading