Skip to content

Conversation

@fghanmi
Copy link
Member

@fghanmi fghanmi commented Nov 21, 2025

PR Type

Enhancement


Description

  • Restructure artifact verification API response with detailed ViewModel data

    • Replace flat details object with structured SignatureView and AttestationView arrays
    • Add ArtifactSummaryView with signature, attestation, and Rekor entry counts
  • Extend certificate handling with parsed certificate chain information

    • Introduce ParsedCertificate model with role, subject, issuer, SANs, and validity dates
    • Add CertificateRole enum (leaf, intermediate, root) for certificate classification
  • Refactor verification logic to process multiple signatures and attestations

    • Split single-layer verification into multi-layer processing for signatures and attestations
    • Extract signature and attestation views with detailed metadata and timestamps
  • Simplify ImageMetadataResponse by removing embedded signatures and attestations


Diagram Walkthrough

flowchart LR
  A["VerifyArtifact Request"] --> B["Process Signing Layers"]
  A --> C["Process Attestation Layers"]
  B --> D["SignatureView Array"]
  C --> E["AttestationView Array"]
  D --> F["VerifyArtifactResponse"]
  E --> F
  G["ImageMetadata"] --> F
  H["ArtifactSummaryView"] --> F
  F --> I["Structured Response"]
Loading

File Walkthrough

Relevant files
Enhancement
models.go
Add ViewModel data structures for verification response   

internal/models/models.go

  • Add CertificateRole type and constants (leaf, intermediate, root)
  • Introduce ParsedCertificate struct with certificate details (role,
    subject, issuer, SANs, validity dates, PEM)
  • Add SignatureView struct replacing old Signature type with parsed
    certificate chain and metadata
  • Add AttestationView struct with attestation-specific fields (predicate
    type, raw statement JSON, status)
  • Add ArtifactSummaryView struct with signature, attestation, and Rekor
    entry counts
  • Restructure VerifyArtifactResponse to include artifact metadata,
    signatures array, attestations array, and summary
  • Remove Attestations and Signatures fields from ImageMetadataResponse
+80/-18 
verify.go
Refactor verification to build structured ViewModel responses

internal/services/verify/verify.go

  • Refactor VerifyArtifact to return VerifyArtifactResponse instead of
    JSON string
  • Implement multi-layer processing for signatures and attestations with
    separate view extraction
  • Add VerifyLayer function to handle bundle verification logic
  • Add VerifyAndGetSignatureView and VerifyAndGetAttestationView
    functions for detailed view extraction
  • Add GetImageMetadata function to fetch OCI image metadata
  • Add certificate parsing utilities (parsePEMCertificates,
    identifyCertRole, mergeSANs)
  • Add helper functions for error detection (isNotFound, isAuthError,
    isConnectionError)
  • Rename and refactor layer retrieval functions to return multiple
    layers
  • Add extractSignatureViewFromLayer and extractAttestationViewFromLayer
    for view construction
+495/-110
Refactoring
artifact.go
Simplify artifact service by removing signature/attestation logic

internal/services/artifact.go

  • Remove signature and attestation fetching logic from GetImageMetadata
    function
  • Simplify VerifyArtifact to return structured response instead of raw
    JSON details
  • Remove helper functions getImageSignaturesAndChains and
    getImageAttestations
  • Remove unused imports (bytes, encoding/json, crane, v1)
+3/-113 
Documentation
rhtas-console.yaml
Update OpenAPI schema with new ViewModel structures           

internal/api/openapi/rhtas-console.yaml

  • Replace VerifyArtifactResponse properties with signatures,
    attestations, summary, and artifact arrays/objects
  • Add CertificateRole enum schema (leaf, intermediate, root)
  • Add ParsedCertificate schema with certificate details and role
    information
  • Add SignatureView schema with signing certificate, certificate chain,
    and metadata
  • Add AttestationView schema with predicate type, raw statement JSON,
    and status
  • Add ArtifactSummaryView schema with signature, attestation, and Rekor
    entry counts
  • Remove Signature and Signatures schemas
  • Remove signatures and attestations fields from ImageMetadataResponse
+145/-38

@sourcery-ai
Copy link

sourcery-ai bot commented Nov 21, 2025

Reviewer's Guide

Refactors the artifact verification flow to return a rich structured VerifyArtifactResponse with signatures, attestations, artifact metadata, and summary counts, introduces view-model helpers for signatures/attestations, and extends the OpenAPI/models accordingly while removing legacy flat verification and metadata-signature listing logic.

Sequence diagram for the new artifact verification flow

