339 lines
8.5 KiB
Go
339 lines
8.5 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) 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)
|
|
}
|
|
}
|