Skip to content

Commit

Permalink
Fork cosign's replace op stuff (#87)
Browse files Browse the repository at this point in the history
The way this is written in cosign, we re-fetch and parse each layer for
every call to Replace. Since we're replacing 3 things with 3 new things,
we end up having to fetch and parse 9 blobs, which can take several
seconds altogether. Fixing this in cosign could be done by memoizing the
predicateType, but we have a further narrowing case where we add the
predicateType as an annotation on each attestation, so we can do a very
fast lookup of that and skip fetching the blob altogether.

This also drops the dupe checker because it's redundant with the replace
operation and incurs a little bit of overhead we would rather avoid.

Signed-off-by: Jon Johnson <jon.johnson@chainguard.dev>
  • Loading branch information
jonjohnsonjr authored Oct 12, 2023
1 parent 50b2bd6 commit c8b068e
Show file tree
Hide file tree
Showing 2 changed files with 111 additions and 21 deletions.
22 changes: 1 addition & 21 deletions internal/secant/attest.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@ package secant
import (
"bytes"
"context"
"crypto"
"encoding/base64"
"encoding/json"
"fmt"
Expand All @@ -19,14 +18,12 @@ import (
"github.com/secure-systems-lab/go-securesystemslib/dsse"
"github.com/sigstore/cosign/v2/pkg/cosign/attestation"
cbundle "github.com/sigstore/cosign/v2/pkg/cosign/bundle"
cremote "github.com/sigstore/cosign/v2/pkg/cosign/remote"
"github.com/sigstore/cosign/v2/pkg/oci/mutate"
ociremote "github.com/sigstore/cosign/v2/pkg/oci/remote"
"github.com/sigstore/cosign/v2/pkg/oci/static"
ctypes "github.com/sigstore/cosign/v2/pkg/types"
"github.com/sigstore/rekor/pkg/generated/client"
"github.com/sigstore/sigstore/pkg/cryptoutils"
"github.com/sigstore/sigstore/pkg/signature"
"github.com/sigstore/sigstore/pkg/signature/options"
)

Expand Down Expand Up @@ -69,10 +66,6 @@ func Attest(ctx context.Context, statements []*types.Statement, sv types.Cosigne
ropts := []ociremote.Option{ociremote.WithRemoteOptions(ropt...)}
se := ociremote.SignedUnknown(digest, ropts...)

// We use a dupe detector that always verifies because we always want to replace
// things with a matching predicate type.
dd := cremote.NewDupeDetector(&alwaysVerifier{})

for _, statement := range statements {
// Make sure these statements are all for the same subject.
if digest != statement.Digest {
Expand Down Expand Up @@ -158,8 +151,7 @@ func Attest(ctx context.Context, statements []*types.Statement, sv types.Cosigne
}

signOpts := []mutate.SignOption{
mutate.WithDupeDetector(dd),
mutate.WithReplaceOp(cremote.NewReplaceOp(predicateType)),
mutate.WithReplaceOp(newReplaceOp(predicateType)),
}

// Attach the attestation to the entity.
Expand Down Expand Up @@ -193,15 +185,3 @@ func parsePredicateType(t string) (string, error) {
}
return uri, nil
}

type alwaysVerifier struct{}

// This only exists to satisfy cosign interface jungle.
func (av *alwaysVerifier) PublicKey(opts ...signature.PublicKeyOption) (crypto.PublicKey, error) {
panic("this should not get called ever")
}

// This always verifies.
func (av *alwaysVerifier) VerifySignature(signature, message io.Reader, opts ...signature.VerifyOption) error {
return nil
}
110 changes: 110 additions & 0 deletions internal/secant/replace.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
package secant

import (
"encoding/base64"
"encoding/json"
"fmt"
"os"

"github.com/sigstore/cosign/v2/pkg/oci"
)

func newReplaceOp(predicateType string) *ro {
return &ro{predicateType: predicateType}
}

type ro struct {
predicateType string
}

func (r *ro) Replace(signatures oci.Signatures, o oci.Signature) (oci.Signatures, error) {
sigs, err := signatures.Get()
if err != nil {
return nil, err
}

ros := &replaceOCISignatures{Signatures: signatures}

sigsCopy := make([]oci.Signature, 0, len(sigs))
sigsCopy = append(sigsCopy, o)

if len(sigs) == 0 {
ros.attestations = append(ros.attestations, sigsCopy...)
return ros, nil
}

for _, s := range sigs {
pt, err := getPredicateType(s)
if err != nil {
return nil, err
}

if r.predicateType == pt {
fmt.Fprintln(os.Stderr, "Replacing attestation predicate:", r.predicateType)
continue
}

fmt.Fprintln(os.Stderr, "Not replacing attestation predicate:", pt)
sigsCopy = append(sigsCopy, s)
}

ros.attestations = append(ros.attestations, sigsCopy...)

return ros, nil
}

func getPredicateType(s oci.Signature) (string, error) {
anns, err := s.Annotations()
if err != nil {
return "", fmt.Errorf("could not get annotations: %w", err)
}

// Fast path: we have this in the top-level annotations.
if pt, ok := anns["predicateType"]; ok {
return pt, nil
}

// Otherwise we need to fetch and parse the payload.
var signaturePayload map[string]interface{}
p, err := s.Payload()
if err != nil {
return "", fmt.Errorf("could not get payload: %w", err)
}
err = json.Unmarshal(p, &signaturePayload)
if err != nil {
return "", fmt.Errorf("unmarshal payload data: %w", err)
}

val, ok := signaturePayload["payload"]
if !ok {
return "", fmt.Errorf("could not find 'payload' in payload data")
}
decodedPayload, err := base64.StdEncoding.DecodeString(val.(string))
if err != nil {
return "", fmt.Errorf("could not decode 'payload': %w", err)
}

var payloadData map[string]interface{}
if err := json.Unmarshal(decodedPayload, &payloadData); err != nil {
return "", fmt.Errorf("unmarshal payloadData: %w", err)
}
val, ok = payloadData["predicateType"]
if !ok {
return "", fmt.Errorf("could not find 'predicateType' in payload data")
}

pt, ok := val.(string)
if !ok {
return "", fmt.Errorf("expected predicateType to be string, got type %T: %v", val, val)
}
return pt, nil
}

type replaceOCISignatures struct {
oci.Signatures
attestations []oci.Signature
}

func (r *replaceOCISignatures) Get() ([]oci.Signature, error) {
return r.attestations, nil
}

0 comments on commit c8b068e

Please sign in to comment.