1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163
|
//go:build debian_no_fulcio
// +build debian_no_fulcio
package rekor
import (
"context"
"crypto/sha256"
"encoding/base64"
"encoding/hex"
"encoding/json"
"errors"
"fmt"
"net/url"
"strings"
"github.com/containers/image/v5/signature/internal"
signerInternal "github.com/containers/image/v5/signature/sigstore/internal"
"github.com/go-openapi/strfmt"
"github.com/go-openapi/swag"
rekor "github.com/sigstore/rekor/pkg/client"
"github.com/sigstore/rekor/pkg/generated/client"
"github.com/sigstore/rekor/pkg/generated/client/entries"
"github.com/sigstore/rekor/pkg/generated/models"
"github.com/sirupsen/logrus"
)
// WithRekor asks the generated signature to be uploaded to the specified Rekor server,
// and to include a log inclusion proof in the signature.
func WithRekor(rekorURL *url.URL) signerInternal.Option {
return func(s *signerInternal.SigstoreSigner) error {
logrus.Debugf("Using Rekor server at %s", rekorURL.Redacted())
client, err := rekor.GetRekorClient(rekorURL.String(),
rekor.WithLogger(leveledLoggerForLogrus(logrus.StandardLogger())))
if err != nil {
return fmt.Errorf("creating Rekor client: %w", err)
}
u := uploader{
client: client,
}
s.RekorUploader = u.uploadKeyOrCert
return nil
}
}
// uploader wraps a Rekor client, basically so that we can set RekorUploader to a method instead of an one-off closure.
type uploader struct {
client *client.Rekor
}
// rekorEntryToSET converts a Rekor log entry into a sigstore “signed entry timestamp”.
func rekorEntryToSET(entry *models.LogEntryAnon) (internal.UntrustedRekorSET, error) {
// We could plausibly call entry.Validate() here; that mostly just uses unnecessary reflection instead of direct == nil checks.
// Right now the only extra validation .Validate() does is *entry.LogIndex >= 0 and a regex check on *entry.LogID;
// we don’t particularly care about either of these (notably signature verification only uses the Body value).
if entry.Verification == nil || entry.IntegratedTime == nil || entry.LogIndex == nil || entry.LogID == nil {
return internal.UntrustedRekorSET{}, fmt.Errorf("invalid Rekor entry (missing data): %#v", *entry)
}
bodyBase64, ok := entry.Body.(string)
if !ok {
return internal.UntrustedRekorSET{}, fmt.Errorf("unexpected Rekor entry body type: %#v", entry.Body)
}
body, err := base64.StdEncoding.DecodeString(bodyBase64)
if err != nil {
return internal.UntrustedRekorSET{}, fmt.Errorf("error parsing Rekor entry body: %w", err)
}
payloadJSON, err := internal.UntrustedRekorPayload{
Body: body,
IntegratedTime: *entry.IntegratedTime,
LogIndex: *entry.LogIndex,
LogID: *entry.LogID,
}.MarshalJSON()
if err != nil {
return internal.UntrustedRekorSET{}, err
}
return internal.UntrustedRekorSET{
UntrustedSignedEntryTimestamp: entry.Verification.SignedEntryTimestamp,
UntrustedPayload: payloadJSON,
}, nil
}
// uploadEntry ensures proposedEntry exists in Rekor (usually uploading it), and returns the resulting log entry.
func (u *uploader) uploadEntry(ctx context.Context, proposedEntry models.ProposedEntry) (models.LogEntry, error) {
params := entries.NewCreateLogEntryParamsWithContext(ctx)
params.SetProposedEntry(proposedEntry)
logrus.Debugf("Calling Rekor's CreateLogEntry")
resp, err := u.client.Entries.CreateLogEntry(params)
if err != nil {
// In ordinary operation, we should not get duplicate entries, because our payload contains a timestamp,
// so it is supposed to be unique; and the default key format, ECDSA p256, also contains a nonce.
// But conflicts can fairly easily happen during debugging and experimentation, so it pays to handle this.
var conflictErr *entries.CreateLogEntryConflict
if errors.As(err, &conflictErr) && conflictErr.Location != "" {
location := conflictErr.Location.String()
logrus.Debugf("CreateLogEntry reported a conflict, location = %s", location)
// We might be able to just GET the returned Location, but let’s use the generated API client.
// OTOH that requires us to hard-code the URI structure…
uuidDelimiter := strings.LastIndexByte(location, '/')
if uuidDelimiter != -1 { // Otherwise the URI is unexpected, and fall through to the bottom
uuid := location[uuidDelimiter+1:]
logrus.Debugf("Calling Rekor's NewGetLogEntryByUUIDParamsWithContext")
params2 := entries.NewGetLogEntryByUUIDParamsWithContext(ctx)
params2.SetEntryUUID(uuid)
resp2, err := u.client.Entries.GetLogEntryByUUID(params2)
if err != nil {
return nil, fmt.Errorf("Error re-loading previously-created log entry with UUID %s: %w", uuid, err)
}
return resp2.GetPayload(), nil
}
}
return nil, fmt.Errorf("Error uploading a log entry: %w", err)
}
return resp.GetPayload(), nil
}
// uploadKeyOrCert integrates this code into sigstore/internal.Signer.
// Given components of the created signature, it returns a SET that should be added to the signature.
func (u *uploader) uploadKeyOrCert(ctx context.Context, keyOrCertBytes []byte, signatureBytes []byte, payloadBytes []byte) ([]byte, error) {
payloadHash := sha256.Sum256(payloadBytes) // HashedRecord only accepts SHA-256
proposedEntry := models.Hashedrekord{
APIVersion: swag.String(internal.HashedRekordV001APIVersion),
Spec: models.HashedrekordV001Schema{
Data: &models.HashedrekordV001SchemaData{
Hash: &models.HashedrekordV001SchemaDataHash{
Algorithm: swag.String(models.HashedrekordV001SchemaDataHashAlgorithmSha256),
Value: swag.String(hex.EncodeToString(payloadHash[:])),
},
},
Signature: &models.HashedrekordV001SchemaSignature{
Content: strfmt.Base64(signatureBytes),
PublicKey: &models.HashedrekordV001SchemaSignaturePublicKey{
Content: strfmt.Base64(keyOrCertBytes),
},
},
},
}
uploadedPayload, err := u.uploadEntry(ctx, &proposedEntry)
if err != nil {
return nil, err
}
if len(uploadedPayload) != 1 {
return nil, fmt.Errorf("expected 1 Rekor entry, got %d", len(uploadedPayload))
}
var storedEntry *models.LogEntryAnon
// This “loop” extracts the single value from the uploadedPayload map.
for _, p := range uploadedPayload {
storedEntry = &p
break
}
rekorBundle, err := rekorEntryToSET(storedEntry)
if err != nil {
return nil, err
}
rekorSETBytes, err := json.Marshal(rekorBundle)
if err != nil {
return nil, err
}
return rekorSETBytes, nil
}
|