Skip to content

Commit

Permalink
Sign: Upload to tlog and capture sig data
Browse files Browse the repository at this point in the history
This is a large commit that refactors the Sign() method of the attestation.
The main goal is to add two missing features:

1. Register the signature data to Rekor

After signing, we now register the signature in the sigstore transparency
log. This is essentail to allow for keyless verification.

2. New SignatureData Field

The attestation now has a new SignatureData field that captures the results
of the signing operation. This is required to make data like the cert and the
proof of inlclusion available externally (eg to record them in oci annotations).

The attestation.Sign() method has been heavily refactored but should be simppler
as the work it does is now broken into three internal functions:

 initSigning: creates context and options
 signAttestation: Performs the actual signing
 appendSignatureDataToTLog: Uploads data to rekor

Signed-off-by: Adolfo García Veytia (Puerco) <[email protected]>
  • Loading branch information
puerco committed Dec 6, 2023
1 parent badceec commit 4e44f21
Showing 1 changed file with 122 additions and 44 deletions.
166 changes: 122 additions & 44 deletions pkg/attestation/attestation.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,22 +11,45 @@ import (
"errors"
"fmt"
"io"
"os"
"strings"
"time"

"github.com/google/go-containerregistry/pkg/crane"
intoto "github.com/in-toto/in-toto-golang/in_toto"
ovattest "github.com/openvex/go-vex/pkg/attestation"
"github.com/sigstore/cosign/v2/cmd/cosign/cli/options"
"github.com/sigstore/cosign/v2/cmd/cosign/cli/rekor"
"github.com/sigstore/cosign/v2/cmd/cosign/cli/sign"
"github.com/sigstore/cosign/v2/pkg/cosign"
"github.com/sigstore/rekor/pkg/generated/models"
"github.com/sigstore/sigstore/pkg/signature/dsse"
signatureoptions "github.com/sigstore/sigstore/pkg/signature/options"
)

type Attestation struct {
signedData []byte `json:"-"`
ovattest.Attestation

// Sign is boolean that signals if the attestation has been signed
Signed bool `json:"-"`

// signatureData embeds the signed attestaion, the certificate used to sign
// it and the transparency log inclusion proof
SignatureData *SignatureData `json:"-"`
}

type SignatureData struct {
// CertData of the cert used to sign the attestation encodeded in PEM
CertData []byte `json:"-"`

// Chain contains the intermediate certificate chain of the attestation's cert
Chain []byte `json:"-"`

// Entry contains the proof of inclusion to the transparency log
Entry *models.LogEntryAnon `json:"-"`

// signedPayload contains the resulting blob after the attestation was
// signed.
signedPayload []byte
}

