Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

efi: Add support for shim binaries used during snapd spread tests #266

Merged
6 changes: 6 additions & 0 deletions efi/certs_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,9 @@ var (
testUefiSigningKey1_1 []byte
testUefiCACert2 []byte
testUefiCAKey2 []byte

snakeoilCert []byte
snakeoilKey []byte
)

func initTestCertificate(key []byte, subject pkix.Name, serialNumber *big.Int, isCA bool, keyUsage x509.KeyUsage, extKeyUsage []x509.ExtKeyUsage, issuerCert []byte, issuerKey []byte) []byte {
Expand Down Expand Up @@ -125,6 +128,9 @@ func init() {
testUefiSigningKey1_1 = testutil.MustDecodePEMType("RSA PRIVATE KEY", testUefiSigningKey1_1PEM)
testUefiCAKey2 = testutil.MustDecodePEMType("RSA PRIVATE KEY", testUefiCAKey2PEM)

snakeoilCert = testutil.MustDecodePEMType("CERTIFICATE", snakeoilCertPEM)
snakeoilKey = testutil.MustDecodePEMType("RSA PRIVATE KEY", snakeoilKeyPEM)

testPKCert1 = initTestCertificate(
testPKKey1,
pkix.Name{
Expand Down
6 changes: 6 additions & 0 deletions efi/embeds_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -37,9 +37,15 @@ var (

//go:embed testdata/src/certs/canonical-uefi-ca.crt
canonicalCACertPEM []byte

//go:embed testdata/src/certs/PkKek-1-snakeoil.pem
snakeoilCertPEM []byte
)

var (
//go:embed testdata/src/keys/PkKek-1-snakeoil.key
snakeoilKeyPEM []byte

//go:embed testdata/src/keys/TestPk1.key
testPKKey1PEM []byte

Expand Down
10 changes: 10 additions & 0 deletions efi/export_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,8 @@ var (
ShimVersionIs = shimVersionIs
WithAuthority = withAuthority
WithImageRule = withImageRule
WithImageRuleOnlyForTesting = withImageRuleOnlyForTesting
WithSelfSignedSignerOnlyForTesting = withSelfSignedSignerOnlyForTesting
)

// Alias some unexported types for testing. These are required in order to pass these between functions in tests, or to access
Expand Down Expand Up @@ -185,6 +187,14 @@ func MockReadVar(fn func(string, efi.GUID) ([]byte, efi.VariableAttributes, erro
}
}

func MockSnapdenvTesting(testing bool) (restore func()) {
orig := snapdenvTesting
snapdenvTesting = func() bool { return testing }
return func() {
snapdenvTesting = orig
}
}

func NewRootVarReader(host HostEnvironment) *rootVarReader {
return &rootVarReader{
host: host,
Expand Down
26 changes: 26 additions & 0 deletions efi/image_rules_defs.go
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,23 @@ func makeMicrosoftUEFICASecureBootNamespaceRules() *secureBootNamespaceRules {
// pubkey alg
x509.RSA,
),
withSelfSignedSignerOnlyForTesting(
// O = Snake Oil
[]byte{
0x30, 0x14, 0x31, 0x12, 0x30, 0x10, 0x06, 0x03, 0x55, 0x04,
0x0a, 0x0c, 0x09, 0x53, 0x6e, 0x61, 0x6b, 0x65, 0x20, 0x4f,
0x69, 0x6c,
},
// SKID
[]byte{
0x14, 0x3e, 0xce, 0x5d, 0xbd, 0x93, 0xea, 0xc3, 0xb2, 0xb1,
0x1a, 0x37, 0x86, 0x3d, 0x9f, 0xd7, 0x94, 0x97, 0xf0, 0x8f,
},
// pubkey alg
x509.RSA,
// sig alg
x509.SHA256WithRSA,
),
withImageRule(
"SBAT-capable shim with .sbatlevel section",
imageMatchesAll(
Expand Down Expand Up @@ -119,6 +136,15 @@ func makeMicrosoftUEFICASecureBootNamespaceRules() *secureBootNamespaceRules {
),
newShimLoadHandlerConstructor().WithVersion(mustParseShimVersion("15.2")).New,
),
withImageRuleOnlyForTesting(
"Ubuntu shim 15 with required patches, signed with snakeoil key",
imageMatchesAll(
imageSectionExists(".vendor_cert"),
shimVersionIs("==", "15"),
imageSignedByOrganization("Snake Oil"),
),
newShimLoadHandlerConstructor().WithVersion(mustParseShimVersion("15.2")).New,
),
// Pre SBAT shims - unsupported. These will cause an error from
// newShimLoadHandler rather than allowing this to fallback to the
// null handler.
Expand Down
42 changes: 42 additions & 0 deletions efi/image_rules_defs_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,9 @@
package efi_test

import (
"crypto"
"io"

efi "github.com/canonical/go-efilib"

. "gopkg.in/check.v1"
Expand Down Expand Up @@ -109,6 +112,45 @@ func (s *imageRulesDefsSuite) TestMSNewImageLoadHandlerUbuntuShim15WithFixes2(c
c.Check(shimHandler.SbatLevel, DeepEquals, ShimSbatLevel{})
}

func (s *imageRulesDefsSuite) TestMSNewImageLoadHandlerUbuntuShim15WithFixesInTesting(c *C) {
// Verify we get a correctly configured shimLoadHandler for the Ubuntu shim 15 with
// the required fixes (1.41+15+1552672080.a4a1fbe-0ubuntu1), when it is rebuilt and
// re-signed in snapd spread tests.
restore := MockSnapdenvTesting(true)
defer restore()

h := crypto.SHA256.New()
io.WriteString(h, "foo")

// simulate rebuilding and re-signing shim in spread tests
image := newMockUbuntuShimImage15a(c).unsign().withDigest(crypto.SHA256, h.Sum(nil)).sign(c, testutil.ParsePKCS1PrivateKey(c, snakeoilKey), testutil.ParseCertificate(c, snakeoilCert))

rules := MakeMicrosoftUEFICASecureBootNamespaceRules()
handler, err := rules.NewImageLoadHandler(image.newPeImageHandle())
c.Assert(err, IsNil)
c.Assert(handler, testutil.ConvertibleTo, &ShimLoadHandler{})

shimHandler := handler.(*ShimLoadHandler)
c.Check(shimHandler.Flags, Equals, ShimFlags(0))
c.Check(shimHandler.VendorDb, DeepEquals, &SecureBootDB{
Name: efi.VariableDescriptor{Name: "Shim", GUID: ShimGuid},
Contents: efi.SignatureDatabase{efitest.NewSignatureListX509(c, canonicalCACert, efi.GUID{})},
})
c.Check(shimHandler.SbatLevel, DeepEquals, ShimSbatLevel{})
}

func (s *imageRulesDefsSuite) TestMSNewImageLoadHandlerIgnoreTestAuthorityWhenNotInTestMode(c *C) {
// Verify that the snakeoil key used in snapd spread tests is ignored when not in test mode.
restore := MockSnapdenvTesting(false)
defer restore()

image := newMockUbuntuShimImage15a(c).unsign().sign(c, testutil.ParsePKCS1PrivateKey(c, snakeoilKey), testutil.ParseCertificate(c, snakeoilCert))

rules := MakeMicrosoftUEFICASecureBootNamespaceRules()
_, err := rules.NewImageLoadHandler(image.newPeImageHandle())
c.Check(err, Equals, ErrNoHandler)
}

func (s *imageRulesDefsSuite) TestMSNewImageLoadHandlerUbuntuGrubSbat(c *C) {
// Verify we get a correctly configured grubLoadHandler for the Ubuntu grub
image := newMockUbuntuGrubImage3(c)
Expand Down
55 changes: 53 additions & 2 deletions efi/secureboot_namespace_rules.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,9 +23,14 @@ import (
"bytes"
"crypto/x509"

"github.com/snapcore/snapd/snapdenv"
"golang.org/x/xerrors"
)

var (
snapdenvTesting = snapdenv.Testing
)

// vendorAuthorityGetter provides a way for an imageLoadHandler created by
// secureBootNamespaceRules to supplement the CA's associated with a secure
// boot namespace in the case where the associated image contains a delegated
Expand All @@ -43,9 +48,14 @@ type secureBootAuthorityIdentity struct {
subject []byte
subjectKeyId []byte
publicKeyAlgorithm x509.PublicKeyAlgorithm

issuer []byte
authorityKeyId []byte
signatureAlgorithm x509.SignatureAlgorithm
}

// withAuthority adds the specified secure boot authority to a secureBootNamespaceRules.
// Note that this won't match if the specified authority directly signs things.
func withAuthority(subject, subjectKeyId []byte, publicKeyAlgorithm x509.PublicKeyAlgorithm) secureBootNamespaceOption {
return func(ns *secureBootNamespaceRules) {
ns.authorities = append(ns.authorities, &secureBootAuthorityIdentity{
Expand All @@ -55,13 +65,42 @@ func withAuthority(subject, subjectKeyId []byte, publicKeyAlgorithm x509.PublicK
}
}

// withSelfSignedSignerOnlyForTesting adds the specified secure boot authority to a
// secureBootNamespaceRules, only during testing. This also supports the case where the
// specified authority directly signs things. This is used to ensure that binaries signed
// by a production CA that are re-signed during testing by a testing-only CA are still
// detected correctly, in the same way that the production binary would be.
func withSelfSignedSignerOnlyForTesting(subject, subjectKeyId []byte, publicKeyAlgorithm x509.PublicKeyAlgorithm, signatureAlgorithm x509.SignatureAlgorithm) secureBootNamespaceOption {
if !snapdenvTesting() {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

just to double check: these are relevant only on the policy generation side? also to manipulate this we need to influence a root-running service environment? also ultimately what can be booted with is controlled by secureboot?

maybe some of this consideration should go in a comment somewhere?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, this is only relevant when generating policies, and just required for image detection. You can still use an API to generate a policy for binaries signed by any key, it's just that some of the edge cases in the detection might not work and you could end up with the wrong digests.

return func(_ *secureBootNamespaceRules) {}
}
return func(ns *secureBootNamespaceRules) {
ns.authorities = append(ns.authorities, &secureBootAuthorityIdentity{
subject: subject,
subjectKeyId: subjectKeyId,
publicKeyAlgorithm: publicKeyAlgorithm,
issuer: subject,
authorityKeyId: subjectKeyId,
signatureAlgorithm: signatureAlgorithm})
}
}

// withImageRule adds the specified rule to a secureBootNamespaceRules.
func withImageRule(name string, match imagePredicate, create newImageLoadHandlerFn) secureBootNamespaceOption {
return func(ns *secureBootNamespaceRules) {
ns.rules = append(ns.rules, newImageRule(name, match, create))
}
}

// withImageRuleOnlyForTesting adds the specified rule to a secureBootNamespaceRules,
// only during testing.
func withImageRuleOnlyForTesting(name string, match imagePredicate, create newImageLoadHandlerFn) secureBootNamespaceOption {
if !snapdenvTesting() {
return func(_ *secureBootNamespaceRules) {}
}
return withImageRule(name, match, create)
}

type secureBootNamespaceOption func(*secureBootNamespaceRules)

// secureBootNamespaceRules is used to construct an imageLoadHandler from a
Expand All @@ -86,11 +125,17 @@ func newSecureBootNamespaceRules(name string, options ...secureBootNamespaceOpti

func (r *secureBootNamespaceRules) AddAuthorities(certs ...*x509.Certificate) {
for _, cert := range certs {
// Avoid adding duplicates. Note that this is only guaranteed to de-duplicate
// those certificates added via this API, as the built-in certificates only
// have a minimal set of fields populated and we don't try to handle that case.
found := false
for _, authority := range r.authorities {
if bytes.Equal(authority.subject, cert.RawSubject) &&
bytes.Equal(authority.subjectKeyId, cert.SubjectKeyId) &&
authority.publicKeyAlgorithm == cert.PublicKeyAlgorithm {
authority.publicKeyAlgorithm == cert.PublicKeyAlgorithm &&
bytes.Equal(authority.issuer, cert.RawIssuer) &&
bytes.Equal(authority.authorityKeyId, cert.AuthorityKeyId) &&
authority.signatureAlgorithm == cert.SignatureAlgorithm {
Comment on lines +136 to +138
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

for non snakeoil keys both sides here are going to be empty?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

One side will always have all fields. I did consider trying to accommodate this but it looked over complicated and it would only make a difference in an edge case - ie, a shim that embeds the Microsoft CA as a vendor certificate. In this case, it would be added twice, which isn't really a problem anyway.

found = true
break
}
Expand All @@ -100,6 +145,9 @@ func (r *secureBootNamespaceRules) AddAuthorities(certs ...*x509.Certificate) {
subject: cert.RawSubject,
subjectKeyId: cert.SubjectKeyId,
publicKeyAlgorithm: cert.PublicKeyAlgorithm,
issuer: cert.RawIssuer,
authorityKeyId: cert.AuthorityKeyId,
signatureAlgorithm: cert.SignatureAlgorithm,
})
}
}
Expand All @@ -118,7 +166,10 @@ func (r *secureBootNamespaceRules) NewImageLoadHandler(image peImageHandle) (ima
cert := &x509.Certificate{
RawSubject: authority.subject,
SubjectKeyId: authority.subjectKeyId,
PublicKeyAlgorithm: authority.publicKeyAlgorithm}
PublicKeyAlgorithm: authority.publicKeyAlgorithm,
RawIssuer: authority.issuer,
AuthorityKeyId: authority.authorityKeyId,
SignatureAlgorithm: authority.signatureAlgorithm}
for _, sig := range sigs {
if !sig.CertLikelyTrustAnchor(cert) {
continue
Expand Down
115 changes: 115 additions & 0 deletions efi/secureboot_namespace_rules_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -113,6 +113,65 @@ func (s *secureBootNamespaceRulesSuite) TestRulesMatch3(c *C) {
c.Check(handler, DeepEquals, newMockLoadHandler())
}

func (s *secureBootNamespaceRulesSuite) TestRulesMatch4(c *C) {
// This tests that the test only rule successfully matches a binary
// with a single signature created by the authority associated with
// the namespace, when in testing mode.
restore := MockSnapdenvTesting(true)
defer restore()

image := newMockImage().appendSignatures(efitest.ReadWinCertificateAuthenticodeDetached(c, shimUbuntuSig3)).newPeImageHandle()

cert := testutil.ParseCertificate(c, msUefiCACert)

rules := NewSecureBootNamespaceRules(
"test",
WithAuthority(cert.RawSubject, cert.SubjectKeyId, cert.PublicKeyAlgorithm),
WithImageRuleOnlyForTesting(
"rule1",
&mockImagePredicate{result: true},
func(i PeImageHandle) (ImageLoadHandler, error) {
c.Check(i, Equals, image)

return newMockLoadHandler(), nil
},
),
)
handler, err := rules.NewImageLoadHandler(image)
c.Check(err, IsNil)
c.Check(handler, DeepEquals, newMockLoadHandler())
}

func (s *secureBootNamespaceRulesSuite) TestRulesMatch5(c *C) {
// This tests that the rules successfully match a binary with a single
// signature signed directly by the test-only authority associated with the
// namespace, when in testing mode.
restore := MockSnapdenvTesting(true)
defer restore()

cert := testutil.ParseCertificate(c, snakeoilCert)

sig := efitest.ReadWinCertificateAuthenticodeDetached(c, shimUbuntuSig3)
image := newMockImage().withDigest(sig.DigestAlgorithm(), sig.Digest()).sign(c, testutil.ParsePKCS1PrivateKey(c, snakeoilKey), cert).newPeImageHandle()

rules := NewSecureBootNamespaceRules(
"test",
WithSelfSignedSignerOnlyForTesting(cert.RawSubject, cert.SubjectKeyId, cert.PublicKeyAlgorithm, cert.SignatureAlgorithm),
WithImageRule(
"rule1",
&mockImagePredicate{result: true},
func(i PeImageHandle) (ImageLoadHandler, error) {
c.Check(i, Equals, image)

return newMockLoadHandler(), nil
},
),
)
handler, err := rules.NewImageLoadHandler(image)
c.Check(err, IsNil)
c.Check(handler, DeepEquals, newMockLoadHandler())
}

func (s *secureBootNamespaceRulesSuite) TestRulesNoMatch1(c *C) {
// This tests that the rules don't match a binary without a signature
// created by the authority associated with the namespace.
Expand Down Expand Up @@ -186,6 +245,62 @@ func (s *secureBootNamespaceRulesSuite) TestRulesNoMatch3(c *C) {
c.Check(cond.testedImages, IsNil)
}

func (s *secureBootNamespaceRulesSuite) TestRulesNoMatch4(c *C) {
// This tests that testing only rules don't match a binary with a single
// signature created by the authority associated with the namespace.
restore := MockSnapdenvTesting(false)
defer restore()

image := newMockImage().appendSignatures(efitest.ReadWinCertificateAuthenticodeDetached(c, shimUbuntuSig3)).newPeImageHandle()

cert := testutil.ParseCertificate(c, msUefiCACert)

cond := &mockImagePredicate{result: true}
rules := NewSecureBootNamespaceRules(
"test",
WithAuthority(cert.RawSubject, cert.SubjectKeyId, cert.PublicKeyAlgorithm),
WithImageRuleOnlyForTesting(
"rule1",
cond,
func(i PeImageHandle) (ImageLoadHandler, error) {
return nil, errors.New("not reached")
},
),
)
_, err := rules.NewImageLoadHandler(image)
c.Check(err, Equals, ErrNoHandler)
c.Check(cond.testedImages, IsNil)
}

func (s *secureBootNamespaceRulesSuite) TestRulesNoMatch5(c *C) {
// This tests that the rules don't match a binary with a single signature
// signed directly by the test-only authority associated with the namespace,
// when not in testing mode.
restore := MockSnapdenvTesting(false)
defer restore()

cert := testutil.ParseCertificate(c, snakeoilCert)

sig := efitest.ReadWinCertificateAuthenticodeDetached(c, shimUbuntuSig3)
image := newMockImage().withDigest(sig.DigestAlgorithm(), sig.Digest()).sign(c, testutil.ParsePKCS1PrivateKey(c, snakeoilKey), cert).newPeImageHandle()

cond := &mockImagePredicate{result: true}
rules := NewSecureBootNamespaceRules(
"test",
WithSelfSignedSignerOnlyForTesting(cert.RawSubject, cert.SubjectKeyId, cert.PublicKeyAlgorithm, cert.SignatureAlgorithm),
WithImageRule(
"rule1",
cond,
func(i PeImageHandle) (ImageLoadHandler, error) {
return nil, errors.New("not reached")
},
),
)
_, err := rules.NewImageLoadHandler(image)
c.Check(err, Equals, ErrNoHandler)
c.Check(cond.testedImages, IsNil)
}

type mockLoadHandlerWithVendorAuthorities struct {
*mockLoadHandler
vendorCerts []*x509.Certificate
Expand Down
Loading
Loading