Skip to content

Conversation

@fghanmi
Copy link
Collaborator

@fghanmi fghanmi commented Oct 14, 2025

Summary by Sourcery

Enable comprehensive artifact verification, extending the API schema and models, and wiring in a new Sigstore-based verification implementation with support for OIDC, CT/TLog, TUF trust, and optional timestamp checks.

New Features:

  • Introduce a new internal verify package that implements Sigstore-based artifact verification including OCI image bundles, Rekor and CT logs, and optional RFC3161 timestamps.
  • Implement the VerifyArtifact service and handler to invoke the new verification logic and return structured results.

Enhancements:

  • Expand the VerifyArtifactRequest model with fields for digest algorithm, OIDC issuer/SAN expectations, timestamp and transparency log requirements, TUF and root JSON trust configuration, and key-based verification.
  • Remove legacy certificate and Cosign-specific fields and streamline the verification response to a message and status flag.

Build:

  • Add Sigstore, TUF, and protobuf-specs dependencies to go.mod for the new verification functionality.

@sourcery-ai
Copy link

sourcery-ai bot commented Oct 14, 2025

Reviewer's Guide

This PR introduces Sigstore-based verification by replacing the artifact verification endpoint with a GET, extending API models/specs with comprehensive verification parameters, adding a new internal verify package that implements policy-driven Sigstore and TUF flows, and wiring the service to invoke this new logic with updated dependencies.

Sequence diagram for new Sigstore-based artifact verification flow

sequenceDiagram
participant Client
participant API
participant ArtifactService
participant VerifyService
Client->>API: GET /api/v1/artifacts/verify (with verification params)
API->>ArtifactService: VerifyArtifact(request)
ArtifactService->>VerifyService: VerifyArtifact(ctx, verifyOpts)
VerifyService->>VerifyService: Build bundle, apply verification policy
VerifyService-->>ArtifactService: Verification result
ArtifactService-->>API: Verification response
API-->>Client: Verification result
Loading

ER diagram for updated VerifyArtifactRequest and VerifyArtifactResponse models

erDiagram
VERIFYARTIFACTREQUEST {
  string artifact
  string artifactDigest
  string artifactDigestAlgorithm
  string expectedOIDIssuer
  string expectedOIDIssuerRegex
  string expectedSAN
  string expectedSANRegex
  boolean requireTimestamp
  boolean requireCTLog
  boolean requireTLog
  string minBundleVersion
  string trustedPublicKey
  string trustedRootJSONPath
  string tufRootURL
  string tufTrustedRoot
  object annotations
  boolean offline
  string output
}
VERIFYARTIFACTRESPONSE {
  boolean verified
  string message
}
VERIFYARTIFACTREQUEST ||--o{ VERIFYARTIFACTRESPONSE : verifies
Loading

Class diagram for updated VerifyArtifactRequest model

classDiagram
class VerifyArtifactRequest {
  +string Artifact
  +string ArtifactDigest
  +string ArtifactDigestAlgorithm
  +string ExpectedOIDIssuer
  +string ExpectedOIDIssuerRegex
  +string ExpectedSAN
  +string ExpectedSANRegex
  +bool RequireTimestamp
  +bool RequireCTLog
  +bool RequireTLog
  +string MinBundleVersion
  +string TrustedPublicKey
  +string TrustedRootJSONPath
  +string TufRootURL
  +string TufTrustedRoot
  +map<string, string> Annotations
  +bool Offline
  +VerifyArtifactRequestOutput Output
}
class VerifyArtifactRequestOutput {
  <<enum>>
  json
  text
}
VerifyArtifactRequest --> VerifyArtifactRequestOutput
Loading

Class diagram for new VerifyOptions struct in verify package

classDiagram
class VerifyOptions {
  +string OCIImage
  +string ArtifactDigest
  +string ArtifactDigestAlgorithm
  +string ExpectedOIDIssuer
  +string ExpectedOIDIssuerRegex
  +string ExpectedSAN
  +string ExpectedSANRegex
  +bool RequireTimestamp
  +bool RequireCTLog
  +bool RequireTLog
  +string MinBundleVersion
  +string TrustedPublicKey
  +string TrustedRootJSONPath
  +string TUFRootURL
  +string TUFTrustedRoot
}
VerifyOptions : +NewVerifyOptions()
Loading

File-Level Changes

Change Details Files
Switch verification endpoint from POST to GET and update routing/handlers
  • Changed OpenAPI path to GET
  • Renamed JSON body alias and server interface methods
  • Updated handler functions and Chi routes to use GET
internal/api/openapi/rhtas-console.yaml
internal/models/models.go
internal/api/handlers.go
internal/api/routes.go
Extend VerifyArtifactRequest with configurable verification parameters
  • Added digest fields and algorithm
  • Introduced issuer/SAN expectations with regex support
  • Added timestamp, CT log, TLog, bundle version, trust root, and TUF options
internal/api/openapi/rhtas-console.yaml
internal/models/models.go
Implement new internal/services/verify package for policy-driven Sigstore/TUF verification
  • Defined VerifyOptions and defaults
  • Built bundle extraction and Sigstore/TUF material loading
  • Constructed and executed verification policies
internal/services/verify/verify.go
Integrate new verify package in service logic and update dependencies
  • Wired artifactService.VerifyArtifact to invoke verify.VerifyArtifact with options
  • Added sigstore and protobuf-specs (and related libraries) to go.mod and go.sum
internal/services/artifact.go
go.mod
go.sum

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

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 - here's some feedback:

  • The API change to use GET with a request body for verification is non-standard—consider reverting to POST or moving parameters into query strings to align with HTTP conventions.
  • In artifactService.VerifyArtifact you’re dereferencing pointer fields like req.ExpectedOIDIssuer without nil checks—add validation or defaults to prevent runtime panics on missing inputs.
  • The internal verify package is very large and handles multiple concerns; splitting it into smaller modules (e.g. bundle extraction, TUF logic, policy enforcement) would improve readability and maintainability.
Prompt for AI Agents
Please address the comments from this code review:

## Overall Comments
- The API change to use GET with a request body for verification is non-standard—consider reverting to POST or moving parameters into query strings to align with HTTP conventions.
- In artifactService.VerifyArtifact you’re dereferencing pointer fields like req.ExpectedOIDIssuer without nil checks—add validation or defaults to prevent runtime panics on missing inputs.
- The internal verify package is very large and handles multiple concerns; splitting it into smaller modules (e.g. bundle extraction, TUF logic, policy enforcement) would improve readability and maintainability.

## Individual Comments

### Comment 1
<location> `internal/api/handlers.go:55` </location>
<code_context>
 }