sequenceDiagram
  actor "Client"
  participant "ArtifactService" as ArtifactService
  participant "VerifyService" as VerifyService
  participant "Registry" as Registry

  "Client"->>"ArtifactService": "POST /api/v1/artifacts/verify"
  "ArtifactService"->>"VerifyService": "VerifyArtifact(VerifyOptions)"

  "VerifyService"->>"Registry": "signingLayersFromOCIImage(OCIImage)"
  "Registry"-->>"VerifyService": "Signing layers[] or error"
  alt "Error getting signing layers"
    "VerifyService"-->>"ArtifactService": "Error"
    "ArtifactService"-->>"Client": "HTTP error with empty VerifyArtifactResponse"
  else "Signing layers found"
    loop "For each signing layer"
      "VerifyService"->>"VerifyService": "VerifyAndGetSignatureView(verifyOpts, layer)"
      "VerifyService"->>"VerifyService": "bundleFromSigningLayer(layer, hasTlog, hasTimestamp)"
      "VerifyService"->>"VerifyService": "extractSignatureViewFromLayer(layer, bundle)"
      "VerifyService"->>"VerifyService": "VerifyLayer(verifyOpts with ExpectedSAN, bundle)"
      alt "Verification fails"
        "VerifyService"-->>"ArtifactService": "Error (invalid signature layer)"
        "ArtifactService"-->>"Client": "HTTP error with empty VerifyArtifactResponse"
      else "Verification succeeds"
        "VerifyService"->>"VerifyService": "Append SignatureView to VerifyArtifactResponse.Signatures"
        "VerifyService"->>"VerifyService": "Increment SignatureCount and RekorEntryCount in Summary"
      end
    end

    "VerifyService"->>"Registry": "attestationLayersFromOCIImage(OCIImage)"
    "Registry"-->>"VerifyService": "Attestation layers[] or error"
    alt "Error getting attestation layers"
      "VerifyService"-->>"ArtifactService": "Error"
      "ArtifactService"-->>"Client": "HTTP error with empty VerifyArtifactResponse"
    else "Attestation layers found"
      loop "For each attestation layer"
        "VerifyService"->>"VerifyService": "VerifyAndGetAttestationView(verifyOpts, layer)"
        "VerifyService"->>"VerifyService": "bundleFromAttestationLayer(OCIImage, layer, hasTlog, hasTimestamp)"
        "VerifyService"->>"VerifyService": "extractAttestationViewFromLayer(layer, bundle)"
        "VerifyService"->>"VerifyService": "getSANFromCert(certificate annotation)"
        "VerifyService"->>"VerifyService": "VerifyLayer(verifyOpts with ExpectedSAN, bundle)"
        alt "Verification fails"
          "VerifyService"-->>"ArtifactService": "Error (invalid attestation layer)"
          "ArtifactService"-->>"Client": "HTTP error with empty VerifyArtifactResponse"
        else "Verification succeeds"
          "VerifyService"->>"VerifyService": "Populate predicateType and rawStatementJson from VerificationResult"
          "VerifyService"->>"VerifyService": "Append AttestationView to VerifyArtifactResponse.Attestations"
          "VerifyService"->>"VerifyService": "Increment AttestationCount and RekorEntryCount in Summary"
        end
      end
    end

    "VerifyService"->>"VerifyService": "GetImageMetadata(OCIImage, username, password)"
    "VerifyService"->>"Registry": "remote.Get / remote.Image / ConfigFile"
    "Registry"-->>"VerifyService": "Descriptor, Image, Config or error"
    alt "Metadata error"
      "VerifyService"-->>"ArtifactService": "Error"
      "ArtifactService"-->>"Client": "HTTP error with empty VerifyArtifactResponse"
    else "Metadata fetched"
      "VerifyService"->>"VerifyService": "Build ImageMetadataResponse (artifact, metadata, digest)"
      "VerifyService"-->>"ArtifactService": "VerifyArtifactResponse {signatures, attestations, summary, artifact}"
      "ArtifactService"-->>"Client": "200 OK with structured VerifyArtifactResponse"
    end
  end
Loading

Updated class diagram for VerifyArtifactResponse and related view models

classDiagram
  class VerifyArtifactResponse {
    +ImageMetadataResponse artifact
    +AttestationView[] attestations
    +SignatureView[] signatures
    +ArtifactSummaryView summary
  }

  class ArtifactSummaryView {
    +int signatureCount
    +int attestationCount
    +int rekorEntryCount
  }

  class ImageMetadataResponse {
    +string digest
    +string image
    +Metadata metadata
  }

  class Metadata {
    +string mediaType
    +int64 size
    +time.Time created
    +map~string,string~ labels
  }

  class SignatureView {
    +int id
    +string digest
    +ParsedCertificate signingCertificate
    +ParsedCertificate[] certificateChain
    +map~string,interfaceAny~ tlogEntry
    +string rawBundleJson
    +time.Time timestamp
    +string signatureStatus
  }

  class AttestationView {
    +int id
    +string digest
    +map~string,interfaceAny~ tlogEntry
    +string rawBundleJson
    +string rawStatementJson
    +string predicateType
    +time.Time timestamp
    +string attestationStatus
  }

  class ParsedCertificate {
    +CertificateRole role
    +string subject
    +string issuer
    +time.Time notBefore
    +time.Time notAfter
    +string[] sans
    +string serialNumber
    +bool isCa
    +string pem
  }

  class CertificateRole {
    <<enumeration>>
    Leaf
    Intermediate
    Root
  }

  VerifyArtifactResponse --> ImageMetadataResponse : "artifact"
  VerifyArtifactResponse --> SignatureView : "signatures *"
  VerifyArtifactResponse --> AttestationView : "attestations *"
  VerifyArtifactResponse --> ArtifactSummaryView : "summary"

  ImageMetadataResponse --> Metadata : "metadata"

  SignatureView --> ParsedCertificate : "signingCertificate"
  SignatureView --> ParsedCertificate : "certificateChain *"

  ParsedCertificate --> CertificateRole : "role"
Loading

File-Level Changes

