Skip to content

Commit 296d62c

Browse files
committed
feat(runtime): Stage 10 TrustProvider consumer + LocalEmbedded baseline
Lands the capability/evidence/appraisal surface doc 14 defines and wires it into vm_stub_entry so registry entries with a non-zero provider_requirement_hash now run the full evaluate_policy_requirement gate instead of the interim zero-hash short-circuit installed during Stage 8. Provider surface (runtime/include/provider.hpp) - TrustProvider abstract interface with get_capabilities() / attest_runtime() / bind_artifact() entry points per doc 14 §3. - CapabilityStatement, ProviderEvidence, PolicyRequirement, ProviderResult, VerifiedArtifactContext structs carry exactly the fields doc 14 §4–§7 pin; no "tier" / "provider-class string" leaks into the public surface. - ProviderError enum is limited to the 5 public codes doc 14 §9.1 authorises so tier / family / provider identity cannot reach unauthenticated callers. Baseline provider (runtime/src/provider/local_embedded.cpp) - LocalEmbeddedProvider declares ProviderClass::LocalEmbedded, hardware_bound=false, non_exportable_key=false, freshness=None, attestation=None, recovery=self-service, supported_policy_floors= {Debug, Standard}. Producers that require highsec / hardware / remote-attestation are rejected by evaluate_policy_requirement() before any evidence exchange. - appraise() implements doc 14 §11's evidence-binding rule: evidence.package_binding_record_hash / resolved_profile_table_hash / policy_requirement_hash must match the VerifiedArtifactContext the gate verified independently. Evidence claiming a provider class different from the capability's class is rejected so a local_embedded instance cannot masquerade as local_tpm (doc 10 §4). - PolicyRequirement canonicalises to a fixed little-endian layout and hashes under VMPilot::DomainLabels::Hash::PolicyRequirement. The hash is what RuntimeSpecializationRegistry.entries[].provider _requirement_hash commits to, so the runtime reconstructs the requirement from UBR + registry context at dispatch time and rejects any divergence from the registry-committed hash. vm_stub_entry integration - non-zero provider_requirement_hash now: · derives PolicyRequirement from UBR.requested_policy_id + family_id, · checks derived hash == entry.provider_requirement_hash (any drift = fail_closed()), · builds VerifiedArtifactContext from the already-verified PBR canonical bytes + resolved_profile_table_hash, · calls evaluate_policy_requirement on the installed provider, · accepts only ProviderStatus::Satisfied; any other status or any evidence binding failure collapses to fail_closed(). - Default installation uses the built-in LocalEmbeddedProvider; install_provider_for_testing() lets tests inject alternates without touching the singleton getter. Tests (runtime/test/integration/test_provider_local.cpp) - 11 cases exercising doc 14 §10's required behaviours: capability shape, highsec rejection (#1), minimal satisfaction path (#2), cloud-evidence-as-local rejection / provider_class claim mismatch (#3), evidence bound to mismatched package hash (#8), disallowed provider class in allowed_provider_classes, and provider-swap-does-not-change-UBR-verification (#5). - policy_requirement_hash determinism + domain-separation test locks the canonical serializer in place so registry producers and the runtime agree bit-for-bit on requirement commitments. All 87 previously-green runtime integration tests remain green.
1 parent df27b1b commit 296d62c

6 files changed

Lines changed: 896 additions & 7 deletions

File tree

common/include/vm/domain_labels.hpp

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,10 @@ namespace Hash {
4444
// 6/7, sharpens the semantics without changing the label.
4545
constexpr std::string_view UnitBindingTable = "unit-binding-table-v1";
4646
constexpr std::string_view ResolvedProfileTable = "resolved-profile-table-v1";
47+
// Provider / entitlement layer (doc 14, doc 10).
48+
constexpr std::string_view PolicyRequirement = "policy-requirement-v1";
49+
constexpr std::string_view ProviderEvidence = "provider-evidence-v1";
50+
constexpr std::string_view EntitlementCertificate = "entitlement-certificate-v1";
4751
} // namespace Hash
4852