-func (h *Handler) PostApiV1ArtifactsVerify(w http.ResponseWriter, r *http.Request) {
+func (h *Handler) GetApiV1ArtifactsVerify(w http.ResponseWriter, r *http.Request) {
 	var req models.VerifyArtifactRequest
 	if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
</code_context>

<issue_to_address>
**issue:** Decoding request body for GET endpoint may not be standard practice.

Some clients and servers may not support GET requests with a body. Use query parameters for input, or change to POST if a request body is necessary.
</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.

@fghanmi fghanmi changed the title [SECURESIGN-2287] add artifact verification [In progress] [SECURESIGN-2287] add artifact verification Oct 14, 2025
@fghanmi fghanmi changed the title [In progress] [SECURESIGN-2287] add artifact verification [SECURESIGN-2287] add artifact verification Oct 15, 2025
@fghanmi
Copy link
Collaborator Author

fghanmi commented Oct 20, 2025

/review

@qodo-merge-pro
Copy link

PR Reviewer Guide 🔍

Here are some key observations to aid the review process:

⏱️ Estimated effort to review: 4 🔵🔵🔵🔵⚪
🧪 No relevant tests
🔒 Security concerns

Input validation:
The verification paths read remote resources (OCI registry, TUF root.json) and trust inputs like bundle maps. While HTTP responses are handled with status checks and bodies are closed, type assertions on user-provided bundle content (e.g., map[string]interface{}) and annotation parsing lack strict validation and may lead to crashes or undefined behavior rather than exploitation. Consider adding stricter schema validation for the provided bundle and defensive checks when parsing annotations and maps.

⚡ Recommended focus areas for review

Error Handling

On verification failure, the handler returns HTTP 500 with a JSON body using the normal writeJSON path but does not include an error message via writeError, and it sends the partially populated response object. Confirm this behavior is intentional and consistent with API spec; otherwise, return a structured error with appropriate status and message.

}
resp, err := h.artifactService.VerifyArtifact(r.Context(), req)
if err != nil {
	writeJSON(w, http.StatusInternalServerError, resp)
	return
}
Panic Risk

In trustedPublicKeyMaterial, the code asserts the public key as *ecdsa.PublicKey without type checking. If a non-ECDSA key is supplied, this will panic. Add a type assertion check and return a clear error.

	return root.NewTrustedPublicKeyMaterial(func(string) (root.TimeConstrainedVerifier, error) {
		verifier, err := signature.LoadECDSAVerifier(pk.(*ecdsa.PublicKey), crypto.SHA256)
		if err != nil {
			return nil, err
		}
		return &nonExpiringVerifier{verifier}, nil
	})
}
Robustness

getVerificationMaterialTlogEntries assumes specific JSON shapes in the bundle annotation and directly indexes nested maps, which can panic or misinterpret data if fields are missing or of unexpected types. Add defensive type checks and error handling for all lookups (apiVersion, kind, nested maps).

