From 27ea7d4fadb23dd0e3fa2d395a6d09c46a82af1f Mon Sep 17 00:00:00 2001 From: Mahati Chamarthy Date: Fri, 24 Oct 2025 16:08:56 +0100 Subject: [PATCH] Securitypolicy: Move fragment extraction Move inject and load fragment into the securitypolicy pkg Signed-off-by: Mahati Chamarthy --- internal/gcs-sidecar/host.go | 57 +------------- internal/guest/runtime/hcsv2/uvm.go | 54 +------------ pkg/securitypolicy/securitypolicyenforcer.go | 75 ++++++++++++++++++- .../securitypolicyenforcer_rego.go | 11 --- 4 files changed, 78 insertions(+), 119 deletions(-) diff --git a/internal/gcs-sidecar/host.go b/internal/gcs-sidecar/host.go index 82519ac414..0045aa4748 100644 --- a/internal/gcs-sidecar/host.go +++ b/internal/gcs-sidecar/host.go @@ -5,17 +5,10 @@ package bridge import ( "context" - "crypto/sha256" - "encoding/base64" "fmt" "io" - "os" - "path/filepath" "sync" - "time" - "github.com/Microsoft/cosesign1go/pkg/cosesign1" - didx509resolver "github.com/Microsoft/didx509go/pkg/did-x509-resolver" "github.com/Microsoft/hcsshim/internal/bridgeutils/gcserr" hcsschema "github.com/Microsoft/hcsshim/internal/hcs/schema2" "github.com/Microsoft/hcsshim/internal/log" @@ -75,61 +68,15 @@ func NewHost(initialEnforcer securitypolicy.SecurityPolicyEnforcer) *Host { // security policy (done in the regoby LoadFragment) func (h *Host) InjectFragment(ctx context.Context, fragment *guestresource.LCOWSecurityPolicyFragment) (err error) { log.G(ctx).WithField("fragment", fmt.Sprintf("%+v", fragment)).Debug("GCS Host.InjectFragment") - - raw, err := base64.StdEncoding.DecodeString(fragment.Fragment) - if err != nil { - return err - } - blob := []byte(fragment.Fragment) - // keep a copy of the fragment, so we can manually figure out what went wrong - // will be removed eventually. Give it a unique name to avoid any potential - // race conditions. - sha := sha256.New() - sha.Write(blob) - timestamp := time.Now() - fragmentPath := fmt.Sprintf("fragment-%x-%d.blob", sha.Sum(nil), timestamp.UnixMilli()) - _ = os.WriteFile(filepath.Join(os.TempDir(), fragmentPath), blob, 0644) - - unpacked, err := cosesign1.UnpackAndValidateCOSE1CertChain(raw) - if err != nil { - return fmt.Errorf("InjectFragment failed COSE validation: %w", err) - } - - payloadString := string(unpacked.Payload[:]) - issuer := unpacked.Issuer - feed := unpacked.Feed - chainPem := unpacked.ChainPem - - log.G(ctx).WithFields(logrus.Fields{ - "issuer": issuer, // eg the DID:x509:blah.... - "feed": feed, - "cty": unpacked.ContentType, - "chainPem": chainPem, - }).Debugf("unpacked COSE1 cert chain") - - log.G(ctx).WithFields(logrus.Fields{ - "payload": payloadString, - }).Tracef("unpacked COSE1 payload") - - if len(issuer) == 0 || len(feed) == 0 { // must both be present - return fmt.Errorf("either issuer and feed must both be provided in the COSE_Sign1 protected header") - } - - // Resolve returns a did doc that we don't need - // we only care if there was an error or not - _, err = didx509resolver.Resolve(unpacked.ChainPem, issuer, true) + issuer, feed, payloadString, err := securitypolicy.ExtractAndVerifyFragment(ctx, fragment) if err != nil { - log.G(ctx).Printf("Badly formed fragment - did resolver failed to match fragment did:x509 from chain with purported issuer %s, feed %s - err %s", issuer, feed, err.Error()) return err } - // now offer the payload fragment to the policy err = h.securityPolicyEnforcer.LoadFragment(ctx, issuer, feed, payloadString) if err != nil { - return fmt.Errorf("InjectFragment failed policy load: %w", err) + return fmt.Errorf("error loading security policy fragment: %w", err) } - log.G(ctx).Printf("passed fragment into the enforcer.") - return nil } diff --git a/internal/guest/runtime/hcsv2/uvm.go b/internal/guest/runtime/hcsv2/uvm.go index 7b53536100..8d275e67d9 100644 --- a/internal/guest/runtime/hcsv2/uvm.go +++ b/internal/guest/runtime/hcsv2/uvm.go @@ -6,8 +6,6 @@ package hcsv2 import ( "bufio" "context" - "crypto/sha256" - "encoding/base64" "encoding/json" "fmt" "io" @@ -20,8 +18,6 @@ import ( "syscall" "time" - "github.com/Microsoft/cosesign1go/pkg/cosesign1" - didx509resolver "github.com/Microsoft/didx509go/pkg/did-x509-resolver" cgroups "github.com/containerd/cgroups/v3/cgroup1" cgroup1stats "github.com/containerd/cgroups/v3/cgroup1/stats" "github.com/mattn/go-shellwords" @@ -187,61 +183,15 @@ func (h *Host) SetConfidentialUVMOptions(ctx context.Context, r *guestresource.L // security policy (done in the regoby LoadFragment) func (h *Host) InjectFragment(ctx context.Context, fragment *guestresource.LCOWSecurityPolicyFragment) (err error) { log.G(ctx).WithField("fragment", fmt.Sprintf("%+v", fragment)).Debug("GCS Host.InjectFragment") - - raw, err := base64.StdEncoding.DecodeString(fragment.Fragment) - if err != nil { - return err - } - blob := []byte(fragment.Fragment) - // keep a copy of the fragment, so we can manually figure out what went wrong - // will be removed eventually. Give it a unique name to avoid any potential - // race conditions. - sha := sha256.New() - sha.Write(blob) - timestamp := time.Now() - fragmentPath := fmt.Sprintf("fragment-%x-%d.blob", sha.Sum(nil), timestamp.UnixMilli()) - _ = os.WriteFile(filepath.Join("/tmp", fragmentPath), blob, 0644) - - unpacked, err := cosesign1.UnpackAndValidateCOSE1CertChain(raw) - if err != nil { - return fmt.Errorf("InjectFragment failed COSE validation: %w", err) - } - - payloadString := string(unpacked.Payload[:]) - issuer := unpacked.Issuer - feed := unpacked.Feed - chainPem := unpacked.ChainPem - - log.G(ctx).WithFields(logrus.Fields{ - "issuer": issuer, // eg the DID:x509:blah.... - "feed": feed, - "cty": unpacked.ContentType, - "chainPem": chainPem, - }).Debugf("unpacked COSE1 cert chain") - - log.G(ctx).WithFields(logrus.Fields{ - "payload": payloadString, - }).Tracef("unpacked COSE1 payload") - - if len(issuer) == 0 || len(feed) == 0 { // must both be present - return fmt.Errorf("either issuer and feed must both be provided in the COSE_Sign1 protected header") - } - - // Resolve returns a did doc that we don't need - // we only care if there was an error or not - _, err = didx509resolver.Resolve(unpacked.ChainPem, issuer, true) + issuer, feed, payloadString, err := securitypolicy.ExtractAndVerifyFragment(ctx, fragment) if err != nil { - log.G(ctx).Printf("Badly formed fragment - did resolver failed to match fragment did:x509 from chain with purported issuer %s, feed %s - err %s", issuer, feed, err.Error()) return err } - // now offer the payload fragment to the policy err = h.securityPolicyEnforcer.LoadFragment(ctx, issuer, feed, payloadString) if err != nil { - return fmt.Errorf("InjectFragment failed policy load: %w", err) + return fmt.Errorf("error loading security policy fragment: %w", err) } - log.G(ctx).Printf("passed fragment into the enforcer.") - return nil } diff --git a/pkg/securitypolicy/securitypolicyenforcer.go b/pkg/securitypolicy/securitypolicyenforcer.go index 760f7a4996..127b679ae7 100644 --- a/pkg/securitypolicy/securitypolicyenforcer.go +++ b/pkg/securitypolicy/securitypolicyenforcer.go @@ -2,12 +2,22 @@ package securitypolicy import ( "context" + "crypto/sha256" + "encoding/base64" "fmt" + "os" + "path/filepath" "syscall" + "time" + "github.com/Microsoft/cosesign1go/pkg/cosesign1" + didx509resolver "github.com/Microsoft/didx509go/pkg/did-x509-resolver" + "github.com/Microsoft/hcsshim/internal/log" "github.com/Microsoft/hcsshim/internal/protocol/guestrequest" + "github.com/Microsoft/hcsshim/internal/protocol/guestresource" oci "github.com/opencontainers/runtime-spec/specs-go" "github.com/pkg/errors" + "github.com/sirupsen/logrus" ) type createEnforcerFunc func(base64EncodedPolicy string, criMounts, criPrivilegedMounts []oci.Mount, maxErrorMessageLength int) (SecurityPolicyEnforcer, error) @@ -121,7 +131,7 @@ type SecurityPolicyEnforcer interface { EnforceGetPropertiesPolicy(ctx context.Context) error EnforceDumpStacksPolicy(ctx context.Context) error EnforceRuntimeLoggingPolicy(ctx context.Context) (err error) - LoadFragment(ctx context.Context, issuer string, feed string, code string) error + LoadFragment(ctx context.Context, issuer string, feed string, rego string) error EnforceScratchMountPolicy(ctx context.Context, scratchPath string, encrypted bool) (err error) EnforceScratchUnmountPolicy(ctx context.Context, scratchPath string) (err error) GetUserInfo(spec *oci.Process, rootPath string) (IDName, []IDName, string, error) @@ -142,6 +152,69 @@ func (s stringSet) contains(item string) bool { return contains } +// Fragment extends current security policy with additional constraints +// from the incoming fragment. Note that it is base64 encoded over the bridge/ +// +// There are three checking steps: +// 1 - Unpack the cose document and check it was actually signed with the cert +// chain inside its header +// 2 - Check that the issuer field did:x509 identifier is for that cert chain +// (ie fingerprint of a non leaf cert and the subject matches the leaf cert) +// 3 - Check that this issuer/feed match the requirement of the user provided +// security policy (done in the regoby LoadFragment) +func ExtractAndVerifyFragment(ctx context.Context, fragment *guestresource.LCOWSecurityPolicyFragment) (issuer string, feed string, payloadString string, err error) { + log.G(ctx).WithField("fragment", fmt.Sprintf("%+v", fragment)).Debug("VerifyAndExtractFragment") + + raw, err := base64.StdEncoding.DecodeString(fragment.Fragment) + if err != nil { + return "", "", "", fmt.Errorf("failed to decode fragment: %w", err) + } + blob := []byte(fragment.Fragment) + // keep a copy of the fragment, so we can manually figure out what went wrong + // will be removed eventually. Give it a unique name to avoid any potential + // race conditions. + sha := sha256.New() + sha.Write(blob) + timestamp := time.Now() + fragmentPath := fmt.Sprintf("fragment-%x-%d.blob", sha.Sum(nil), timestamp.UnixMilli()) + _ = os.WriteFile(filepath.Join(os.TempDir(), fragmentPath), blob, 0644) + + unpacked, err := cosesign1.UnpackAndValidateCOSE1CertChain(raw) + if err != nil { + return "", "", "", fmt.Errorf("InjectFragment failed COSE validation: %w", err) + } + + payloadString = string(unpacked.Payload[:]) + issuer = unpacked.Issuer + feed = unpacked.Feed + chainPem := unpacked.ChainPem + + log.G(ctx).WithFields(logrus.Fields{ + "issuer": issuer, // eg the DID:x509:blah.... + "feed": feed, + "cty": unpacked.ContentType, + "chainPem": chainPem, + }).Debugf("unpacked COSE1 cert chain") + + log.G(ctx).WithFields(logrus.Fields{ + "payload": payloadString, + }).Tracef("unpacked COSE1 payload") + + if len(issuer) == 0 || len(feed) == 0 { // must both be present + return "", "", "", fmt.Errorf("either issuer and feed must both be provided in the COSE_Sign1 protected header") + } + + // Resolve returns a did doc that we don't need + // we only care if there was an error or not + _, err = didx509resolver.Resolve(unpacked.ChainPem, issuer, true) + if err != nil { + log.G(ctx).Printf("Badly formed fragment - did resolver failed to match fragment did:x509 from chain with purported issuer %s, feed %s - err %s", issuer, feed, err.Error()) + return "", "", "", err + } + + return issuer, feed, payloadString, nil +} + // CreateSecurityPolicyEnforcer returns an appropriate enforcer for input // parameters. Returns an error if the requested `enforcer` implementation // isn't registered. diff --git a/pkg/securitypolicy/securitypolicyenforcer_rego.go b/pkg/securitypolicy/securitypolicyenforcer_rego.go index f12d8d1fb1..bb2fc27530 100644 --- a/pkg/securitypolicy/securitypolicyenforcer_rego.go +++ b/pkg/securitypolicy/securitypolicyenforcer_rego.go @@ -59,17 +59,6 @@ type regoEnforcer struct { var _ SecurityPolicyEnforcer = (*regoEnforcer)(nil) -//nolint:unused -/*func (sp SecurityPolicy) toInternal() (*securityPolicyInternal, error) { - policy := new(securityPolicyInternal) - var err error - if policy.Containers, err = sp.Containers.toInternal(); err != nil { - return nil, err - } - - return policy, nil -}*/ - func toStringSet(items []string) stringSet { s := make(stringSet) for _, item := range items {