Change Details Files
Refactor VerifyArtifact to operate on individual signing/attestation layers and return a structured VerifyArtifactResponse view model instead of a raw JSON string.
  • Change VerifyArtifact signature to return models.VerifyArtifactResponse and remove bundle-or-OCI branching in favor of iterating over all signing and attestation layers from the OCI image.
  • Introduce VerifyLayer helper that encapsulates bundle verification and returns verify.VerificationResult alongside a success flag.
  • Add VerifyAndGetSignatureView and VerifyAndGetAttestationView helpers to build SignatureView and AttestationView models per layer, including SAN derivation and status handling.
  • Replace single simpleSigning/attestation handling with signingLayersFromOCIImage and attestationLayersFromOCIImage utilities that return slices of descriptors and drop predicateType-based selection.
  • Expand bundle construction helpers bundleFromSigningLayer and bundleFromAttestationLayer to work with per-layer descriptors instead of re-fetching from image refs.
internal/services/verify/verify.go
Introduce rich view models for signatures, attestations, artifact summaries, and parsed certificates, wiring them through OpenAPI and generated models.
  • Update VerifyArtifactResponse schema in OpenAPI to expose signatures, attestations, summary, and artifact instead of verified/details, and regenerate corresponding Go models.
  • Define SignatureView, AttestationView, ArtifactSummaryView, ParsedCertificate, and CertificateRole types in both OpenAPI and internal/models with appropriate required fields.
  • Add helper functions extractSignatureViewFromLayer and extractAttestationViewFromLayer to populate the new view models including certificate chains, Rekor entries, raw bundle JSON, timestamps, and predicate info.
  • Ensure models.VerifyArtifactResponse in models.go aligns with new API schema and references ImageMetadataResponse for embedded artifact metadata.
internal/api/openapi/rhtas-console.yaml
internal/models/models.go
internal/services/verify/verify.go
Extend image metadata fetching to include error mapping and reuse it for verification responses while removing old signature/attestation listing from the artifact service.
  • Add GetImageMetadata helper in verify.go that fetches image descriptors/authenticated metadata using go-containerregistry remote APIs, maps errors to console-specific error values, and returns models.ImageMetadataResponse including digest and labels.
  • Swap artifactService.VerifyArtifact to directly return the VerifyArtifactResponse produced by verify.VerifyArtifact and stop wrapping it in a simple verified/details envelope.
  • Simplify artifactService.GetImageMetadata by removing getImageSignaturesAndChains/getImageAttestations and associated fields from ImageMetadataResponse, now only returning image, metadata, and digest.
  • Remove now-unused helper functions for listing signatures and attestations in internal/services/artifact.go and the Signatures/Signature types from internal/models and OpenAPI.
internal/services/verify/verify.go
internal/services/artifact.go
internal/models/models.go
internal/api/openapi/rhtas-console.yaml
Add certificate parsing, classification, and error helper utilities required by the new view models.
  • Introduce CertWithPEM struct and parsePEMCertificates helper to parse multi-PEM chains into structured certificates plus PEM payloads.
  • Add identifyCertRole and mergeSANs utilities to classify certificates as leaf/intermediate/root and to aggregate SANs from different fields.
  • Add isoFromUnix plus isNotFound/isAuthError/isConnectionError helpers in verify.go to normalize timestamps and categorize remote errors when fetching images.
internal/services/verify/verify.go
internal/models/models.go

Tips and commands

Interacting with Sourcery

  • Trigger a new review: Comment @sourcery-ai review on the pull request.
  • Continue discussions: Reply directly to Sourcery's review comments.
  • Generate a GitHub issue from a review comment: Ask Sourcery to create an
    issue from a review comment by replying to it. You can also reply to a
    review comment with @sourcery-ai issue to create an issue from it.
  • Generate a pull request title: Write @sourcery-ai anywhere in the pull
    request title to generate a title at any time. You can also comment
    @sourcery-ai title on the pull request to (re-)generate the title at any time.
  • Generate a pull request summary: Write @sourcery-ai summary anywhere in
    the pull request body to generate a PR summary at any time exactly where you
    want it. You can also comment @sourcery-ai summary on the pull request to
    (re-)generate the summary at any time.
  • Generate reviewer's guide: Comment @sourcery-ai guide on the pull
    request to (re-)generate the reviewer's guide at any time.
  • Resolve all Sourcery comments: Comment @sourcery-ai resolve on the
    pull request to resolve all Sourcery comments. Useful if you've already
    addressed all the comments and don't want to see them anymore.
  • Dismiss all Sourcery reviews: Comment @sourcery-ai dismiss on the pull
    request to dismiss all existing Sourcery reviews. Especially useful if you
    want to start fresh with a new review - don't forget to comment
    @sourcery-ai review to trigger a new review!

Customizing Your Experience

Access your dashboard to:

  • Enable or disable review features such as the Sourcery-generated pull request
    summary, the reviewer's guide, and others.
  • Change the review language.
  • Add, remove or edit custom review instructions.
  • Adjust other review settings.

Getting Help

@qodo-code-review
Copy link

qodo-code-review bot commented Nov 21, 2025

PR Compliance Guide 🔍

Below is a summary of compliance checks for this PR:

Security Compliance
Insecure error handling

Description: Error classification uses substring matching on error messages (e.g., "not found",
"unauthorized", "connection refused"), which is unreliable and can lead to improper
handling or disclosure of errors; registry/client libraries expose typed/sentinel errors
that should be checked instead.
verify.go [1149-1175]

