Skip to content

Commit 0c95ac4

Browse files
leodidoona-agent
andcommitted
fix(signing): check attestation existence before skipping upload
When artifact exists, check if attestation also exists: - If both exist: skip both uploads (prevents overwrites) - If only artifact exists: upload attestation only (handles old builds) - If HasFile check fails: fallback to upload (safe default) This handles the case where artifacts exist from old builds before SLSA attestation was implemented, ensuring attestations are uploaded for those artifacts. Co-authored-by: Ona <no-reply@ona.com>
1 parent d9f0edb commit 0c95ac4

File tree

2 files changed

+131
-16
lines changed

2 files changed

+131
-16
lines changed

pkg/leeway/signing/upload.go

Lines changed: 29 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -100,13 +100,39 @@ func (u *ArtifactUploader) UploadArtifactWithAttestation(ctx context.Context, ar
100100
"artifact_key": artifactKey,
101101
}).Info("Artifact already exists in remote cache, skipping upload")
102102

103-
// Also skip attestation upload - the existing artifact already has an attestation
104-
// Uploading a new attestation for a different local artifact would cause verification failures
103+
// Check if attestation also exists
104+
attestationExists, err := u.remoteCache.HasFile(ctx, attestationKey)
105+
if err != nil {
106+
log.WithError(err).WithField("key", attestationKey).Warn("Failed to check if attestation exists, will attempt upload")
107+
attestationExists = false // Assume it doesn't exist and try to upload
108+
}
109+
110+
if attestationExists {
111+
// Both artifact and attestation exist, skip both uploads
112+
log.WithFields(log.Fields{
113+
"artifact": artifactPath,
114+
"artifact_key": artifactKey,
115+
"att_key": attestationKey,
116+
}).Info("Skipping attestation upload (artifact and attestation already exist)")
117+
return nil
118+
}
119+
120+
// Artifact exists but attestation missing, upload attestation only
121+
log.WithFields(log.Fields{
122+
"artifact": artifactPath,
123+
"artifact_key": artifactKey,
124+
"att_key": attestationKey,
125+
}).Info("Artifact exists but attestation missing, uploading attestation only")
126+
127+
if err := u.remoteCache.UploadFile(ctx, attestationPath, attestationKey); err != nil {
128+
return fmt.Errorf("failed to upload attestation: %w", err)
129+
}
130+
105131
log.WithFields(log.Fields{
106132
"artifact": artifactPath,
107133
"artifact_key": artifactKey,
108134
"att_key": attestationKey,
109-
}).Info("Skipping attestation upload (artifact already exists with attestation)")
135+
}).Info("Successfully uploaded attestation")
110136
return nil
111137
}
112138

pkg/leeway/signing/upload_test.go

Lines changed: 102 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -382,7 +382,7 @@ func TestArtifactUploader_ConcurrentUploads(t *testing.T) {
382382
}
383383

