// -*- Mode: Go; indent-tabs-mode: t -*-

/*
 * Copyright (C) 2024 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 preinstall

import (
	"bytes"
	"context"
	"errors"
	"fmt"
	"io"

	efi "github.com/canonical/go-efilib"
	"github.com/canonical/go-tpm2"
	"github.com/canonical/tcglog-parser"
	secboot_efi "github.com/snapcore/secboot/efi"
	internal_efi "github.com/snapcore/secboot/internal/efi"
)

var (
	efiComputePeImageDigest = efi.ComputePeImageDigest
)

type bootManagerCodeResult struct {
	HasAbsolute bool
	SysprepApps []*LoadedImageInfo
}

// checkBootManagerCodeMeasurements performs some checks on the boot manager code PCR (4).
//
// The supplied context is used to attach an EFI variable backend to, for functions that read
// from EFI variables. The supplied env and log arguments provide other inputs to this function.
// The pcrAlg argument is the PCR bank that is chosen as the best one to use. The loadImages
// argument provides a way to supply the load images associated with the current boot, in the
// order in which they are loaded. These images are used to verify the digests of the
// EV_EFI_BOOT_SERVICES_APPLICATION events.
//
// This function ensures that the pre-OS environment is well formed. Either it contains a single
// EV_OMIT_BOOT_DEVICE_EVENT event or an optional EV_EFI_ACTION "Calling EFI Application from Boot
// Option" event if the EV_OMIT_BOOT_DEVICE_EVENT event is not present. If the EV_EFI_ACTION event
// is present, then the next expected event is the EV_SEPARATOR to signal the transition to OS-present.
// The function considers any EV_EFI_BOOT_SERVICES_APPLICATION events before this to be system
// preparation applications, and it will return information about these in the returned result. If
// the BootOptionSupport EFI variable indicates that sysprep apps are not supported but they are present,
// then an error is returned. If any pre-OS EV_EFI_BOOT_SERVICES_APPLICATION event is associated with
// Absolute, then this is indicated separately in the returned result.
//
// The function expects the next event after the EV_SEPARATOR to be a EV_EFI_BOOT_SERVICES_APPLICATION
// event, either the one associated with the IBL (initial boot loader) for the OS, or a component of
// Absolute. If it is Absolute, then this is indicated in the returned result. It then expects the next
// event to be the one associated with the IBL (based on the value of the BootCurrent EFI variable,
// and the corresponding EFI_LOAD_OPTION in the TCG log). If the event data is inconsistent with the
// EFI_LOAD_OPTION for BootCurrent, it returns an error. It verifies that the digest of the event matches
// the Authenticode digest of the first supplied image, and returns an error if it isn't.
//
// Once the IBL image digest is verified, then the digests of all other EV_EFI_BOOT_SERVICES_APPLICATION
// events in the log are checked, if enough images associated with the current boot are supplied via the
// loadImages argument. It isn't possible to determine whether these events are generated by the firmware
// via a call to LoadImage, or whether they are generated by an OS component using the EFI_TCG2_PROTOCOL.
// In any case, if any OS component loads the next component itself and measures a digest directly without
// using the LoadImage API, it depends on the presence of the EFI_TCG2_PROTOCOL interface with support for
// the PE_COFF_IMAGE flag. There's no direct way to test for this, so for this reason, this function requires
// that the EV_EFI_BOOT_SERVICES_APPLICATION digests associated with subsequent loader stages match the
// Authenticode digest of the images supplied via the loadImages argument. If they don't, then an error is
// returned.
func checkBootManagerCodeMeasurements(ctx context.Context, env internal_efi.HostEnvironment, log *tcglog.Log, pcrAlg tpm2.HashAlgorithmId, loadImages []secboot_efi.Image) (result *bootManagerCodeResult, err error) {
	varCtx := env.VarContext(ctx)

	// Obtain the boot option support
	opts, err := efi.ReadBootOptionSupportVariable(varCtx)
	switch {
	case errors.Is(err, efi.ErrVarNotExist):
		// We want RunChecks to not return the EFI variable access error in this case.
		return nil, errors.New("cannot obtain boot option support: variable doesn't exist")
	case err != nil:
		return nil, fmt.Errorf("cannot obtain boot option support: %w", err)
	}

	// Obtain the load option from the current boot so we can identify which load
	// event corresponds to the initial OS boot loader.
	bootOpt, err := readCurrentBootLoadOptionFromLog(varCtx, log)
	if err != nil {
		return nil, err
	}

	var (
		sysprepOrder []uint16
		sysprepOpts  []*efi.LoadOption
	)
	if opts&efi.BootOptionSupportSysPrep > 0 {
		sysprepOpts, sysprepOrder, err = readOrderedLoadOptionVariables(varCtx, efi.LoadOptionClassSysPrep)
		if err != nil && !errors.Is(err, efi.ErrVarNotExist) {
			return nil, fmt.Errorf("cannot read sysprep app load option variables: %w", err)
		}
	}

	result = new(bootManagerCodeResult)

	var (
		omitBootDeviceEventsSeen       = false // a EV_OMIT_BOOT_DEVICE_EVENTS event has been seen
		expectingTransitionToOSPresent = false // The next events in PCR4 are expected to be the transition to OS-present
		seenOSComponentLaunches        = 0     // The number of EV_EFI_BOOT_SERVICES_APPLICATION events associated with OS component launches we've seen
	)

	phaseTracker := newTcgLogPhaseTracker()
NextEvent:
	for _, ev := range log.Events {
		phase, err := phaseTracker.processEvent(ev)
		if err != nil {
			return nil, err
		}

		switch phase {
		case tcglogPhaseFirmwareLaunch, tcglogPhasePreOSThirdPartyDispatch:
			if ev.PCRIndex != internal_efi.BootManagerCodePCR {
				// Not PCR4
				continue NextEvent
			}

			// Make sure the event data is valid
			if err, isErr := ev.Data.(error); isErr {
				return nil, fmt.Errorf("invalid %v event data: %w", ev.EventType, err)
			}

			if expectingTransitionToOSPresent {
				// The next events in PCR4 should have taken us to OS-present
				return nil, fmt.Errorf("unexpected event type %v: expecting transition from pre-OS to OS-present event", ev.EventType)
			}

			switch ev.EventType {
			case tcglog.EventTypeOmitBootDeviceEvents:
				// The digest is the tagged hash of the event data, but we don't bother verifying
				// that because we just copy this event into the profile if it's present.
				if omitBootDeviceEventsSeen {
					return nil, errors.New("already seen a EV_OMIT_BOOT_DEVICE_EVENTS event")
				}
				omitBootDeviceEventsSeen = true
			case tcglog.EventTypeEFIAction:
				// ok, although 1.05 of the TCG PFP spec is a bit ambiguous here, section 8.2.4 says
				// the event associated with the first boot attempt, if it is measured, occurs before
				// the separator (as part of pre-OS). The actual PCR usage section 3.3.4.5 in this version
				// of the spec and older contradicts this and mentions a bunch of EV_ACTION events that
				// pertain to BIOS boot. On every device we've tested, this event occurs before the
				// separator and there are no BIOS boot related EV_ACTION events. 1.06 of the TCG PFP
				// spec tries to clean this up a bit, removing reference to the EV_ACTION events and
				// correcting the "Method for measurement" subsection of section 3.3.4.5 to match
				// section 8.2.4. We reject any EV_ACTION events in PCR4 here anyway.
				//
				// EV_EFI_ACTION event digests are the tagged hash of the event data, but we don't bother
				// verifying this because we just copy the events into the profile.
				if ev.Data == tcglog.EFICallingEFIApplicationEvent {
					// This is the signal from BDS that we're about to hand over to the OS.
					if phase == tcglogPhaseFirmwareLaunch {
						return nil, fmt.Errorf("unexpected EV_EFI_ACTION event %q (before secure boot config was measured)", ev.Data)
					}
					if omitBootDeviceEventsSeen {
						return nil, fmt.Errorf("unexpected EV_EFI_ACTION event %q (because of earlier EV_OMIT_BOOT_DEVICE_EVENTS event)", ev.Data)
					}

					// The next event we're expecting is the pre-OS to OS-present transition.
					//
					// TODO(chrisccoulson): The TCG PFP spec 1.06 r49 expects there to be a
					// EV_EFI_ACTION event immediately following this one with the string
					// "Booting to <Boot####> Option". Whilst the current profile generation code
					// will preserve what's currently in the log, there needs to be an API for boot
					// configuration code to specificy the actual boot option to ensure that we
					// predict the correct value. We currently fail support for PCR4 if this
					// unsupported EV_EFI_ACTION event is present next.
					expectingTransitionToOSPresent = true
				} else {
					// We're not expecting any other EV_EFI_ACTION event types, although see
					// the TODO above.
					return nil, fmt.Errorf("unexpected EV_EFI_ACTION event %q", ev.Data)
				}
			case tcglog.EventTypeEFIBootServicesApplication:
				// Assume all pre-OS application launches are SysPrep applications. There shouldn't
				// really be anything else here and there isn't really a reliable way to detect.
				// It might be possible to match the device path with the next variable in SysPrepOrder,
				// but these can be modified at runtime to not reflect what they were at boot time,
				// and SysPrep variables are not measured to the TCG log.
				//
				// As we don't do any prediction of sysprep applications (yet - never say never), we
				// don't verify that the measured Authenticode digest matches the binary at the end of the
				// device path, if it's reachable from the OS. Although this also suffers from a similar
				// variation of the issue described above - that path could have been updated between
				// booting and now.
				data := ev.Data.(*tcglog.EFIImageLoadEvent) // this is safe as we already checked that the data is valid.

				if phase == tcglogPhaseFirmwareLaunch {
					// Application launches before the secure boot configuration has been measured is a bug.
					return nil, fmt.Errorf("encountered pre-OS EV_EFI_BOOT_SERVICES_APPLICATION event for %v before secure boot configuration has been measured", data.DevicePath)
				}

				switch isAbsolute, err := internal_efi.IsAbsoluteAgentLaunch(ev); {
				case err != nil:
					return nil, fmt.Errorf("cannot determine if pre-OS EV_EFI_BOOT_SERVICES_APPLICATION event for %v is associated with Absolute: %w", data.DevicePath, err)
				case isAbsolute && result.HasAbsolute:
					return nil, errors.New("encountered more than one EV_EFI_BOOT_SERVICES_APPLICATION event associated with Absolute")
				case isAbsolute:
					result.HasAbsolute = true
				case len(sysprepOpts) == 0:
					// We are not expecting any sysprep applications.
					return nil, fmt.Errorf("encountered pre-OS EV_EFI_BOOT_SERVICES_APPLICATION event for %v when no sysprep applications are expected", data.DevicePath)
				default:
					for {
						if len(sysprepOpts) == 0 {
							return nil, fmt.Errorf("encountered unexpected pre-OS EV_EFI_BOOT_SERVICES_APPLICATION event for %v", data.DevicePath)
						}

						opt := sysprepOpts[0]
						n := sysprepOrder[0]
						sysprepOpts = sysprepOpts[1:]
						sysprepOrder = sysprepOrder[1:]

						ok, err := isLaunchedFromLoadOption(ev, opt)
						if err != nil {
							return nil, fmt.Errorf("cannot determine if pre-OS EV_EFI_BOOT_SERVICES_APPLICATION event for %v is associated with the next sysprep load option: %w", data.DevicePath, err)
						}
						if ok {
							result.SysprepApps = append(result.SysprepApps, &LoadedImageInfo{
								Description:    opt.Description,
								LoadOptionName: efi.FormatLoadOptionVariableName(efi.LoadOptionClassSysPrep, n),
								DevicePath:     data.DevicePath,
								DigestAlg:      pcrAlg,
								Digest:         ev.Digests[pcrAlg],
							})
							break
						}
					}
				}
			default:
				// We're not expecting any other event types during the pre-OS phase.
				return nil, fmt.Errorf("unexpected pre-OS event type %v", ev.EventType)
			}
		case tcglogPhaseOSPresent:
			if ev.PCRIndex != internal_efi.BootManagerCodePCR {
				// Not PCR4
				continue NextEvent
			}

			if ev.EventType != tcglog.EventTypeEFIBootServicesApplication {
				// Only care about EV_EFI_BOOT_SERVICES_APPLICATION events for checking
				if seenOSComponentLaunches == 0 {
					// The only events we're expecting in OS-present for now is EV_EFI_BOOT_SERVICES_APPLICATION.
					return nil, fmt.Errorf("unexpected OS-present log event type %v (expected EV_EFI_BOOT_SERVICES_APPLICATION)", ev.EventType)
				}
				// Once the IBL has launched, other event types are acceptable as long as the policy generation
				// code associated with the component in the secboot efi package emits them.
				continue NextEvent
			}

			switch seenOSComponentLaunches {
			case 0:
				// Check if this launch is associated with the EFI_LOAD_OPTION associated with
				// the current boot. This will fail if the data associated with the event is invalid.
				isBootOptLaunch, err := isLaunchedFromLoadOption(ev, bootOpt)
				if err != nil {
					return nil, fmt.Errorf("cannot determine if OS-present EV_EFI_BOOT_SERVICES_APPLICATION event is associated with the current boot load option: %w", err)
				}
				if isBootOptLaunch {
					// We have the EV_EFI_BOOT_SERVICES_APPLICATION event associated with the IBL launch.
					seenOSComponentLaunches += 1
				} else {
					// We have an EV_EFI_BOOT_SERVICES_APPLICATION that didn't come from the load option
					// associated with the current boot.
					// Test to see if it's part of Absolute. If it is, that's fine - we copy this into
					// the profile, so we don't need to do any other verification of it and we don't have
					// anything to verify the Authenticode digest against anyway. We have a device path,
					// but not one that we're able to read back from.
					//
					// If this isn't Absolute, we bail with an error. We don't support anything else being
					// loaded here, and ideally Absolute will be turned off as well.

					data := ev.Data.(*tcglog.EFIImageLoadEvent) // this is safe, else the earlier isLaunchedFromLoadOption would have returned an error

					switch isAbsolute, err := internal_efi.IsAbsoluteAgentLaunch(ev); {
					case err != nil:
						return nil, fmt.Errorf("cannot determine if OS-present EV_EFI_BOOT_SERVICES_APPLICATION event for %v is associated with Absolute: %w", data.DevicePath, err)
					case !isAbsolute:
						return nil, fmt.Errorf("OS-present EV_EFI_BOOT_SERVICES_APPLICATION event for %v is not associated with the current boot load option and is not Absolute", data.DevicePath)
					case result.HasAbsolute:
						return nil, errors.New("encountered more than one EV_EFI_BOOT_SERVICES_APPLICATION event associated with Absolute")
					default:
						result.HasAbsolute = true
					}
					continue NextEvent // We want to start a new iteration, else we'll consume one of the loadImages below.
				}
			default:
				seenOSComponentLaunches += 1
			}

			if len(loadImages) == 0 {
				if data, ok := ev.Data.(*tcglog.EFIImageLoadEvent); ok && len(data.DevicePath) > 0 {
					return nil, fmt.Errorf("cannot verify correctness of EV_EFI_BOOT_SERVICES_APPLICATION event digest for %v: not enough images supplied", data.DevicePath)
				}
				return nil, errors.New("cannot verify correctness of EV_EFI_BOOT_SERVICES_APPLICATION event digest: not enough images supplied")
			}

			image := loadImages[0]
			loadImages = loadImages[1:]

			err := func() error {
				r, err := image.Open()
				if err != nil {
					return fmt.Errorf("cannot open image %s: %w", image, err)
				}
				defer r.Close()

				digest, err := efiComputePeImageDigest(pcrAlg.GetHash(), r, r.Size())
				if err != nil {
					return fmt.Errorf("cannot compute Authenticode digest of OS-present application %s: %w", image, err)
				}
				if bytes.Equal(digest, ev.Digests[pcrAlg]) {
					// The PE digest of the application matches what's in the log, so we're all good.
					return nil
				}

				// Digest in log does not match PE image digest. Compute flat-file digest and compare against that
				// for diagnostic purposes.
				r2 := io.NewSectionReader(r, 0, r.Size())
				h := pcrAlg.NewHash()
				if _, err := io.Copy(h, r2); err != nil {
					return fmt.Errorf("cannot compute flat file digest of OS-present application %s: %w", image, err)
				}
				if !bytes.Equal(h.Sum(nil), ev.Digests[pcrAlg]) {
					// Still no digest match
					return fmt.Errorf("log contains unexpected EV_EFI_BOOT_SERVICES_APPLICATION digest for OS-present application %s (calculated PE digest: %#x, log value: %#x) - were the correct boot images supplied?",
						image, digest, ev.Digests[pcrAlg])
				}
				// We have a digest match, so something loaded this component outside of the LoadImage API and used the
				// legacy EFI_TCG_PROTOCOL API to measure it, or used the proper EFI_TCG2_PROTOCOL API without the
				// PE_COFF_IMAGE flag. In any case, WithBootManagerCodeProfile() will mis-predict the loading of this.
				return fmt.Errorf("log contains unexpected EV_EFI_BOOT_SERVICES_APPLICATION digest for OS-present application %s: log digest matches flat file digest (%#x) which suggests an image loaded outside of the LoadImage API and firmware lacking support for the EFI_TCG2_PROTOCOL and/or the PE_COFF_IMAGE flag", image, h.Sum(nil))
			}()
			if err != nil {
				return nil, err
			}
		}
	}
	return result, nil
}