4953
} // namespace VMPilot::DomainLabels

runtime/CMakeLists.txt

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,7 @@ add_library(VMPilot_Runtime STATIC
6767
src/binding/unit.cpp
6868
src/binding/resolved_profile.cpp
6969
src/registry/registry.cpp
70+
src/provider/local_embedded.cpp
7071
${PLATFORM_ASM_SOURCES}
7172
)
7273

@@ -196,6 +197,7 @@ if(ENABLE_TESTS)
196197
test_native_call
197198
test_policy_matrix
198199
test_tls_intrinsic
200+
test_provider_local
199201
)
200202
vmpilot_add_runtime_test(${_t} SOURCES test/integration/${_t}.cpp)
201203
endforeach()

runtime/include/provider.hpp

Lines changed: 240 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,240 @@
1+
#ifndef VMPILOT_RUNTIME_PROVIDER_HPP
2+
#define VMPILOT_RUNTIME_PROVIDER_HPP
3+
4+
#include <array>
5+
#include <cstdint>
6+
#include <string>
7+
#include <vector>
8+
9+
#include <tl/expected.hpp>
10+
11+
#include "vm/family_policy.hpp"
12+
13+
// TrustProvider contract (doc 14 §3–§7). A TrustProvider is the
14+
// abstraction behind provider_requirement_hash lookups on the
15+
// RuntimeSpecializationRegistry. Baseline runtime ships one provider,
16+
// `local_embedded` (doc 10 §4), which carries no hardware root; richer
17+
// providers (`local_tpm`, `local_tee`, `cloud_attested_vm`,
18+
// `cloud_hsm`, `external_kms`) land in future iterations.
19+
//
20+
// Design constraints doc 14 bakes in:
21+
// - public surface never exposes tier / family / provider-class strings
22+
// - status=degraded never satisfies highsec
23+
// - evidence must be bound to the current package + profile table +
24+
// policy requirement hash; provider MAC does NOT replace the
25+
// vendor signature over the PackageBindingRecord
26+
27+
namespace VMPilot::Runtime::Provider {
28+
29+
enum class ProviderClass : std::uint8_t {
30+
LocalEmbedded = 1,
31+
LocalTpm,
32+
LocalTee,
33+
CloudAttestedVm,
34+
CloudHsm,
35+
ExternalKms,
36+
};
37+
38+
enum class CloneResistanceClass : std::uint8_t {
39+
None = 1,
40+
Soft,
41+
Hardware,
42+
};
43+
44+
enum class FreshnessClass : std::uint8_t {
45+
None = 1,
46+
Periodic,
47+
OnlineVerified,
48+
};
49+
50+
enum class KeyCustodyClass : std::uint8_t {
51+
Self = 1,
52+
Hsm,
53+
Vendor,
54+
};
55+
56+
enum class RecoveryModel : std::uint8_t {
57+
SelfService = 1,
58+
SignedReprovision,
59+
Quorum,
60+
};
61+
62+
enum class PrivacyModel : std::uint8_t {
63+
Pairwise = 1,
64+
Pseudonymous,
65+
// Raw hardware identity is forbidden (doc 10 §7) — no enum value.
66+
};
67+
68+
enum class AttestationFormat : std::uint8_t {
69+
None = 1,
70+
LocalEmbeddedV1,
71+
TpmQuoteV1,
72+
TeeReportV1,
73+
CloudAttestationV1,
74+
};
75+
76+
enum class ProviderStatus : std::uint8_t {
77+
Satisfied = 1,
78+
NotSatisfied,
79+
Degraded, // doc 14 §7.1: never accepted for highsec
80+
};
81+
82+
// Stable identifier for a rejection reason; public surface must not
83+
// reveal which provider / tier / family triggered it (doc 14 §9.1).
84+
enum class ProviderError : std::uint8_t {
85+
ArtifactPolicyNotSatisfied = 1,
86+
ProviderRequirementNotSatisfied,
87+
ProviderEvidenceInvalid,
88+
RecoveryRequired,
89+
RuntimeSpecializationUnavailable,
90+
};
91+
92+
// doc 14 §4.
93+
struct CapabilityStatement {
94+
ProviderClass provider_class;
95+
std::string provider_instance_pseudonym; // NOT raw hardware id
96+
bool hardware_bound;
97+
bool non_exportable_key;
98+
bool online_required;
99+
bool migratable;
100+
CloneResistanceClass clone_resistance_class;
101+
FreshnessClass freshness_class;
102+
AttestationFormat attestation_format;
103+
KeyCustodyClass key_custody_class;
104+
RecoveryModel recovery_model;
105+
PrivacyModel privacy_model;
106+
std::vector<VMPilot::DomainLabels::PolicyId> supported_policy_floors;
107+
std::vector<VMPilot::DomainLabels::FamilyId> supported_family_set;
108+
std::array<std::uint8_t, 32> provider_measurement_hash; // zeros → absent
109+
};
110+
111+
// doc 14 §5. `attestation_payload` and `evidence_signature_or_mac` are
112+
// provider-specific opaque bytes; the runtime only verifies the field
113+
// bindings and hands the envelope to the appraiser.
114+
struct ProviderEvidence {
115+
std::string evidence_version;
116+
ProviderClass provider_class;
117+
std::string provider_instance_pseudonym;
118+
std::array<std::uint8_t, 32> nonce;
119+
std::array<std::uint8_t, 32> package_binding_record_hash;
120+
std::array<std::uint8_t, 32> resolved_profile_table_hash;
121+
std::array<std::uint8_t, 32> policy_requirement_hash;
122+
std::array<std::uint8_t, 32> runtime_measurement_hash; // zeros → absent
123+
std::vector<std::uint8_t> attestation_payload;
124+
std::array<std::uint8_t, 32> freshness_proof_hash;
125+
std::vector<std::uint8_t> evidence_signature_or_mac;
126+
};
127+
128+
// doc 14 §6. Built per-call from ResolvedFamilyProfile +
129+
// RuntimeSpecializationRegistry. The hash is a stable commitment to
130+
// every field in this struct and is what entries in
131+
// RuntimeSpecializationRegistry.entries[].provider_requirement_hash
132+
// commit to.
133+
struct PolicyRequirement {
134+
std::string requirement_version;
135+
VMPilot::DomainLabels::PolicyId required_policy_floor;
136+
std::vector<VMPilot::DomainLabels::FamilyId> required_family_set;
137+
bool require_hardware_bound;
138+
bool require_non_exportable_key;
139+
bool require_online_freshness;
140+
bool require_remote_attestation;
141+
RecoveryModel require_recovery_model;
142+
std::vector<ProviderClass> allowed_provider_classes;
143+
std::uint64_t minimum_provider_epoch;
144+
};
145+
146+
// doc 14 §7.
147+
struct ProviderResult {
148+
ProviderStatus status;
149+
std::vector<std::string> satisfied_requirements;
150+
std::vector<std::string> unsatisfied_requirements;
151+
ProviderClass provider_class;
152+
std::array<std::uint8_t, 32> evidence_hash;
153+
std::array<std::uint8_t, 32> nonce;
154+
std::vector<std::uint8_t> result_signature; // verifier signature
155+
};
156+
157+
// Context the gate hands down to TrustProvider::bind_artifact. All
158+
// three hashes must already be verified by the caller (package /
159+
// profile / requirement); the provider binds its evidence to them,
160+
// never replaces them.
161+
struct VerifiedArtifactContext {
162+
std::array<std::uint8_t, 32> package_binding_record_hash;
163+
std::array<std::uint8_t, 32> resolved_profile_table_hash;
164+
std::array<std::uint8_t, 32> policy_requirement_hash;
165+
};
166+
167+
class TrustProvider {
168+
public:
169+
virtual ~TrustProvider() = default;
170+
171+
virtual CapabilityStatement get_capabilities() const noexcept = 0;
172+
173+
virtual tl::expected<ProviderEvidence, ProviderError>
174+
attest_runtime(const std::array<std::uint8_t, 32>& nonce,
175+
const std::array<std::uint8_t, 32>& runtime_measurement,
176+
const std::array<std::uint8_t, 32>&
177+
profile_requirement_hash) noexcept = 0;
178+
179+
virtual tl::expected<ProviderEvidence, ProviderError>
180+
bind_artifact(const std::array<std::uint8_t, 32>& nonce,
181+
const VerifiedArtifactContext& ctx) noexcept = 0;
182+
};
183+
184+
// Canonical domain-separated hash of a PolicyRequirement. Used to
185+
// verify that the requirement derived at runtime matches what the
186+
// registry entry committed to (doc 14 §7 + doc 08 §4 rule 4).
187+
std::array<std::uint8_t, 32>
188+
policy_requirement_hash(const PolicyRequirement& req) noexcept;
189+
190+
// Core appraisal: checks evidence bindings and maps capability vs
191+
// requirement into a ProviderResult. Does NOT replace vendor signature
192+
// verification on the PackageBindingRecord (doc 14 §11).
193+
ProviderResult appraise(const CapabilityStatement& caps,
194+
const PolicyRequirement& req,
195+
const ProviderEvidence& evidence,
196+
const VerifiedArtifactContext& ctx) noexcept;
197+
198+
// Gate the runtime dispatch path uses. Returns ProviderError on any
199+
// failure; maps doc 14 §7.1's degraded-vs-highsec rule internally.
200+
tl::expected<ProviderResult, ProviderError>
201+
evaluate_policy_requirement(TrustProvider& provider,
202+
const PolicyRequirement& requirement,
203+
const VerifiedArtifactContext& ctx,
204+
const std::array<std::uint8_t, 32>&
205+
requested_policy_floor_override) noexcept;
206+
207+
// Baseline provider bundled with 1.0 runtime. Claims no hardware root,
208+
// no remote attestation, no freshness guarantees. Consequently it can
209+
// only satisfy PolicyRequirements that explicitly allow
210+
// ProviderClass::LocalEmbedded and require none of {hardware_bound,
211+
// non_exportable_key, online_freshness, remote_attestation}.
212+
class LocalEmbeddedProvider final : public TrustProvider {
213+
public:
214+
LocalEmbeddedProvider() = default;
215+
explicit LocalEmbeddedProvider(std::string instance_pseudonym) noexcept;
216+
217+
CapabilityStatement get_capabilities() const noexcept override;
218+
219+
tl::expected<ProviderEvidence, ProviderError>
220+
attest_runtime(const std::array<std::uint8_t, 32>& nonce,
221+
const std::array<std::uint8_t, 32>& runtime_measurement,
222+
const std::array<std::uint8_t, 32>&
223+
profile_requirement_hash) noexcept override;
224+
225+
tl::expected<ProviderEvidence, ProviderError>
226+
bind_artifact(const std::array<std::uint8_t, 32>& nonce,
227+
const VerifiedArtifactContext& ctx) noexcept override;
228+
229+
private:
230+
std::string instance_pseudonym_{"local-embedded-default"};
231+
};
232+
233+
// TrustProvider singleton used by the runtime dispatch path. Tests may
234+
// swap in an alternate provider via install_provider_for_testing().
235+
TrustProvider& runtime_provider() noexcept;
236+
void install_provider_for_testing(TrustProvider* provider) noexcept;
237+
238+
} // namespace VMPilot::Runtime::Provider
239+
240+
#endif // VMPILOT_RUNTIME_PROVIDER_HPP

0 commit comments

Comments
 (0)