Skip to content

Commit 05046b5

Browse files
committed
review comments
Signed-off-by: Matthias Bertschy <matthias.bertschy@gmail.com>
1 parent 9887527 commit 05046b5

File tree

6 files changed

+124
-100
lines changed

6 files changed

+124
-100
lines changed

pkg/objectcache/applicationprofilecache/applicationprofilecache.go

Lines changed: 8 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -247,7 +247,7 @@ func (apc *ApplicationProfileCacheImpl) updateAllProfiles(ctx context.Context) {
247247
}
248248

249249
// Verify signature if enabled
250-
if err := apc.verifyApplicationProfile(fullProfile, workloadID, "profile"); err != nil {
250+
if err := apc.verifyApplicationProfile(fullProfile, workloadID, "profile", true); err != nil {
251251
// Continue to next profile as per requirements: skip on verification failure
252252
continue
253253
}
@@ -276,7 +276,7 @@ func (apc *ApplicationProfileCacheImpl) updateAllProfiles(ctx context.Context) {
276276
// verifyApplicationProfile verifies the profile signature if verification is enabled.
277277
// Returns error if verification fails, nil otherwise (including when verification is disabled).
278278
// Also updates profileState with error details if verification fails.
279-
func (apc *ApplicationProfileCacheImpl) verifyApplicationProfile(profile *v1beta1.ApplicationProfile, workloadID, context string) error {
279+
func (apc *ApplicationProfileCacheImpl) verifyApplicationProfile(profile *v1beta1.ApplicationProfile, workloadID, context string, recordFailure bool) error {
280280
if !apc.cfg.EnableSignatureVerification {
281281
return nil
282282
}
@@ -298,7 +298,9 @@ func (apc *ApplicationProfileCacheImpl) verifyApplicationProfile(profile *v1beta
298298
}
299299

300300
// Update profile state with verification error
301-
apc.setVerificationFailed(workloadID, profile.Name, err)
301+
if recordFailure {
302+
apc.setVerificationFailed(workloadID, profile.Name, err)
303+
}
302304

303305
return err
304306
}
@@ -368,7 +370,7 @@ func (apc *ApplicationProfileCacheImpl) handleUserManagedProfile(profile *v1beta
368370
}
369371

370372
// Verify signature if enabled
371-
if err := apc.verifyApplicationProfile(fullUserProfile, toMerge.wlid, "user-managed profile"); err != nil {
373+
if err := apc.verifyApplicationProfile(fullUserProfile, toMerge.wlid, "user-managed profile", false); err != nil {
372374
return
373375
}
374376

@@ -593,18 +595,13 @@ func (apc *ApplicationProfileCacheImpl) addContainer(container *containercollect
593595
}
594596

595597
// Verify signature if enabled
596-
if err := apc.verifyApplicationProfile(fullProfile, workloadID, "user-defined profile"); err != nil {
597-
logger.L().Warning("user-defined profile signature verification failed, continuing without signature",
598-
helpers.String("containerID", containerID),
599-
helpers.String("workloadID", workloadID),
600-
helpers.String("namespace", container.K8s.Namespace),
601-
helpers.String("profileName", userDefinedProfile),
602-
helpers.Error(err))
598+
if err := apc.verifyApplicationProfile(fullProfile, workloadID, "user-defined profile", false); err != nil {
603599
// Update the profile state to indicate an error
604600
profileState := &objectcache.ProfileState{
605601
Error: fmt.Errorf("signature verification failed: %w", err),
606602
}
607603
apc.workloadIDToProfileState.Set(workloadID, profileState)
604+
return nil
608605
}
609606

610607
// Update the profile in the cache

pkg/objectcache/networkneighborhoodcache/networkneighborhoodcache.go

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -342,6 +342,8 @@ func (nnc *NetworkNeighborhoodCacheImpl) handleUserManagedNetworkNeighborhood(nn
342342
Error: fmt.Errorf("signature verification failed: %w", err),
343343
}
344344
nnc.workloadIDToProfileState.Set(toMerge.wlid, profileState)
345+
// Evict stale merged profile from cache on verification failure
346+
nnc.workloadIDToNetworkNeighborhood.Delete(toMerge.wlid)
345347
return
346348
}
347349
}
@@ -362,13 +364,23 @@ func (nnc *NetworkNeighborhoodCacheImpl) handleUserManagedNetworkNeighborhood(nn
362364
Error: fmt.Errorf("signature verification failed: %w", err),
363365
}
364366
nnc.workloadIDToProfileState.Set(toMerge.wlid, profileState)
367+
// Restore cache to originalNN on user-managed verification failure
368+
nnc.workloadIDToNetworkNeighborhood.Set(toMerge.wlid, originalNN)
365369
return
366370
}
367371
}
368372

369373
// Merge the network neighborhoods
370374
mergedNN := nnc.performMerge(originalNN, fullUserNN)
371375

376+
// Clear stale signature annotations after merge
377+
delete(mergedNN.Annotations, signature.AnnotationSignature)
378+
delete(mergedNN.Annotations, signature.AnnotationCertificate)
379+
delete(mergedNN.Annotations, signature.AnnotationRekorBundle)
380+
delete(mergedNN.Annotations, signature.AnnotationIssuer)
381+
delete(mergedNN.Annotations, signature.AnnotationIdentity)
382+
delete(mergedNN.Annotations, signature.AnnotationTimestamp)
383+
372384
// Update the cache with the merged network neighborhood
373385
nnc.workloadIDToNetworkNeighborhood.Set(toMerge.wlid, mergedNN)
374386
// Update profile state for the merged profile

pkg/signature/cluster_flow_test.go

Lines changed: 11 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,6 @@ import (
88
"crypto/rand"
99
"crypto/x509"
1010
"crypto/x509/pkix"
11-
"encoding/base64"
1211
"encoding/pem"
1312
"math/big"
1413
"testing"
@@ -101,48 +100,22 @@ func TestReproduceClusterVerificationFlow(t *testing.T) {
101100
t.Fatalf("Failed to generate test certificate: %v", err)
102101
}
103102

104-
// Add signature annotations
105-
adapter.SetAnnotations(map[string]string{
106-
"signature.kubescape.io/signature": base64.StdEncoding.EncodeToString(sig),
107-
"signature.kubescape.io/certificate": base64.StdEncoding.EncodeToString(certBytes),
108-
})
109-
110-
// Now verify using the higher-level flow via CosignVerifier
111-
// Create verifier and decode signature from annotations
112-
sigObj, err := cosignAdapter.DecodeSignatureFromAnnotations(adapter.GetAnnotations())
113-
if err != nil {
114-
t.Fatalf("Failed to decode signature from annotations: %v", err)
115-
}
116-
117-
// Parse certificate from decoded signature
118-
block, _ := pem.Decode(sigObj.Certificate)
119-
if block == nil {
120-
t.Fatalf("Failed to decode PEM from certificate data: certificate data is %d bytes", len(sigObj.Certificate))
121-
}
122-
if block.Type != "CERTIFICATE" {
123-
t.Fatalf("Wrong PEM block type: got %q, want CERTIFICATE", block.Type)
103+
// Use the package-level annotation flow
104+
sigObj := &Signature{
105+
Signature: sig,
106+
Certificate: certBytes,
107+
Timestamp: time.Now().Unix(),
124108
}
125-
126-
cert, err := x509.ParseCertificate(block.Bytes)
127-
if err != nil {
128-
t.Fatalf("Failed to parse certificate: %v", err)
129-
}
130-
131-
// Verify using the certificate's public key (exercise the real verification path)
132-
verifier, err := sigstore_signature.LoadVerifier(cert.PublicKey, crypto.SHA256)
109+
annotations, err := cosignAdapter.EncodeSignatureToAnnotations(sigObj)
133110
if err != nil {
134-
t.Fatalf("Failed to load verifier from certificate: %v", err)
111+
t.Fatalf("Failed to encode signature to annotations: %v", err)
135112
}
113+
adapter.SetAnnotations(annotations)
136114

137-
// Verify signature against hash string
138-
err = verifier.VerifySignature(bytes.NewReader(sig), bytes.NewReader([]byte(hash)))
115+
// Now verify using the higher-level flow
116+
err = VerifyObjectAllowUntrusted(adapter)
139117
if err != nil {
140-
t.Fatalf("Verification failed: %v", err)
141-
}
142-
143-
// Clean up: verify the signature is correctly stored and retrieved
144-
if sigObj.Signature == nil {
145-
t.Error("Signature was not properly decoded from annotations")
118+
t.Fatalf("VerifyObjectAllowUntrusted failed: %v", err)
146119
}
147120
}
148121

pkg/signature/cosign_adapter.go

Lines changed: 56 additions & 48 deletions
Original file line numberDiff line numberDiff line change
@@ -131,7 +131,9 @@ func (c *CosignAdapter) signKeyless(data []byte) (*Signature, error) {
131131
if err != nil {
132132
return nil, fmt.Errorf("failed to provide OIDC token: %w", err)
133133
}
134-
// In CI, identity/issuer are usually provided by the environment
134+
// In a production environment, we should parse the JWT to extract "sub" and "iss"
135+
// For now, we continue to use the constants as we lack a JWT parser dependency
136+
// or direct access to the claims from the provider.
135137
identity = sigstoreOIDC
136138
issuer = sigstoreIssuer
137139
} else {
@@ -324,6 +326,9 @@ func (c *CosignAdapter) GetPublicKeyPEM() ([]byte, error) {
324326
}
325327

326328
func (c *CosignAdapter) VerifyData(data []byte, sig *Signature, allowUntrusted bool) error {
329+
if sig == nil {
330+
return fmt.Errorf("signature object is nil")
331+
}
327332
var verifier sigstore_signature.Verifier
328333
var err error
329334

@@ -332,68 +337,71 @@ func (c *CosignAdapter) VerifyData(data []byte, sig *Signature, allowUntrusted b
332337
// For now, we continue to support the simplified verification but using sigstore's abstractions.
333338

334339
block, _ := pem.Decode(sig.Certificate)
335-
if block != nil && (block.Type == "CERTIFICATE" || block.Type == "PUBLIC KEY") {
336-
if block.Type == "CERTIFICATE" {
337-
var cert *x509.Certificate
338-
cert, err = x509.ParseCertificate(block.Bytes)
339-
if err != nil {
340-
return fmt.Errorf("failed to parse certificate: %w", err)
340+
if block != nil && block.Type == "CERTIFICATE" {
341+
var cert *x509.Certificate
342+
cert, err = x509.ParseCertificate(block.Bytes)
343+
if err != nil {
344+
return fmt.Errorf("failed to parse certificate: %w", err)
345+
}
346+
347+
if !allowUntrusted {
348+
if cert.IsCA {
349+
return fmt.Errorf("invalid certificate: must not be CA")
341350
}
342351

343-
if !allowUntrusted {
344-
if cert.IsCA {
345-
return fmt.Errorf("invalid certificate: must not be CA")
346-
}
352+
if time.Unix(sig.Timestamp, 0).Before(cert.NotBefore) || time.Unix(sig.Timestamp, 0).After(cert.NotAfter) {
353+
return fmt.Errorf("certificate was not valid at signing time")
354+
}
347355

348-
if time.Now().Before(cert.NotBefore) || time.Now().After(cert.NotAfter) {
349-
return fmt.Errorf("certificate is not valid at this time")
356+
// In a production environment, we would verify the certificate chain here
357+
// against the Fulcio root set and system roots.
358+
// roots, _ := fulcioroots.Get()
359+
// cert.Verify(x509.VerifyOptions{Roots: roots})
360+
361+
// Check identity. Fulcio certs store identity in Subject Alternative Name (SAN)
362+
// but many systems still look at CommonName or use specific extensions.
363+
// Sigstore's verify library is usually used for this, but for now we'll check SANs.
364+
foundIdentity := false
365+
if cert.Subject.CommonName == sig.Identity {
366+
foundIdentity = true
367+
} else {
368+
for _, email := range cert.EmailAddresses {
369+
if email == sig.Identity {
370+
foundIdentity = true
371+
break
372+
}
350373
}
351-
352-
// Check identity. Fulcio certs store identity in Subject Alternative Name (SAN)
353-
// but many systems still look at CommonName or use specific extensions.
354-
// Sigstore's verify library is usually used for this, but for now we'll check SANs.
355-
foundIdentity := false
356-
if cert.Subject.CommonName == sig.Identity {
357-
foundIdentity = true
358-
} else {
359-
for _, email := range cert.EmailAddresses {
360-
if email == sig.Identity {
374+
if !foundIdentity {
375+
for _, uri := range cert.URIs {
376+
if uri.String() == sig.Identity {
361377
foundIdentity = true
362378
break
363379
}
364380
}
365-
if !foundIdentity {
366-
for _, uri := range cert.URIs {
367-
if uri.String() == sig.Identity {
368-
foundIdentity = true
369-
break
370-
}
371-
}
372-
}
373-
}
374-
375-
if sig.Identity != "" && !foundIdentity {
376-
return fmt.Errorf("identity mismatch: certificate does not match signature identity %q (CN: %q, SANs: %v)", sig.Identity, cert.Subject.CommonName, cert.EmailAddresses)
377381
}
378382
}
379-
verifier, err = sigstore_signature.LoadVerifier(cert.PublicKey, crypto.SHA256)
380-
if err != nil {
381-
return fmt.Errorf("failed to load verifier from certificate: %w", err)
382-
}
383-
} else {
384-
// PUBLIC KEY block
385-
pubKey, err := cryptoutils.UnmarshalPEMToPublicKey(sig.Certificate)
386-
if err != nil {
387-
return fmt.Errorf("failed to parse public key: %w", err)
383+
384+
if sig.Identity != "" && !foundIdentity {
385+
return fmt.Errorf("identity mismatch: certificate does not match signature identity %q (CN: %q, SANs: %v)", sig.Identity, cert.Subject.CommonName, cert.EmailAddresses)
388386
}
389-
verifier, err = sigstore_signature.LoadVerifier(pubKey, crypto.SHA256)
390-
if err != nil {
391-
return fmt.Errorf("failed to load verifier from public key: %w", err)
387+
388+
// Validate Rekor/CT evidence if Rekor bundle is present
389+
if len(sig.RekorBundle) > 0 {
390+
// In a full implementation, we would use cosign.VerifyBundle
391+
// for now we acknowledge its presence for strict verification
392+
} else if sig.Issuer != "local" && sig.Issuer != "" {
393+
// For non-local certificates, we expect a Rekor bundle in strict mode
394+
return fmt.Errorf("strict verification failed: missing Rekor bundle for certificate from %q", sig.Issuer)
392395
}
393396
}
397+
verifier, err = sigstore_signature.LoadVerifier(cert.PublicKey, crypto.SHA256)
398+
if err != nil {
399+
return fmt.Errorf("failed to load verifier from certificate: %w", err)
400+
}
394401
} else {
402+
// If not a certificate, it must be a public key
395403
if !allowUntrusted {
396-
return fmt.Errorf("untrusted certificate rejected: require valid x509 certificate chain")
404+
return fmt.Errorf("untrusted public key rejected: require valid x509 certificate chain")
397405
}
398406

399407
pubKey, err := cryptoutils.UnmarshalPEMToPublicKey(sig.Certificate)

pkg/signature/profiles/networkneighborhood_adapter_test.go

Lines changed: 35 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -52,7 +52,7 @@ func TestNetworkNeighborhoodAdapter(t *testing.T) {
5252
content := adapter.GetContent().(map[string]interface{})
5353
assert.Equal(t, "NetworkNeighborhood", content["kind"])
5454
assert.Equal(t, "softwarecomposition.kubescape.io/v1beta1", content["apiVersion"])
55-
55+
5656
metadata := content["metadata"].(map[string]interface{})
5757
assert.Equal(t, "test-nn", metadata["name"])
5858
assert.Equal(t, "test-ns", metadata["namespace"])
@@ -63,3 +63,37 @@ func TestNetworkNeighborhoodAdapter(t *testing.T) {
6363

6464
assert.Equal(t, nn, adapter.GetUpdatedObject())
6565
}
66+
67+
func TestNetworkNeighborhoodAdapter_EmptyTypeMeta(t *testing.T) {
68+
nn := &v1beta1.NetworkNeighborhood{
69+
TypeMeta: metav1.TypeMeta{
70+
Kind: "",
71+
APIVersion: "",
72+
},
73+
ObjectMeta: metav1.ObjectMeta{
74+
Name: "test-nn",
75+
Namespace: "test-ns",
76+
},
77+
Spec: v1beta1.NetworkNeighborhoodSpec{
78+
Containers: []v1beta1.NetworkNeighborhoodContainer{
79+
{
80+
Name: "test-container",
81+
},
82+
},
83+
},
84+
}
85+
86+
adapter := NewNetworkNeighborhoodAdapter(nn)
87+
content := adapter.GetContent().(map[string]interface{})
88+
89+
assert.Equal(t, "NetworkNeighborhood", content["kind"])
90+
assert.Equal(t, "softwarecomposition.kubescape.io/v1beta1", content["apiVersion"])
91+
92+
metadata := content["metadata"].(map[string]interface{})
93+
assert.Equal(t, "test-nn", metadata["name"])
94+
assert.Equal(t, "test-ns", metadata["namespace"])
95+
96+
spec := content["spec"].(v1beta1.NetworkNeighborhoodSpec)
97+
assert.Equal(t, 1, len(spec.Containers))
98+
assert.Equal(t, "test-container", spec.Containers[0].Name)
99+
}

pkg/signature/verify.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -21,11 +21,11 @@ func VerifyObject(obj SignableObject, opts ...VerifyOption) error {
2121

2222
annotations := obj.GetAnnotations()
2323
if annotations == nil {
24-
return fmt.Errorf("object has no annotations")
24+
return fmt.Errorf("%w (missing %s annotation)", ErrObjectNotSigned, AnnotationSignature)
2525
}
2626

2727
if _, ok := annotations[AnnotationSignature]; !ok {
28-
return ErrObjectNotSigned
28+
return fmt.Errorf("%w (missing %s annotation)", ErrObjectNotSigned, AnnotationSignature)
2929
}
3030

3131
// useKeyless=true is fine for verification since we use the certificate

0 commit comments

Comments
 (0)