Referred Code
// isNotFound checks if the error indicates the image was not found
func isNotFound(err error) bool {
	if err == nil {
		return false
	}
	return strings.Contains(strings.ToLower(err.Error()), "not found") ||
		strings.Contains(strings.ToLower(err.Error()), "404") ||
		strings.Contains(strings.ToLower(err.Error()), "name unknown")
}

// isAuthError checks if the error indicates an authentication failure
func isAuthError(err error) bool {
	if err == nil {
		return false
	}
	return strings.Contains(strings.ToLower(err.Error()), "unauthorized") ||
		strings.Contains(strings.ToLower(err.Error()), "401") ||
		strings.Contains(strings.ToLower(err.Error()), "authentication required")
}

// isConnectionError checks if the connection failed


 ... (clipped 6 lines)
Unvalidated timestamp usage

Description: Timestamps are parsed from untrusted bundle data and stored; while validated by Rekor,
failing to check parsing errors for empty IntegratedTime and treating zero as valid could
allow misleading timestamps—ensure zero/empty times are rejected or omitted.
verify.go [525-541]

Referred Code
var isoTime time.Time
if len(tlogEntries) > 0 && tlogEntries[0] != nil {
	isoTime, err = time.Parse(time.RFC3339, isoFromUnix(tlogEntries[0].IntegratedTime))
	if err != nil {
		return models.SignatureView{}, fmt.Errorf("invalid timestamp: %w", err)
	}
}

digestStr := layer.Digest.String()
signatureView = models.SignatureView{
	Digest:             digestStr,
	CertificateChain:   parsedCerts,
	SigningCertificate: parsedSigningCert,
	TlogEntry:          tlogMap,
	RawBundleJson:      rawBundle,
	Timestamp:          &isoTime,
}
Type mismatch risk

Description: The helper identifyCertRole returns string literals including "unknown" which are not part
of the CertificateRole enum, risking inconsistent validation/serialization; ensure only
enum values are emitted and unknowns are handled safely.
verify.go [1219-1266]

