// -*- 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 relatively well formed. Before the secure
// boot configuration is measured, it may contain an optional EV_OMIT_BOOT_DEVICE_EVENT event Whilst
// there should only be one of these, this isn't enforced and generated profiles will still be
// correct. After the secure boot configuration is measured, it may contain an optional
// EV_OMIT_BOOT_DEVICE_EVENT event, EV_EFI_BOOT_SERVICES_APPLICATION events and an optional
// EV_EFI_ACTION "Calling EFI Application from Boot Option" event. Whilst the EV_EFI_ACTION event
// should only be present if there is no EV_OMIT_BOOT_DEVICE_EVENT event, this isn't enforced and
// generated profiles will still be correct. If the EV_EFI_ACTION event is present, the next
// event is expected to be an EV_SEPARATOR to signal the transition to OS-present. This function
// considers any pre-OS EV_EFI_BOOT_SERVICES_APPLICATION events to be system preparation applications
// unless they are loaded from SPI flash, and it will return information about these in the returned
// result. 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. 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)

	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 (
		expectingTransitionToOSPresent = false // The next events in PCR4 are expected to be the transition to OS-present
		seenOSLaunch                   = false // Whether we've seen the EV_EFI_BOOT_SERVICES_APPLICATION event for the initial OS loader
	)

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

		switch phase {
		case tcglogPhaseFirmwareLaunch:
			if ev.PCRIndex != internal_efi.BootManagerCodePCR {
				// Not PCR4
				continue NextEvent
			}
			switch ev.EventType {
			case tcglog.EventTypeOmitBootDeviceEvents:
				// ok
			default:
				// We're not expecting any other event types during the pre-OS phase.
				// TODO: Permit vendor types here, with changes to efi/fw_load_handler.go
				return nil, fmt.Errorf("unexpected pre-OS event type %v", ev.EventType)
			}
		case tcglogPhasePreOSThirdPartyDispatch:
			if ev.PCRIndex != internal_efi.BootManagerCodePCR {
				// Not PCR4
				continue NextEvent
			}

			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:
				// ok
			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.
					// 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, ok := ev.Data.(*tcglog.EFIImageLoadEvent)
				if !ok {
					// The data resulting from decode errors are guaranteed to implement the error interface
					return nil, fmt.Errorf("invalid %v event data: %w", ev.EventType, ev.Data.(error))
				}

				switch isFwVol, err := internal_efi.IsLaunchedFromFirmwareVolume(ev); {
				case err != nil:
					return nil, fmt.Errorf("cannot determine if pre-OS EV_EFI_BOOT_SERVICES_APPLICATION event for %v is launched from firmware volume: %w", data.DevicePath, err)
				case isFwVol:
					// Consider anything loaded from SPI flash to not be a sysprep application. Check if it is Absolute.
					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 = true
					}
				case len(sysprepOpts) == 0:
					// Not a launch from SPI flash and 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:
					// This is a sysprep application.
					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.
				// TODO: Permit vendor types here, with changes to efi/fw_load_handler.go
				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 !seenOSLaunch {
					// 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
			}

			if !seenOSLaunch {
				// If we haven't seen the EV_EFI_BOOT_SERVICES_APPLICATION event for the OS initial
				// loader yet, check whether this event is associated with Absolute and permit it if
				// it is. This handles the case where some firmware implementations measure this as
				// part of OS-present. As this event is just copied into the profile, we don't need
				// to do any other verification of this event.
				data, ok := ev.Data.(*tcglog.EFIImageLoadEvent)
				if !ok {
					// The data resulting from decode errors are guaranteed to implement the error interface
					return nil, fmt.Errorf("invalid %v event data: %w", ev.EventType, ev.Data.(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:
					result.HasAbsolute = true
					continue NextEvent // We want to start a new iteration, else we'll consume one of the loadImages below.
				default:
					seenOSLaunch = true
				}
			}

			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)",
						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
}