func New() *Attestation {
Expand All @@ -38,51 +61,18 @@ func New() *Attestation {

// Sign the attestation
func (att *Attestation) Sign() error {
ctx := context.Background()
var timeout time.Duration /// TODO move to options
var certPath, certChainPath string
ko := options.KeyOpts{
// KeyRef: s.options.PrivateKeyPath,
// IDToken: identityToken,
FulcioURL: options.DefaultFulcioURL,
RekorURL: options.DefaultRekorURL,
OIDCIssuer: options.DefaultOIDCIssuerURL,
OIDCClientID: "sigstore",

InsecureSkipFulcioVerify: false,
SkipConfirmation: true,
// FulcioAuthFlow: "",
}

if timeout != 0 {
var cancelFn context.CancelFunc
ctx, cancelFn = context.WithTimeout(ctx, timeout)
defer cancelFn()
}

sv, err := sign.SignerFromKeyOpts(ctx, certPath, certChainPath, ko)
if err != nil {
return fmt.Errorf("getting signer: %w", err)
}
defer sv.Close()
ctx, ko := initSigning()

// Wrap the attestation in the DSSE envelope
wrapped := dsse.WrapSigner(sv, "application/vnd.in-toto+json")

var b bytes.Buffer
if err := att.ToJSON(&b); err != nil {
return fmt.Errorf("serializing attestation to json: %w", err)
// Sign the attestaion.
if err := signAttestation(ctx, ko, att); err != nil {
return fmt.Errorf("signing attestation: %w", err)
}

signedPayload, err := wrapped.SignMessage(
bytes.NewReader(b.Bytes()), signatureoptions.WithContext(ctx),
)
if err != nil {
return fmt.Errorf("signing attestation: %w", err)
// Register the signature in rekor
if err := appendSignatureDataToTLog(ctx, ko, att); err != nil {
return fmt.Errorf("recording signature data to transparency log: %w", err)
}

att.Signed = true
att.signedData = signedPayload
return nil
}

Expand Down Expand Up @@ -114,12 +104,100 @@ func (att *Attestation) ToJSON(w io.Writer) error {
if !att.Signed {
return att.Attestation.ToJSON(w)
}
if len(att.signedData) == 0 {
if att.SignatureData == nil || len(att.SignatureData.signedPayload) == 0 {
return errors.New("consistency error: attestation is signed but data is empty")
}

if _, err := w.Write(att.signedData); err != nil {
if _, err := w.Write(att.SignatureData.signedPayload); err != nil {
return fmt.Errorf("writing signed attestation: %w", err)
}
return nil
}

// initSigning initializes the options and context needed to sign. Right now
// it only sets up some default options and a backgrous context but we
// should wire the options set from the CLI to this function
func initSigning() (context.Context, options.KeyOpts) {
ko := options.KeyOpts{
FulcioURL: options.DefaultFulcioURL,
RekorURL: options.DefaultRekorURL,
OIDCIssuer: options.DefaultOIDCIssuerURL,
OIDCClientID: "sigstore",
InsecureSkipFulcioVerify: false,
SkipConfirmation: true,
}

ctx := context.Background()
// TODO(puerco): Support context.WithTimeout(ctx, timeout)

return ctx, ko
}

// signAttestation creates a signer and signs the attestation. The attestation's
// SignatureData field will be populated with the certificate, chain and the
// attestaion data wrapped in its DSSE envelope.
func signAttestation(ctx context.Context, ko options.KeyOpts, att *Attestation) error {
// TODO(puerco): Investigate supporting certificates preloaded in the
// attestation. We would need to dump them to disk and load them into
// the args here and if we're reusing the bundle, set it in ko.BundlePath
// Note that in this call we hardocde the pats empty, but we should get them
// from somewhere.
sv, err := sign.SignerFromKeyOpts(ctx, "", "", ko)
if err != nil {
return fmt.Errorf("getting signer: %w", err)
}
defer sv.Close()

// Wrap the attestation in the DSSE envelope
wrapped := dsse.WrapSigner(sv, "application/vnd.in-toto+json")

var b bytes.Buffer
if err := att.ToJSON(&b); err != nil {
return fmt.Errorf("serializing attestation to json: %w", err)
}

// SIGN!
signedPayload, err := wrapped.SignMessage(
bytes.NewReader(b.Bytes()), signatureoptions.WithContext(ctx),
)
if err != nil {
return fmt.Errorf("signing attestation: %w", err)
}

// Assign the new data to the attestation
att.SignatureData = &SignatureData{
CertData: sv.Cert,
Chain: sv.Chain,
signedPayload: signedPayload,
}
att.Signed = true

return nil
}

// appendSignatureDataToTLog records the signature data to the transparency log
// (rekor). The proof of inclusion will be added to the attestation's SignatureData
// struct.
// If uploading fails, the signature data will be destroyed to guarantee an atomic
// operation of attesation.Sign()
func appendSignatureDataToTLog(ctx context.Context, ko options.KeyOpts, att *Attestation) error {
tlogClient, err := rekor.NewClient(ko.RekorURL)
if err != nil {
att.SignatureData = nil
return fmt.Errorf("creating rekor client: %w", err)
}

// ...and upload the signature data
entry, err := cosign.TLogUploadDSSEEnvelope(
ctx, tlogClient, att.SignatureData.signedPayload, att.SignatureData.CertData,
)
if err != nil {
att.SignatureData = nil
return fmt.Errorf("uploading to transparency log: %w", err)
}

att.SignatureData.Entry = entry
fmt.Fprintln(os.Stderr, "tlog entry created with index:", *entry.LogIndex)

return nil
}

0 comments on commit 4e44f21

Please sign in to comment.