Referred Code
func identifyCertRole(cert *x509.Certificate) models.CertificateRole {
	if cert == nil {
		return "unknown"
	}

	// Leaf certificate: not a CA
	if !cert.IsCA {
		return "leaf"
	}

	// Check for self-signed: subject == issuer
	isSelfSigned := bytes.Equal(cert.RawSubject, cert.RawIssuer)

	// Verify signature with its own public key
	if isSelfSigned {
		if err := cert.CheckSignatureFrom(cert); err != nil {
			isSelfSigned = false
		}
	}

	if isSelfSigned {


 ... (clipped 27 lines)
Ticket Compliance
🎫 No ticket provided
  • Create ticket/issue
Codebase Duplication Compliance
Codebase context is not defined

Follow the guide to enable codebase context checks.

Custom Compliance
🟢
Generic: Meaningful Naming and Self-Documenting Code

Objective: Ensure all identifiers clearly express their purpose and intent, making code
self-documenting

Status: Passed

Learn more about managing compliance generic rules or creating your own custom rules

Generic: Secure Logging Practices

Objective: To ensure logs are useful for debugging and auditing without exposing sensitive
information like PII, PHI, or cardholder data.

Status: Passed

Learn more about managing compliance generic rules or creating your own custom rules

Generic: Comprehensive Audit Trails

Objective: To create a detailed and reliable record of critical system actions for security analysis
and compliance.

Status:
Missing audit logs: New verification flows for signatures and attestations perform critical security actions
without emitting audit logs containing user/context, action, and outcome.

Referred Code
func VerifyArtifact(verifyOpts VerifyOptions) (verifyArtifactResponse models.VerifyArtifactResponse, err error) {

	// SignatureViews
	// loop over all signing layers
	signatureCount := 0
	rekorEntryCount := 0
	verifyArtifactResponse = models.VerifyArtifactResponse{}
	signingLayers, err := signingLayersFromOCIImage(verifyOpts.OCIImage)
	if err != nil {
		return models.VerifyArtifactResponse{}, fmt.Errorf("error getting signing layers: %w", err)
	}

	signingLayerId := 0
	for _, layer := range signingLayers {
		details, err := VerifyAndGetSignatureView(verifyOpts, layer)
		details.Id = signingLayerId
		signingLayerId++
		if err != nil {
			return models.VerifyArtifactResponse{}, fmt.Errorf("error verifying signing layer: %w", err)
		}
		signatureCount++


 ... (clipped 37 lines)

Learn more about managing compliance generic rules or creating your own custom rules

Generic: Robust Error Handling and Edge Case Management

Objective: Ensure comprehensive error handling that provides meaningful context and graceful
degradation

Status:
Partial edge handling: While many errors are wrapped with context, some branches set fields from potentially
empty slices (e.g., first SAN, first tlog entry) without guarding empties, risking panics
or unclear failures.

Referred Code
func VerifyAndGetSignatureView(verifyOpts VerifyOptions, layer *v1.Descriptor) (signatureView models.SignatureView, err error) {
	var b *bundle.Bundle
	b, verifyOpts.ArtifactDigest, err = bundleFromSigningLayer(layer, verifyOpts.RequireTLog, verifyOpts.RequireTimestamp)

	invalidSignatureView := models.SignatureView{SignatureStatus: "invalid"}
	signatureView, err = extractSignatureViewFromLayer(layer, b)
	if err != nil {
		return invalidSignatureView, fmt.Errorf("failed to extract SignatureView from layer %w", err)
	}
	verifyOpts.ExpectedSAN = signatureView.SigningCertificate.Sans[0]

	verified, _, err := VerifyLayer(verifyOpts, b)
	if err != nil && !verified {
		return invalidSignatureView, fmt.Errorf("failed to verify signing layer: %w", err)
	}

	signatureView.SignatureStatus = "Verified"
	return signatureView, nil
}

func VerifyAndGetAttestationView(verifyOpts VerifyOptions, layer *v1.Descriptor) (attestationView models.AttestationView, err error) {


 ... (clipped 41 lines)

Learn more about managing compliance generic rules or creating your own custom rules

Generic: Secure Error Handling

Objective: To prevent the leakage of sensitive system information through error messages while
providing sufficient detail for internal debugging.

Status:
Internal details exposed: Error wrapping returns low-level messages (e.g., from registry and verification internals)
which may be propagated to clients, potentially exposing internal implementation details.

Referred Code
opts := []remote.Option{remote.WithAuth(auth)}

descriptor, err := remote.Get(ref, opts...)
if err != nil {
	if isNotFound(err) {
		return models.ImageMetadataResponse{}, fmt.Errorf("%w: %v", console_errors.ErrImageNotFound, err)

	} else if isAuthError(err) {
		return models.ImageMetadataResponse{}, fmt.Errorf("%w: %v", console_errors.ErrArtifactAuthFailed, err)

	} else if isConnectionError(err) {
		return models.ImageMetadataResponse{}, fmt.Errorf("%w: %v", console_errors.ErrArtifactConnectionRefused, err)

	} else {
		return models.ImageMetadataResponse{}, fmt.Errorf("%w: %v", console_errors.ErrFetchImageMetadataFailed, err)
	}

Learn more about managing compliance generic rules or creating your own custom rules

Generic: Security-First Input Validation and Data Handling

Objective: Ensure all data inputs are validated, sanitized, and handled securely to prevent
vulnerabilities

Status:
Input validation gaps: Functions accept external image references and process annotations without explicit
validation or normalization in the new code paths, relying on downstream libraries without
enforcing allowed patterns.

Referred Code
func signingLayersFromOCIImage(imageRef string) ([]*v1.Descriptor, error) {
	// 1. Get the image reference
	ref, err := name.ParseReference(imageRef)
	if err != nil {
		return nil, fmt.Errorf("error parsing image reference: %w", err)
	}
	// 2. Get the image descriptor
	desc, err := remote.Get(ref)
	if err != nil {
		return nil, fmt.Errorf("error getting image descriptor: %w", err)
	}
	// 3. Get the digest
	digest := ref.Context().Digest(desc.Digest.String())
	h, err := v1.NewHash(digest.Identifier())
	if err != nil {
		return nil, fmt.Errorf("error getting hash: %w", err)
	}
	// 4. Construct the signature reference - sha256-<hash>.sig
	sigTag := digest.Context().Tag(fmt.Sprint(h.Algorithm, "-", h.Hex, ".sig"))
	// 5. Get the manifest of the signature
	mf, err := crane.Manifest(sigTag.Name())


 ... (clipped 47 lines)

Learn more about managing compliance generic rules or creating your own custom rules

  • Update
Compliance status legend 🟢 - Fully Compliant
🟡 - Partial Compliant
🔴 - Not Compliant
⚪ - Requires Further Human Verification
🏷️ - Compliance label

@qodo-code-review
Copy link

qodo-code-review bot commented Nov 21, 2025

PR Code Suggestions ✨

Explore these optional code suggestions:

CategorySuggestion                                                                                                                                    Impact
Possible issue
Fix Rekor entry double-counting bug

Fix a bug in VerifyArtifact that double-counts rekorEntryCount. Calculate the
count after processing signatures and attestations by summing their respective
totals.

internal/services/verify/verify.go [120-167]

 func VerifyArtifact(verifyOpts VerifyOptions) (verifyArtifactResponse models.VerifyArtifactResponse, err error) {
 
 	// SignatureViews
 	// loop over all signing layers
-	signatureCount := 0
-	rekorEntryCount := 0
 	verifyArtifactResponse = models.VerifyArtifactResponse{}
 	signingLayers, err := signingLayersFromOCIImage(verifyOpts.OCIImage)
 	if err != nil {
 		return models.VerifyArtifactResponse{}, fmt.Errorf("error getting signing layers: %w", err)
 	}
 
 	signingLayerId := 0
 	for _, layer := range signingLayers {
 		details, err := VerifyAndGetSignatureView(verifyOpts, layer)
 		details.Id = signingLayerId
 		signingLayerId++
 		if err != nil {
 			return models.VerifyArtifactResponse{}, fmt.Errorf("error verifying signing layer: %w", err)
 		}
-		signatureCount++
-		rekorEntryCount++
-		verifyArtifactResponse.Summary.SignatureCount = signatureCount
-		verifyArtifactResponse.Summary.RekorEntryCount = rekorEntryCount
 		verifyArtifactResponse.Signatures = append(verifyArtifactResponse.Signatures, details)
 	}
 
 	// AttestationViews
 	// loop over all attestation layers
-	attestationCount := 0
 	attestationLayers, err := attestationLayersFromOCIImage(verifyOpts.OCIImage)
 	if err != nil {
 		return models.VerifyArtifactResponse{}, fmt.Errorf("error getting attestation layers: %w", err)
 	}
 	attestationLayerId := 0
 	for _, layer := range attestationLayers {
 		details, err := VerifyAndGetAttestationView(verifyOpts, layer)
 		details.Id = attestationLayerId
 		attestationLayerId++
 		if err != nil {
 			return models.VerifyArtifactResponse{}, fmt.Errorf("error verifying signing layer: %w", err)
 		}
-		attestationCount++
-		rekorEntryCount++
-		verifyArtifactResponse.Summary.AttestationCount = attestationCount
-		verifyArtifactResponse.Summary.RekorEntryCount = rekorEntryCount
 		verifyArtifactResponse.Attestations = append(verifyArtifactResponse.Attestations, details)
 	}
+
+	// Summary
+	verifyArtifactResponse.Summary.SignatureCount = len(verifyArtifactResponse.Signatures)
+	verifyArtifactResponse.Summary.AttestationCount = len(verifyArtifactResponse.Attestations)
+	verifyArtifactResponse.Summary.RekorEntryCount = verifyArtifactResponse.Summary.SignatureCount + verifyArtifactResponse.Summary.AttestationCount
 ...

[To ensure code accuracy, apply this suggestion manually]

Suggestion importance[1-10]: 8

__

Why: The suggestion correctly identifies a bug where rekorEntryCount is double-counted for artifacts with both signatures and attestations, and the proposed fix correctly calculates the total count after processing both.

Medium
High-level
Refactor to avoid duplicating logic

Refactor the code to eliminate the duplicated GetImageMetadata function. The
verification flow should receive the metadata from the artifact service instead
of fetching it again.

Examples:

internal/services/verify/verify.go [370-436]
func GetImageMetadata(image string, username string, password string) (models.ImageMetadataResponse, error) {
	ref, err := name.ParseReference(image)
	if err != nil {
		return models.ImageMetadataResponse{}, fmt.Errorf("invalid image URI: %w", err)
	}

	auth := authn.Anonymous
	if username != "" && password != "" {
		auth = &authn.Basic{Username: username, Password: password}
	}

 ... (clipped 57 lines)
internal/services/artifact.go [120-186]
func (s *artifactService) GetImageMetadata(ctx context.Context, image string, username string, password string) (models.ImageMetadataResponse, error) {
	ref, err := name.ParseReference(image)
	if err != nil {
		return models.ImageMetadataResponse{}, fmt.Errorf("invalid image URI: %w", err)
	}

	auth := authn.Anonymous
	if username != "" && password != "" {
		auth = &authn.Basic{Username: username, Password: password}
	}

 ... (clipped 57 lines)

Solution Walkthrough:

Before:

// internal/services/artifact.go
func (s *artifactService) VerifyArtifact(req) (response, error) {
  // ...
  details, err := verify.VerifyArtifact(verifyOpts) // Calls verify package
  return details, err
}

// internal/services/verify/verify.go
func VerifyArtifact(verifyOpts) (response, error) {
  // ... verify signatures and attestations
  
  // Calls its own duplicated metadata fetcher
  artifactMetadata, err := GetImageMetadata(verifyOpts.OCIImage, "", "") 
  response.Artifact = artifactMetadata
  return response, nil
}

// internal/services/verify/verify.go
func GetImageMetadata(image, user, pass) (metadata, error) {
  // ... complex logic to fetch image metadata from registry
  // This logic is duplicated from the artifact service.
}

After:

// internal/services/artifact.go
func (s *artifactService) VerifyArtifact(req) (response, error) {
  // ...
  // 1. Fetch metadata using the existing service method
  metadata, err := s.GetImageMetadata(context.Background(), *req.OciImage, "", "")
  if err != nil {
    return ...
  }

  // 2. Pass metadata to the verification function
  response, err = verify.VerifyArtifact(verifyOpts, metadata)
  return response, err
}

// internal/services/verify/verify.go
// The duplicated GetImageMetadata function is removed.
func VerifyArtifact(verifyOpts, artifactMetadata) (response, error) {
  // ... verify signatures and attestations
  
  // 3. Assign the pre-fetched metadata
  response.Artifact = artifactMetadata
  return response, nil
}
Suggestion importance[1-10]: 7

__

Why: The suggestion correctly identifies significant code duplication between verify.GetImageMetadata and artifact.GetImageMetadata, which impacts maintainability.

Medium
  • Update

Copy link

@sourcery-ai sourcery-ai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hey there - I've reviewed your changes and found some issues that need to be addressed.

  • In extractSignatureViewFromLayer/VerifyAndGetSignatureView you assume at least one parsed certificate and at least one SAN (e.g. cert slice index 0 and SigningCertificate.Sans[0]), which can panic on malformed or SAN-less certificates; consider returning a clear error when no certs/SANs are present instead of indexing blindly.
  • identifyCertRole can return the string "unknown" for models.CertificateRole, but the OpenAPI enum for CertificateRole only allows leaf/intermediate/root; either extend the enum or change the function to return one of the defined values to keep API and implementation consistent.
  • The helper functions isNotFound/isAuthError/isConnectionError now exist both in internal/services/verify and internal/services/artifact with similar logic; consider centralizing them to avoid duplication and potential drift in behavior.
Prompt for AI Agents
Please address the comments from this code review:

## Overall Comments
- In extractSignatureViewFromLayer/VerifyAndGetSignatureView you assume at least one parsed certificate and at least one SAN (e.g. cert slice index 0 and SigningCertificate.Sans[0]), which can panic on malformed or SAN-less certificates; consider returning a clear error when no certs/SANs are present instead of indexing blindly.
- identifyCertRole can return the string "unknown" for models.CertificateRole, but the OpenAPI enum for CertificateRole only allows leaf/intermediate/root; either extend the enum or change the function to return one of the defined values to keep API and implementation consistent.
- The helper functions isNotFound/isAuthError/isConnectionError now exist both in internal/services/verify and internal/services/artifact with similar logic; consider centralizing them to avoid duplication and potential drift in behavior.

## Individual Comments

### Comment 1
<location> `internal/services/verify/verify.go:314` </location>
<code_context>
-		return "", fmt.Errorf("failed to marshal verifier result: %w", err)
+		return invalidSignatureView, fmt.Errorf("failed to extract SignatureView from layer %w", err)
 	}
+	verifyOpts.ExpectedSAN = signatureView.SigningCertificate.Sans[0]

-	if err := json.Unmarshal(resultBytes, &resultMap); err != nil {
</code_context>

<issue_to_address>
**issue (bug_risk):** Accessing the first SAN without checking length risks a panic when no SANs are present.

This assumes `SigningCertificate.Sans` has at least one entry and will panic if it’s empty. Please add a length check and, if there are no SANs, either fall back to `ExpectedSANRegex` or return a clear error/mark the signature as invalid instead of indexing `Sans[0]`.
</issue_to_address>

### Comment 2
<location> `internal/services/verify/verify.go:484-488` </location>
<code_context>
+		signingCertStr = cert
+	}
+
+	cert, err := parsePEMCertificates(signingCertStr)
 	if err != nil {
-		return nil, fmt.Errorf("failed to marshal map to JSON: %w", err)
+		return models.SignatureView{}, fmt.Errorf("failed parsing signing certificate: %w", err)
+	}
+	c := cert[0]
+	sanList := mergeSANs(c.Cert)
+	sn := c.Cert.SerialNumber.String()
</code_context>

<issue_to_address>
**issue (bug_risk):** `parsePEMCertificates` can return an empty slice, which will cause an index-out-of-range panic on `cert[0]`.

If the `dev.sigstore.cosign/certificate` annotation is missing or malformed, `signingCertStr` may be empty or contain no valid PEM blocks. In that case `parsePEMCertificates` returns an empty slice and `cert[0]` panics.

Please guard this by checking `len(cert) > 0` before indexing and return a clear error (and `invalidSignatureView`) when no certificate is parsed, consistent with how the attestation path handles missing certificates.
</issue_to_address>

### Comment 3
<location> `internal/services/verify/verify.go:334-343` </location>
<code_context>
+	attestationView, err = extractAttestationViewFromLayer(layer, b)
</code_context>

<issue_to_address>
**suggestion:** Error handling in `VerifyAndGetAttestationView` is inconsistent for the statement marshaling error case.

For consistency, this path should also return `invalidAttestationView` when `json.MarshalIndent(verificationResult.Statement, ...)` fails, rather than a zero-valued `models.AttestationView{}`. That way, any caller can reliably assume that a non-nil error always pairs with `invalidAttestationView`, making the helper less error-prone to reuse.

Suggested implementation:

```golang
	statementBytes, err := json.MarshalIndent(verificationResult.Statement, "", "  ")
	if err != nil {
		return invalidAttestationView, fmt.Errorf("failed to marshal statement: %w", err)
	}

```

I assumed the marshaling code inside VerifyAndGetAttestationView currently looks like:

- It calls json.MarshalIndent(verificationResult.Statement, ..., ...) into a variable (e.g., statementBytes).
- On error, it returns models.AttestationView{} and the error.

If the exact variable names or error message differ, please adjust the SEARCH block to match your current code and keep the same change in behavior: always return invalidAttestationView instead of a zero-valued models.AttestationView{} when json.MarshalIndent fails. This ensures all error paths in VerifyAndGetAttestationView consistently return invalidAttestationView alongside a non-nil error.
</issue_to_address>

### Comment 4
<location> `internal/services/verify/verify.go:971-974` </location>
<code_context>
-// attestationLayerFromOCIImage returns the attestation layer from the OCI image reference
-func attestationLayerFromOCIImage(imageRef string, predicateType string) (*v1.Descriptor, error) {
+// attestationLayersFromOCIImage returns the attestation layer from signing layer
+func attestationLayersFromOCIImage(imageRef string) ([]*v1.Descriptor, error) {
 	ref, err := name.ParseReference(imageRef)
 	if err != nil {
</code_context>

<issue_to_address>
**suggestion (bug_risk):** All attestation manifest layers are returned without filtering, which may cause verification to fail on non-DSSE layers.

Previously, `attestationLayerFromOCIImage` filtered by media type and predicate type so only DSSE attestations were processed. Now `attestationLayersFromOCIImage` returns all layers, and `VerifyAndGetAttestationView` assumes each is a valid DSSE bundle. If the manifest includes non-`application/vnd.dsse.envelope.v1+json` layers or mixed predicate types, `bundleFromAttestationLayer` and downstream logic may fail and abort verification for the whole artifact. Please reintroduce media-type filtering here (and, if still applicable, predicate-type filtering based on `verifyOpts.PredicateType`) so only DSSE attestation layers are returned.

Suggested implementation:

```golang
 // attestationLayersFromOCIImage returns only DSSE attestation layers (and optionally
 // filters by predicate type) from the signing image manifest. This mirrors the
 // previous attestationLayerFromOCIImage behavior so that downstream verification
 // only sees DSSE bundles with the expected predicate type.
 func attestationLayersFromOCIImage(imageRef string, predicateType string) ([]*v1.Descriptor, error) {
 	ref, err := name.ParseReference(imageRef)
 	if err != nil {
 		return nil, fmt.Errorf("parsing image reference %q: %w", imageRef, err)
 	}

 	// Resolve the top-level descriptor for the image reference.
 	desc, err := remote.Get(ref)
 	if err != nil {
 		return nil, fmt.Errorf("getting remote descriptor for %q: %w", imageRef, err)
 	}

 	// We expect an index that contains an attestation manifest.
 	idx, err := desc.ImageIndex()
 	if err != nil {
 		return nil, fmt.Errorf("getting image index for %q: %w", imageRef, err)
 	}

 	indexManifest, err := idx.IndexManifest()
 	if err != nil {
 		return nil, fmt.Errorf("getting index manifest for %q: %w", imageRef, err)
 	}

 	var result []*v1.Descriptor

 	for _, m := range indexManifest.Manifests {
 		img, err := idx.Image(m.Digest)
 		if err != nil {
 			// Skip this child on error instead of aborting the whole verification.
 			continue
 		}

 		manifest, err := img.Manifest()
 		if err != nil {
 			continue
 		}

 		for i := range manifest.Layers {
 			layer := manifest.Layers[i]

 			// 1) Filter by DSSE envelope media type so we don't try to treat
 			//    arbitrary layers as DSSE bundles.
 			if string(layer.MediaType) != "application/vnd.dsse.envelope.v1+json" {
 				continue
 			}

 			// 2) If a specific predicate type is requested, further filter based
 			//    on the standard cosign predicateType annotation when present.
 			if predicateType != "" && layer.Annotations != nil {
 				if pt, ok := layer.Annotations["dev.sigstore.cosign/predicateType"]; ok && pt != predicateType {
 					continue
 				}
 			}

 			// Append a pointer to this descriptor.
 			l := layer
 			result = append(result, &l)
 		}
 	}

 	return result, nil
 }

```

To fully implement the requested behavior and integrate this change with the rest of the file, you should also:
1) Update all call sites of attestationLayersFromOCIImage to pass the predicate type:
   - Wherever you currently have:
        attLayers, err := attestationLayersFromOCIImage(imageRef)
     change it to:
        attLayers, err := attestationLayersFromOCIImage(imageRef, verifyOpts.PredicateType)
     (or pass the appropriate predicateType string if your verify options are named differently).
2) Ensure the necessary imports are present at the top of internal/services/verify/verify.go:
   - "fmt"
   - "github.com/google/go-containerregistry/pkg/name"
   - "github.com/google/go-containerregistry/pkg/v1"
   - "github.com/google/go-containerregistry/pkg/v1/remote"
   If your project already imports these (likely, given the existing OCI logic), no further changes are needed.
3) If your codebase already defines constants for:
   - the DSSE media type (e.g., application/vnd.dsse.envelope.v1+json), or
   - the predicateType annotation key (e.g., dev.sigstore.cosign/predicateType),
   replace the string literals in the new function with those existing constants to stay consistent with the rest of the codebase.
</issue_to_address>

### Comment 5
<location> `internal/services/verify/verify.go:1219-1228` </location>
<code_context>
+}
+
+// identifyCertRole classifies a certificate as leaf, intermediate, root, or unknown.
+func identifyCertRole(cert *x509.Certificate) models.CertificateRole {
+	if cert == nil {
+		return "unknown"
+	}
+
+	// Leaf certificate: not a CA
+	if !cert.IsCA {
+		return "leaf"
+	}
+
+	// Check for self-signed: subject == issuer
+	isSelfSigned := bytes.Equal(cert.RawSubject, cert.RawIssuer)
+
+	// Verify signature with its own public key
+	if isSelfSigned {
+		if err := cert.CheckSignatureFrom(cert); err != nil {
+			isSelfSigned = false
+		}
+	}
+
+	if isSelfSigned {
+		return "root"
+	}
+
</code_context>

<issue_to_address>
**nitpick (bug_risk):** The `identifyCertRole` default return value does not align with the defined `CertificateRole` constants and OpenAPI enum.

Because `models.CertificateRole` is a string alias, the compiler won’t prevent returning values outside the allowed set. The `nil`-cert branch returns "unknown", which isn’t one of the defined constants or the OpenAPI enum values (`leaf`, `intermediate`, `root`). This violates the documented contract and could confuse or break clients if it’s ever hit. I’d recommend either preventing `nil` from reaching `identifyCertRole`, or mapping the default to a documented value (or empty string) and handling that explicitly on the client side.
</issue_to_address>

Sourcery is free for open source - if you like our reviews please consider sharing them ✨
Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.

Copy link

@kahboom kahboom left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

thank you @fghanmi !

@fghanmi fghanmi merged commit 63899a3 into main Nov 26, 2025
5 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants