mirror of https://github.com/containers/image.git
275 lines
8.3 KiB
Go
275 lines
8.3 KiB
Go
package signature
|
|
|
|
import (
|
|
"crypto/ecdsa"
|
|
"crypto/elliptic"
|
|
"crypto/rand"
|
|
"crypto/x509"
|
|
"crypto/x509/pkix"
|
|
"encoding/asn1"
|
|
"encoding/pem"
|
|
"os"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/sigstore/sigstore/pkg/cryptoutils"
|
|
"github.com/stretchr/testify/assert"
|
|
"github.com/stretchr/testify/require"
|
|
)
|
|
|
|
func TestPKITrustRootValidate(t *testing.T) {
|
|
certs := x509.NewCertPool() // Empty is valid enough for our purposes.
|
|
intermediateCerts := x509.NewCertPool() // Empty is valid enough for our purposes.
|
|
|
|
for _, tr := range []pkiTrustRoot{
|
|
{
|
|
caRootsCertificates: certs,
|
|
caIntermediateCertificates: intermediateCerts,
|
|
},
|
|
} {
|
|
err := tr.validate()
|
|
assert.Error(t, err)
|
|
}
|
|
|
|
for _, tr := range []pkiTrustRoot{
|
|
{
|
|
caRootsCertificates: certs,
|
|
subjectEmail: "",
|
|
subjectHostname: "hostname",
|
|
},
|
|
{
|
|
caRootsCertificates: certs,
|
|
subjectEmail: "email",
|
|
subjectHostname: "",
|
|
},
|
|
{
|
|
caRootsCertificates: certs,
|
|
caIntermediateCertificates: intermediateCerts,
|
|
subjectEmail: "",
|
|
subjectHostname: "hostname",
|
|
},
|
|
{
|
|
caRootsCertificates: certs,
|
|
caIntermediateCertificates: intermediateCerts,
|
|
subjectEmail: "email",
|
|
subjectHostname: "",
|
|
},
|
|
} {
|
|
err := tr.validate()
|
|
assert.NoError(t, err)
|
|
}
|
|
}
|
|
|
|
func TestPKIVerify(t *testing.T) {
|
|
caRootsCertificates := x509.NewCertPool()
|
|
pkiRootsCrtPEM, err := os.ReadFile("fixtures/pki_root_crts.pem")
|
|
require.NoError(t, err)
|
|
ok := caRootsCertificates.AppendCertsFromPEM(pkiRootsCrtPEM)
|
|
require.True(t, ok)
|
|
caIntermediateCertificates := x509.NewCertPool()
|
|
pkiIntermediateCrtPEMs, err := os.ReadFile("fixtures/pki_intermediate_crts.pem")
|
|
require.NoError(t, err)
|
|
ok = caIntermediateCertificates.AppendCertsFromPEM(pkiIntermediateCrtPEMs)
|
|
require.True(t, ok)
|
|
certBytes, err := os.ReadFile("fixtures/pki-cert")
|
|
require.NoError(t, err)
|
|
chainBytes, err := os.ReadFile("fixtures/pki-chain")
|
|
require.NoError(t, err)
|
|
|
|
// Success
|
|
tr := &pkiTrustRoot{
|
|
caRootsCertificates: caRootsCertificates,
|
|
caIntermediateCertificates: caIntermediateCertificates,
|
|
subjectEmail: "qiwan@redhat.com",
|
|
subjectHostname: "myhost.example.com",
|
|
}
|
|
pk, err := verifyPKI(tr, certBytes, chainBytes)
|
|
require.NoError(t, err)
|
|
assertPublicKeyMatchesCert(t, certBytes, pk)
|
|
|
|
// Invalid intermediate certificate
|
|
pk, err = verifyPKI(tr, certBytes, []byte("not a certificate"))
|
|
assert.Error(t, err)
|
|
assert.Nil(t, pk)
|
|
|
|
// Invalid leaf certificate
|
|
pk, err = verifyPKI(tr, []byte("not a certificate"), chainBytes)
|
|
assert.Error(t, err)
|
|
assert.Nil(t, pk)
|
|
|
|
// Failure with intermediates provided in neither signature nor config
|
|
pk, err = verifyPKI(&pkiTrustRoot{
|
|
caRootsCertificates: caRootsCertificates,
|
|
}, certBytes, []byte{})
|
|
require.Error(t, err)
|
|
assert.Nil(t, pk)
|
|
|
|
// Success with intermediate provided in config only
|
|
pk, err = verifyPKI(&pkiTrustRoot{
|
|
caRootsCertificates: caRootsCertificates,
|
|
caIntermediateCertificates: caIntermediateCertificates,
|
|
}, certBytes, []byte{})
|
|
require.NoError(t, err)
|
|
assertPublicKeyMatchesCert(t, certBytes, pk)
|
|
|
|
// Success with intermediate provided in signature only
|
|
pk, err = verifyPKI(&pkiTrustRoot{
|
|
caRootsCertificates: caRootsCertificates,
|
|
}, certBytes, chainBytes)
|
|
require.NoError(t, err)
|
|
assertPublicKeyMatchesCert(t, certBytes, pk)
|
|
|
|
referenceTime := time.Now()
|
|
testCAKey, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
|
|
require.NoError(t, err)
|
|
testCASN, err := cryptoutils.GenerateSerialNumber()
|
|
require.NoError(t, err)
|
|
testCAContents := x509.Certificate{
|
|
SerialNumber: testCASN,
|
|
Subject: pkix.Name{CommonName: "root CA"},
|
|
NotBefore: referenceTime.Add(-1 * time.Minute),
|
|
NotAfter: referenceTime.Add(1 * time.Hour),
|
|
BasicConstraintsValid: true,
|
|
IsCA: true,
|
|
}
|
|
testCACertBytes, err := x509.CreateCertificate(rand.Reader, &testCAContents, &testCAContents,
|
|
testCAKey.Public(), testCAKey)
|
|
require.NoError(t, err)
|
|
testCACert, err := x509.ParseCertificate(testCACertBytes)
|
|
require.NoError(t, err)
|
|
testCACertPool := x509.NewCertPool()
|
|
testCACertPool.AddCert(testCACert)
|
|
|
|
for _, c := range []struct {
|
|
name string
|
|
fn func(cert *x509.Certificate)
|
|
errorFragment string
|
|
}{
|
|
{
|
|
// OtherName SAN element, non-Fulcio certificates does not create this value
|
|
// should cause a failure if none of the Go-parsed SAN elements present
|
|
name: "OtherName in SAN",
|
|
fn: func(cert *x509.Certificate) {
|
|
// Setting SAN in ExtraExtensions causes EmailAddresses to be ignored,
|
|
// so we need to construct the whole SAN manually.
|
|
sansBytes, err := asn1.Marshal([]asn1.RawValue{
|
|
{
|
|
Class: 2,
|
|
Tag: 0,
|
|
IsCompound: false,
|
|
Bytes: []byte("otherName"),
|
|
},
|
|
})
|
|
require.NoError(t, err)
|
|
cert.ExtraExtensions = append(cert.ExtraExtensions, pkix.Extension{
|
|
Id: cryptoutils.SANOID,
|
|
Critical: true,
|
|
Value: sansBytes,
|
|
})
|
|
},
|
|
errorFragment: "unhandled critical extension",
|
|
},
|
|
{ // Other completely unrecognized critical extensions still cause failures
|
|
name: "Unhandled critical extension",
|
|
fn: func(cert *x509.Certificate) {
|
|
cert.ExtraExtensions = append(cert.ExtraExtensions, pkix.Extension{
|
|
Id: asn1.ObjectIdentifier{2, 99999, 99998, 99997, 99996},
|
|
Critical: true,
|
|
Value: []byte("whatever"),
|
|
})
|
|
},
|
|
errorFragment: "unhandled critical extension",
|
|
},
|
|
{
|
|
name: "Missing subject hostname",
|
|
fn: func(cert *x509.Certificate) {
|
|
cert.DNSNames = nil
|
|
},
|
|
errorFragment: "Unexpected subject hostname",
|
|
},
|
|
{
|
|
name: "Multiple hostnames, one matches",
|
|
fn: func(cert *x509.Certificate) {
|
|
cert.DNSNames = []string{"a.example.com", "myhost.example.com", "c.example.com"}
|
|
},
|
|
errorFragment: "",
|
|
},
|
|
{
|
|
name: "Hostname mismatch",
|
|
fn: func(cert *x509.Certificate) {
|
|
cert.DNSNames = []string{"this-does-not-match.example.com"}
|
|
},
|
|
errorFragment: "Unexpected subject hostname",
|
|
},
|
|
{
|
|
name: "Multiple hostnames, no matches",
|
|
fn: func(cert *x509.Certificate) {
|
|
cert.DNSNames = []string{"a.example.com", "b.example.com", "c.example.com"}
|
|
},
|
|
errorFragment: "Unexpected subject hostname",
|
|
},
|
|
{
|
|
name: "Missing subject email",
|
|
fn: func(cert *x509.Certificate) {
|
|
cert.EmailAddresses = nil
|
|
},
|
|
errorFragment: `Required email "test-user@example.com" not found`,
|
|
},
|
|
{
|
|
name: "Multiple emails, one matches",
|
|
fn: func(cert *x509.Certificate) {
|
|
cert.EmailAddresses = []string{"a@example.com", "test-user@example.com", "c@example.com"}
|
|
},
|
|
errorFragment: "",
|
|
},
|
|
{
|
|
name: "Email mismatch",
|
|
fn: func(cert *x509.Certificate) {
|
|
cert.EmailAddresses = []string{"a@example.com"}
|
|
},
|
|
errorFragment: `Required email "test-user@example.com" not found`,
|
|
},
|
|
{
|
|
name: "Multiple emails, no matches",
|
|
fn: func(cert *x509.Certificate) {
|
|
cert.EmailAddresses = []string{"a@example.com", "b@example.com", "c@example.com"}
|
|
},
|
|
errorFragment: `Required email "test-user@example.com" not found`,
|
|
},
|
|
} {
|
|
testLeafKey, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
|
|
require.NoError(t, err, c.name)
|
|
testLeafSN, err := cryptoutils.GenerateSerialNumber()
|
|
require.NoError(t, err, c.name)
|
|
testLeafContents := x509.Certificate{
|
|
SerialNumber: testLeafSN,
|
|
Subject: pkix.Name{CommonName: "leaf"},
|
|
NotBefore: referenceTime.Add(-1 * time.Minute),
|
|
NotAfter: referenceTime.Add(1 * time.Hour),
|
|
EmailAddresses: []string{"test-user@example.com"},
|
|
DNSNames: []string{"myhost.example.com"},
|
|
}
|
|
c.fn(&testLeafContents)
|
|
testLeafCert, err := x509.CreateCertificate(rand.Reader, &testLeafContents, testCACert, testLeafKey.Public(), testCAKey)
|
|
require.NoError(t, err, c.name)
|
|
tr := pkiTrustRoot{
|
|
caRootsCertificates: testCACertPool,
|
|
caIntermediateCertificates: caIntermediateCertificates,
|
|
subjectHostname: "myhost.example.com",
|
|
subjectEmail: "test-user@example.com",
|
|
}
|
|
testLeafPEM := pem.EncodeToMemory(&pem.Block{
|
|
Type: "CERTIFICATE",
|
|
Bytes: testLeafCert,
|
|
})
|
|
pk, err := verifyPKI(&tr, testLeafPEM, chainBytes)
|
|
if c.errorFragment == "" {
|
|
require.NoError(t, err, c.name)
|
|
assertPublicKeyMatchesCert(t, testLeafPEM, pk)
|
|
} else {
|
|
assert.ErrorContains(t, err, c.errorFragment, c.name)
|
|
assert.Nil(t, pk, c.name)
|
|
}
|
|
}
|
|
}
|