func getVerificationMaterialTlogEntries(manifestLayer *v1.Descriptor) ([]*protorekor.TransparencyLogEntry, error) {
	// 1. Get the bundle annotation
	bun := manifestLayer.Annotations["dev.sigstore.cosign/bundle"]
	var jsonData map[string]interface{}
	err := json.Unmarshal([]byte(bun), &jsonData)
	if err != nil {
		return nil, fmt.Errorf("error unmarshaling json: %w", err)
	}

	// 2. Get the log index, log ID, integrated time, signed entry timestamp and body
	logIndex, ok := jsonData["Payload"].(map[string]interface{})["logIndex"].(float64)
	if !ok {
		return nil, fmt.Errorf("error getting logIndex")
	}
	li, ok := jsonData["Payload"].(map[string]interface{})["logID"].(string)
	if !ok {
		return nil, fmt.Errorf("error getting logID")
	}
	logID, err := hex.DecodeString(li)
	if err != nil {
		return nil, fmt.Errorf("error decoding logID: %w", err)
	}
	integratedTime, ok := jsonData["Payload"].(map[string]interface{})["integratedTime"].(float64)
	if !ok {
		return nil, fmt.Errorf("error getting integratedTime")
	}
	set, ok := jsonData["SignedEntryTimestamp"].(string)
	if !ok {
		return nil, fmt.Errorf("error getting SignedEntryTimestamp")
	}
	signedEntryTimestamp, err := base64.StdEncoding.DecodeString(set)
	if err != nil {
		return nil, fmt.Errorf("error decoding signedEntryTimestamp: %w", err)
	}
	// 3. Unmarshal the body and extract the rekor KindVersion details
	body, ok := jsonData["Payload"].(map[string]interface{})["body"].(string)
	if !ok {
		return nil, fmt.Errorf("error getting body")
	}
	bodyBytes, err := base64.StdEncoding.DecodeString(body)
	if err != nil {
		return nil, fmt.Errorf("error decoding body: %w", err)
	}
	err = json.Unmarshal(bodyBytes, &jsonData)
	if err != nil {
		return nil, fmt.Errorf("error unmarshaling json: %w", err)
	}
	apiVersion := jsonData["apiVersion"].(string)
	kind := jsonData["kind"].(string)
	// 4. Construct the transparency log entry list
	return []*protorekor.TransparencyLogEntry{
		{
			LogIndex: int64(logIndex),
			LogId: &protocommon.LogId{
				KeyId: logID,
			},
			KindVersion: &protorekor.KindVersion{
				Kind:    kind,
				Version: apiVersion,
			},
			IntegratedTime: int64(integratedTime),
			InclusionPromise: &protorekor.InclusionPromise{
				SignedEntryTimestamp: signedEntryTimestamp,
			},
			InclusionProof:    nil,
			CanonicalizedBody: bodyBytes,
		},
	}, nil
}

README.md Outdated
# RHTAS Console

The RHTAS Console is a Go-based RESTful API server, providing functionality for signing and verifying software artifacts using Cosign, interacting with Sigstore's Rekor transparency log, and managing trust configurations with TUF and Fulcio. This repository serves as the backend for the RHTAS Console application, with plans to potentially add a frontend in the future.
The RHTAS Console is a Go-based RESTful API server, providing functionality for verifying software artifacts, interacting with Sigstore's Rekor transparency log, and managing trust configurations with TUF and Fulcio. This repository serves as the backend for the RHTAS Console application, with plans to potentially add a frontend in the future.
Copy link

@kahboom kahboom Oct 20, 2025

Choose a reason for hiding this comment

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

with plans to potentially add a frontend in the future

I think we can already link to the frontend here 😄 https://github.com/securesign/rhtas-console-ui

}, nil
}

func (s *artifactService) VerifyArtifact(ctx context.Context, req models.VerifyArtifactRequest) (models.VerifyArtifactResponse, error) {
Copy link

Choose a reason for hiding this comment

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

Shouldn't we also be listing and verifying all attestations? Or is that out of scope for this PR?

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

@kahboom in the latest commits, I've added support for attestation verification.
Do we need another endpoint to list all attestations/signatures ?

Copy link

Choose a reason for hiding this comment

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

My guess is that it would be good to have a separate endpoint but I would like to spend a little bit more time investigating this, if you don't mind. Could we leave that for a separate PR and I'll create a Jira issue if we decide to go that direction? What do you think?

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Sure! we can do like this.
So to resume, this PR objective is to verify both signatures and attestations.

@securesign securesign deleted a comment from qodo-merge-pro bot Oct 22, 2025
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.

New security issues found

}
}

func VerifyArtifact(ctx context.Context, verifyOpts VerifyOptions) (details string, err error) {

Choose a reason for hiding this comment

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

ctx is passed in here but not used

// Annotations Optional key-value annotations to verify in the signature
Annotations *map[string]string `json:"annotations,omitempty"`
// PredicateType The type of the predicate for the attestation.
PredicateType *string `json:"PredicateType,omitempty"`

Choose a reason for hiding this comment

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

Suggested change
PredicateType *string `json:"PredicateType,omitempty"`
PredicateType *string `json:"predicateType,omitempty"`

Comment on lines 484 to 485
apiVersion := jsonData["apiVersion"].(string)
kind := jsonData["kind"].(string)

Choose a reason for hiding this comment

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

It could be worth check this assertion is ok using ok

@fghanmi fghanmi merged commit da0dbb4 into main Oct 29, 2025
3 of 4 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