policy: image signature verification support
Signed-off-by: Tonis Tiigi <tonistiigi@gmail.com>
This commit is contained in:
+8
-1
@@ -67,11 +67,18 @@ func verifyPGPSignature(obj *gitobject.GitObject, sig *packet.Signature, pubKeyD
|
||||
return err
|
||||
}
|
||||
|
||||
config := &packet.Config{}
|
||||
if policy == nil || !policy.RejectExpiredKeys {
|
||||
config.Time = func() time.Time {
|
||||
return sig.CreationTime
|
||||
}
|
||||
}
|
||||
|
||||
signer, err := openpgp.CheckDetachedSignature(
|
||||
ents,
|
||||
bytes.NewReader([]byte(obj.SignedData)),
|
||||
bytes.NewReader(sigBlock),
|
||||
&packet.Config{},
|
||||
config,
|
||||
)
|
||||
if err != nil {
|
||||
if sig.IssuerKeyId != nil {
|
||||
|
||||
@@ -0,0 +1,2 @@
|
||||
bin
|
||||
vendor
|
||||
+130
@@ -0,0 +1,130 @@
|
||||
version: "2"
|
||||
|
||||
linters:
|
||||
default: none
|
||||
enable:
|
||||
- bodyclose
|
||||
- depguard
|
||||
- durationcheck
|
||||
- errname
|
||||
- errorlint
|
||||
- fatcontext
|
||||
- forbidigo
|
||||
- gocritic
|
||||
- gosec
|
||||
- govet
|
||||
- importas
|
||||
- ineffassign
|
||||
- makezero
|
||||
- misspell
|
||||
- noctx
|
||||
- nolintlint
|
||||
- revive
|
||||
- staticcheck
|
||||
- testifylint
|
||||
- unused
|
||||
- usestdlibvars
|
||||
- whitespace
|
||||
settings:
|
||||
depguard:
|
||||
rules:
|
||||
main:
|
||||
deny:
|
||||
- pkg: github.com/containerd/containerd/errdefs
|
||||
desc: The containerd errdefs package was migrated to a separate module. Use github.com/containerd/errdefs instead.
|
||||
- pkg: github.com/containerd/containerd/log
|
||||
desc: The containerd log package was migrated to a separate module. Use github.com/containerd/log instead.
|
||||
- pkg: github.com/containerd/containerd/platforms
|
||||
desc: The containerd platforms package was migrated to a separate module. Use github.com/containerd/platforms instead.
|
||||
- pkg: io/ioutil
|
||||
desc: The io/ioutil package has been deprecated.
|
||||
forbidigo:
|
||||
forbid:
|
||||
- pattern: ^context\.WithCancel(# use context\.WithCancelCause instead)?$
|
||||
- pattern: ^context\.WithDeadline(# use context\.WithDeadline instead)?$
|
||||
- pattern: ^context\.WithTimeout(# use context\.WithTimeoutCause instead)?$
|
||||
- pattern: ^ctx\.Err(# use context\.Cause instead)?$
|
||||
- pattern: ^fmt\.Errorf(# use errors\.Errorf instead)?$
|
||||
- pattern: ^logrus\.(Trace|Debug|Info|Warn|Warning|Error|Fatal)(f|ln)?(# use bklog\.G or bklog\.L instead of logrus directly)?$
|
||||
- pattern: ^log\.G\(ctx\)\.(# use bklog import instead of shadowing stdlib)?
|
||||
- pattern: ^log\.L\.(# use bklog import instead of shadowing stdlib)?
|
||||
- pattern: ^platforms\.DefaultString(# use platforms\.Format(platforms\.DefaultSpec()) instead\. Be aware that DefaultSpec is for the local platform, so must be avoided when working cross-platform)?$
|
||||
gocritic:
|
||||
disabled-checks:
|
||||
- ifElseChain
|
||||
- assignOp
|
||||
- appendAssign
|
||||
- singleCaseSwitch
|
||||
gosec:
|
||||
excludes:
|
||||
- G101
|
||||
- G402
|
||||
- G504
|
||||
- G601
|
||||
- G115
|
||||
config:
|
||||
G306: "0644"
|
||||
govet:
|
||||
enable:
|
||||
- nilness
|
||||
- unusedwrite
|
||||
importas:
|
||||
alias:
|
||||
- pkg: github.com/containerd/errdefs
|
||||
alias: cerrdefs
|
||||
- pkg: github.com/opencontainers/image-spec/specs-go/v1
|
||||
alias: ocispecs
|
||||
- pkg: github.com/opencontainers/go-digest
|
||||
alias: digest
|
||||
no-unaliased: true
|
||||
staticcheck:
|
||||
checks:
|
||||
- all
|
||||
testifylint:
|
||||
disable:
|
||||
- empty
|
||||
- bool-compare
|
||||
- len
|
||||
- negative-positive
|
||||
exclusions:
|
||||
generated: lax
|
||||
presets:
|
||||
- comments
|
||||
- common-false-positives
|
||||
- legacy
|
||||
- std-error-handling
|
||||
rules:
|
||||
- linters:
|
||||
- revive
|
||||
text: stutters
|
||||
- linters:
|
||||
- revive
|
||||
text: empty-block
|
||||
- linters:
|
||||
- revive
|
||||
text: superfluous-else
|
||||
- linters:
|
||||
- revive
|
||||
text: unused-parameter
|
||||
- linters:
|
||||
- revive
|
||||
text: redefines-builtin-id
|
||||
- linters:
|
||||
- revive
|
||||
text: if-return
|
||||
paths:
|
||||
- .*\.pb\.go$
|
||||
- examples
|
||||
|
||||
formatters:
|
||||
enable:
|
||||
- gofmt
|
||||
- goimports
|
||||
exclusions:
|
||||
generated: lax
|
||||
paths:
|
||||
- .*\.pb\.go$
|
||||
|
||||
issues:
|
||||
max-issues-per-linter: 0
|
||||
max-same-issues: 0
|
||||
+43
@@ -0,0 +1,43 @@
|
||||
# syntax=docker/dockerfile:1.19-labs
|
||||
|
||||
ARG ALPINE_VERSION=3.22
|
||||
ARG ROOT_SIGNING_VERSION=main
|
||||
ARG GOLANG_VERSION=1.25
|
||||
ARG XX_VERSION=1.8.0
|
||||
ARG DOCKER_HARDENED_IMAGES_KEYRING_VERSION=main
|
||||
|
||||
FROM scratch AS sigstore-root-signing
|
||||
ARG ROOT_SIGNING_VERSION
|
||||
ADD https://www.github.com/sigstore/root-signing.git#${ROOT_SIGNING_VERSION} /
|
||||
|
||||
FROM scratch AS tuf-root
|
||||
COPY --from=sigstore-root-signing metadata/root.json metadata/snapshot.json metadata/timestamp.json metadata/targets.json /
|
||||
COPY --parents --from=sigstore-root-signing targets/trusted_root.json /
|
||||
|
||||
FROM alpine:${ALPINE_VERSION} AS validate-tuf-root
|
||||
RUN --mount=type=bind,from=tuf-root,target=/a \
|
||||
--mount=type=bind,source=roots/tuf-root,target=/b \
|
||||
diff -ruN /a /b
|
||||
|
||||
FROM --platform=$BUILDPLATFORM tonistiigi/xx:${XX_VERSION} AS xx
|
||||
|
||||
FROM scratch AS dhi-keyring
|
||||
ARG DOCKER_HARDENED_IMAGES_KEYRING_VERSION
|
||||
ADD https://www.github.com/docker-hardened-images/keyring.git#${DOCKER_HARDENED_IMAGES_KEYRING_VERSION} /
|
||||
|
||||
FROM scratch AS dhi-pubkey
|
||||
COPY --from=dhi-keyring /publickey/dhi-latest.pub /dhi.pub
|
||||
|
||||
FROM alpine:${ALPINE_VERSION} AS validate-dhi-pubkey
|
||||
RUN --mount=type=bind,from=dhi-pubkey,target=/a \
|
||||
--mount=type=bind,source=roots/dhi,target=/b \
|
||||
diff -u /a/dhi-latest.pub /b/dhi.pub
|
||||
|
||||
FROM --platform=$BUILDPLATFORM golang:${GOLANG_VERSION}-alpine${ALPINE_VERSION} AS build
|
||||
COPY --from=xx / /
|
||||
WORKDIR /go/src/github.com/moby/policy-helpers
|
||||
ARG TARGETPLATFORM
|
||||
RUN --mount=target=. xx-go build -o /out/policy-helper ./cmd/policy-helper
|
||||
|
||||
FROM scratch AS binary
|
||||
COPY --from=build /out/policy-helper /
|
||||
+201
@@ -0,0 +1,201 @@
|
||||
Apache License
|
||||
Version 2.0, January 2004
|
||||
http://www.apache.org/licenses/
|
||||
|
||||
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
||||
|
||||
1. Definitions.
|
||||
|
||||
"License" shall mean the terms and conditions for use, reproduction,
|
||||
and distribution as defined by Sections 1 through 9 of this document.
|
||||
|
||||
"Licensor" shall mean the copyright owner or entity authorized by
|
||||
the copyright owner that is granting the License.
|
||||
|
||||
"Legal Entity" shall mean the union of the acting entity and all
|
||||
other entities that control, are controlled by, or are under common
|
||||
control with that entity. For the purposes of this definition,
|
||||
"control" means (i) the power, direct or indirect, to cause the
|
||||
direction or management of such entity, whether by contract or
|
||||
otherwise, or (ii) ownership of fifty percent (50%) or more of the
|
||||
outstanding shares, or (iii) beneficial ownership of such entity.
|
||||
|
||||
"You" (or "Your") shall mean an individual or Legal Entity
|
||||
exercising permissions granted by this License.
|
||||
|
||||
"Source" form shall mean the preferred form for making modifications,
|
||||
including but not limited to software source code, documentation
|
||||
source, and configuration files.
|
||||
|
||||
"Object" form shall mean any form resulting from mechanical
|
||||
transformation or translation of a Source form, including but
|
||||
not limited to compiled object code, generated documentation,
|
||||
and conversions to other media types.
|
||||
|
||||
"Work" shall mean the work of authorship, whether in Source or
|
||||
Object form, made available under the License, as indicated by a
|
||||
copyright notice that is included in or attached to the work
|
||||
(an example is provided in the Appendix below).
|
||||
|
||||
"Derivative Works" shall mean any work, whether in Source or Object
|
||||
form, that is based on (or derived from) the Work and for which the
|
||||
editorial revisions, annotations, elaborations, or other modifications
|
||||
represent, as a whole, an original work of authorship. For the purposes
|
||||
of this License, Derivative Works shall not include works that remain
|
||||
separable from, or merely link (or bind by name) to the interfaces of,
|
||||
the Work and Derivative Works thereof.
|
||||
|
||||
"Contribution" shall mean any work of authorship, including
|
||||
the original version of the Work and any modifications or additions
|
||||
to that Work or Derivative Works thereof, that is intentionally
|
||||
submitted to Licensor for inclusion in the Work by the copyright owner
|
||||
or by an individual or Legal Entity authorized to submit on behalf of
|
||||
the copyright owner. For the purposes of this definition, "submitted"
|
||||
means any form of electronic, verbal, or written communication sent
|
||||
to the Licensor or its representatives, including but not limited to
|
||||
communication on electronic mailing lists, source code control systems,
|
||||
and issue tracking systems that are managed by, or on behalf of, the
|
||||
Licensor for the purpose of discussing and improving the Work, but
|
||||
excluding communication that is conspicuously marked or otherwise
|
||||
designated in writing by the copyright owner as "Not a Contribution."
|
||||
|
||||
"Contributor" shall mean Licensor and any individual or Legal Entity
|
||||
on behalf of whom a Contribution has been received by Licensor and
|
||||
subsequently incorporated within the Work.
|
||||
|
||||
2. Grant of Copyright License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
copyright license to reproduce, prepare Derivative Works of,
|
||||
publicly display, publicly perform, sublicense, and distribute the
|
||||
Work and such Derivative Works in Source or Object form.
|
||||
|
||||
3. Grant of Patent License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
(except as stated in this section) patent license to make, have made,
|
||||
use, offer to sell, sell, import, and otherwise transfer the Work,
|
||||
where such license applies only to those patent claims licensable
|
||||
by such Contributor that are necessarily infringed by their
|
||||
Contribution(s) alone or by combination of their Contribution(s)
|
||||
with the Work to which such Contribution(s) was submitted. If You
|
||||
institute patent litigation against any entity (including a
|
||||
cross-claim or counterclaim in a lawsuit) alleging that the Work
|
||||
or a Contribution incorporated within the Work constitutes direct
|
||||
or contributory patent infringement, then any patent licenses
|
||||
granted to You under this License for that Work shall terminate
|
||||
as of the date such litigation is filed.
|
||||
|
||||
4. Redistribution. You may reproduce and distribute copies of the
|
||||
Work or Derivative Works thereof in any medium, with or without
|
||||
modifications, and in Source or Object form, provided that You
|
||||
meet the following conditions:
|
||||
|
||||
(a) You must give any other recipients of the Work or
|
||||
Derivative Works a copy of this License; and
|
||||
|
||||
(b) You must cause any modified files to carry prominent notices
|
||||
stating that You changed the files; and
|
||||
|
||||
(c) You must retain, in the Source form of any Derivative Works
|
||||
that You distribute, all copyright, patent, trademark, and
|
||||
attribution notices from the Source form of the Work,
|
||||
excluding those notices that do not pertain to any part of
|
||||
the Derivative Works; and
|
||||
|
||||
(d) If the Work includes a "NOTICE" text file as part of its
|
||||
distribution, then any Derivative Works that You distribute must
|
||||
include a readable copy of the attribution notices contained
|
||||
within such NOTICE file, excluding those notices that do not
|
||||
pertain to any part of the Derivative Works, in at least one
|
||||
of the following places: within a NOTICE text file distributed
|
||||
as part of the Derivative Works; within the Source form or
|
||||
documentation, if provided along with the Derivative Works; or,
|
||||
within a display generated by the Derivative Works, if and
|
||||
wherever such third-party notices normally appear. The contents
|
||||
of the NOTICE file are for informational purposes only and
|
||||
do not modify the License. You may add Your own attribution
|
||||
notices within Derivative Works that You distribute, alongside
|
||||
or as an addendum to the NOTICE text from the Work, provided
|
||||
that such additional attribution notices cannot be construed
|
||||
as modifying the License.
|
||||
|
||||
You may add Your own copyright statement to Your modifications and
|
||||
may provide additional or different license terms and conditions
|
||||
for use, reproduction, or distribution of Your modifications, or
|
||||
for any such Derivative Works as a whole, provided Your use,
|
||||
reproduction, and distribution of the Work otherwise complies with
|
||||
the conditions stated in this License.
|
||||
|
||||
5. Submission of Contributions. Unless You explicitly state otherwise,
|
||||
any Contribution intentionally submitted for inclusion in the Work
|
||||
by You to the Licensor shall be under the terms and conditions of
|
||||
this License, without any additional terms or conditions.
|
||||
Notwithstanding the above, nothing herein shall supersede or modify
|
||||
the terms of any separate license agreement you may have executed
|
||||
with Licensor regarding such Contributions.
|
||||
|
||||
6. Trademarks. This License does not grant permission to use the trade
|
||||
names, trademarks, service marks, or product names of the Licensor,
|
||||
except as required for reasonable and customary use in describing the
|
||||
origin of the Work and reproducing the content of the NOTICE file.
|
||||
|
||||
7. Disclaimer of Warranty. Unless required by applicable law or
|
||||
agreed to in writing, Licensor provides the Work (and each
|
||||
Contributor provides its Contributions) on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
||||
implied, including, without limitation, any warranties or conditions
|
||||
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
|
||||
PARTICULAR PURPOSE. You are solely responsible for determining the
|
||||
appropriateness of using or redistributing the Work and assume any
|
||||
risks associated with Your exercise of permissions under this License.
|
||||
|
||||
8. Limitation of Liability. In no event and under no legal theory,
|
||||
whether in tort (including negligence), contract, or otherwise,
|
||||
unless required by applicable law (such as deliberate and grossly
|
||||
negligent acts) or agreed to in writing, shall any Contributor be
|
||||
liable to You for damages, including any direct, indirect, special,
|
||||
incidental, or consequential damages of any character arising as a
|
||||
result of this License or out of the use or inability to use the
|
||||
Work (including but not limited to damages for loss of goodwill,
|
||||
work stoppage, computer failure or malfunction, or any and all
|
||||
other commercial damages or losses), even if such Contributor
|
||||
has been advised of the possibility of such damages.
|
||||
|
||||
9. Accepting Warranty or Additional Liability. While redistributing
|
||||
the Work or Derivative Works thereof, You may choose to offer,
|
||||
and charge a fee for, acceptance of support, warranty, indemnity,
|
||||
or other liability obligations and/or rights consistent with this
|
||||
License. However, in accepting such obligations, You may act only
|
||||
on Your own behalf and on Your sole responsibility, not on behalf
|
||||
of any other Contributor, and only if You agree to indemnify,
|
||||
defend, and hold each Contributor harmless for any liability
|
||||
incurred by, or claims asserted against, such Contributor by reason
|
||||
of your accepting any such warranty or additional liability.
|
||||
|
||||
END OF TERMS AND CONDITIONS
|
||||
|
||||
APPENDIX: How to apply the Apache License to your work.
|
||||
|
||||
To apply the Apache License to your work, attach the following
|
||||
boilerplate notice, with the fields enclosed by brackets "[]"
|
||||
replaced with your own identifying information. (Don't include
|
||||
the brackets!) The text should be enclosed in the appropriate
|
||||
comment syntax for the file format. We also recommend that a
|
||||
file or class name and description of purpose be included on the
|
||||
same "printed page" as the copyright notice for easier
|
||||
identification within third-party archives.
|
||||
|
||||
Copyright [yyyy] [name of copyright owner]
|
||||
|
||||
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.
|
||||
+1
@@ -0,0 +1 @@
|
||||
Work in progress policy helpers for Moby & BuildKit.
|
||||
+116
@@ -0,0 +1,116 @@
|
||||
variable "ROOT_SIGNING_VERSION" {
|
||||
type = string
|
||||
# default = "8842feefbb65effea46ff4a0f2b6aad91e685fe9" # expired root
|
||||
# default = "9d8b5c5e3bed603c80b57fcc316b7a1af688c57e" # expired timestamp
|
||||
default = "b72505e865a7c68bd75e03272fa66512bcb41bb1"
|
||||
description = "The git commit hash of sigstore/root-signing to use for embedded roots."
|
||||
}
|
||||
|
||||
variable "DOCKER_HARDENED_IMAGES_KEYRING_VERSION" {
|
||||
type = string
|
||||
default = "04ae44966821da8e5cdcb4c51137dee69297161a"
|
||||
description = "The git branch or commit hash of docker/hardened-images-keyring to use for DHI verification."
|
||||
}
|
||||
|
||||
target "tuf-root" {
|
||||
target = "tuf-root"
|
||||
output = [{
|
||||
type = "local",
|
||||
dest = "roots/tuf-root"
|
||||
}]
|
||||
args = {
|
||||
ROOT_SIGNING_VERSION = ROOT_SIGNING_VERSION
|
||||
}
|
||||
}
|
||||
|
||||
target "validate-tuf-root" {
|
||||
target = "validate-tuf-root"
|
||||
output = [{
|
||||
type = "cacheonly"
|
||||
}]
|
||||
args = {
|
||||
ROOT_SIGNING_VERSION = ROOT_SIGNING_VERSION
|
||||
}
|
||||
}
|
||||
|
||||
group "validate-all" {
|
||||
targets = ["lint", "lint-gopls", "validate-dockerfile", "validate-generated-files"]
|
||||
}
|
||||
|
||||
group "validate-generated-files" {
|
||||
targets = ["validate-tuf-root"]
|
||||
}
|
||||
|
||||
target "lint" {
|
||||
dockerfile = "./hack/dockerfiles/lint.Dockerfile"
|
||||
output = ["type=cacheonly"]
|
||||
args = {
|
||||
GOLANGCI_FROM_SOURCE = "true"
|
||||
}
|
||||
}
|
||||
|
||||
target "validate-dockerfile" {
|
||||
matrix = {
|
||||
dockerfile = [
|
||||
"Dockerfile",
|
||||
"./hack/dockerfiles/lint.Dockerfile",
|
||||
]
|
||||
}
|
||||
name = "validate-dockerfile-${md5(dockerfile)}"
|
||||
dockerfile = dockerfile
|
||||
call = "check"
|
||||
}
|
||||
|
||||
target "lint-gopls" {
|
||||
inherits = [ "lint" ]
|
||||
target = "gopls-analyze"
|
||||
}
|
||||
|
||||
target "binary" {
|
||||
target = "binary"
|
||||
platforms = [ "local" ]
|
||||
output = [{
|
||||
type = "local",
|
||||
dest = "bin/"
|
||||
}]
|
||||
}
|
||||
|
||||
target "_all_platforms" {
|
||||
platforms = [
|
||||
"freebsd/amd64",
|
||||
"linux/amd64",
|
||||
"linux/arm64",
|
||||
"linux/s390x",
|
||||
"linux/ppc64le",
|
||||
"linux/riscv64",
|
||||
"windows/amd64",
|
||||
"windows/arm64",
|
||||
"darwin/amd64",
|
||||
"darwin/arm64",
|
||||
]
|
||||
}
|
||||
|
||||
target "binary-all" {
|
||||
inherits = [ "binary", "_all_platforms" ]
|
||||
}
|
||||
|
||||
target "dhi-pubkey" {
|
||||
target = "dhi-pubkey"
|
||||
output = [{
|
||||
type = "local",
|
||||
dest = "roots/dhi"
|
||||
}]
|
||||
args = {
|
||||
DOCKER_HARDENED_IMAGES_KEYRING_VERSION = DOCKER_HARDENED_IMAGES_KEYRING_VERSION
|
||||
}
|
||||
}
|
||||
|
||||
target "validate-dhi-pubkey" {
|
||||
target = "validate-dhi-pubkey"
|
||||
output = [{
|
||||
type = "cacheonly"
|
||||
}]
|
||||
args = {
|
||||
DOCKER_HARDENED_IMAGES_KEYRING_VERSION = DOCKER_HARDENED_IMAGES_KEYRING_VERSION
|
||||
}
|
||||
}
|
||||
+21
@@ -0,0 +1,21 @@
|
||||
package verifier
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
digest "github.com/opencontainers/go-digest"
|
||||
)
|
||||
|
||||
type NoSigChainError struct {
|
||||
Target digest.Digest
|
||||
HasAttestation bool
|
||||
}
|
||||
|
||||
var _ error = &NoSigChainError{}
|
||||
|
||||
func (e *NoSigChainError) Error() string {
|
||||
if e.HasAttestation {
|
||||
return fmt.Sprintf("no signature found for image %s", e.Target)
|
||||
}
|
||||
return fmt.Sprintf("no provenance attestation found for image %s", e.Target)
|
||||
}
|
||||
+338
@@ -0,0 +1,338 @@
|
||||
package verifier
|
||||
|
||||
import (
|
||||
"crypto/x509"
|
||||
"encoding/base64"
|
||||
"encoding/hex"
|
||||
"encoding/json"
|
||||
"encoding/pem"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
ocispecs "github.com/opencontainers/image-spec/specs-go/v1"
|
||||
"github.com/pkg/errors"
|
||||
v1common "github.com/sigstore/protobuf-specs/gen/pb-go/common/v1"
|
||||
v1 "github.com/sigstore/protobuf-specs/gen/pb-go/rekor/v1"
|
||||
"github.com/sigstore/sigstore-go/pkg/bundle"
|
||||
"github.com/sigstore/sigstore-go/pkg/root"
|
||||
"github.com/sigstore/sigstore-go/pkg/tlog"
|
||||
"github.com/sigstore/sigstore-go/pkg/verify"
|
||||
)
|
||||
|
||||
const (
|
||||
annotationCert = "dev.sigstore.cosign/certificate"
|
||||
// annotationChain = "dev.sigstore.cosign/chain"
|
||||
annotationSignature = "dev.cosignproject.cosign/signature"
|
||||
annotationBundle = "dev.sigstore.cosign/bundle"
|
||||
)
|
||||
|
||||
// hashedRecordSignedEntity implements verify.SignedEntity using cosign oldbundle format.
|
||||
type hashedRecordSignedEntity struct {
|
||||
mfst *ocispecs.Manifest
|
||||
cert verify.VerificationContent
|
||||
sig *messageSignatureContent
|
||||
isDHI bool
|
||||
}
|
||||
|
||||
var _ verify.SignedEntity = &hashedRecordSignedEntity{}
|
||||
var _ verify.SignatureContent = &hashedRecordSignedEntity{}
|
||||
var _ verify.VerificationContent = &hashedRecordSignedEntity{}
|
||||
|
||||
func newHashedRecordSignedEntity(mfst *ocispecs.Manifest, isDHI bool) (verify.SignedEntity, error) {
|
||||
if len(mfst.Layers) == 0 {
|
||||
return nil, errors.New("no layers in manifest")
|
||||
}
|
||||
desc := mfst.Layers[0]
|
||||
sigStr, ok := desc.Annotations[annotationSignature]
|
||||
if !ok {
|
||||
return nil, errors.New("no signature annotation found")
|
||||
}
|
||||
sig, err := base64.StdEncoding.DecodeString(sigStr)
|
||||
if err != nil {
|
||||
return nil, errors.Wrapf(err, "decode signature")
|
||||
}
|
||||
dgstBytest, err := hex.DecodeString(desc.Digest.Hex())
|
||||
if err != nil {
|
||||
return nil, errors.Wrapf(err, "decode digest")
|
||||
}
|
||||
|
||||
hr := &hashedRecordSignedEntity{
|
||||
mfst: mfst,
|
||||
sig: &messageSignatureContent{
|
||||
digest: dgstBytest,
|
||||
signature: sig,
|
||||
digestAlgorithm: desc.Digest.Algorithm().String(),
|
||||
},
|
||||
isDHI: isDHI,
|
||||
}
|
||||
|
||||
if !isDHI {
|
||||
certData := desc.Annotations[annotationCert]
|
||||
if certData == "" {
|
||||
return nil, errors.Errorf("no certificate annotation found")
|
||||
}
|
||||
block, _ := pem.Decode([]byte(certData))
|
||||
if block == nil {
|
||||
return nil, errors.New("no PEM certificate found in annotation")
|
||||
}
|
||||
cert, err := x509.ParseCertificate(block.Bytes)
|
||||
if err != nil {
|
||||
return nil, errors.WithStack(err)
|
||||
}
|
||||
hr.cert = bundle.NewCertificate(cert)
|
||||
}
|
||||
|
||||
return hr, nil
|
||||
}
|
||||
|
||||
func (d *hashedRecordSignedEntity) HasInclusionPromise() bool {
|
||||
return true
|
||||
}
|
||||
|
||||
func (d *hashedRecordSignedEntity) HasInclusionProof() bool {
|
||||
return true
|
||||
}
|
||||
|
||||
func (d *hashedRecordSignedEntity) SignatureContent() (verify.SignatureContent, error) {
|
||||
return d, nil
|
||||
}
|
||||
|
||||
func (d *hashedRecordSignedEntity) Timestamps() ([][]byte, error) {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
func (d *hashedRecordSignedEntity) TlogEntries() ([]*tlog.Entry, error) {
|
||||
bundleBytes, ok := d.extractBundle()
|
||||
if !ok {
|
||||
return nil, nil
|
||||
}
|
||||
bundle, err := parseRekorBundle(bundleBytes)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "parse rekor bundle")
|
||||
}
|
||||
logIDRaw, err := hex.DecodeString(bundle.LogID)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "decode logID")
|
||||
}
|
||||
|
||||
tl, err := tlog.NewTlogEntry(&v1.TransparencyLogEntry{
|
||||
LogIndex: bundle.LogIndex,
|
||||
LogId: &v1common.LogId{KeyId: logIDRaw},
|
||||
IntegratedTime: bundle.IntegratedTime,
|
||||
CanonicalizedBody: bundle.Body,
|
||||
KindVersion: &v1.KindVersion{
|
||||
Kind: "hashedrekord",
|
||||
Version: "0.0.1",
|
||||
},
|
||||
InclusionPromise: &v1.InclusionPromise{
|
||||
SignedEntryTimestamp: bundle.Signature,
|
||||
},
|
||||
})
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "create tlog entry")
|
||||
}
|
||||
return []*tlog.Entry{tl}, nil
|
||||
}
|
||||
|
||||
func (d *hashedRecordSignedEntity) VerificationContent() (verify.VerificationContent, error) {
|
||||
return d, nil
|
||||
}
|
||||
|
||||
func (d *hashedRecordSignedEntity) Version() (string, error) {
|
||||
return "v0.1", nil
|
||||
}
|
||||
|
||||
func (d *hashedRecordSignedEntity) Signature() []byte {
|
||||
return d.sig.signature
|
||||
}
|
||||
|
||||
func (d *hashedRecordSignedEntity) EnvelopeContent() verify.EnvelopeContent {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (d *hashedRecordSignedEntity) MessageSignatureContent() verify.MessageSignatureContent {
|
||||
return d.sig
|
||||
}
|
||||
|
||||
type messageSignatureContent struct {
|
||||
digest []byte
|
||||
digestAlgorithm string
|
||||
signature []byte
|
||||
}
|
||||
|
||||
func (m *messageSignatureContent) Digest() []byte {
|
||||
return m.digest
|
||||
}
|
||||
|
||||
func (m *messageSignatureContent) DigestAlgorithm() string {
|
||||
return m.digestAlgorithm
|
||||
}
|
||||
|
||||
func (m *messageSignatureContent) Signature() []byte {
|
||||
return m.signature
|
||||
}
|
||||
|
||||
// CompareKey traces parameters and returns false.
|
||||
func (d *hashedRecordSignedEntity) CompareKey(k any, tm root.TrustedMaterial) bool {
|
||||
if d.isDHI {
|
||||
return (&bundle.PublicKey{}).CompareKey(k, tm)
|
||||
}
|
||||
if _, ok := k.(*x509.Certificate); !ok {
|
||||
return false
|
||||
}
|
||||
return d.cert.CompareKey(k, tm)
|
||||
}
|
||||
|
||||
func (d *hashedRecordSignedEntity) ValidAtTime(t time.Time, tm root.TrustedMaterial) bool {
|
||||
if d.isDHI {
|
||||
return (&bundle.PublicKey{}).ValidAtTime(t, tm)
|
||||
}
|
||||
return d.cert.ValidAtTime(t, tm)
|
||||
}
|
||||
|
||||
func (d *hashedRecordSignedEntity) Certificate() *x509.Certificate {
|
||||
if d.isDHI {
|
||||
return nil
|
||||
}
|
||||
return d.cert.Certificate()
|
||||
}
|
||||
|
||||
func (d *hashedRecordSignedEntity) PublicKey() verify.PublicKeyProvider {
|
||||
if d.isDHI {
|
||||
return bundle.PublicKey{}
|
||||
}
|
||||
return d.cert.PublicKey()
|
||||
}
|
||||
|
||||
func (d *hashedRecordSignedEntity) extractBundle() ([]byte, bool) {
|
||||
if len(d.mfst.Layers) == 0 {
|
||||
return nil, false
|
||||
}
|
||||
desc := d.mfst.Layers[0]
|
||||
bundleStr := desc.Annotations[annotationBundle]
|
||||
if bundleStr == "" {
|
||||
return nil, false
|
||||
}
|
||||
return []byte(bundleStr), true
|
||||
}
|
||||
|
||||
type rekorBundle struct {
|
||||
Body []byte
|
||||
Signature []byte
|
||||
LogID string
|
||||
IntegratedTime int64
|
||||
LogIndex int64
|
||||
}
|
||||
|
||||
func parseRekorBundle(bundleBytes []byte) (*rekorBundle, error) {
|
||||
var nb struct {
|
||||
Content struct {
|
||||
VerificationMaterial struct {
|
||||
TlogEntries []struct {
|
||||
LogIndex any `json:"logIndex"`
|
||||
LogID struct {
|
||||
KeyID string `json:"keyId"`
|
||||
} `json:"logId"`
|
||||
IntegratedTime any `json:"integratedTime"`
|
||||
InclusionPromise struct {
|
||||
SignedEntryTimestamp []byte `json:"signedEntryTimestamp"`
|
||||
} `json:"inclusionPromise"`
|
||||
CanonicalizedBody []byte `json:"canonicalizedBody"`
|
||||
} `json:"tlogEntries"`
|
||||
} `json:"verificationMaterial"`
|
||||
} `json:"content"`
|
||||
}
|
||||
if json.Unmarshal(bundleBytes, &nb) == nil && len(nb.Content.VerificationMaterial.TlogEntries) > 0 {
|
||||
e := nb.Content.VerificationMaterial.TlogEntries[0]
|
||||
if len(e.CanonicalizedBody) != 0 && len(e.InclusionPromise.SignedEntryTimestamp) != 0 {
|
||||
b := &rekorBundle{
|
||||
Body: e.CanonicalizedBody,
|
||||
Signature: e.InclusionPromise.SignedEntryTimestamp,
|
||||
LogID: strings.ToLower(e.LogID.KeyID),
|
||||
}
|
||||
|
||||
it, err1 := anyToInt64(e.IntegratedTime)
|
||||
if err1 == nil {
|
||||
b.IntegratedTime = it
|
||||
}
|
||||
li, err2 := anyToInt64(e.LogIndex)
|
||||
if err2 == nil {
|
||||
b.LogIndex = li
|
||||
}
|
||||
return b, nil
|
||||
}
|
||||
}
|
||||
|
||||
// Fallback to older cosign bundle shape
|
||||
var bundle struct {
|
||||
SignedEntryTimestamp []byte `json:"SignedEntryTimestamp"`
|
||||
Payload struct {
|
||||
Body []byte `json:"body"`
|
||||
LogID any `json:"logID"`
|
||||
IntegratedTime any `json:"integratedTime"`
|
||||
LogIndex any `json:"logIndex"`
|
||||
} `json:"Payload"`
|
||||
LogID any `json:"logID"`
|
||||
IntegratedTime any `json:"integratedTime"`
|
||||
LogIndex any `json:"logIndex"`
|
||||
}
|
||||
if err := json.Unmarshal(bundleBytes, &bundle); err != nil {
|
||||
return nil, errors.Wrap(err, "parse bundle json")
|
||||
}
|
||||
|
||||
b := &rekorBundle{
|
||||
Body: bundle.Payload.Body,
|
||||
Signature: bundle.SignedEntryTimestamp,
|
||||
}
|
||||
|
||||
// Prefer top-level fields when present; otherwise fall back to nested under Payload
|
||||
// Handle string/number types
|
||||
if s, ok := bundle.LogID.(string); ok {
|
||||
b.LogID = s
|
||||
}
|
||||
if b.LogID == "" {
|
||||
if s, ok := bundle.Payload.LogID.(string); ok {
|
||||
b.LogID = s
|
||||
}
|
||||
}
|
||||
if v, err := anyToInt64(bundle.IntegratedTime); err == nil {
|
||||
b.IntegratedTime = v
|
||||
}
|
||||
if b.IntegratedTime == 0 {
|
||||
if v, err := anyToInt64(bundle.Payload.IntegratedTime); err == nil {
|
||||
b.IntegratedTime = v
|
||||
}
|
||||
}
|
||||
if v, err := anyToInt64(bundle.LogIndex); err == nil {
|
||||
b.LogIndex = v
|
||||
}
|
||||
if b.LogIndex == 0 {
|
||||
if v, err := anyToInt64(bundle.Payload.LogIndex); err == nil {
|
||||
b.LogIndex = v
|
||||
}
|
||||
}
|
||||
return b, nil
|
||||
}
|
||||
|
||||
func anyToInt64(v any) (int64, error) {
|
||||
switch t := v.(type) {
|
||||
case nil:
|
||||
return 0, errors.New("nil")
|
||||
case float64:
|
||||
return int64(t), nil
|
||||
case json.Number:
|
||||
return t.Int64()
|
||||
case string:
|
||||
if t == "" {
|
||||
return 0, errors.New("empty")
|
||||
}
|
||||
return strconv.ParseInt(t, 10, 64)
|
||||
case int64:
|
||||
return t, nil
|
||||
case int:
|
||||
return int64(t), nil
|
||||
default:
|
||||
return 0, errors.Errorf("unsupported type %T", v)
|
||||
}
|
||||
}
|
||||
+45
@@ -0,0 +1,45 @@
|
||||
package image
|
||||
|
||||
import (
|
||||
"context"
|
||||
"strings"
|
||||
|
||||
"github.com/containerd/containerd/v2/core/content"
|
||||
"github.com/containerd/containerd/v2/core/remotes"
|
||||
digest "github.com/opencontainers/go-digest"
|
||||
ocispecs "github.com/opencontainers/image-spec/specs-go/v1"
|
||||
)
|
||||
|
||||
type dhiKey struct{}
|
||||
|
||||
func isDHIIndex(idx ocispecs.Index) bool {
|
||||
for _, desc := range idx.Manifests {
|
||||
if buildid, ok := desc.Annotations["com.docker.dhi.build.id"]; !ok || buildid == "" {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return strings.HasPrefix(idx.Annotations["org.opencontainers.image.title"], "dhi/")
|
||||
}
|
||||
|
||||
func contextWithDHI(ctx context.Context) context.Context {
|
||||
return context.WithValue(ctx, dhiKey{}, struct{}{})
|
||||
}
|
||||
|
||||
func IsDHI(ctx context.Context) bool {
|
||||
_, ok := ctx.Value(dhiKey{}).(struct{})
|
||||
return ok
|
||||
}
|
||||
|
||||
type dhiReferrersProvider struct {
|
||||
ReferrersProvider
|
||||
}
|
||||
|
||||
func (d *dhiReferrersProvider) FetchReferrers(ctx context.Context, dgst digest.Digest, opts ...remotes.FetchReferrersOpt) ([]ocispecs.Descriptor, error) {
|
||||
ctx = contextWithDHI(ctx)
|
||||
return d.ReferrersProvider.FetchReferrers(ctx, dgst, opts...)
|
||||
}
|
||||
|
||||
func (d *dhiReferrersProvider) ReaderAt(ctx context.Context, desc ocispecs.Descriptor) (content.ReaderAt, error) {
|
||||
ctx = contextWithDHI(ctx)
|
||||
return d.ReferrersProvider.ReaderAt(ctx, desc)
|
||||
}
|
||||
+239
@@ -0,0 +1,239 @@
|
||||
package image
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"slices"
|
||||
"sort"
|
||||
"sync"
|
||||
|
||||
"github.com/containerd/containerd/v2/core/content"
|
||||
"github.com/containerd/containerd/v2/core/images"
|
||||
"github.com/containerd/containerd/v2/core/remotes"
|
||||
cerrdefs "github.com/containerd/errdefs"
|
||||
"github.com/containerd/platforms"
|
||||
digest "github.com/opencontainers/go-digest"
|
||||
ocispecs "github.com/opencontainers/image-spec/specs-go/v1"
|
||||
"github.com/pkg/errors"
|
||||
)
|
||||
|
||||
type ReferrersProvider interface {
|
||||
content.Provider
|
||||
remotes.ReferrersFetcher
|
||||
}
|
||||
|
||||
const (
|
||||
AnnotationDockerReferenceDigest = "vnd.docker.reference.digest"
|
||||
AnnotationDockerReferenceType = "vnd.docker.reference.type"
|
||||
AttestationManifestType = "attestation-manifest"
|
||||
)
|
||||
|
||||
const (
|
||||
ArtifactTypeCosignSignature = "application/vnd.dev.cosign.artifact.sig.v1+json"
|
||||
ArtifactTypeSigstoreBundle = "application/vnd.dev.sigstore.bundle.v0.3+json"
|
||||
ArtifactTypeInTotoJSON = "application/vnd.in-toto+json"
|
||||
MediaTypeCosignSimpleSigning = "application/vnd.dev.cosign.simplesigning.v1+json"
|
||||
SLSAProvenancePredicateType02 = "https://slsa.dev/provenance/v0.2"
|
||||
SLSAProvenancePredicateType1 = "https://slsa.dev/provenance/v1"
|
||||
)
|
||||
|
||||
func resolveImageManifest(idx ocispecs.Index, platform ocispecs.Platform) (ocispecs.Descriptor, error) {
|
||||
pMatcher := platforms.Only(platform)
|
||||
|
||||
var descs []ocispecs.Descriptor
|
||||
for _, d := range idx.Manifests {
|
||||
// TODO: confirm handling of nested indexes
|
||||
if !images.IsManifestType(d.MediaType) {
|
||||
continue
|
||||
}
|
||||
if d.Platform == nil || pMatcher.Match(*d.Platform) {
|
||||
descs = append(descs, d)
|
||||
}
|
||||
}
|
||||
|
||||
sort.SliceStable(descs, func(i, j int) bool {
|
||||
if descs[i].Platform == nil {
|
||||
return false
|
||||
}
|
||||
if descs[j].Platform == nil {
|
||||
return true
|
||||
}
|
||||
return pMatcher.Less(*descs[i].Platform, *descs[j].Platform)
|
||||
})
|
||||
|
||||
if len(descs) == 0 {
|
||||
return ocispecs.Descriptor{}, errors.Wrapf(cerrdefs.ErrNotFound, "no manifest for platform %+v", platforms.FormatAll(platform))
|
||||
}
|
||||
return descs[0], nil
|
||||
}
|
||||
|
||||
type Manifest struct {
|
||||
ocispecs.Descriptor
|
||||
mu sync.Mutex
|
||||
manifest *ocispecs.Manifest
|
||||
data []byte
|
||||
}
|
||||
|
||||
type SignatureChain struct {
|
||||
ImageManifest *Manifest
|
||||
AttestationManifest *Manifest
|
||||
SignatureManifest *Manifest
|
||||
Provider content.Provider
|
||||
DHI bool
|
||||
}
|
||||
|
||||
func (sc *SignatureChain) ManifestBytes(ctx context.Context, m *Manifest) ([]byte, error) {
|
||||
m.mu.Lock()
|
||||
defer m.mu.Unlock()
|
||||
if m.data != nil {
|
||||
return m.data, nil
|
||||
}
|
||||
dt, err := ReadBlob(ctx, sc.Provider, m.Descriptor)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
m.data = dt
|
||||
return dt, nil
|
||||
}
|
||||
|
||||
func (sc *SignatureChain) OCIManifest(ctx context.Context, m *Manifest) (*ocispecs.Manifest, error) {
|
||||
m.mu.Lock()
|
||||
if m.manifest != nil {
|
||||
m.mu.Unlock()
|
||||
return m.manifest, nil
|
||||
}
|
||||
m.mu.Unlock()
|
||||
dt, err := sc.ManifestBytes(ctx, m)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
var manifest ocispecs.Manifest
|
||||
if err := json.Unmarshal(dt, &manifest); err != nil {
|
||||
return nil, errors.Wrapf(err, "unmarshaling manifest %s", m.Digest)
|
||||
}
|
||||
m.mu.Lock()
|
||||
m.manifest = &manifest
|
||||
m.mu.Unlock()
|
||||
return &manifest, nil
|
||||
}
|
||||
|
||||
func ResolveSignatureChain(ctx context.Context, provider ReferrersProvider, desc ocispecs.Descriptor, platform *ocispecs.Platform) (*SignatureChain, error) {
|
||||
if desc.MediaType != ocispecs.MediaTypeImageIndex {
|
||||
return nil, errors.Errorf("expected image index descriptor, got %s", desc.MediaType)
|
||||
}
|
||||
|
||||
dt, err := ReadBlob(ctx, provider, desc)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
var index ocispecs.Index
|
||||
if err := json.Unmarshal(dt, &index); err != nil {
|
||||
return nil, errors.Wrapf(err, "unmarshaling image index")
|
||||
}
|
||||
|
||||
isDHI := isDHIIndex(index)
|
||||
|
||||
if platform == nil {
|
||||
p := platforms.Normalize(platforms.DefaultSpec())
|
||||
platform = &p
|
||||
}
|
||||
|
||||
manifestDesc, err := resolveImageManifest(index, *platform)
|
||||
if err != nil {
|
||||
return nil, errors.Wrapf(err, "resolving image manifest for platform %+v", platform)
|
||||
}
|
||||
|
||||
var attestationDesc *ocispecs.Descriptor
|
||||
if isDHI {
|
||||
provider = &dhiReferrersProvider{ReferrersProvider: provider}
|
||||
allRefs, err := provider.FetchReferrers(ctx, manifestDesc.Digest,
|
||||
remotes.WithReferrerArtifactTypes(ArtifactTypeInTotoJSON),
|
||||
remotes.WithReferrerQueryFilter("predicateType", SLSAProvenancePredicateType02),
|
||||
remotes.WithReferrerQueryFilter("predicateType", SLSAProvenancePredicateType1),
|
||||
)
|
||||
if err != nil {
|
||||
return nil, errors.Wrapf(err, "fetching referrers for manifest %s", manifestDesc.Digest)
|
||||
}
|
||||
refs := make([]ocispecs.Descriptor, 0, len(allRefs))
|
||||
for _, r := range allRefs {
|
||||
if r.ArtifactType == ArtifactTypeInTotoJSON {
|
||||
switch r.Annotations["in-toto.io/predicate-type"] {
|
||||
case SLSAProvenancePredicateType02, SLSAProvenancePredicateType1:
|
||||
refs = append(refs, r)
|
||||
}
|
||||
}
|
||||
}
|
||||
if len(refs) == 0 {
|
||||
return nil, errors.Errorf("no attestation referrers found for DHI manifest %s", manifestDesc.Digest)
|
||||
}
|
||||
attestationDesc = &refs[0]
|
||||
} else {
|
||||
for _, d := range index.Manifests {
|
||||
if d.Annotations[AnnotationDockerReferenceType] == AttestationManifestType && d.Annotations[AnnotationDockerReferenceDigest] == manifestDesc.Digest.String() {
|
||||
attestationDesc = &d
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
sh := &SignatureChain{
|
||||
ImageManifest: &Manifest{
|
||||
Descriptor: manifestDesc,
|
||||
},
|
||||
Provider: provider,
|
||||
DHI: isDHI,
|
||||
}
|
||||
|
||||
if attestationDesc == nil {
|
||||
return sh, nil
|
||||
}
|
||||
|
||||
sh.AttestationManifest = &Manifest{
|
||||
Descriptor: *attestationDesc,
|
||||
}
|
||||
|
||||
// currently not setting WithReferrerArtifactTypes in here as some registries(e.g. aws) don't know how to filter two types at once.
|
||||
allRefs, err := provider.FetchReferrers(ctx, attestationDesc.Digest)
|
||||
if err != nil {
|
||||
return nil, errors.Wrapf(err, "fetching referrers for attestation manifest %s", attestationDesc.Digest)
|
||||
}
|
||||
|
||||
refs := make([]ocispecs.Descriptor, 0, len(allRefs))
|
||||
for _, r := range allRefs {
|
||||
if r.ArtifactType == ArtifactTypeSigstoreBundle || r.ArtifactType == ArtifactTypeCosignSignature {
|
||||
refs = append(refs, r)
|
||||
}
|
||||
}
|
||||
|
||||
if len(refs) == 0 {
|
||||
return sh, nil
|
||||
}
|
||||
|
||||
// only allowing one signature manifest for now
|
||||
// if multiple are found, prefer bundle format
|
||||
slices.SortStableFunc(refs, func(a, b ocispecs.Descriptor) int {
|
||||
aIsBundle := a.ArtifactType == ArtifactTypeSigstoreBundle
|
||||
bIsBundle := b.ArtifactType == ArtifactTypeSigstoreBundle
|
||||
if aIsBundle && !bIsBundle {
|
||||
return -1
|
||||
} else if !aIsBundle && bIsBundle {
|
||||
return 1
|
||||
}
|
||||
return 0
|
||||
})
|
||||
|
||||
sh.SignatureManifest = &Manifest{
|
||||
Descriptor: refs[0],
|
||||
}
|
||||
return sh, nil
|
||||
}
|
||||
|
||||
func ReadBlob(ctx context.Context, provider content.Provider, desc ocispecs.Descriptor) ([]byte, error) {
|
||||
dt, err := content.ReadBlob(ctx, provider, desc)
|
||||
if err != nil {
|
||||
return nil, errors.Wrapf(err, "reading blob %s", desc.Digest)
|
||||
}
|
||||
if desc.Digest != digest.FromBytes(dt) {
|
||||
return nil, errors.Wrapf(err, "digest mismatch for blob %s", desc.Digest)
|
||||
}
|
||||
return dt, nil
|
||||
}
|
||||
+4
@@ -0,0 +1,4 @@
|
||||
-----BEGIN PUBLIC KEY-----
|
||||
MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEKdROmntRJFBrOJOQF5ww6gDBJqGm
|
||||
Fxa4333s1KsL9ISjtmRzGNih9lNRsqfRVjgFgJIdL6EQ9dohdanvn7r2cg==
|
||||
-----END PUBLIC KEY-----
|
||||
+67
@@ -0,0 +1,67 @@
|
||||
package dhi
|
||||
|
||||
import (
|
||||
_ "embed"
|
||||
"time"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
"github.com/sigstore/sigstore-go/pkg/root"
|
||||
"github.com/sigstore/sigstore/pkg/cryptoutils"
|
||||
"github.com/sigstore/sigstore/pkg/signature"
|
||||
)
|
||||
|
||||
//go:embed dhi.pub
|
||||
var pubkeyPEM string
|
||||
|
||||
// This may need to be updated if key rotation occurs.
|
||||
const dhiEpoch = 1743595200 // 2025-04-02
|
||||
|
||||
func TrustedRoot(fulcioTrustedRoot root.TrustedMaterial) (root.TrustedMaterial, error) {
|
||||
pubKey, err := cryptoutils.UnmarshalPEMToPublicKey([]byte(pubkeyPEM))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
v, err := signature.LoadVerifierWithOpts(pubKey)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "loading DHI public key verifier")
|
||||
}
|
||||
return &dhiTrustedMaterial{
|
||||
dhiVerifier: &dhiVerifier{v},
|
||||
fulcio: fulcioTrustedRoot,
|
||||
}, nil
|
||||
}
|
||||
|
||||
type dhiTrustedMaterial struct {
|
||||
*dhiVerifier
|
||||
fulcio root.TrustedMaterial
|
||||
}
|
||||
|
||||
var _ root.TrustedMaterial = &dhiTrustedMaterial{}
|
||||
|
||||
func (d *dhiTrustedMaterial) PublicKeyVerifier(_ string) (root.TimeConstrainedVerifier, error) {
|
||||
return d.dhiVerifier, nil
|
||||
}
|
||||
|
||||
func (d *dhiTrustedMaterial) TimestampingAuthorities() []root.TimestampingAuthority {
|
||||
return d.fulcio.TimestampingAuthorities()
|
||||
}
|
||||
|
||||
func (d *dhiTrustedMaterial) FulcioCertificateAuthorities() []root.CertificateAuthority {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (d *dhiTrustedMaterial) RekorLogs() map[string]*root.TransparencyLog {
|
||||
return d.fulcio.RekorLogs()
|
||||
}
|
||||
|
||||
func (d *dhiTrustedMaterial) CTLogs() map[string]*root.TransparencyLog {
|
||||
return nil
|
||||
}
|
||||
|
||||
type dhiVerifier struct {
|
||||
signature.Verifier
|
||||
}
|
||||
|
||||
func (d *dhiVerifier) ValidAtTime(t time.Time) bool {
|
||||
return t.Unix() >= dhiEpoch
|
||||
}
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
package roots
|
||||
|
||||
import "embed"
|
||||
|
||||
//go:embed tuf-root
|
||||
var EmbeddedTUF embed.FS
|
||||
+343
@@ -0,0 +1,343 @@
|
||||
package roots
|
||||
|
||||
import (
|
||||
"context"
|
||||
"embed"
|
||||
"io"
|
||||
"io/fs"
|
||||
"net/url"
|
||||
"os"
|
||||
"path"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/gofrs/flock"
|
||||
"github.com/pkg/errors"
|
||||
"github.com/sigstore/sigstore-go/pkg/root"
|
||||
"github.com/sigstore/sigstore-go/pkg/tuf"
|
||||
"github.com/theupdateframework/go-tuf/v2/metadata"
|
||||
"github.com/theupdateframework/go-tuf/v2/metadata/fetcher"
|
||||
)
|
||||
|
||||
type SigstoreRootsConfig struct {
|
||||
CachePath string
|
||||
UpdateInterval time.Duration
|
||||
RequireOnline bool
|
||||
}
|
||||
|
||||
type TrustProvider struct {
|
||||
mu sync.RWMutex
|
||||
config SigstoreRootsConfig
|
||||
client *tuf.Client
|
||||
fetcher *airgappedFetcher
|
||||
|
||||
status Status
|
||||
}
|
||||
|
||||
type Status struct {
|
||||
Error error `json:"error,omitempty"`
|
||||
LastUpdated *time.Time `json:"lastUpdated,omitempty"`
|
||||
}
|
||||
|
||||
const (
|
||||
trustedRootFilename = "trusted_root.json"
|
||||
)
|
||||
|
||||
func NewTrustProvider(cfg SigstoreRootsConfig) (*TrustProvider, error) {
|
||||
if cfg.CachePath == "" {
|
||||
return nil, errors.Errorf("cache path must be provided for trust provider")
|
||||
}
|
||||
def := tuf.DefaultOptions()
|
||||
cacheDir := filepath.Join(cfg.CachePath, tuf.URLToPath(def.RepositoryBaseURL))
|
||||
if err := os.MkdirAll(cacheDir, 0o755); err != nil {
|
||||
return nil, errors.Wrap(err, "creating cache directory for trust provider")
|
||||
}
|
||||
|
||||
tp := &TrustProvider{
|
||||
config: cfg,
|
||||
}
|
||||
|
||||
unlock, err := tp.lock()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer unlock()
|
||||
|
||||
root, err := os.OpenRoot(cacheDir)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "opening cache directory for trust provider")
|
||||
}
|
||||
defer root.Close()
|
||||
if _, err := root.Lstat("root.json"); err != nil {
|
||||
if !os.IsNotExist(err) {
|
||||
return nil, errors.Wrap(err, "statting root.json in cache directory for trust provider")
|
||||
}
|
||||
if err := copyEmbeddedRoot(EmbeddedTUF, root); err != nil {
|
||||
return nil, errors.Wrap(err, "initializing cache directory for trust provider with embedded root")
|
||||
}
|
||||
}
|
||||
|
||||
agf := &airgappedFetcher{
|
||||
baseURL: def.RepositoryBaseURL,
|
||||
cacheDir: cacheDir,
|
||||
onlineFetcher: fetcher.NewDefaultFetcher(),
|
||||
isOnline: true,
|
||||
}
|
||||
tp.fetcher = agf
|
||||
|
||||
tufOpts, err := tp.tufClientOpts()
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "creating TUF client options for trust provider")
|
||||
}
|
||||
|
||||
c, err := tuf.New(tufOpts)
|
||||
if err != nil {
|
||||
// try again with airgapped fetcher
|
||||
// this can still fail if the last root or timestamps file has expired
|
||||
|
||||
agf.isOnline = false
|
||||
tufOpts, err := tp.tufClientOpts()
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "creating TUF client options for trust provider with airgapped fetcher")
|
||||
}
|
||||
c, err = tuf.New(tufOpts)
|
||||
if err != nil {
|
||||
return nil, errors.WithStack(err)
|
||||
}
|
||||
}
|
||||
tp.client = c
|
||||
agf.isOnline = true
|
||||
|
||||
go tp.update()
|
||||
|
||||
if cfg.UpdateInterval > 0 {
|
||||
go func() {
|
||||
ticker := time.NewTicker(cfg.UpdateInterval)
|
||||
defer ticker.Stop()
|
||||
// TODO: stop condition
|
||||
for range ticker.C {
|
||||
tp.update()
|
||||
}
|
||||
}()
|
||||
}
|
||||
|
||||
return tp, nil
|
||||
}
|
||||
|
||||
func (tp *TrustProvider) tufClientOpts() (*tuf.Options, error) {
|
||||
def := tuf.DefaultOptions()
|
||||
cacheDir := filepath.Join(tp.config.CachePath, tuf.URLToPath(def.RepositoryBaseURL))
|
||||
root, err := os.OpenRoot(cacheDir)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "opening cache directory for trust provider")
|
||||
}
|
||||
defer root.Close()
|
||||
|
||||
dt, err := EmbeddedTUF.ReadFile("tuf-root/root.json")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
def.Root = dt
|
||||
def.CachePath = tp.config.CachePath
|
||||
def.ForceCache = !tp.config.RequireOnline
|
||||
def.Fetcher = tp.fetcher
|
||||
return def, nil
|
||||
}
|
||||
|
||||
func (tp *TrustProvider) update() (err error) {
|
||||
defer func() {
|
||||
if err != nil {
|
||||
tp.mu.Lock()
|
||||
tp.status.Error = err
|
||||
tp.mu.Unlock()
|
||||
}
|
||||
}()
|
||||
|
||||
unlock, err := tp.lock()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer unlock()
|
||||
tufOpts, err := tp.tufClientOpts()
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "creating TUF client options for trust provider")
|
||||
}
|
||||
c, err := tuf.New(tufOpts)
|
||||
if err != nil {
|
||||
return errors.WithStack(err)
|
||||
}
|
||||
err = c.Refresh()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
tp.mu.Lock()
|
||||
defer tp.mu.Unlock()
|
||||
now := time.Now().UTC()
|
||||
tp.status = Status{LastUpdated: &now}
|
||||
tp.client = c
|
||||
return nil
|
||||
}
|
||||
|
||||
func (tp *TrustProvider) wait(ctx context.Context) (*tuf.Client, error) {
|
||||
first := true
|
||||
errCh := make(chan error, 1)
|
||||
for {
|
||||
tp.mu.RLock()
|
||||
status := tp.status
|
||||
client := tp.client
|
||||
tp.mu.RUnlock()
|
||||
if status.LastUpdated != nil && status.Error == nil {
|
||||
return client, nil
|
||||
}
|
||||
// try update if we are in error from some old reason that might be resolved now
|
||||
if status.Error != nil && first {
|
||||
go func() {
|
||||
errCh <- tp.update()
|
||||
}()
|
||||
first = false
|
||||
}
|
||||
select {
|
||||
case err := <-errCh:
|
||||
if err == nil {
|
||||
continue
|
||||
}
|
||||
return nil, err
|
||||
case <-ctx.Done():
|
||||
return nil, context.Cause(ctx)
|
||||
case <-time.After(100 * time.Millisecond):
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (tp *TrustProvider) lock() (func() error, error) {
|
||||
lockPath := path.Join(tp.config.CachePath, ".lock")
|
||||
fileLock := flock.New(lockPath)
|
||||
if err := fileLock.Lock(); err != nil {
|
||||
return nil, errors.Wrap(err, "acquiring lock on trust provider cache")
|
||||
}
|
||||
return fileLock.Unlock, nil
|
||||
}
|
||||
|
||||
func (tp *TrustProvider) TrustedRoot(ctx context.Context) (*root.TrustedRoot, Status, error) {
|
||||
ctx, cnclFn := context.WithCancelCause(ctx)
|
||||
ctx, _ = context.WithTimeoutCause(ctx, time.Second*5, errors.WithStack(context.DeadlineExceeded)) //nolint:govet
|
||||
defer cnclFn(errors.WithStack(context.Canceled))
|
||||
|
||||
var st Status
|
||||
client, err := tp.wait(ctx)
|
||||
if err != nil { // return indication of last refresh error? TODO(@tonistiigi) does this make GetTarget fail as well and separate instance of client is needed for optional refresh?
|
||||
st.Error = err
|
||||
tp.mu.RLock()
|
||||
client = tp.client
|
||||
tp.mu.RUnlock()
|
||||
}
|
||||
|
||||
jsonBytes, err := client.GetTarget(trustedRootFilename)
|
||||
if err != nil {
|
||||
return nil, st, err
|
||||
}
|
||||
tr, err := root.NewTrustedRootFromJSON(jsonBytes)
|
||||
return tr, st, err
|
||||
}
|
||||
|
||||
type airgappedFetcher struct {
|
||||
baseURL string
|
||||
cacheDir string
|
||||
onlineFetcher fetcher.Fetcher
|
||||
isOnline bool
|
||||
}
|
||||
|
||||
func (f *airgappedFetcher) DownloadFile(urlPath string, maxLength int64, dur time.Duration) ([]byte, error) {
|
||||
if f.isOnline {
|
||||
dt, err := f.onlineFetcher.DownloadFile(urlPath, maxLength, dur)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
// save root chain to cache so that it can be reverified while offline
|
||||
u, err := url.Parse(urlPath)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "parsing URL in trust provider fetcher")
|
||||
}
|
||||
cache, err := os.OpenRoot(f.cacheDir)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "opening cache directory for trust provider")
|
||||
}
|
||||
defer cache.Close()
|
||||
if strings.HasSuffix(u.Path, ".root.json") {
|
||||
base := path.Base(u.Path)
|
||||
if err := cache.MkdirAll("roots", 0o755); err != nil {
|
||||
return nil, errors.Wrap(err, "creating roots directory in trust provider cache")
|
||||
}
|
||||
if err := cache.WriteFile(path.Join("roots", base), dt, 0o644); err != nil {
|
||||
return nil, errors.Wrap(err, "caching root file in trust provider cache")
|
||||
}
|
||||
}
|
||||
return dt, nil
|
||||
}
|
||||
const timestampFilename = "timestamp.json"
|
||||
if urlPath == f.baseURL+"/"+timestampFilename {
|
||||
cache, err := os.OpenRoot(f.cacheDir)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "opening cache directory for trust provider")
|
||||
}
|
||||
defer cache.Close()
|
||||
if dt, err := cache.ReadFile(timestampFilename); err == nil {
|
||||
return dt, nil
|
||||
}
|
||||
}
|
||||
if strings.HasSuffix(urlPath, ".root.json") {
|
||||
u, err := url.Parse(urlPath)
|
||||
if err == nil {
|
||||
base := path.Base(u.Path)
|
||||
if urlPath == f.baseURL+"/"+base && strings.HasSuffix(base, ".root.json") {
|
||||
cache, err := os.OpenRoot(f.cacheDir)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "opening cache directory for trust provider")
|
||||
}
|
||||
defer cache.Close()
|
||||
dt, err := cache.ReadFile("roots/" + base)
|
||||
if err == nil {
|
||||
return dt, nil
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil, &metadata.ErrDownloadHTTP{
|
||||
StatusCode: 404,
|
||||
}
|
||||
}
|
||||
|
||||
func copyEmbeddedRoot(src embed.FS, dest *os.Root) error {
|
||||
subFS, err := fs.Sub(src, "tuf-root")
|
||||
if err != nil {
|
||||
return errors.WithStack(err)
|
||||
}
|
||||
return fs.WalkDir(subFS, ".", func(p string, d fs.DirEntry, err error) error {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if d.IsDir() {
|
||||
return dest.MkdirAll(p, 0o755)
|
||||
}
|
||||
in, err := subFS.Open(p)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer in.Close()
|
||||
|
||||
if err := dest.MkdirAll(path.Dir(p), 0o755); err != nil {
|
||||
return err
|
||||
}
|
||||
out, err := dest.OpenFile(p, os.O_CREATE|os.O_WRONLY|os.O_TRUNC, 0o644)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if _, err := io.Copy(out, in); err != nil {
|
||||
out.Close()
|
||||
return err
|
||||
}
|
||||
return out.Close()
|
||||
})
|
||||
}
|
||||
+145
@@ -0,0 +1,145 @@
|
||||
{
|
||||
"signatures": [
|
||||
{
|
||||
"keyid": "6f260089d5923daf20166ca657c543af618346ab971884a99962b01988bbe0c3",
|
||||
"sig": ""
|
||||
},
|
||||
{
|
||||
"keyid": "e71a54d543835ba86adad9460379c7641fb8726d164ea766801a1c522aba7ea2",
|
||||
"sig": "3045022100bbddd464f8066ceb88ba787375c12cd6330680e08c2910703e6538c71cc79ad202205190b06e4537fe961b3ef81fe68edcd0089c19f919afed423b9aafd700641153"
|
||||
},
|
||||
{
|
||||
"keyid": "22f4caec6d8e6f9555af66b3d4c3cb06a3bb23fdc7e39c916c61f462e6f52b06",
|
||||
"sig": "3044022069306cd5257f732a740c1afe60a8e433c5de58eafeadbe99c336c9c71d198cf802200d773953ae7dbc48d3e5bad9a6f64bafff196b7e2ad4a52a19519367d47dc042"
|
||||
},
|
||||
{
|
||||
"keyid": "61643838125b440b40db6942f5cb5a31c0dc04368316eb2aaa58b95904a58222",
|
||||
"sig": "304402204d21a2ec80df66e61f6fe2912951dc47df836036f8c0ab10816d375e71dbf79e0220547adce1afdf04e6794efa203dd5264c6f7e0ef78e57fe934b0d26cb994eec76"
|
||||
},
|
||||
{
|
||||
"keyid": "a687e5bf4fab82b0ee58d46e05c9535145a2c9afb458f43d42b45ca0fdce2a70",
|
||||
"sig": "3045022060826496557144eb1649893ed5f6f4ea54536feb0ca82f8b89ae641be39743e5022100ad7118b5e9d4837326206e412fc6da2999925d110328a7c166b06c624336c93f"
|
||||
},
|
||||
{
|
||||
"keyid": "183e64f37670dc13ca0d28995a3053f3740954ddce44321a41e46534cf44e632",
|
||||
"sig": "3046022100d8179439c2e73eb0c1733abee7faf832dcaea7263edcb4919891c3a247f05923022100e1a437e0797e803f9b72dc9d2d92155b0a2270c24efdd5f4b3a5d8f0b0f431a7"
|
||||
}
|
||||
],
|
||||
"signed": {
|
||||
"_type": "root",
|
||||
"consistent_snapshot": true,
|
||||
"expires": "2026-01-22T13:05:59Z",
|
||||
"keys": {
|
||||
"0c87432c3bf09fd99189fdc32fa5eaedf4e4a5fac7bab73fa04a2e0fc64af6f5": {
|
||||
"keyid_hash_algorithms": [
|
||||
"sha256",
|
||||
"sha512"
|
||||
],
|
||||
"keytype": "ecdsa",
|
||||
"keyval": {
|
||||
"public": "-----BEGIN PUBLIC KEY-----\nMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEWRiGr5+j+3J5SsH+Ztr5nE2H2wO7\nBV+nO3s93gLca18qTOzHY1oWyAGDykMSsGTUBSt9D+An0KfKsD2mfSM42Q==\n-----END PUBLIC KEY-----\n"
|
||||
},
|
||||
"scheme": "ecdsa-sha2-nistp256",
|
||||
"x-tuf-on-ci-online-uri": "gcpkms:projects/sigstore-root-signing/locations/global/keyRings/root/cryptoKeys/timestamp/cryptoKeyVersions/1"
|
||||
},
|
||||
"183e64f37670dc13ca0d28995a3053f3740954ddce44321a41e46534cf44e632": {
|
||||
"keytype": "ecdsa",
|
||||
"keyval": {
|
||||
"public": "-----BEGIN PUBLIC KEY-----\nMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEMxpPOJCIZ5otG4106fGJseEQi3V9\npkMYQ4uyV9Tj1M7WHXIyLG+jkfvuG0glQ1JZbRZZBV3gAR4sojdGHISeow==\n-----END PUBLIC KEY-----\n"
|
||||
},
|
||||
"scheme": "ecdsa-sha2-nistp256",
|
||||
"x-tuf-on-ci-keyowner": "@lance"
|
||||
},
|
||||
"22f4caec6d8e6f9555af66b3d4c3cb06a3bb23fdc7e39c916c61f462e6f52b06": {
|
||||
"keyid_hash_algorithms": [
|
||||
"sha256",
|
||||
"sha512"
|
||||
],
|
||||
"keytype": "ecdsa",
|
||||
"keyval": {
|
||||
"public": "-----BEGIN PUBLIC KEY-----\nMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEzBzVOmHCPojMVLSI364WiiV8NPrD\n6IgRxVliskz/v+y3JER5mcVGcONliDcWMC5J2lfHmjPNPhb4H7xm8LzfSA==\n-----END PUBLIC KEY-----\n"
|
||||
},
|
||||
"scheme": "ecdsa-sha2-nistp256",
|
||||
"x-tuf-on-ci-keyowner": "@santiagotorres"
|
||||
},
|
||||
"61643838125b440b40db6942f5cb5a31c0dc04368316eb2aaa58b95904a58222": {
|
||||
"keyid_hash_algorithms": [
|
||||
"sha256",
|
||||
"sha512"
|
||||
],
|
||||
"keytype": "ecdsa",
|
||||
"keyval": {
|
||||
"public": "-----BEGIN PUBLIC KEY-----\nMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEinikSsAQmYkNeH5eYq/CnIzLaacO\nxlSaawQDOwqKy/tCqxq5xxPSJc21K4WIhs9GyOkKfzueY3GILzcMJZ4cWw==\n-----END PUBLIC KEY-----\n"
|
||||
},
|
||||
"scheme": "ecdsa-sha2-nistp256",
|
||||
"x-tuf-on-ci-keyowner": "@bobcallaway"
|
||||
},
|
||||
"a687e5bf4fab82b0ee58d46e05c9535145a2c9afb458f43d42b45ca0fdce2a70": {
|
||||
"keyid_hash_algorithms": [
|
||||
"sha256",
|
||||
"sha512"
|
||||
],
|
||||
"keytype": "ecdsa",
|
||||
"keyval": {
|
||||
"public": "-----BEGIN PUBLIC KEY-----\nMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAE0ghrh92Lw1Yr3idGV5WqCtMDB8Cx\n+D8hdC4w2ZLNIplVRoVGLskYa3gheMyOjiJ8kPi15aQ2//7P+oj7UvJPGw==\n-----END PUBLIC KEY-----\n"
|
||||
},
|
||||
"scheme": "ecdsa-sha2-nistp256",
|
||||
"x-tuf-on-ci-keyowner": "@joshuagl"
|
||||
},
|
||||
"e71a54d543835ba86adad9460379c7641fb8726d164ea766801a1c522aba7ea2": {
|
||||
"keyid_hash_algorithms": [
|
||||
"sha256",
|
||||
"sha512"
|
||||
],
|
||||
"keytype": "ecdsa",
|
||||
"keyval": {
|
||||
"public": "-----BEGIN PUBLIC KEY-----\nMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEEXsz3SZXFb8jMV42j6pJlyjbjR8K\nN3Bwocexq6LMIb5qsWKOQvLN16NUefLc4HswOoumRsVVaajSpQS6fobkRw==\n-----END PUBLIC KEY-----\n"
|
||||
},
|
||||
"scheme": "ecdsa-sha2-nistp256",
|
||||
"x-tuf-on-ci-keyowner": "@mnm678"
|
||||
}
|
||||
},
|
||||
"roles": {
|
||||
"root": {
|
||||
"keyids": [
|
||||
"e71a54d543835ba86adad9460379c7641fb8726d164ea766801a1c522aba7ea2",
|
||||
"22f4caec6d8e6f9555af66b3d4c3cb06a3bb23fdc7e39c916c61f462e6f52b06",
|
||||
"61643838125b440b40db6942f5cb5a31c0dc04368316eb2aaa58b95904a58222",
|
||||
"a687e5bf4fab82b0ee58d46e05c9535145a2c9afb458f43d42b45ca0fdce2a70",
|
||||
"183e64f37670dc13ca0d28995a3053f3740954ddce44321a41e46534cf44e632"
|
||||
],
|
||||
"threshold": 3
|
||||
},
|
||||
"snapshot": {
|
||||
"keyids": [
|
||||
"0c87432c3bf09fd99189fdc32fa5eaedf4e4a5fac7bab73fa04a2e0fc64af6f5"
|
||||
],
|
||||
"threshold": 1,
|
||||
"x-tuf-on-ci-expiry-period": 3650,
|
||||
"x-tuf-on-ci-signing-period": 365
|
||||
},
|
||||
"targets": {
|
||||
"keyids": [
|
||||
"e71a54d543835ba86adad9460379c7641fb8726d164ea766801a1c522aba7ea2",
|
||||
"22f4caec6d8e6f9555af66b3d4c3cb06a3bb23fdc7e39c916c61f462e6f52b06",
|
||||
"61643838125b440b40db6942f5cb5a31c0dc04368316eb2aaa58b95904a58222",
|
||||
"a687e5bf4fab82b0ee58d46e05c9535145a2c9afb458f43d42b45ca0fdce2a70",
|
||||
"183e64f37670dc13ca0d28995a3053f3740954ddce44321a41e46534cf44e632"
|
||||
],
|
||||
"threshold": 3
|
||||
},
|
||||
"timestamp": {
|
||||
"keyids": [
|
||||
"0c87432c3bf09fd99189fdc32fa5eaedf4e4a5fac7bab73fa04a2e0fc64af6f5"
|
||||
],
|
||||
"threshold": 1,
|
||||
"x-tuf-on-ci-expiry-period": 7,
|
||||
"x-tuf-on-ci-signing-period": 6
|
||||
}
|
||||
},
|
||||
"spec_version": "1.0",
|
||||
"version": 13,
|
||||
"x-tuf-on-ci-expiry-period": 197,
|
||||
"x-tuf-on-ci-signing-period": 46
|
||||
}
|
||||
}
|
||||
+54
@@ -0,0 +1,54 @@
|
||||
{
|
||||
"signatures": [
|
||||
{
|
||||
"keyid": "0c87432c3bf09fd99189fdc32fa5eaedf4e4a5fac7bab73fa04a2e0fc64af6f5",
|
||||
"sig": "3044022043cdc6f2ee47ee7b4486ab92ce58424ef0b7b351a47853ea68316b67133a4a69022037c5cd433cc2cde76558c579c59a14dd9fc0bc85c496feaa17d90896cd4145fb"
|
||||
}
|
||||
],
|
||||
"signed": {
|
||||
"_type": "snapshot",
|
||||
"expires": "2035-10-08T16:46:31Z",
|
||||
"meta": {
|
||||
"registry.npmjs.org.json": {
|
||||
"version": 6
|
||||
},
|
||||
"rekor.json": {
|
||||
"hashes": {
|
||||
"sha256": "9d2e1a5842937d8e0d3e3759170b0ad15c56c5df36afc5cf73583ddd283a463b",
|
||||
"sha512": "176e9e710ddddd1b357a7d7970831bae59763395a0c18976110cbd35b25e5412dc50f356ec421a7a30265670cf7aec9ed84ee944ba700ec2394b9c876645b960"
|
||||
},
|
||||
"length": 797,
|
||||
"version": 3
|
||||
},
|
||||
"revocation.json": {
|
||||
"hashes": {
|
||||
"sha256": "6f60848ba8fb0955a02abfd1232fb3845dc9ee9f418bf03521a7ddb48217e040",
|
||||
"sha512": "a965dddd0d0edef6c59e84cf02ecf5a53299f633fd339b2b61814a4219ab4df672a6390f265b8b29e1c8cea9368ea3440df013790759d50231a30df1c1f02551"
|
||||
},
|
||||
"length": 800,
|
||||
"version": 2
|
||||
},
|
||||
"root.json": {
|
||||
"hashes": {
|
||||
"sha256": "f5ad897c9414cca99629f400ac3585e41bd8ebb44c5af07fb08dd636a9eced9c",
|
||||
"sha512": "7445ddfdd338ef786c324fc3d68f75be28cb95b7fb581d2a383e3e5dde18aa17029a5636ec0a22e9631931bbcb34057788311718ea41e21e7cdd3c0de13ede42"
|
||||
},
|
||||
"length": 5297,
|
||||
"version": 2
|
||||
},
|
||||
"staging.json": {
|
||||
"hashes": {
|
||||
"sha256": "cda57759abac5375397eea3531d7ca51e3a67da9a2dc93f2cdab749e2ae73149",
|
||||
"sha512": "e9e59587bde453144c7079884a880c706f1d43f26e8bb23fac2b96a99569a2a30ae6cf51ec51c2454f760ce83d4c20915e062aede7f319b3da6a6ed1d26ca281"
|
||||
},
|
||||
"length": 401,
|
||||
"version": 2
|
||||
},
|
||||
"targets.json": {
|
||||
"version": 13
|
||||
}
|
||||
},
|
||||
"spec_version": "1.0",
|
||||
"version": 162
|
||||
}
|
||||
}
|
||||
+167
@@ -0,0 +1,167 @@
|
||||
{
|
||||
"signatures": [
|
||||
{
|
||||
"keyid": "e71a54d543835ba86adad9460379c7641fb8726d164ea766801a1c522aba7ea2",
|
||||
"sig": "3044022054d6a8fe5f1eb2884cf4d6bb39e37515c70fc14f5730d07d8240b5da287aa2df022029054f010dd87e5bf64c75e061c81337a7946b3c870376587812156519572c2b"
|
||||
},
|
||||
{
|
||||
"keyid": "22f4caec6d8e6f9555af66b3d4c3cb06a3bb23fdc7e39c916c61f462e6f52b06",
|
||||
"sig": "30450220531a618d5c05608521ae05807299db298fc3319492fab15c4de6ac30d8011844022100b8fc501ccc1fb0aabc536a19abb805c4951c7fe0a957eb935eeca7b8072e9c58"
|
||||
},
|
||||
{
|
||||
"keyid": "61643838125b440b40db6942f5cb5a31c0dc04368316eb2aaa58b95904a58222",
|
||||
"sig": "30440220180f029a56100e3f02ef26f7355595cad0c9eb8235fd6fcf71ba419c243f59cd022023d5619c686320409d71971137ca8eb2ce01b29da5864b36053e6b5207d6d39c"
|
||||
},
|
||||
{
|
||||
"keyid": "a687e5bf4fab82b0ee58d46e05c9535145a2c9afb458f43d42b45ca0fdce2a70",
|
||||
"sig": ""
|
||||
},
|
||||
{
|
||||
"keyid": "183e64f37670dc13ca0d28995a3053f3740954ddce44321a41e46534cf44e632",
|
||||
"sig": "3046022100f748b26ba6c6d02bddea73df9d84d39dc365513bc60b6495b1391db785fffe1a022100aca703faf2839c8ab45cc22a8311b4b86b10422392b14cc94c1f0ed7ee634d37"
|
||||
}
|
||||
],
|
||||
"signed": {
|
||||
"_type": "targets",
|
||||
"delegations": {
|
||||
"keys": {
|
||||
"5e3a4021b11a425fd0a444f1670457ce5b15bbe036144f2417426f7f4b9721da": {
|
||||
"keytype": "ecdsa",
|
||||
"keyval": {
|
||||
"public": "-----BEGIN PUBLIC KEY-----\nMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEVfei1dXQRVeArCMcTDgxJtYg+Fs7\nV87DjhQbGlRJPyC7SW5TbNNkmvpmi4LeTv6moLVZ7T2nVqiRZbSkD+cf8w==\n-----END PUBLIC KEY-----\n"
|
||||
},
|
||||
"scheme": "ecdsa-sha2-nistp256",
|
||||
"x-tuf-on-ci-online-uri": "azurekms://npm-tuf-delegate.vault.azure.net/keys/npm-tuf-delegate-2024-08/e2772c1d01ca400da571096889f1660e"
|
||||
}
|
||||
},
|
||||
"roles": [
|
||||
{
|
||||
"keyids": [
|
||||
"5e3a4021b11a425fd0a444f1670457ce5b15bbe036144f2417426f7f4b9721da"
|
||||
],
|
||||
"name": "registry.npmjs.org",
|
||||
"paths": [
|
||||
"registry.npmjs.org/*"
|
||||
],
|
||||
"terminating": true,
|
||||
"threshold": 1
|
||||
}
|
||||
]
|
||||
},
|
||||
"expires": "2035-09-29T08:18:33Z",
|
||||
"spec_version": "1.0",
|
||||
"targets": {
|
||||
"artifact.pub": {
|
||||
"custom": {
|
||||
"sigstore": {
|
||||
"status": "Active",
|
||||
"usage": "Unknown"
|
||||
}
|
||||
},
|
||||
"hashes": {
|
||||
"sha256": "59ebf97a9850aecec4bc39c1f5c1dc46e6490a6b5fd2a6cacdcac0c3a6fc4cbf"
|
||||
},
|
||||
"length": 177
|
||||
},
|
||||
"ctfe.pub": {
|
||||
"custom": {
|
||||
"sigstore": {
|
||||
"status": "Active",
|
||||
"uri": "https://ctfe.sigstore.dev/test",
|
||||
"usage": "CTFE"
|
||||
}
|
||||
},
|
||||
"hashes": {
|
||||
"sha256": "7fcb94a5d0ed541260473b990b99a6c39864c1fb16f3f3e594a5a3cebbfe138a"
|
||||
},
|
||||
"length": 177
|
||||
},
|
||||
"ctfe_2022.pub": {
|
||||
"custom": {
|
||||
"sigstore": {
|
||||
"status": "Active",
|
||||
"uri": "https://ctfe.sigstore.dev/2022",
|
||||
"usage": "CTFE"
|
||||
}
|
||||
},
|
||||
"hashes": {
|
||||
"sha256": "270488a309d22e804eeb245493e87c667658d749006b9fee9cc614572d4fbbdc"
|
||||
},
|
||||
"length": 178
|
||||
},
|
||||
"fulcio.crt.pem": {
|
||||
"custom": {
|
||||
"sigstore": {
|
||||
"status": "Expired",
|
||||
"uri": "https://fulcio.sigstore.dev",
|
||||
"usage": "Fulcio"
|
||||
}
|
||||
},
|
||||
"hashes": {
|
||||
"sha256": "f360c53b2e13495a628b9b8096455badcb6d375b185c4816d95a5d746ff29908"
|
||||
},
|
||||
"length": 744
|
||||
},
|
||||
"fulcio_intermediate_v1.crt.pem": {
|
||||
"custom": {
|
||||
"sigstore": {
|
||||
"status": "Active",
|
||||
"uri": "https://fulcio.sigstore.dev",
|
||||
"usage": "Fulcio"
|
||||
}
|
||||
},
|
||||
"hashes": {
|
||||
"sha256": "f8cbecf186db7714624a5f4e99da31a917cbef70a94dd6921f5c3ca969dfe30a"
|
||||
},
|
||||
"length": 789
|
||||
},
|
||||
"fulcio_v1.crt.pem": {
|
||||
"custom": {
|
||||
"sigstore": {
|
||||
"status": "Active",
|
||||
"uri": "https://fulcio.sigstore.dev",
|
||||
"usage": "Fulcio"
|
||||
}
|
||||
},
|
||||
"hashes": {
|
||||
"sha256": "f989aa23def87c549404eadba767768d2a3c8d6d30a8b793f9f518a8eafd2cf5"
|
||||
},
|
||||
"length": 740
|
||||
},
|
||||
"rekor.pub": {
|
||||
"custom": {
|
||||
"sigstore": {
|
||||
"status": "Active",
|
||||
"uri": "https://rekor.sigstore.dev",
|
||||
"usage": "Rekor"
|
||||
}
|
||||
},
|
||||
"hashes": {
|
||||
"sha256": "dce5ef715502ec9f3cdfd11f8cc384b31a6141023d3e7595e9908a81cb6241bd"
|
||||
},
|
||||
"length": 178
|
||||
},
|
||||
"signing_config.json": {
|
||||
"hashes": {
|
||||
"sha256": "d358c75d032833f4193500f5b01b5760409410558fac962c599439adbb268b0f"
|
||||
},
|
||||
"length": 219
|
||||
},
|
||||
"signing_config.v0.2.json": {
|
||||
"hashes": {
|
||||
"sha256": "9711a6d5375706957a4859af31c5866a4474f81f0544f9f4b76c9c4f4c8a539c"
|
||||
},
|
||||
"length": 1034
|
||||
},
|
||||
"trusted_root.json": {
|
||||
"hashes": {
|
||||
"sha256": "6494e21ea73fa7ee769f85f57d5a3e6a08725eae1e38c755fc3517c9e6bc0b66"
|
||||
},
|
||||
"length": 6787
|
||||
}
|
||||
},
|
||||
"version": 13,
|
||||
"x-tuf-on-ci-expiry-period": 3650,
|
||||
"x-tuf-on-ci-signing-period": 31
|
||||
}
|
||||
}
|
||||
+126
@@ -0,0 +1,126 @@
|
||||
{
|
||||
"mediaType": "application/vnd.dev.sigstore.trustedroot+json;version=0.1",
|
||||
"tlogs": [
|
||||
{
|
||||
"baseUrl": "https://rekor.sigstore.dev",
|
||||
"hashAlgorithm": "SHA2_256",
|
||||
"publicKey": {
|
||||
"rawBytes": "MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAE2G2Y+2tabdTV5BcGiBIx0a9fAFwrkBbmLSGtks4L3qX6yYY0zufBnhC8Ur/iy55GhWP/9A/bY2LhC30M9+RYtw==",
|
||||
"keyDetails": "PKIX_ECDSA_P256_SHA_256",
|
||||
"validFor": {
|
||||
"start": "2021-01-12T11:53:27Z"
|
||||
}
|
||||
},
|
||||
"logId": {
|
||||
"keyId": "wNI9atQGlz+VWfO6LRygH4QUfY/8W4RFwiT5i5WRgB0="
|
||||
}
|
||||
},
|
||||
{
|
||||
"baseUrl": "https://log2025-1.rekor.sigstore.dev",
|
||||
"hashAlgorithm": "SHA2_256",
|
||||
"publicKey": {
|
||||
"rawBytes": "MCowBQYDK2VwAyEAt8rlp1knGwjfbcXAYPYAkn0XiLz1x8O4t0YkEhie244=",
|
||||
"keyDetails": "PKIX_ED25519",
|
||||
"validFor": {
|
||||
"start": "2025-09-23T00:00:00Z"
|
||||
}
|
||||
},
|
||||
"logId": {
|
||||
"keyId": "zxGZFVvd0FEmjR8WrFwMdcAJ9vtaY/QXf44Y1wUeP6A="
|
||||
}
|
||||
}
|
||||
],
|
||||
"certificateAuthorities": [
|
||||
{
|
||||
"subject": {
|
||||
"organization": "sigstore.dev",
|
||||
"commonName": "sigstore"
|
||||
},
|
||||
"uri": "https://fulcio.sigstore.dev",
|
||||
"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:29Z",
|
||||
"end": "2022-12-31T23:59:59.999Z"
|
||||
}
|
||||
},
|
||||
{
|
||||
"subject": {
|
||||
"organization": "sigstore.dev",
|
||||
"commonName": "sigstore"
|
||||
},
|
||||
"uri": "https://fulcio.sigstore.dev",
|
||||
"certChain": {
|
||||
"certificates": [
|
||||
{
|
||||
"rawBytes": "MIICGjCCAaGgAwIBAgIUALnViVfnU0brJasmRkHrn/UnfaQwCgYIKoZIzj0EAwMwKjEVMBMGA1UEChMMc2lnc3RvcmUuZGV2MREwDwYDVQQDEwhzaWdzdG9yZTAeFw0yMjA0MTMyMDA2MTVaFw0zMTEwMDUxMzU2NThaMDcxFTATBgNVBAoTDHNpZ3N0b3JlLmRldjEeMBwGA1UEAxMVc2lnc3RvcmUtaW50ZXJtZWRpYXRlMHYwEAYHKoZIzj0CAQYFK4EEACIDYgAE8RVS/ysH+NOvuDZyPIZtilgUF9NlarYpAd9HP1vBBH1U5CV77LSS7s0ZiH4nE7Hv7ptS6LvvR/STk798LVgMzLlJ4HeIfF3tHSaexLcYpSASr1kS0N/RgBJz/9jWCiXno3sweTAOBgNVHQ8BAf8EBAMCAQYwEwYDVR0lBAwwCgYIKwYBBQUHAwMwEgYDVR0TAQH/BAgwBgEB/wIBADAdBgNVHQ4EFgQU39Ppz1YkEZb5qNjpKFWixi4YZD8wHwYDVR0jBBgwFoAUWMAeX5FFpWapesyQoZMi0CrFxfowCgYIKoZIzj0EAwMDZwAwZAIwPCsQK4DYiZYDPIaDi5HFKnfxXx6ASSVmERfsynYBiX2X6SJRnZU84/9DZdnFvvxmAjBOt6QpBlc4J/0DxvkTCqpclvziL6BCCPnjdlIB3Pu3BxsPmygUY7Ii2zbdCdliiow="
|
||||
},
|
||||
{
|
||||
"rawBytes": "MIIB9zCCAXygAwIBAgIUALZNAPFdxHPwjeDloDwyYChAO/4wCgYIKoZIzj0EAwMwKjEVMBMGA1UEChMMc2lnc3RvcmUuZGV2MREwDwYDVQQDEwhzaWdzdG9yZTAeFw0yMTEwMDcxMzU2NTlaFw0zMTEwMDUxMzU2NThaMCoxFTATBgNVBAoTDHNpZ3N0b3JlLmRldjERMA8GA1UEAxMIc2lnc3RvcmUwdjAQBgcqhkjOPQIBBgUrgQQAIgNiAAT7XeFT4rb3PQGwS4IajtLk3/OlnpgangaBclYpsYBr5i+4ynB07ceb3LP0OIOZdxexX69c5iVuyJRQ+Hz05yi+UF3uBWAlHpiS5sh0+H2GHE7SXrk1EC5m1Tr19L9gg92jYzBhMA4GA1UdDwEB/wQEAwIBBjAPBgNVHRMBAf8EBTADAQH/MB0GA1UdDgQWBBRYwB5fkUWlZql6zJChkyLQKsXF+jAfBgNVHSMEGDAWgBRYwB5fkUWlZql6zJChkyLQKsXF+jAKBggqhkjOPQQDAwNpADBmAjEAj1nHeXZp+13NWBNa+EDsDP8G1WWg1tCMWP/WHPqpaVo0jhsweNFZgSs0eE7wYI4qAjEA2WB9ot98sIkoF3vZYdd3/VtWB5b9TNMea7Ix/stJ5TfcLLeABLE4BNJOsQ4vnBHJ"
|
||||
}
|
||||
]
|
||||
},
|
||||
"validFor": {
|
||||
"start": "2022-04-13T20:06:15Z"
|
||||
}
|
||||
}
|
||||
],
|
||||
"ctlogs": [
|
||||
{
|
||||
"baseUrl": "https://ctfe.sigstore.dev/test",
|
||||
"hashAlgorithm": "SHA2_256",
|
||||
"publicKey": {
|
||||
"rawBytes": "MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEbfwR+RJudXscgRBRpKX1XFDy3PyudDxz/SfnRi1fT8ekpfBd2O1uoz7jr3Z8nKzxA69EUQ+eFCFI3zeubPWU7w==",
|
||||
"keyDetails": "PKIX_ECDSA_P256_SHA_256",
|
||||
"validFor": {
|
||||
"start": "2021-03-14T00:00:00Z",
|
||||
"end": "2022-10-31T23:59:59.999Z"
|
||||
}
|
||||
},
|
||||
"logId": {
|
||||
"keyId": "CGCS8ChS/2hF0dFrJ4ScRWcYrBY9wzjSbea8IgY2b3I="
|
||||
}
|
||||
},
|
||||
{
|
||||
"baseUrl": "https://ctfe.sigstore.dev/2022",
|
||||
"hashAlgorithm": "SHA2_256",
|
||||
"publicKey": {
|
||||
"rawBytes": "MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEiPSlFi0CmFTfEjCUqF9HuCEcYXNKAaYalIJmBZ8yyezPjTqhxrKBpMnaocVtLJBI1eM3uXnQzQGAJdJ4gs9Fyw==",
|
||||
"keyDetails": "PKIX_ECDSA_P256_SHA_256",
|
||||
"validFor": {
|
||||
"start": "2022-10-20T00:00:00Z"
|
||||
}
|
||||
},
|
||||
"logId": {
|
||||
"keyId": "3T0wasbHETJjGR4cmWc3AqJKXrjePK3/h4pygC8p7o4="
|
||||
}
|
||||
}
|
||||
],
|
||||
"timestampAuthorities": [
|
||||
{
|
||||
"subject": {
|
||||
"organization": "sigstore.dev",
|
||||
"commonName": "sigstore-tsa-selfsigned"
|
||||
},
|
||||
"uri": "https://timestamp.sigstore.dev/api/v1/timestamp",
|
||||
"certChain": {
|
||||
"certificates": [
|
||||
{
|
||||
"rawBytes": "MIICEDCCAZagAwIBAgIUOhNULwyQYe68wUMvy4qOiyojiwwwCgYIKoZIzj0EAwMwOTEVMBMGA1UEChMMc2lnc3RvcmUuZGV2MSAwHgYDVQQDExdzaWdzdG9yZS10c2Etc2VsZnNpZ25lZDAeFw0yNTA0MDgwNjU5NDNaFw0zNTA0MDYwNjU5NDNaMC4xFTATBgNVBAoTDHNpZ3N0b3JlLmRldjEVMBMGA1UEAxMMc2lnc3RvcmUtdHNhMHYwEAYHKoZIzj0CAQYFK4EEACIDYgAE4ra2Z8hKNig2T9kFjCAToGG30jky+WQv3BzL+mKvh1SKNR/UwuwsfNCg4sryoYAd8E6isovVA3M4aoNdm9QDi50Z8nTEyvqgfDPtTIwXItfiW/AFf1V7uwkbkAoj0xxco2owaDAOBgNVHQ8BAf8EBAMCB4AwHQYDVR0OBBYEFIn9eUOHz9BlRsMCRscsc1t9tOsDMB8GA1UdIwQYMBaAFJjsAe9/u1H/1JUeb4qImFMHic6/MBYGA1UdJQEB/wQMMAoGCCsGAQUFBwMIMAoGCCqGSM49BAMDA2gAMGUCMDtpsV/6KaO0qyF/UMsX2aSUXKQFdoGTptQGc0ftq1csulHPGG6dsmyMNd3JB+G3EQIxAOajvBcjpJmKb4Nv+2Taoj8Uc5+b6ih6FXCCKraSqupe07zqswMcXJTe1cExvHvvlw=="
|
||||
},
|
||||
{
|
||||
"rawBytes": "MIIB9zCCAXygAwIBAgIUV7f0GLDOoEzIh8LXSW80OJiUp14wCgYIKoZIzj0EAwMwOTEVMBMGA1UEChMMc2lnc3RvcmUuZGV2MSAwHgYDVQQDExdzaWdzdG9yZS10c2Etc2VsZnNpZ25lZDAeFw0yNTA0MDgwNjU5NDNaFw0zNTA0MDYwNjU5NDNaMDkxFTATBgNVBAoTDHNpZ3N0b3JlLmRldjEgMB4GA1UEAxMXc2lnc3RvcmUtdHNhLXNlbGZzaWduZWQwdjAQBgcqhkjOPQIBBgUrgQQAIgNiAAQUQNtfRT/ou3YATa6wB/kKTe70cfJwyRIBovMnt8RcJph/COE82uyS6FmppLLL1VBPGcPfpQPYJNXzWwi8icwhKQ6W/Qe2h3oebBb2FHpwNJDqo+TMaC/tdfkv/ElJB72jRTBDMA4GA1UdDwEB/wQEAwIBBjASBgNVHRMBAf8ECDAGAQH/AgEAMB0GA1UdDgQWBBSY7AHvf7tR/9SVHm+KiJhTB4nOvzAKBggqhkjOPQQDAwNpADBmAjEAwGEGrfGZR1cen1R8/DTVMI943LssZmJRtDp/i7SfGHmGRP6gRbuj9vOK3b67Z0QQAjEAuT2H673LQEaHTcyQSZrkp4mX7WwkmF+sVbkYY5mXN+RMH13KUEHHOqASaemYWK/E"
|
||||
}
|
||||
]
|
||||
},
|
||||
"validFor": {
|
||||
"start": "2025-07-04T00:00:00Z"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
+19
@@ -0,0 +1,19 @@
|
||||
{
|
||||
"signatures": [
|
||||
{
|
||||
"keyid": "0c87432c3bf09fd99189fdc32fa5eaedf4e4a5fac7bab73fa04a2e0fc64af6f5",
|
||||
"sig": "3046022100baf8a66a62531e9db32df4a1475099798cc6126add00193c8b641ed3dfb83b2c022100eedbc40511f1f72459b95188995917ad564992a2258436a96d27885c063b7de1"
|
||||
}
|
||||
],
|
||||
"signed": {
|
||||
"_type": "timestamp",
|
||||
"expires": "2025-11-02T01:55:53Z",
|
||||
"meta": {
|
||||
"snapshot.json": {
|
||||
"version": 162
|
||||
}
|
||||
},
|
||||
"spec_version": "1.0",
|
||||
"version": 498
|
||||
}
|
||||
}
|
||||
+26
@@ -0,0 +1,26 @@
|
||||
package types
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"github.com/sigstore/sigstore-go/pkg/fulcio/certificate"
|
||||
)
|
||||
|
||||
type TimestampVerificationResult struct {
|
||||
Type string `json:"type"`
|
||||
URI string `json:"uri"`
|
||||
Timestamp time.Time `json:"timestamp"`
|
||||
}
|
||||
|
||||
type TrustRootStatus struct {
|
||||
Error string `json:"error,omitempty"`
|
||||
LastUpdated *time.Time `json:"lastUpdated,omitempty"`
|
||||
}
|
||||
|
||||
type SignatureInfo struct {
|
||||
Signer *certificate.Summary `json:"signer,omitempty"`
|
||||
Timestamps []TimestampVerificationResult `json:"timestamps,omitempty"`
|
||||
DockerReference string `json:"dockerReference,omitempty"`
|
||||
TrustRootStatus TrustRootStatus `json:"trustRootStatus,omitzero"`
|
||||
IsDHI bool `json:"isDHI,omitempty"`
|
||||
}
|
||||
+407
@@ -0,0 +1,407 @@
|
||||
package verifier
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/hex"
|
||||
"encoding/json"
|
||||
"path/filepath"
|
||||
"time"
|
||||
|
||||
slsa02 "github.com/in-toto/in-toto-golang/in_toto/slsa_provenance/v0.2"
|
||||
slsa1 "github.com/in-toto/in-toto-golang/in_toto/slsa_provenance/v1"
|
||||
"github.com/moby/policy-helpers/image"
|
||||
"github.com/moby/policy-helpers/roots"
|
||||
"github.com/moby/policy-helpers/roots/dhi"
|
||||
"github.com/moby/policy-helpers/types"
|
||||
digest "github.com/opencontainers/go-digest"
|
||||
ocispecs "github.com/opencontainers/image-spec/specs-go/v1"
|
||||
"github.com/pkg/errors"
|
||||
protobundle "github.com/sigstore/protobuf-specs/gen/pb-go/bundle/v1"
|
||||
"github.com/sigstore/sigstore-go/pkg/bundle"
|
||||
"github.com/sigstore/sigstore-go/pkg/fulcio/certificate"
|
||||
"github.com/sigstore/sigstore-go/pkg/root"
|
||||
"github.com/sigstore/sigstore-go/pkg/verify"
|
||||
"golang.org/x/sync/singleflight"
|
||||
)
|
||||
|
||||
type Config struct {
|
||||
UpdateInterval time.Duration
|
||||
RequireOnline bool
|
||||
StateDir string
|
||||
}
|
||||
|
||||
type Verifier struct {
|
||||
cfg Config
|
||||
sf singleflight.Group
|
||||
tp *roots.TrustProvider // tp may be nil if initialization failed
|
||||
}
|
||||
|
||||
func NewVerifier(cfg Config) (*Verifier, error) {
|
||||
if cfg.StateDir == "" {
|
||||
return nil, errors.Errorf("state directory must be provided")
|
||||
}
|
||||
v := &Verifier{cfg: cfg}
|
||||
|
||||
v.loadTrustProvider() // initialization fails on expired root/timestamp
|
||||
|
||||
return v, nil
|
||||
}
|
||||
|
||||
func (v *Verifier) VerifyArtifact(ctx context.Context, dgst digest.Digest, bundleBytes []byte) (*types.SignatureInfo, error) {
|
||||
anyCert, err := anyCerificateIdentity()
|
||||
if err != nil {
|
||||
return nil, errors.WithStack(err)
|
||||
}
|
||||
alg, rawDgst, err := rawDigest(dgst)
|
||||
if err != nil {
|
||||
return nil, errors.WithStack(err)
|
||||
}
|
||||
policy := verify.NewPolicy(verify.WithArtifactDigest(alg, rawDgst), anyCert)
|
||||
|
||||
b, err := loadBundle(bundleBytes)
|
||||
if err != nil {
|
||||
return nil, errors.WithStack(err)
|
||||
}
|
||||
|
||||
tp, err := v.loadTrustProvider()
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "loading trust provider")
|
||||
}
|
||||
|
||||
trustedRoot, st, err := tp.TrustedRoot(ctx)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "getting trusted root")
|
||||
}
|
||||
|
||||
gv, err := verify.NewVerifier(trustedRoot, verify.WithSignedCertificateTimestamps(1), verify.WithTransparencyLog(1), verify.WithObserverTimestamps(1))
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "creating verifier")
|
||||
}
|
||||
|
||||
result, err := gv.Verify(b, policy)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "verifying bundle")
|
||||
}
|
||||
|
||||
if result.Signature == nil || result.Signature.Certificate == nil {
|
||||
return nil, errors.Errorf("no valid signatures found")
|
||||
}
|
||||
|
||||
if !isSLSAPredicateType(result.Statement.PredicateType) {
|
||||
return nil, errors.Errorf("unexpected predicate type %q, expecting SLSA provenance", result.Statement.PredicateType)
|
||||
}
|
||||
|
||||
return &types.SignatureInfo{
|
||||
TrustRootStatus: toRootStatus(st),
|
||||
Signer: result.Signature.Certificate,
|
||||
Timestamps: toTimestamps(result.VerifiedTimestamps),
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (v *Verifier) VerifyImage(ctx context.Context, provider image.ReferrersProvider, desc ocispecs.Descriptor, platform *ocispecs.Platform) (*types.SignatureInfo, error) {
|
||||
sc, err := image.ResolveSignatureChain(ctx, provider, desc, platform)
|
||||
if err != nil {
|
||||
return nil, errors.Wrapf(err, "resolving signature chain for image %s", desc.Digest)
|
||||
}
|
||||
|
||||
if sc.AttestationManifest == nil || sc.SignatureManifest == nil {
|
||||
return nil, errors.WithStack(&NoSigChainError{
|
||||
Target: desc.Digest,
|
||||
HasAttestation: sc.AttestationManifest != nil,
|
||||
})
|
||||
}
|
||||
|
||||
attestationBytes, err := sc.ManifestBytes(ctx, sc.AttestationManifest)
|
||||
if err != nil {
|
||||
return nil, errors.Wrapf(err, "reading attestation manifest %s", sc.AttestationManifest.Digest)
|
||||
}
|
||||
|
||||
var attestation ocispecs.Manifest
|
||||
if err := json.Unmarshal(attestationBytes, &attestation); err != nil {
|
||||
return nil, errors.Wrapf(err, "unmarshaling attestation manifest %s", sc.AttestationManifest.Digest)
|
||||
}
|
||||
|
||||
if attestation.Subject == nil {
|
||||
return nil, errors.Errorf("attestation manifest %s has no subject", sc.AttestationManifest.Digest)
|
||||
}
|
||||
if attestation.Subject.Digest != sc.ImageManifest.Digest {
|
||||
return nil, errors.Errorf("attestation manifest %s subject digest %s does not match image manifest digest %s", sc.AttestationManifest.Digest, attestation.Subject.Digest, sc.ImageManifest.Digest)
|
||||
}
|
||||
if attestation.Subject.MediaType != ocispecs.MediaTypeImageManifest && attestation.Subject.MediaType != ocispecs.MediaTypeImageIndex {
|
||||
return nil, errors.Errorf("attestation manifest %s subject media type %s is not an image manifest or index", sc.AttestationManifest.Digest, attestation.Subject.MediaType)
|
||||
}
|
||||
if attestation.Subject.Size != sc.ImageManifest.Size {
|
||||
return nil, errors.Errorf("attestation manifest %s subject size %d does not match image manifest size %d", sc.AttestationManifest.Digest, attestation.Subject.Size, sc.ImageManifest.Size)
|
||||
}
|
||||
hasSLSA := false
|
||||
for _, l := range attestation.Layers {
|
||||
if isSLSAPredicateType(l.Annotations["in-toto.io/predicate-type"]) {
|
||||
hasSLSA = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if !hasSLSA {
|
||||
return nil, errors.Errorf("attestation manifest %s has no SLSA provenance layer", sc.AttestationManifest.Digest)
|
||||
}
|
||||
|
||||
anyCert, err := anyCerificateIdentity()
|
||||
if err != nil {
|
||||
return nil, errors.WithStack(err)
|
||||
}
|
||||
var artifactPolicy verify.ArtifactPolicyOption
|
||||
|
||||
tp, err := v.loadTrustProvider()
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "loading trust provider")
|
||||
}
|
||||
var trustedRoot root.TrustedMaterial
|
||||
fulcioRoot, st, err := tp.TrustedRoot(ctx)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "getting trusted root")
|
||||
}
|
||||
|
||||
sigBytes, err := sc.ManifestBytes(ctx, sc.SignatureManifest)
|
||||
if err != nil {
|
||||
return nil, errors.Wrapf(err, "reading signature manifest %s", sc.SignatureManifest.Digest)
|
||||
}
|
||||
|
||||
var mfst ocispecs.Manifest
|
||||
if err := json.Unmarshal(sigBytes, &mfst); err != nil {
|
||||
return nil, errors.Wrapf(err, "unmarshaling signature manifest %s", sc.SignatureManifest.Digest)
|
||||
}
|
||||
|
||||
// basic validations
|
||||
if mfst.Subject == nil {
|
||||
return nil, errors.Errorf("signature manifest %s has no subject", sc.SignatureManifest.Digest)
|
||||
}
|
||||
if mfst.Subject.Digest != sc.AttestationManifest.Digest {
|
||||
return nil, errors.Errorf("signature manifest %s subject digest %s does not match attestation manifest digest %s", sc.SignatureManifest.Digest, mfst.Subject.Digest, sc.AttestationManifest.Digest)
|
||||
}
|
||||
if mfst.Subject.MediaType != ocispecs.MediaTypeImageManifest && mfst.Subject.MediaType != ocispecs.MediaTypeImageIndex {
|
||||
return nil, errors.Errorf("signature manifest %s subject media type %s is not an image manifest or index", sc.SignatureManifest.Digest, mfst.Subject.MediaType)
|
||||
}
|
||||
if mfst.Subject.Size != sc.AttestationManifest.Size {
|
||||
return nil, errors.Errorf("signature manifest %s subject size %d does not match attestation manifest size %d", sc.SignatureManifest.Digest, mfst.Subject.Size, sc.AttestationManifest.Size)
|
||||
}
|
||||
if len(mfst.Layers) == 0 {
|
||||
return nil, errors.Errorf("signature manifest %s has %d layers, expected 1", sc.SignatureManifest.Digest, len(mfst.Layers))
|
||||
}
|
||||
layer := mfst.Layers[0]
|
||||
|
||||
var dockerReference string
|
||||
|
||||
var se verify.SignedEntity
|
||||
switch layer.MediaType {
|
||||
case image.ArtifactTypeSigstoreBundle:
|
||||
if mfst.ArtifactType != image.ArtifactTypeSigstoreBundle {
|
||||
return nil, errors.Errorf("signature manifest %s is not a bundle (artifact type %q)", sc.SignatureManifest.Digest, mfst.ArtifactType)
|
||||
}
|
||||
bundleBytes, err := image.ReadBlob(ctx, sc.Provider, layer)
|
||||
if err != nil {
|
||||
return nil, errors.Wrapf(err, "reading bundle layer %s from signature manifest %s", layer.Digest, sc.SignatureManifest.Digest)
|
||||
}
|
||||
b, err := loadBundle(bundleBytes)
|
||||
if err != nil {
|
||||
return nil, errors.Wrapf(err, "loading signature bundle from manifest %s", sc.SignatureManifest.Digest)
|
||||
}
|
||||
se = b
|
||||
|
||||
alg, rawDgst, err := rawDigest(sc.AttestationManifest.Digest)
|
||||
if err != nil {
|
||||
return nil, errors.WithStack(err)
|
||||
}
|
||||
artifactPolicy = verify.WithArtifactDigest(alg, rawDgst)
|
||||
case image.MediaTypeCosignSimpleSigning:
|
||||
payloadBytes, err := image.ReadBlob(ctx, sc.Provider, layer)
|
||||
if err != nil {
|
||||
return nil, errors.Wrapf(err, "reading bundle layer %s from signature manifest %s", layer.Digest, sc.SignatureManifest.Digest)
|
||||
}
|
||||
var payload struct {
|
||||
Critical struct {
|
||||
Identity struct {
|
||||
DockerReference string `json:"docker-reference"`
|
||||
} `json:"identity"`
|
||||
Image struct {
|
||||
DockerManifestDigest string `json:"docker-manifest-digest"`
|
||||
} `json:"image"`
|
||||
Type string `json:"type"`
|
||||
} `json:"critical"`
|
||||
Optional map[string]any `json:"optional"`
|
||||
}
|
||||
if err := json.Unmarshal(payloadBytes, &payload); err != nil {
|
||||
return nil, errors.Wrapf(err, "unmarshaling simple signing payload from manifest %s", sc.SignatureManifest.Digest)
|
||||
}
|
||||
if payload.Critical.Image.DockerManifestDigest != sc.AttestationManifest.Digest.String() {
|
||||
return nil, errors.Errorf("simple signing payload in manifest %s has docker-manifest-digest %s which does not match attestation manifest digest %s", sc.SignatureManifest.Digest, payload.Critical.Image.DockerManifestDigest, sc.AttestationManifest.Digest)
|
||||
}
|
||||
if payload.Critical.Type != "cosign container image signature" {
|
||||
return nil, errors.Errorf("simple signing payload in manifest %s has invalid type %q", sc.SignatureManifest.Digest, payload.Critical.Type)
|
||||
}
|
||||
dockerReference = payload.Critical.Identity.DockerReference
|
||||
// TODO: are more consistency checks needed for hashedrekord payload vs annotations?
|
||||
|
||||
hrse, err := newHashedRecordSignedEntity(&mfst, sc.DHI)
|
||||
if err != nil {
|
||||
return nil, errors.Wrapf(err, "loading hashed record signed entity from manifest %s", sc.SignatureManifest.Digest)
|
||||
}
|
||||
se = hrse
|
||||
alg, rawDgst, err := rawDigest(layer.Digest)
|
||||
if err != nil {
|
||||
return nil, errors.WithStack(err)
|
||||
}
|
||||
artifactPolicy = verify.WithArtifactDigest(alg, rawDgst)
|
||||
default:
|
||||
return nil, errors.Errorf("signature manifest %s layer has invalid media type %s", sc.SignatureManifest.Digest, layer.MediaType)
|
||||
}
|
||||
|
||||
verifierOpts := []verify.VerifierOption{}
|
||||
|
||||
if sc.DHI {
|
||||
trustedRoot, err = dhi.TrustedRoot(fulcioRoot)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "getting DHI trust root")
|
||||
}
|
||||
// DHI signature may or may not have transparency data
|
||||
// validation needs to be done in a later additional policy step
|
||||
if _, hasBundleAnnotation := layer.Annotations["dev.sigstore.cosign/bundle"]; !hasBundleAnnotation {
|
||||
verifierOpts = append(verifierOpts, verify.WithNoObserverTimestamps())
|
||||
} else {
|
||||
verifierOpts = append(verifierOpts,
|
||||
verify.WithObserverTimestamps(1),
|
||||
verify.WithTransparencyLog(1),
|
||||
)
|
||||
}
|
||||
// signed with pubkey without cert identity
|
||||
anyCert = verify.WithoutIdentitiesUnsafe()
|
||||
} else {
|
||||
trustedRoot = fulcioRoot
|
||||
verifierOpts = append(verifierOpts,
|
||||
verify.WithObserverTimestamps(1),
|
||||
verify.WithTransparencyLog(1),
|
||||
verify.WithSignedCertificateTimestamps(1),
|
||||
)
|
||||
}
|
||||
gv, err := verify.NewVerifier(trustedRoot, verifierOpts...)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "creating verifier")
|
||||
}
|
||||
|
||||
policy := verify.NewPolicy(artifactPolicy, anyCert)
|
||||
|
||||
result, err := gv.Verify(se, policy)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "verifying bundle")
|
||||
}
|
||||
|
||||
if result.Signature == nil || (result.Signature.Certificate == nil && !sc.DHI) {
|
||||
return nil, errors.Errorf("no valid signatures found")
|
||||
}
|
||||
|
||||
return &types.SignatureInfo{
|
||||
TrustRootStatus: toRootStatus(st),
|
||||
Signer: result.Signature.Certificate,
|
||||
Timestamps: toTimestamps(result.VerifiedTimestamps),
|
||||
DockerReference: dockerReference,
|
||||
IsDHI: sc.DHI,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (v *Verifier) loadTrustProvider() (*roots.TrustProvider, error) {
|
||||
var tpCache *roots.TrustProvider
|
||||
_, err, _ := v.sf.Do("", func() (any, error) {
|
||||
if v.tp != nil {
|
||||
tpCache = v.tp
|
||||
return nil, nil
|
||||
}
|
||||
tp, err := roots.NewTrustProvider(roots.SigstoreRootsConfig{
|
||||
CachePath: filepath.Join(v.cfg.StateDir, "tuf"),
|
||||
UpdateInterval: v.cfg.UpdateInterval,
|
||||
RequireOnline: v.cfg.RequireOnline,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
v.tp = tp
|
||||
tpCache = tp
|
||||
return nil, nil
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return tpCache, nil
|
||||
}
|
||||
|
||||
func anyCerificateIdentity() (verify.PolicyOption, error) {
|
||||
sanMatcher, err := verify.NewSANMatcher("", ".*")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
issuerMatcher, err := verify.NewIssuerMatcher("", ".*")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
extensions := certificate.Extensions{
|
||||
RunnerEnvironment: "github-hosted",
|
||||
}
|
||||
|
||||
certID, err := verify.NewCertificateIdentity(sanMatcher, issuerMatcher, extensions)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return verify.WithCertificateIdentity(certID), nil
|
||||
}
|
||||
|
||||
func loadBundle(dt []byte) (*bundle.Bundle, error) {
|
||||
var bundle bundle.Bundle
|
||||
bundle.Bundle = new(protobundle.Bundle)
|
||||
|
||||
err := bundle.UnmarshalJSON(dt)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &bundle, nil
|
||||
}
|
||||
|
||||
func rawDigest(d digest.Digest) (string, []byte, error) {
|
||||
alg := d.Algorithm().String()
|
||||
b, err := hex.DecodeString(d.Encoded())
|
||||
if err != nil {
|
||||
return "", nil, errors.Wrapf(err, "decoding digest %s", d)
|
||||
}
|
||||
return alg, b, nil
|
||||
}
|
||||
|
||||
func isSLSAPredicateType(v string) bool {
|
||||
switch v {
|
||||
case slsa1.PredicateSLSAProvenance, slsa02.PredicateSLSAProvenance:
|
||||
return true
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
func toTimestamps(ts []verify.TimestampVerificationResult) []types.TimestampVerificationResult {
|
||||
tsout := make([]types.TimestampVerificationResult, len(ts))
|
||||
for i, t := range ts {
|
||||
tsout[i] = types.TimestampVerificationResult{
|
||||
Type: t.Type,
|
||||
URI: t.URI,
|
||||
Timestamp: t.Timestamp,
|
||||
}
|
||||
}
|
||||
return tsout
|
||||
}
|
||||
|
||||
func toRootStatus(st roots.Status) types.TrustRootStatus {
|
||||
trs := types.TrustRootStatus{
|
||||
LastUpdated: st.LastUpdated,
|
||||
}
|
||||
if st.Error != nil {
|
||||
trs.Error = st.Error.Error()
|
||||
}
|
||||
return trs
|
||||
}
|
||||
Reference in New Issue
Block a user