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
2 changes: 1 addition & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ tool (
)

require (
github.com/Microsoft/cosesign1go v1.4.0
github.com/Microsoft/cosesign1go v1.6.0-alpha1
github.com/Microsoft/didx509go v0.0.3
github.com/Microsoft/go-winio v0.6.3-0.20251027160822-ad3df93bed29
github.com/blang/semver/v4 v4.0.0
Expand Down
4 changes: 4 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -364,6 +364,10 @@ github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/resourcemapp
github.com/JohnCGriffin/overflow v0.0.0-20211019200055-46fa312c352c/go.mod h1:X0CRv0ky0k6m906ixxpzmDRLvX58TFUKS2eePweuyxk=
github.com/Microsoft/cosesign1go v1.4.0 h1:VdiqzsilEE6t1GQi98I/h0WpVFM7AyMEeyP8ud7V/BY=
github.com/Microsoft/cosesign1go v1.4.0/go.mod h1:1La/HcGw19rRLhPW0S6u55K6LKfti+GQSgGCtrfhVe8=
github.com/Microsoft/cosesign1go v1.5.0 h1:YmQCF8z7dGp50Rp/+rLTLFOFgIfZ1GSUHXPgLLlOlNk=
github.com/Microsoft/cosesign1go v1.5.0/go.mod h1:s7E3nBWxb//ZLhuLAU5u9EZ1qMGBdgZzrKIUW1H/OIY=
github.com/Microsoft/cosesign1go v1.6.0-alpha1 h1:hFyLTdNp9JCBbeRCx7DGwS6secReX4t9JzlIGHTiz6w=
github.com/Microsoft/cosesign1go v1.6.0-alpha1/go.mod h1:7x+fdYtZ4ureEgfVtl2K+nY4MMfujMsCIb5kRuncpmg=
github.com/Microsoft/didx509go v0.0.3 h1:n/owuFOXVzCEzSyzivMEolKEouBm9G0NrEDgoTekM8A=
github.com/Microsoft/didx509go v0.0.3/go.mod h1:wWt+iQsLzn3011+VfESzznLIp/Owhuj7rLF7yLglYbk=
github.com/Microsoft/go-winio v0.6.3-0.20251027160822-ad3df93bed29 h1:0kQAzHq8vLs7Pptv+7TxjdETLf/nIqJpIB4oC6Ba4vY=
Expand Down
4 changes: 4 additions & 0 deletions internal/protocol/guestresource/resources.go
Original file line number Diff line number Diff line change
Expand Up @@ -240,4 +240,8 @@ type ConfidentialOptions struct {

type SecurityPolicyFragment struct {
Fragment string `json:"Fragment,omitempty"`
// MediaType is the media type of the blob carried in Fragment. An empty
// value is treated by the guest as the default "application/cose-x509+rego"
// for backward compatibility with older hosts that do not set this field.
MediaType string `json:"MediaType,omitempty"`
}
13 changes: 13 additions & 0 deletions internal/regopolicyinterpreter/regopolicyinterpreter.go
Original file line number Diff line number Diff line change
Expand Up @@ -513,6 +513,19 @@ func (r RegoQueryResult) Object(key string) (map[string]interface{}, error) {
}
}

// Array attempts to interpret the result value as an array.
func (r RegoQueryResult) Array(key string) ([]interface{}, error) {
if value, ok := r[key]; ok {
if arr, ok := value.([]interface{}); ok {
return arr, nil
} else {
return nil, fmt.Errorf("value for '%s' is not an array", key)
}
} else {
return nil, fmt.Errorf("unable to find value for key '%s'", key)
}
}

// Bool attempts to interpret a result value as a boolean.
func (r RegoQueryResult) Bool(key string) (bool, error) {
if value, ok := r[key]; ok {
Expand Down
3 changes: 2 additions & 1 deletion internal/uvm/security_policy.go
Original file line number Diff line number Diff line change
Expand Up @@ -130,7 +130,8 @@ func (uvm *UtilityVM) InjectPolicyFragment(ctx context.Context, fragment *ctrdta
ResourceType: guestresource.ResourceTypePolicyFragment,
RequestType: guestrequest.RequestTypeAdd,
Settings: guestresource.SecurityPolicyFragment{
Fragment: fragment.Fragment,
Fragment: fragment.Fragment,
MediaType: fragment.MediaType,
},
},
}
Expand Down
6 changes: 6 additions & 0 deletions pkg/ctrdtaskapi/update.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,12 @@ type PolicyFragment struct {
// The value is a base64 encoded COSE_Sign1 document that contains the
// fragment and any additional information required for validation.
Fragment string `json:"fragment,omitempty"`
// MediaType is the media type of the blob carried in Fragment. It allows
// the same delivery mechanism to carry payloads other than Rego policy
// fragments (e.g. a Transparency Trust List). An empty value is treated by
// the guest as the default "application/cose-x509+rego" for backward
// compatibility with older hosts that do not set this field.
MediaType string `json:"mediaType,omitempty"`
}

type ContainerMount struct {
Expand Down
1 change: 1 addition & 0 deletions pkg/securitypolicy/api.rego
Original file line number Diff line number Diff line change
Expand Up @@ -24,4 +24,5 @@ enforcement_points := {
"load_fragment": {"introducedVersion": "0.9.0", "default_results": {"allowed": false, "add_module": false}, "use_framework": false},
"scratch_mount": {"introducedVersion": "0.10.0", "default_results": {"allowed": true}, "use_framework": false},
"scratch_unmount": {"introducedVersion": "0.10.0", "default_results": {"allowed": true}, "use_framework": false},
"load_transparency_trust_list": {"introducedVersion": "0.12.0", "default_results": {"allowed": false}, "use_framework": false},
}
215 changes: 211 additions & 4 deletions pkg/securitypolicy/framework.rego
Original file line number Diff line number Diff line change
Expand Up @@ -1141,6 +1141,9 @@ default fragment_external_processes := []

fragment_external_processes := data[input.namespace].external_processes

default fragment_transparency_roots := []
fragment_transparency_roots := data[input.namespace].transparency_roots

apply_defaults(name, raw_values, framework_version) := values {
semver.compare(framework_version, version) == 0
values := raw_values
Expand Down Expand Up @@ -1170,6 +1173,20 @@ apply_defaults("fragment", raw_values, framework_version) := values {
]
}

# transparency_roots is introduced in framework version 0.5.0. If an old policy
# has it, silently ignore as it might be using the name for something else.

apply_defaults("transparency_roots", raw_values, framework_version) := values {
semver.compare(framework_version, version) < 0
semver.compare(framework_version, "0.5.0") >= 0
values := raw_values
}

apply_defaults("transparency_roots", raw_values, framework_version) := values {
semver.compare(framework_version, "0.5.0") < 0
values := []
}

default fragment_framework_version := null
fragment_framework_version := data[input.namespace].framework_version

Expand All @@ -1178,7 +1195,8 @@ extract_fragment_includes(includes) := fragment {
objects := {
"containers": apply_defaults("container", fragment_containers, framework_version),
"fragments": apply_defaults("fragment", fragment_fragments, framework_version),
"external_processes": apply_defaults("external_process", fragment_external_processes, framework_version)
"external_processes": apply_defaults("external_process", fragment_external_processes, framework_version),
"transparency_roots": apply_defaults("transparency_roots", fragment_transparency_roots, framework_version),
}

fragment := {
Expand Down Expand Up @@ -1260,27 +1278,74 @@ fragment_issuer_feed_ok(fragment) {
input.feed == fragment.feed
}

header_svn_ok(fragment) {
not input.has_header_svn
}

header_svn_ok(fragment) {
input.has_header_svn
svn_ok(input.header_svn, fragment.minimum_svn)
}

svn_ok_if_defined(minimum_svn) {
data[input.namespace].svn # This also works if the svn is 0
not input.has_header_svn
svn_ok(data[input.namespace].svn, minimum_svn)
}

svn_ok_if_defined(minimum_svn) {
data[input.namespace].svn
input.has_header_svn
# Use to_number as fragment may define svn as a string
to_number(input.header_svn) == to_number(data[input.namespace].svn)
svn_ok(data[input.namespace].svn, minimum_svn)
}

# If not defined in fragment, require SVN to present in the header
svn_ok_if_defined(minimum_svn) {
not data[input.namespace].svn
input.has_header_svn
svn_ok(input.header_svn, minimum_svn)
}

# A fragment rule may require transparency receipts from one or more ledgers
# (the receipt issuers). input.receipt_issuers is the set of ledgers for which
# the enforcer successfully validated a receipt attached to this fragment. If
# not set, no receipts are required.
fragment_receipts_ok(fragment) {
required := object.get(fragment, "required_receipt_issuers", [])
every required_issuer in required {
required_issuer in input.receipt_issuers
}
}

default load_fragment := {"allowed": false}

# load_fragment gets called twice - first before loading the fragment as a Rego
# module, with input.fragment_loaded set to false, in which case we do not yet
# have access to anything under data[fragment.namespace] yet, and so we only
# check that the fragment issuer and feed is valid, but does not actually load
# the fragment into metadata. It will then be called a second time, at which
# point we can check the SVN defined in the fragment is valid, and if
# successful, add the fragment to the metadata.
# point we can check the SVN defined in the fragment is valid (if the SVN is not
# in the header, and thus we could not have checked earlier), and if successful,
# add the fragment to the metadata.

load_fragment := {"allowed": true} {
not input.fragment_loaded
some fragment in candidate_fragments
fragment_issuer_feed_ok(fragment)
# If SVN provided in header, validate it now.
header_svn_ok(fragment)
fragment_receipts_ok(fragment)
}

load_fragment := {"metadata": [updateIssuer], "add_module": add_module, "allowed": true} {
input.fragment_loaded
some fragment in candidate_fragments
fragment_issuer_feed_ok(fragment)
svn_ok(data[input.namespace].svn, fragment.minimum_svn)
# If SVN is defined in the fragment's Rego module, also validate it.
# If header SVN was present, it must match that.
svn_ok_if_defined(fragment.minimum_svn)

issuer := update_issuer(fragment.includes)
updateIssuer := {
Expand All @@ -1293,6 +1358,54 @@ load_fragment := {"metadata": [updateIssuer], "add_module": add_module, "allowed
add_module := "namespace" in fragment.includes
}

# transparency_roots declares which signed Transparency Trust Lists (TTLs) the
# policy is willing to accept, and which ledgers each such TTL may contribute
# keys for. Like candidate_fragments, the candidate set is the union of the
# top-level policy's transparency_roots and any contributed by already-loaded
# fragments that included "transparency_roots".
default policy_transparency_roots := []
policy_transparency_roots := data.policy.transparency_roots

candidate_transparency_roots := roots {
fragment_roots := [r |
feed := data.metadata.issuers[_].feeds[_]
fragment := feed[_]
r := fragment.transparency_roots[_]
]

roots := array.concat(policy_transparency_roots, fragment_roots)
}

# The set of ledger names a matching transparency root authorizes for the given
# (issuer, subject, svn). "*" is a wildcard meaning "any ledger".
ttl_allowed_ledgers_for_issuer_subject_svn(issuer, subject, svn) := allowed_ledgers {
allowed_ledgers := {l |
ttl := candidate_transparency_roots[_]
ttl.issuer == issuer
ttl.subject == subject
svn_ok(svn, ttl.minimum_svn)
l := ttl.allowed_ledgers[_]
}
}

intersect_or_allow_all_if_wildcard(allowed_ledgers, input_ledgers) := result {
not "*" in allowed_ledgers
result := {l | l := input_ledgers[_]; l in allowed_ledgers}
}

intersect_or_allow_all_if_wildcard(allowed_ledgers, input_ledgers) := result {
"*" in allowed_ledgers
result := {l | l := input_ledgers[_]}
}

default load_transparency_trust_list := {"allowed": false}

load_transparency_trust_list := {"allowed": true, "allowed_ledgers": allowed_ledgers} {
root_ledgers := ttl_allowed_ledgers_for_issuer_subject_svn(input.issuer, input.subject, input.svn)
allowed_ledgers := intersect_or_allow_all_if_wildcard(root_ledgers, input.ledgers)
count(allowed_ledgers) > 0
}

default scratch_mount := {"allowed": false}

scratch_mounted(target) {
Expand Down Expand Up @@ -1810,6 +1923,14 @@ fragment_version_is_valid {
svn_ok(data[input.namespace].svn, fragment.minimum_svn)
}

fragment_version_is_valid {
some fragment in candidate_fragments
fragment.issuer == input.issuer
fragment.feed == input.feed
input.has_header_svn
svn_ok(input.header_svn, fragment.minimum_svn)
}

default svn_mismatch := false

svn_mismatch {
Expand All @@ -1830,6 +1951,39 @@ svn_mismatch {
to_number(fragment.minimum_svn)
}

# Header SVN is always a number, not semver
svn_mismatch {
some fragment in candidate_fragments
fragment.issuer == input.issuer
fragment.feed == input.feed
input.fragment_loaded
semver.is_valid(fragment.minimum_svn)
input.has_header_svn
}

default header_svn_not_match_fragment := false

header_svn_not_match_fragment {
input.has_header_svn
some fragment in candidate_fragments
fragment.issuer == input.issuer
fragment.feed == input.feed
input.fragment_loaded
data[input.namespace].svn
to_number(data[input.namespace].svn) != to_number(input.header_svn)
}

default missing_svn := false

missing_svn {
not input.has_header_svn
some fragment in candidate_fragments
fragment.issuer == input.issuer
fragment.feed == input.feed
input.fragment_loaded
not data[input.namespace].svn
}

errors["fragment svn is below the specified minimum"] {
input.rule == "load_fragment"
fragment_feed_matches
Expand All @@ -1845,6 +1999,52 @@ errors["fragment svn and the specified minimum are different types"] {
svn_mismatch
}

errors[svnMismatchError] {
input.rule == "load_fragment"
fragment_feed_matches
input.fragment_loaded
header_svn_not_match_fragment

svnMismatchError := sprintf("svn in header %v does not match svn in fragment rego %v", [input.header_svn, data[input.namespace].svn])
}

errors["missing fragment svn in either header or rego payload"] {
input.rule == "load_fragment"
fragment_feed_matches
input.fragment_loaded
missing_svn
}

errors[receipt_error] {
input.rule == "load_fragment"
not input.fragment_loaded
some fragment in candidate_fragments
fragment_issuer_feed_ok(fragment)
required := object.get(fragment, "required_receipt_issuers", [])
some required_issuer in required
not required_issuer in input.receipt_issuers
receipt_error := sprintf("missing receipt from %s", [required_issuer])
}

default transparency_root_matches := false

transparency_root_matches {
some ttl in candidate_transparency_roots
ttl.issuer == input.issuer
ttl.subject == input.subject
svn_ok(input.svn, ttl.minimum_svn)
}

errors["no transparency root matches the trust list issuer, subject and svn"] {
input.rule == "load_transparency_trust_list"
not transparency_root_matches
}

errors["transparency trust list carries no ledgers authorized by any transparency root"] {
input.rule == "load_transparency_trust_list"
transparency_root_matches
}

errors["scratch already mounted at path"] {
input.rule == "scratch_mount"
scratch_mounted(input.target)
Expand Down Expand Up @@ -2331,6 +2531,13 @@ check_fragment(raw_fragment, framework_version) := fragment {
"feed": raw_fragment.feed,
"minimum_svn": raw_fragment.minimum_svn,
"includes": raw_fragment.includes,

# required_receipt_issuers was added in 0.5.0. Older policies default to
# [], i.e. no transparency receipts are required, but if any is
# specified, even when the policy has an older framework_version, we
# respect it since it is restrictive.
"required_receipt_issuers": object.get(raw_fragment, "required_receipt_issuers", []),

# Additional fields need to have default logic applied
}
}
Expand Down
1 change: 1 addition & 0 deletions pkg/securitypolicy/open_door.rego
Original file line number Diff line number Diff line change
Expand Up @@ -23,3 +23,4 @@ runtime_logging := {"allowed": true}
load_fragment := {"allowed": true}
scratch_mount := {"allowed": true}
scratch_unmount := {"allowed": true}
load_transparency_trust_list := {"allowed": true}
1 change: 1 addition & 0 deletions pkg/securitypolicy/policy.rego
Original file line number Diff line number Diff line change
Expand Up @@ -26,4 +26,5 @@ runtime_logging := data.framework.runtime_logging
load_fragment := data.framework.load_fragment
scratch_mount := data.framework.scratch_mount
scratch_unmount := data.framework.scratch_unmount
load_transparency_trust_list := data.framework.load_transparency_trust_list
reason := data.framework.reason
Loading
Loading