-
Notifications
You must be signed in to change notification settings - Fork 22
efi: add CheckImageSignatureIsValidForHost function #532
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
base: master
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||
|---|---|---|---|---|---|---|
| @@ -0,0 +1,296 @@ | ||||||
| // -*- Mode: Go; indent-tabs-mode: t -*- | ||||||
|
|
||||||
| /* | ||||||
| * Copyright (C) 2026 Canonical Ltd | ||||||
| * | ||||||
| * This program is free software: you can redistribute it and/or modify | ||||||
| * it under the terms of the GNU General Public License version 3 as | ||||||
| * published by the Free Software Foundation. | ||||||
| * | ||||||
| * This program is distributed in the hope that it will be useful, | ||||||
| * but WITHOUT ANY WARRANTY; without even the implied warranty of | ||||||
| * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the | ||||||
| * GNU General Public License for more details. | ||||||
| * | ||||||
| * You should have received a copy of the GNU General Public License | ||||||
| * along with this program. If not, see <http://www.gnu.org/licenses/>. | ||||||
| * | ||||||
| */ | ||||||
|
|
||||||
| package efi | ||||||
|
|
||||||
| import ( | ||||||
| "bytes" | ||||||
| "context" | ||||||
| "crypto" | ||||||
| "crypto/x509" | ||||||
| "errors" | ||||||
|
|
||||||
| efi "github.com/canonical/go-efilib" | ||||||
| "golang.org/x/xerrors" | ||||||
| ) | ||||||
|
|
||||||
| var mockedCheckImageSignatureIsValidForHost func(ctx context.Context, image Image) error | ||||||
|
|
||||||
| // CheckImageSignatureIsValidForHost checks whether the supplied image has at | ||||||
| // least one Authenticode signature that is authorized by the host's authorized | ||||||
| // signature database (the UEFI "db" variable). | ||||||
| // | ||||||
| // The image must have at least one Authenticode signature. It is not possible | ||||||
| // to authorize an unsigned image based solely on its digest being present in db. | ||||||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The firmware will authorize an image in this way regardless of the presence of a signature. |
||||||
| // | ||||||
| // The image is authorized if: | ||||||
| // - At least one of its Authenticode signatures chains to an X.509 certificate | ||||||
| // authority that is enrolled in db, OR | ||||||
|
Comment on lines
+35
to
+44
|
||||||
| // - The PE image digest matches a digest entry in db, where the PE image digest | ||||||
| // is computed for the hash algorithm implied by the EFI_SIGNATURE_LIST type | ||||||
| // (e.g., CertSHA256Guid implies SHA256, CertSHA384Guid implies SHA384). | ||||||
| // | ||||||
| // For digest-based authorization, the function computes the PE image digest for | ||||||
| // each digest algorithm present in db and checks for a match against the | ||||||
| // corresponding signature list entries. This allows an image to be verified | ||||||
| // against digest entries regardless of the hash algorithm used by its | ||||||
| // Authenticode signature. | ||||||
| // | ||||||
| // This is intended to be used during boot asset updates to verify that a new | ||||||
| // image will actually be loadable, when secure boot is enforced, by the host's | ||||||
| // firmware before it is installed. | ||||||
| // | ||||||
|
zyga marked this conversation as resolved.
|
||||||
| // For example, a shim binary signed only by a newer Microsoft UEFI CA will not | ||||||
| // be loadable on older hardware whose db only contains the older CA. Similarly, | ||||||
| // an image whose digest is not in db (if digest-based authorization is used) will | ||||||
| // not be loadable. An unsigned image cannot be loadable even if its digest is | ||||||
| // enrolled in db. | ||||||
| // | ||||||
| // The context must provide access to the EFI variable backend via go-efilib's | ||||||
| // context mechanism. In general, pass the result of | ||||||
| // [HostEnvironment.VarContext] or [efi.DefaultVarContext]. | ||||||
| // | ||||||
| // Possible error conditions: | ||||||
| // - The image cannot be opened or is not a valid PE binary. | ||||||
| // - The image has no secure boot signatures. | ||||||
| // - The host's db variable cannot be read (eg, if EFI variables are unavailable). | ||||||
| // - The host's dbx variable cannot be read (eg, if EFI variables are unavailable). | ||||||
| // - The image is revoked by a certificate or digest entry in the host's dbx. | ||||||
| // - No signature on the image is authorized by the host's db. | ||||||
| func CheckImageSignatureIsValidForHost(ctx context.Context, image Image) error { | ||||||
|
Comment on lines
+73
to
+76
|
||||||
| if mockedCheckImageSignatureIsValidForHost != nil { | ||||||
| return mockedCheckImageSignatureIsValidForHost(ctx, image) | ||||||
| } | ||||||
|
|
||||||
| // Extract signatures from the image, and check for the presence of at | ||||||
| // least one signature before doing any further work, to give a more | ||||||
| // specific error if the image is not signed at all. | ||||||
| pei, err := openPeImage(image) | ||||||
| if err != nil { | ||||||
| return xerrors.Errorf("cannot open image: %w", err) | ||||||
| } | ||||||
|
|
||||||
| defer pei.Close() | ||||||
|
|
||||||
| sigs, err := pei.SecureBootSignatures() | ||||||
| if err != nil { | ||||||
| return xerrors.Errorf("cannot obtain secure boot signatures for image: %w", err) | ||||||
| } | ||||||
|
|
||||||
| if len(sigs) == 0 { | ||||||
| return errors.New("image has no secure boot signatures") | ||||||
| } | ||||||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. It's odd that an image is tested against enrolled digests only if it is signed. An unsigned image would still authenticate ok if it matched an enrolled digest, even if there are no signatures. |
||||||
|
|
||||||
| // Check for forbidden signatures in DBX first, to give a more specific | ||||||
| // error if the image is actually signed by a trusted CA but is revoked | ||||||
| // by DBX. | ||||||
| if err := checkDbxRevocation(ctx, pei, sigs); err != nil { | ||||||
| return err | ||||||
| } | ||||||
|
|
||||||
| // Check for a trusted signature in DB. | ||||||
| return checkDbAuthorization(ctx, pei, sigs) | ||||||
| } | ||||||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Should this function return true if secure boot is disabled?
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Probably yes |
||||||
|
|
||||||
| func checkDbxRevocation(ctx context.Context, pei peImageHandle, sigs []*efi.WinCertificateAuthenticode) error { | ||||||
| dbx, err := efi.ReadSignatureDatabaseVariable(ctx, Dbx) | ||||||
| if err != nil { | ||||||
| if !errors.Is(err, efi.ErrVarNotExist) { | ||||||
| return xerrors.Errorf("cannot read forbidden signature database: %w", err) | ||||||
| } | ||||||
|
|
||||||
| // If DBX doesn't exist, then there are no forbidden signatures, so we can | ||||||
| // just treat it as empty and allow the image to be authorized by DB. | ||||||
| return nil | ||||||
| } | ||||||
|
zyga marked this conversation as resolved.
|
||||||
|
|
||||||
| digests := newImageDigestCache(pei) | ||||||
| revokedByDigest, err := imageDigestIsForbiddenByDbx(digests, dbx) | ||||||
| if err != nil { | ||||||
| return err | ||||||
| } | ||||||
|
|
||||||
| if revokedByDigest { | ||||||
| return errors.New("secure boot signature is forbidden by the current host's signature databases") | ||||||
| } | ||||||
|
|
||||||
| // Per UEFI secure boot semantics, any match in DBX is sufficient to | ||||||
| // revoke an image, even if it has additional valid signatures. | ||||||
| for _, sig := range sigs { | ||||||
| if certIsForbiddenByDbx(sig, dbx) { | ||||||
| return errors.New("secure boot signature is forbidden by the current host's signature databases") | ||||||
| } | ||||||
|
zyga marked this conversation as resolved.
|
||||||
| } | ||||||
|
|
||||||
| return nil | ||||||
| } | ||||||
|
|
||||||
| var errNoTrustedSignature = errors.New("cannot find any secure boot signature that is trusted by the current host's authorized signature database") | ||||||
|
Comment on lines
+135
to
+144
|
||||||
|
|
||||||
| func checkDbAuthorization(ctx context.Context, pei peImageHandle, imageSigs []*efi.WinCertificateAuthenticode) error { | ||||||
| db, err := efi.ReadSignatureDatabaseVariable(ctx, Db) | ||||||
| if err != nil { | ||||||
| if !errors.Is(err, efi.ErrVarNotExist) { | ||||||
| return xerrors.Errorf("cannot read authorized signature database: %w", err) | ||||||
| } | ||||||
| // If DB doesn't exist, then there are no authorized signatures, so the image cannot be | ||||||
| // authorized. | ||||||
| return errNoTrustedSignature | ||||||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I don't know whether I would handle the |
||||||
| } | ||||||
|
|
||||||
| digests := newImageDigestCache(pei) | ||||||
|
|
||||||
| for _, sigList := range db { | ||||||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Iterating through db then through signatures seems wrong. I think the spec specifies the way the loop is and it is the opposite. We should check. While the result will be the same, the certificate that will used to validate might be different, and if this function evolves for example to return which certificate would be measured... that will be wrong.
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I have been looking through the multiple specifications. And I actually do not find anything about it. I really thought measurements in PCR 7 had to be predictable. But I am not so sure anymore. Anyway, I do think we should have the same kind of loop as in secureBootPolicyMixin.DetermineAuthority This is also what edk2 does. And it seems that edk2 also verifies first signatures, then the digest. |
||||||
| // Check for X.509 certificate-based authorization | ||||||
| if sigList.Type == efi.CertX509Guid { | ||||||
| for _, sigEntry := range sigList.Signatures { | ||||||
| cert, err := x509.ParseCertificate(sigEntry.Data) | ||||||
| if err != nil { | ||||||
| continue | ||||||
| } | ||||||
|
|
||||||
| for _, imageSig := range imageSigs { | ||||||
| if !imageSig.CertWithIDLikelyTrustAnchor(efi.NewX509CertIDFromCertificate(cert)) { | ||||||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Would be neater here? |
||||||
| continue | ||||||
| } | ||||||
|
|
||||||
| // If the signature chains to a trusted certificate, then the image is authorized. | ||||||
| return nil | ||||||
| } | ||||||
| } | ||||||
| } else { | ||||||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Something I would like to check the specs. Is something signed never checked for its digest?
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Images are tested against enrolled digests whether they are signed or not. |
||||||
| // Skip unrecognized signature list types since we cannot use them for verification | ||||||
|
||||||
| // Skip unrecognized signature list types since we cannot use them for verification | |
| // Skip signature lists with no digest match (or unrecognized type) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
There are some cert types that this doesn't handle - EFI_CERT_X509_SHA{256,384,512}_GUID. These contain TBS digests of revoked certificates and a revocation time, and a match of any certificate in the signing chain marks a signature as forbidden if the image signature has no timestamp signature, or if there is a timestamp signature but the timestamp is before the revocation time (on systems that support timestamp revocation).
Adding this adds a lot of complexity but without it, the result could indicate an image. I'm not sure whether checking revocations is worth the complexity, given the case we most care about is upgrades where we're not expected to upgrade from an image that is not revoked to one that is.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Does this make the earlier call to efiSignatureListTypeToDigestAlg in checkDbAuthorizations redundant?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This function doesn't take a signature list, so I wonder if there could be a better name for it.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This isn't used anywhere