1
0
mirror of https://github.com/containers/image.git synced 2025-04-18 19:44:05 +03:00

Merge pull request #2531 from mtrmac/5.32-backport

Release 5.32.2
This commit is contained in:
Tom Sweeney 2024-08-20 13:05:58 -04:00 committed by GitHub
commit 51d37e8368
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
16 changed files with 858 additions and 246 deletions

View File

@ -320,7 +320,9 @@ This requirement requires an image to be signed using a sigstore signature with
{
"type": "sigstoreSigned",
"keyPath": "/path/to/local/public/key/file",
"keyPaths": ["/path/to/first/public/key/one", "/path/to/first/public/key/two"],
"keyData": "base64-encoded-public-key-data",
"keyDatas": ["base64-encoded-public-key-one-data", "base64-encoded-public-key-two-data"]
"fulcio": {
"caPath": "/path/to/local/CA/file",
"caData": "base64-encoded-CA-data",
@ -328,28 +330,33 @@ This requirement requires an image to be signed using a sigstore signature with
"subjectEmail", "expected-signing-user@example.com",
},
"rekorPublicKeyPath": "/path/to/local/public/key/file",
"rekorPublicKeyPaths": ["/path/to/local/public/key/one","/path/to/local/public/key/two"],
"rekorPublicKeyData": "base64-encoded-public-key-data",
"rekorPublicKeyDatas": ["base64-encoded-public-key-one-data","base64-encoded-public-key-two-data"],
"signedIdentity": identity_requirement
}
```
Exactly one of `keyPath`, `keyData` and `fulcio` must be present.
Exactly one of `keyPath`, `keyPaths`, `keyData`, `keyDatas` and `fulcio` must be present.
If `keyPath` or `keyData` is present, it contains a sigstore public key.
Only signatures made by this key are accepted.
If `keyPaths` or `keyDatas` is present, it contains sigstore public keys.
Only signatures made by any key in the list are accepted.
If `fulcio` is present, the signature must be based on a Fulcio-issued certificate.
One of `caPath` and `caData` must be specified, containing the public key of the Fulcio instance.
Both `oidcIssuer` and `subjectEmail` are mandatory,
exactly specifying the expected identity provider,
and the identity of the user obtaining the Fulcio certificate.
At most one of `rekorPublicKeyPath` and `rekorPublicKeyData` can be present;
At most one of `rekorPublicKeyPath`, `rekorPublicKeyPaths`, `rekorPublicKeyData` and `rekorPublicKeyDatas` can be present;
it is mandatory if `fulcio` is specified.
If a Rekor public key is specified,
the signature must have been uploaded to a Rekor server
and the signature must contain an (offline-verifiable) “signed entry timestamp”
proving the existence of the Rekor log record,
signed by the provided public key.
signed by one of the provided public keys.
The `signedIdentity` field has the same semantics as in the `signedBy` requirement described above.
Note that `cosign`-created signatures only contain a repository, so only `matchRepository` and `exactRepository` can be used to accept them (and that does not protect against substitution of a signed image with an unexpected tag).

View File

@ -195,10 +195,10 @@ func (f *fulcioTrustRoot) verifyFulcioCertificateAtTime(relevantTime time.Time,
return untrustedCertificate.PublicKey, nil
}
func verifyRekorFulcio(rekorPublicKey *ecdsa.PublicKey, fulcioTrustRoot *fulcioTrustRoot, untrustedRekorSET []byte,
func verifyRekorFulcio(rekorPublicKeys []*ecdsa.PublicKey, fulcioTrustRoot *fulcioTrustRoot, untrustedRekorSET []byte,
untrustedCertificateBytes []byte, untrustedIntermediateChainBytes []byte, untrustedBase64Signature string,
untrustedPayloadBytes []byte) (crypto.PublicKey, error) {
rekorSETTime, err := internal.VerifyRekorSET(rekorPublicKey, untrustedRekorSET, untrustedCertificateBytes,
rekorSETTime, err := internal.VerifyRekorSET(rekorPublicKeys, untrustedRekorSET, untrustedCertificateBytes,
untrustedBase64Signature, untrustedPayloadBytes)
if err != nil {
return nil, err

View File

@ -442,6 +442,7 @@ func TestVerifyRekorFulcio(t *testing.T) {
require.NoError(t, err)
rekorKeyECDSA, ok := rekorKey.(*ecdsa.PublicKey)
require.True(t, ok)
rekorKeysECDSA := []*ecdsa.PublicKey{rekorKeyECDSA}
setBytes, err := os.ReadFile("fixtures/rekor-set")
require.NoError(t, err)
sigBase64, err := os.ReadFile("fixtures/rekor-sig")
@ -450,7 +451,7 @@ func TestVerifyRekorFulcio(t *testing.T) {
require.NoError(t, err)
// Success
pk, err := verifyRekorFulcio(rekorKeyECDSA, &fulcioTrustRoot{
pk, err := verifyRekorFulcio(rekorKeysECDSA, &fulcioTrustRoot{
caCertificates: caCertificates,
oidcIssuer: "https://github.com/login/oauth",
subjectEmail: "mitr@redhat.com",
@ -459,7 +460,7 @@ func TestVerifyRekorFulcio(t *testing.T) {
assertPublicKeyMatchesCert(t, certBytes, pk)
// Rekor failure
pk, err = verifyRekorFulcio(rekorKeyECDSA, &fulcioTrustRoot{
pk, err = verifyRekorFulcio(rekorKeysECDSA, &fulcioTrustRoot{
caCertificates: caCertificates,
oidcIssuer: "https://github.com/login/oauth",
subjectEmail: "mitr@redhat.com",
@ -468,7 +469,7 @@ func TestVerifyRekorFulcio(t *testing.T) {
assert.Nil(t, pk)
// Fulcio failure
pk, err = verifyRekorFulcio(rekorKeyECDSA, &fulcioTrustRoot{
pk, err = verifyRekorFulcio(rekorKeysECDSA, &fulcioTrustRoot{
caCertificates: caCertificates,
oidcIssuer: "https://github.com/login/oauth",
subjectEmail: "this-does-not-match@example.com",

View File

@ -113,7 +113,7 @@ func (p UntrustedRekorPayload) MarshalJSON() ([]byte, error) {
// 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) {
func VerifyRekorSET(publicKeys []*ecdsa.PublicKey, unverifiedRekorSET []byte, unverifiedKeyOrCertBytes []byte, unverifiedBase64Signature string, unverifiedPayloadBytes []byte) (time.Time, error) {
// FIXME: Should the publicKey parameter hard-code ecdsa?
// == Parse SET bytes
@ -130,7 +130,14 @@ func VerifyRekorSET(publicKey *ecdsa.PublicKey, unverifiedRekorSET []byte, unver
return time.Time{}, NewInvalidSignatureError(fmt.Sprintf("canonicalizing Rekor SET JSON: %v", err))
}
untrustedSETPayloadHash := sha256.Sum256(untrustedSETPayloadCanonicalBytes)
if !ecdsa.VerifyASN1(publicKey, untrustedSETPayloadHash[:], untrustedSET.UntrustedSignedEntryTimestamp) {
publicKeymatched := false
for _, pk := range publicKeys {
if ecdsa.VerifyASN1(pk, untrustedSETPayloadHash[:], untrustedSET.UntrustedSignedEntryTimestamp) {
publicKeymatched = true
break
}
}
if !publicKeymatched {
return time.Time{}, NewInvalidSignatureError("cryptographic signature verification of Rekor SET failed")
}

View File

@ -185,6 +185,7 @@ func TestVerifyRekorSET(t *testing.T) {
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")
@ -193,25 +194,34 @@ func TestVerifyRekorSET(t *testing.T) {
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
tm, err := VerifyRekorSET(cosignRekorKeyECDSA, cosignSETBytes, cosignCertBytes, string(cosignSigBase64), cosignPayloadBytes)
require.NoError(t, err)
assert.Equal(t, time.Unix(1670870899, 0), tm)
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(cosignRekorKeyECDSA, []byte{}, cosignCertBytes, string(cosignSigBase64), cosignPayloadBytes)
tm, err := VerifyRekorSET(cosignRekorKeysECDSA, []byte{}, cosignCertBytes, string(cosignSigBase64), cosignPayloadBytes)
assert.Error(t, err)
assert.Zero(t, tm)
tm, err = VerifyRekorSET(cosignRekorKeyECDSA, []byte("invalid signature"), cosignCertBytes, string(cosignSigBase64), cosignPayloadBytes)
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)
@ -227,14 +237,19 @@ func TestVerifyRekorSET(t *testing.T) {
UntrustedPayload: json.RawMessage(invalidPayload),
})
require.NoError(t, err)
tm, err = VerifyRekorSET(&testKey.PublicKey, invalidSET, cosignCertBytes, string(cosignSigBase64), cosignPayloadBytes)
tm, err = VerifyRekorSET(testPublicKeys, invalidSET, cosignCertBytes, string(cosignSigBase64), cosignPayloadBytes)
assert.Error(t, err)
assert.Zero(t, tm)
// Cryptographic verification fails (a mismatched public key)
tm, err = VerifyRekorSET(&testKey.PublicKey, cosignSETBytes, cosignCertBytes, string(cosignSigBase64), cosignPayloadBytes)
assert.Error(t, err)
assert.Zero(t, tm)
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(`{}`)
@ -245,7 +260,7 @@ func TestVerifyRekorSET(t *testing.T) {
UntrustedPayload: json.RawMessage(invalidPayload),
})
require.NoError(t, err)
tm, err = VerifyRekorSET(&testKey.PublicKey, invalidSET, cosignCertBytes, string(cosignSigBase64), cosignPayloadBytes)
tm, err = VerifyRekorSET(testPublicKeys, invalidSET, cosignCertBytes, string(cosignSigBase64), cosignPayloadBytes)
assert.Error(t, err)
assert.Zero(t, tm)
@ -379,7 +394,7 @@ func TestVerifyRekorSET(t *testing.T) {
UntrustedPayload: json.RawMessage(testPayload),
})
require.NoError(t, err)
tm, err = VerifyRekorSET(&testKey.PublicKey, testSET, cosignCertBytes, string(cosignSigBase64), cosignPayloadBytes)
tm, err = VerifyRekorSET(testPublicKeys, testSET, cosignCertBytes, string(cosignSigBase64), cosignPayloadBytes)
assert.Error(t, err)
assert.Zero(t, tm)
}
@ -387,7 +402,7 @@ func TestVerifyRekorSET(t *testing.T) {
// Invalid unverifiedBase64Signature parameter
truncatedBase64 := cosignSigBase64
truncatedBase64 = truncatedBase64[:len(truncatedBase64)-1]
tm, err = VerifyRekorSET(cosignRekorKeyECDSA, cosignSETBytes, cosignCertBytes,
tm, err = VerifyRekorSET(cosignRekorKeysECDSA, cosignSETBytes, cosignCertBytes,
string(truncatedBase64), cosignPayloadBytes)
assert.Error(t, err)
assert.Zero(t, tm)
@ -399,7 +414,7 @@ func TestVerifyRekorSET(t *testing.T) {
[]byte("this is not PEM"),
bytes.Repeat(cosignCertBytes, 2),
} {
tm, err = VerifyRekorSET(cosignRekorKeyECDSA, cosignSETBytes, c,
tm, err = VerifyRekorSET(cosignRekorKeysECDSA, cosignSETBytes, c,
string(cosignSigBase64), cosignPayloadBytes)
assert.Error(t, err)
assert.Zero(t, tm)

View File

@ -7,6 +7,7 @@ import (
"encoding/json"
"errors"
"fmt"
"strings"
"time"
"github.com/containers/image/v5/version"
@ -171,24 +172,62 @@ type SigstorePayloadAcceptanceRules struct {
ValidateSignedDockerManifestDigest func(digest.Digest) error
}
// VerifySigstorePayload verifies unverifiedBase64Signature of unverifiedPayload was correctly created by publicKey, and that its principal components
// verifySigstorePayloadBlobSignature verifies unverifiedSignature of unverifiedPayload was correctly created
// by any of the public keys in publicKeys.
//
// This is an internal implementation detail of VerifySigstorePayload and should have no other callers.
// It is INSUFFICIENT alone to consider the signature acceptable.
func verifySigstorePayloadBlobSignature(publicKeys []crypto.PublicKey, unverifiedPayload, unverifiedSignature []byte) error {
if len(publicKeys) == 0 {
return errors.New("Need at least one public key to verify the sigstore payload, but got 0")
}
verifiers := make([]sigstoreSignature.Verifier, 0, len(publicKeys))
for _, key := range publicKeys {
// Failing to load a verifier indicates that something is really, really
// invalid about the public key; prefer to fail even if the signature might be
// valid with other keys, so that users fix their fallback keys before they need them.
// For that reason, we even initialize all verifiers before trying to validate the signature
// with any key.
verifier, err := sigstoreSignature.LoadVerifier(key, sigstoreHarcodedHashAlgorithm)
if err != nil {
return err
}
verifiers = append(verifiers, verifier)
}
var failures []string
for _, verifier := range verifiers {
// github.com/sigstore/cosign/pkg/cosign.verifyOCISignature uses signatureoptions.WithContext(),
// which seems to be not used by anything. So we dont bother.
err := verifier.VerifySignature(bytes.NewReader(unverifiedSignature), bytes.NewReader(unverifiedPayload))
if err == nil {
return nil
}
failures = append(failures, err.Error())
}
if len(failures) == 0 {
// Coverage: We have checked there is at least one public key, any success causes an early return,
// and any failure adds an entry to failures => there must be at least one error.
return fmt.Errorf("Internal error: signature verification failed but no errors have been recorded")
}
return NewInvalidSignatureError("cryptographic signature verification failed: " + strings.Join(failures, ", "))
}
// VerifySigstorePayload verifies unverifiedBase64Signature of unverifiedPayload was correctly created by any of the public keys in publicKeys, and that its principal components
// match expected values, both as specified by rules, and returns it.
// We return an *UntrustedSigstorePayload, although nothing actually uses it,
// just to double-check against stupid typos.
func VerifySigstorePayload(publicKey crypto.PublicKey, unverifiedPayload []byte, unverifiedBase64Signature string, rules SigstorePayloadAcceptanceRules) (*UntrustedSigstorePayload, error) {
verifier, err := sigstoreSignature.LoadVerifier(publicKey, sigstoreHarcodedHashAlgorithm)
if err != nil {
return nil, fmt.Errorf("creating verifier: %w", err)
}
func VerifySigstorePayload(publicKeys []crypto.PublicKey, unverifiedPayload []byte, unverifiedBase64Signature string, rules SigstorePayloadAcceptanceRules) (*UntrustedSigstorePayload, error) {
unverifiedSignature, err := base64.StdEncoding.DecodeString(unverifiedBase64Signature)
if err != nil {
return nil, NewInvalidSignatureError(fmt.Sprintf("base64 decoding: %v", err))
}
// github.com/sigstore/cosign/pkg/cosign.verifyOCISignature uses signatureoptions.WithContext(),
// which seems to be not used by anything. So we dont bother.
if err := verifier.VerifySignature(bytes.NewReader(unverifiedSignature), bytes.NewReader(unverifiedPayload)); err != nil {
return nil, NewInvalidSignatureError(fmt.Sprintf("cryptographic signature verification failed: %v", err))
if err := verifySigstorePayloadBlobSignature(publicKeys, unverifiedPayload, unverifiedSignature); err != nil {
return nil, err
}
var unmatchedPayload UntrustedSigstorePayload

View File

@ -2,6 +2,7 @@ package internal
import (
"bytes"
"crypto"
"encoding/base64"
"encoding/json"
"errors"
@ -204,11 +205,18 @@ func TestUntrustedSigstorePayloadUnmarshalJSON(t *testing.T) {
assert.Equal(t, validSig, s)
}
// verifySigstorePayloadBlobSignature is tested by TestVerifySigstorePayload
func TestVerifySigstorePayload(t *testing.T) {
publicKeyPEM, err := os.ReadFile("./testdata/cosign.pub")
require.NoError(t, err)
publicKey, err := cryptoutils.UnmarshalPEMToPublicKey(publicKeyPEM)
require.NoError(t, err)
publicKeyPEM2, err := os.ReadFile("./testdata/cosign2.pub")
require.NoError(t, err)
publicKey2, err := cryptoutils.UnmarshalPEMToPublicKey(publicKeyPEM2)
require.NoError(t, err)
singlePublicKey := []crypto.PublicKey{publicKey}
type acceptanceData struct {
signedDockerReference string
@ -248,28 +256,26 @@ func TestVerifySigstorePayload(t *testing.T) {
}
// Successful verification
wanted = signatureData
recorded = acceptanceData{}
res, err := VerifySigstorePayload(publicKey, sigstoreSig.UntrustedPayload(), cryptoBase64Sig, recordingRules)
require.NoError(t, err)
assert.Equal(t, res, &UntrustedSigstorePayload{
untrustedDockerManifestDigest: TestSigstoreManifestDigest,
untrustedDockerReference: TestSigstoreSignatureReference,
untrustedCreatorID: nil,
untrustedTimestamp: nil,
})
assert.Equal(t, signatureData, recorded)
for _, publicKeys := range [][]crypto.PublicKey{
singlePublicKey,
{publicKey, publicKey2}, // The matching key is first
{publicKey2, publicKey}, // The matching key is second
} {
wanted = signatureData
recorded = acceptanceData{}
res, err := VerifySigstorePayload(publicKeys, sigstoreSig.UntrustedPayload(), cryptoBase64Sig, recordingRules)
require.NoError(t, err)
assert.Equal(t, res, &UntrustedSigstorePayload{
untrustedDockerManifestDigest: TestSigstoreManifestDigest,
untrustedDockerReference: TestSigstoreSignatureReference,
untrustedCreatorID: nil,
untrustedTimestamp: nil,
})
assert.Equal(t, signatureData, recorded)
}
// For extra paranoia, test that we return a nil signature object on error.
// Invalid verifier
recorded = acceptanceData{}
invalidPublicKey := struct{}{} // crypto.PublicKey is, for some reason, just an any, so this is acceptable.
res, err = VerifySigstorePayload(invalidPublicKey, sigstoreSig.UntrustedPayload(), cryptoBase64Sig, recordingRules)
assert.Error(t, err)
assert.Nil(t, res)
assert.Equal(t, acceptanceData{}, recorded)
// Invalid base64 encoding
for _, invalidBase64Sig := range []string{
"&", // Invalid base64 characters
@ -277,7 +283,28 @@ func TestVerifySigstorePayload(t *testing.T) {
cryptoBase64Sig[:len(cryptoBase64Sig)-1], // Truncated base64 data
} {
recorded = acceptanceData{}
res, err = VerifySigstorePayload(publicKey, sigstoreSig.UntrustedPayload(), invalidBase64Sig, recordingRules)
res, err := VerifySigstorePayload(singlePublicKey, sigstoreSig.UntrustedPayload(), invalidBase64Sig, recordingRules)
assert.Error(t, err)
assert.Nil(t, res)
assert.Equal(t, acceptanceData{}, recorded)
}
// No public keys
recorded = acceptanceData{}
res, err := VerifySigstorePayload([]crypto.PublicKey{}, sigstoreSig.UntrustedPayload(), cryptoBase64Sig, recordingRules)
assert.Error(t, err)
assert.Nil(t, res)
assert.Equal(t, acceptanceData{}, recorded)
// Invalid verifier:
// crypto.PublicKey is, for some reason, just an any, so using a struct{}{} to trigger this code path works.
for _, invalidPublicKeys := range [][]crypto.PublicKey{
{struct{}{}}, // A single invalid key
{struct{}{}, publicKey}, // An invalid key, followed by a matching key
{publicKey, struct{}{}}, // A matching key, but the configuration also includes an invalid key
} {
recorded = acceptanceData{}
res, err = VerifySigstorePayload(invalidPublicKeys, sigstoreSig.UntrustedPayload(), cryptoBase64Sig, recordingRules)
assert.Error(t, err)
assert.Nil(t, res)
assert.Equal(t, acceptanceData{}, recorded)
@ -292,7 +319,19 @@ func TestVerifySigstorePayload(t *testing.T) {
append(bytes.Clone(validSignatureBytes), validSignatureBytes...),
} {
recorded = acceptanceData{}
res, err = VerifySigstorePayload(publicKey, sigstoreSig.UntrustedPayload(), base64.StdEncoding.EncodeToString(invalidSig), recordingRules)
res, err = VerifySigstorePayload(singlePublicKey, sigstoreSig.UntrustedPayload(), base64.StdEncoding.EncodeToString(invalidSig), recordingRules)
assert.Error(t, err)
assert.Nil(t, res)
assert.Equal(t, acceptanceData{}, recorded)
}
// No matching public keys
for _, nonmatchingPublicKeys := range [][]crypto.PublicKey{
{publicKey2},
{publicKey2, publicKey2},
} {
recorded = acceptanceData{}
res, err = VerifySigstorePayload(nonmatchingPublicKeys, sigstoreSig.UntrustedPayload(), cryptoBase64Sig, recordingRules)
assert.Error(t, err)
assert.Nil(t, res)
assert.Equal(t, acceptanceData{}, recorded)
@ -300,14 +339,14 @@ func TestVerifySigstorePayload(t *testing.T) {
// Valid signature of non-JSON
recorded = acceptanceData{}
res, err = VerifySigstorePayload(publicKey, []byte("&"), "MEUCIARnnxZQPALBfqkB4aNAYXad79Qs6VehcrgIeZ8p7I2FAiEAzq2HXwXlz1iJeh+ucUR3L0zpjynQk6Rk0+/gXYp49RU=", recordingRules)
res, err = VerifySigstorePayload(singlePublicKey, []byte("&"), "MEUCIARnnxZQPALBfqkB4aNAYXad79Qs6VehcrgIeZ8p7I2FAiEAzq2HXwXlz1iJeh+ucUR3L0zpjynQk6Rk0+/gXYp49RU=", recordingRules)
assert.Error(t, err)
assert.Nil(t, res)
assert.Equal(t, acceptanceData{}, recorded)
// Valid signature of an unacceptable JSON
recorded = acceptanceData{}
res, err = VerifySigstorePayload(publicKey, []byte("{}"), "MEUCIQDkySOBGxastVP0+koTA33NH5hXjwosFau4rxTPN6g48QIgb7eWKkGqfEpHMM3aT4xiqyP/170jEkdFuciuwN4mux4=", recordingRules)
res, err = VerifySigstorePayload(singlePublicKey, []byte("{}"), "MEUCIQDkySOBGxastVP0+koTA33NH5hXjwosFau4rxTPN6g48QIgb7eWKkGqfEpHMM3aT4xiqyP/170jEkdFuciuwN4mux4=", recordingRules)
assert.Error(t, err)
assert.Nil(t, res)
assert.Equal(t, acceptanceData{}, recorded)
@ -316,7 +355,7 @@ func TestVerifySigstorePayload(t *testing.T) {
wanted = signatureData
wanted.signedDockerManifestDigest = "invalid digest"
recorded = acceptanceData{}
res, err = VerifySigstorePayload(publicKey, sigstoreSig.UntrustedPayload(), cryptoBase64Sig, recordingRules)
res, err = VerifySigstorePayload(singlePublicKey, sigstoreSig.UntrustedPayload(), cryptoBase64Sig, recordingRules)
assert.Error(t, err)
assert.Nil(t, res)
assert.Equal(t, acceptanceData{
@ -327,7 +366,7 @@ func TestVerifySigstorePayload(t *testing.T) {
wanted = signatureData
wanted.signedDockerReference = "unexpected docker reference"
recorded = acceptanceData{}
res, err = VerifySigstorePayload(publicKey, sigstoreSig.UntrustedPayload(), cryptoBase64Sig, recordingRules)
res, err = VerifySigstorePayload(singlePublicKey, sigstoreSig.UntrustedPayload(), cryptoBase64Sig, recordingRules)
assert.Error(t, err)
assert.Nil(t, res)
assert.Equal(t, signatureData, recorded)

1
signature/internal/testdata/cosign2.pub vendored Symbolic link
View File

@ -0,0 +1 @@
../../fixtures/cosign2.pub

View File

@ -2,7 +2,6 @@ package signature
import (
"encoding/json"
"errors"
"fmt"
"github.com/containers/image/v5/signature/internal"
@ -15,29 +14,57 @@ type PRSigstoreSignedOption func(*prSigstoreSigned) error
func PRSigstoreSignedWithKeyPath(keyPath string) PRSigstoreSignedOption {
return func(pr *prSigstoreSigned) error {
if pr.KeyPath != "" {
return errors.New(`"keyPath" already specified`)
return InvalidPolicyFormatError(`"keyPath" already specified`)
}
pr.KeyPath = keyPath
return nil
}
}
// PRSigstoreSignedWithKeyPaths specifies a value for the "keyPaths" field when calling NewPRSigstoreSigned.
func PRSigstoreSignedWithKeyPaths(keyPaths []string) PRSigstoreSignedOption {
return func(pr *prSigstoreSigned) error {
if pr.KeyPaths != nil {
return InvalidPolicyFormatError(`"keyPaths" already specified`)
}
if len(keyPaths) == 0 {
return InvalidPolicyFormatError(`"keyPaths" contains no entries`)
}
pr.KeyPaths = keyPaths
return nil
}
}
// PRSigstoreSignedWithKeyData specifies a value for the "keyData" field when calling NewPRSigstoreSigned.
func PRSigstoreSignedWithKeyData(keyData []byte) PRSigstoreSignedOption {
return func(pr *prSigstoreSigned) error {
if pr.KeyData != nil {
return errors.New(`"keyData" already specified`)
return InvalidPolicyFormatError(`"keyData" already specified`)
}
pr.KeyData = keyData
return nil
}
}
// PRSigstoreSignedWithKeyDatas specifies a value for the "keyDatas" field when calling NewPRSigstoreSigned.
func PRSigstoreSignedWithKeyDatas(keyDatas [][]byte) PRSigstoreSignedOption {
return func(pr *prSigstoreSigned) error {
if pr.KeyDatas != nil {
return InvalidPolicyFormatError(`"keyDatas" already specified`)
}
if len(keyDatas) == 0 {
return InvalidPolicyFormatError(`"keyDatas" contains no entries`)
}
pr.KeyDatas = keyDatas
return nil
}
}
// PRSigstoreSignedWithFulcio specifies a value for the "fulcio" field when calling NewPRSigstoreSigned.
func PRSigstoreSignedWithFulcio(fulcio PRSigstoreSignedFulcio) PRSigstoreSignedOption {
return func(pr *prSigstoreSigned) error {
if pr.Fulcio != nil {
return errors.New(`"fulcio" already specified`)
return InvalidPolicyFormatError(`"fulcio" already specified`)
}
pr.Fulcio = fulcio
return nil
@ -48,29 +75,57 @@ func PRSigstoreSignedWithFulcio(fulcio PRSigstoreSignedFulcio) PRSigstoreSignedO
func PRSigstoreSignedWithRekorPublicKeyPath(rekorPublicKeyPath string) PRSigstoreSignedOption {
return func(pr *prSigstoreSigned) error {
if pr.RekorPublicKeyPath != "" {
return errors.New(`"rekorPublicKeyPath" already specified`)
return InvalidPolicyFormatError(`"rekorPublicKeyPath" already specified`)
}
pr.RekorPublicKeyPath = rekorPublicKeyPath
return nil
}
}
// PRSigstoreSignedWithRekorPublicKeyPaths specifies a value for the rRekorPublickeyPaths" field when calling NewPRSigstoreSigned.
func PRSigstoreSignedWithRekorPublicKeyPaths(rekorPublickeyPaths []string) PRSigstoreSignedOption {
return func(pr *prSigstoreSigned) error {
if pr.RekorPublicKeyPaths != nil {
return InvalidPolicyFormatError(`"rekorPublickeyPaths" already specified`)
}
if len(rekorPublickeyPaths) == 0 {
return InvalidPolicyFormatError(`"rekorPublickeyPaths" contains no entries`)
}
pr.RekorPublicKeyPaths = rekorPublickeyPaths
return nil
}
}
// PRSigstoreSignedWithRekorPublicKeyData specifies a value for the "rekorPublicKeyData" field when calling NewPRSigstoreSigned.
func PRSigstoreSignedWithRekorPublicKeyData(rekorPublicKeyData []byte) PRSigstoreSignedOption {
return func(pr *prSigstoreSigned) error {
if pr.RekorPublicKeyData != nil {
return errors.New(`"rekorPublicKeyData" already specified`)
return InvalidPolicyFormatError(`"rekorPublicKeyData" already specified`)
}
pr.RekorPublicKeyData = rekorPublicKeyData
return nil
}
}
// PRSigstoreSignedWithRekorPublicKeyDatas specifies a value for the "rekorPublickeyDatas" field when calling NewPRSigstoreSigned.
func PRSigstoreSignedWithRekorPublicKeyDatas(rekorPublickeyDatas [][]byte) PRSigstoreSignedOption {
return func(pr *prSigstoreSigned) error {
if pr.RekorPublicKeyDatas != nil {
return InvalidPolicyFormatError(`"rekorPublickeyDatas" already specified`)
}
if len(rekorPublickeyDatas) == 0 {
return InvalidPolicyFormatError(`"rekorPublickeyDatas" contains no entries`)
}
pr.RekorPublicKeyDatas = rekorPublickeyDatas
return nil
}
}
// PRSigstoreSignedWithSignedIdentity specifies a value for the "signedIdentity" field when calling NewPRSigstoreSigned.
func PRSigstoreSignedWithSignedIdentity(signedIdentity PolicyReferenceMatch) PRSigstoreSignedOption {
return func(pr *prSigstoreSigned) error {
if pr.SignedIdentity != nil {
return errors.New(`"signedIdentity" already specified`)
return InvalidPolicyFormatError(`"signedIdentity" already specified`)
}
pr.SignedIdentity = signedIdentity
return nil
@ -92,21 +147,40 @@ func newPRSigstoreSigned(options ...PRSigstoreSignedOption) (*prSigstoreSigned,
if res.KeyPath != "" {
keySources++
}
if res.KeyPaths != nil {
keySources++
}
if res.KeyData != nil {
keySources++
}
if res.KeyDatas != nil {
keySources++
}
if res.Fulcio != nil {
keySources++
}
if keySources != 1 {
return nil, InvalidPolicyFormatError("exactly one of keyPath, keyData and fulcio must be specified")
return nil, InvalidPolicyFormatError("exactly one of keyPath, keyPaths, keyData, keyDatas and fulcio must be specified")
}
if res.RekorPublicKeyPath != "" && res.RekorPublicKeyData != nil {
return nil, InvalidPolicyFormatError("rekorPublickeyType and rekorPublickeyData cannot be used simultaneously")
rekorSources := 0
if res.RekorPublicKeyPath != "" {
rekorSources++
}
if res.Fulcio != nil && res.RekorPublicKeyPath == "" && res.RekorPublicKeyData == nil {
return nil, InvalidPolicyFormatError("At least one of RekorPublickeyPath and RekorPublickeyData must be specified if fulcio is used")
if res.RekorPublicKeyPaths != nil {
rekorSources++
}
if res.RekorPublicKeyData != nil {
rekorSources++
}
if res.RekorPublicKeyDatas != nil {
rekorSources++
}
if rekorSources > 1 {
return nil, InvalidPolicyFormatError("at most one of rekorPublickeyPath, rekorPublicKeyPaths, rekorPublickeyData and rekorPublicKeyDatas can be used simultaneously")
}
if res.Fulcio != nil && rekorSources == 0 {
return nil, InvalidPolicyFormatError("At least one of rekorPublickeyPath, rekorPublicKeyPaths, rekorPublickeyData and rekorPublicKeyDatas must be specified if fulcio is used")
}
if res.SignedIdentity == nil {
@ -144,7 +218,8 @@ var _ json.Unmarshaler = (*prSigstoreSigned)(nil)
func (pr *prSigstoreSigned) UnmarshalJSON(data []byte) error {
*pr = prSigstoreSigned{}
var tmp prSigstoreSigned
var gotKeyPath, gotKeyData, gotFulcio, gotRekorPublicKeyPath, gotRekorPublicKeyData bool
var gotKeyPath, gotKeyPaths, gotKeyData, gotKeyDatas, gotFulcio bool
var gotRekorPublicKeyPath, gotRekorPublicKeyPaths, gotRekorPublicKeyData, gotRekorPublicKeyDatas bool
var fulcio prSigstoreSignedFulcio
var signedIdentity json.RawMessage
if err := internal.ParanoidUnmarshalJSONObject(data, func(key string) any {
@ -154,18 +229,30 @@ func (pr *prSigstoreSigned) UnmarshalJSON(data []byte) error {
case "keyPath":
gotKeyPath = true
return &tmp.KeyPath
case "keyPaths":
gotKeyPaths = true
return &tmp.KeyPaths
case "keyData":
gotKeyData = true
return &tmp.KeyData
case "keyDatas":
gotKeyDatas = true
return &tmp.KeyDatas
case "fulcio":
gotFulcio = true
return &fulcio
case "rekorPublicKeyPath":
gotRekorPublicKeyPath = true
return &tmp.RekorPublicKeyPath
case "rekorPublicKeyPaths":
gotRekorPublicKeyPaths = true
return &tmp.RekorPublicKeyPaths
case "rekorPublicKeyData":
gotRekorPublicKeyData = true
return &tmp.RekorPublicKeyData
case "rekorPublicKeyDatas":
gotRekorPublicKeyDatas = true
return &tmp.RekorPublicKeyDatas
case "signedIdentity":
return &signedIdentity
default:
@ -192,18 +279,30 @@ func (pr *prSigstoreSigned) UnmarshalJSON(data []byte) error {
if gotKeyPath {
opts = append(opts, PRSigstoreSignedWithKeyPath(tmp.KeyPath))
}
if gotKeyPaths {
opts = append(opts, PRSigstoreSignedWithKeyPaths(tmp.KeyPaths))
}
if gotKeyData {
opts = append(opts, PRSigstoreSignedWithKeyData(tmp.KeyData))
}
if gotKeyDatas {
opts = append(opts, PRSigstoreSignedWithKeyDatas(tmp.KeyDatas))
}
if gotFulcio {
opts = append(opts, PRSigstoreSignedWithFulcio(&fulcio))
}
if gotRekorPublicKeyPath {
opts = append(opts, PRSigstoreSignedWithRekorPublicKeyPath(tmp.RekorPublicKeyPath))
}
if gotRekorPublicKeyPaths {
opts = append(opts, PRSigstoreSignedWithRekorPublicKeyPaths(tmp.RekorPublicKeyPaths))
}
if gotRekorPublicKeyData {
opts = append(opts, PRSigstoreSignedWithRekorPublicKeyData(tmp.RekorPublicKeyData))
}
if gotRekorPublicKeyDatas {
opts = append(opts, PRSigstoreSignedWithRekorPublicKeyDatas(tmp.RekorPublicKeyDatas))
}
opts = append(opts, PRSigstoreSignedWithSignedIdentity(tmp.SignedIdentity))
res, err := newPRSigstoreSigned(opts...)
@ -221,7 +320,7 @@ type PRSigstoreSignedFulcioOption func(*prSigstoreSignedFulcio) error
func PRSigstoreSignedFulcioWithCAPath(caPath string) PRSigstoreSignedFulcioOption {
return func(f *prSigstoreSignedFulcio) error {
if f.CAPath != "" {
return errors.New(`"caPath" already specified`)
return InvalidPolicyFormatError(`"caPath" already specified`)
}
f.CAPath = caPath
return nil
@ -232,7 +331,7 @@ func PRSigstoreSignedFulcioWithCAPath(caPath string) PRSigstoreSignedFulcioOptio
func PRSigstoreSignedFulcioWithCAData(caData []byte) PRSigstoreSignedFulcioOption {
return func(f *prSigstoreSignedFulcio) error {
if f.CAData != nil {
return errors.New(`"caData" already specified`)
return InvalidPolicyFormatError(`"caData" already specified`)
}
f.CAData = caData
return nil
@ -243,7 +342,7 @@ func PRSigstoreSignedFulcioWithCAData(caData []byte) PRSigstoreSignedFulcioOptio
func PRSigstoreSignedFulcioWithOIDCIssuer(oidcIssuer string) PRSigstoreSignedFulcioOption {
return func(f *prSigstoreSignedFulcio) error {
if f.OIDCIssuer != "" {
return errors.New(`"oidcIssuer" already specified`)
return InvalidPolicyFormatError(`"oidcIssuer" already specified`)
}
f.OIDCIssuer = oidcIssuer
return nil
@ -254,7 +353,7 @@ func PRSigstoreSignedFulcioWithOIDCIssuer(oidcIssuer string) PRSigstoreSignedFul
func PRSigstoreSignedFulcioWithSubjectEmail(subjectEmail string) PRSigstoreSignedFulcioOption {
return func(f *prSigstoreSignedFulcio) error {
if f.SubjectEmail != "" {
return errors.New(`"subjectEmail" already specified`)
return InvalidPolicyFormatError(`"subjectEmail" already specified`)
}
f.SubjectEmail = subjectEmail
return nil

View File

@ -20,7 +20,9 @@ func xNewPRSigstoreSigned(options ...PRSigstoreSignedOption) PolicyRequirement {
func TestNewPRSigstoreSigned(t *testing.T) {
const testKeyPath = "/foo/bar"
const testKeyPath2 = "/baz/bar"
testKeyData := []byte("abc")
testKeyData2 := []byte("def")
testFulcio, err := NewPRSigstoreSignedFulcio(
PRSigstoreSignedFulcioWithCAPath("fixtures/fulcio_v1.crt.pem"),
PRSigstoreSignedFulcioWithOIDCIssuer("https://github.com/login/oauth"),
@ -45,7 +47,24 @@ func TestNewPRSigstoreSigned(t *testing.T) {
expected: prSigstoreSigned{
prCommon: prCommon{prTypeSigstoreSigned},
KeyPath: testKeyPath,
KeyPaths: nil,
KeyData: nil,
KeyDatas: nil,
Fulcio: nil,
SignedIdentity: testIdentity,
},
},
{
options: []PRSigstoreSignedOption{
PRSigstoreSignedWithKeyPaths([]string{testKeyPath, testKeyPath2}),
PRSigstoreSignedWithSignedIdentity(testIdentity),
},
expected: prSigstoreSigned{
prCommon: prCommon{prTypeSigstoreSigned},
KeyPath: "",
KeyPaths: []string{testKeyPath, testKeyPath2},
KeyData: nil,
KeyDatas: nil,
Fulcio: nil,
SignedIdentity: testIdentity,
},
@ -58,7 +77,24 @@ func TestNewPRSigstoreSigned(t *testing.T) {
expected: prSigstoreSigned{
prCommon: prCommon{prTypeSigstoreSigned},
KeyPath: "",
KeyPaths: nil,
KeyData: testKeyData,
KeyDatas: nil,
Fulcio: nil,
SignedIdentity: testIdentity,
},
},
{
options: []PRSigstoreSignedOption{
PRSigstoreSignedWithKeyDatas([][]byte{testKeyData, testKeyData2}),
PRSigstoreSignedWithSignedIdentity(testIdentity),
},
expected: prSigstoreSigned{
prCommon: prCommon{prTypeSigstoreSigned},
KeyPath: "",
KeyPaths: nil,
KeyData: nil,
KeyDatas: [][]byte{testKeyData, testKeyData2},
Fulcio: nil,
SignedIdentity: testIdentity,
},
@ -72,7 +108,9 @@ func TestNewPRSigstoreSigned(t *testing.T) {
expected: prSigstoreSigned{
prCommon: prCommon{prTypeSigstoreSigned},
KeyPath: "",
KeyPaths: nil,
KeyData: nil,
KeyDatas: nil,
Fulcio: testFulcio,
SignedIdentity: testIdentity,
},
@ -94,6 +132,14 @@ func TestNewPRSigstoreSigned(t *testing.T) {
RekorPublicKeyPath: testRekorKeyPath,
},
},
{
rekorOptions: []PRSigstoreSignedOption{
PRSigstoreSignedWithRekorPublicKeyPaths([]string{testRekorKeyPath, testKeyPath}),
},
rekorExpected: prSigstoreSigned{
RekorPublicKeyPaths: []string{testRekorKeyPath, testKeyPath},
},
},
{
rekorOptions: []PRSigstoreSignedOption{
PRSigstoreSignedWithRekorPublicKeyData(testRekorKeyData),
@ -102,6 +148,14 @@ func TestNewPRSigstoreSigned(t *testing.T) {
RekorPublicKeyData: testRekorKeyData,
},
},
{
rekorOptions: []PRSigstoreSignedOption{
PRSigstoreSignedWithRekorPublicKeyDatas([][]byte{testRekorKeyData, testKeyData}),
},
rekorExpected: prSigstoreSigned{
RekorPublicKeyDatas: [][]byte{testRekorKeyData, testKeyData},
},
},
} {
// Special-case this rejected combination:
if c.requiresRekor && len(c2.rekorOptions) == 0 {
@ -111,7 +165,9 @@ func TestNewPRSigstoreSigned(t *testing.T) {
require.NoError(t, err)
expected := c.expected // A shallow copy
expected.RekorPublicKeyPath = c2.rekorExpected.RekorPublicKeyPath
expected.RekorPublicKeyPaths = c2.rekorExpected.RekorPublicKeyPaths
expected.RekorPublicKeyData = c2.rekorExpected.RekorPublicKeyData
expected.RekorPublicKeyDatas = c2.rekorExpected.RekorPublicKeyDatas
assert.Equal(t, &expected, pr)
}
}
@ -123,19 +179,19 @@ func TestNewPRSigstoreSigned(t *testing.T) {
)
require.NoError(t, err)
for _, c := range [][]PRSigstoreSignedOption{
{}, // None of keyPath nor keyData, fulcio specified
{}, // None of keyPath, keyPaths, keyData, keyDatas, fulcio specified
{ // Both keyPath and keyData specified
PRSigstoreSignedWithKeyPath(testKeyPath),
PRSigstoreSignedWithKeyData(testKeyData),
PRSigstoreSignedWithSignedIdentity(testIdentity),
},
{ // both keyPath and fulcio specified
{ // Both keyPath and fulcio specified
PRSigstoreSignedWithKeyPath(testKeyPath),
PRSigstoreSignedWithFulcio(testFulcio),
PRSigstoreSignedWithRekorPublicKeyPath(testRekorKeyPath),
PRSigstoreSignedWithSignedIdentity(testIdentity),
},
{ // both keyData and fulcio specified
{ // Both keyData and fulcio specified
PRSigstoreSignedWithKeyData(testKeyData),
PRSigstoreSignedWithFulcio(testFulcio),
PRSigstoreSignedWithRekorPublicKeyPath(testRekorKeyPath),
@ -146,11 +202,39 @@ func TestNewPRSigstoreSigned(t *testing.T) {
PRSigstoreSignedWithKeyPath(testKeyPath + "1"),
PRSigstoreSignedWithSignedIdentity(testIdentity),
},
{ // Empty keypaths
PRSigstoreSignedWithKeyPaths([]string{}),
PRSigstoreSignedWithSignedIdentity(testIdentity),
},
{ // Duplicate keyPaths
PRSigstoreSignedWithKeyPaths([]string{testKeyPath, testKeyPath2}),
PRSigstoreSignedWithKeyPaths([]string{testKeyPath + "1", testKeyPath2 + "1"}),
PRSigstoreSignedWithSignedIdentity(testIdentity),
},
{ // keyPath & keyPaths both set
PRSigstoreSignedWithKeyPath("foobar"),
PRSigstoreSignedWithKeyPaths([]string{"foobar"}),
PRSigstoreSignedWithSignedIdentity(testIdentity),
},
{ // Duplicate keyData
PRSigstoreSignedWithKeyData(testKeyData),
PRSigstoreSignedWithKeyData([]byte("def")),
PRSigstoreSignedWithSignedIdentity(testIdentity),
},
{ // Empty keyDatas
PRSigstoreSignedWithKeyDatas([][]byte{}),
PRSigstoreSignedWithSignedIdentity(testIdentity),
},
{ // Duplicate keyDatas
PRSigstoreSignedWithKeyDatas([][]byte{testKeyData, testKeyData2}),
PRSigstoreSignedWithKeyDatas([][]byte{append(testKeyData, 'a'), append(testKeyData2, 'a')}),
PRSigstoreSignedWithSignedIdentity(testIdentity),
},
{ // keyData & keyDatas both set
PRSigstoreSignedWithKeyData([]byte("bar")),
PRSigstoreSignedWithKeyDatas([][]byte{[]byte("foo")}),
PRSigstoreSignedWithSignedIdentity(testIdentity),
},
{ // Duplicate fulcio
PRSigstoreSignedWithFulcio(testFulcio),
PRSigstoreSignedWithFulcio(testFulcio2),
@ -173,16 +257,50 @@ func TestNewPRSigstoreSigned(t *testing.T) {
PRSigstoreSignedWithRekorPublicKeyPath(testRekorKeyPath + "1"),
PRSigstoreSignedWithSignedIdentity(testIdentity),
},
{ // Duplicate keyData
{ // Both rekorKeyPath and rekorKeyPaths specified
PRSigstoreSignedWithKeyPath(testKeyPath),
PRSigstoreSignedWithRekorPublicKeyPath(testRekorKeyPath),
PRSigstoreSignedWithRekorPublicKeyPaths([]string{testRekorKeyPath, testKeyPath}),
PRSigstoreSignedWithSignedIdentity(testIdentity),
},
{ // Empty rekorKeyPaths
PRSigstoreSignedWithKeyPath(testKeyPath),
PRSigstoreSignedWithRekorPublicKeyPaths([]string{}),
PRSigstoreSignedWithSignedIdentity(testIdentity),
},
{ // Duplicate rekorKeyPaths
PRSigstoreSignedWithKeyPath(testKeyPath),
PRSigstoreSignedWithRekorPublicKeyPaths([]string{testRekorKeyPath, testKeyPath}),
PRSigstoreSignedWithRekorPublicKeyPaths([]string{testRekorKeyPath + "1", testKeyPath + "1"}),
PRSigstoreSignedWithSignedIdentity(testIdentity),
},
{ // Duplicate rekorKeyData
PRSigstoreSignedWithKeyPath(testKeyPath),
PRSigstoreSignedWithRekorPublicKeyData(testRekorKeyData),
PRSigstoreSignedWithRekorPublicKeyData([]byte("def")),
PRSigstoreSignedWithSignedIdentity(testIdentity),
},
{ // Both rekorKeyData and rekorKeyDatas specified
PRSigstoreSignedWithKeyPath(testKeyPath),
PRSigstoreSignedWithRekorPublicKeyData(testRekorKeyData),
PRSigstoreSignedWithRekorPublicKeyDatas([][]byte{testRekorKeyData, []byte("def")}),
PRSigstoreSignedWithSignedIdentity(testIdentity),
},
{ // Empty rekorKeyDatas
PRSigstoreSignedWithKeyPath(testKeyPath),
PRSigstoreSignedWithRekorPublicKeyDatas([][]byte{}),
PRSigstoreSignedWithSignedIdentity(testIdentity),
},
{ // Duplicate rekorKeyData
PRSigstoreSignedWithKeyPath(testKeyPath),
PRSigstoreSignedWithRekorPublicKeyDatas([][]byte{testRekorKeyData, testKeyData}),
PRSigstoreSignedWithRekorPublicKeyDatas([][]byte{[]byte("abc"), []byte("def")}),
PRSigstoreSignedWithSignedIdentity(testIdentity),
},
{ // Missing signedIdentity
PRSigstoreSignedWithKeyPath(testKeyPath),
},
{ // Duplicate signedIdentity}
{ // Duplicate signedIdentity
PRSigstoreSignedWithKeyPath(testKeyPath),
PRSigstoreSignedWithSignedIdentity(testIdentity),
PRSigstoreSignedWithSignedIdentity(newPRMMatchRepository()),
@ -248,10 +366,14 @@ func TestPRSigstoreSignedUnmarshalJSON(t *testing.T) {
func(v mSA) { v["type"] = "this is invalid" },
// Extra top-level sub-object
func(v mSA) { v["unexpected"] = 1 },
// All of "keyPath" and "keyData", and "fulcio" is missing
// All of "keyPath", "keyPaths", "keyData", "keyDatas", and "fulcio" is missing
func(v mSA) { delete(v, "keyData") },
// Both "keyPath" and "keyData" is present
func(v mSA) { v["keyPath"] = "/foo/bar" },
// Both "keyPaths" and "keyData" is present
func(v mSA) { v["keyPaths"] = []string{"/foo/bar", "/foo/baz"} },
// Both "keyData" and "keyDatas" is present
func(v mSA) { v["keyDatas"] = [][]byte{[]byte("abc"), []byte("def")} },
// Both "keyData" and "fulcio" is present
func(v mSA) {
v["fulcio"] = mSA{
@ -262,14 +384,22 @@ func TestPRSigstoreSignedUnmarshalJSON(t *testing.T) {
},
// Invalid "keyPath" field
func(v mSA) { delete(v, "keyData"); v["keyPath"] = 1 },
// Invalid "keyPaths" field
func(v mSA) { delete(v, "keyData"); v["keyPaths"] = 1 },
func(v mSA) { delete(v, "keyData"); v["keyPaths"] = mSA{} },
func(v mSA) { delete(v, "keyData"); v["keyPaths"] = []string{} },
// Invalid "keyData" field
func(v mSA) { v["keyData"] = 1 },
func(v mSA) { v["keyData"] = "this is invalid base64" },
// Invalid "keyDatas" field
func(v mSA) { delete(v, "keyData"); v["keyDatas"] = 1 },
func(v mSA) { delete(v, "keyData"); v["keyDatas"] = mSA{} },
func(v mSA) { delete(v, "keyData"); v["keyDatas"] = [][]byte{} },
// Invalid "fulcio" field
func(v mSA) { v["fulcio"] = 1 },
func(v mSA) { v["fulcio"] = mSA{} },
func(v mSA) { delete(v, "keyData"); v["fulcio"] = 1 },
func(v mSA) { delete(v, "keyData"); v["fulcio"] = mSA{} },
// "fulcio" is explicit nil
func(v mSA) { v["fulcio"] = nil },
func(v mSA) { delete(v, "keyData"); v["fulcio"] = nil },
// Both "rekorKeyPath" and "rekorKeyData" is present
func(v mSA) {
v["rekorPublicKeyPath"] = "/foo/baz"
@ -277,9 +407,27 @@ func TestPRSigstoreSignedUnmarshalJSON(t *testing.T) {
},
// Invalid "rekorPublicKeyPath" field
func(v mSA) { v["rekorPublicKeyPath"] = 1 },
// Both "rekorKeyPath" and "rekorKeyPaths" is present
func(v mSA) {
v["rekorPublicKeyPath"] = "/foo/baz"
v["rekorPublicKeyPaths"] = []string{"/baz/a", "/baz/b"}
},
// Invalid "rekorPublicKeyPaths" field
func(v mSA) { v["rekorPublicKeyPaths"] = 1 },
func(v mSA) { v["rekorPublicKeyPaths"] = mSA{} },
func(v mSA) { v["rekorPublicKeyPaths"] = []string{} },
// Invalid "rekorPublicKeyData" field
func(v mSA) { v["rekorPublicKeyData"] = 1 },
func(v mSA) { v["rekorPublicKeyData"] = "this is invalid base64" },
// Both "rekorPublicKeyData" and "rekorPublicKeyDatas" is present
func(v mSA) {
v["rekorPublicKeyData"] = []byte("a")
v["rekorPublicKeyDatas"] = [][]byte{[]byte("a"), []byte("b")}
},
// Invalid "rekorPublicKeyDatas" field
func(v mSA) { v["rekorPublicKeyDatas"] = 1 },
func(v mSA) { v["rekorPublicKeyDatas"] = mSA{} },
func(v mSA) { v["rekorPublicKeyDatas"] = [][]byte{} },
// Invalid "signedIdentity" field
func(v mSA) { v["signedIdentity"] = "this is invalid" },
// "signedIdentity" an explicit nil
@ -288,7 +436,7 @@ func TestPRSigstoreSignedUnmarshalJSON(t *testing.T) {
duplicateFields: []string{"type", "keyData", "signedIdentity"},
}
keyDataTests.run(t)
// Test keyPath-specific duplicate fields
// Test keyPath and keyPath-specific duplicate fields
policyJSONUmarshallerTests[PolicyRequirement]{
newDest: func() json.Unmarshaler { return &prSigstoreSigned{} },
newValidObject: func() (PolicyRequirement, error) {
@ -297,6 +445,30 @@ func TestPRSigstoreSignedUnmarshalJSON(t *testing.T) {
otherJSONParser: newPolicyRequirementFromJSON,
duplicateFields: []string{"type", "keyPath", "signedIdentity"},
}.run(t)
// Test keyPaths and keyPaths-specific duplicate fields
policyJSONUmarshallerTests[PolicyRequirement]{
newDest: func() json.Unmarshaler { return &prSigstoreSigned{} },
newValidObject: func() (PolicyRequirement, error) {
return NewPRSigstoreSigned(
PRSigstoreSignedWithKeyPaths([]string{"/foo/bar", "/foo/baz"}),
PRSigstoreSignedWithSignedIdentity(NewPRMMatchRepoDigestOrExact()),
)
},
otherJSONParser: newPolicyRequirementFromJSON,
duplicateFields: []string{"type", "keyPaths", "signedIdentity"},
}.run(t)
// Test keyDatas and keyDatas-specific duplicate fields
policyJSONUmarshallerTests[PolicyRequirement]{
newDest: func() json.Unmarshaler { return &prSigstoreSigned{} },
newValidObject: func() (PolicyRequirement, error) {
return NewPRSigstoreSigned(
PRSigstoreSignedWithKeyDatas([][]byte{[]byte("abc"), []byte("def")}),
PRSigstoreSignedWithSignedIdentity(NewPRMMatchRepoDigestOrExact()),
)
},
otherJSONParser: newPolicyRequirementFromJSON,
duplicateFields: []string{"type", "keyDatas", "signedIdentity"},
}.run(t)
// Test Fulcio and rekorPublicKeyPath duplicate fields
testFulcio, err := NewPRSigstoreSignedFulcio(
PRSigstoreSignedFulcioWithCAPath("fixtures/fulcio_v1.crt.pem"),
@ -316,6 +488,19 @@ func TestPRSigstoreSignedUnmarshalJSON(t *testing.T) {
otherJSONParser: newPolicyRequirementFromJSON,
duplicateFields: []string{"type", "fulcio", "rekorPublicKeyPath", "signedIdentity"},
}.run(t)
// Test rekorPublicKeyPaths duplicate fields
policyJSONUmarshallerTests[PolicyRequirement]{
newDest: func() json.Unmarshaler { return &prSigstoreSigned{} },
newValidObject: func() (PolicyRequirement, error) {
return NewPRSigstoreSigned(
PRSigstoreSignedWithKeyPath("/foo/bar"),
PRSigstoreSignedWithRekorPublicKeyPaths([]string{"/baz/a", "/baz/b"}),
PRSigstoreSignedWithSignedIdentity(NewPRMMatchRepoDigestOrExact()),
)
},
otherJSONParser: newPolicyRequirementFromJSON,
duplicateFields: []string{"type", "keyPath", "rekorPublicKeyPaths", "signedIdentity"},
}.run(t)
// Test rekorPublicKeyData duplicate fields
policyJSONUmarshallerTests[PolicyRequirement]{
newDest: func() json.Unmarshaler { return &prSigstoreSigned{} },
@ -329,6 +514,19 @@ func TestPRSigstoreSignedUnmarshalJSON(t *testing.T) {
otherJSONParser: newPolicyRequirementFromJSON,
duplicateFields: []string{"type", "keyPath", "rekorPublicKeyData", "signedIdentity"},
}.run(t)
// Test rekorPublicKeyDatas duplicate fields
policyJSONUmarshallerTests[PolicyRequirement]{
newDest: func() json.Unmarshaler { return &prSigstoreSigned{} },
newValidObject: func() (PolicyRequirement, error) {
return NewPRSigstoreSigned(
PRSigstoreSignedWithKeyPath("/foo/bar"),
PRSigstoreSignedWithRekorPublicKeyDatas([][]byte{[]byte("foo"), []byte("bar")}),
PRSigstoreSignedWithSignedIdentity(NewPRMMatchRepoDigestOrExact()),
)
},
otherJSONParser: newPolicyRequirementFromJSON,
duplicateFields: []string{"type", "keyPath", "rekorPublicKeyDatas", "signedIdentity"},
}.run(t)
var pr prSigstoreSigned

View File

@ -6,7 +6,6 @@ import (
"context"
"errors"
"fmt"
"os"
"slices"
"github.com/containers/image/v5/internal/multierr"
@ -27,33 +26,18 @@ func (pr *prSignedBy) isSignatureAuthorAccepted(ctx context.Context, image priva
}
// FIXME: move this to per-context initialization
var data [][]byte
keySources := 0
if pr.KeyPath != "" {
keySources++
d, err := os.ReadFile(pr.KeyPath)
if err != nil {
return sarRejected, nil, err
}
data = [][]byte{d}
const notOneSourceErrorText = `Internal inconsistency: not exactly one of "keyPath", "keyPaths" and "keyData" specified`
data, err := loadBytesFromConfigSources(configBytesSources{
inconsistencyErrorMessage: notOneSourceErrorText,
path: pr.KeyPath,
paths: pr.KeyPaths,
data: pr.KeyData,
})
if err != nil {
return sarRejected, nil, err
}
if pr.KeyPaths != nil {
keySources++
data = [][]byte{}
for _, path := range pr.KeyPaths {
d, err := os.ReadFile(path)
if err != nil {
return sarRejected, nil, err
}
data = append(data, d)
}
}
if pr.KeyData != nil {
keySources++
data = [][]byte{pr.KeyData}
}
if keySources != 1 {
return sarRejected, nil, errors.New(`Internal inconsistency: not exactly one of "keyPath", "keyPaths" and "keyData" specified`)
if data == nil {
return sarRejected, nil, errors.New(notOneSourceErrorText)
}
// FIXME: move this to per-context initialization

View File

@ -10,6 +10,7 @@ import (
"errors"
"fmt"
"os"
"strings"
"github.com/containers/image/v5/internal/multierr"
"github.com/containers/image/v5/internal/private"
@ -20,37 +21,69 @@ import (
"github.com/sigstore/sigstore/pkg/cryptoutils"
)
// loadBytesFromDataOrPath ensures there is at most one of ${prefix}Data and ${prefix}Path set,
// configBytesSources contains configuration fields which may result in one or more []byte values
type configBytesSources struct {
inconsistencyErrorMessage string // Error to return if more than one source is set
path string // …Path: a path to a file containing the data, or ""
paths []string // …Paths: paths to files containing the data, or nil
data []byte // …Data: a single instance ofhe raw data, or nil
datas [][]byte // …Datas: the raw data, or nil // codespell:ignore datas
}
// loadBytesFromConfigSources ensures at most one of the sources in src is set,
// and returns the referenced data, or nil if neither is set.
func loadBytesFromDataOrPath(prefix string, data []byte, path string) ([]byte, error) {
switch {
case data != nil && path != "":
return nil, fmt.Errorf(`Internal inconsistency: both "%sPath" and "%sData" specified`, prefix, prefix)
case path != "":
d, err := os.ReadFile(path)
func loadBytesFromConfigSources(src configBytesSources) ([][]byte, error) {
sources := 0
var data [][]byte // = nil
if src.path != "" {
sources++
d, err := os.ReadFile(src.path)
if err != nil {
return nil, err
}
return d, nil
case data != nil:
return data, nil
default: // Nothing
return nil, nil
data = [][]byte{d}
}
if src.paths != nil {
sources++
data = [][]byte{}
for _, path := range src.paths {
d, err := os.ReadFile(path)
if err != nil {
return nil, err
}
data = append(data, d)
}
}
if src.data != nil {
sources++
data = [][]byte{src.data}
}
if src.datas != nil { // codespell:ignore datas
sources++
data = src.datas // codespell:ignore datas
}
if sources > 1 {
return nil, errors.New(src.inconsistencyErrorMessage)
}
return data, nil
}
// prepareTrustRoot creates a fulcioTrustRoot from the input data.
// (This also prevents external implementations of this interface, ensuring that prSigstoreSignedFulcio is the only one.)
func (f *prSigstoreSignedFulcio) prepareTrustRoot() (*fulcioTrustRoot, error) {
caCertBytes, err := loadBytesFromDataOrPath("fulcioCA", f.CAData, f.CAPath)
caCertPEMs, err := loadBytesFromConfigSources(configBytesSources{
inconsistencyErrorMessage: `Internal inconsistency: both "caPath" and "caData" specified`,
path: f.CAPath,
data: f.CAData,
})
if err != nil {
return nil, err
}
if caCertBytes == nil {
return nil, errors.New(`Internal inconsistency: Fulcio specified with neither "caPath" nor "caData"`)
if len(caCertPEMs) != 1 {
return nil, errors.New(`Internal inconsistency: Fulcio specified with not exactly one of "caPath" nor "caData"`)
}
certs := x509.NewCertPool()
if ok := certs.AppendCertsFromPEM(caCertBytes); !ok {
if ok := certs.AppendCertsFromPEM(caCertPEMs[0]); !ok {
return nil, errors.New("error loading Fulcio CA certificates")
}
fulcio := fulcioTrustRoot{
@ -66,24 +99,35 @@ func (f *prSigstoreSignedFulcio) prepareTrustRoot() (*fulcioTrustRoot, error) {
// sigstoreSignedTrustRoot contains an already parsed version of the prSigstoreSigned policy
type sigstoreSignedTrustRoot struct {
publicKey crypto.PublicKey
fulcio *fulcioTrustRoot
rekorPublicKey *ecdsa.PublicKey
publicKeys []crypto.PublicKey
fulcio *fulcioTrustRoot
rekorPublicKeys []*ecdsa.PublicKey
}
func (pr *prSigstoreSigned) prepareTrustRoot() (*sigstoreSignedTrustRoot, error) {
res := sigstoreSignedTrustRoot{}
publicKeyPEM, err := loadBytesFromDataOrPath("key", pr.KeyData, pr.KeyPath)
publicKeyPEMs, err := loadBytesFromConfigSources(configBytesSources{
inconsistencyErrorMessage: `Internal inconsistency: more than one of "keyPath", "keyPaths", "keyData", "keyDatas" specified`,
path: pr.KeyPath,
paths: pr.KeyPaths,
data: pr.KeyData,
datas: pr.KeyDatas, // codespell:ignore datas
})
if err != nil {
return nil, err
}
if publicKeyPEM != nil {
pk, err := cryptoutils.UnmarshalPEMToPublicKey(publicKeyPEM)
if err != nil {
return nil, fmt.Errorf("parsing public key: %w", err)
if publicKeyPEMs != nil {
for index, keyData := range publicKeyPEMs {
pk, err := cryptoutils.UnmarshalPEMToPublicKey(keyData)
if err != nil {
return nil, fmt.Errorf("parsing public key %d: %w", index+1, err)
}
res.publicKeys = append(res.publicKeys, pk)
}
if len(res.publicKeys) == 0 {
return nil, errors.New(`Internal inconsistency: "keyPath", "keyPaths", "keyData" and "keyDatas" produced no public keys`)
}
res.publicKey = pk
}
if pr.Fulcio != nil {
@ -94,21 +138,32 @@ func (pr *prSigstoreSigned) prepareTrustRoot() (*sigstoreSignedTrustRoot, error)
res.fulcio = f
}
rekorPublicKeyPEM, err := loadBytesFromDataOrPath("rekorPublicKey", pr.RekorPublicKeyData, pr.RekorPublicKeyPath)
rekorPublicKeyPEMs, err := loadBytesFromConfigSources(configBytesSources{
inconsistencyErrorMessage: `Internal inconsistency: both "rekorPublicKeyPath" and "rekorPublicKeyData" specified`,
path: pr.RekorPublicKeyPath,
paths: pr.RekorPublicKeyPaths,
data: pr.RekorPublicKeyData,
datas: pr.RekorPublicKeyDatas, // codespell:ignore datas
})
if err != nil {
return nil, err
}
if rekorPublicKeyPEM != nil {
pk, err := cryptoutils.UnmarshalPEMToPublicKey(rekorPublicKeyPEM)
if err != nil {
return nil, fmt.Errorf("parsing Rekor public key: %w", err)
}
pkECDSA, ok := pk.(*ecdsa.PublicKey)
if !ok {
return nil, fmt.Errorf("Rekor public key is not using ECDSA")
if rekorPublicKeyPEMs != nil {
for index, pem := range rekorPublicKeyPEMs {
pk, err := cryptoutils.UnmarshalPEMToPublicKey(pem)
if err != nil {
return nil, fmt.Errorf("parsing Rekor public key %d: %w", index+1, err)
}
pkECDSA, ok := pk.(*ecdsa.PublicKey)
if !ok {
return nil, fmt.Errorf("Rekor public key %d is not using ECDSA", index+1)
}
res.rekorPublicKeys = append(res.rekorPublicKeys, pkECDSA)
}
if len(res.rekorPublicKeys) == 0 {
return nil, errors.New(`Internal inconsistency: "rekorPublicKeyPath", "rekorPublicKeyPaths", "rekorPublicKeyData" and "rekorPublicKeyDatas" produced no public keys`)
}
res.rekorPublicKey = pkECDSA
}
return &res, nil
@ -134,37 +189,51 @@ func (pr *prSigstoreSigned) isSignatureAccepted(ctx context.Context, image priva
}
untrustedPayload := sig.UntrustedPayload()
var publicKey crypto.PublicKey
var publicKeys []crypto.PublicKey
switch {
case trustRoot.publicKey != nil && trustRoot.fulcio != nil: // newPRSigstoreSigned rejects such combinations.
case trustRoot.publicKeys != nil && trustRoot.fulcio != nil: // newPRSigstoreSigned rejects such combinations.
return sarRejected, errors.New("Internal inconsistency: Both a public key and Fulcio CA specified")
case trustRoot.publicKey == nil && trustRoot.fulcio == nil: // newPRSigstoreSigned rejects such combinations.
case trustRoot.publicKeys == nil && trustRoot.fulcio == nil: // newPRSigstoreSigned rejects such combinations.
return sarRejected, errors.New("Internal inconsistency: Neither a public key nor a Fulcio CA specified")
case trustRoot.publicKey != nil:
if trustRoot.rekorPublicKey != nil {
case trustRoot.publicKeys != nil:
if trustRoot.rekorPublicKeys != nil {
untrustedSET, ok := untrustedAnnotations[signature.SigstoreSETAnnotationKey]
if !ok { // For user convenience; passing an empty []byte to VerifyRekorSet should work.
return sarRejected, fmt.Errorf("missing %s annotation", signature.SigstoreSETAnnotationKey)
}
// We could use publicKeyPEM directly, but lets re-marshal to avoid inconsistencies.
// FIXME: We could just generate DER instead of the full PEM text
recreatedPublicKeyPEM, err := cryptoutils.MarshalPublicKeyToPEM(trustRoot.publicKey)
if err != nil {
// Coverage: The key was loaded from a PEM format, so its unclear how this could fail.
// (PEM is not essential, MarshalPublicKeyToPEM can only fail if marshaling to ASN1.DER fails.)
return sarRejected, fmt.Errorf("re-marshaling public key to PEM: %w", err)
var rekorFailures []string
for _, candidatePublicKey := range trustRoot.publicKeys {
// We could use publicKeyPEM directly, but lets re-marshal to avoid inconsistencies.
// FIXME: We could just generate DER instead of the full PEM text
recreatedPublicKeyPEM, err := cryptoutils.MarshalPublicKeyToPEM(candidatePublicKey)
if err != nil {
// Coverage: The key was loaded from a PEM format, so its unclear how this could fail.
// (PEM is not essential, MarshalPublicKeyToPEM can only fail if marshaling to ASN1.DER fails.)
return sarRejected, fmt.Errorf("re-marshaling public key to PEM: %w", err)
}
// We dont care about the Rekor timestamp, just about log presence.
_, err = internal.VerifyRekorSET(trustRoot.rekorPublicKeys, []byte(untrustedSET), recreatedPublicKeyPEM, untrustedBase64Signature, untrustedPayload)
if err == nil {
publicKeys = append(publicKeys, candidatePublicKey)
break // The SET can only accept one public key entry, so if we found one, the rest either doesnt match or is a duplicate
}
rekorFailures = append(rekorFailures, err.Error())
}
// We dont care about the Rekor timestamp, just about log presence.
if _, err := internal.VerifyRekorSET(trustRoot.rekorPublicKey, []byte(untrustedSET), recreatedPublicKeyPEM, untrustedBase64Signature, untrustedPayload); err != nil {
return sarRejected, err
if len(publicKeys) == 0 {
if len(rekorFailures) == 0 {
// Coverage: We have ensured that len(trustRoot.publicKeys) != 0, when nothing succeeds, there must be at least one failure.
return sarRejected, errors.New(`Internal inconsistency: Rekor SET did not match any key but we have no failures.`)
}
return sarRejected, internal.NewInvalidSignatureError(fmt.Sprintf("No public key verified against the RekorSET: %s", strings.Join(rekorFailures, ", ")))
}
} else {
publicKeys = trustRoot.publicKeys
}
publicKey = trustRoot.publicKey
case trustRoot.fulcio != nil:
if trustRoot.rekorPublicKey == nil { // newPRSigstoreSigned rejects such combinations.
if trustRoot.rekorPublicKeys == nil { // newPRSigstoreSigned rejects such combinations.
return sarRejected, errors.New("Internal inconsistency: Fulcio CA specified without a Rekor public key")
}
untrustedSET, ok := untrustedAnnotations[signature.SigstoreSETAnnotationKey]
@ -179,19 +248,20 @@ func (pr *prSigstoreSigned) isSignatureAccepted(ctx context.Context, image priva
if untrustedIntermediateChain, ok := untrustedAnnotations[signature.SigstoreIntermediateCertificateChainAnnotationKey]; ok {
untrustedIntermediateChainBytes = []byte(untrustedIntermediateChain)
}
pk, err := verifyRekorFulcio(trustRoot.rekorPublicKey, trustRoot.fulcio,
pk, err := verifyRekorFulcio(trustRoot.rekorPublicKeys, trustRoot.fulcio,
[]byte(untrustedSET), []byte(untrustedCert), untrustedIntermediateChainBytes, untrustedBase64Signature, untrustedPayload)
if err != nil {
return sarRejected, err
}
publicKey = pk
publicKeys = []crypto.PublicKey{pk}
}
if publicKey == nil {
// Coverage: This should never happen, we have already excluded the possibility in the switch above.
if len(publicKeys) == 0 {
// Coverage: This should never happen, we ensured that trustRoot.publicKeys is non-empty if set,
// and we have already excluded the possibility in the switch above.
return sarRejected, fmt.Errorf("Internal inconsistency: publicKey not set before verifying sigstore payload")
}
signature, err := internal.VerifySigstorePayload(publicKey, untrustedPayload, untrustedBase64Signature, internal.SigstorePayloadAcceptanceRules{
signature, err := internal.VerifySigstorePayload(publicKeys, untrustedPayload, untrustedBase64Signature, internal.SigstorePayloadAcceptanceRules{
ValidateSignedDockerReference: func(ref string) error {
if !pr.SignedIdentity.matchesDockerReference(image, ref) {
return PolicyRequirementError(fmt.Sprintf("Signature for identity %q is not accepted", ref))

View File

@ -89,8 +89,11 @@ func TestPRSigstoreSignedFulcioPrepareTrustRoot(t *testing.T) {
func TestPRSigstoreSignedPrepareTrustRoot(t *testing.T) {
const testKeyPath = "fixtures/cosign.pub"
const testKeyPath2 = "fixtures/cosign2.pub"
testKeyData, err := os.ReadFile(testKeyPath)
require.NoError(t, err)
testKeyData2, err := os.ReadFile(testKeyPath2)
require.NoError(t, err)
testFulcio, err := NewPRSigstoreSignedFulcio(
PRSigstoreSignedFulcioWithCAPath("fixtures/fulcio_v1.crt.pem"),
PRSigstoreSignedFulcioWithOIDCIssuer("https://github.com/login/oauth"),
@ -104,23 +107,23 @@ func TestPRSigstoreSignedPrepareTrustRoot(t *testing.T) {
testIdentityOption := PRSigstoreSignedWithSignedIdentity(testIdentity)
// Success with public key
for _, c := range [][]PRSigstoreSignedOption{
{
PRSigstoreSignedWithKeyPath(testKeyPath),
testIdentityOption,
},
{
PRSigstoreSignedWithKeyData(testKeyData),
testIdentityOption,
},
for _, c := range []struct {
option PRSigstoreSignedOption
numKeys int
}{
{PRSigstoreSignedWithKeyPath(testKeyPath), 1},
{PRSigstoreSignedWithKeyPaths([]string{testKeyPath, testKeyPath2}), 2},
{PRSigstoreSignedWithKeyData(testKeyData), 1},
{PRSigstoreSignedWithKeyDatas([][]byte{testKeyData, testKeyData2}), 2},
} {
pr, err := newPRSigstoreSigned(c...)
pr, err := newPRSigstoreSigned(c.option, testIdentityOption)
require.NoError(t, err)
res, err := pr.prepareTrustRoot()
require.NoError(t, err)
assert.NotNil(t, res.publicKey)
assert.NotNil(t, res.publicKeys)
assert.Len(t, res.publicKeys, c.numKeys)
assert.Nil(t, res.fulcio)
assert.Nil(t, res.rekorPublicKey)
assert.Nil(t, res.rekorPublicKeys)
}
// Success with Fulcio
pr, err := newPRSigstoreSigned(
@ -131,29 +134,33 @@ func TestPRSigstoreSignedPrepareTrustRoot(t *testing.T) {
require.NoError(t, err)
res, err := pr.prepareTrustRoot()
require.NoError(t, err)
assert.Nil(t, res.publicKey)
assert.Nil(t, res.publicKeys)
assert.NotNil(t, res.fulcio)
assert.NotNil(t, res.rekorPublicKey)
assert.Len(t, res.rekorPublicKeys, 1)
// Success with Rekor public key
for _, c := range [][]PRSigstoreSignedOption{
{
PRSigstoreSignedWithKeyData(testKeyData),
PRSigstoreSignedWithRekorPublicKeyPath(testRekorPublicKeyPath),
testIdentityOption,
},
{
PRSigstoreSignedWithKeyData(testKeyData),
PRSigstoreSignedWithRekorPublicKeyData(testRekorPublicKeyData),
testIdentityOption,
},
for _, keyOption := range []PRSigstoreSignedOption{
PRSigstoreSignedWithKeyData(testKeyData),
PRSigstoreSignedWithKeyPaths([]string{testKeyPath, testKeyPath2}),
PRSigstoreSignedWithKeyData(testKeyData),
PRSigstoreSignedWithKeyDatas([][]byte{testKeyData, testKeyData2}),
} {
pr, err := newPRSigstoreSigned(c...)
require.NoError(t, err)
res, err := pr.prepareTrustRoot()
require.NoError(t, err)
assert.NotNil(t, res.publicKey)
assert.Nil(t, res.fulcio)
assert.NotNil(t, res.rekorPublicKey)
for _, rekor := range []struct {
option PRSigstoreSignedOption
numKeys int
}{
{PRSigstoreSignedWithRekorPublicKeyPath(testRekorPublicKeyPath), 1},
{PRSigstoreSignedWithRekorPublicKeyPaths([]string{testRekorPublicKeyPath, testKeyPath}), 2},
{PRSigstoreSignedWithRekorPublicKeyData(testRekorPublicKeyData), 1},
{PRSigstoreSignedWithRekorPublicKeyDatas([][]byte{testRekorPublicKeyData, testKeyData}), 2},
} {
pr, err := newPRSigstoreSigned(keyOption, rekor.option, testIdentityOption)
require.NoError(t, err)
res, err := pr.prepareTrustRoot()
require.NoError(t, err)
assert.NotNil(t, res.publicKeys)
assert.Nil(t, res.fulcio)
assert.Len(t, res.rekorPublicKeys, rekor.numKeys)
}
}
// Failure
@ -171,10 +178,52 @@ func TestPRSigstoreSignedPrepareTrustRoot(t *testing.T) {
KeyPath: "fixtures/this/does/not/exist",
SignedIdentity: testIdentity,
},
{ // Both KeyPath and KeyPaths specified
KeyPath: testKeyPath,
KeyPaths: []string{testKeyPath, testKeyPath2},
SignedIdentity: testIdentity,
},
{ // Empty KeyPaths
KeyPaths: []string{},
SignedIdentity: testIdentity,
},
{ // Invalid KeyPaths element
KeyPaths: []string{"fixtures/image.signature", testKeyPath2},
SignedIdentity: testIdentity,
},
{
KeyPaths: []string{testKeyPath2, "fixtures/image.signature"},
SignedIdentity: testIdentity,
},
{ // Unusable KeyPaths element
KeyPaths: []string{"fixtures/this/does/not/exist", testKeyPath2},
SignedIdentity: testIdentity,
},
{
KeyPaths: []string{testKeyPath2, "fixtures/this/does/not/exist"},
SignedIdentity: testIdentity,
},
{ // Invalid public key data
KeyData: []byte("this is invalid"),
SignedIdentity: testIdentity,
},
{ // Both KeyData and KeyDatas specified
KeyData: testKeyData,
KeyDatas: [][]byte{testKeyData, testKeyData2},
SignedIdentity: testIdentity,
},
{ // Empty KeyDatas
KeyDatas: [][]byte{},
SignedIdentity: testIdentity,
},
{ // Invalid KeyDatas element
KeyDatas: [][]byte{[]byte("this is invalid"), testKeyData2},
SignedIdentity: testIdentity,
},
{
KeyDatas: [][]byte{testKeyData, []byte("this is invalid")},
SignedIdentity: testIdentity,
},
{ // Invalid Fulcio configuration
Fulcio: &prSigstoreSignedFulcio{},
RekorPublicKeyData: testKeyData,
@ -191,11 +240,53 @@ func TestPRSigstoreSignedPrepareTrustRoot(t *testing.T) {
RekorPublicKeyPath: "fixtures/image.signature",
SignedIdentity: testIdentity,
},
{ // Both RekorPublicKeyPath and RekorPublicKeyPaths specified
KeyData: testKeyData,
RekorPublicKeyPath: testRekorPublicKeyPath,
RekorPublicKeyPaths: []string{testRekorPublicKeyPath, testKeyPath},
SignedIdentity: testIdentity,
},
{ // Empty RekorPublicKeyPaths
KeyData: testKeyData,
RekorPublicKeyPaths: []string{},
SignedIdentity: testIdentity,
},
{ // Invalid RekorPublicKeyPaths
KeyData: testKeyData,
RekorPublicKeyPaths: []string{"fixtures/image.signature", testRekorPublicKeyPath},
SignedIdentity: testIdentity,
},
{ // Invalid RekorPublicKeyPaths
KeyData: testKeyData,
RekorPublicKeyPaths: []string{testRekorPublicKeyPath, "fixtures/image.signature"},
SignedIdentity: testIdentity,
},
{ // Invalid Rekor public key data
KeyData: testKeyData,
RekorPublicKeyData: []byte("this is invalid"),
SignedIdentity: testIdentity,
},
{ // Both RekorPublicKeyData and RekorPublicKeyDatas specified
KeyData: testKeyData,
RekorPublicKeyData: testRekorPublicKeyData,
RekorPublicKeyDatas: [][]byte{testRekorPublicKeyData, testKeyData},
SignedIdentity: testIdentity,
},
{ // Empty RekorPublicKeyDatas
KeyData: testKeyData,
RekorPublicKeyDatas: [][]byte{},
SignedIdentity: testIdentity,
},
{ // Invalid RekorPublicKeyDatas
KeyData: testKeyData,
RekorPublicKeyDatas: [][]byte{[]byte("this is invalid"), testRekorPublicKeyData},
SignedIdentity: testIdentity,
},
{
KeyData: testKeyData,
RekorPublicKeyDatas: [][]byte{testRekorPublicKeyData, []byte("this is invalid")},
SignedIdentity: testIdentity,
},
{ // Rekor public key is not ECDSA
KeyData: testKeyData,
RekorPublicKeyPath: "fixtures/some-rsa-key.pub",
@ -272,6 +363,8 @@ func TestPRrSigstoreSignedIsSignatureAccepted(t *testing.T) {
testFulcioRekorImageSig := sigstoreSignatureFromFile(t, "fixtures/dir-img-cosign-fulcio-rekor-valid/signature-1")
keyData, err := os.ReadFile("fixtures/cosign.pub")
require.NoError(t, err)
keyData2, err := os.ReadFile("fixtures/cosign2.pub")
require.NoError(t, err)
// prepareTrustRoot fails
pr := &prSigstoreSigned{
@ -319,16 +412,35 @@ func TestPRrSigstoreSignedIsSignatureAccepted(t *testing.T) {
assertRejected(sar, err)
// Successful key+Rekor use
for _, keyPaths := range [][]string{
{"fixtures/cosign2.pub"},
{"fixtures/cosign2.pub", "fixtures/cosign.pub"},
{"fixtures/cosign.pub", "fixtures/cosign2.pub"},
} {
for _, rekorKeyPaths := range [][]string{
{"fixtures/rekor.pub"},
{"fixtures/rekor.pub", "fixtures/cosign.pub"},
{"fixtures/cosign.pub", "fixtures/rekor.pub"},
} {
pr, err := newPRSigstoreSigned(
PRSigstoreSignedWithKeyPaths(keyPaths),
PRSigstoreSignedWithRekorPublicKeyPaths(rekorKeyPaths),
PRSigstoreSignedWithSignedIdentity(prm),
)
require.NoError(t, err)
sar, err := pr.isSignatureAccepted(context.Background(), testKeyRekorImage, testKeyRekorImageSig)
require.NoError(t, err)
assertAccepted(sar, err)
}
}
// key+Rekor, missing Rekor SET annotation
pr, err = newPRSigstoreSigned(
PRSigstoreSignedWithKeyPath("fixtures/cosign2.pub"),
PRSigstoreSignedWithRekorPublicKeyPath("fixtures/rekor.pub"),
PRSigstoreSignedWithSignedIdentity(prm),
)
require.NoError(t, err)
sar, err = pr.isSignatureAccepted(context.Background(), testKeyRekorImage, testKeyRekorImageSig)
assertAccepted(sar, err)
// key+Rekor, missing Rekor SET annotation
sar, err = pr.isSignatureAccepted(context.Background(), nil,
sigstoreSignatureWithoutAnnotation(t, testKeyRekorImageSig, signature.SigstoreSETAnnotationKey))
assertRejected(sar, err)
@ -339,15 +451,36 @@ func TestPRrSigstoreSignedIsSignatureAccepted(t *testing.T) {
sigstoreSignatureWithModifiedAnnotation(testKeyRekorImageSig, signature.SigstoreSETAnnotationKey,
"this is not a valid SET"))
assertRejected(sar, err)
// Fulcio: A Rekor SET which we dont accept (one of many reasons)
pr2, err := newPRSigstoreSigned(
PRSigstoreSignedWithKeyPath("fixtures/cosign2.pub"),
PRSigstoreSignedWithRekorPublicKeyPath("fixtures/cosign.pub"), // not rekor.pub = a key mismatch
// key+Rekor: A Rekor SET which we dont accept (one of many reasons)
for _, keyPaths := range [][]string{
{"fixtures/cosign2.pub"},
{"fixtures/cosign2.pub", "fixtures/cosign.pub"},
{"fixtures/cosign.pub", "fixtures/cosign2.pub"},
} {
pr, err := newPRSigstoreSigned(
PRSigstoreSignedWithKeyPaths(keyPaths),
PRSigstoreSignedWithRekorPublicKeyPath("fixtures/cosign.pub"), // not rekor.pub = a key mismatch
PRSigstoreSignedWithSignedIdentity(prm),
)
require.NoError(t, err)
// Pass a nil pointer to, kind of, test that the return value does not depend on the image.
sar, err = pr.isSignatureAccepted(context.Background(), nil, testKeyRekorImageSig)
assertRejected(sar, err)
}
// key+Rekor: A valid Rekor SET for one accepted key, but a signature with a _different_ accepted key
pr, err = newPRSigstoreSigned(
PRSigstoreSignedWithKeyPaths([]string{"fixtures/cosign.pub", "fixtures/cosign2.pub"}),
PRSigstoreSignedWithRekorPublicKeyPath("fixtures/rekor.pub"),
PRSigstoreSignedWithSignedIdentity(prm),
)
require.NoError(t, err)
// Pass a nil pointer to, kind of, test that the return value does not depend on the image.
sar, err = pr2.isSignatureAccepted(context.Background(), nil, testKeyRekorImageSig)
// testKeyImageSig is signed by cosign.pub; the SET contains cosign2.pub.
// The SET includes the signature contents… so this actually fails on "signature in Rekor SET does not match"
// rather than accepting the SET and later failing to validate payload; wed need a way to generate the same signature
// using two different private keys.
sar, err = pr.isSignatureAccepted(context.Background(), nil, sigstoreSignatureWithModifiedAnnotation(testKeyImageSig,
signature.SigstoreSETAnnotationKey, testKeyRekorImageSig.UntrustedAnnotations()[signature.SigstoreSETAnnotationKey]))
assertRejected(sar, err)
// Successful Fulcio certificate use
@ -357,18 +490,24 @@ func TestPRrSigstoreSignedIsSignatureAccepted(t *testing.T) {
PRSigstoreSignedFulcioWithSubjectEmail("mitr@redhat.com"),
)
require.NoError(t, err)
pr, err = newPRSigstoreSigned(
PRSigstoreSignedWithFulcio(fulcio),
PRSigstoreSignedWithRekorPublicKeyPath("fixtures/rekor.pub"),
PRSigstoreSignedWithSignedIdentity(prm),
)
require.NoError(t, err)
sar, err = pr.isSignatureAccepted(context.Background(), testFulcioRekorImage,
testFulcioRekorImageSig)
assertAccepted(sar, err)
for _, rekorKeyPaths := range [][]string{
{"fixtures/rekor.pub"},
{"fixtures/rekor.pub", "fixtures/cosign.pub"},
{"fixtures/cosign.pub", "fixtures/rekor.pub"},
} {
pr, err = newPRSigstoreSigned(
PRSigstoreSignedWithFulcio(fulcio),
PRSigstoreSignedWithRekorPublicKeyPaths(rekorKeyPaths),
PRSigstoreSignedWithSignedIdentity(prm),
)
require.NoError(t, err)
sar, err = pr.isSignatureAccepted(context.Background(), testFulcioRekorImage,
testFulcioRekorImageSig)
assertAccepted(sar, err)
}
// Fulcio, no Rekor requirement
pr2 = &prSigstoreSigned{
pr2 := &prSigstoreSigned{
Fulcio: fulcio,
SignedIdentity: prm,
}
@ -449,22 +588,23 @@ func TestPRrSigstoreSignedIsSignatureAccepted(t *testing.T) {
sar, err = pr2.isSignatureAccepted(context.Background(), nil, testFulcioRekorImageSig)
assertRejected(sar, err)
// Successful validation, with KeyData and KeyPath
pr, err = newPRSigstoreSigned(
// Successful validation, with KeyPath/KeyPaths/KeyData/KeyDatas
for _, opt := range []PRSigstoreSignedOption{
PRSigstoreSignedWithKeyPath("fixtures/cosign.pub"),
PRSigstoreSignedWithSignedIdentity(prm),
)
require.NoError(t, err)
sar, err = pr.isSignatureAccepted(context.Background(), testKeyImage, testKeyImageSig)
assertAccepted(sar, err)
pr, err = newPRSigstoreSigned(
PRSigstoreSignedWithKeyPaths([]string{"fixtures/cosign.pub", "fixtures/cosign2.pub"}),
PRSigstoreSignedWithKeyPaths([]string{"fixtures/cosign2.pub", "fixtures/cosign.pub"}),
PRSigstoreSignedWithKeyData(keyData),
PRSigstoreSignedWithSignedIdentity(prm),
)
require.NoError(t, err)
sar, err = pr.isSignatureAccepted(context.Background(), testKeyImage, testKeyImageSig)
assertAccepted(sar, err)
PRSigstoreSignedWithKeyDatas([][]byte{keyData, keyData2}),
PRSigstoreSignedWithKeyDatas([][]byte{keyData2, keyData}),
} {
pr, err := newPRSigstoreSigned(
opt,
PRSigstoreSignedWithSignedIdentity(prm),
)
require.NoError(t, err)
sar, err = pr.isSignatureAccepted(context.Background(), testKeyImage, testKeyImageSig)
assertAccepted(sar, err)
}
// A signature which does not verify
pr, err = newPRSigstoreSigned(

View File

@ -74,7 +74,7 @@ type prSignedBy struct {
// KeyPath is a pathname to a local file containing the trusted key(s). Exactly one of KeyPath, KeyPaths and KeyData must be specified.
KeyPath string `json:"keyPath,omitempty"`
// KeyPaths if a set of pathnames to local files containing the trusted key(s). Exactly one of KeyPath, KeyPaths and KeyData must be specified.
// KeyPaths is a set of pathnames to local files containing the trusted key(s). Exactly one of KeyPath, KeyPaths and KeyData must be specified.
KeyPaths []string `json:"keyPaths,omitempty"`
// KeyData contains the trusted key(s), base64-encoded. Exactly one of KeyPath, KeyPaths and KeyData must be specified.
KeyData []byte `json:"keyData,omitempty"`
@ -111,24 +111,35 @@ type prSignedBaseLayer struct {
type prSigstoreSigned struct {
prCommon
// KeyPath is a pathname to a local file containing the trusted key. Exactly one of KeyPath, KeyData, Fulcio must be specified.
// KeyPath is a pathname to a local file containing the trusted key. Exactly one of KeyPath, KeyPaths, KeyData, KeyDatas and Fulcio must be specified.
KeyPath string `json:"keyPath,omitempty"`
// KeyData contains the trusted key, base64-encoded. Exactly one of KeyPath, KeyData, Fulcio must be specified.
// KeyPaths is a set of pathnames to local files containing the trusted key(s). Exactly one of KeyPath, KeyPaths, KeyData, KeyDatas and Fulcio must be specified.
KeyPaths []string `json:"keyPaths,omitempty"`
// KeyData contains the trusted key, base64-encoded. Exactly one of KeyPath, KeyPaths, KeyData, KeyDatas and Fulcio must be specified.
KeyData []byte `json:"keyData,omitempty"`
// FIXME: Multiple public keys?
// KeyDatas is a set of trusted keys, base64-encoded. Exactly one of KeyPath, KeyPaths, KeyData, KeyDatas and Fulcio must be specified.
KeyDatas [][]byte `json:"keyDatas,omitempty"`
// Fulcio specifies which Fulcio-generated certificates are accepted. Exactly one of KeyPath, KeyData, Fulcio must be specified.
// Fulcio specifies which Fulcio-generated certificates are accepted. Exactly one of KeyPath, KeyPaths, KeyData, KeyDatas and Fulcio must be specified.
// If Fulcio is specified, one of RekorPublicKeyPath or RekorPublicKeyData must be specified as well.
Fulcio PRSigstoreSignedFulcio `json:"fulcio,omitempty"`
// RekorPublicKeyPath is a pathname to local file containing a public key of a Rekor server which must record acceptable signatures.
// If Fulcio is used, one of RekorPublicKeyPath or RekorPublicKeyData must be specified as well; otherwise it is optional
// (and Rekor inclusion is not required if a Rekor public key is not specified).
// If Fulcio is used, one of RekorPublicKeyPath, RekorPublicKeyPaths, RekorPublicKeyData and RekorPublicKeyDatas must be specified as well;
// otherwise it is optional (and Rekor inclusion is not required if a Rekor public key is not specified).
RekorPublicKeyPath string `json:"rekorPublicKeyPath,omitempty"`
// RekorPublicKeyPaths is a set of pathnames to local files, each containing a public key of a Rekor server. One of the keys must record acceptable signatures.
// If Fulcio is used, one of RekorPublicKeyPath, RekorPublicKeyPaths, RekorPublicKeyData and RekorPublicKeyDatas must be specified as well;
// otherwise it is optional (and Rekor inclusion is not required if a Rekor public key is not specified).
RekorPublicKeyPaths []string `json:"rekorPublicKeyPaths,omitempty"`
// RekorPublicKeyPath contain a base64-encoded public key of a Rekor server which must record acceptable signatures.
// If Fulcio is used, one of RekorPublicKeyPath or RekorPublicKeyData must be specified as well; otherwise it is optional
// (and Rekor inclusion is not required if a Rekor public key is not specified).
// If Fulcio is used, one of RekorPublicKeyPath, RekorPublicKeyPaths, RekorPublicKeyData and RekorPublicKeyDatas must be specified as well;
// otherwise it is optional (and Rekor inclusion is not required if a Rekor public key is not specified).
RekorPublicKeyData []byte `json:"rekorPublicKeyData,omitempty"`
// RekorPublicKeyDatas each contain a base64-encoded public key of a Rekor server. One of the keys must record acceptable signatures.
// If Fulcio is used, one of RekorPublicKeyPath, RekorPublicKeyPaths, RekorPublicKeyData and RekorPublicKeyDatas must be specified as well;
// otherwise it is optional (and Rekor inclusion is not required if a Rekor public key is not specified).
RekorPublicKeyDatas [][]byte `json:"rekorPublicKeyDatas,omitempty"`
// SignedIdentity specifies what image identity the signature must be claiming about the image.
// Defaults to "matchRepoDigestOrExact" if not specified.

View File

@ -2,6 +2,7 @@ package sigstore
import (
"context"
"crypto"
"os"
"path/filepath"
"testing"
@ -44,7 +45,7 @@ func TestGenerateKeyPair(t *testing.T) {
publicKey, err := cryptoutils.UnmarshalPEMToPublicKey(keyPair.PublicKey)
require.NoError(t, err)
_, err = internal.VerifySigstorePayload(publicKey, sig.UntrustedPayload(),
_, err = internal.VerifySigstorePayload([]crypto.PublicKey{publicKey}, sig.UntrustedPayload(),
sig.UntrustedAnnotations()[signature.SigstoreSignatureAnnotationKey],
internal.SigstorePayloadAcceptanceRules{
ValidateSignedDockerReference: func(ref string) error {

View File

@ -8,7 +8,7 @@ const (
// VersionMinor is for functionality in a backwards-compatible manner
VersionMinor = 32
// VersionPatch is for backwards-compatible bug fixes
VersionPatch = 2
VersionPatch = 3
// VersionDev indicates development branch. Releases will be empty string.
VersionDev = "-dev"