mirror of
https://github.com/containers/image.git
synced 2025-04-18 19:44:05 +03:00
422 lines
20 KiB
Go
422 lines
20 KiB
Go
//go:build !containers_image_rekor_stub
|
|
|
|
package internal
|
|
|
|
import (
|
|
"bytes"
|
|
"crypto"
|
|
"crypto/ecdsa"
|
|
"crypto/elliptic"
|
|
"crypto/rand"
|
|
"crypto/sha256"
|
|
"crypto/sha512"
|
|
"encoding/base64"
|
|
"encoding/hex"
|
|
"encoding/json"
|
|
"os"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/go-openapi/strfmt"
|
|
"github.com/sigstore/rekor/pkg/generated/models"
|
|
"github.com/sigstore/sigstore/pkg/cryptoutils"
|
|
sigstoreSignature "github.com/sigstore/sigstore/pkg/signature"
|
|
"github.com/stretchr/testify/assert"
|
|
"github.com/stretchr/testify/require"
|
|
)
|
|
|
|
// Verify that input can be unmarshaled as an UntrustedRekorSET.
|
|
func successfullyUnmarshalUntrustedRekorSET(t *testing.T, input []byte) UntrustedRekorSET {
|
|
var s UntrustedRekorSET
|
|
err := json.Unmarshal(input, &s)
|
|
require.NoError(t, err, string(input))
|
|
|
|
return s
|
|
}
|
|
|
|
// Verify that input can't be unmarshaled as an UntrustedRekorSET.
|
|
func assertUnmarshalUntrustedRekorSETFails(t *testing.T, input []byte) {
|
|
var s UntrustedRekorSET
|
|
err := json.Unmarshal(input, &s)
|
|
assert.Error(t, err, string(input))
|
|
}
|
|
|
|
func TestUntrustedRekorSETUnmarshalJSON(t *testing.T) {
|
|
// Invalid input. Note that json.Unmarshal is guaranteed to validate input before calling our
|
|
// UnmarshalJSON implementation; so test that first, then test our error handling for completeness.
|
|
assertUnmarshalUntrustedRekorSETFails(t, []byte("&"))
|
|
var s UntrustedRekorSET
|
|
err := s.UnmarshalJSON([]byte("&"))
|
|
assert.Error(t, err)
|
|
|
|
// Not an object
|
|
assertUnmarshalUntrustedRekorSETFails(t, []byte("1"))
|
|
|
|
// Start with a valid JSON.
|
|
validSET := UntrustedRekorSET{
|
|
UntrustedSignedEntryTimestamp: []byte("signedTimestamp#@!"),
|
|
UntrustedPayload: json.RawMessage(`["payload#@!"]`),
|
|
}
|
|
validJSON, err := json.Marshal(validSET)
|
|
require.NoError(t, err)
|
|
|
|
// Success
|
|
s = successfullyUnmarshalUntrustedRekorSET(t, validJSON)
|
|
assert.Equal(t, validSET, s)
|
|
|
|
// A /usr/bin/cosign-generated payload is handled correctly
|
|
setBytes, err := os.ReadFile("testdata/rekor-set")
|
|
require.NoError(t, err)
|
|
s = successfullyUnmarshalUntrustedRekorSET(t, setBytes)
|
|
expectedSET, err := base64.StdEncoding.DecodeString(`MEYCIQDdeujdGLpMTgFdew9wsSJ3WF7olX9PawgzGeX2RmJd8QIhAPxGJf+HjUFVpQc0hgPaUSK8LsONJ08fZFEBVKDeLj4S`)
|
|
require.NoError(t, err)
|
|
assert.Equal(t, UntrustedRekorSET{
|
|
UntrustedSignedEntryTimestamp: expectedSET,
|
|
UntrustedPayload: []byte(`{"body":"eyJhcGlWZXJzaW9uIjoiMC4wLjEiLCJraW5kIjoiaGFzaGVkcmVrb3JkIiwic3BlYyI6eyJkYXRhIjp7Imhhc2giOnsiYWxnb3JpdGhtIjoic2hhMjU2IiwidmFsdWUiOiIwZmQxZTk4MzJjYzVhNWY1MDJlODAwZmU5Y2RlZWZiZDMxMzYyZGYxNmZlOGMyMjUwZDMwOGFlYTNmYjFmYzY5In19LCJzaWduYXR1cmUiOnsiY29udGVudCI6Ik1FUUNJRUpjOTZlMDQxVkFoS0EwM1N2ZkNZYldvZElNSVFQeUF0V3lEUDRGblBxcEFpQWFJUzYwRWpoUkRoU2Fub0Zzb0l5OGZLcXFLZVc1cHMvdExYU0dwYXlpMmc9PSIsInB1YmxpY0tleSI6eyJjb250ZW50IjoiTFMwdExTMUNSVWRKVGlCRFJWSlVTVVpKUTBGVVJTMHRMUzB0Q2sxSlNVTnVWRU5EUVdsUFowRjNTVUpCWjBsVlJ6UTFkV0ZETW5vNFZuWjFUM2Q2ZW0wM09WSlFabU5yYjNoM2QwTm5XVWxMYjFwSmVtb3dSVUYzVFhjS1RucEZWazFDVFVkQk1WVkZRMmhOVFdNeWJHNWpNMUoyWTIxVmRWcEhWakpOVWpSM1NFRlpSRlpSVVVSRmVGWjZZVmRrZW1SSE9YbGFVekZ3WW01U2JBcGpiVEZzV2tkc2FHUkhWWGRJYUdOT1RXcEplRTFxUlhsTlZHY3dUMFJGTkZkb1kwNU5ha2w0VFdwRmVVMVVaekZQUkVVMFYycEJRVTFHYTNkRmQxbElDa3R2V2tsNmFqQkRRVkZaU1V0dldrbDZhakJFUVZGalJGRm5RVVZLUW5WeWEyaFRVbTFJZFd0SVZtZ3pWa0U0YmxsMVUxZHhXalJzZEdGbVJIZDVWMndLWXpOak9VNWhURzkyYlhVclRrTTBUbWxWZEUxTWFXWk1MMUF6Ym5GbFpHSnVZM1JMUW5WWmJXWkpVMGRwV214V2VVdFBRMEZWU1hkblowVXJUVUUwUndwQk1WVmtSSGRGUWk5M1VVVkJkMGxJWjBSQlZFSm5UbFpJVTFWRlJFUkJTMEpuWjNKQ1owVkdRbEZqUkVGNlFXUkNaMDVXU0ZFMFJVWm5VVlZCYlhaSENqUkxObXhPYXk5emF5OW1OR0ZwWVdocVdWSnhVaXRqZDBoM1dVUldVakJxUWtKbmQwWnZRVlV6T1ZCd2VqRlphMFZhWWpWeFRtcHdTMFpYYVhocE5Ga0tXa1E0ZDBoUldVUldVakJTUVZGSUwwSkNUWGRGV1VWUVlsZHNNR05yUW5sYVYxSnZXVmhSZFZreU9YUk5RM2RIUTJselIwRlJVVUpuTnpoM1FWRkZSUXBJYldnd1pFaENlazlwT0haYU1td3dZVWhXYVV4dFRuWmlVemx6WWpKa2NHSnBPWFpaV0ZZd1lVUkRRbWxSV1V0TGQxbENRa0ZJVjJWUlNVVkJaMUkzQ2tKSWEwRmtkMEl4UVU0d09VMUhja2Q0ZUVWNVdYaHJaVWhLYkc1T2QwdHBVMncyTkROcWVYUXZOR1ZMWTI5QmRrdGxOazlCUVVGQ2FGRmxjV3gzVlVFS1FVRlJSRUZGV1hkU1FVbG5WbWt5VTNaT05WSmxSMWxwVDFOb1dUaE1SbE5TUnpWRU5tOUJWRXR4U0dJMmEwNHZSRXBvTW5KQlZVTkpRVTVUTmtGeGNBcDRZVmhwU0hkVVNGVnlNM2hRVTBkaE5XazJhSGwzYldKaVVrTTJUakJyU1dWRVRUWk5RVzlIUTBOeFIxTk5ORGxDUVUxRVFUSm5RVTFIVlVOTlEweDFDbU5hZEVWVFNVNHdiRzAyTkVOdkwySmFOamhEUTFKclYyeHJkRmcwYlcxS2FWSm9TMms1WXpsUlJEWXlRelZUZFZwb1l6QjJkbTgyVFU5TGJWUlJTWGdLUVVsdkwwMXZlbHBsYjFVM2NtUk9hakJ3V2t0MVFtVkRiVTF4YlVwaFJGTnpkekU1ZEV0cEwySXhjRVZ0ZFhjclUyWXlRa2t5TlVkblNXSkxlblJITVFvNWR6MDlDaTB0TFMwdFJVNUVJRU5GVWxSSlJrbERRVlJGTFMwdExTMEsifX19fQ==","integratedTime":1670870899,"logIndex":8949589,"logID":"c0d23d6ad406973f9559f3ba2d1ca01f84147d8ffc5b8445c224f98b9591801d"}`),
|
|
}, s)
|
|
|
|
// Various ways to corrupt the JSON
|
|
breakFns := []func(mSA){
|
|
// A top-level field is missing
|
|
func(v mSA) { delete(v, "SignedEntryTimestamp") },
|
|
func(v mSA) { delete(v, "Payload") },
|
|
// Extra top-level sub-object
|
|
func(v mSA) { v["unexpected"] = 1 },
|
|
// "SignedEntryTimestamp" not a string
|
|
func(v mSA) { v["critical"] = 1 },
|
|
// "Payload" not an object
|
|
func(v mSA) { v["optional"] = 1 },
|
|
}
|
|
for _, fn := range breakFns {
|
|
testJSON := modifiedJSON(t, validJSON, fn)
|
|
assertUnmarshalUntrustedRekorSETFails(t, testJSON)
|
|
}
|
|
}
|
|
|
|
// Verify that input can be unmarshaled as an UntrustedRekorPayload.
|
|
func successfullyUnmarshalUntrustedRekorPayload(t *testing.T, input []byte) UntrustedRekorPayload {
|
|
var s UntrustedRekorPayload
|
|
err := json.Unmarshal(input, &s)
|
|
require.NoError(t, err, string(input))
|
|
|
|
return s
|
|
}
|
|
|
|
// Verify that input can't be unmarshaled as an UntrustedRekorPayload.
|
|
func assertUnmarshalUntrustedRekorPayloadFails(t *testing.T, input []byte) {
|
|
var s UntrustedRekorPayload
|
|
err := json.Unmarshal(input, &s)
|
|
assert.Error(t, err, string(input))
|
|
}
|
|
|
|
func TestUntrustedRekorPayloadUnmarshalJSON(t *testing.T) {
|
|
// Invalid input. Note that json.Unmarshal is guaranteed to validate input before calling our
|
|
// UnmarshalJSON implementation; so test that first, then test our error handling for completeness.
|
|
assertUnmarshalUntrustedRekorPayloadFails(t, []byte("&"))
|
|
var p UntrustedRekorPayload
|
|
err := p.UnmarshalJSON([]byte("&"))
|
|
assert.Error(t, err)
|
|
|
|
// Not an object
|
|
assertUnmarshalUntrustedRekorPayloadFails(t, []byte("1"))
|
|
|
|
// Start with a valid JSON.
|
|
validPayload := UntrustedRekorPayload{
|
|
Body: []byte(`["json"]`),
|
|
IntegratedTime: 1,
|
|
LogIndex: 2,
|
|
LogID: "abc",
|
|
}
|
|
validJSON, err := validPayload.MarshalJSON()
|
|
require.NoError(t, err)
|
|
|
|
// Success
|
|
p = successfullyUnmarshalUntrustedRekorPayload(t, validJSON)
|
|
assert.Equal(t, validPayload, p)
|
|
|
|
// A /usr/bin/cosign-generated payload is handled correctly
|
|
setBytes, err := os.ReadFile("testdata/rekor-set")
|
|
require.NoError(t, err)
|
|
s := successfullyUnmarshalUntrustedRekorSET(t, setBytes)
|
|
p = successfullyUnmarshalUntrustedRekorPayload(t, s.UntrustedPayload)
|
|
expectedBody, err := base64.StdEncoding.DecodeString(`eyJhcGlWZXJzaW9uIjoiMC4wLjEiLCJraW5kIjoiaGFzaGVkcmVrb3JkIiwic3BlYyI6eyJkYXRhIjp7Imhhc2giOnsiYWxnb3JpdGhtIjoic2hhMjU2IiwidmFsdWUiOiIwZmQxZTk4MzJjYzVhNWY1MDJlODAwZmU5Y2RlZWZiZDMxMzYyZGYxNmZlOGMyMjUwZDMwOGFlYTNmYjFmYzY5In19LCJzaWduYXR1cmUiOnsiY29udGVudCI6Ik1FUUNJRUpjOTZlMDQxVkFoS0EwM1N2ZkNZYldvZElNSVFQeUF0V3lEUDRGblBxcEFpQWFJUzYwRWpoUkRoU2Fub0Zzb0l5OGZLcXFLZVc1cHMvdExYU0dwYXlpMmc9PSIsInB1YmxpY0tleSI6eyJjb250ZW50IjoiTFMwdExTMUNSVWRKVGlCRFJWSlVTVVpKUTBGVVJTMHRMUzB0Q2sxSlNVTnVWRU5EUVdsUFowRjNTVUpCWjBsVlJ6UTFkV0ZETW5vNFZuWjFUM2Q2ZW0wM09WSlFabU5yYjNoM2QwTm5XVWxMYjFwSmVtb3dSVUYzVFhjS1RucEZWazFDVFVkQk1WVkZRMmhOVFdNeWJHNWpNMUoyWTIxVmRWcEhWakpOVWpSM1NFRlpSRlpSVVVSRmVGWjZZVmRrZW1SSE9YbGFVekZ3WW01U2JBcGpiVEZzV2tkc2FHUkhWWGRJYUdOT1RXcEplRTFxUlhsTlZHY3dUMFJGTkZkb1kwNU5ha2w0VFdwRmVVMVVaekZQUkVVMFYycEJRVTFHYTNkRmQxbElDa3R2V2tsNmFqQkRRVkZaU1V0dldrbDZhakJFUVZGalJGRm5RVVZLUW5WeWEyaFRVbTFJZFd0SVZtZ3pWa0U0YmxsMVUxZHhXalJzZEdGbVJIZDVWMndLWXpOak9VNWhURzkyYlhVclRrTTBUbWxWZEUxTWFXWk1MMUF6Ym5GbFpHSnVZM1JMUW5WWmJXWkpVMGRwV214V2VVdFBRMEZWU1hkblowVXJUVUUwUndwQk1WVmtSSGRGUWk5M1VVVkJkMGxJWjBSQlZFSm5UbFpJVTFWRlJFUkJTMEpuWjNKQ1owVkdRbEZqUkVGNlFXUkNaMDVXU0ZFMFJVWm5VVlZCYlhaSENqUkxObXhPYXk5emF5OW1OR0ZwWVdocVdWSnhVaXRqZDBoM1dVUldVakJxUWtKbmQwWnZRVlV6T1ZCd2VqRlphMFZhWWpWeFRtcHdTMFpYYVhocE5Ga0tXa1E0ZDBoUldVUldVakJTUVZGSUwwSkNUWGRGV1VWUVlsZHNNR05yUW5sYVYxSnZXVmhSZFZreU9YUk5RM2RIUTJselIwRlJVVUpuTnpoM1FWRkZSUXBJYldnd1pFaENlazlwT0haYU1td3dZVWhXYVV4dFRuWmlVemx6WWpKa2NHSnBPWFpaV0ZZd1lVUkRRbWxSV1V0TGQxbENRa0ZJVjJWUlNVVkJaMUkzQ2tKSWEwRmtkMEl4UVU0d09VMUhja2Q0ZUVWNVdYaHJaVWhLYkc1T2QwdHBVMncyTkROcWVYUXZOR1ZMWTI5QmRrdGxOazlCUVVGQ2FGRmxjV3gzVlVFS1FVRlJSRUZGV1hkU1FVbG5WbWt5VTNaT05WSmxSMWxwVDFOb1dUaE1SbE5TUnpWRU5tOUJWRXR4U0dJMmEwNHZSRXBvTW5KQlZVTkpRVTVUTmtGeGNBcDRZVmhwU0hkVVNGVnlNM2hRVTBkaE5XazJhSGwzYldKaVVrTTJUakJyU1dWRVRUWk5RVzlIUTBOeFIxTk5ORGxDUVUxRVFUSm5RVTFIVlVOTlEweDFDbU5hZEVWVFNVNHdiRzAyTkVOdkwySmFOamhEUTFKclYyeHJkRmcwYlcxS2FWSm9TMms1WXpsUlJEWXlRelZUZFZwb1l6QjJkbTgyVFU5TGJWUlJTWGdLUVVsdkwwMXZlbHBsYjFVM2NtUk9hakJ3V2t0MVFtVkRiVTF4YlVwaFJGTnpkekU1ZEV0cEwySXhjRVZ0ZFhjclUyWXlRa2t5TlVkblNXSkxlblJITVFvNWR6MDlDaTB0TFMwdFJVNUVJRU5GVWxSSlJrbERRVlJGTFMwdExTMEsifX19fQ==`)
|
|
require.NoError(t, err)
|
|
assert.Equal(t, UntrustedRekorPayload{
|
|
Body: expectedBody,
|
|
IntegratedTime: 1670870899,
|
|
LogIndex: 8949589,
|
|
LogID: "c0d23d6ad406973f9559f3ba2d1ca01f84147d8ffc5b8445c224f98b9591801d",
|
|
}, p)
|
|
|
|
// Various ways to corrupt the JSON
|
|
breakFns := []func(mSA){
|
|
// A top-level field is missing
|
|
func(v mSA) { delete(v, "body") },
|
|
func(v mSA) { delete(v, "integratedTime") },
|
|
func(v mSA) { delete(v, "logIndex") },
|
|
func(v mSA) { delete(v, "logID") },
|
|
// Extra top-level sub-object
|
|
func(v mSA) { v["unexpected"] = 1 },
|
|
// "body" not a string
|
|
func(v mSA) { v["body"] = 1 },
|
|
// "integratedTime" not an integer
|
|
func(v mSA) { v["integratedTime"] = "hello" },
|
|
// "logIndex" not an integer
|
|
func(v mSA) { v["logIndex"] = "hello" },
|
|
// "logID" not a string
|
|
func(v mSA) { v["logID"] = 1 },
|
|
}
|
|
for _, fn := range breakFns {
|
|
testJSON := modifiedJSON(t, validJSON, fn)
|
|
assertUnmarshalUntrustedRekorPayloadFails(t, testJSON)
|
|
}
|
|
}
|
|
|
|
// stringPtr returns a pointer to the provided string value.
|
|
func stringPtr(s string) *string {
|
|
return &s
|
|
}
|
|
|
|
func TestVerifyRekorSET(t *testing.T) {
|
|
cosignRekorKeyPEM, err := os.ReadFile("testdata/rekor.pub")
|
|
require.NoError(t, err)
|
|
cosignRekorKey, err := cryptoutils.UnmarshalPEMToPublicKey(cosignRekorKeyPEM)
|
|
require.NoError(t, err)
|
|
cosignRekorKeyECDSA, ok := cosignRekorKey.(*ecdsa.PublicKey)
|
|
require.True(t, ok)
|
|
cosignRekorKeysECDSA := []*ecdsa.PublicKey{cosignRekorKeyECDSA}
|
|
cosignSETBytes, err := os.ReadFile("testdata/rekor-set")
|
|
require.NoError(t, err)
|
|
cosignCertBytes, err := os.ReadFile("testdata/rekor-cert")
|
|
require.NoError(t, err)
|
|
cosignSigBase64, err := os.ReadFile("testdata/rekor-sig")
|
|
require.NoError(t, err)
|
|
cosignPayloadBytes, err := os.ReadFile("testdata/rekor-payload")
|
|
require.NoError(t, err)
|
|
mismatchingKey, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader) // A key which did not sign anything
|
|
require.NoError(t, err)
|
|
|
|
// Successful verification
|
|
for _, acceptableKeys := range [][]*ecdsa.PublicKey{
|
|
{cosignRekorKeyECDSA},
|
|
{cosignRekorKeyECDSA, &mismatchingKey.PublicKey},
|
|
{&mismatchingKey.PublicKey, cosignRekorKeyECDSA},
|
|
} {
|
|
tm, err := VerifyRekorSET(acceptableKeys, cosignSETBytes, cosignCertBytes, string(cosignSigBase64), cosignPayloadBytes)
|
|
require.NoError(t, err)
|
|
assert.Equal(t, time.Unix(1670870899, 0), tm)
|
|
}
|
|
|
|
// For extra paranoia, test that we return a zero time on error.
|
|
|
|
// A completely invalid SET.
|
|
tm, err := VerifyRekorSET(cosignRekorKeysECDSA, []byte{}, cosignCertBytes, string(cosignSigBase64), cosignPayloadBytes)
|
|
assert.Error(t, err)
|
|
assert.Zero(t, tm)
|
|
|
|
tm, err = VerifyRekorSET(cosignRekorKeysECDSA, []byte("invalid signature"), cosignCertBytes, string(cosignSigBase64), cosignPayloadBytes)
|
|
assert.Error(t, err)
|
|
assert.Zero(t, tm)
|
|
|
|
testKey, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
|
|
require.NoError(t, err)
|
|
testPublicKeys := []*ecdsa.PublicKey{&testKey.PublicKey}
|
|
testSigner, err := sigstoreSignature.LoadECDSASigner(testKey, crypto.SHA256)
|
|
require.NoError(t, err)
|
|
|
|
// JSON canonicalization fails:
|
|
// This payload is invalid because it has duplicate fields.
|
|
// Right now, that particular failure (unlike more blatantly invalid JSON) is allowed
|
|
// by json.Marshal, but detected by jsoncanonicalizer.Transform.
|
|
invalidPayload := []byte(`{"logIndex":1, "integratedTime":2,"body":"abc","logID":"def","body":"ABC"}`)
|
|
invalidPayloadSig, err := testSigner.SignMessage(bytes.NewReader(invalidPayload))
|
|
require.NoError(t, err)
|
|
invalidSET, err := json.Marshal(UntrustedRekorSET{
|
|
UntrustedSignedEntryTimestamp: invalidPayloadSig,
|
|
UntrustedPayload: json.RawMessage(invalidPayload),
|
|
})
|
|
require.NoError(t, err)
|
|
tm, err = VerifyRekorSET(testPublicKeys, invalidSET, cosignCertBytes, string(cosignSigBase64), cosignPayloadBytes)
|
|
assert.Error(t, err)
|
|
assert.Zero(t, tm)
|
|
|
|
// Cryptographic verification fails (a mismatched public key)
|
|
for _, mismatchingKeys := range [][]*ecdsa.PublicKey{
|
|
{&testKey.PublicKey},
|
|
{&testKey.PublicKey, &mismatchingKey.PublicKey},
|
|
} {
|
|
tm, err := VerifyRekorSET(mismatchingKeys, cosignSETBytes, cosignCertBytes, string(cosignSigBase64), cosignPayloadBytes)
|
|
assert.Error(t, err)
|
|
assert.Zero(t, tm)
|
|
}
|
|
|
|
// Parsing UntrustedRekorPayload fails
|
|
invalidPayload = []byte(`{}`)
|
|
invalidPayloadSig, err = testSigner.SignMessage(bytes.NewReader(invalidPayload))
|
|
require.NoError(t, err)
|
|
invalidSET, err = json.Marshal(UntrustedRekorSET{
|
|
UntrustedSignedEntryTimestamp: invalidPayloadSig,
|
|
UntrustedPayload: json.RawMessage(invalidPayload),
|
|
})
|
|
require.NoError(t, err)
|
|
tm, err = VerifyRekorSET(testPublicKeys, invalidSET, cosignCertBytes, string(cosignSigBase64), cosignPayloadBytes)
|
|
assert.Error(t, err)
|
|
assert.Zero(t, tm)
|
|
|
|
// A correctly signed UntrustedRekorPayload is invalid
|
|
cosignPayloadSHA256 := sha256.Sum256(cosignPayloadBytes)
|
|
cosignSigBytes, err := base64.StdEncoding.DecodeString(string(cosignSigBase64))
|
|
require.NoError(t, err)
|
|
validHashedRekord := models.Hashedrekord{
|
|
APIVersion: stringPtr(HashedRekordV001APIVersion),
|
|
Spec: models.HashedrekordV001Schema{
|
|
Data: &models.HashedrekordV001SchemaData{
|
|
Hash: &models.HashedrekordV001SchemaDataHash{
|
|
Algorithm: stringPtr(models.HashedrekordV001SchemaDataHashAlgorithmSha256),
|
|
Value: stringPtr(hex.EncodeToString(cosignPayloadSHA256[:])),
|
|
},
|
|
},
|
|
Signature: &models.HashedrekordV001SchemaSignature{
|
|
Content: strfmt.Base64(cosignSigBytes),
|
|
PublicKey: &models.HashedrekordV001SchemaSignaturePublicKey{
|
|
Content: strfmt.Base64(cosignCertBytes),
|
|
},
|
|
},
|
|
},
|
|
}
|
|
validHashedRekordJSON, err := json.Marshal(validHashedRekord)
|
|
require.NoError(t, err)
|
|
for _, fn := range []func(mSA){
|
|
// A Hashedrekord field is missing
|
|
func(v mSA) { delete(v, "apiVersion") },
|
|
func(v mSA) { delete(v, "kind") }, // "kind" is not visible in the type definition, but required by the implementation
|
|
func(v mSA) { delete(v, "spec") },
|
|
// This, along with many other extra fields, is currently accepted. That is NOT an API commitment.
|
|
// func(v mSA) { v["unexpected"] = 1 }, // Extra top-level field:
|
|
// Invalid apiVersion
|
|
func(v mSA) { v["apiVersion"] = nil },
|
|
func(v mSA) { v["apiVersion"] = 1 },
|
|
func(v mSA) { v["apiVersion"] = mSA{} },
|
|
func(v mSA) { v["apiVersion"] = "99.0.99" },
|
|
// Invalid kind
|
|
func(v mSA) { v["kind"] = nil },
|
|
func(v mSA) { v["kind"] = 1 },
|
|
func(v mSA) { v["kind"] = "notHashedRekord" },
|
|
// Invalid spec
|
|
func(v mSA) { v["spec"] = nil },
|
|
func(v mSA) { v["spec"] = 1 },
|
|
// A HashedRekordV001Schema field is missing
|
|
func(v mSA) { delete(x(v, "spec"), "data") },
|
|
func(v mSA) { delete(x(v, "spec"), "signature") },
|
|
// Invalid spec.data
|
|
func(v mSA) { x(v, "spec")["data"] = nil },
|
|
func(v mSA) { x(v, "spec")["data"] = 1 },
|
|
// Missing spec.data.hash
|
|
func(v mSA) { delete(x(v, "spec", "data"), "hash") },
|
|
// Invalid spec.data.hash
|
|
func(v mSA) { x(v, "spec", "data")["hash"] = nil },
|
|
func(v mSA) { x(v, "spec", "data")["hash"] = 1 },
|
|
// A spec.data.hash field is missing
|
|
func(v mSA) { delete(x(v, "spec", "data", "hash"), "algorithm") },
|
|
func(v mSA) { delete(x(v, "spec", "data", "hash"), "value") },
|
|
// Invalid spec.data.hash.algorithm
|
|
func(v mSA) { x(v, "spec", "data", "hash")["algorithm"] = nil },
|
|
func(v mSA) { x(v, "spec", "data", "hash")["algorithm"] = 1 },
|
|
// Invalid spec.data.hash.value
|
|
func(v mSA) { x(v, "spec", "data", "hash")["value"] = nil },
|
|
func(v mSA) { x(v, "spec", "data", "hash")["value"] = 1 },
|
|
func(v mSA) { // An odd number of hexadecimal digits
|
|
x(v, "spec", "data", "hash")["value"] = "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"
|
|
},
|
|
// spec.data.hash does not match
|
|
func(v mSA) {
|
|
x(v, "spec", "data", "hash")["value"] = "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"
|
|
},
|
|
// A non-sha256 hash
|
|
func(v mSA) {
|
|
x(v, "spec", "data", "hash")["algorithm"] = "sha512"
|
|
h := sha512.Sum512(cosignPayloadBytes)
|
|
x(v, "spec", "data", "hash")["value"] = hex.EncodeToString(h[:])
|
|
},
|
|
// Invalid spec.signature
|
|
func(v mSA) { x(v, "spec")["signature"] = nil },
|
|
func(v mSA) { x(v, "spec")["signature"] = 1 },
|
|
// A spec.signature field is missing
|
|
func(v mSA) { delete(x(v, "spec", "signature"), "content") },
|
|
func(v mSA) { delete(x(v, "spec", "signature"), "publicKey") },
|
|
// Invalid spec.signature.content
|
|
func(v mSA) { x(v, "spec", "signature")["content"] = nil },
|
|
func(v mSA) { x(v, "spec", "signature")["content"] = 1 },
|
|
func(v mSA) { x(v, "spec", "signature")["content"] = "" },
|
|
func(v mSA) { x(v, "spec", "signature")["content"] = "+" }, // Invalid base64
|
|
// spec.signature.content does not match
|
|
func(v mSA) {
|
|
x(v, "spec", "signature")["content"] = base64.StdEncoding.EncodeToString([]byte("does not match"))
|
|
},
|
|
// Invalid spec.signature.publicKey
|
|
func(v mSA) { x(v, "spec", "signature")["publicKey"] = nil },
|
|
func(v mSA) { x(v, "spec", "signature")["publicKey"] = 1 },
|
|
// Missing spec.signature.publicKey.content
|
|
func(v mSA) { delete(x(v, "spec", "signature", "publicKey"), "content") },
|
|
// Invalid spec.signature.publicKey.content
|
|
func(v mSA) { x(v, "spec", "signature", "publicKey")["content"] = nil },
|
|
func(v mSA) { x(v, "spec", "signature", "publicKey")["content"] = 1 },
|
|
func(v mSA) { x(v, "spec", "signature", "publicKey")["content"] = "" },
|
|
func(v mSA) { x(v, "spec", "signature", "publicKey")["content"] = "+" }, // Invalid base64
|
|
func(v mSA) {
|
|
x(v, "spec", "signature", "publicKey")["content"] = base64.StdEncoding.EncodeToString([]byte("not PEM"))
|
|
},
|
|
func(v mSA) { // Multiple PEM blocks
|
|
x(v, "spec", "signature", "publicKey")["content"] = base64.StdEncoding.EncodeToString(bytes.Repeat(cosignCertBytes, 2))
|
|
},
|
|
// spec.signature.publicKey.content does not match
|
|
func(v mSA) {
|
|
otherKey, err := testSigner.PublicKey()
|
|
require.NoError(t, err)
|
|
otherPEM, err := cryptoutils.MarshalPublicKeyToPEM(otherKey)
|
|
require.NoError(t, err)
|
|
x(v, "spec", "signature", "publicKey")["content"] = base64.StdEncoding.EncodeToString(otherPEM)
|
|
},
|
|
} {
|
|
testHashedRekordJSON := modifiedJSON(t, validHashedRekordJSON, fn)
|
|
testPayload, err := json.Marshal(UntrustedRekorPayload{
|
|
Body: testHashedRekordJSON,
|
|
IntegratedTime: 1,
|
|
LogIndex: 2,
|
|
LogID: "logID",
|
|
})
|
|
require.NoError(t, err)
|
|
testPayloadSig, err := testSigner.SignMessage(bytes.NewReader(testPayload))
|
|
require.NoError(t, err)
|
|
testSET, err := json.Marshal(UntrustedRekorSET{
|
|
UntrustedSignedEntryTimestamp: testPayloadSig,
|
|
UntrustedPayload: json.RawMessage(testPayload),
|
|
})
|
|
require.NoError(t, err)
|
|
tm, err = VerifyRekorSET(testPublicKeys, testSET, cosignCertBytes, string(cosignSigBase64), cosignPayloadBytes)
|
|
assert.Error(t, err)
|
|
assert.Zero(t, tm)
|
|
}
|
|
|
|
// Invalid unverifiedBase64Signature parameter
|
|
truncatedBase64 := cosignSigBase64
|
|
truncatedBase64 = truncatedBase64[:len(truncatedBase64)-1]
|
|
tm, err = VerifyRekorSET(cosignRekorKeysECDSA, cosignSETBytes, cosignCertBytes,
|
|
string(truncatedBase64), cosignPayloadBytes)
|
|
assert.Error(t, err)
|
|
assert.Zero(t, tm)
|
|
|
|
// Invalid unverifiedKeyOrCertBytes
|
|
for _, c := range [][]byte{
|
|
nil,
|
|
{},
|
|
[]byte("this is not PEM"),
|
|
bytes.Repeat(cosignCertBytes, 2),
|
|
} {
|
|
tm, err = VerifyRekorSET(cosignRekorKeysECDSA, cosignSETBytes, c,
|
|
string(cosignSigBase64), cosignPayloadBytes)
|
|
assert.Error(t, err)
|
|
assert.Zero(t, tm)
|
|
}
|
|
}
|