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 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239
|
//go:build debian_no_rekor
// +build debian_no_rekor
package internal
import (
"bytes"
"crypto/ecdsa"
"crypto/sha256"
"encoding/base64"
"encoding/hex"
"encoding/json"
"encoding/pem"
"fmt"
"time"
"github.com/cyberphone/json-canonicalization/go/src/webpki.org/jsoncanonicalizer"
"github.com/sigstore/rekor/pkg/generated/models"
)
// This is the github.com/sigstore/rekor/pkg/generated/models.Hashedrekord.APIVersion for github.com/sigstore/rekor/pkg/generated/models.HashedrekordV001Schema.
// We could alternatively use github.com/sigstore/rekor/pkg/types/hashedrekord.APIVERSION, but that subpackage adds too many dependencies.
const HashedRekordV001APIVersion = "0.0.1"
// UntrustedRekorSET is a parsed content of the sigstore-signature Rekor SET
// (note that this a signature-specific format, not a format directly used by the Rekor API).
// This corresponds to github.com/sigstore/cosign/bundle.RekorBundle, but we impose a stricter decoder.
type UntrustedRekorSET struct {
UntrustedSignedEntryTimestamp []byte // A signature over some canonical JSON form of UntrustedPayload
UntrustedPayload json.RawMessage
}
type UntrustedRekorPayload struct {
Body []byte // In cosign, this is an any, but only a string works
IntegratedTime int64
LogIndex int64
LogID string
}
// A compile-time check that UntrustedRekorSET implements json.Unmarshaler
var _ json.Unmarshaler = (*UntrustedRekorSET)(nil)
// UnmarshalJSON implements the json.Unmarshaler interface
func (s *UntrustedRekorSET) UnmarshalJSON(data []byte) error {
err := s.strictUnmarshalJSON(data)
if err != nil {
if formatErr, ok := err.(JSONFormatError); ok {
err = NewInvalidSignatureError(formatErr.Error())
}
}
return err
}
// strictUnmarshalJSON is UnmarshalJSON, except that it may return the internal JSONFormatError error type.
// Splitting it into a separate function allows us to do the JSONFormatError → InvalidSignatureError in a single place, the caller.
func (s *UntrustedRekorSET) strictUnmarshalJSON(data []byte) error {
return ParanoidUnmarshalJSONObjectExactFields(data, map[string]any{
"SignedEntryTimestamp": &s.UntrustedSignedEntryTimestamp,
"Payload": &s.UntrustedPayload,
})
}
// A compile-time check that UntrustedRekorSET and *UntrustedRekorSET implements json.Marshaler
var _ json.Marshaler = UntrustedRekorSET{}
var _ json.Marshaler = (*UntrustedRekorSET)(nil)
// MarshalJSON implements the json.Marshaler interface.
func (s UntrustedRekorSET) MarshalJSON() ([]byte, error) {
return json.Marshal(map[string]any{
"SignedEntryTimestamp": s.UntrustedSignedEntryTimestamp,
"Payload": s.UntrustedPayload,
})
}
// A compile-time check that UntrustedRekorPayload implements json.Unmarshaler
var _ json.Unmarshaler = (*UntrustedRekorPayload)(nil)
// UnmarshalJSON implements the json.Unmarshaler interface
func (p *UntrustedRekorPayload) UnmarshalJSON(data []byte) error {
err := p.strictUnmarshalJSON(data)
if err != nil {
if formatErr, ok := err.(JSONFormatError); ok {
err = NewInvalidSignatureError(formatErr.Error())
}
}
return err
}
// strictUnmarshalJSON is UnmarshalJSON, except that it may return the internal JSONFormatError error type.
// Splitting it into a separate function allows us to do the JSONFormatError → InvalidSignatureError in a single place, the caller.
func (p *UntrustedRekorPayload) strictUnmarshalJSON(data []byte) error {
return ParanoidUnmarshalJSONObjectExactFields(data, map[string]any{
"body": &p.Body,
"integratedTime": &p.IntegratedTime,
"logIndex": &p.LogIndex,
"logID": &p.LogID,
})
}
// A compile-time check that UntrustedRekorPayload and *UntrustedRekorPayload implements json.Marshaler
var _ json.Marshaler = UntrustedRekorPayload{}
var _ json.Marshaler = (*UntrustedRekorPayload)(nil)
// MarshalJSON implements the json.Marshaler interface.
func (p UntrustedRekorPayload) MarshalJSON() ([]byte, error) {
return json.Marshal(map[string]any{
"body": p.Body,
"integratedTime": p.IntegratedTime,
"logIndex": p.LogIndex,
"logID": p.LogID,
})
}
// VerifyRekorSET verifies that unverifiedRekorSET is correctly signed by publicKey and matches the rest of the data.
// Returns bundle upload time on success.
func VerifyRekorSET(publicKey *ecdsa.PublicKey, unverifiedRekorSET []byte, unverifiedKeyOrCertBytes []byte, unverifiedBase64Signature string, unverifiedPayloadBytes []byte) (time.Time, error) {
// FIXME: Should the publicKey parameter hard-code ecdsa?
// == Parse SET bytes
var untrustedSET UntrustedRekorSET
// Sadly. we need to parse and transform untrusted data before verifying a cryptographic signature...
if err := json.Unmarshal(unverifiedRekorSET, &untrustedSET); err != nil {
return time.Time{}, NewInvalidSignatureError(err.Error())
}
// == Verify SET signature
// Cosign unmarshals and re-marshals UntrustedPayload; that seems unnecessary,
// assuming jsoncanonicalizer is designed to operate on untrusted data.
untrustedSETPayloadCanonicalBytes, err := jsoncanonicalizer.Transform(untrustedSET.UntrustedPayload)
if err != nil {
return time.Time{}, NewInvalidSignatureError(fmt.Sprintf("canonicalizing Rekor SET JSON: %v", err))
}
untrustedSETPayloadHash := sha256.Sum256(untrustedSETPayloadCanonicalBytes)
if !ecdsa.VerifyASN1(publicKey, untrustedSETPayloadHash[:], untrustedSET.UntrustedSignedEntryTimestamp) {
return time.Time{}, NewInvalidSignatureError("cryptographic signature verification of Rekor SET failed")
}
// == Parse SET payload
// Parse the cryptographically-verified canonicalized variant, NOT the originally-delivered representation,
// to decrease risk of exploiting the JSON parser. Note that if there were an arbitrary execution vulnerability, the attacker
// could have exploited the parsing of unverifiedRekorSET above already; so this, at best, ensures more consistent processing
// of the SET payload.
var rekorPayload UntrustedRekorPayload
if err := json.Unmarshal(untrustedSETPayloadCanonicalBytes, &rekorPayload); err != nil {
return time.Time{}, NewInvalidSignatureError(fmt.Sprintf("parsing Rekor SET payload: %v", err.Error()))
}
// FIXME: Use a different decoder implementation? The Swagger-generated code is kinda ridiculous, with the need to re-marshal
// hashedRekor.Spec and so on.
// Especially if we anticipate needing to decode different data formats…
// That would also allow being much more strict about JSON.
//
// Alternatively, rely on the existing .Validate() methods instead of manually checking for nil all over the place.
var hashedRekord models.Hashedrekord
if err := json.Unmarshal(rekorPayload.Body, &hashedRekord); err != nil {
return time.Time{}, NewInvalidSignatureError(fmt.Sprintf("decoding the body of a Rekor SET payload: %v", err))
}
// The decode of models.HashedRekord validates the "kind": "hashedrecord" field, which is otherwise invisible to us.
if hashedRekord.APIVersion == nil {
return time.Time{}, NewInvalidSignatureError("missing Rekor SET Payload API version")
}
if *hashedRekord.APIVersion != HashedRekordV001APIVersion {
return time.Time{}, NewInvalidSignatureError(fmt.Sprintf("unsupported Rekor SET Payload hashedrekord version %#v", hashedRekord.APIVersion))
}
hashedRekordV001Bytes, err := json.Marshal(hashedRekord.Spec)
if err != nil {
// Coverage: hashedRekord.Spec is an any that was just unmarshaled,
// so this should never fail.
return time.Time{}, NewInvalidSignatureError(fmt.Sprintf("re-creating hashedrekord spec: %v", err))
}
var hashedRekordV001 models.HashedrekordV001Schema
if err := json.Unmarshal(hashedRekordV001Bytes, &hashedRekordV001); err != nil {
return time.Time{}, NewInvalidSignatureError(fmt.Sprintf("decoding hashedrekod spec: %v", err))
}
// == Match unverifiedKeyOrCertBytes
if hashedRekordV001.Signature == nil {
return time.Time{}, NewInvalidSignatureError(`Missing "signature" field in hashedrekord`)
}
if hashedRekordV001.Signature.PublicKey == nil {
return time.Time{}, NewInvalidSignatureError(`Missing "signature.publicKey" field in hashedrekord`)
}
rekorKeyOrCertPEM, rest := pem.Decode(hashedRekordV001.Signature.PublicKey.Content)
if rekorKeyOrCertPEM == nil {
return time.Time{}, NewInvalidSignatureError("publicKey in Rekor SET is not in PEM format")
}
if len(rest) != 0 {
return time.Time{}, NewInvalidSignatureError("publicKey in Rekor SET has trailing data")
}
// FIXME: For public keys, let the caller provide the DER-formatted blob instead
// of round-tripping through PEM.
unverifiedKeyOrCertPEM, rest := pem.Decode(unverifiedKeyOrCertBytes)
if unverifiedKeyOrCertPEM == nil {
return time.Time{}, NewInvalidSignatureError("public key or cert to be matched against publicKey in Rekor SET is not in PEM format")
}
if len(rest) != 0 {
return time.Time{}, NewInvalidSignatureError("public key or cert to be matched against publicKey in Rekor SET has trailing data")
}
// NOTE: This compares the PEM payload, but not the object type or headers.
if !bytes.Equal(rekorKeyOrCertPEM.Bytes, unverifiedKeyOrCertPEM.Bytes) {
return time.Time{}, NewInvalidSignatureError("publicKey in Rekor SET does not match")
}
// == Match unverifiedSignatureBytes
unverifiedSignatureBytes, err := base64.StdEncoding.DecodeString(unverifiedBase64Signature)
if err != nil {
return time.Time{}, NewInvalidSignatureError(fmt.Sprintf("decoding signature base64: %v", err))
}
if !bytes.Equal(hashedRekordV001.Signature.Content, unverifiedSignatureBytes) {
return time.Time{}, NewInvalidSignatureError(fmt.Sprintf("signature in Rekor SET does not match: %#v vs. %#v",
string(hashedRekordV001.Signature.Content), string(unverifiedSignatureBytes)))
}
// == Match unverifiedPayloadBytes
if hashedRekordV001.Data == nil {
return time.Time{}, NewInvalidSignatureError(`Missing "data" field in hashedrekord`)
}
if hashedRekordV001.Data.Hash == nil {
return time.Time{}, NewInvalidSignatureError(`Missing "data.hash" field in hashedrekord`)
}
if hashedRekordV001.Data.Hash.Algorithm == nil {
return time.Time{}, NewInvalidSignatureError(`Missing "data.hash.algorithm" field in hashedrekord`)
}
if *hashedRekordV001.Data.Hash.Algorithm != models.HashedrekordV001SchemaDataHashAlgorithmSha256 {
return time.Time{}, NewInvalidSignatureError(fmt.Sprintf(`Unexpected "data.hash.algorithm" value %#v`, *hashedRekordV001.Data.Hash.Algorithm))
}
if hashedRekordV001.Data.Hash.Value == nil {
return time.Time{}, NewInvalidSignatureError(`Missing "data.hash.value" field in hashedrekord`)
}
rekorPayloadHash, err := hex.DecodeString(*hashedRekordV001.Data.Hash.Value)
if err != nil {
return time.Time{}, NewInvalidSignatureError(fmt.Sprintf(`Invalid "data.hash.value" field in hashedrekord: %v`, err))
}
unverifiedPayloadHash := sha256.Sum256(unverifiedPayloadBytes)
if !bytes.Equal(rekorPayloadHash, unverifiedPayloadHash[:]) {
return time.Time{}, NewInvalidSignatureError("payload in Rekor SET does not match")
}
// == All OK; return the relevant time.
return time.Unix(rekorPayload.IntegratedTime, 0), nil
}
|