From cd768e26846166b777462de48b004f2a26941415 Mon Sep 17 00:00:00 2001 From: Pierre-Gilles Mialon Date: Tue, 10 Mar 2026 14:23:29 +0000 Subject: [PATCH] Add custom Sigstore trusted root support for OCIRepository Enable signature verification of OCI artifacts against self-hosted Sigstore infrastructure (custom Fulcio CA, self-hosted Rekor instance) by introducing a trustedRootSecretRef field on the verify spec. When set, the controller reads a trusted_root.json from the referenced Secret, extracts the Rekor URL from the transparency log entries, and creates a verifier using the custom trusted material instead of the public Sigstore TUF root. Signed-off-by: Pierre-Gilles Mialon --- api/v1/ociverification_types.go | 7 + ...rce.toolkit.fluxcd.io_ocirepositories.yaml | 13 ++ docs/spec/v1/ocirepositories.md | 61 ++++++- .../controller/ocirepository_controller.go | 33 ++++ .../controller/ocirepository_verify_test.go | 103 ++++++++++++ internal/oci/cosign/cosign.go | 149 ++++++++++++------ internal/oci/cosign/cosign_test.go | 146 +++++++++++++++++ 7 files changed, 460 insertions(+), 52 deletions(-) create mode 100644 internal/controller/ocirepository_verify_test.go diff --git a/api/v1/ociverification_types.go b/api/v1/ociverification_types.go index de74be343..a2cf4c4ed 100644 --- a/api/v1/ociverification_types.go +++ b/api/v1/ociverification_types.go @@ -38,6 +38,13 @@ type OCIRepositoryVerification struct { // specified matchers match against the identity. // +optional MatchOIDCIdentity []OIDCIdentityMatch `json:"matchOIDCIdentity,omitempty"` + + // TrustedRootSecretRef specifies the Kubernetes Secret containing a + // Sigstore trusted_root.json file. This enables verification against + // self-hosted Sigstore infrastructure (custom Fulcio CA, self-hosted + // Rekor instance). The Secret must contain a key named "trusted_root.json". + // +optional + TrustedRootSecretRef *meta.LocalObjectReference `json:"trustedRootSecretRef,omitempty"` } // OIDCIdentityMatch specifies options for verifying the certificate identity, diff --git a/config/crd/bases/source.toolkit.fluxcd.io_ocirepositories.yaml b/config/crd/bases/source.toolkit.fluxcd.io_ocirepositories.yaml index f3a57d1b4..61cb36468 100644 --- a/config/crd/bases/source.toolkit.fluxcd.io_ocirepositories.yaml +++ b/config/crd/bases/source.toolkit.fluxcd.io_ocirepositories.yaml @@ -246,6 +246,19 @@ spec: required: - name type: object + trustedRootSecretRef: + description: |- + TrustedRootSecretRef specifies the Kubernetes Secret containing a + Sigstore trusted_root.json file. This enables verification against + self-hosted Sigstore infrastructure (custom Fulcio CA, self-hosted + Rekor instance). The Secret must contain a key named "trusted_root.json". + properties: + name: + description: Name of the referent. + type: string + required: + - name + type: object required: - provider type: object diff --git a/docs/spec/v1/ocirepositories.md b/docs/spec/v1/ocirepositories.md index d2bfa399e..fa3c5a2d7 100644 --- a/docs/spec/v1/ocirepositories.md +++ b/docs/spec/v1/ocirepositories.md @@ -641,11 +641,64 @@ spec: subject: "^https://github.com/stefanprodan/podinfo.*$" ``` -The controller verifies the signatures using the Fulcio root CA and the Rekor -instance hosted at [rekor.sigstore.dev](https://rekor.sigstore.dev/). +By default, the controller verifies the signatures using the Fulcio root CA and +the Rekor instance hosted at [rekor.sigstore.dev](https://rekor.sigstore.dev/). -Note that keyless verification is an **experimental feature**, using -custom root CAs or self-hosted Rekor instances are not currently supported. +##### Custom Sigstore infrastructure (self-hosted Rekor / Fulcio) + +To verify artifacts signed with a self-hosted Sigstore deployment, provide a +Sigstore `trusted_root.json` via the `.spec.verify.trustedRootSecretRef` field. +The trusted root bundles the Fulcio root CA chain, Rekor public key and URL, +CT log keys, and optionally TSA certificates. The Rekor URL is extracted +automatically from the `baseUrl` field in the transparency log entries. + +The `trusted_root.json` file follows the +[Sigstore trusted root format](https://github.com/sigstore/protobuf-specs). + +Generate the file using `cosign trusted-root create`: + +```sh +cosign trusted-root create \ + --fulcio="url=https://fulcio.example.com,certificate-chain=/path/to/fulcio-chain.pem" \ + --rekor="url=https://rekor.example.com,public-key=/path/to/rekor.pub,start-time=2024-01-01T00:00:00Z" \ + --ctfe="url=https://ctfe.example.com,public-key=/path/to/ctfe.pub,start-time=2024-01-01T00:00:00Z" \ + --out trusted_root.json +``` + +The `--tsa` flag can also be used if a custom timestamp authority is deployed: + +```sh +cosign trusted-root create \ + --tsa="url=https://tsa.example.com/api/v1/timestamp,certificate-chain=/path/to/tsa-chain.pem" \ + ... +``` + +Create the Kubernetes Secret from the generated file: + +```sh +kubectl create secret generic sigstore-trusted-root \ + --from-file=trusted_root.json=./trusted_root.json \ + -n +``` + +Reference it in the OCIRepository: + +```yaml +apiVersion: source.toolkit.fluxcd.io/v1 +kind: OCIRepository +metadata: + name: podinfo +spec: + interval: 5m + url: oci://registry.example.com/manifests/podinfo + verify: + provider: cosign + trustedRootSecretRef: + name: sigstore-trusted-root + matchOIDCIdentity: + - issuer: "^https://oidc-issuer.example.com$" + subject: "^https://ci.example.com/.*$" +``` #### Notation diff --git a/internal/controller/ocirepository_controller.go b/internal/controller/ocirepository_controller.go index ebde8aa2d..7ab7ef12e 100644 --- a/internal/controller/ocirepository_controller.go +++ b/internal/controller/ocirepository_controller.go @@ -680,6 +680,16 @@ func (r *OCIRepositoryReconciler) verifySignature(ctx context.Context, obj *sour scosign.WithRemoteOptions(opt...), } + // If a trusted root secret is provided, read and pass it to the verifier. + if trustedRootRef := obj.Spec.Verify.TrustedRootSecretRef; trustedRootRef != nil { + data, err := readTrustedRootFromSecret(ctxTimeout, r.Client, obj.Namespace, trustedRootRef) + if err != nil { + return soci.VerificationResultFailed, fmt.Errorf("failed to read trusted root from secret '%s/%s': %w", + obj.Namespace, trustedRootRef.Name, err) + } + defaultCosignOciOpts = append(defaultCosignOciOpts, scosign.WithTrustedRoot(data)) + } + // get the public keys from the given secret if secretRef := obj.Spec.Verify.SecretRef; secretRef != nil { @@ -1357,6 +1367,29 @@ func layerSelectorEqual(a, b *sourcev1.OCILayerSelector) bool { return *a == *b } +const trustedRootKey = "trusted_root.json" + +// readTrustedRootFromSecret reads and returns the trusted_root.json data from +// the Kubernetes Secret referenced by the given LocalObjectReference. +func readTrustedRootFromSecret(ctx context.Context, c client.Reader, namespace string, ref *meta.LocalObjectReference) ([]byte, error) { + secretName := types.NamespacedName{ + Namespace: namespace, + Name: ref.Name, + } + + var secret corev1.Secret + if err := c.Get(ctx, secretName, &secret); err != nil { + return nil, err + } + + data, ok := secret.Data[trustedRootKey] + if !ok { + return nil, fmt.Errorf("'%s' not found in secret '%s'", trustedRootKey, secretName.String()) + } + + return data, nil +} + func filterTags(filter string) filterFunc { return func(tags []string) ([]string, error) { if filter == "" { diff --git a/internal/controller/ocirepository_verify_test.go b/internal/controller/ocirepository_verify_test.go new file mode 100644 index 000000000..1c347027e --- /dev/null +++ b/internal/controller/ocirepository_verify_test.go @@ -0,0 +1,103 @@ +/* +Copyright 2026 The Flux 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 controller + +import ( + "context" + "testing" + + "github.com/fluxcd/pkg/apis/meta" + . "github.com/onsi/gomega" + corev1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/runtime" + "sigs.k8s.io/controller-runtime/pkg/client/fake" +) + +func TestReadTrustedRootFromSecret(t *testing.T) { + scheme := runtime.NewScheme() + _ = corev1.AddToScheme(scheme) + + tests := []struct { + name string + namespace string + ref *meta.LocalObjectReference + secret *corev1.Secret + wantData []byte + wantErr string + }{ + { + name: "reads trusted_root.json from secret", + namespace: "default", + ref: &meta.LocalObjectReference{Name: "sigstore-root"}, + secret: &corev1.Secret{ + ObjectMeta: metav1.ObjectMeta{ + Name: "sigstore-root", + Namespace: "default", + }, + Data: map[string][]byte{ + "trusted_root.json": []byte(`{"mediaType":"application/vnd.dev.sigstore.trustedroot+json;version=0.1"}`), + }, + }, + wantData: []byte(`{"mediaType":"application/vnd.dev.sigstore.trustedroot+json;version=0.1"}`), + }, + { + name: "error when secret does not exist", + namespace: "default", + ref: &meta.LocalObjectReference{Name: "missing-secret"}, + wantErr: `"missing-secret" not found`, + }, + { + name: "error when key is missing from secret", + namespace: "default", + ref: &meta.LocalObjectReference{Name: "no-key-secret"}, + secret: &corev1.Secret{ + ObjectMeta: metav1.ObjectMeta{ + Name: "no-key-secret", + Namespace: "default", + }, + Data: map[string][]byte{ + "other-key": []byte("data"), + }, + }, + wantErr: "'trusted_root.json' not found in secret", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + g := NewWithT(t) + + builder := fake.NewClientBuilder().WithScheme(scheme) + if tt.secret != nil { + builder = builder.WithObjects(tt.secret) + } + c := builder.Build() + + data, err := readTrustedRootFromSecret(context.Background(), c, tt.namespace, tt.ref) + + if tt.wantErr != "" { + g.Expect(err).To(HaveOccurred()) + g.Expect(err.Error()).To(ContainSubstring(tt.wantErr)) + return + } + + g.Expect(err).NotTo(HaveOccurred()) + g.Expect(data).To(Equal(tt.wantData)) + }) + } +} diff --git a/internal/oci/cosign/cosign.go b/internal/oci/cosign/cosign.go index f68f27129..d87d91dae 100644 --- a/internal/oci/cosign/cosign.go +++ b/internal/oci/cosign/cosign.go @@ -41,9 +41,10 @@ import ( // options is a struct that holds options for verifier. type options struct { - publicKey []byte - rOpt []remote.Option - identities []cosign.Identity + publicKey []byte + rOpt []remote.Option + identities []cosign.Identity + trustedRoot []byte } // Options is a function that configures the options applied to a Verifier. @@ -72,6 +73,16 @@ func WithIdentities(identities []cosign.Identity) Options { } } +// WithTrustedRoot sets the Sigstore trusted root JSON bytes. When provided, +// verification uses the custom trusted root instead of the public Sigstore +// TUF root. The Rekor URL is extracted from the trusted root's transparency +// log entries. +func WithTrustedRoot(trustedRoot []byte) Options { + return func(opts *options) { + opts.trustedRoot = trustedRoot + } +} + // CosignVerifier is a struct which is responsible for executing verification logic. type CosignVerifier struct { opts *cosign.CheckOpts @@ -123,8 +134,7 @@ func (f *CosignVerifierFactory) NewCosignVerifier(ctx context.Context, opts ...O checkOpts.RegistryClientOpts = co - // If a public key is provided, it will use it to verify the signature. - // If there is no public key provided, it will try keyless verification. + // If a public key is provided, use it to verify the signature. // https://github.com/sigstore/cosign/blob/main/KEYLESS.md. if len(o.publicKey) > 0 { checkOpts.Offline = true @@ -141,64 +151,107 @@ func (f *CosignVerifierFactory) NewCosignVerifier(ctx context.Context, opts ...O if err != nil { return nil, err } - } else { - checkOpts.RekorClient, err = rekor.NewClient(coptions.DefaultRekorURL) + + return &CosignVerifier{opts: checkOpts}, nil + } + + // Keyless verification: when a custom trusted root is provided, use it + // directly instead of the public Sigstore infrastructure. The Rekor URL + // is extracted from the trusted root's transparency log entries. + if len(o.trustedRoot) > 0 { + customRoot, err := root.NewTrustedRootFromJSON(o.trustedRoot) if err != nil { - return nil, fmt.Errorf("unable to create Rekor client: %w", err) + return nil, fmt.Errorf("unable to parse trusted root: %w", err) } - // Initialize TrustedMaterial for v3/Bundle verification - f.mu.Lock() - if f.trustedMaterial != nil { - checkOpts.TrustedMaterial = f.trustedMaterial - f.mu.Unlock() - } else { - // Check if we should init or retry - if f.initErr == nil || time.Since(f.lastAttempt) >= f.retryInterval { - f.lastAttempt = time.Now() - // TODO(stealthybox): it would be nice to control the http client here for the TrustedRoot fetcher - // with the current state of this part of the cosign SDK, that would involve duplicating a lot of - // their ENV, options, and defaulting code. - f.trustedMaterial, f.initErr = cosign.TrustedRoot() - } - - err := f.initErr - tm := f.trustedMaterial - f.mu.Unlock() - - if err != nil { - return nil, fmt.Errorf("unable to initialize trusted root: %w", err) - } - checkOpts.TrustedMaterial = tm + checkOpts.TrustedMaterial = customRoot + + rekorURL, err := rekorURLFromTrustedRoot(customRoot) + if err != nil { + return nil, fmt.Errorf("unable to extract Rekor URL from trusted root: %w", err) } - // Initialize legacy setup for v2 compatibility + checkOpts.RekorClient, err = rekor.NewClient(rekorURL) + if err != nil { + return nil, fmt.Errorf("unable to create Rekor client: %w", err) + } + + return &CosignVerifier{opts: checkOpts}, nil + } + + // Keyless verification using the public Sigstore infrastructure. + checkOpts.RekorClient, err = rekor.NewClient(coptions.DefaultRekorURL) + if err != nil { + return nil, fmt.Errorf("unable to create Rekor client: %w", err) + } - // This performs an online fetch of the Rekor public keys, but this is needed - // for verifying tlog entries (both online and offline). - // TODO(hidde): above note is important to keep in mind when we implement - // "offline" tlog above. - if checkOpts.RekorPubKeys, err = cosign.GetRekorPubs(ctx); err != nil { - return nil, fmt.Errorf("unable to get Rekor public keys: %w", err) + // Initialize TrustedMaterial for v3/Bundle verification. + f.mu.Lock() + if f.trustedMaterial != nil { + checkOpts.TrustedMaterial = f.trustedMaterial + f.mu.Unlock() + } else { + // Check if we should init or retry. + if f.initErr == nil || time.Since(f.lastAttempt) >= f.retryInterval { + f.lastAttempt = time.Now() + // TODO(stealthybox): it would be nice to control the http client here for the TrustedRoot fetcher + // with the current state of this part of the cosign SDK, that would involve duplicating a lot of + // their ENV, options, and defaulting code. + f.trustedMaterial, f.initErr = cosign.TrustedRoot() } - checkOpts.CTLogPubKeys, err = cosign.GetCTLogPubs(ctx) + err := f.initErr + tm := f.trustedMaterial + f.mu.Unlock() + if err != nil { - return nil, fmt.Errorf("unable to get CTLog public keys: %w", err) + return nil, fmt.Errorf("unable to initialize trusted root: %w", err) } + checkOpts.TrustedMaterial = tm + } - if checkOpts.RootCerts, err = fulcio.GetRoots(); err != nil { - return nil, fmt.Errorf("unable to get Fulcio root certs: %w", err) - } + // Initialize legacy setup for v2 compatibility. + + // This performs an online fetch of the Rekor public keys, but this is needed + // for verifying tlog entries (both online and offline). + // TODO(hidde): above note is important to keep in mind when we implement + // "offline" tlog above. + if checkOpts.RekorPubKeys, err = cosign.GetRekorPubs(ctx); err != nil { + return nil, fmt.Errorf("unable to get Rekor public keys: %w", err) + } + + checkOpts.CTLogPubKeys, err = cosign.GetCTLogPubs(ctx) + if err != nil { + return nil, fmt.Errorf("unable to get CTLog public keys: %w", err) + } + + if checkOpts.RootCerts, err = fulcio.GetRoots(); err != nil { + return nil, fmt.Errorf("unable to get Fulcio root certs: %w", err) + } + + if checkOpts.IntermediateCerts, err = fulcio.GetIntermediates(); err != nil { + return nil, fmt.Errorf("unable to get Fulcio intermediate certs: %w", err) + } + + return &CosignVerifier{opts: checkOpts}, nil +} + +// rekorURLFromTrustedRoot extracts the Rekor base URL from a trusted root's +// transparency log entries. It returns the BaseURL of the first entry that +// has one set. +func rekorURLFromTrustedRoot(tr *root.TrustedRoot) (string, error) { + logs := tr.RekorLogs() + if len(logs) == 0 { + return "", fmt.Errorf("no transparency log entries found in trusted root") + } - if checkOpts.IntermediateCerts, err = fulcio.GetIntermediates(); err != nil { - return nil, fmt.Errorf("unable to get Fulcio intermediate certs: %w", err) + for _, log := range logs { + if log.BaseURL != "" { + return log.BaseURL, nil } } - return &CosignVerifier{ - opts: checkOpts, - }, nil + return "", fmt.Errorf("no transparency log entry with a BaseURL found in trusted root") } // Verify verifies the authenticity of the given ref OCI image. diff --git a/internal/oci/cosign/cosign_test.go b/internal/oci/cosign/cosign_test.go index 21113ed91..36df501b5 100644 --- a/internal/oci/cosign/cosign_test.go +++ b/internal/oci/cosign/cosign_test.go @@ -29,6 +29,7 @@ import ( "github.com/google/go-containerregistry/pkg/v1/remote" . "github.com/onsi/gomega" "github.com/sigstore/cosign/v3/pkg/cosign" + "github.com/sigstore/sigstore-go/pkg/root" testproxy "github.com/fluxcd/source-controller/tests/proxy" testregistry "github.com/fluxcd/source-controller/tests/registry" @@ -42,6 +43,12 @@ func TestOptions(t *testing.T) { }{{ name: "no options", want: &options{}, + }, { + name: "trusted root option", + opts: []Options{WithTrustedRoot([]byte(`{"mediaType":"application/vnd.dev.sigstore.trustedroot+json;version=0.1"}`))}, + want: &options{ + trustedRoot: []byte(`{"mediaType":"application/vnd.dev.sigstore.trustedroot+json;version=0.1"}`), + }, }, { name: "signature option", opts: []Options{WithPublicKey([]byte("foo"))}, @@ -121,6 +128,10 @@ func TestOptions(t *testing.T) { t.Errorf("got %#v, want %#v", &o.publicKey, test.want.publicKey) } + if !reflect.DeepEqual(o.trustedRoot, test.want.trustedRoot) { + t.Errorf("got trustedRoot %#v, want %#v", o.trustedRoot, test.want.trustedRoot) + } + if test.want.rOpt != nil { if len(o.rOpt) != len(test.want.rOpt) { t.Errorf("got %d remote options, want %d", len(o.rOpt), len(test.want.rOpt)) @@ -137,6 +148,141 @@ func TestOptions(t *testing.T) { } } +func TestRekorURLFromTrustedRoot(t *testing.T) { + tests := []struct { + name string + json string + wantURL string + wantErr string + }{ + { + name: "extracts base URL from tlog entry", + json: trustedRootJSON("https://rekor.example.com"), + wantURL: "https://rekor.example.com", + }, + { + name: "error when no tlogs", + json: `{"mediaType":"application/vnd.dev.sigstore.trustedroot+json;version=0.1","tlogs":[]}`, + wantErr: "no transparency log entries found", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + g := NewWithT(t) + + tr, err := root.NewTrustedRootFromJSON([]byte(tt.json)) + if tt.wantErr != "" { + // If parsing succeeds with no tlogs, check rekorURLFromTrustedRoot. + if err == nil { + _, err = rekorURLFromTrustedRoot(tr) + g.Expect(err).To(HaveOccurred()) + g.Expect(err.Error()).To(ContainSubstring(tt.wantErr)) + } + return + } + g.Expect(err).NotTo(HaveOccurred()) + + gotURL, err := rekorURLFromTrustedRoot(tr) + g.Expect(err).NotTo(HaveOccurred()) + g.Expect(gotURL).To(Equal(tt.wantURL)) + }) + } +} + +func TestNewCosignVerifierWithTrustedRoot(t *testing.T) { + g := NewWithT(t) + + ctx := context.Background() + vf := NewCosignVerifierFactory() + + t.Run("keyless with custom trusted root", func(t *testing.T) { + trJSON := trustedRootJSON("https://rekor.custom.example.com") + + verifier, err := vf.NewCosignVerifier(ctx, + WithTrustedRoot([]byte(trJSON)), + WithIdentities([]cosign.Identity{ + { + SubjectRegExp: ".*", + IssuerRegExp: ".*", + }, + }), + ) + g.Expect(err).NotTo(HaveOccurred()) + g.Expect(verifier).NotTo(BeNil()) + g.Expect(verifier.opts.TrustedMaterial).NotTo(BeNil()) + g.Expect(verifier.opts.RekorClient).NotTo(BeNil()) + }) + + t.Run("invalid trusted root JSON", func(t *testing.T) { + _, err := vf.NewCosignVerifier(ctx, + WithTrustedRoot([]byte("not-valid-json")), + ) + g.Expect(err).To(HaveOccurred()) + g.Expect(err.Error()).To(ContainSubstring("unable to parse trusted root")) + }) +} + +// trustedRootJSON returns a minimal valid trusted_root.json with the given +// Rekor base URL. The ECDSA P-256 public key is a test key. +func trustedRootJSON(rekorURL string) string { + return fmt.Sprintf(`{ + "mediaType": "application/vnd.dev.sigstore.trustedroot+json;version=0.1", + "tlogs": [ + { + "baseUrl": "%s", + "hashAlgorithm": "SHA2_256", + "publicKey": { + "rawBytes": "MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAE2G2Y+2tabdTV5BcGiBIx0a9fAFwrkBbmLSGtks4L3qX6yYY0zufBnhC8Ur/iy55GhWP/9A/bY2LhC30M9+RYtw==", + "keyDetails": "PKIX_ECDSA_P256_SHA_256", + "validFor": { + "start": "2021-01-12T11:53:27.000Z" + } + }, + "logId": { + "keyId": "wNI9atQGlz+VWfO6LRygH4QUfY/8W4RFwiT5i5WRgB0=" + } + } + ], + "certificateAuthorities": [ + { + "subject": { + "organization": "test", + "commonName": "test" + }, + "uri": "https://fulcio.example.com", + "certChain": { + "certificates": [ + { + "rawBytes": "MIIB+DCCAX6gAwIBAgITNVkDZoCiofPDsy7dfm6geLbuhzAKBggqhkjOPQQDAzAqMRUwEwYDVQQKEwxzaWdzdG9yZS5kZXYxETAPBgNVBAMTCHNpZ3N0b3JlMB4XDTIxMDMwNzAzMjAyOVoXDTMxMDIyMzAzMjAyOVowKjEVMBMGA1UEChMMc2lnc3RvcmUuZGV2MREwDwYDVQQDEwhzaWdzdG9yZTB2MBAGByqGSM49AgEGBSuBBAAiA2IABLSyA7Ii5k+pNO8ZEWY0ylemWDowOkNa3kL+GZE5Z5GWehL9/A9bRNA3RbrsZ5i0JcastaRL7Sp5fp/jD5dxqc/UdTVnlvS16an+2Yfswe/QuLolRUCrcOE2+2iA5+tzd6NmMGQwDgYDVR0PAQH/BAQDAgEGMBIGA1UdEwEB/wQIMAYBAf8CAQEwHQYDVR0OBBYEFMjFHQBBmiQpMlEk6w2uSu1KBtPsMB8GA1UdIwQYMBaAFMjFHQBBmiQpMlEk6w2uSu1KBtPsMAoGCCqGSM49BAMDA2gAMGUCMH8liWJfMui6vXXBhjDgY4MwslmN/TJxVe/83WrFomwmNf056y1X48F9c4m3a3ozXAIxAKjRay5/aj/jsKKGIkmQatjI8uupHr/+CxFvaJWmpYqNkLDGRU+9orzh5hI2RrcuaQ==" + } + ] + }, + "validFor": { + "start": "2021-03-07T03:20:29.000Z", + "end": "2099-12-31T23:59:59.999Z" + } + } + ], + "ctlogs": [ + { + "baseUrl": "https://ctfe.example.com", + "hashAlgorithm": "SHA2_256", + "publicKey": { + "rawBytes": "MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAE2G2Y+2tabdTV5BcGiBIx0a9fAFwrkBbmLSGtks4L3qX6yYY0zufBnhC8Ur/iy55GhWP/9A/bY2LhC30M9+RYtw==", + "keyDetails": "PKIX_ECDSA_P256_SHA_256", + "validFor": { + "start": "2021-01-12T11:53:27.000Z" + } + }, + "logId": { + "keyId": "wNI9atQGlz+VWfO6LRygH4QUfY/8W4RFwiT5i5WRgB0=" + } + } + ] +}`, rekorURL) +} + func TestPrivateKeyVerificationWithProxy(t *testing.T) { g := NewWithT(t)