384384
// TestArtifactUploader_SkipsExistingArtifacts tests that existing artifacts are not re-uploaded
385-
// and attestation upload is also skipped to prevent overwriting existing attestations
385+
// and attestation upload is also skipped when both artifact and attestation exist
386386
func TestArtifactUploader_SkipsExistingArtifacts(t *testing.T) {
387387
tmpDir := t.TempDir()
388388
artifactPath := filepath.Join(tmpDir, "existing.tar.gz")
@@ -395,8 +395,10 @@ func TestArtifactUploader_SkipsExistingArtifacts(t *testing.T) {
395395
uploadCalls: make(map[string]int),
396396
}
397397

398-
// Pre-populate the cache with the artifact (simulating it already exists)
398+
// Pre-populate the cache with both artifact and attestation (simulating they already exist)
399+
existingAttestation := []byte(`{"existing":"attestation"}`)
399400
mockCache.uploadedFiles["existing.tar.gz"] = artifactContent
401+
mockCache.uploadedFiles["existing.tar.gz.att"] = existingAttestation
400402

401403
uploader := NewArtifactUploader(mockCache)
402404
attestation := []byte(`{"test":"attestation"}`)
@@ -407,10 +409,43 @@ func TestArtifactUploader_SkipsExistingArtifacts(t *testing.T) {
407409
// CRITICAL: Verify that UploadFile was NOT called for the artifact
408410
assert.Equal(t, 0, mockCache.uploadCalls["existing.tar.gz"], "Artifact should not be uploaded when it already exists")
409411

410-
// CRITICAL: Verify that the attestation was NOT uploaded (FIXED BEHAVIOR)
411-
// This prevents overwriting the existing artifact's attestation with a potentially different one
412-
assert.Equal(t, 0, mockCache.uploadCalls["existing.tar.gz.att"], "Attestation should NOT be uploaded when artifact already exists")
413-
assert.NotContains(t, mockCache.uploadedFiles, "existing.tar.gz.att", "Attestation should NOT be in cache")
412+
// CRITICAL: Verify that the attestation was NOT uploaded when both exist
413+
assert.Equal(t, 0, mockCache.uploadCalls["existing.tar.gz.att"], "Attestation should NOT be uploaded when both artifact and attestation exist")
414+
415+
// Verify existing attestation is preserved
416+
assert.Equal(t, existingAttestation, mockCache.uploadedFiles["existing.tar.gz.att"], "Existing attestation should be preserved")
417+
}
418+
419+
// TestArtifactUploader_UploadsAttestationWhenMissing tests that attestation is uploaded
420+
// when artifact exists but attestation is missing (e.g., old build before SLSA)
421+
func TestArtifactUploader_UploadsAttestationWhenMissing(t *testing.T) {
422+
tmpDir := t.TempDir()
423+
artifactPath := filepath.Join(tmpDir, "existing.tar.gz")
424+
artifactContent := []byte("existing artifact content")
425+
err := os.WriteFile(artifactPath, artifactContent, 0644)
426+
require.NoError(t, err)
427+
428+
mockCache := &mockRemoteCacheUpload{
429+
uploadedFiles: make(map[string][]byte),
430+
uploadCalls: make(map[string]int),
431+
}
432+
433+
// Pre-populate the cache with artifact only (no attestation - simulating old build)
434+
mockCache.uploadedFiles["existing.tar.gz"] = artifactContent
435+
436+
uploader := NewArtifactUploader(mockCache)
437+
attestation := []byte(`{"test":"attestation"}`)
438+
439+
err = uploader.UploadArtifactWithAttestation(context.Background(), artifactPath, attestation)
440+
require.NoError(t, err)
441+
442+
// Verify that artifact was NOT re-uploaded
443+
assert.Equal(t, 0, mockCache.uploadCalls["existing.tar.gz"], "Artifact should not be re-uploaded when it already exists")
444+
445+
// CRITICAL: Verify that attestation WAS uploaded (missing attestation case)
446+
assert.Equal(t, 1, mockCache.uploadCalls["existing.tar.gz.att"], "Attestation should be uploaded when missing")
447+
assert.Contains(t, mockCache.uploadedFiles, "existing.tar.gz.att", "Attestation should be in cache")
448+
assert.Equal(t, attestation, mockCache.uploadedFiles["existing.tar.gz.att"], "Attestation content should match")
414449
}
415450

416451
// TestArtifactUploader_UploadsNewArtifacts tests that new artifacts are uploaded
@@ -459,10 +494,13 @@ func TestArtifactUploader_SimulatesDownloadedArtifactWorkflow(t *testing.T) {
459494
uploadCalls: make(map[string]int),
460495
}
461496

462-
// Step 1: Simulate build job uploading artifact
497+
// Step 1: Simulate build job uploading artifact and attestation
463498
buildArtifactContent := []byte("artifact built by build job")
499+
buildAttestation := []byte(`{"build":"attestation"}`)
464500
mockCache.uploadedFiles["downloaded.tar.gz"] = buildArtifactContent
501+
mockCache.uploadedFiles["downloaded.tar.gz.att"] = buildAttestation
465502
mockCache.uploadCalls["downloaded.tar.gz"] = 1 // Track that build job uploaded it
503+
mockCache.uploadCalls["downloaded.tar.gz.att"] = 1
466504

467505
// Step 2: Simulate sign job downloading artifact (creates local file)
468506
downloadedArtifactPath := filepath.Join(tmpDir, "downloaded.tar.gz")
@@ -481,12 +519,13 @@ func TestArtifactUploader_SimulatesDownloadedArtifactWorkflow(t *testing.T) {
481519
assert.Equal(t, 1, mockCache.uploadCalls["downloaded.tar.gz"],
482520
"Downloaded artifact should NOT be re-uploaded by sign job")
483521

484-
// CRITICAL: Attestation should NOT be uploaded
485-
// This prevents overwriting the existing artifact's attestation
486-
assert.Equal(t, 0, mockCache.uploadCalls["downloaded.tar.gz.att"],
487-
"Attestation should NOT be uploaded when artifact already exists")
488-
assert.NotContains(t, mockCache.uploadedFiles, "downloaded.tar.gz.att",
489-
"Attestation should NOT be in cache")
522+
// CRITICAL: Attestation should NOT be uploaded when both exist
523+
assert.Equal(t, 1, mockCache.uploadCalls["downloaded.tar.gz.att"],
524+
"Attestation should NOT be uploaded when both artifact and attestation exist")
525+
526+
// Verify existing attestation is preserved
527+
assert.Equal(t, buildAttestation, mockCache.uploadedFiles["downloaded.tar.gz.att"],
528+
"Existing attestation should be preserved")
490529

491530
// Artifact content should remain unchanged (not overwritten)
492531
assert.Equal(t, buildArtifactContent, mockCache.uploadedFiles["downloaded.tar.gz"],
@@ -632,3 +671,53 @@ func (m *mockRemoteCacheUploadWithHasFileError) HasFile(ctx context.Context, key
632671
}
633672
return m.mockRemoteCacheUpload.HasFile(ctx, key)
634673
}
674+
675+
// TestArtifactUploader_AttestationCheckError tests fallback behavior when attestation HasFile check fails
676+
func TestArtifactUploader_AttestationCheckError(t *testing.T) {
677+
tmpDir := t.TempDir()
678+
artifactPath := filepath.Join(tmpDir, "test.tar.gz")
679+
artifactContent := []byte("test content")
680+
err := os.WriteFile(artifactPath, artifactContent, 0644)
681+
require.NoError(t, err)
682+
683+
// Create a mock where artifact exists but attestation check fails
684+
mockCache := &mockRemoteCacheUploadWithSelectiveError{
685+
mockRemoteCacheUpload: mockRemoteCacheUpload{
686+
uploadedFiles: map[string][]byte{
687+
"test.tar.gz": artifactContent, // Artifact exists
688+
},
689+
uploadCalls: make(map[string]int),
690+
},
691+
errorKeys: map[string]error{
692+
"test.tar.gz.att": fmt.Errorf("S3 connection failed for attestation check"),
693+
},
694+
}
695+
696+
uploader := NewArtifactUploader(mockCache)
697+
attestation := []byte(`{"test":"attestation"}`)
698+
699+
// Should proceed with attestation upload despite HasFile error (safe fallback)
700+
err = uploader.UploadArtifactWithAttestation(context.Background(), artifactPath, attestation)
701+
require.NoError(t, err)
702+
703+
// Artifact should not be re-uploaded
704+
assert.Equal(t, 0, mockCache.uploadCalls["test.tar.gz"],
705+
"Artifact should not be uploaded when it exists")
706+
707+
// Attestation should be uploaded (fallback behavior assumes it doesn't exist)
708+
assert.Equal(t, 1, mockCache.uploadCalls["test.tar.gz.att"],
709+
"Attestation should be uploaded when HasFile check fails")
710+
}
711+
712+
// mockRemoteCacheUploadWithSelectiveError allows different errors for different keys
713+
type mockRemoteCacheUploadWithSelectiveError struct {
714+
mockRemoteCacheUpload
715+
errorKeys map[string]error
716+
}
717+
718+
func (m *mockRemoteCacheUploadWithSelectiveError) HasFile(ctx context.Context, key string) (bool, error) {
719+
if err, exists := m.errorKeys[key]; exists {
720+
return false, err
721+
}
722+
return m.mockRemoteCacheUpload.HasFile(ctx, key)
723+
}

0 commit comments

Comments
 (0)