Files
buildx/vendor/github.com/moby/policy-helpers/hashedrecordbundle.go
2026-07-22 15:27:47 -07:00

346 lines
8.6 KiB
Go

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) Intermediates() []*x509.Certificate {
if d.isDHI {
return nil
}
return d.cert.Intermediates()
}
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)
}
}