diff --git a/cmd/cosign/cli/attest/attest.go b/cmd/cosign/cli/attest/attest.go index a0daf322631..ebde751a55c 100644 --- a/cmd/cosign/cli/attest/attest.go +++ b/cmd/cosign/cli/attest/attest.go @@ -18,9 +18,14 @@ package attest import ( "bytes" "context" + "crypto/ecdsa" + "crypto/rsa" _ "crypto/sha256" // for `crypto.SHA256` + "crypto/x509" "encoding/json" + "encoding/pem" "fmt" + "net/url" "os" "time" @@ -28,66 +33,29 @@ import ( v1 "github.com/google/go-containerregistry/pkg/v1" "github.com/pkg/errors" + "github.com/sigstore/cosign/cmd/cosign/cli/fulcio" + "github.com/sigstore/cosign/cmd/cosign/cli/fulcio/fulcioverifier" "github.com/sigstore/cosign/cmd/cosign/cli/options" "github.com/sigstore/cosign/cmd/cosign/cli/sign" + "github.com/sigstore/cosign/internal/pkg/cosign/payload" + irekor "github.com/sigstore/cosign/internal/pkg/cosign/rekor" "github.com/sigstore/cosign/pkg/cosign" "github.com/sigstore/cosign/pkg/cosign/attestation" + "github.com/sigstore/cosign/pkg/cosign/pivkey" + "github.com/sigstore/cosign/pkg/cosign/pkcs11key" cremote "github.com/sigstore/cosign/pkg/cosign/remote" - "github.com/sigstore/cosign/pkg/oci" "github.com/sigstore/cosign/pkg/oci/mutate" ociremote "github.com/sigstore/cosign/pkg/oci/remote" - "github.com/sigstore/cosign/pkg/oci/static" - sigs "github.com/sigstore/cosign/pkg/signature" - "github.com/sigstore/cosign/pkg/types" + "github.com/sigstore/cosign/pkg/providers" rekPkgClient "github.com/sigstore/rekor/pkg/client" - "github.com/sigstore/rekor/pkg/generated/client" - "github.com/sigstore/rekor/pkg/generated/models" - "github.com/sigstore/sigstore/pkg/signature/dsse" - signatureoptions "github.com/sigstore/sigstore/pkg/signature/options" -) - -// TODO(dekkagaijin): remove this in favor of a function in pkg which handles both signatures and attestations -func bundle(entry *models.LogEntryAnon) *oci.Bundle { - if entry.Verification == nil { - return nil - } - return &oci.Bundle{ - SignedEntryTimestamp: entry.Verification.SignedEntryTimestamp, - Payload: oci.BundlePayload{ - Body: entry.Body, - IntegratedTime: *entry.IntegratedTime, - LogIndex: *entry.LogIndex, - LogID: *entry.LogID, - }, - } -} - -type tlogUploadFn func(*client.Rekor, []byte) (*models.LogEntryAnon, error) - -func uploadToTlog(ctx context.Context, sv *sign.SignerVerifier, rekorURL string, upload tlogUploadFn) (*oci.Bundle, error) { - var rekorBytes []byte - // Upload the cert or the public key, depending on what we have - if sv.Cert != nil { - rekorBytes = sv.Cert - } else { - pemBytes, err := sigs.PublicKeyPem(sv, signatureoptions.WithContext(ctx)) - if err != nil { - return nil, err - } - rekorBytes = pemBytes - } + "github.com/sigstore/sigstore/pkg/cryptoutils" + "github.com/sigstore/sigstore/pkg/signature" - rekorClient, err := rekPkgClient.GetRekorClient(rekorURL) - if err != nil { - return nil, err - } - entry, err := upload(rekorClient, rekorBytes) - if err != nil { - return nil, err - } - fmt.Fprintln(os.Stderr, "tlog entry created with index:", *entry.LogIndex) - return bundle(entry), nil -} + icos "github.com/sigstore/cosign/internal/pkg/cosign" + ifulcio "github.com/sigstore/cosign/internal/pkg/cosign/fulcio" + sigs "github.com/sigstore/cosign/pkg/signature" + fulcPkgClient "github.com/sigstore/fulcio/pkg/client" +) //nolint func AttestCmd(ctx context.Context, ko sign.KeyOpts, regOpts options.RegistryOptions, imageRef string, certPath string, @@ -133,12 +101,13 @@ func AttestCmd(ctx context.Context, ko sign.KeyOpts, regOpts options.RegistryOpt // each access. ref = digest // nolint - sv, err := sign.SignerFromKeyOpts(ctx, certPath, ko) + attestor, sv, closeFn, err := AttestorFromKeyOpts(ctx, certPath, predicateURI, ko) if err != nil { return errors.Wrap(err, "getting signer") } - defer sv.Close() - wrapped := dsse.WrapSigner(sv, predicateURI) + if closeFn != nil { + defer closeFn() + } dd := cremote.NewDupeDetector(sv) fmt.Fprintln(os.Stderr, "Using payload from:", predicatePath) @@ -162,37 +131,30 @@ func AttestCmd(ctx context.Context, ko sign.KeyOpts, regOpts options.RegistryOpt if err != nil { return err } - signedPayload, err := wrapped.SignMessage(bytes.NewReader(payload), signatureoptions.WithContext(ctx)) - if err != nil { - return errors.Wrap(err, "signing") - } - - if noUpload { - fmt.Println(string(signedPayload)) - return nil - } - - opts := []static.Option{static.WithLayerMediaType(types.DssePayloadType)} - if sv.Cert != nil { - opts = append(opts, static.WithCertChain(sv.Cert, sv.Chain)) - } // Check whether we should be uploading to the transparency log if sign.ShouldUploadToTlog(ctx, digest, force, ko.RekorURL) { - bundle, err := uploadToTlog(ctx, sv, ko.RekorURL, func(r *client.Rekor, b []byte) (*models.LogEntryAnon, error) { - return cosign.TLogUploadInTotoAttestation(ctx, r, signedPayload, b) - }) + rClient, err := rekPkgClient.GetRekorClient(ko.RekorURL) if err != nil { return err } - opts = append(opts, static.WithBundle(bundle)) + attestor = irekor.WrapDSSEAttestor(attestor, rClient) } - sig, err := static.NewAttestation(signedPayload, opts...) + ociAtt, _, err := attestor.Attest(ctx, bytes.NewReader(payload)) if err != nil { return err } + if noUpload { + signedPayload, err := ociAtt.Payload() + if err != nil { + return err + } + fmt.Println(string(signedPayload)) + return nil + } + se, err := ociremote.SignedEntity(digest, ociremoteOpts...) if err != nil { return err @@ -208,7 +170,7 @@ func AttestCmd(ctx context.Context, ko sign.KeyOpts, regOpts options.RegistryOpt } // Attach the attestation to the entity. - newSE, err := mutate.AttachAttestationToEntity(se, sig, signOpts...) + newSE, err := mutate.AttachAttestationToEntity(se, ociAtt, signOpts...) if err != nil { return err } @@ -216,3 +178,148 @@ func AttestCmd(ctx context.Context, ko sign.KeyOpts, regOpts options.RegistryOpt // Publish the attestations associated with this entity return ociremote.WriteAttestations(digest.Repository, newSE, ociremoteOpts...) } + +func attestorFromSecurityKey(keySlot, predicateURI string) (attestor icos.Attestor, sv signature.SignerVerifier, closeFn func(), err error) { + sk, err := pivkey.GetKeyWithSlot(keySlot) + if err != nil { + return nil, nil, nil, err + } + sv, err = sk.SignerVerifier() + if err != nil { + sk.Close() + return nil, nil, nil, err + } + + // Handle the -cert flag. + // With PIV, we assume the certificate is in the same slot on the PIV + // token as the private key. If it's not there, show a warning to the + // user. + certFromPIV, err := sk.Certificate() + var certPem []byte + if err != nil { + fmt.Fprintln(os.Stderr, "warning: no x509 certificate retrieved from the PIV token") + } else { + certPem, err = cryptoutils.MarshalCertificateToPEM(certFromPIV) + if err != nil { + sk.Close() + return nil, nil, nil, err + } + } + + return payload.NewDSSEAttestor(sv, nil, nil, certPem, nil, predicateURI), sv, sk.Close, nil +} + +func attestorFromKeyRef(ctx context.Context, certPath, keyRef string, passFunc cosign.PassFunc, predicateURI string) (attestor icos.Attestor, sv signature.SignerVerifier, closeFn func(), err error) { + k, err := sigs.SignerVerifierFromKeyRef(ctx, keyRef, passFunc) + if err != nil { + return nil, nil, nil, errors.Wrap(err, "reading key") + } + + var certBytes []byte + + // Handle the -cert flag + // With PKCS11, we assume the certificate is in the same slot on the PKCS11 + // token as the private key. If it's not there, show a warning to the + // user. + if pkcs11Key, ok := k.(*pkcs11key.Key); ok { + certFromPKCS11, _ := pkcs11Key.Certificate() + if certFromPKCS11 == nil { + fmt.Fprintln(os.Stderr, "warning: no x509 certificate retrieved from the PKCS11 token") + } else { + certBytes, err = cryptoutils.MarshalCertificateToPEM(certFromPKCS11) + if err != nil { + pkcs11Key.Close() + return nil, nil, nil, err + } + } + + return payload.NewDSSEAttestor(k, nil, nil, certBytes, nil, predicateURI), k, pkcs11Key.Close, nil + } + + if certPath == "" { + return payload.NewDSSEAttestor(k, nil, nil, nil, nil, predicateURI), k, nil, nil + } + + certBytes, err = os.ReadFile(certPath) + if err != nil { + return nil, nil, nil, errors.Wrap(err, "read certificate") + } + // Handle PEM. + if bytes.HasPrefix(certBytes, []byte("-----")) { + decoded, _ := pem.Decode(certBytes) + if decoded.Type != "CERTIFICATE" { + return nil, nil, nil, fmt.Errorf("supplied PEM file is not a certificate: %s", certPath) + } + certBytes = decoded.Bytes + } + parsedCert, err := x509.ParseCertificate(certBytes) + if err != nil { + return nil, nil, nil, errors.Wrap(err, "parse x509 certificate") + } + pk, err := k.PublicKey() + if err != nil { + return nil, nil, nil, errors.Wrap(err, "get public key") + } + switch kt := parsedCert.PublicKey.(type) { + case *ecdsa.PublicKey: + if !kt.Equal(pk) { + return nil, nil, nil, errors.New("public key in certificate does not match that in the signing key") + } + case *rsa.PublicKey: + if !kt.Equal(pk) { + return nil, nil, nil, errors.New("public key in certificate does not match that in the signing key") + } + default: + return nil, nil, nil, fmt.Errorf("unsupported key type: %T", parsedCert.PublicKey) + } + pemBytes, err := cryptoutils.MarshalCertificateToPEM(parsedCert) + if err != nil { + return nil, nil, nil, errors.Wrap(err, "marshaling certificate to PEM") + } + + return payload.NewDSSEAttestor(k, nil, nil, pemBytes, nil, predicateURI), k, nil, nil +} + +func keylessAttestor(ctx context.Context, predicateURI string, ko sign.KeyOpts) (attestor icos.Attestor, sv signature.SignerVerifier, err error) { + fulcioServer, err := url.Parse(ko.FulcioURL) + if err != nil { + return nil, nil, errors.Wrap(err, "parsing Fulcio URL") + } + fClient := fulcPkgClient.New(fulcioServer) + tok := ko.IDToken + if providers.Enabled(ctx) { + tok, err = providers.Provide(ctx, "sigstore") + if err != nil { + return nil, nil, errors.Wrap(err, "fetching ambient OIDC credentials") + } + } + + var k *fulcio.Signer + + if ko.InsecureSkipFulcioVerify { + if k, err = fulcio.NewSigner(ctx, tok, ko.OIDCIssuer, ko.OIDCClientID, fClient); err != nil { + return nil, nil, errors.Wrap(err, "getting key from Fulcio") + } + } else { + if k, err = fulcioverifier.NewSigner(ctx, tok, ko.OIDCIssuer, ko.OIDCClientID, fClient); err != nil { + return nil, nil, errors.Wrap(err, "getting key from Fulcio") + } + } + + return ifulcio.WrapAttestor(payload.NewDSSEAttestor(k, nil, nil, nil, nil, predicateURI), k.Cert, k.Chain), k, nil +} + +func AttestorFromKeyOpts(ctx context.Context, certPath, predicateURI string, ko sign.KeyOpts) (attestor icos.Attestor, sv signature.SignerVerifier, closeFn func(), err error) { + if ko.Sk { + return attestorFromSecurityKey(ko.Slot, predicateURI) + } + + if ko.KeyRef != "" { + return attestorFromKeyRef(ctx, certPath, ko.KeyRef, ko.PassFunc, predicateURI) + } + + // Default Keyless! + fmt.Fprintln(os.Stderr, "Generating ephemeral keys...") + attestor, sv, err = keylessAttestor(ctx, predicateURI, ko) + return attestor, sv, nil, err +} diff --git a/cmd/cosign/cli/sign/sign.go b/cmd/cosign/cli/sign/sign.go index 1729183ea2d..89ca5c98c3e 100644 --- a/cmd/cosign/cli/sign/sign.go +++ b/cmd/cosign/cli/sign/sign.go @@ -205,15 +205,17 @@ func signDigest(ctx context.Context, digest name.Digest, payload []byte, ko KeyO } } + // TODO(dekkagaijin): hoist the creation of these into SignerFromKeyOpts var s icos.Signer - s = ipayload.NewSigner(sv, nil, nil) + s = ipayload.NewSigner(sv, nil, nil, nil, nil) s = ifulcio.NewSigner(s, sv.Cert, sv.Chain) + if ShouldUploadToTlog(ctx, digest, force, ko.RekorURL) { rClient, err := rekorClient.GetRekorClient(ko.RekorURL) if err != nil { return err } - s = irekor.NewSigner(s, rClient) + s = irekor.WrapSigner(s, rClient) } ociSig, _, err := s.Sign(ctx, bytes.NewReader(payload)) diff --git a/internal/pkg/cosign/attest.go b/internal/pkg/cosign/attest.go new file mode 100644 index 00000000000..3603eeeb3fa --- /dev/null +++ b/internal/pkg/cosign/attest.go @@ -0,0 +1,30 @@ +// Copyright 2021 The Sigstore Authors. +// +// 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. + +package cosign + +import ( + "context" + "crypto" + "io" + + "github.com/sigstore/cosign/pkg/oci" +) + +// Attestor creates attestations in the form of `oci.Signature`s +type Attestor interface { + // Attest creates an attestation, in the form of an `oci.Signature`, from the given payload. + // The signature and payload are stored as a DSSE envelope in `osi.Signature.Payload()` + Attest(ctx context.Context, payload io.Reader) (oci.Signature, crypto.PublicKey, error) +} diff --git a/internal/pkg/cosign/fulcio/attestor.go b/internal/pkg/cosign/fulcio/attestor.go new file mode 100644 index 00000000000..1af07f38bb6 --- /dev/null +++ b/internal/pkg/cosign/fulcio/attestor.go @@ -0,0 +1,85 @@ +// Copyright 2021 The Sigstore Authors. +// +// 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. + +package fulcio + +import ( + "context" + "crypto" + "io" + + "github.com/sigstore/cosign/internal/pkg/cosign" + "github.com/sigstore/cosign/pkg/oci" + "github.com/sigstore/cosign/pkg/oci/static" +) + +// fulcioAttestor still needs to actually upload keys to Fulcio and receive +// the resulting `Cert` and `Chain`, which are added to the returned `oci.Signature` +type fulcioAttestor struct { + inner cosign.Attestor + + cert, chain []byte +} + +var _ cosign.Attestor = (*fulcioAttestor)(nil) + +// Attest implements `cosign.Attestor` +func (fa *fulcioAttestor) Attest(ctx context.Context, payload io.Reader) (oci.Signature, crypto.PublicKey, error) { + att, pub, err := fa.inner.Attest(ctx, payload) + if err != nil { + return nil, nil, err + } + + payloadBytes, err := att.Payload() + if err != nil { + return nil, nil, err + } + + // TODO(dekkagaijin): move the fulcio SignerVerififer logic here + opts := []static.Option{static.WithCertChain(fa.cert, fa.chain)} + + // Copy over the other attributes: + if annotations, err := att.Annotations(); err != nil { + return nil, nil, err + } else if len(annotations) > 0 { + opts = append(opts, static.WithAnnotations(annotations)) + } + if bundle, err := att.Bundle(); err != nil { + return nil, nil, err + } else if bundle != nil { + opts = append(opts, static.WithBundle(bundle)) + } + if mt, err := att.MediaType(); err != nil { + return nil, nil, err + } else if mt != "" { + opts = append(opts, static.WithLayerMediaType(mt)) + } + + newAtt, err := static.NewAttestation(payloadBytes, opts...) + if err != nil { + return nil, nil, err + } + + return newAtt, pub, nil +} + +// WrapAttestor returns a `cosign.Attestor` which leverages Fulcio to create +// a Cert and Chain for the attestation's signature created by the inner `Attestor` +func WrapAttestor(inner cosign.Attestor, cert, chain []byte) cosign.Attestor { + return &fulcioAttestor{ + inner: inner, + cert: cert, + chain: chain, + } +} diff --git a/internal/pkg/cosign/fulcio/fulcio.go b/internal/pkg/cosign/fulcio/signer.go similarity index 100% rename from internal/pkg/cosign/fulcio/fulcio.go rename to internal/pkg/cosign/fulcio/signer.go diff --git a/internal/pkg/cosign/payload/attestor.go b/internal/pkg/cosign/payload/attestor.go new file mode 100644 index 00000000000..fdcb685cd72 --- /dev/null +++ b/internal/pkg/cosign/payload/attestor.go @@ -0,0 +1,88 @@ +// Copyright 2021 The Sigstore Authors. +// +// 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. + +package payload + +import ( + "bytes" + "context" + "crypto" + "encoding/base64" + "encoding/json" + "io" + + "github.com/secure-systems-lab/go-securesystemslib/dsse" + "github.com/sigstore/cosign/internal/pkg/cosign" + "github.com/sigstore/cosign/pkg/oci" + "github.com/sigstore/cosign/pkg/oci/static" + "github.com/sigstore/cosign/pkg/types" + "github.com/sigstore/sigstore/pkg/signature" +) + +type payloadAttestor struct { + payloadSigner + + payloadType string +} + +var _ cosign.Attestor = (*payloadAttestor)(nil) + +// Attest implements `cosign.Attestor` +func (pa *payloadAttestor) Attest(ctx context.Context, payload io.Reader) (oci.Signature, crypto.PublicKey, error) { + p, err := io.ReadAll(payload) + if err != nil { + return nil, nil, err + } + pae := dsse.PAE(pa.payloadType, string(p)) + + _, sig, pk, err := pa.signPayload(ctx, bytes.NewReader(pae)) + if err != nil { + return nil, nil, err + } + + envelope := dsse.Envelope{ + PayloadType: pa.payloadType, + Payload: base64.StdEncoding.EncodeToString(p), + Signatures: []dsse.Signature{{ + Sig: base64.StdEncoding.EncodeToString(sig), + }}, + } + + envelopeJSON, err := json.Marshal(envelope) + if err != nil { + return nil, nil, err + } + + opts := []static.Option{static.WithLayerMediaType(types.DssePayloadType)} + + att, err := static.NewAttestation(envelopeJSON, opts...) + if err != nil { + return nil, nil, err + } + + return att, pk, nil +} + +// NewDSSEAttestor returns a `cosign.Attestor` which uses the given `signature.Signer` to create an attestation out of given payloads. +// The cert and chain, if provided, will be included in returned `oci.Signature`s. +func NewDSSEAttestor(s signature.Signer, + sOpts []signature.SignOption, + pkOpts []signature.PublicKeyOption, + certPEM, chainPEM []byte, + payloadType string) cosign.Attestor { + return &payloadAttestor{ + payloadSigner: newSigner(s, sOpts, pkOpts, certPEM, chainPEM), + payloadType: payloadType, + } +} diff --git a/internal/pkg/cosign/payload/payload.go b/internal/pkg/cosign/payload/signer.go similarity index 59% rename from internal/pkg/cosign/payload/payload.go rename to internal/pkg/cosign/payload/signer.go index 3ca86fe7b90..fe19542e492 100644 --- a/internal/pkg/cosign/payload/payload.go +++ b/internal/pkg/cosign/payload/signer.go @@ -32,32 +32,49 @@ type payloadSigner struct { payloadSigner signature.Signer payloadSignerOpts []signature.SignOption publicKeyProviderOpts []signature.PublicKeyOption + + certPEM, chainPEM []byte } var _ cosign.Signer = (*payloadSigner)(nil) -// Sign implements `Signer` -func (ps *payloadSigner) Sign(ctx context.Context, payload io.Reader) (oci.Signature, crypto.PublicKey, error) { - payloadBytes, err := io.ReadAll(payload) +func (ps *payloadSigner) signPayload(ctx context.Context, payload io.Reader) (payloadBytes, sig []byte, pk crypto.PublicKey, err error) { + payloadBytes, err = io.ReadAll(payload) if err != nil { - return nil, nil, err + return nil, nil, nil, err } + sOpts := []signature.SignOption{signatureoptions.WithContext(ctx)} sOpts = append(sOpts, ps.payloadSignerOpts...) - sig, err := ps.payloadSigner.SignMessage(bytes.NewReader(payloadBytes), sOpts...) + sig, err = ps.payloadSigner.SignMessage(bytes.NewReader(payloadBytes), sOpts...) if err != nil { - return nil, nil, err + return nil, nil, nil, err } pkOpts := []signature.PublicKeyOption{signatureoptions.WithContext(ctx)} pkOpts = append(pkOpts, ps.publicKeyProviderOpts...) - pk, err := ps.payloadSigner.PublicKey(pkOpts...) + pk, err = ps.payloadSigner.PublicKey(pkOpts...) + if err != nil { + return nil, nil, nil, err + } + + return payloadBytes, sig, pk, nil +} + +// Sign implements `Signer` +func (ps *payloadSigner) Sign(ctx context.Context, payload io.Reader) (oci.Signature, crypto.PublicKey, error) { + payloadBytes, sig, pk, err := ps.signPayload(ctx, payload) if err != nil { return nil, nil, err } b64sig := base64.StdEncoding.EncodeToString(sig) - ociSig, err := static.NewSignature(payloadBytes, b64sig) + + var opts []static.Option + if len(ps.certPEM) > 0 { + opts = []static.Option{static.WithCertChain(ps.certPEM, ps.chainPEM)} + } + ociSig, err := static.NewSignature(payloadBytes, b64sig, opts...) if err != nil { return nil, nil, err } @@ -65,13 +82,26 @@ func (ps *payloadSigner) Sign(ctx context.Context, payload io.Reader) (oci.Signa return ociSig, pk, nil } -// NewSigner returns a `cosign.Signer` uses the given `signature.Signer` to sign the requested payload, then returns the signature, the public key associated with it, the signed payload -func NewSigner(s signature.Signer, +func newSigner(s signature.Signer, sOpts []signature.SignOption, - pkOpts []signature.PublicKeyOption) cosign.Signer { - return &payloadSigner{ + pkOpts []signature.PublicKeyOption, + certPEM, chainPEM []byte) payloadSigner { + return payloadSigner{ payloadSigner: s, payloadSignerOpts: sOpts, publicKeyProviderOpts: pkOpts, + + certPEM: certPEM, + chainPEM: chainPEM, } } + +// NewSigner returns a `cosign.Signer` which uses the given `signature.Signer` to sign requested payloads. +// The cert and chain, if provided, will be included in returned `oci.Signature`s. +func NewSigner(s signature.Signer, + sOpts []signature.SignOption, + pkOpts []signature.PublicKeyOption, + certPEM, chainPEM []byte) cosign.Signer { + ps := newSigner(s, sOpts, pkOpts, certPEM, chainPEM) + return &ps +} diff --git a/internal/pkg/cosign/rekor/attestor.go b/internal/pkg/cosign/rekor/attestor.go new file mode 100644 index 00000000000..42de633745d --- /dev/null +++ b/internal/pkg/cosign/rekor/attestor.go @@ -0,0 +1,120 @@ +// Copyright 2021 The Sigstore Authors. +// +// 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. + +package rekor + +import ( + "context" + "crypto" + "crypto/x509" + "fmt" + "io" + "os" + + "github.com/sigstore/cosign/internal/pkg/cosign" + cosignv1 "github.com/sigstore/cosign/pkg/cosign" + "github.com/sigstore/cosign/pkg/oci" + "github.com/sigstore/cosign/pkg/oci/static" + "github.com/sigstore/rekor/pkg/generated/client" + "github.com/sigstore/sigstore/pkg/cryptoutils" +) + +func unpackAttestation(ociSig oci.Signature) (envelope []byte, cert *x509.Certificate, err error) { + // attestations store both signatures and signed payload as an envelope in `Payload` + envelope, err = ociSig.Payload() + if err != nil { + return nil, nil, err + } + + cert, err = ociSig.Cert() + if err != nil { + return nil, nil, err + } + + return envelope, cert, nil +} + +// attestorWrapper calls a wrapped, inner attestor then uploads either the Cert or Pub(licKey) of the results to Rekor, then adds the resulting `Bundle` +type attestorWrapper struct { + inner cosign.Attestor + + rClient *client.Rekor +} + +var _ cosign.Attestor = (*attestorWrapper)(nil) + +// Attest implements `cosign.Attestor` +func (ra *attestorWrapper) Attest(ctx context.Context, payload io.Reader) (oci.Signature, crypto.PublicKey, error) { + att, pub, err := ra.inner.Attest(ctx, payload) + if err != nil { + return nil, nil, err + } + + envelope, cert, err := unpackAttestation(att) + if err != nil { + return nil, nil, err + } + + rekorBytes, err := rekorBytes(cert, pub) + if err != nil { + return nil, nil, err + } + + entry, err := cosignv1.TLogUploadInTotoAttestation(ctx, ra.rClient, envelope, rekorBytes) + if err != nil { + return nil, nil, err + } + // TODO: hook up to real logging + fmt.Fprintln(os.Stderr, "tlog entry created with index:", *entry.LogIndex) + + opts := []static.Option{static.WithBundle(bundle(entry))} + + // Copy over the other attributes: + if cert != nil { + chain, err := att.Chain() + if err != nil { + return nil, nil, err + } + chainBytes, err := cryptoutils.MarshalCertificatesToPEM(chain) + if err != nil { + return nil, nil, err + } + opts = append(opts, static.WithCertChain(rekorBytes, chainBytes)) + } + if annotations, err := att.Annotations(); err != nil { + return nil, nil, err + } else if len(annotations) > 0 { + opts = append(opts, static.WithAnnotations(annotations)) + } + if mt, err := att.MediaType(); err != nil { + return nil, nil, err + } else if mt != "" { + opts = append(opts, static.WithLayerMediaType(mt)) + } + + newAtt, err := static.NewAttestation(envelope, opts...) + if err != nil { + return nil, nil, err + } + + return newAtt, pub, nil +} + +// WrapDSSEAttestor returns a `cosign.Attestor` which uploads the signature in the DSSE attestation to Rekor +func WrapDSSEAttestor(inner cosign.Attestor, rClient *client.Rekor) cosign.Attestor { + return &attestorWrapper{ + inner: inner, + rClient: rClient, + } +} diff --git a/internal/pkg/cosign/rekor/rekor.go b/internal/pkg/cosign/rekor/signer.go similarity index 62% rename from internal/pkg/cosign/rekor/rekor.go rename to internal/pkg/cosign/rekor/signer.go index 907f8a06a4b..a6a84cb6059 100644 --- a/internal/pkg/cosign/rekor/rekor.go +++ b/internal/pkg/cosign/rekor/signer.go @@ -17,6 +17,7 @@ package rekor import ( "context" "crypto" + "crypto/x509" "encoding/base64" "fmt" "io" @@ -27,34 +28,29 @@ import ( "github.com/sigstore/cosign/pkg/oci" "github.com/sigstore/cosign/pkg/oci/static" "github.com/sigstore/rekor/pkg/generated/client" - "github.com/sigstore/rekor/pkg/generated/models" "github.com/sigstore/sigstore/pkg/cryptoutils" ) -func bundle(entry *models.LogEntryAnon) *oci.Bundle { - if entry.Verification == nil { - return nil +func unpackSignature(ociSig oci.Signature) (payload []byte, b64Sig string, sig []byte, cert *x509.Certificate, err error) { + payload, err = ociSig.Payload() + if err != nil { + return nil, "", nil, nil, err } - return &oci.Bundle{ - SignedEntryTimestamp: entry.Verification.SignedEntryTimestamp, - Payload: oci.BundlePayload{ - Body: entry.Body, - IntegratedTime: *entry.IntegratedTime, - LogIndex: *entry.LogIndex, - LogID: *entry.LogID, - }, + b64Sig, err = ociSig.Base64Signature() + if err != nil { + return nil, "", nil, nil, err + } + sig, err = base64.StdEncoding.DecodeString(b64Sig) + if err != nil { + return nil, "", nil, nil, err } -} - -type tlogUploadFn func(*client.Rekor, []byte) (*models.LogEntryAnon, error) -func uploadToTlog(rekorBytes []byte, rClient *client.Rekor, upload tlogUploadFn) (*oci.Bundle, error) { - entry, err := upload(rClient, rekorBytes) + cert, err = ociSig.Cert() if err != nil { - return nil, err + return nil, "", nil, nil, err } - fmt.Fprintln(os.Stderr, "tlog entry created with index:", *entry.LogIndex) - return bundle(entry), nil + + return payload, b64Sig, sig, cert, nil } // signerWrapper calls a wrapped, inner signer then uploads either the Cert or Pub(licKey) of the results to Rekor, then adds the resulting `Bundle` @@ -73,46 +69,26 @@ func (rs *signerWrapper) Sign(ctx context.Context, payload io.Reader) (oci.Signa return nil, nil, err } - payloadBytes, err := sig.Payload() - if err != nil { - return nil, nil, err - } - b64Sig, err := sig.Base64Signature() - if err != nil { - return nil, nil, err - } - sigBytes, err := base64.StdEncoding.DecodeString(b64Sig) - if err != nil { - return nil, nil, err - } - - // Upload the cert or the public key, depending on what we have - cert, err := sig.Cert() + payloadBytes, b64Sig, sigBytes, cert, err := unpackSignature(sig) if err != nil { return nil, nil, err } - var rekorBytes []byte - if cert != nil { - rekorBytes, err = cryptoutils.MarshalCertificateToPEM(cert) - } else { - rekorBytes, err = cryptoutils.MarshalPublicKeyToPEM(pub) - } + rekorBytes, err := rekorBytes(cert, pub) if err != nil { return nil, nil, err } - bundle, err := uploadToTlog(rekorBytes, rs.rClient, func(r *client.Rekor, b []byte) (*models.LogEntryAnon, error) { - return cosignv1.TLogUpload(ctx, r, sigBytes, payloadBytes, b) - }) + entry, err := cosignv1.TLogUpload(ctx, rs.rClient, sigBytes, payloadBytes, rekorBytes) if err != nil { return nil, nil, err } + // TODO: hook up to real logging + fmt.Fprintln(os.Stderr, "tlog entry created with index:", *entry.LogIndex) - opts := []static.Option{static.WithBundle(bundle)} + opts := []static.Option{static.WithBundle(bundle(entry))} // Copy over the other attributes: - if cert != nil { chain, err := sig.Chain() if err != nil { @@ -143,8 +119,8 @@ func (rs *signerWrapper) Sign(ctx context.Context, payload io.Reader) (oci.Signa return newSig, pub, nil } -// NewSigner returns a `cosign.Signer` which uploads the signature to Rekor -func NewSigner(inner cosign.Signer, rClient *client.Rekor) cosign.Signer { +// WrapSigner returns a `cosign.Signer` which uploads the signature to Rekor +func WrapSigner(inner cosign.Signer, rClient *client.Rekor) cosign.Signer { return &signerWrapper{ inner: inner, rClient: rClient, diff --git a/internal/pkg/cosign/rekor/util.go b/internal/pkg/cosign/rekor/util.go new file mode 100644 index 00000000000..e49a65eafb8 --- /dev/null +++ b/internal/pkg/cosign/rekor/util.go @@ -0,0 +1,47 @@ +// Copyright 2021 The Sigstore Authors. +// +// 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. + +package rekor + +import ( + "crypto" + "crypto/x509" + + "github.com/sigstore/cosign/pkg/oci" + "github.com/sigstore/rekor/pkg/generated/models" + "github.com/sigstore/sigstore/pkg/cryptoutils" +) + +func bundle(entry *models.LogEntryAnon) *oci.Bundle { + if entry.Verification == nil { + return nil + } + return &oci.Bundle{ + SignedEntryTimestamp: entry.Verification.SignedEntryTimestamp, + Payload: oci.BundlePayload{ + Body: entry.Body, + IntegratedTime: *entry.IntegratedTime, + LogIndex: *entry.LogIndex, + LogID: *entry.LogID, + }, + } +} + +func rekorBytes(cert *x509.Certificate, pub crypto.PublicKey) ([]byte, error) { + // Upload the cert or the public key, depending on what we have + if cert != nil { + return cryptoutils.MarshalCertificateToPEM(cert) + } + return cryptoutils.MarshalPublicKeyToPEM(pub) +} diff --git a/internal/pkg/cosign/rekor/util_test.go b/internal/pkg/cosign/rekor/util_test.go new file mode 100644 index 00000000000..7ed566704b1 --- /dev/null +++ b/internal/pkg/cosign/rekor/util_test.go @@ -0,0 +1,151 @@ +// Copyright 2021 The Sigstore Authors. +// +// 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. + +package rekor + +import ( + "bytes" + "crypto" + "crypto/ecdsa" + "crypto/elliptic" + "crypto/rand" + "crypto/x509" + "testing" + + "github.com/go-openapi/strfmt" + "github.com/google/go-cmp/cmp" + "github.com/sigstore/cosign/pkg/oci" + "github.com/sigstore/rekor/pkg/generated/models" + "github.com/sigstore/sigstore/pkg/cryptoutils" +) + +const ( + testCertPEM = `-----BEGIN CERTIFICATE----- +MIICyTCCAlCgAwIBAgITYZXhosLz4+Q/XCUwBySVDmU2jTAKBggqhkjOPQQDAzAq +MRUwEwYDVQQKEwxzaWdzdG9yZS5kZXYxETAPBgNVBAMTCHNpZ3N0b3JlMB4XDTIx +MDMwOTA0NDYwOVoXDTIxMDMwOTA1MDYwMlowOjEbMBkGA1UECgwSbG9yZW5jLmRA +Z21haWwuY29tMRswGQYDVQQDDBJsb3JlbmMuZEBnbWFpbC5jb20wdjAQBgcqhkjO +PQIBBgUrgQQAIgNiAARIA8thgk3Zext2UWP1aBE1uoIAqetevPiEDuGKtSUPYxBv +AhzrwhDTbHrj6vMQCBNE7o4AfewyJAZf6CKbee8WIakPfAjRSTQjjnZBzKvSHn4K +u8SByXjFN0rde8qDqo+jggEmMIIBIjAOBgNVHQ8BAf8EBAMCB4AwEwYDVR0lBAww +CgYIKwYBBQUHAwMwDAYDVR0TAQH/BAIwADAdBgNVHQ4EFgQUQeDktDb9QFrYxF8H +xBXkAHQmvqwwHwYDVR0jBBgwFoAUyMUdAEGaJCkyUSTrDa5K7UoG0+wwgY0GCCsG +AQUFBwEBBIGAMH4wfAYIKwYBBQUHMAKGcGh0dHA6Ly9wcml2YXRlY2EtY29udGVu +dC02MDNmZTdlNy0wMDAwLTIyMjctYmY3NS1mNGY1ZTgwZDI5NTQuc3RvcmFnZS5n +b29nbGVhcGlzLmNvbS9jYTM2YTFlOTYyNDJiOWZjYjE0Ni9jYS5jcnQwHQYDVR0R +BBYwFIESbG9yZW5jLmRAZ21haWwuY29tMAoGCCqGSM49BAMDA2cAMGQCMAgjOcjN +P3w/xB8bi/hKXJ9RdNH/DMADiusGtd1d/xxyFVj1xYosQ7y1G6y84VDBvQIwMfQG +8Tp8zsxDg5Oz4qUBZ/AKmkPJHhgmiHftwbb5I1S+1xdhzJtJ8Eg0M00/nqok +-----END CERTIFICATE----- +` +) + +var testCert = &x509.Certificate{ + Raw: []byte{0x30, 0x82, 0x2, 0xc9, 0x30, 0x82, 0x2, 0x50, 0xa0, 0x3, 0x2, 0x1, 0x2, 0x2, 0x13, 0x61, 0x95, 0xe1, 0xa2, 0xc2, 0xf3, 0xe3, 0xe4, 0x3f, 0x5c, 0x25, 0x30, 0x7, 0x24, 0x95, 0xe, 0x65, 0x36, 0x8d, 0x30, 0xa, 0x6, 0x8, 0x2a, 0x86, 0x48, 0xce, 0x3d, 0x4, 0x3, 0x3, 0x30, 0x2a, 0x31, 0x15, 0x30, 0x13, 0x6, 0x3, 0x55, 0x4, 0xa, 0x13, 0xc, 0x73, 0x69, 0x67, 0x73, 0x74, 0x6f, 0x72, 0x65, 0x2e, 0x64, 0x65, 0x76, 0x31, 0x11, 0x30, 0xf, 0x6, 0x3, 0x55, 0x4, 0x3, 0x13, 0x8, 0x73, 0x69, 0x67, 0x73, 0x74, 0x6f, 0x72, 0x65, 0x30, 0x1e, 0x17, 0xd, 0x32, 0x31, 0x30, 0x33, 0x30, 0x39, 0x30, 0x34, 0x34, 0x36, 0x30, 0x39, 0x5a, 0x17, 0xd, 0x32, 0x31, 0x30, 0x33, 0x30, 0x39, 0x30, 0x35, 0x30, 0x36, 0x30, 0x32, 0x5a, 0x30, 0x3a, 0x31, 0x1b, 0x30, 0x19, 0x6, 0x3, 0x55, 0x4, 0xa, 0xc, 0x12, 0x6c, 0x6f, 0x72, 0x65, 0x6e, 0x63, 0x2e, 0x64, 0x40, 0x67, 0x6d, 0x61, 0x69, 0x6c, 0x2e, 0x63, 0x6f, 0x6d, 0x31, 0x1b, 0x30, 0x19, 0x6, 0x3, 0x55, 0x4, 0x3, 0xc, 0x12, 0x6c, 0x6f, 0x72, 0x65, 0x6e, 0x63, 0x2e, 0x64, 0x40, 0x67, 0x6d, 0x61, 0x69, 0x6c, 0x2e, 0x63, 0x6f, 0x6d, 0x30, 0x76, 0x30, 0x10, 0x6, 0x7, 0x2a, 0x86, 0x48, 0xce, 0x3d, 0x2, 0x1, 0x6, 0x5, 0x2b, 0x81, 0x4, 0x0, 0x22, 0x3, 0x62, 0x0, 0x4, 0x48, 0x3, 0xcb, 0x61, 0x82, 0x4d, 0xd9, 0x7b, 0x1b, 0x76, 0x51, 0x63, 0xf5, 0x68, 0x11, 0x35, 0xba, 0x82, 0x0, 0xa9, 0xeb, 0x5e, 0xbc, 0xf8, 0x84, 0xe, 0xe1, 0x8a, 0xb5, 0x25, 0xf, 0x63, 0x10, 0x6f, 0x2, 0x1c, 0xeb, 0xc2, 0x10, 0xd3, 0x6c, 0x7a, 0xe3, 0xea, 0xf3, 0x10, 0x8, 0x13, 0x44, 0xee, 0x8e, 0x0, 0x7d, 0xec, 0x32, 0x24, 0x6, 0x5f, 0xe8, 0x22, 0x9b, 0x79, 0xef, 0x16, 0x21, 0xa9, 0xf, 0x7c, 0x8, 0xd1, 0x49, 0x34, 0x23, 0x8e, 0x76, 0x41, 0xcc, 0xab, 0xd2, 0x1e, 0x7e, 0xa, 0xbb, 0xc4, 0x81, 0xc9, 0x78, 0xc5, 0x37, 0x4a, 0xdd, 0x7b, 0xca, 0x83, 0xaa, 0x8f, 0xa3, 0x82, 0x1, 0x26, 0x30, 0x82, 0x1, 0x22, 0x30, 0xe, 0x6, 0x3, 0x55, 0x1d, 0xf, 0x1, 0x1, 0xff, 0x4, 0x4, 0x3, 0x2, 0x7, 0x80, 0x30, 0x13, 0x6, 0x3, 0x55, 0x1d, 0x25, 0x4, 0xc, 0x30, 0xa, 0x6, 0x8, 0x2b, 0x6, 0x1, 0x5, 0x5, 0x7, 0x3, 0x3, 0x30, 0xc, 0x6, 0x3, 0x55, 0x1d, 0x13, 0x1, 0x1, 0xff, 0x4, 0x2, 0x30, 0x0, 0x30, 0x1d, 0x6, 0x3, 0x55, 0x1d, 0xe, 0x4, 0x16, 0x4, 0x14, 0x41, 0xe0, 0xe4, 0xb4, 0x36, 0xfd, 0x40, 0x5a, 0xd8, 0xc4, 0x5f, 0x7, 0xc4, 0x15, 0xe4, 0x0, 0x74, 0x26, 0xbe, 0xac, 0x30, 0x1f, 0x6, 0x3, 0x55, 0x1d, 0x23, 0x4, 0x18, 0x30, 0x16, 0x80, 0x14, 0xc8, 0xc5, 0x1d, 0x0, 0x41, 0x9a, 0x24, 0x29, 0x32, 0x51, 0x24, 0xeb, 0xd, 0xae, 0x4a, 0xed, 0x4a, 0x6, 0xd3, 0xec, 0x30, 0x81, 0x8d, 0x6, 0x8, 0x2b, 0x6, 0x1, 0x5, 0x5, 0x7, 0x1, 0x1, 0x4, 0x81, 0x80, 0x30, 0x7e, 0x30, 0x7c, 0x6, 0x8, 0x2b, 0x6, 0x1, 0x5, 0x5, 0x7, 0x30, 0x2, 0x86, 0x70, 0x68, 0x74, 0x74, 0x70, 0x3a, 0x2f, 0x2f, 0x70, 0x72, 0x69, 0x76, 0x61, 0x74, 0x65, 0x63, 0x61, 0x2d, 0x63, 0x6f, 0x6e, 0x74, 0x65, 0x6e, 0x74, 0x2d, 0x36, 0x30, 0x33, 0x66, 0x65, 0x37, 0x65, 0x37, 0x2d, 0x30, 0x30, 0x30, 0x30, 0x2d, 0x32, 0x32, 0x32, 0x37, 0x2d, 0x62, 0x66, 0x37, 0x35, 0x2d, 0x66, 0x34, 0x66, 0x35, 0x65, 0x38, 0x30, 0x64, 0x32, 0x39, 0x35, 0x34, 0x2e, 0x73, 0x74, 0x6f, 0x72, 0x61, 0x67, 0x65, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x61, 0x70, 0x69, 0x73, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x63, 0x61, 0x33, 0x36, 0x61, 0x31, 0x65, 0x39, 0x36, 0x32, 0x34, 0x32, 0x62, 0x39, 0x66, 0x63, 0x62, 0x31, 0x34, 0x36, 0x2f, 0x63, 0x61, 0x2e, 0x63, 0x72, 0x74, 0x30, 0x1d, 0x6, 0x3, 0x55, 0x1d, 0x11, 0x4, 0x16, 0x30, 0x14, 0x81, 0x12, 0x6c, 0x6f, 0x72, 0x65, 0x6e, 0x63, 0x2e, 0x64, 0x40, 0x67, 0x6d, 0x61, 0x69, 0x6c, 0x2e, 0x63, 0x6f, 0x6d, 0x30, 0xa, 0x6, 0x8, 0x2a, 0x86, 0x48, 0xce, 0x3d, 0x4, 0x3, 0x3, 0x3, 0x67, 0x0, 0x30, 0x64, 0x2, 0x30, 0x8, 0x23, 0x39, 0xc8, 0xcd, 0x3f, 0x7c, 0x3f, 0xc4, 0x1f, 0x1b, 0x8b, 0xf8, 0x4a, 0x5c, 0x9f, 0x51, 0x74, 0xd1, 0xff, 0xc, 0xc0, 0x3, 0x8a, 0xeb, 0x6, 0xb5, 0xdd, 0x5d, 0xff, 0x1c, 0x72, 0x15, 0x58, 0xf5, 0xc5, 0x8a, 0x2c, 0x43, 0xbc, 0xb5, 0x1b, 0xac, 0xbc, 0xe1, 0x50, 0xc1, 0xbd, 0x2, 0x30, 0x31, 0xf4, 0x6, 0xf1, 0x3a, 0x7c, 0xce, 0xcc, 0x43, 0x83, 0x93, 0xb3, 0xe2, 0xa5, 0x1, 0x67, 0xf0, 0xa, 0x9a, 0x43, 0xc9, 0x1e, 0x18, 0x26, 0x88, 0x77, 0xed, 0xc1, 0xb6, 0xf9, 0x23, 0x54, 0xbe, 0xd7, 0x17, 0x61, 0xcc, 0x9b, 0x49, 0xf0, 0x48, 0x34, 0x33, 0x4d, 0x3f, 0x9e, 0xaa, 0x24}, +} + +func TestRekorbytes(t *testing.T) { + key, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader) + if err != nil { + t.Fatalf("could not generate key: %v", err) + } + testPub := &key.PublicKey + testPubPEM, err := cryptoutils.MarshalPublicKeyToPEM(testPub) + if err != nil { + t.Fatalf("could not marshall public key to PEM: %v", err) + } + + testCases := []struct { + desc string + + cert *x509.Certificate + pub crypto.PublicKey + + expected []byte + }{{ + desc: "cert only", + + cert: testCert, + expected: []byte(testCertPEM), + }, { + desc: "pub only", + + pub: testPub, + expected: testPubPEM, + }, { + desc: "pub and cert", + + cert: testCert, + pub: testPub, + expected: []byte(testCertPEM), + }} + + for _, tc := range testCases { + t.Run(tc.desc, func(t *testing.T) { + result, err := rekorBytes(tc.cert, tc.pub) + if err != nil { + t.Fatalf("rekorBytes returned error: %v", err) + } + + if !bytes.Equal(result, tc.expected) { + t.Errorf("rekorBytes returned %q, wanted %q", string(result), string(tc.expected)) + } + }) + } +} + +func TestBundle(t *testing.T) { + testEntryTime := int64(1234567890) + testLogIndex := int64(55555) + testLogID := "test log ID" + testBody := "test body" + + testEntry := &models.LogEntryAnon{ + IntegratedTime: &testEntryTime, + Body: testBody, + LogIndex: &testLogIndex, + LogID: &testLogID, + } + + t.Run("no verification", func(t *testing.T) { + result := bundle(testEntry) + if result != nil { + t.Errorf("bundle() returned %v, wanted `nil`", result) + } + }) + + testSET := strfmt.Base64([]byte{0x05, 0x04, 0x03, 0x02, 0x01}) + + testVerification := &models.LogEntryAnonVerification{ + SignedEntryTimestamp: testSET, + } + testEntry.Verification = testVerification + + expected := &oci.Bundle{ + SignedEntryTimestamp: testSET, + Payload: oci.BundlePayload{ + IntegratedTime: testEntryTime, + Body: testBody, + LogIndex: testLogIndex, + LogID: testLogID, + }, + } + + t.Run("happy case", func(t *testing.T) { + result := bundle(testEntry) + if d := cmp.Diff(expected, result); d != "" { + t.Errorf("bundle() returned unexpected result (-want +got): %s", d) + } + }) +}