Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
12 changes: 12 additions & 0 deletions internal/api/openapi/rhtas-console.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -657,6 +657,18 @@ components:
type: string
description: The container image's digest (e.g., SHA256 hash)
example: sha256:abcdef1234567890abcdef1234567890abcdef1234567890abcdef1234567890
signatures:
type: array
description: A list of cryptographic signatures that verify the integrity and authenticity of the image
items:
type: string
description: A single cryptographic signature encoded as a string
attestations:
type: array
description: A list of signed attestations associated with the image
items:
type: string
description: A single signed attestation payload or reference
required:
- artifact
- metadata
Expand Down
18 changes: 10 additions & 8 deletions internal/errors/errors.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,12 +5,14 @@ import (
)

var (
ErrImageNotFound = errors.New("image not found")
ErrArtifactAuthFailed = errors.New("authentication failed")
ErrArtifactInvalidImageURI = errors.New("invalid image uri")
ErrArtifactFailedToFetchImage = errors.New("failed to fetch image")
ErrArtifactFailedToComputeDigest = errors.New("failed to compute digest")
ErrArtifactFailedToFetchConfig = errors.New("failed to fetch config file")
ErrArtifactConnectionRefused = errors.New("connection refused")
ErrFetchImageMetadataFailed = errors.New("failed to fetch image metadata")
ErrImageNotFound = errors.New("image not found")
ErrArtifactAuthFailed = errors.New("authentication failed")
ErrArtifactInvalidImageURI = errors.New("invalid image uri")
ErrArtifactFailedToFetchImage = errors.New("failed to fetch image")
ErrArtifactFailedToComputeDigest = errors.New("failed to compute digest")
ErrArtifactFailedToFetchConfig = errors.New("failed to fetch config file")
ErrArtifactFailedToFetchSignatures = errors.New("failed to fetch image cryptographic signatures")
ErrArtifactFailedToFetchAttestations = errors.New("failed to fetch image signed attestations")
ErrArtifactConnectionRefused = errors.New("connection refused")
ErrFetchImageMetadataFailed = errors.New("failed to fetch image metadata")
)
8 changes: 7 additions & 1 deletion internal/models/models.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

79 changes: 78 additions & 1 deletion internal/services/artifact.go
Original file line number Diff line number Diff line change
@@ -1,14 +1,17 @@
package services

import (
"bytes"
"context"
"encoding/json"
"fmt"
"strings"
"time"

"github.com/google/go-containerregistry/pkg/authn"
"github.com/google/go-containerregistry/pkg/crane"
"github.com/google/go-containerregistry/pkg/name"
v1 "github.com/google/go-containerregistry/pkg/v1"
"github.com/google/go-containerregistry/pkg/v1/remote"
console_errors "github.com/securesign/rhtas-console/internal/errors"
"github.com/securesign/rhtas-console/internal/models"
Expand Down Expand Up @@ -183,6 +186,16 @@ func (s *artifactService) GetImageMetadata(ctx context.Context, image string, us
labels = nil
}

signatureList, err := getImageSignatures(digest, ref)
if err != nil {
return models.ImageMetadataResponse{}, fmt.Errorf("%w: %v", console_errors.ErrArtifactFailedToFetchSignatures, err)
}

attestationList, err := getImageAttestations(digest, ref)
if err != nil {
return models.ImageMetadataResponse{}, fmt.Errorf("%w: %v", console_errors.ErrArtifactFailedToFetchSignatures, err)
}

response := models.ImageMetadataResponse{
Image: &image,
Metadata: models.Metadata{
Expand All @@ -191,11 +204,75 @@ func (s *artifactService) GetImageMetadata(ctx context.Context, image string, us
Created: createdTime,
Labels: &labels,
},
Digest: digest.String(),
Digest: digest.String(),
Signatures: &signatureList,
Attestations: &attestationList,
}
return response, nil
}

// getImageSignatures returns the list of unique cryptographic signatures associated to the imageDigest
func getImageSignatures(imageDigest v1.Hash, ref name.Reference) ([]string, error) {
digest := ref.Context().Digest(imageDigest.String())
h, err := v1.NewHash(digest.Identifier())
if err != nil {
return nil, fmt.Errorf("error getting hash: %w", err)
}
// Construct the signature reference - sha256-<hash>.sig
sigTag := digest.Context().Tag(fmt.Sprint(h.Algorithm, "-", h.Hex, ".sig"))
// Get the manifest of the signature
mf, err := crane.Manifest(sigTag.Name())
if err != nil {
return nil, fmt.Errorf("error getting signature manifest: %w", err)
}
sigManifest, err := v1.ParseManifest(bytes.NewReader(mf))
if err != nil {
return nil, fmt.Errorf("error parsing signature manifest: %w", err)
}
signatureList := []string{}
seen := make(map[string]struct{}) // to track signature duplicates

for _, layer := range sigManifest.Layers {
digestStr := layer.Digest.String()
if _, exists := seen[digestStr]; !exists {
seen[digestStr] = struct{}{}
signatureList = append(signatureList, digestStr)
}
}
return signatureList, nil
}

// getImageAttestations returns the unique list of signed attestations associated with the imageDigest
func getImageAttestations(imageDigest v1.Hash, ref name.Reference) ([]string, error) {
digest := ref.Context().Digest(imageDigest.String())
h, err := v1.NewHash(digest.Identifier())
if err != nil {
return nil, fmt.Errorf("error getting hash: %w", err)
}
// Construct the attestation reference - sha256-<hash>.att
attTag := digest.Context().Tag(fmt.Sprint(h.Algorithm, "-", h.Hex, ".att"))
// Get the manifest of the attestation
mf, err := crane.Manifest(attTag.Name())
if err != nil {
return nil, fmt.Errorf("error getting attestation manifest: %w", err)
}
sigManifest, err := v1.ParseManifest(bytes.NewReader(mf))
if err != nil {
return nil, fmt.Errorf("error parsing attestation manifest: %w", err)
}
attestationList := []string{}
seen := make(map[string]struct{}) // to track attestation duplicates

for _, layer := range sigManifest.Layers {
digestStr := layer.Digest.String()
if _, exists := seen[digestStr]; !exists {
seen[digestStr] = struct{}{}
attestationList = append(attestationList, digestStr)
}
}
return attestationList, nil
}

// isNotFound checks if the error indicates the image was not found
func isNotFound(err error) bool {
if err == nil {
Expand Down
Loading