Skip to content
Open
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
21 changes: 20 additions & 1 deletion internal/gcs-sidecar/handlers.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,8 +19,10 @@ import (
"github.com/Microsoft/hcsshim/internal/copyfile"
"github.com/Microsoft/hcsshim/internal/fsformatter"
"github.com/Microsoft/hcsshim/internal/gcs/prot"
"github.com/Microsoft/hcsshim/internal/guestpath"
hcsschema "github.com/Microsoft/hcsshim/internal/hcs/schema2"
"github.com/Microsoft/hcsshim/internal/log"
"github.com/Microsoft/hcsshim/internal/logfields"
oci "github.com/Microsoft/hcsshim/internal/oci"
"github.com/Microsoft/hcsshim/internal/ot"
"github.com/Microsoft/hcsshim/internal/protocol/guestrequest"
Expand Down Expand Up @@ -159,8 +161,10 @@ func (b *Bridge) createContainer(req *request) (err error) {
}
}()

var securityContextDir string

if oci.ParseAnnotationsBool(ctx, spec.Annotations, annotations.WCOWSecurityPolicyEnv, true) {
securityContextDir, err := b.hostState.securityOptions.WriteSecurityContextDir(&spec)
securityContextDir, err = b.hostState.securityOptions.WriteSecurityContextDir(&spec)
if err != nil {
return fmt.Errorf("failed to write security context dir: %w", err)
}
Expand All @@ -177,6 +181,21 @@ func (b *Bridge) createContainer(req *request) (err error) {
cwcowHostedSystemConfig.Spec = spec
}

// Add this fragments.debug mount after policy enforcement and
// reconcile checks so the policy does not have to explicitly
// allow it.
if securityContextDir != "" {
if err := os.MkdirAll(guestpath.WCOWFragmentsPath, 0755); err != nil {
log.G(ctx).WithError(err).WithField(logfields.Path, guestpath.WCOWFragmentsPath).Warn("failed to create fragments debug mount path in uVM")
} else {
container.MappedDirectories = append(container.MappedDirectories, hcsschema.MappedDirectory{
HostPath: guestpath.WCOWFragmentsPath,
ContainerPath: filepath.Join(`C:\`, filepath.Base(securityContextDir), "fragments.debug"),
ReadOnly: true,
})
}
}

// Strip the spec field
hostedSystemBytes, err := json.Marshal(cwcowHostedSystem)

Expand Down
17 changes: 16 additions & 1 deletion internal/guest/runtime/hcsv2/uvm.go
Original file line number Diff line number Diff line change
Expand Up @@ -739,9 +739,24 @@ func (h *Host) CreateContainer(ctx context.Context, id string, settings *prot.VM
}

if oci.ParseAnnotationsBool(ctx, settings.OCISpecification.Annotations, annotations.LCOWSecurityPolicyEnv, true) {
if _, err := h.securityOptions.WriteSecurityContextDir(settings.OCISpecification); err != nil {
securityContextDir, err := h.securityOptions.WriteSecurityContextDir(settings.OCISpecification)
if err != nil {
return nil, fmt.Errorf("failed to write security context dir: %w", err)
}
// Add this special fragments debug mount after policy enforcement
// so the policy does not have to explicitly allow it.
if securityContextDir != "" {
if err := os.MkdirAll(guestpath.LCOWFragmentsPath, 0755); err != nil {
log.G(ctx).WithError(err).WithField(logfields.Path, guestpath.LCOWFragmentsPath).Warn("failed to create fragments debug mount path in uVM")
} else {
settings.OCISpecification.Mounts = append(settings.OCISpecification.Mounts, specs.Mount{
Destination: path.Join("/", filepath.Base(securityContextDir), "fragments.debug"),
Type: "bind",
Source: guestpath.LCOWFragmentsPath,
Options: []string{"bind", "ro"},
})
}
}
}

// Create the BundlePath
Expand Down
6 changes: 6 additions & 0 deletions internal/guestpath/paths.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,12 @@ const (
// WCOWRootPrefixInUVM is the path inside UVM where WCOW container's root
// file system will be mounted
WCOWRootPrefixInUVM = `C:\c`
// LCOWFragmentsPath is the path inside the UVM where injected security
// policy fragments are stored.
LCOWFragmentsPath = "/tmp/fragments"
// WCOWFragmentsPath is the path inside the UVM where injected security
// policy fragments are stored.
WCOWFragmentsPath = `C:\InjectedFragments`
// SandboxMountPrefix is mount prefix used in container spec to mark a
// sandbox-mount
SandboxMountPrefix = "sandbox://"
Expand Down
106 changes: 91 additions & 15 deletions pkg/securitypolicy/securitypolicy_options.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,16 +4,19 @@
"context"
"crypto/sha256"
"encoding/base64"
"encoding/hex"
"encoding/json"
"fmt"
"io"
"math"
"os"
"path/filepath"
"sync"
"time"
"sync/atomic"

"github.com/Microsoft/cosesign1go/pkg/cosesign1"
didx509resolver "github.com/Microsoft/didx509go/pkg/did-x509-resolver"
"github.com/Microsoft/hcsshim/internal/guestpath"
"github.com/Microsoft/hcsshim/internal/log"
"github.com/Microsoft/hcsshim/internal/ot"
"github.com/Microsoft/hcsshim/internal/protocol/guestresource"
Expand All @@ -35,6 +38,18 @@
logWriter io.Writer
}

// Global counter for fragment injection requests, used to create unique
// error / success marker files even when the same fragment is injected
// multiple times.
var FragmentRequestId atomic.Uint64

Check failure on line 44 in pkg/securitypolicy/securitypolicy_options.go

View workflow job for this annotation

GitHub Actions / lint (windows, wcow)

ST1003: var FragmentRequestId should be FragmentRequestID (staticcheck)

Check failure on line 44 in pkg/securitypolicy/securitypolicy_options.go

View workflow job for this annotation

GitHub Actions / lint (windows)

ST1003: var FragmentRequestId should be FragmentRequestID (staticcheck)

Check failure on line 44 in pkg/securitypolicy/securitypolicy_options.go

View workflow job for this annotation

GitHub Actions / lint (windows, lcow)

ST1003: var FragmentRequestId should be FragmentRequestID (staticcheck)

Check failure on line 44 in pkg/securitypolicy/securitypolicy_options.go

View workflow job for this annotation

GitHub Actions / lint (linux)

ST1003: var FragmentRequestId should be FragmentRequestID (staticcheck)

Comment on lines +41 to +45
func fragmentsPath() string {
if osType == "windows" {
return guestpath.WCOWFragmentsPath
}
return guestpath.LCOWFragmentsPath
}

func NewSecurityOptions(enforcer SecurityPolicyEnforcer, enforcerSet bool, uvmReferenceInfo string, uvmHashEnvelopeReferenceInfo string, logWriter io.Writer) *SecurityOptions {
return &SecurityOptions{
PolicyEnforcer: enforcer,
Expand All @@ -59,6 +74,14 @@
return errors.New("security policy has already been set")
}

// Pre-create this directory so that we can mount this dir into
// containers even if no fragments have been injected yet when the
// container starts.
if err := os.MkdirAll(fragmentsPath(), 0755); err != nil {
// This is not fatal, don't fail here.
log.G(ctx).WithError(err).Error("failed to create injected fragments directory")
}

hostData, err := NewSecurityPolicyDigest(encodedSecurityPolicy)
if err != nil {
return err
Expand Down Expand Up @@ -140,6 +163,42 @@
}
}

func writeFileIfNotExists(filename string, data []byte, perm os.FileMode) error {
file, err := os.OpenFile(filename, os.O_WRONLY|os.O_CREATE|os.O_EXCL, perm)
if os.IsExist(err) {
return nil
}
if err != nil {
return err
}

if _, err := file.Write(data); err != nil {
_ = file.Close()
return err
}
return file.Close()
}

func writeFragmentMetadata(ctx context.Context, filename, issuer, feed string, headerSVN *int64) {
metadata := struct {
Issuer string `json:"issuer"`
Feed string `json:"feed"`
HeaderSVN *int64 `json:"headerSvn"`
}{
Issuer: issuer,
Feed: feed,
HeaderSVN: headerSVN,
}
contents, err := json.Marshal(metadata)
if err != nil {
log.G(ctx).WithError(err).Warn("failed to marshal injected fragment metadata")
return
}
if err := writeFileIfNotExists(filename, contents, 0644); err != nil {
log.G(ctx).WithError(err).Warn("failed to write injected fragment metadata")
}
}

// Fragment extends current security policy with additional constraints
// from the incoming fragment. Note that it is base64 encoded over the bridge/
//
Expand All @@ -155,13 +214,40 @@
defer span.End()
defer func() { ot.SetSpanStatus(span, err) }()
span.SetAttributes(attribute.String("fragment", fmt.Sprintf("%+v", fragment)))
currReqId := FragmentRequestId.Add(1)

Check failure on line 217 in pkg/securitypolicy/securitypolicy_options.go

View workflow job for this annotation

GitHub Actions / lint (windows, wcow)

ST1003: var currReqId should be currReqID (staticcheck)

Check failure on line 217 in pkg/securitypolicy/securitypolicy_options.go

View workflow job for this annotation

GitHub Actions / lint (windows)

ST1003: var currReqId should be currReqID (staticcheck)

Check failure on line 217 in pkg/securitypolicy/securitypolicy_options.go

View workflow job for this annotation

GitHub Actions / lint (windows, lcow)

ST1003: var currReqId should be currReqID (staticcheck)

Check failure on line 217 in pkg/securitypolicy/securitypolicy_options.go

View workflow job for this annotation

GitHub Actions / lint (linux)

ST1003: var currReqId should be currReqID (staticcheck)

// An empty media type defaults to a Rego policy fragment, for backward
// compatibility with older hosts that do not set the field.
mediaType := fragment.MediaType
if mediaType == "" {
mediaType = mediaTypeFragment
}

raw, err := base64.StdEncoding.DecodeString(fragment.Fragment)
if err != nil {
return fmt.Errorf("failed to decode fragment: %w", err)
}
sha := sha256.Sum256(raw)
shaHex := hex.EncodeToString(sha[:])
thisFragmentDir := filepath.Join(fragmentsPath(), shaHex)
defer func() {
markerName := fmt.Sprintf("%d.succeed", currReqId)
var markerContents []byte
if err != nil {
markerName = fmt.Sprintf("%d.fail", currReqId)
markerContents = []byte(err.Error())
}
if markerErr := os.WriteFile(filepath.Join(thisFragmentDir, markerName), markerContents, 0644); markerErr != nil {
log.G(ctx).WithError(markerErr).Warnf("failed to write injected fragment outcome marker %s", markerName)
}
}()

if err := os.MkdirAll(thisFragmentDir, 0755); err != nil {
return fmt.Errorf("failed to create injected fragment directory: %w", err)
}
if err := writeFileIfNotExists(filepath.Join(thisFragmentDir, "fragment.cose"), raw, 0644); err != nil {
return fmt.Errorf("failed to write fragment.cose: %w", err)
}
switch mediaType {
case mediaTypeFragment, mediaTypeTransparencyTrustList:
default:
Expand All @@ -172,24 +258,13 @@
return fmt.Errorf("cannot inject fragment blob with unsupported media type %q", mediaType)
}

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)
}
if err := writeFileIfNotExists(filepath.Join(thisFragmentDir, "fragment"), unpacked.Payload, 0644); err != nil {
return fmt.Errorf("failed to write injected fragment payload: %w", err)
}

cwtClaimsRaw, hasCwtClaims := unpacked.Protected[cosesign1.COSE_Header_CWTClaims]
var cwtClaims map[any]any
Expand Down Expand Up @@ -241,6 +316,7 @@
svnFromCwt = &svn
}
}
writeFragmentMetadata(ctx, filepath.Join(thisFragmentDir, "metadata.json"), issuer, feed, svnFromCwt)

switch mediaType {
case mediaTypeTransparencyTrustList:
Expand Down
Loading