From bd1599957cc74324eaf53dd3b8983fb88d609cd0 Mon Sep 17 00:00:00 2001 From: Maksim An Date: Wed, 4 Mar 2026 11:16:08 -0800 Subject: [PATCH] Plumb hash_envelope_reference_info.cose through confidential UVM pipeline Add support for a new COSE_Sign1 signed document (hash_envelope_reference_info.cose) alongside the existing reference_info.cose. The file is read from the boot files directory, base64-encoded, sent over the wire via LCOWConfidentialOptions, and written into each container's security context directory. A single platform-agnostic annotation (io.microsoft.virtualmachine.uvm-hash-envelope-reference-info-file) is used for both LCOW and WCOW, placed in a new "Confidential UVM annotations" section. Changes: - Add UVMHashEnvelopeReferenceInfoFile annotation constant - Add EncodedUVMHashEnvelopeReference to wire protocol - Add WithUVMHashEnvelopeReferenceInfo confidential UVM option - Plumb new reference info to linux GCS and windows gcs-sidecar - Write hash-envelope-reference-info-base64 to security context dir Signed-off-by: Maksim An --- internal/gcs-sidecar/handlers.go | 3 +- internal/gcs-sidecar/host.go | 1 + internal/guest/runtime/hcsv2/uvm.go | 4 ++- internal/oci/uvm.go | 2 ++ internal/protocol/guestresource/resources.go | 7 +++-- internal/uvm/create.go | 13 ++++---- internal/uvm/create_lcow.go | 9 ++++-- internal/uvm/create_wcow.go | 4 +++ internal/uvm/security_policy.go | 22 ++++++++++++++ internal/uvm/start.go | 5 ++- pkg/annotations/annotations.go | 6 ++++ pkg/securitypolicy/securitypolicy.go | 9 +++--- pkg/securitypolicy/securitypolicy_options.go | 32 ++++++++++++-------- test/gcs/main_test.go | 2 +- 14 files changed, 88 insertions(+), 31 deletions(-) diff --git a/internal/gcs-sidecar/handlers.go b/internal/gcs-sidecar/handlers.go index 2eee764569..6a98268c50 100644 --- a/internal/gcs-sidecar/handlers.go +++ b/internal/gcs-sidecar/handlers.go @@ -560,7 +560,8 @@ func (b *Bridge) modifySettings(req *request) (err error) { err := b.hostState.securityOptions.SetConfidentialOptions(ctx, securityPolicyRequest.EnforcerType, securityPolicyRequest.EncodedSecurityPolicy, - securityPolicyRequest.EncodedUVMReference) + securityPolicyRequest.EncodedUVMReference, + securityPolicyRequest.EncodedUVMHashEnvelopeReference) if err != nil { return errors.Wrap(err, "Failed to set Confidentia UVM Options") } diff --git a/internal/gcs-sidecar/host.go b/internal/gcs-sidecar/host.go index 2c73f34ff3..ebe4f5687e 100644 --- a/internal/gcs-sidecar/host.go +++ b/internal/gcs-sidecar/host.go @@ -52,6 +52,7 @@ func NewHost(initialEnforcer securitypolicy.SecurityPolicyEnforcer, logWriter io initialEnforcer, false, "", + "", logWriter, ) return &Host{ diff --git a/internal/guest/runtime/hcsv2/uvm.go b/internal/guest/runtime/hcsv2/uvm.go index 6637af0a44..7160ec43d2 100644 --- a/internal/guest/runtime/hcsv2/uvm.go +++ b/internal/guest/runtime/hcsv2/uvm.go @@ -97,6 +97,7 @@ func NewHost(rtime runtime.Runtime, vsock transport.Transport, initialEnforcer s initialEnforcer, false, "", + "", logWriter, ) return &Host{ @@ -657,7 +658,8 @@ func (h *Host) modifyHostSettings(ctx context.Context, containerID string, req * return h.securityOptions.SetConfidentialOptions(ctx, r.EnforcerType, r.EncodedSecurityPolicy, - r.EncodedUVMReference) + r.EncodedUVMReference, + r.EncodedUVMHashEnvelopeReference) case guestresource.ResourceTypePolicyFragment: r, ok := req.Settings.(*guestresource.SecurityPolicyFragment) if !ok { diff --git a/internal/oci/uvm.go b/internal/oci/uvm.go index 9b139ee959..596ca1ecb2 100644 --- a/internal/oci/uvm.go +++ b/internal/oci/uvm.go @@ -249,6 +249,7 @@ func handleWCOWSecurityPolicy(ctx context.Context, a map[string]string, wopts *u wopts.DisableSecureBoot = ParseAnnotationsBool(ctx, a, annotations.WCOWDisableSecureBoot, false) wopts.GuestStateFilePath = ParseAnnotationsString(a, annotations.WCOWGuestStateFile, uvm.GetDefaultConfidentialVMGSPath()) wopts.UVMReferenceInfoFile = ParseAnnotationsString(a, annotations.WCOWReferenceInfoFile, uvm.GetDefaultReferenceInfoFilePath()) + wopts.UVMHashEnvelopeReferenceInfoFile = ParseAnnotationsString(a, annotations.UVMHashEnvelopeReferenceInfoFile, uvm.GetDefaultHashEnvelopeReferenceInfoFilePath()) wopts.IsolationType = "SecureNestedPaging" if noSecurityHardware := ParseAnnotationsBool(ctx, a, annotations.NoSecurityHardware, false); noSecurityHardware { wopts.IsolationType = "GuestStateOnly" @@ -376,6 +377,7 @@ func SpecToUVMCreateOpts(ctx context.Context, s *specs.Spec, id, owner string) ( lopts.SecurityPolicy = ParseAnnotationsString(s.Annotations, annotations.LCOWSecurityPolicy, lopts.SecurityPolicy) lopts.SecurityPolicyEnforcer = ParseAnnotationsString(s.Annotations, annotations.LCOWSecurityPolicyEnforcer, lopts.SecurityPolicyEnforcer) lopts.UVMReferenceInfoFile = ParseAnnotationsString(s.Annotations, annotations.LCOWReferenceInfoFile, lopts.UVMReferenceInfoFile) + lopts.UVMHashEnvelopeReferenceInfoFile = ParseAnnotationsString(s.Annotations, annotations.UVMHashEnvelopeReferenceInfoFile, lopts.UVMHashEnvelopeReferenceInfoFile) lopts.KernelBootOptions = ParseAnnotationsString(s.Annotations, annotations.KernelBootOptions, lopts.KernelBootOptions) lopts.DisableTimeSyncService = ParseAnnotationsBool(ctx, s.Annotations, annotations.DisableLCOWTimeSyncService, lopts.DisableTimeSyncService) lopts.WritableOverlayDirs = ParseAnnotationsBool(ctx, s.Annotations, iannotations.WritableOverlayDirs, lopts.WritableOverlayDirs) diff --git a/internal/protocol/guestresource/resources.go b/internal/protocol/guestresource/resources.go index 8a58949281..7d5988d930 100644 --- a/internal/protocol/guestresource/resources.go +++ b/internal/protocol/guestresource/resources.go @@ -232,9 +232,10 @@ type SignalProcessOptionsWCOW struct { // ConfidentialOptions is used to set various confidential container specific // options. type ConfidentialOptions struct { - EnforcerType string `json:"EnforcerType,omitempty"` - EncodedSecurityPolicy string `json:"EncodedSecurityPolicy,omitempty"` - EncodedUVMReference string `json:"EncodedUVMReference,omitempty"` + EnforcerType string `json:"EnforcerType,omitempty"` + EncodedSecurityPolicy string `json:"EncodedSecurityPolicy,omitempty"` + EncodedUVMReference string `json:"EncodedUVMReference,omitempty"` + EncodedUVMHashEnvelopeReference string `json:"EncodedUVMHashEnvelopeReference,omitempty"` } type SecurityPolicyFragment struct { diff --git a/internal/uvm/create.go b/internal/uvm/create.go index d4965f6740..37dfb3c951 100644 --- a/internal/uvm/create.go +++ b/internal/uvm/create.go @@ -132,12 +132,13 @@ type Options struct { } type ConfidentialCommonOptions struct { - GuestStateFilePath string // The vmgs file path to load - SecurityPolicy string // Optional security policy - SecurityPolicyEnabled bool // Set when there is a security policy to apply on actual SNP hardware, use this rathen than checking the string length - SecurityPolicyEnforcer string // Set which security policy enforcer to use (open door or rego). This allows for better fallback mechanic. - UVMReferenceInfoFile string // Path to the file that contains the signed UVM measurements - BundleDirectory string // This allows paths to be constructed relative to a per-VM bundle directory. + GuestStateFilePath string // The vmgs file path to load + SecurityPolicy string // Optional security policy + SecurityPolicyEnabled bool // Set when there is a security policy to apply on actual SNP hardware, use this rathen than checking the string length + SecurityPolicyEnforcer string // Set which security policy enforcer to use (open door or rego). This allows for better fallback mechanic. + UVMReferenceInfoFile string // Path to the file that contains the signed UVM measurements + UVMHashEnvelopeReferenceInfoFile string // Path to the file that contains the hash envelope signed UVM measurements + BundleDirectory string // This allows paths to be constructed relative to a per-VM bundle directory. } func verifyWCOWBootFiles(bootFiles *WCOWBootFiles) error { diff --git a/internal/uvm/create_lcow.go b/internal/uvm/create_lcow.go index 1700b3e3ac..de569ef669 100644 --- a/internal/uvm/create_lcow.go +++ b/internal/uvm/create_lcow.go @@ -88,6 +88,10 @@ const ( // reference UVM info, which can be made available to workload containers // and can be used for validation purposes. UVMReferenceInfoFile = "reference_info.cose" + // UVMHashEnvelopeReferenceInfoFile is the default file name for a COSE_Sign1 + // hash envelope reference UVM info, which can be made available to workload + // containers and can be used for validation purposes. + UVMHashEnvelopeReferenceInfoFile = "hash_envelope_reference_info.cose" ) type ConfidentialLCOWOptions struct { @@ -162,8 +166,9 @@ func NewDefaultOptionsLCOW(id, owner string) *OptionsLCOW { DisableTimeSyncService: false, ConfidentialLCOWOptions: &ConfidentialLCOWOptions{ ConfidentialCommonOptions: &ConfidentialCommonOptions{ - SecurityPolicyEnabled: false, - UVMReferenceInfoFile: UVMReferenceInfoFile, + SecurityPolicyEnabled: false, + UVMReferenceInfoFile: UVMReferenceInfoFile, + UVMHashEnvelopeReferenceInfoFile: UVMHashEnvelopeReferenceInfoFile, }, }, } diff --git a/internal/uvm/create_wcow.go b/internal/uvm/create_wcow.go index d417b04eb7..692b0c63fb 100644 --- a/internal/uvm/create_wcow.go +++ b/internal/uvm/create_wcow.go @@ -95,6 +95,10 @@ func GetDefaultReferenceInfoFilePath() string { return filepath.Join(defaultConfidentialWCOWOSBootFilesPath(), "reference_info.cose") } +func GetDefaultHashEnvelopeReferenceInfoFilePath() string { + return filepath.Join(defaultConfidentialWCOWOSBootFilesPath(), "hash_envelope_reference_info.cose") +} + // NewDefaultOptionsWCOW creates the default options for a bootable version of // WCOW. The caller `MUST` set the `BootFiles` on the returned value. // diff --git a/internal/uvm/security_policy.go b/internal/uvm/security_policy.go index 3fa47e87b1..5778919b9d 100644 --- a/internal/uvm/security_policy.go +++ b/internal/uvm/security_policy.go @@ -67,6 +67,28 @@ func WithUVMReferenceInfo(referenceRoot string, referenceName string) Confidenti } } +// WithUVMHashEnvelopeReferenceInfo reads UVM hash envelope reference info file +// and base64 encodes the content before setting it for the resource. This is +// no-op if the `referenceName` is empty or the file doesn't exist. +func WithUVMHashEnvelopeReferenceInfo(referenceRoot string, referenceName string) ConfidentialUVMOpt { + return func(ctx context.Context, r *guestresource.ConfidentialOptions) error { + if referenceName == "" { + return nil + } + fullFilePath := filepath.Join(referenceRoot, referenceName) + encoded, err := base64EncodeFileContents(fullFilePath) + if err != nil { + if os.IsNotExist(err) { + log.G(ctx).WithField("filePath", fullFilePath).Debug("UVM hash envelope reference info file not found") + return nil + } + return fmt.Errorf("failed to read UVM hash envelope reference info file: %w", err) + } + r.EncodedUVMHashEnvelopeReference = encoded + return nil + } +} + // SetConfidentialUVMOptions sends information required to run the UVM on // SNP hardware, e.g., security policy and enforcer type, signed UVM reference // information, etc. diff --git a/internal/uvm/start.go b/internal/uvm/start.go index 89e71fda90..17f81ab0de 100644 --- a/internal/uvm/start.go +++ b/internal/uvm/start.go @@ -375,21 +375,24 @@ func (uvm *UtilityVM) Start(ctx context.Context) (err error) { uvm.SCSIManager = mgr if uvm.HasConfidentialPolicy() { - var policy, enforcer, referenceInfoFileRoot, referenceInfoFilePath string + var policy, enforcer, referenceInfoFileRoot, referenceInfoFilePath, hashEnvelopeReferenceInfoFilePath string if uvm.OS() == "linux" { policy = uvm.createOpts.(*OptionsLCOW).SecurityPolicy enforcer = uvm.createOpts.(*OptionsLCOW).SecurityPolicyEnforcer referenceInfoFilePath = uvm.createOpts.(*OptionsLCOW).UVMReferenceInfoFile + hashEnvelopeReferenceInfoFilePath = uvm.createOpts.(*OptionsLCOW).UVMHashEnvelopeReferenceInfoFile referenceInfoFileRoot = vmutils.DefaultLCOWOSBootFilesPath() } else if uvm.OS() == "windows" { policy = uvm.createOpts.(*OptionsWCOW).SecurityPolicy enforcer = uvm.createOpts.(*OptionsWCOW).SecurityPolicyEnforcer referenceInfoFilePath = uvm.createOpts.(*OptionsWCOW).UVMReferenceInfoFile + hashEnvelopeReferenceInfoFilePath = uvm.createOpts.(*OptionsWCOW).UVMHashEnvelopeReferenceInfoFile } copts := []ConfidentialUVMOpt{ WithSecurityPolicy(policy), WithSecurityPolicyEnforcer(enforcer), WithUVMReferenceInfo(referenceInfoFileRoot, referenceInfoFilePath), + WithUVMHashEnvelopeReferenceInfo(referenceInfoFileRoot, hashEnvelopeReferenceInfoFilePath), } if err := uvm.SetConfidentialUVMOptions(ctx, copts...); err != nil { return err diff --git a/pkg/annotations/annotations.go b/pkg/annotations/annotations.go index ef35d6c325..3b25efc735 100644 --- a/pkg/annotations/annotations.go +++ b/pkg/annotations/annotations.go @@ -312,6 +312,12 @@ const ( VirtualMachineKernelDrivers = "io.microsoft.virtualmachine.kerneldrivers" ) +// Confidential UVM annotations. +const ( + // UVMHashEnvelopeReferenceInfoFile specifies the filename of a hash envelope signed UVM reference file to be passed to UVM. + UVMHashEnvelopeReferenceInfoFile = "io.microsoft.virtualmachine.uvm-hash-envelope-reference-info-file" +) + // uVM CPU annotations. const ( // CPUGroupID specifies the cpugroup ID that a UVM should be assigned to, if any. diff --git a/pkg/securitypolicy/securitypolicy.go b/pkg/securitypolicy/securitypolicy.go index c294da2c11..23d994094a 100644 --- a/pkg/securitypolicy/securitypolicy.go +++ b/pkg/securitypolicy/securitypolicy.go @@ -47,10 +47,11 @@ const ( const plan9Prefix = "plan9://" const ( - SecurityContextDirTemplate = "security-context-*" - PolicyFilename = "security-policy-base64" - HostAMDCertFilename = "host-amd-cert-base64" - ReferenceInfoFilename = "reference-info-base64" + SecurityContextDirTemplate = "security-context-*" + PolicyFilename = "security-policy-base64" + HostAMDCertFilename = "host-amd-cert-base64" + ReferenceInfoFilename = "reference-info-base64" + HashEnvelopeReferenceInfoFilename = "hash-envelope-reference-info-base64" ) // PolicyConfig contains toml or JSON config for security policy. diff --git a/pkg/securitypolicy/securitypolicy_options.go b/pkg/securitypolicy/securitypolicy_options.go index 35dd755367..cf993780cd 100644 --- a/pkg/securitypolicy/securitypolicy_options.go +++ b/pkg/securitypolicy/securitypolicy_options.go @@ -24,19 +24,21 @@ import ( type SecurityOptions struct { // state required for the security policy enforcement - PolicyEnforcer SecurityPolicyEnforcer - PolicyEnforcerSet bool - UvmReferenceInfo string - policyMutex sync.Mutex - logWriter io.Writer + PolicyEnforcer SecurityPolicyEnforcer + PolicyEnforcerSet bool + UvmReferenceInfo string + UvmHashEnvelopeReferenceInfo string + policyMutex sync.Mutex + logWriter io.Writer } -func NewSecurityOptions(enforcer SecurityPolicyEnforcer, enforcerSet bool, uvmReferenceInfo string, logWriter io.Writer) *SecurityOptions { +func NewSecurityOptions(enforcer SecurityPolicyEnforcer, enforcerSet bool, uvmReferenceInfo string, uvmHashEnvelopeReferenceInfo string, logWriter io.Writer) *SecurityOptions { return &SecurityOptions{ - PolicyEnforcer: enforcer, - PolicyEnforcerSet: enforcerSet, - UvmReferenceInfo: uvmReferenceInfo, - logWriter: logWriter, + PolicyEnforcer: enforcer, + PolicyEnforcerSet: enforcerSet, + UvmReferenceInfo: uvmReferenceInfo, + UvmHashEnvelopeReferenceInfo: uvmHashEnvelopeReferenceInfo, + logWriter: logWriter, } } @@ -46,7 +48,7 @@ func NewSecurityOptions(enforcer SecurityPolicyEnforcer, enforcerSet bool, uvmRe // encoded security policy and signed UVM reference information The security // policy and uvm reference information can be further presented to workload // containers for validation and attestation purposes. -func (s *SecurityOptions) SetConfidentialOptions(ctx context.Context, enforcerType string, encodedSecurityPolicy string, encodedUVMReference string) error { +func (s *SecurityOptions) SetConfidentialOptions(ctx context.Context, enforcerType string, encodedSecurityPolicy string, encodedUVMReference string, encodedUVMHashEnvelopeReference string) error { s.policyMutex.Lock() defer s.policyMutex.Unlock() @@ -95,6 +97,7 @@ func (s *SecurityOptions) SetConfidentialOptions(ctx context.Context, enforcerTy s.PolicyEnforcer = p s.PolicyEnforcerSet = true s.UvmReferenceInfo = encodedUVMReference + s.UvmHashEnvelopeReferenceInfo = encodedUVMHashEnvelopeReference return nil } @@ -191,7 +194,7 @@ func writeFileInDir(dir string, filename string, data []byte, perm os.FileMode) func (s *SecurityOptions) WriteSecurityContextDir(spec *specs.Spec) error { encodedPolicy := s.PolicyEnforcer.EncodedSecurityPolicy() hostAMDCert := spec.Annotations[annotations.WCOWHostAMDCertificate] - if len(encodedPolicy) > 0 || len(hostAMDCert) > 0 || len(s.UvmReferenceInfo) > 0 { + if len(encodedPolicy) > 0 || len(hostAMDCert) > 0 || len(s.UvmReferenceInfo) > 0 || len(s.UvmHashEnvelopeReferenceInfo) > 0 { // Use os.MkdirTemp to make sure that the directory is unique. securityContextDir, err := os.MkdirTemp(spec.Root.Path, SecurityContextDirTemplate) if err != nil { @@ -212,6 +215,11 @@ func (s *SecurityOptions) WriteSecurityContextDir(spec *specs.Spec) error { return fmt.Errorf("failed to write UVM reference info: %w", err) } } + if len(s.UvmHashEnvelopeReferenceInfo) > 0 { + if err := writeFileInDir(securityContextDir, HashEnvelopeReferenceInfoFilename, []byte(s.UvmHashEnvelopeReferenceInfo), 0777); err != nil { + return fmt.Errorf("failed to write UVM hash envelope reference info: %w", err) + } + } if len(hostAMDCert) > 0 { if err := writeFileInDir(securityContextDir, HostAMDCertFilename, []byte(hostAMDCert), 0777); err != nil { diff --git a/test/gcs/main_test.go b/test/gcs/main_test.go index ce399e0767..80e2891e5f 100644 --- a/test/gcs/main_test.go +++ b/test/gcs/main_test.go @@ -168,7 +168,7 @@ func getHostErr(rt runtime.Runtime, tp transport.Transport) (*hcsv2.Host, error) h := hcsv2.NewHost(rt, tp, &securitypolicy.OpenDoorSecurityPolicyEnforcer{}, os.Stdout) if err := h.SecurityOptions().SetConfidentialOptions( context.Background(), - "", "", "", + "", "", "", "", ); err != nil { return nil, fmt.Errorf("could not set host security policy: %w", err) }