Skip to content
Merged
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: 4 additions & 8 deletions app/cli/cmd/attestation_verify.go
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
//
// Copyright 2025 The Chainloop Authors.
// Copyright 2025-2026 The Chainloop Authors.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
Expand Down Expand Up @@ -35,15 +35,11 @@ func newAttestationVerifyCmd() *cobra.Command {
# verify an attestation stored in an https endpoint
chainloop attestation verify -b https://myrepository/attestation.json`,
RunE: func(cmd *cobra.Command, _ []string) error {
res, err := action.NewAttestationVerifyAction(ActionOpts).Run(cmd.Context(), fileOrURL)
if err != nil {
if err := action.NewAttestationVerifyAction(ActionOpts).Run(cmd.Context(), fileOrURL); err != nil {
return fmt.Errorf("verifying attestation: %w", err)
}
if res {
ActionOpts.Logger.Info().Msg("attestation verified successfully")
} else {
ActionOpts.Logger.Warn().Msg("attestation couldn't be verified")
}

ActionOpts.Logger.Info().Msg("attestation verified successfully")

return nil
},
Expand Down
64 changes: 44 additions & 20 deletions app/cli/pkg/action/attestation_verify.go
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
//
// Copyright 2025 The Chainloop Authors.
// Copyright 2025-2026 The Chainloop Authors.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
Expand Down Expand Up @@ -27,6 +27,12 @@ import (
"google.golang.org/grpc/status"
)

// ErrAttestationNotVerifiable indicates that the attestation's authenticity could not be
// established at all, either because the bundle carries no verification material or because
// the control plane has no trusted root to verify it against. It is a verification failure:
// a bundle that was never verified must never be reported as verified.
var ErrAttestationNotVerifiable = errors.New("attestation could not be verified")

type AttestationVerifyAction struct {
cfg *ActionsOpts
}
Expand All @@ -35,39 +41,57 @@ func NewAttestationVerifyAction(cfg *ActionsOpts) *AttestationVerifyAction {
return &AttestationVerifyAction{cfg}
}

func (action *AttestationVerifyAction) Run(ctx context.Context, fileOrURL string) (bool, error) {
// Run verifies the given attestation bundle and returns an error if it can not be
// verified, so callers gating on the exit code never take an unverified bundle as good.
func (action *AttestationVerifyAction) Run(ctx context.Context, fileOrURL string) error {
content, err := blob.LoadFileOrURL(fileOrURL)
if err != nil {
return false, fmt.Errorf("loading attestation: %w", err)
return fmt.Errorf("loading attestation: %w", err)
}

return verifyBundle(ctx, content, action.cfg)
return verifyBundleOrFail(ctx, content, action.cfg)
}

func verifyBundle(ctx context.Context, content []byte, opts *ActionsOpts) (bool, error) {
// verifyBundleOrFail verifies the bundle against the trusted root configured in the control
// plane. Any outcome other than a successful verification is returned as an error.
func verifyBundleOrFail(ctx context.Context, content []byte, opts *ActionsOpts) error {
sc := pb.NewSigningServiceClient(opts.CPConnection)
trResp, err := sc.GetTrustedRoot(ctx, &pb.GetTrustedRootRequest{})
if err != nil {
// if trusted root is not implemented, skip verification
if status.Code(err) != codes.Unimplemented {
return false, fmt.Errorf("failed getting trusted root: %w", err)
if status.Code(err) == codes.Unimplemented {
return fmt.Errorf("%w: the control plane has no trusted root configured", ErrAttestationNotVerifiable)
}

return fmt.Errorf("failed getting trusted root: %w", err)
}

if trResp != nil {
tr, err := trustedRootPbToVerifier(trResp)
if err != nil {
return false, fmt.Errorf("getting roots: %w", err)
tr, err := trustedRootPbToVerifier(trResp)
if err != nil {
return fmt.Errorf("getting roots: %w", err)
}

if err := verifier.VerifyBundle(ctx, content, tr); err != nil {
if errors.Is(err, verifier.ErrMissingVerificationMaterial) {
return fmt.Errorf("%w: the bundle does not contain verification material", ErrAttestationNotVerifiable)
}
if err = verifier.VerifyBundle(ctx, content, tr); err != nil {
if !errors.Is(err, verifier.ErrMissingVerificationMaterial) {
opts.Logger.Debug().Err(err).Msg("bundle verification failed")
return false, errors.New("bundle verification failed")
}
} else {
return true, nil

opts.Logger.Debug().Err(err).Msg("bundle verification failed")
return errors.New("bundle verification failed")
}

return nil
}

// verifyBundle reports whether the bundle could be verified. Unlike verifyBundleOrFail, a
// bundle that is not verifiable is not an error, it is simply reported as not verified.
func verifyBundle(ctx context.Context, content []byte, opts *ActionsOpts) (bool, error) {
if err := verifyBundleOrFail(ctx, content, opts); err != nil {
if errors.Is(err, ErrAttestationNotVerifiable) {
return false, nil
}

return false, err
Comment thread
jiparis marked this conversation as resolved.
}

return false, nil
return true, nil
}
143 changes: 143 additions & 0 deletions app/cli/pkg/action/attestation_verify_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,143 @@
//
// Copyright 2026 The Chainloop Authors.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

package action

import (
"context"
"errors"
"net"
"os"
"testing"

pb "github.com/chainloop-dev/chainloop/app/controlplane/api/controlplane/v1"
"github.com/rs/zerolog"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"google.golang.org/grpc"
"google.golang.org/grpc/codes"
"google.golang.org/grpc/credentials/insecure"
"google.golang.org/grpc/status"
"google.golang.org/grpc/test/bufconn"
)

// caKeyID is the authority key identifier of testdata/ca.pub, the CA that signed
// the certificate embedded in the valid test bundles.
const caKeyID = "2a522d9652e0933d2a1237c395bc116e012f86dffff13122da59f76e0d2abe27"

func TestAttestationVerifyRun(t *testing.T) {
testCases := []struct {
name string
bundle string
// withTrustedRoot serves the test CA as trusted root, otherwise the
// signing service replies Unimplemented, simulating a control plane
// with no signing CA configured.
withTrustedRoot bool
expectErr string
// expectNotVerifiable asserts the error is the sentinel telling the
// bundle was never verified, as opposed to failing verification.
expectNotVerifiable bool
}{
{
name: "valid bundle is verified",
bundle: "testdata/bundle_valid.json",
withTrustedRoot: true,
},
{
name: "bundle without verification material fails",
bundle: "testdata/bundle_valid_nomaterial.json",
withTrustedRoot: true,
expectErr: "verification material",
expectNotVerifiable: true,
},
{
name: "no trusted root configured fails",
bundle: "testdata/bundle_valid.json",
expectErr: "trusted root",
expectNotVerifiable: true,
},
{
name: "tampered bundle fails verification",
bundle: "testdata/bundle_invalid.json",
withTrustedRoot: true,
expectErr: "bundle verification failed",
},
}

for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
opts := &ActionsOpts{
CPConnection: newSigningServiceConn(t, tc.withTrustedRoot),
Logger: zerolog.Nop(),
}

err := NewAttestationVerifyAction(opts).Run(context.Background(), tc.bundle)
if tc.expectErr == "" {
assert.NoError(t, err)
return
}

require.Error(t, err)
assert.Contains(t, err.Error(), tc.expectErr)
assert.Equal(t, tc.expectNotVerifiable, errors.Is(err, ErrAttestationNotVerifiable))
})
}
}

// fakeSigningService serves the test CA as trusted root, or replies Unimplemented
// when none is configured.
type fakeSigningService struct {
pb.UnimplementedSigningServiceServer
trustedRoot *pb.GetTrustedRootResponse
}

func (s *fakeSigningService) GetTrustedRoot(context.Context, *pb.GetTrustedRootRequest) (*pb.GetTrustedRootResponse, error) {
if s.trustedRoot == nil {
return nil, status.Error(codes.Unimplemented, "trusted root not available")
}

return s.trustedRoot, nil
}

func newSigningServiceConn(t *testing.T, withTrustedRoot bool) *grpc.ClientConn {
t.Helper()

svc := &fakeSigningService{}
if withTrustedRoot {
ca, err := os.ReadFile("testdata/ca.pub")
require.NoError(t, err)
svc.trustedRoot = &pb.GetTrustedRootResponse{
Keys: map[string]*pb.CertificateChain{caKeyID: {Certificates: []string{string(ca)}}},
}
}

l := bufconn.Listen(1 << 20)
srv := grpc.NewServer()
pb.RegisterSigningServiceServer(srv, svc)
go func() { _ = srv.Serve(l) }()

conn, err := grpc.NewClient("passthrough:///bufnet",
grpc.WithContextDialer(func(ctx context.Context, _ string) (net.Conn, error) { return l.DialContext(ctx) }),
grpc.WithTransportCredentials(insecure.NewCredentials()),
)
require.NoError(t, err)

t.Cleanup(func() {
conn.Close()
srv.Stop()
})

return conn
}
17 changes: 17 additions & 0 deletions app/cli/pkg/action/testdata/bundle_invalid.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
{
"mediaType": "application/vnd.dev.sigstore.bundle+json;version=0.3",
"verificationMaterial": {
Comment thread
jiparis marked this conversation as resolved.
"certificate": {
"rawBytes": "MIIDtzCCAZ+gAwIBAgIUHfh44qKz/2gMfB9BpJea03EW5SowDQYJKoZIhvcNAQELBQAwXjELMAkGA1UEBhMCRVMxEzARBgNVBAgMClNvbWUtU3RhdGUxEjAQBgNVBAoMCUNoYWlubG9vcDESMBAGA1UECwwJY2hhaW5sb29wMRIwEAYDVQQDDAljaGFpbmxvb3AwHhcNMjUwMjEyMDkxOTEzWhcNMjUwMjEyMDkyOTEzWjAvMS0wKwYDVQQKEyQxOGMzZjc4Mi00OTM2LTQ2MzAtYWI4ZC0yMGI1MTEzNjY2OTkwWTATBgcqhkjOPQIBBggqhkjOPQMBBwNCAATKeTBItaVgTUP55i2Xh4ygIvWeeQGGezrb1xee1k/h3COyK37D/xR+uo8/wBnti02rWJmsZQtOq7CZwONU48n6o2cwZTAOBgNVHQ8BAf8EBAMCB4AwEwYDVR0lBAwwCgYIKwYBBQUHAwMwHQYDVR0OBBYEFN8DZs+da6GELfQB1YuAfNay2rXPMB8GA1UdIwQYMBaAFKDk5Tdk8eOOMf/+GDw+62HNH6XKMA0GCSqGSIb3DQEBCwUAA4ICAQBwFFmXUNEB5yvD96iFclWxPhiCC+nGSrZl/Mq3emOQD31jkSgvH+SBkoLOG+CUwN3GANmSP99hQjcQUZW7WTvlrScUNVKmhKoODhlwajjbZ/FHK4JcKRX+2JlCPAauRiP2Z5ZSTqQT0CySs/eDdffMxzG6bzRJN/+fQheKbdnHwFgIuF2QD3TDPhXj8D7mHalWNAf4Urq7tR+8GJj9LhQwz5ioqBLBN5FxngpVZGcKX+B+jbGXij/s8Zn8PXJFQ+YtFu65++AGVWS0HpPMUDH6n/M/oInmh/jYworVr0qCMYmohVRu1doyL7LXfndApWmUOgZHhxdBRIpCBM8xy90TRylYfnWiJ338ERfh9fc+luNPxmfBADTY8Tli9OkgQXHdOW55B9WFnZSfIGWWgBH9aPzj6+VbOe/2jHKS1Gg3WbqYGfVTLjwPqpRkisnWPbWY/istvX53x1bGWvzZT+MmDlmfXnPAS/sjbKOK3EVofjNw0ziYaEuh/85P/iKk0+kV5ufmedte7J52sivJYISUi/+zys5jUpiOuVcAQN/1PlwtILZ29H4e9CNjPtGcTZUgJj48bfSgjJSBPxIoKY7L+yHpSmx27AfCiYw7R/U+wWHmQ0FCDgLOIv02jvKGNn+8H9pw//gNs9CnKZUSIOsAY5UeZLnOvp0C8TZormG4GA=="
}
},
"dsseEnvelope": {
"payload": "eyJfdHlwZSI6Imh0dHBzOi8vaW4tdG90by5pby9TdGF0ZW1lbnQvdjEiLCAic3ViamVjdCI6W3sibmFtZSI6ImNoYWlubG9vcC53b3JrZmxvdy5teXdmIiwgImRpZ2VzdCI6eyJzaGEyNTYiOiIyNjNjOTM1MDMxMGI5YzBiM2U0OGIwY2Y1ZDg0ZDM4NDAxZDBkZTAxMzMxM2RhOTcxZGE2NWViOWRiYzViODQ1In19LCB7Im5hbWUiOiJnaXQuaGVhZCIsICJkaWdlc3QiOnsic2hhMSI6ImJlYjBmOGMzYjRlNTYxOWIxM2Y2NDY5MTA2NDIwOTlhMTQzZDk1YTYifSwgImFubm90YXRpb25zIjp7ImF1dGhvci5lbWFpbCI6ImppcGFyaXNAY2hhaW5sb29wLmRldiIsICJhdXRob3IubmFtZSI6Ikpvc2UgSS4gUGFyaXMiLCAiZGF0ZSI6IjIwMjUtMDItMTJUMDk6MTg6MTZaIiwgIm1lc3NhZ2UiOiJNZXJnZSBicmFuY2ggJ21haW4nIGludG8gcGZtLTIyODMtdmVyaWYtY2hhaW5sb29wXG5cblNpZ25lZC1vZmYtYnk6IEpvc2UgSS4gUGFyaXMgPGppcGFyaXNAY2hhaW5sb29wLmRldj5cbiIsICJyZW1vdGVzIjpbeyJuYW1lIjoib3JpZ2luIiwgInVybCI6ImdpdEBnaXRodWIuY29tOmppcGFyaXMvY2hhaW5sb29wLmdpdCJ9LCB7Im5hbWUiOiJ1cHN0cmVhbSIsICJ1cmwiOiJnaXRAZ2l0aHViLmNvbTpjaGFpbmxvb3AtZGV2L2NoYWlubG9vcC5naXQifSwgeyJuYW1lIjoibWlndWUiLCAidXJsIjoiaHR0cHM6Ly9naXRodWIuY29tL21pZ21hcnRyaS9jaGFpbmxvb3AvIn0sIHsibmFtZSI6ImphdmkiLCAidXJsIjoiaHR0cHM6Ly9naXRodWIuY29tL2phdmlybG4vY2hhaW5sb29wIn1dLCAic2lnbmF0dXJlIjoiLS0tLS1CRUdJTiBQR1AgU0lHTkFUVVJFLS0tLS1cblxuaVFJeUJBQUJDQUFkRmlFRWpINTFITVdrQ05ZMmRoWndvSXR5Q2srbTN0NEZBbWVzWjFvQUNna1FvSXR5Q2srbVxuM3Q2K2lRLzRzdnhhUWpRU1NEUVVzNU9iZkJieGpZWkViTkgyemd0b0NYZ3U3QWdXcXNHbzVRUFRPblFEVm1VQ1xuajZrZnViejZ4WW9ZUUg4UjhTcEdPbWlJMTRiVUoweHZhTUZzdUp4WXVCdkVpZm9aQ2F0Mk1mM2RlUklYT2ZFRlxuSVdGdFlhb3B1blpSd05SOHNyYUZaaUtDcjNkVWJLbDZNeHdQek5JN2kxOEt5REcyUUsxcDdaUmxJK25ncGVaRVxuNU1XakM1UUFCTEhVMkJsMlpPSjFJa212RENTaXJKY3Arakw4eHk4TXJhOWQvSFVyNng2TlFDTmtYVEM5U2svNVxucHlPYWVBNVpaZExkaWgremc1cmNTL2RBYlJYMktueHdXVktJMzI2dWx0by9lN0tYVWZJNEwzRkNSQXNPWjJ0cVxuV2p0Y3JxWkhqRkVZOWdQVEU2TGppaU9uNkZMRXFmdk5ySHY1M0NNS0wxNnZLMTJEdUZrN3dMemU1eEpYMVRhdVxuQjc0dnFra21tcjVXSDBOeGhyS0p0UVhXZ291R2lJY1MyOFByV1F2QmtDdE90cDM2djRrUTBEOWdFOTVXcFVSNFxuRU9hTHB2WHpJdDBPR2IwMWRHZ2pkbm9kZFZRNVQvSDM1N3BwREtLZ254THhBWHFqRDRIejFmM0RoS1gvODZxOVxueGlLeWVZVXZDbGorOHkzQkFMcE4weitROWlNTjE4VFlRbnFsTE9kdG10ejM1TjdlQTdSSzRTc3l0eEsrNnVTZ1xuT2Z5SGQwZjd3T1AwUU1PczBiOTdyeWRvTFAxUU9kc1VjekhNQ2xkVy81aEZwbXp5SytWQ3B3Yi9pNGU1a0wyWVxuN293WFNOcUNRMTgyQWhoV3crZ1A0bzhhaVVCRytGcG41Y2FtNGhHaG1Ha0p4WDJrbEE9PVxuPXA3N1Fcbi0tLS0tRU5EIFBHUCBTSUdOQVRVUkUtLS0tLVxuIn19XSwgInByZWRpY2F0ZVR5cGUiOiJjaGFpbmxvb3AuZGV2L2F0dGVzdGF0aW9uL3YwLjIiLCAicHJlZGljYXRlIjp7ImJ1aWxkVHlwZSI6ImNoYWlubG9vcC5kZXYvd29ya2Zsb3dydW4vdjAuMSIsICJidWlsZGVyIjp7ImlkIjoiY2hhaW5sb29wLmRldi9jbGkvZGV2QHNoYTI1NjpkY2M0OGE5OGRlNjlkYTQwZDQ4NjgwYjBmMmM0YjQ1YWFhODNjYzY1YmIzMGE1MjBhYzk3MTRjYmZmZDFmZDdhIn0sICJtZXRhZGF0YSI6eyJjb250cmFjdE5hbWUiOiJteXByb2plY3QtbXl3ZiIsICJjb250cmFjdFZlcnNpb24iOiI5MyIsICJmaW5pc2hlZEF0IjoiMjAyNS0wMi0xMlQwOToxOToxMy43NzIyMTNaIiwgImluaXRpYWxpemVkQXQiOiIyMDI1LTAyLTEyVDA5OjE4OjUwLjU2ODEyWiIsICJuYW1lIjoibXl3ZiIsICJvcmdhbml6YXRpb24iOiJteS1vcmciLCAicHJvamVjdCI6Im15cHJvamVjdCIsICJwcm9qZWN0VmVyc2lvbiI6InYwLjE2Mi4wIiwgInByb2plY3RWZXJzaW9uUHJlcmVsZWFzZSI6dHJ1ZSwgInRlYW0iOiIiLCAid29ya2Zsb3dJRCI6Ijk1ZjFiNTk3LTIwZTEtNDk4Yy1iOTMxLWZjNmQ3MzMxODYwZSIsICJ3b3JrZmxvd1J1bklEIjoiNWZlZDk5ZWUtYzQ4ZS00NzVlLWFiOWQtMjk1NGMxZjliNDk1In0sICJwb2xpY3lBdHRCbG9ja2VkIjpmYWxzZSwgInBvbGljeUJsb2NrQnlwYXNzRW5hYmxlZCI6ZmFsc2UsICJwb2xpY3lDaGVja0Jsb2NraW5nU3RyYXRlZ3kiOiJBRFZJU09SWSIsICJwb2xpY3lFdmFsdWF0aW9ucyI6eyJDSEFJTkxPT1AuQVRURVNUQVRJT04iOlt7ImRlc2NyaXB0aW9uIjoiVmVyaWZpZXMgdGhhdCB0aGUgYXR0ZXN0YXRpb24gZXhwbGljaXRseSByZWZlcmVuY2VzIGEgc3BlY2lmaWMgR2l0IGNvbW1pdCIsICJuYW1lIjoic291cmNlLWNvbW1pdCIsICJwb2xpY3lSZWZlcmVuY2UiOnsiYW5ub3RhdGlvbnMiOnsibmFtZSI6InNvdXJjZS1jb21taXQiLCAib3JnYW5pemF0aW9uIjoiIn0sICJkaWdlc3QiOnsic2hhMjU2IjoiMTY0YmMzMjJiZjM1MGQ2YmI0NTc2OGM4NmRiMTgwZjUzMTUwODVjNzkxYWRlYmFmMDk0OTBmOWNjZTk5NTI4YyJ9LCAibmFtZSI6InNvdXJjZS1jb21taXQiLCAidXJpIjoiY2hhaW5sb29wOi8vbG9jYWxob3N0OjgwMDIvc291cmNlLWNvbW1pdCJ9LCAicmVxdWlyZW1lbnRzIjpbImNoYWlubG9vcC1iZXN0LXByYWN0aWNlcy9jb21taXQtc2lnbmVkIl0sICJza2lwcGVkIjpmYWxzZSwgInR5cGUiOiJBVFRFU1RBVElPTiIsICJ3aXRoIjp7ImNoZWNrX3NpZ25hdHVyZSI6InRydWUifX0sIHsiYW5ub3RhdGlvbnMiOnsiY2F0ZWdvcnkiOiJzYm9tIn0sICJkZXNjcmlwdGlvbiI6IkNoZWNrcyB0aGF0IGVpdGhlciBhIFNQRFggb3IgQ3ljbG9uZURYIFNCT00gbWF0ZXJpYWwgaXMgcHJlc2VudCBpbiB0aGUgYXR0ZXN0YXRpb24iLCAiZ3JvdXBSZWZlcmVuY2UiOnsiYW5ub3RhdGlvbnMiOnsibmFtZSI6InNib20tcXVhbGl0eSIsICJvcmdhbml6YXRpb24iOiIifSwgImRpZ2VzdCI6eyJzaGEyNTYiOiI3MDM3Y2Q2ZTk0YWY1Y2I4MDAyZGZjZWZjNzJlYjhkMDMyM2RmMDZjNTI2YzQxOGQzMTVmMGRhYWNjOWRjYzI2In0sICJuYW1lIjoic2JvbS1xdWFsaXR5IiwgInVyaSI6ImNoYWlubG9vcDovL2xvY2FsaG9zdDo4MDAyL3Nib20tcXVhbGl0eSJ9LCAibmFtZSI6InNib20tcHJlc2VudCIsICJwb2xpY3lSZWZlcmVuY2UiOnsiYW5ub3RhdGlvbnMiOnsibmFtZSI6InNib20tcHJlc2VudCIsICJvcmdhbml6YXRpb24iOiIifSwgImRpZ2VzdCI6eyJzaGEyNTYiOiJlMmRhMDdjNGRhYjc2YTU4YjQ3NGM2OGEzYzVhNTQzM2JmNGEyOGVkODgxZDRjZWFhMTZlOTE3MTYxNjg0ZWEyIn0sICJuYW1lIjoic2JvbS1wcmVzZW50IiwgInVyaSI6ImNoYWlubG9vcDovL2xvY2FsaG9zdDo4MDAyL3Nib20tcHJlc2VudCJ9LCAicmVxdWlyZW1lbnRzIjpbImNoYWlubG9vcC1iZXN0LXByYWN0aWNlcy9zYm9tLXF1YWxpdHkiXSwgInNraXBwZWQiOmZhbHNlLCAidHlwZSI6IkFUVEVTVEFUSU9OIiwgInZpb2xhdGlvbnMiOlt7Im1lc3NhZ2UiOiJtaXNzaW5nIFNCT00gbWF0ZXJpYWwiLCAic3ViamVjdCI6InNib20tcHJlc2VudCJ9XX1dfSwgInBvbGljeUhhc1Zpb2xhdGlvbnMiOnRydWUsICJydW5uZXJUeXBlIjoiUlVOTkVSX1RZUEVfVU5TUEVDSUZJRUQifX0=",
"payloadType": "application/vnd.in-toto+json",
"signatures": [
{
"sig": "TUVVQ0lCR09pQkVOdDVGQ3l6OUp2d2tGaVRpZzgvQWN2YmJoNG0ycnJnd0trODg4QWlFQTA3YUdyMUk2MndaY0RaNGdWNTJBb0pCUXRUdVg3Mi9TRU4xOVdLZWdvNzA9"
}
]
}
}
Loading
Loading