Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
57 changes: 2 additions & 55 deletions internal/gcs-sidecar/host.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -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
}

Expand Down
54 changes: 2 additions & 52 deletions internal/guest/runtime/hcsv2/uvm.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,6 @@ package hcsv2
import (
"bufio"
"context"
"crypto/sha256"
"encoding/base64"
"encoding/json"
"fmt"
"io"
Expand All @@ -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"
Expand Down Expand Up @@ -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
}

Expand Down
75 changes: 74 additions & 1 deletion pkg/securitypolicy/securitypolicyenforcer.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -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)
Expand All @@ -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) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

we could also put this into its own file, e.g. pkg/securitypolicy/fragments.go

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

At this point, it's just a helper function, so leaving it here seems appropriate.

Copy link
Member Author

@MahatiC MahatiC Nov 3, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@anmaxvl Actually, I have an upcoming PR that does move this along with some other common functions into a different file #2544 - (I haven't pushed all my changes into that PR yet)

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)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

any reason we're ignoring this error?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is just purely for internal debug purposes, so we don't want to throw any errors on failure to write this.


unpacked, err := cosesign1.UnpackAndValidateCOSE1CertChain(raw)
if err != nil {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

is it expected that we always keep the fragment file in temp directory regardless of the validation result?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, as it is just for debug purpose.

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,
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

do we want to log chainPem?

Copy link
Member Author

@MahatiC MahatiC Nov 3, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is a CA cert pem which contains public certs - used for troubleshooting

}).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.
Expand Down
11 changes: 0 additions & 11 deletions pkg/securitypolicy/securitypolicyenforcer_rego.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down