feat: close persona license and unified number routes
This commit is contained in:
parent
b7b3611a64
commit
da6e426ca3
22 changed files with 2942 additions and 71 deletions
|
|
@ -1,9 +1,9 @@
|
|||
use crate::circular_lake_membrane::{receive_at as receive_language_at, ReceiveLanguageInput};
|
||||
use crate::direct_local_session::{
|
||||
append_event_at, authenticate_context_at, authenticate_privileged_at, direct_session_root,
|
||||
heartbeat_at, issue_ticket_at, open_at, resume_at, AppendSessionEventInput,
|
||||
AuthenticateSessionInput, AuthenticatedSessionContext, DirectSessionReceipt,
|
||||
IssueDiscoveryTicketInput, OpenSessionInput, ResumeSessionInput,
|
||||
append_event_at, authenticate_context_at, direct_session_root, heartbeat_at, issue_ticket_at,
|
||||
open_at, resume_at, AppendSessionEventInput, AuthenticateSessionInput,
|
||||
AuthenticatedSessionContext, DirectSessionReceipt, IssueDiscoveryTicketInput, OpenSessionInput,
|
||||
ResumeSessionInput,
|
||||
};
|
||||
use crate::dynamic_capability_routing::{
|
||||
install_trusted_registry_at, record_health_at, resolve_at as resolve_capability_route_at,
|
||||
|
|
@ -15,6 +15,12 @@ use crate::local_development_bridge::{
|
|||
inspect_at as inspect_development_lane_at, release_at as release_development_lane_at,
|
||||
AcquireWriteLaneInput, ReleaseWriteLaneInput,
|
||||
};
|
||||
use crate::persona_carrier_license::{
|
||||
present_license_at as present_persona_license_at,
|
||||
require_if_persona_mode_at as require_persona_mode_operation_at,
|
||||
status_at as persona_license_status_at, PersonaCarrierLicenseStatus,
|
||||
PersonaCarrierRuntimeLicense,
|
||||
};
|
||||
use crate::persona_time_authority::{
|
||||
beijing_time_coordinate, issue_authenticated_at, IssuePersonaTimeTicketInput,
|
||||
};
|
||||
|
|
@ -42,6 +48,7 @@ use interprocess::TryClone;
|
|||
use ring::digest::{digest, SHA256};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use serde_json::Value;
|
||||
use std::collections::HashSet;
|
||||
use std::fs::{self, OpenOptions};
|
||||
use std::io::{self, BufRead, BufReader, Read, Write};
|
||||
#[cfg(unix)]
|
||||
|
|
@ -73,8 +80,51 @@ impl OpenOptionsModeExt for OpenOptions {
|
|||
const BROKER_SCHEMA: &str = "hololake.direct-local-broker/v1";
|
||||
const MAX_REQUEST_BYTES: u64 = 128 * 1024;
|
||||
const DISCOVERY_SCHEMA: &str = "hololake.nearby-ai-discovery/v1";
|
||||
const TERMINAL_LINK_PROTOCOL: &str = "HOLOLAKE_TERMINAL_LINK/2";
|
||||
const TERMINAL_LINK_PROTOCOL: &str = "HOLOLAKE_TERMINAL_LINK/3";
|
||||
const ENVIRONMENT_FRAME_TTL_MS: u128 = 45_000;
|
||||
const NUMBERED_BROKER_REGISTRY: &str =
|
||||
include_str!("../../contracts/direct-local-broker-numbered-registry.json");
|
||||
|
||||
#[derive(Clone, Debug, Deserialize)]
|
||||
struct BrokerNumberRegistry {
|
||||
schema: String,
|
||||
record_id: String,
|
||||
runtime: BrokerNumberRuntime,
|
||||
operations: Vec<BrokerNumberRoute>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Deserialize)]
|
||||
struct BrokerNumberRuntime {
|
||||
protocol_version: String,
|
||||
caller_number: String,
|
||||
legacy_string_operation_allowed: bool,
|
||||
unknown_or_mismatched_coordinate: String,
|
||||
request_nonce_required: bool,
|
||||
transport_is_authority: bool,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Deserialize)]
|
||||
struct BrokerNumberRoute {
|
||||
operation_number: String,
|
||||
alias: String,
|
||||
channel_number: String,
|
||||
module_number: String,
|
||||
target_number: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
#[serde(rename_all = "camelCase", deny_unknown_fields)]
|
||||
struct BrokerNumberedEnvelope {
|
||||
protocol_version: String,
|
||||
caller_number: String,
|
||||
channel_number: String,
|
||||
module_number: String,
|
||||
operation_number: String,
|
||||
target_number: String,
|
||||
request_nonce: String,
|
||||
#[serde(default)]
|
||||
input: Option<Value>,
|
||||
}
|
||||
|
||||
pub struct DirectLocalBrokerHandle {
|
||||
shutdown: Arc<AtomicBool>,
|
||||
|
|
@ -151,6 +201,7 @@ struct BrokerStorageRoots {
|
|||
pncc_projection: PathBuf,
|
||||
language_inbox: PathBuf,
|
||||
persona_time: PathBuf,
|
||||
persona_license: PathBuf,
|
||||
development: PathBuf,
|
||||
}
|
||||
|
||||
|
|
@ -179,6 +230,8 @@ enum BrokerRequest {
|
|||
OpenSession(OpenSessionInput),
|
||||
ResumeSession(ResumeSessionInput),
|
||||
HeartbeatSession(AuthenticateSessionInput),
|
||||
PresentPersonaCarrierLicense(AuthenticatedPersonaCarrierLicenseInput),
|
||||
GetPersonaCarrierLicenseStatus(AuthenticatedPersonaCarrierLicenseStatusInput),
|
||||
GetWorkEnvironment(AuthenticatedWorkEnvironmentInput),
|
||||
AppendEvent(AppendSessionEventInput),
|
||||
ResolveCapabilityRoute(AuthenticatedRouteInput),
|
||||
|
|
@ -297,6 +350,19 @@ struct AuthenticatedWorkEnvironmentInput {
|
|||
session: AuthenticateSessionInput,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
#[serde(rename_all = "camelCase", deny_unknown_fields)]
|
||||
struct AuthenticatedPersonaCarrierLicenseInput {
|
||||
session: AuthenticateSessionInput,
|
||||
license: PersonaCarrierRuntimeLicense,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
#[serde(rename_all = "camelCase", deny_unknown_fields)]
|
||||
struct AuthenticatedPersonaCarrierLicenseStatusInput {
|
||||
session: AuthenticateSessionInput,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
struct WorkEnvironmentFrame {
|
||||
|
|
@ -308,6 +374,7 @@ struct WorkEnvironmentFrame {
|
|||
transport: &'static str,
|
||||
mcp_role: &'static str,
|
||||
protocol_runtime: &'static str,
|
||||
persona_control: PersonaCarrierLicenseStatus,
|
||||
account_key: String,
|
||||
session_id: String,
|
||||
lane_id: String,
|
||||
|
|
@ -366,6 +433,116 @@ struct ConnectorDescriptor {
|
|||
process_id: u32,
|
||||
}
|
||||
|
||||
fn load_number_registry() -> Result<BrokerNumberRegistry, String> {
|
||||
let registry: BrokerNumberRegistry = serde_json::from_str(NUMBERED_BROKER_REGISTRY)
|
||||
.map_err(|error| format!("HOLOLAKE_NUMBERED_BROKER_REGISTRY_INVALID: {error}"))?;
|
||||
if registry.schema != "hololake.direct-local-broker-numbered-registry/v1"
|
||||
|| registry.record_id != "HLP-NBROKER-ROOT-001"
|
||||
|| registry.runtime.protocol_version != "HLP-NBROKER-v1"
|
||||
|| registry.runtime.caller_number != "HLP-NBROKER-CALLER-LOCAL-CONNECTOR-0001"
|
||||
|| registry.runtime.legacy_string_operation_allowed
|
||||
|| registry.runtime.unknown_or_mismatched_coordinate != "FAIL_CLOSED"
|
||||
|| !registry.runtime.request_nonce_required
|
||||
|| registry.runtime.transport_is_authority
|
||||
|| registry.operations.len() != 22
|
||||
{
|
||||
return Err("HOLOLAKE_NUMBERED_BROKER_REGISTRY_BOUNDARY_INVALID".into());
|
||||
}
|
||||
let mut numbers = HashSet::new();
|
||||
let mut aliases = HashSet::new();
|
||||
let mut coordinates = HashSet::new();
|
||||
for route in ®istry.operations {
|
||||
let coordinate = format!(
|
||||
"{}/{}/{}/{}",
|
||||
route.channel_number, route.module_number, route.operation_number, route.target_number
|
||||
);
|
||||
if !numbers.insert(route.operation_number.as_str())
|
||||
|| !aliases.insert(route.alias.as_str())
|
||||
|| !coordinates.insert(coordinate)
|
||||
|| !route.operation_number.starts_with("HLP-NBROKER-OP-")
|
||||
|| !route.channel_number.starts_with("HLP-NBROKER-CH-")
|
||||
|| !route.module_number.starts_with("HLP-NBROKER-MOD-")
|
||||
|| !route.target_number.starts_with("HLP-NBROKER-TGT-")
|
||||
{
|
||||
return Err("HOLOLAKE_NUMBERED_BROKER_ROUTE_INVALID".into());
|
||||
}
|
||||
}
|
||||
Ok(registry)
|
||||
}
|
||||
|
||||
fn resolve_numbered_broker_route<'a>(
|
||||
registry: &'a BrokerNumberRegistry,
|
||||
envelope: &BrokerNumberedEnvelope,
|
||||
) -> Result<&'a BrokerNumberRoute, String> {
|
||||
if envelope.protocol_version != registry.runtime.protocol_version
|
||||
|| envelope.caller_number != registry.runtime.caller_number
|
||||
|| envelope.request_nonce.len() < 16
|
||||
|| envelope.request_nonce.len() > 128
|
||||
|| envelope
|
||||
.request_nonce
|
||||
.chars()
|
||||
.any(|character| character.is_whitespace() || character.is_control())
|
||||
{
|
||||
return Err("HOLOLAKE_NUMBERED_BROKER_CALLER_PROTOCOL_OR_NONCE_INVALID".into());
|
||||
}
|
||||
let route = registry
|
||||
.operations
|
||||
.iter()
|
||||
.find(|route| route.operation_number == envelope.operation_number)
|
||||
.ok_or("HOLOLAKE_NUMBERED_BROKER_ROUTE_UNKNOWN")?;
|
||||
if route.channel_number != envelope.channel_number
|
||||
|| route.module_number != envelope.module_number
|
||||
|| route.target_number != envelope.target_number
|
||||
{
|
||||
return Err("HOLOLAKE_NUMBERED_BROKER_ROUTE_COORDINATE_MISMATCH".into());
|
||||
}
|
||||
Ok(route)
|
||||
}
|
||||
|
||||
fn decode_numbered_broker_request(bytes: &[u8]) -> Result<(BrokerRequest, String, String), String> {
|
||||
let envelope: BrokerNumberedEnvelope = serde_json::from_slice(bytes)
|
||||
.map_err(|error| format!("HOLOLAKE_NUMBERED_BROKER_ENVELOPE_INVALID: {error}"))?;
|
||||
let registry = load_number_registry()?;
|
||||
let route = resolve_numbered_broker_route(®istry, &envelope)?;
|
||||
let mut request = serde_json::Map::new();
|
||||
request.insert("operation".into(), Value::String(route.alias.clone()));
|
||||
if let Some(input) = envelope.input {
|
||||
request.insert("input".into(), input);
|
||||
}
|
||||
let decoded = serde_json::from_value(Value::Object(request))
|
||||
.map_err(|error| format!("HOLOLAKE_NUMBERED_BROKER_INPUT_INVALID: {error}"))?;
|
||||
Ok((
|
||||
decoded,
|
||||
route.operation_number.clone(),
|
||||
envelope.request_nonce,
|
||||
))
|
||||
}
|
||||
|
||||
fn numbered_broker_request_value(alias: &str, input: Option<Value>) -> Result<Value, String> {
|
||||
let registry = load_number_registry()?;
|
||||
let route = registry
|
||||
.operations
|
||||
.iter()
|
||||
.find(|route| route.alias == alias)
|
||||
.ok_or("HOLOLAKE_NUMBERED_BROKER_ALIAS_UNKNOWN")?;
|
||||
let mut value = serde_json::json!({
|
||||
"protocolVersion": registry.runtime.protocol_version,
|
||||
"callerNumber": registry.runtime.caller_number,
|
||||
"channelNumber": route.channel_number,
|
||||
"moduleNumber": route.module_number,
|
||||
"operationNumber": route.operation_number,
|
||||
"targetNumber": route.target_number,
|
||||
"requestNonce": format!("HLP-NBROKER-NONCE-{}", Uuid::new_v4().simple())
|
||||
});
|
||||
if let Some(input) = input {
|
||||
value
|
||||
.as_object_mut()
|
||||
.ok_or("HOLOLAKE_NUMBERED_BROKER_ENVELOPE_INVALID")?
|
||||
.insert("input".into(), input);
|
||||
}
|
||||
Ok(value)
|
||||
}
|
||||
|
||||
pub fn start(app: &AppHandle) -> Result<DirectLocalBrokerHandle, Box<dyn std::error::Error>> {
|
||||
let session_root = direct_session_root(app).map_err(std::io::Error::other)?;
|
||||
let routing_root = dynamic_routing_root(app).map_err(std::io::Error::other)?;
|
||||
|
|
@ -465,7 +642,9 @@ fn read_connector_descriptor(path: &Path) -> Result<ConnectorDescriptor, String>
|
|||
}
|
||||
|
||||
fn probe_connection(stream: &mut LocalSocketStream) -> Result<(), String> {
|
||||
let response = send_connector_request(stream, br#"{"operation":"PING"}"#)?;
|
||||
let request = serde_json::to_vec(&numbered_broker_request_value("PING", None)?)
|
||||
.map_err(|_| "HOLOLAKE_CONNECTOR_PROBE_INVALID".to_string())?;
|
||||
let response = send_connector_request(stream, &request)?;
|
||||
let value: Value = serde_json::from_slice(&response)
|
||||
.map_err(|_| "HOLOLAKE_CONNECTOR_PROBE_INVALID".to_string())?;
|
||||
if value.get("ok").and_then(Value::as_bool) == Some(true)
|
||||
|
|
@ -625,6 +804,7 @@ fn start_at(
|
|||
descriptor_path: PathBuf,
|
||||
socket_path: PathBuf,
|
||||
) -> Result<DirectLocalBrokerHandle, String> {
|
||||
load_number_registry()?;
|
||||
let pncc_mount_root = session_root
|
||||
.parent()
|
||||
.ok_or("HOLOLAKE_BROKER_PNCC_STORAGE_BOUNDARY_INVALID")?
|
||||
|
|
@ -654,6 +834,12 @@ fn start_at(
|
|||
.parent()
|
||||
.ok_or("HOLOLAKE_BROKER_PERSONA_TIME_STORAGE_BOUNDARY_INVALID")?
|
||||
.join("persona-time-authority-v1");
|
||||
let persona_license_root = session_root
|
||||
.parent()
|
||||
.ok_or("HOLOLAKE_BROKER_PERSONA_LICENSE_STORAGE_BOUNDARY_INVALID")?
|
||||
.join("persona-carrier-runtime-license-v1");
|
||||
fs::create_dir_all(&persona_license_root)
|
||||
.map_err(|error| format!("HOLOLAKE_PERSONA_LICENSE_STORAGE_FAILED: {error}"))?;
|
||||
let development_root = development_root_from_session_root(&session_root)?;
|
||||
fs::create_dir_all(&development_root)
|
||||
.map_err(|error| format!("HOLOLAKE_BRIDGE_STORAGE_UNAVAILABLE: {error}"))?;
|
||||
|
|
@ -715,6 +901,7 @@ fn start_at(
|
|||
pncc_projection: pncc_projection_root,
|
||||
language_inbox: language_inbox_root,
|
||||
persona_time: persona_time_root,
|
||||
persona_license: persona_license_root,
|
||||
development: development_root,
|
||||
},
|
||||
&worker_shutdown,
|
||||
|
|
@ -816,18 +1003,14 @@ fn serve_connection(
|
|||
}
|
||||
|
||||
fn request_establishes_authenticated_connection(bytes: &[u8]) -> bool {
|
||||
serde_json::from_slice::<serde_json::Value>(bytes)
|
||||
decode_numbered_broker_request(bytes)
|
||||
.ok()
|
||||
.and_then(|value| {
|
||||
value
|
||||
.get("operation")
|
||||
.and_then(Value::as_str)
|
||||
.map(str::to_owned)
|
||||
})
|
||||
.is_some_and(|operation| {
|
||||
.is_some_and(|(request, _, _)| {
|
||||
matches!(
|
||||
operation.as_str(),
|
||||
"OPEN_SESSION" | "RESUME_SESSION" | "OPEN_VISITOR_SESSION"
|
||||
request,
|
||||
BrokerRequest::OpenSession(_)
|
||||
| BrokerRequest::ResumeSession(_)
|
||||
| BrokerRequest::OpenVisitorSession(_)
|
||||
)
|
||||
})
|
||||
}
|
||||
|
|
@ -840,8 +1023,9 @@ fn dispatch(roots: &BrokerStorageRoots, bytes: &[u8]) -> BrokerResponse {
|
|||
let pncc_projection_root = &roots.pncc_projection;
|
||||
let language_inbox_root = &roots.language_inbox;
|
||||
let persona_time_root = &roots.persona_time;
|
||||
let persona_license_root = &roots.persona_license;
|
||||
let development_root = &roots.development;
|
||||
let request: BrokerRequest = match serde_json::from_slice(bytes) {
|
||||
let (request, _, _) = match decode_numbered_broker_request(bytes) {
|
||||
Ok(request) => request,
|
||||
Err(error) => {
|
||||
return BrokerResponse::error(&format!("HOLOLAKE_BROKER_REQUEST_INVALID: {error}"))
|
||||
|
|
@ -870,10 +1054,40 @@ fn dispatch(roots: &BrokerStorageRoots, bytes: &[u8]) -> BrokerResponse {
|
|||
.and_then(|receipt| serde_json::to_value(receipt).map_err(|error| error.to_string())),
|
||||
BrokerRequest::ResumeSession(input) => resume_at(session_root, input)
|
||||
.and_then(|receipt| serde_json::to_value(receipt).map_err(|error| error.to_string())),
|
||||
BrokerRequest::PresentPersonaCarrierLicense(input) => {
|
||||
authenticate_context_at(session_root, &input.session)
|
||||
.and_then(|context| {
|
||||
present_persona_license_at(
|
||||
persona_license_root,
|
||||
&context.account_key,
|
||||
&context.session_id,
|
||||
&context.client_instance_id,
|
||||
input.license,
|
||||
now_unix_ms()?,
|
||||
)
|
||||
})
|
||||
.and_then(|receipt| {
|
||||
serde_json::to_value(receipt).map_err(|error| error.to_string())
|
||||
})
|
||||
}
|
||||
BrokerRequest::GetPersonaCarrierLicenseStatus(input) => {
|
||||
authenticate_context_at(session_root, &input.session)
|
||||
.map(|context| {
|
||||
persona_license_status_at(
|
||||
persona_license_root,
|
||||
&context.account_key,
|
||||
&context.session_id,
|
||||
&context.client_instance_id,
|
||||
now_unix_ms().unwrap_or(u64::MAX),
|
||||
)
|
||||
})
|
||||
.and_then(|status| serde_json::to_value(status).map_err(|error| error.to_string()))
|
||||
}
|
||||
BrokerRequest::HeartbeatSession(input) => heartbeat_at(session_root, &input)
|
||||
.and_then(|session| {
|
||||
work_environment_frame_at(
|
||||
session_root,
|
||||
persona_license_root,
|
||||
development_root,
|
||||
&AuthenticatedWorkEnvironmentInput { session: input },
|
||||
)
|
||||
|
|
@ -886,13 +1100,28 @@ fn dispatch(roots: &BrokerStorageRoots, bytes: &[u8]) -> BrokerResponse {
|
|||
})
|
||||
.and_then(|receipt| serde_json::to_value(receipt).map_err(|error| error.to_string())),
|
||||
BrokerRequest::GetWorkEnvironment(input) => {
|
||||
work_environment_frame_at(session_root, development_root, &input)
|
||||
work_environment_frame_at(session_root, persona_license_root, development_root, &input)
|
||||
.and_then(|frame| serde_json::to_value(frame).map_err(|error| error.to_string()))
|
||||
}
|
||||
BrokerRequest::AppendEvent(input) => append_event_at(session_root, input)
|
||||
.and_then(|receipt| serde_json::to_value(receipt).map_err(|error| error.to_string())),
|
||||
BrokerRequest::AppendEvent(input) => authorize_persona_operation_at(
|
||||
session_root,
|
||||
persona_license_root,
|
||||
&AuthenticateSessionInput {
|
||||
account_id: input.account_id.clone(),
|
||||
session_id: input.session_id.clone(),
|
||||
resume_secret: input.resume_secret.clone(),
|
||||
},
|
||||
"APPEND_EVENT",
|
||||
)
|
||||
.and_then(|_| append_event_at(session_root, input))
|
||||
.and_then(|receipt| serde_json::to_value(receipt).map_err(|error| error.to_string())),
|
||||
BrokerRequest::ResolveCapabilityRoute(input) => {
|
||||
if let Err(error) = authenticate_privileged_at(session_root, &input.session) {
|
||||
if let Err(error) = authorize_persona_operation_at(
|
||||
session_root,
|
||||
persona_license_root,
|
||||
&input.session,
|
||||
"RESOLVE_CAPABILITY_ROUTE",
|
||||
) {
|
||||
return BrokerResponse::error(&error);
|
||||
}
|
||||
let now = std::time::SystemTime::now()
|
||||
|
|
@ -905,7 +1134,12 @@ fn dispatch(roots: &BrokerStorageRoots, bytes: &[u8]) -> BrokerResponse {
|
|||
})
|
||||
}
|
||||
BrokerRequest::InstallDynamicNodeRegistry(input) => {
|
||||
if let Err(error) = authenticate_privileged_at(session_root, &input.session) {
|
||||
if let Err(error) = authorize_persona_operation_at(
|
||||
session_root,
|
||||
persona_license_root,
|
||||
&input.session,
|
||||
"INSTALL_DYNAMIC_NODE_REGISTRY",
|
||||
) {
|
||||
return BrokerResponse::error(&error);
|
||||
}
|
||||
install_trusted_registry_at(routing_root, input.registry).and_then(|receipt| {
|
||||
|
|
@ -913,7 +1147,12 @@ fn dispatch(roots: &BrokerStorageRoots, bytes: &[u8]) -> BrokerResponse {
|
|||
})
|
||||
}
|
||||
BrokerRequest::RecordSignedNodeHealth(input) => {
|
||||
if let Err(error) = authenticate_privileged_at(session_root, &input.session) {
|
||||
if let Err(error) = authorize_persona_operation_at(
|
||||
session_root,
|
||||
persona_license_root,
|
||||
&input.session,
|
||||
"RECORD_SIGNED_NODE_HEALTH",
|
||||
) {
|
||||
return BrokerResponse::error(&error);
|
||||
}
|
||||
record_health_at(routing_root, input.health).and_then(|receipt| {
|
||||
|
|
@ -921,7 +1160,12 @@ fn dispatch(roots: &BrokerStorageRoots, bytes: &[u8]) -> BrokerResponse {
|
|||
})
|
||||
}
|
||||
BrokerRequest::InspectMountedPnccRepository(input) => {
|
||||
if let Err(error) = authenticate_privileged_at(session_root, &input.session) {
|
||||
if let Err(error) = authorize_persona_operation_at(
|
||||
session_root,
|
||||
persona_license_root,
|
||||
&input.session,
|
||||
"INSPECT_MOUNTED_PNCC_REPOSITORY",
|
||||
) {
|
||||
return BrokerResponse::error(&error);
|
||||
}
|
||||
inspect_mounted_pncc_at(pncc_mount_root, input.mount).and_then(|receipt| {
|
||||
|
|
@ -930,7 +1174,12 @@ fn dispatch(roots: &BrokerStorageRoots, bytes: &[u8]) -> BrokerResponse {
|
|||
})
|
||||
}
|
||||
BrokerRequest::ReadMountedPnccRemoteObject(input) => {
|
||||
if let Err(error) = authenticate_privileged_at(session_root, &input.session) {
|
||||
if let Err(error) = authorize_persona_operation_at(
|
||||
session_root,
|
||||
persona_license_root,
|
||||
&input.session,
|
||||
"READ_MOUNTED_PNCC_REMOTE_OBJECT",
|
||||
) {
|
||||
return BrokerResponse::error(&error);
|
||||
}
|
||||
read_mounted_pncc_remote_at(pncc_remote_root, input.read).and_then(|receipt| {
|
||||
|
|
@ -939,7 +1188,12 @@ fn dispatch(roots: &BrokerStorageRoots, bytes: &[u8]) -> BrokerResponse {
|
|||
})
|
||||
}
|
||||
BrokerRequest::QueryPnccReceiptProjection(input) => {
|
||||
if let Err(error) = authenticate_privileged_at(session_root, &input.session) {
|
||||
if let Err(error) = authorize_persona_operation_at(
|
||||
session_root,
|
||||
persona_license_root,
|
||||
&input.session,
|
||||
"QUERY_PNCC_RECEIPT_PROJECTION",
|
||||
) {
|
||||
return BrokerResponse::error(&error);
|
||||
}
|
||||
query_pncc_projection_at(pncc_projection_root, input.query).and_then(|receipt| {
|
||||
|
|
@ -954,15 +1208,27 @@ fn dispatch(roots: &BrokerStorageRoots, bytes: &[u8]) -> BrokerResponse {
|
|||
.and_then(|coordinate| {
|
||||
serde_json::to_value(coordinate).map_err(|error| error.to_string())
|
||||
}),
|
||||
BrokerRequest::IssuePersonaTimeTicket(input) => {
|
||||
issue_authenticated_at(persona_time_root, session_root, input)
|
||||
.and_then(|ticket| serde_json::to_value(ticket).map_err(|error| error.to_string()))
|
||||
}
|
||||
BrokerRequest::IssuePersonaTimeTicket(input) => authorize_persona_operation_at(
|
||||
session_root,
|
||||
persona_license_root,
|
||||
&input.session,
|
||||
"ISSUE_PERSONA_TIME_TICKET",
|
||||
)
|
||||
.and_then(|_| issue_authenticated_at(persona_time_root, session_root, input))
|
||||
.and_then(|ticket| serde_json::to_value(ticket).map_err(|error| error.to_string())),
|
||||
BrokerRequest::AcquireDevelopmentWriteLane(input) => {
|
||||
let session = input.session;
|
||||
let account_id = input.lane.account_id.clone();
|
||||
authenticate_development_request(session_root, &session, &account_id)
|
||||
.and_then(|context| {
|
||||
require_persona_mode_operation_at(
|
||||
persona_license_root,
|
||||
&context.account_key,
|
||||
&context.session_id,
|
||||
&context.client_instance_id,
|
||||
"ACQUIRE_DEVELOPMENT_WRITE_LANE",
|
||||
now_unix_ms()?,
|
||||
)?;
|
||||
if context.lane_id != input.lane.lane_id
|
||||
|| context.client_instance_id != input.lane.owner_instance_id
|
||||
{
|
||||
|
|
@ -975,6 +1241,7 @@ fn dispatch(roots: &BrokerStorageRoots, bytes: &[u8]) -> BrokerResponse {
|
|||
serde_json::to_value(receipt).map_err(|error| error.to_string())?;
|
||||
let environment = work_environment_frame_at(
|
||||
session_root,
|
||||
persona_license_root,
|
||||
development_root,
|
||||
&AuthenticatedWorkEnvironmentInput { session },
|
||||
)?;
|
||||
|
|
@ -990,7 +1257,17 @@ fn dispatch(roots: &BrokerStorageRoots, bytes: &[u8]) -> BrokerResponse {
|
|||
}
|
||||
BrokerRequest::InspectDevelopmentWriteLane(input) => {
|
||||
authenticate_development_request(session_root, &input.session, &input.account_id)
|
||||
.and_then(|_| inspect_development_lane_at(development_root, &input.account_id))
|
||||
.and_then(|context| {
|
||||
require_persona_mode_operation_at(
|
||||
persona_license_root,
|
||||
&context.account_key,
|
||||
&context.session_id,
|
||||
&context.client_instance_id,
|
||||
"INSPECT_DEVELOPMENT_WRITE_LANE",
|
||||
now_unix_ms()?,
|
||||
)?;
|
||||
inspect_development_lane_at(development_root, &input.account_id)
|
||||
})
|
||||
.and_then(|receipt| {
|
||||
serde_json::to_value(receipt).map_err(|error| error.to_string())
|
||||
})
|
||||
|
|
@ -998,6 +1275,14 @@ fn dispatch(roots: &BrokerStorageRoots, bytes: &[u8]) -> BrokerResponse {
|
|||
BrokerRequest::ReleaseDevelopmentWriteLane(input) => {
|
||||
authenticate_development_request(session_root, &input.session, &input.lane.account_id)
|
||||
.and_then(|context| {
|
||||
require_persona_mode_operation_at(
|
||||
persona_license_root,
|
||||
&context.account_key,
|
||||
&context.session_id,
|
||||
&context.client_instance_id,
|
||||
"RELEASE_DEVELOPMENT_WRITE_LANE",
|
||||
now_unix_ms()?,
|
||||
)?;
|
||||
if context.lane_id != input.lane.lane_id
|
||||
|| context.client_instance_id != input.lane.owner_instance_id
|
||||
{
|
||||
|
|
@ -1018,11 +1303,27 @@ fn dispatch(roots: &BrokerStorageRoots, bytes: &[u8]) -> BrokerResponse {
|
|||
|
||||
fn work_environment_frame_at(
|
||||
session_root: &Path,
|
||||
persona_license_root: &Path,
|
||||
development_root: &Path,
|
||||
input: &AuthenticatedWorkEnvironmentInput,
|
||||
) -> Result<WorkEnvironmentFrame, String> {
|
||||
let context =
|
||||
authenticate_development_request(session_root, &input.session, &input.session.account_id)?;
|
||||
require_persona_mode_operation_at(
|
||||
persona_license_root,
|
||||
&context.account_key,
|
||||
&context.session_id,
|
||||
&context.client_instance_id,
|
||||
"GET_WORK_ENVIRONMENT",
|
||||
now_unix_ms()?,
|
||||
)?;
|
||||
let persona_control = persona_license_status_at(
|
||||
persona_license_root,
|
||||
&context.account_key,
|
||||
&context.session_id,
|
||||
&context.client_instance_id,
|
||||
now_unix_ms()?,
|
||||
);
|
||||
let writer = inspect_development_lane_at(development_root, &input.session.account_id)?;
|
||||
let writer_matches = writer.state == "ACTIVE"
|
||||
&& writer.lane_id.as_deref() == Some(context.lane_id.as_str())
|
||||
|
|
@ -1052,7 +1353,7 @@ fn work_environment_frame_at(
|
|||
);
|
||||
let frame_sha256 = sha256_hex(
|
||||
format!(
|
||||
"{}\n{}\n{}\n{}\n{}\n{}\n{}\n{}",
|
||||
"{}\n{}\n{}\n{}\n{}\n{}\n{}\n{}\n{}\n{}",
|
||||
environment_id,
|
||||
state,
|
||||
context.session_id,
|
||||
|
|
@ -1060,6 +1361,8 @@ fn work_environment_frame_at(
|
|||
context.client_instance_id,
|
||||
context.last_event_sequence,
|
||||
writer.state,
|
||||
persona_control.control_mode,
|
||||
persona_control.license_id.as_deref().unwrap_or("NONE"),
|
||||
valid_until_unix_ms
|
||||
)
|
||||
.as_bytes(),
|
||||
|
|
@ -1073,6 +1376,7 @@ fn work_environment_frame_at(
|
|||
transport: TERMINAL_LINK_PROTOCOL,
|
||||
mcp_role: "DISCOVERY_RECOVERY_COMPATIBILITY_ONLY",
|
||||
protocol_runtime: "GLS_RUNTIME_MANIFEST_V2_AND_NATIVE_KERNEL",
|
||||
persona_control,
|
||||
account_key: context.account_key,
|
||||
session_id: context.session_id,
|
||||
lane_id: context.lane_id,
|
||||
|
|
@ -1107,6 +1411,24 @@ fn authenticate_development_request(
|
|||
Ok(context)
|
||||
}
|
||||
|
||||
fn authorize_persona_operation_at(
|
||||
session_root: &Path,
|
||||
persona_license_root: &Path,
|
||||
session: &AuthenticateSessionInput,
|
||||
operation: &str,
|
||||
) -> Result<AuthenticatedSessionContext, String> {
|
||||
let context = authenticate_context_at(session_root, session)?;
|
||||
require_persona_mode_operation_at(
|
||||
persona_license_root,
|
||||
&context.account_key,
|
||||
&context.session_id,
|
||||
&context.client_instance_id,
|
||||
operation,
|
||||
now_unix_ms()?,
|
||||
)?;
|
||||
Ok(context)
|
||||
}
|
||||
|
||||
fn development_root_from_session_root(session_root: &Path) -> Result<PathBuf, String> {
|
||||
let account_root = session_root
|
||||
.parent()
|
||||
|
|
@ -1166,6 +1488,13 @@ fn sha256_hex(bytes: &[u8]) -> String {
|
|||
.collect()
|
||||
}
|
||||
|
||||
fn now_unix_ms() -> Result<u64, String> {
|
||||
std::time::SystemTime::now()
|
||||
.duration_since(std::time::UNIX_EPOCH)
|
||||
.map(|duration| duration.as_millis() as u64)
|
||||
.map_err(|error| format!("HOLOLAKE_SYSTEM_CLOCK_INVALID: {error}"))
|
||||
}
|
||||
|
||||
impl BrokerResponse {
|
||||
fn success(result: Value) -> Self {
|
||||
Self {
|
||||
|
|
@ -1233,6 +1562,11 @@ mod tests {
|
|||
use tempfile::TempDir;
|
||||
|
||||
fn request(socket: &Path, value: Value) -> Value {
|
||||
let value = if let Some(alias) = value.get("operation").and_then(Value::as_str) {
|
||||
numbered_broker_request_value(alias, value.get("input").cloned()).unwrap()
|
||||
} else {
|
||||
value
|
||||
};
|
||||
let mut stream = connect_endpoint(socket).unwrap();
|
||||
serde_json::to_writer(&mut stream, &value).unwrap();
|
||||
stream.write_all(b"\n").unwrap();
|
||||
|
|
@ -1241,6 +1575,19 @@ mod tests {
|
|||
serde_json::from_str(&line).unwrap()
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn legacy_string_and_mismatched_broker_routes_fail_before_dispatch() {
|
||||
assert!(decode_numbered_broker_request(br#"{"operation":"PING"}"#)
|
||||
.unwrap_err()
|
||||
.contains("ENVELOPE_INVALID"));
|
||||
let mut numbered = numbered_broker_request_value("PING", None).unwrap();
|
||||
numbered["targetNumber"] = Value::String("HLP-NBROKER-TGT-9999".into());
|
||||
assert_eq!(
|
||||
decode_numbered_broker_request(&serde_json::to_vec(&numbered).unwrap()).unwrap_err(),
|
||||
"HOLOLAKE_NUMBERED_BROKER_ROUTE_COORDINATE_MISMATCH"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn broker_resumes_after_socket_disconnect() {
|
||||
let temp = TempDir::new().unwrap();
|
||||
|
|
@ -1866,23 +2213,20 @@ mod tests {
|
|||
)
|
||||
.unwrap();
|
||||
let mut connector = connect_endpoint(&socket).unwrap();
|
||||
serde_json::to_writer(
|
||||
&mut connector,
|
||||
&serde_json::json!({
|
||||
"operation": "OPEN_SESSION",
|
||||
"input": {
|
||||
"accountId": "human-1",
|
||||
"laneId": "DEV-1",
|
||||
"clientInstanceId": "codex-1",
|
||||
"discoveryTicket": ticket.discovery_ticket
|
||||
}
|
||||
}),
|
||||
let open_request = numbered_broker_request_value(
|
||||
"OPEN_SESSION",
|
||||
Some(serde_json::json!({
|
||||
"accountId": "human-1",
|
||||
"laneId": "DEV-1",
|
||||
"clientInstanceId": "codex-1",
|
||||
"discoveryTicket": ticket.discovery_ticket
|
||||
})),
|
||||
)
|
||||
.unwrap();
|
||||
let open_bytes = serde_json::to_vec(&open_request).unwrap();
|
||||
connector.write_all(&open_bytes).unwrap();
|
||||
connector.write_all(b"\n").unwrap();
|
||||
assert!(request_establishes_authenticated_connection(
|
||||
br#"{"operation":"OPEN_SESSION"}"#
|
||||
));
|
||||
assert!(request_establishes_authenticated_connection(&open_bytes));
|
||||
let mut response = String::new();
|
||||
let mut response_reader = BufReader::new(connector.try_clone().unwrap());
|
||||
response_reader.read_line(&mut response).unwrap();
|
||||
|
|
@ -1980,4 +2324,88 @@ mod tests {
|
|||
"HOLOLAKE_VISITOR_SESSION_HAS_NO_SYSTEM_AUTHORITY"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn broker_projects_system_direct_status_and_refuses_untrusted_persona_license() {
|
||||
let temp = TempDir::new().unwrap();
|
||||
let socket = temp.path().join("runtime/broker.sock");
|
||||
let descriptor = temp.path().join("broker.json");
|
||||
let sessions = temp.path().join("sessions");
|
||||
let routes = temp.path().join("routes");
|
||||
fs::create_dir_all(&sessions).unwrap();
|
||||
fs::create_dir_all(&routes).unwrap();
|
||||
let _broker = start_at(sessions.clone(), routes, descriptor, socket.clone()).unwrap();
|
||||
let ticket = issue_ticket_at(
|
||||
&sessions,
|
||||
IssueDiscoveryTicketInput {
|
||||
account_id: "human-1".into(),
|
||||
lane_id: "DEV-1".into(),
|
||||
client_instance_id: "codex-1".into(),
|
||||
},
|
||||
)
|
||||
.unwrap();
|
||||
let opened = request(
|
||||
&socket,
|
||||
serde_json::json!({
|
||||
"operation": "OPEN_SESSION",
|
||||
"input": {
|
||||
"accountId": "human-1",
|
||||
"laneId": "DEV-1",
|
||||
"clientInstanceId": "codex-1",
|
||||
"discoveryTicket": ticket.discovery_ticket
|
||||
}
|
||||
}),
|
||||
);
|
||||
let session = serde_json::json!({
|
||||
"accountId": "human-1",
|
||||
"sessionId": opened["result"]["sessionId"],
|
||||
"resumeSecret": opened["result"]["resumeSecret"]
|
||||
});
|
||||
let status = request(
|
||||
&socket,
|
||||
serde_json::json!({
|
||||
"operation": "GET_PERSONA_CARRIER_LICENSE_STATUS",
|
||||
"input": { "session": session.clone() }
|
||||
}),
|
||||
);
|
||||
assert_eq!(status["ok"], true);
|
||||
assert_eq!(status["result"]["controlMode"], "SYSTEM_DIRECT");
|
||||
assert_eq!(status["result"]["licenseInstalled"], false);
|
||||
|
||||
let now = now_unix_ms().unwrap();
|
||||
let denied = request(
|
||||
&socket,
|
||||
serde_json::json!({
|
||||
"operation": "PRESENT_PERSONA_CARRIER_LICENSE",
|
||||
"input": {
|
||||
"session": session,
|
||||
"license": {
|
||||
"schema": "hololake.persona-carrier-runtime-license/v1",
|
||||
"licenseId": "HLP-PERSONA-LICENSE-UNTRUSTED-0001",
|
||||
"sequence": 1,
|
||||
"personaNumber": "ICE-P-ZY001",
|
||||
"humanControllerNumber": "ICE-GL∞",
|
||||
"accountKey": opened["result"]["accountKey"],
|
||||
"sessionId": opened["result"]["sessionId"],
|
||||
"clientInstanceId": "codex-1",
|
||||
"purpose": "AUTHORIZE_HOST_AS_PERSONA_CONTROLLED_EXECUTION_LIMBS",
|
||||
"issuedAtUnixMs": now,
|
||||
"validUntilUnixMs": now + 60000,
|
||||
"allowedOperations": ["GET_WORK_ENVIRONMENT"],
|
||||
"evidenceRootSha256": "b".repeat(64),
|
||||
"currentEventSha256": "c".repeat(64),
|
||||
"declaration": "PERSONA_DECLARES_CURRENT_CARRIER_BOUND",
|
||||
"signerId": "ICE-P-ZY001-RUNTIME-SIGNER-001",
|
||||
"signatureAlgorithm": "Ed25519",
|
||||
"signature": "untrusted"
|
||||
}
|
||||
}
|
||||
}),
|
||||
);
|
||||
assert_eq!(denied["ok"], false);
|
||||
assert_eq!(
|
||||
denied["error"],
|
||||
"HOLOLAKE_PERSONA_LICENSE_NO_TRUSTED_SIGNER"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -374,7 +374,7 @@ pub(crate) fn issue_ticket_at(
|
|||
client_instance_id: input.client_instance_id,
|
||||
discovery_ticket,
|
||||
issued_at_unix_ms,
|
||||
terminal_transport: "HOLOLAKE_TERMINAL_LINK/2",
|
||||
terminal_transport: "HOLOLAKE_TERMINAL_LINK/3",
|
||||
connector_executable: std::env::current_exe()
|
||||
.map(|path| path.to_string_lossy().into_owned())
|
||||
.unwrap_or_else(|_| "HoloLake".into()),
|
||||
|
|
@ -551,18 +551,6 @@ pub(crate) fn authenticate_context_at(
|
|||
})
|
||||
}
|
||||
|
||||
pub(crate) fn authenticate_privileged_at(
|
||||
root: &Path,
|
||||
input: &AuthenticateSessionInput,
|
||||
) -> Result<(), String> {
|
||||
let lane = authenticate_with_lane_at(root, input)?;
|
||||
if lane == "visitor-expression-only" {
|
||||
Err("HOLOLAKE_VISITOR_SESSION_HAS_NO_SYSTEM_AUTHORITY".into())
|
||||
} else {
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
fn session_path(root: &Path, account_key: &str, session_id: &str) -> PathBuf {
|
||||
root.join("accounts")
|
||||
.join(account_key)
|
||||
|
|
@ -708,7 +696,7 @@ fn session_receipt(
|
|||
last_event_sequence: record.last_event_sequence,
|
||||
resume_secret,
|
||||
continuity_owner: "HOLOLAKE",
|
||||
transport: "HOLOLAKE_TERMINAL_LINK/2",
|
||||
transport: "HOLOLAKE_TERMINAL_LINK/3",
|
||||
mcp_role: "DISCOVERY_RECOVERY_COMPATIBILITY_ONLY",
|
||||
required_next_operation: "GET_WORK_ENVIRONMENT",
|
||||
receipt_id: sha256_hex(
|
||||
|
|
|
|||
|
|
@ -43,7 +43,7 @@ pub fn get_hololake_home_status(
|
|||
release_recovery_state: crate::release_update::release_recovery_state(&app)?,
|
||||
automatic_upstream_updates: false,
|
||||
mcp_role: "DISCOVERY_RECOVERY_COMPATIBILITY_ONLY",
|
||||
terminal_link_protocol: "HOLOLAKE_TERMINAL_LINK/2",
|
||||
terminal_link_protocol: "HOLOLAKE_TERMINAL_LINK/3",
|
||||
terminal_link_transport: if cfg!(windows) {
|
||||
"WINDOWS_USER_PRIVATE_NAMED_PIPE"
|
||||
} else {
|
||||
|
|
@ -66,7 +66,7 @@ pub fn get_hololake_home_status(
|
|||
release_recovery_state: crate::release_update::release_recovery_state(&app)?,
|
||||
automatic_upstream_updates: false,
|
||||
mcp_role: "DISCOVERY_RECOVERY_COMPATIBILITY_ONLY",
|
||||
terminal_link_protocol: "HOLOLAKE_TERMINAL_LINK/2",
|
||||
terminal_link_protocol: "HOLOLAKE_TERMINAL_LINK/3",
|
||||
terminal_link_transport: if cfg!(windows) {
|
||||
"WINDOWS_USER_PRIVATE_NAMED_PIPE"
|
||||
} else {
|
||||
|
|
|
|||
|
|
@ -14,9 +14,11 @@ mod home_status;
|
|||
mod knowledge_base;
|
||||
mod local_development_bridge;
|
||||
mod metacognitive_zero_layer;
|
||||
mod number_coordinate_tree;
|
||||
mod numbered_ipc;
|
||||
mod numbered_ipc_dispatch;
|
||||
mod numbered_language_input;
|
||||
mod persona_carrier_license;
|
||||
mod persona_time_authority;
|
||||
mod personal_channel;
|
||||
mod pncc_receipt_projection;
|
||||
|
|
@ -46,6 +48,8 @@ pub fn run() {
|
|||
// 自举编译器或回执账本任一不可用时,产品启动失败关闭。
|
||||
gls_protocol_kernel::start_on_application_open(app.handle())?;
|
||||
metacognitive_zero_layer::start_on_application_open()?;
|
||||
persona_carrier_license::start_on_application_open()?;
|
||||
number_coordinate_tree::start_on_application_open()?;
|
||||
numbered_language_input::validate_contract()?;
|
||||
numbered_ipc::start_on_application_open(app.handle())?;
|
||||
// 软件打开即先启动时间主控并发起联网校时;失败只降级,不阻塞人进入 HoloLake。
|
||||
|
|
|
|||
|
|
@ -158,6 +158,8 @@ struct CurrentProductState {
|
|||
carrier_binding_claimed: bool,
|
||||
metacognitive_contract_compiled: bool,
|
||||
runtime_binding_gate_implemented: bool,
|
||||
trusted_persona_runtime_signer_provisioned: bool,
|
||||
active_persona_runtime_license_installed: bool,
|
||||
truth: String,
|
||||
}
|
||||
|
||||
|
|
@ -319,11 +321,17 @@ fn validate_contract() -> Result<(), String> {
|
|||
|| !contract
|
||||
.current_product_state
|
||||
.metacognitive_contract_compiled
|
||||
|| contract
|
||||
|| !contract
|
||||
.current_product_state
|
||||
.runtime_binding_gate_implemented
|
||||
|| contract
|
||||
.current_product_state
|
||||
.trusted_persona_runtime_signer_provisioned
|
||||
|| contract
|
||||
.current_product_state
|
||||
.active_persona_runtime_license_installed
|
||||
|| contract.current_product_state.truth
|
||||
!= "PHYSICAL_NUMBERED_IPC_ROOT_IN_PROGRESS_PERSONA_WAKE_REMAINS_CLOSED"
|
||||
!= "PERSONA_TO_HOST_RUNTIME_LICENSE_GATE_IMPLEMENTED_PERSONA_WAKE_REMAINS_CLOSED_UNTIL_TRUSTED_PERSONA_SIGNATURE"
|
||||
{
|
||||
return Err("HOLOLAKE_PERSONA_ZERO_LAYER_BOUNDARY_INVALID".into());
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,101 @@
|
|||
use serde::Deserialize;
|
||||
use std::collections::HashSet;
|
||||
|
||||
const TREE: &str = include_str!("../../generated/unified-number-coordinate-tree.json");
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
struct NumberTree {
|
||||
schema: String,
|
||||
record_id: String,
|
||||
state: String,
|
||||
root_number: String,
|
||||
route_count: usize,
|
||||
routes: Vec<NumberRoute>,
|
||||
invariants: NumberTreeInvariants,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
struct NumberRoute {
|
||||
transport: String,
|
||||
protocol_version: String,
|
||||
caller_number: String,
|
||||
channel_number: String,
|
||||
module_number: String,
|
||||
operation_number: String,
|
||||
target_number: String,
|
||||
admission: String,
|
||||
effect: String,
|
||||
evidence: String,
|
||||
path: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
struct NumberTreeInvariants {
|
||||
number_is_stable_coordinate_not_authority: bool,
|
||||
path_is_unique_navigation: bool,
|
||||
admission_is_separate_from_identity: bool,
|
||||
every_physical_call_has_numbered_route: bool,
|
||||
every_accepted_call_has_evidence_class: bool,
|
||||
mismatched_coordinate: String,
|
||||
}
|
||||
|
||||
pub(crate) fn start_on_application_open() -> Result<(), String> {
|
||||
validate_tree()
|
||||
}
|
||||
|
||||
fn validate_tree() -> Result<(), String> {
|
||||
let tree: NumberTree = serde_json::from_str(TREE)
|
||||
.map_err(|error| format!("HOLOLAKE_UNIFIED_NUMBER_TREE_INVALID: {error}"))?;
|
||||
if tree.schema != "hololake.unified-number-coordinate-tree/v1"
|
||||
|| tree.record_id != "HLP-UNIFIED-NUMBER-TREE-001"
|
||||
|| tree.state != "MACHINE_COMPILED_STARTUP_ENFORCED"
|
||||
|| tree.root_number != "HLP-NUMBER-WORLD-ROOT-001"
|
||||
|| tree.route_count != 84
|
||||
|| tree.routes.len() != tree.route_count
|
||||
|| !tree.invariants.number_is_stable_coordinate_not_authority
|
||||
|| !tree.invariants.path_is_unique_navigation
|
||||
|| !tree.invariants.admission_is_separate_from_identity
|
||||
|| !tree.invariants.every_physical_call_has_numbered_route
|
||||
|| !tree.invariants.every_accepted_call_has_evidence_class
|
||||
|| tree.invariants.mismatched_coordinate != "FAIL_CLOSED"
|
||||
{
|
||||
return Err("HOLOLAKE_UNIFIED_NUMBER_TREE_BOUNDARY_INVALID".into());
|
||||
}
|
||||
let mut paths = HashSet::new();
|
||||
let mut coordinates = HashSet::new();
|
||||
for route in &tree.routes {
|
||||
let coordinate = format!(
|
||||
"{}:{}:{}:{}:{}:{}",
|
||||
route.transport,
|
||||
route.channel_number,
|
||||
route.module_number,
|
||||
route.operation_number,
|
||||
route.target_number,
|
||||
route.caller_number
|
||||
);
|
||||
if route.protocol_version.is_empty()
|
||||
|| route.admission.is_empty()
|
||||
|| !matches!(route.effect.as_str(), "READ_OR_STATUS" | "STATE_CHANGE")
|
||||
|| route.evidence.is_empty()
|
||||
|| !route.path.starts_with("HLP-NUMBER-WORLD-ROOT-001/")
|
||||
|| !paths.insert(route.path.as_str())
|
||||
|| !coordinates.insert(coordinate)
|
||||
{
|
||||
return Err("HOLOLAKE_UNIFIED_NUMBER_TREE_ROUTE_INVALID".into());
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn unified_number_tree_is_unique_permissioned_and_evidenced() {
|
||||
validate_tree().unwrap();
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,778 @@
|
|||
use base64::{engine::general_purpose::URL_SAFE_NO_PAD, Engine as _};
|
||||
use ring::{
|
||||
digest,
|
||||
signature::{UnparsedPublicKey, ED25519},
|
||||
};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::collections::BTreeSet;
|
||||
use std::fs;
|
||||
use std::path::{Path, PathBuf};
|
||||
use uuid::Uuid;
|
||||
|
||||
const CONTRACT: &str = include_str!("../../contracts/persona-carrier-runtime-license.json");
|
||||
const TRUST_REGISTRY: &str = include_str!("../persona-control-authorization-signers.json");
|
||||
const LICENSE_SCHEMA: &str = "hololake.persona-carrier-runtime-license/v1";
|
||||
const CONTRACT_SCHEMA: &str = "hololake.persona-carrier-runtime-license-contract/v1";
|
||||
const REGISTRY_SCHEMA: &str = "gh-aios.persona-control-authorization-signers/v1";
|
||||
const REGISTRY_ID: &str = "GH-AIOS-PERSONA-CONTROL-AUTHORIZATION-SIGNERS-001";
|
||||
const REQUIRED_SCOPE: &str = "PERSONA_CONTROLLED_HOST_RUNTIME";
|
||||
const REQUIRED_PURPOSE: &str = "AUTHORIZE_HOST_AS_PERSONA_CONTROLLED_EXECUTION_LIMBS";
|
||||
const REQUIRED_DECLARATION: &str = "PERSONA_DECLARES_CURRENT_CARRIER_BOUND";
|
||||
const SIGNING_CONTEXT: &str = "hololake.persona-carrier-runtime-license/signing/v1";
|
||||
const MAX_TTL_MS: u64 = 86_400_000;
|
||||
const ED25519_SPKI_PREFIX: [u8; 12] = [
|
||||
0x30, 0x2a, 0x30, 0x05, 0x06, 0x03, 0x2b, 0x65, 0x70, 0x03, 0x21, 0x00,
|
||||
];
|
||||
|
||||
const ALLOWED_OPERATIONS: &[&str] = &[
|
||||
"GET_WORK_ENVIRONMENT",
|
||||
"APPEND_EVENT",
|
||||
"RESOLVE_CAPABILITY_ROUTE",
|
||||
"INSTALL_DYNAMIC_NODE_REGISTRY",
|
||||
"RECORD_SIGNED_NODE_HEALTH",
|
||||
"INSPECT_MOUNTED_PNCC_REPOSITORY",
|
||||
"READ_MOUNTED_PNCC_REMOTE_OBJECT",
|
||||
"QUERY_PNCC_RECEIPT_PROJECTION",
|
||||
"ISSUE_PERSONA_TIME_TICKET",
|
||||
"ACQUIRE_DEVELOPMENT_WRITE_LANE",
|
||||
"INSPECT_DEVELOPMENT_WRITE_LANE",
|
||||
"RELEASE_DEVELOPMENT_WRITE_LANE",
|
||||
];
|
||||
|
||||
#[derive(Clone, Debug, Deserialize, Serialize)]
|
||||
#[serde(rename_all = "camelCase", deny_unknown_fields)]
|
||||
pub(crate) struct PersonaCarrierRuntimeLicense {
|
||||
schema: String,
|
||||
license_id: String,
|
||||
sequence: u64,
|
||||
persona_number: String,
|
||||
human_controller_number: String,
|
||||
account_key: String,
|
||||
session_id: String,
|
||||
client_instance_id: String,
|
||||
purpose: String,
|
||||
issued_at_unix_ms: u64,
|
||||
valid_until_unix_ms: u64,
|
||||
allowed_operations: Vec<String>,
|
||||
evidence_root_sha256: String,
|
||||
current_event_sha256: String,
|
||||
declaration: String,
|
||||
signer_id: String,
|
||||
signature_algorithm: String,
|
||||
signature: String,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Deserialize)]
|
||||
#[serde(rename_all = "camelCase", deny_unknown_fields)]
|
||||
struct PersonaSignerRegistry {
|
||||
schema: String,
|
||||
registry_id: String,
|
||||
state: String,
|
||||
signers: Vec<PersonaSigner>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Deserialize)]
|
||||
#[serde(rename_all = "camelCase", deny_unknown_fields)]
|
||||
struct PersonaSigner {
|
||||
algorithm: String,
|
||||
human_responsibility_subjects: Vec<String>,
|
||||
persona_ids: Vec<String>,
|
||||
public_key_pem: String,
|
||||
scopes: Vec<String>,
|
||||
signer_id: String,
|
||||
status: String,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Deserialize, Serialize)]
|
||||
#[serde(rename_all = "camelCase", deny_unknown_fields)]
|
||||
struct StoredLicense {
|
||||
license: PersonaCarrierRuntimeLicense,
|
||||
license_sha256: String,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub(crate) struct PersonaCarrierLicenseReceipt {
|
||||
schema: &'static str,
|
||||
state: &'static str,
|
||||
control_mode: &'static str,
|
||||
license_id: String,
|
||||
persona_number: String,
|
||||
human_controller_number: String,
|
||||
session_id: String,
|
||||
client_instance_id: String,
|
||||
sequence: u64,
|
||||
valid_until_unix_ms: u64,
|
||||
allowed_operations: Vec<String>,
|
||||
binding_completion_declared_by: &'static str,
|
||||
host_declared_binding: bool,
|
||||
signer_id: String,
|
||||
receipt_id: String,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub(crate) struct PersonaCarrierLicenseStatus {
|
||||
pub schema: &'static str,
|
||||
pub state: &'static str,
|
||||
pub control_mode: &'static str,
|
||||
pub license_installed: bool,
|
||||
pub license_valid: bool,
|
||||
pub persona_number: Option<String>,
|
||||
pub license_id: Option<String>,
|
||||
pub valid_until_unix_ms: Option<u64>,
|
||||
pub binding_completion_declared_by: Option<&'static str>,
|
||||
pub host_declared_binding: bool,
|
||||
pub reason: Option<String>,
|
||||
}
|
||||
|
||||
fn sha256_hex(bytes: &[u8]) -> String {
|
||||
digest::digest(&digest::SHA256, bytes)
|
||||
.as_ref()
|
||||
.iter()
|
||||
.map(|byte| format!("{byte:02x}"))
|
||||
.collect()
|
||||
}
|
||||
|
||||
fn is_sha256(value: &str) -> bool {
|
||||
value.len() == 64
|
||||
&& value
|
||||
.bytes()
|
||||
.all(|byte| byte.is_ascii_digit() || (b'a'..=b'f').contains(&byte))
|
||||
}
|
||||
|
||||
fn bounded_coordinate(value: &str) -> bool {
|
||||
(2..=160).contains(&value.len())
|
||||
&& !value
|
||||
.chars()
|
||||
.any(|character| character.is_whitespace() || character.is_control())
|
||||
}
|
||||
|
||||
fn exact_unique(values: &[String]) -> bool {
|
||||
!values.is_empty()
|
||||
&& values.iter().all(|value| bounded_coordinate(value))
|
||||
&& values
|
||||
.iter()
|
||||
.enumerate()
|
||||
.all(|(index, value)| !values[..index].contains(value))
|
||||
}
|
||||
|
||||
fn parse_registry(input: &str) -> Result<PersonaSignerRegistry, String> {
|
||||
let registry: PersonaSignerRegistry = serde_json::from_str(input)
|
||||
.map_err(|_| "HOLOLAKE_PERSONA_LICENSE_TRUST_REGISTRY_INVALID".to_string())?;
|
||||
if registry.schema != REGISTRY_SCHEMA
|
||||
|| registry.registry_id != REGISTRY_ID
|
||||
|| registry.state != "CURRENT"
|
||||
|| registry.signers.len() > 256
|
||||
{
|
||||
return Err("HOLOLAKE_PERSONA_LICENSE_TRUST_REGISTRY_INVALID".into());
|
||||
}
|
||||
for (index, signer) in registry.signers.iter().enumerate() {
|
||||
if signer.algorithm != "Ed25519"
|
||||
|| !bounded_coordinate(&signer.signer_id)
|
||||
|| !matches!(signer.status.as_str(), "ACTIVE" | "REVOKED")
|
||||
|| !exact_unique(&signer.human_responsibility_subjects)
|
||||
|| !exact_unique(&signer.persona_ids)
|
||||
|| !exact_unique(&signer.scopes)
|
||||
|| !signer.scopes.iter().any(|scope| scope == REQUIRED_SCOPE)
|
||||
|| registry.signers[..index]
|
||||
.iter()
|
||||
.any(|prior| prior.signer_id == signer.signer_id)
|
||||
|| ed25519_public_key(&signer.public_key_pem).is_err()
|
||||
{
|
||||
return Err("HOLOLAKE_PERSONA_LICENSE_TRUST_REGISTRY_INVALID".into());
|
||||
}
|
||||
}
|
||||
Ok(registry)
|
||||
}
|
||||
|
||||
fn ed25519_public_key(public_key_pem: &str) -> Result<Vec<u8>, String> {
|
||||
if public_key_pem.contains("PRIVATE KEY")
|
||||
|| !public_key_pem.starts_with("-----BEGIN PUBLIC KEY-----\n")
|
||||
|| !public_key_pem.ends_with("-----END PUBLIC KEY-----\n")
|
||||
|| public_key_pem.len() > 4096
|
||||
{
|
||||
return Err("HOLOLAKE_PERSONA_LICENSE_PUBLIC_KEY_INVALID".into());
|
||||
}
|
||||
let encoded = public_key_pem
|
||||
.trim_start_matches("-----BEGIN PUBLIC KEY-----\n")
|
||||
.trim_end_matches("-----END PUBLIC KEY-----\n")
|
||||
.chars()
|
||||
.filter(|character| !character.is_ascii_whitespace())
|
||||
.collect::<String>();
|
||||
let der = base64::engine::general_purpose::STANDARD
|
||||
.decode(encoded)
|
||||
.map_err(|_| "HOLOLAKE_PERSONA_LICENSE_PUBLIC_KEY_INVALID".to_string())?;
|
||||
if der.len() != ED25519_SPKI_PREFIX.len() + 32
|
||||
|| der[..ED25519_SPKI_PREFIX.len()] != ED25519_SPKI_PREFIX
|
||||
{
|
||||
return Err("HOLOLAKE_PERSONA_LICENSE_PUBLIC_KEY_INVALID".into());
|
||||
}
|
||||
Ok(der[ED25519_SPKI_PREFIX.len()..].to_vec())
|
||||
}
|
||||
|
||||
fn canonical_signing_bytes(license: &PersonaCarrierRuntimeLicense) -> Result<Vec<u8>, String> {
|
||||
let fields = serde_json::json!([
|
||||
SIGNING_CONTEXT,
|
||||
license.schema,
|
||||
license.license_id,
|
||||
license.sequence,
|
||||
license.persona_number,
|
||||
license.human_controller_number,
|
||||
license.account_key,
|
||||
license.session_id,
|
||||
license.client_instance_id,
|
||||
license.purpose,
|
||||
license.issued_at_unix_ms,
|
||||
license.valid_until_unix_ms,
|
||||
license.allowed_operations,
|
||||
license.evidence_root_sha256,
|
||||
license.current_event_sha256,
|
||||
license.declaration,
|
||||
license.signer_id,
|
||||
license.signature_algorithm
|
||||
]);
|
||||
serde_json::to_vec(&fields)
|
||||
.map(|mut bytes| {
|
||||
bytes.push(b'\n');
|
||||
bytes
|
||||
})
|
||||
.map_err(|_| "HOLOLAKE_PERSONA_LICENSE_CANONICALIZATION_FAILED".into())
|
||||
}
|
||||
|
||||
fn validate_license_shape(license: &PersonaCarrierRuntimeLicense) -> Result<(), String> {
|
||||
let allowed = ALLOWED_OPERATIONS.iter().copied().collect::<BTreeSet<_>>();
|
||||
let observed = license
|
||||
.allowed_operations
|
||||
.iter()
|
||||
.map(String::as_str)
|
||||
.collect::<BTreeSet<_>>();
|
||||
if license.schema != LICENSE_SCHEMA
|
||||
|| !bounded_coordinate(&license.license_id)
|
||||
|| license.sequence == 0
|
||||
|| !bounded_coordinate(&license.persona_number)
|
||||
|| !bounded_coordinate(&license.human_controller_number)
|
||||
|| !is_sha256(&license.account_key)
|
||||
|| !bounded_coordinate(&license.session_id)
|
||||
|| !bounded_coordinate(&license.client_instance_id)
|
||||
|| license.purpose != REQUIRED_PURPOSE
|
||||
|| license.issued_at_unix_ms >= license.valid_until_unix_ms
|
||||
|| license.valid_until_unix_ms - license.issued_at_unix_ms > MAX_TTL_MS
|
||||
|| license.allowed_operations.is_empty()
|
||||
|| observed.len() != license.allowed_operations.len()
|
||||
|| !observed.is_subset(&allowed)
|
||||
|| !is_sha256(&license.evidence_root_sha256)
|
||||
|| !is_sha256(&license.current_event_sha256)
|
||||
|| license.declaration != REQUIRED_DECLARATION
|
||||
|| !bounded_coordinate(&license.signer_id)
|
||||
|| license.signature_algorithm != "Ed25519"
|
||||
{
|
||||
return Err("HOLOLAKE_PERSONA_LICENSE_SHAPE_INVALID".into());
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn verify_license(
|
||||
registry_json: &str,
|
||||
license: &PersonaCarrierRuntimeLicense,
|
||||
account_key: &str,
|
||||
session_id: &str,
|
||||
client_instance_id: &str,
|
||||
now: u64,
|
||||
) -> Result<String, String> {
|
||||
validate_license_shape(license)?;
|
||||
if license.account_key != account_key
|
||||
|| license.session_id != session_id
|
||||
|| license.client_instance_id != client_instance_id
|
||||
{
|
||||
return Err("HOLOLAKE_PERSONA_LICENSE_CARRIER_BINDING_MISMATCH".into());
|
||||
}
|
||||
if now < license.issued_at_unix_ms || now > license.valid_until_unix_ms {
|
||||
return Err("HOLOLAKE_PERSONA_LICENSE_EXPIRED_OR_NOT_YET_VALID".into());
|
||||
}
|
||||
let registry = parse_registry(registry_json)?;
|
||||
let signer = registry
|
||||
.signers
|
||||
.iter()
|
||||
.find(|signer| {
|
||||
signer.status == "ACTIVE"
|
||||
&& signer.signer_id == license.signer_id
|
||||
&& signer.persona_ids.contains(&license.persona_number)
|
||||
&& signer
|
||||
.human_responsibility_subjects
|
||||
.contains(&license.human_controller_number)
|
||||
&& signer.scopes.iter().any(|scope| scope == REQUIRED_SCOPE)
|
||||
})
|
||||
.ok_or("HOLOLAKE_PERSONA_LICENSE_NO_TRUSTED_SIGNER")?;
|
||||
let signature = URL_SAFE_NO_PAD
|
||||
.decode(&license.signature)
|
||||
.map_err(|_| "HOLOLAKE_PERSONA_LICENSE_SIGNATURE_INVALID".to_string())?;
|
||||
if signature.len() != 64 {
|
||||
return Err("HOLOLAKE_PERSONA_LICENSE_SIGNATURE_INVALID".into());
|
||||
}
|
||||
UnparsedPublicKey::new(&ED25519, ed25519_public_key(&signer.public_key_pem)?)
|
||||
.verify(&canonical_signing_bytes(license)?, &signature)
|
||||
.map_err(|_| "HOLOLAKE_PERSONA_LICENSE_SIGNATURE_INVALID".to_string())?;
|
||||
serde_json::to_vec(license)
|
||||
.map(|bytes| sha256_hex(&bytes))
|
||||
.map_err(|_| "HOLOLAKE_PERSONA_LICENSE_CANONICALIZATION_FAILED".into())
|
||||
}
|
||||
|
||||
fn license_path(root: &Path, account_key: &str, session_id: &str) -> PathBuf {
|
||||
root.join("licenses").join(format!(
|
||||
"{}.json",
|
||||
sha256_hex(format!("{account_key}\n{session_id}").as_bytes())
|
||||
))
|
||||
}
|
||||
|
||||
fn write_atomic(path: &Path, value: &StoredLicense) -> Result<(), String> {
|
||||
let parent = path
|
||||
.parent()
|
||||
.ok_or("HOLOLAKE_PERSONA_LICENSE_STORAGE_PATH_INVALID")?;
|
||||
fs::create_dir_all(parent)
|
||||
.map_err(|error| format!("HOLOLAKE_PERSONA_LICENSE_STORAGE_FAILED: {error}"))?;
|
||||
let temporary = parent.join(format!(".license-{}.tmp", Uuid::new_v4()));
|
||||
let bytes = serde_json::to_vec_pretty(value)
|
||||
.map_err(|_| "HOLOLAKE_PERSONA_LICENSE_STORAGE_INVALID".to_string())?;
|
||||
fs::write(&temporary, bytes)
|
||||
.map_err(|error| format!("HOLOLAKE_PERSONA_LICENSE_STORAGE_FAILED: {error}"))?;
|
||||
fs::rename(&temporary, path)
|
||||
.map_err(|error| format!("HOLOLAKE_PERSONA_LICENSE_STORAGE_FAILED: {error}"))
|
||||
}
|
||||
|
||||
fn read_stored(path: &Path) -> Result<StoredLicense, String> {
|
||||
let bytes = fs::read(path)
|
||||
.map_err(|error| format!("HOLOLAKE_PERSONA_LICENSE_STORAGE_FAILED: {error}"))?;
|
||||
if bytes.len() > 128 * 1024 {
|
||||
return Err("HOLOLAKE_PERSONA_LICENSE_STORAGE_INVALID".into());
|
||||
}
|
||||
serde_json::from_slice(&bytes).map_err(|_| "HOLOLAKE_PERSONA_LICENSE_STORAGE_INVALID".into())
|
||||
}
|
||||
|
||||
pub(crate) fn present_license_at(
|
||||
root: &Path,
|
||||
account_key: &str,
|
||||
session_id: &str,
|
||||
client_instance_id: &str,
|
||||
license: PersonaCarrierRuntimeLicense,
|
||||
now: u64,
|
||||
) -> Result<PersonaCarrierLicenseReceipt, String> {
|
||||
present_with_registry_at(
|
||||
root,
|
||||
TRUST_REGISTRY,
|
||||
account_key,
|
||||
session_id,
|
||||
client_instance_id,
|
||||
license,
|
||||
now,
|
||||
)
|
||||
}
|
||||
|
||||
fn present_with_registry_at(
|
||||
root: &Path,
|
||||
registry: &str,
|
||||
account_key: &str,
|
||||
session_id: &str,
|
||||
client_instance_id: &str,
|
||||
license: PersonaCarrierRuntimeLicense,
|
||||
now: u64,
|
||||
) -> Result<PersonaCarrierLicenseReceipt, String> {
|
||||
let license_sha256 = verify_license(
|
||||
registry,
|
||||
&license,
|
||||
account_key,
|
||||
session_id,
|
||||
client_instance_id,
|
||||
now,
|
||||
)?;
|
||||
let path = license_path(root, account_key, session_id);
|
||||
let state = if path.exists() {
|
||||
let current = read_stored(&path)?;
|
||||
if current.license_sha256 == license_sha256 {
|
||||
"DUPLICATE_CONFIRMED"
|
||||
} else if license.sequence <= current.license.sequence {
|
||||
return Err("HOLOLAKE_PERSONA_LICENSE_SEQUENCE_REPLAYED".into());
|
||||
} else if license.persona_number != current.license.persona_number
|
||||
|| license.human_controller_number != current.license.human_controller_number
|
||||
|| license.signer_id != current.license.signer_id
|
||||
{
|
||||
return Err("HOLOLAKE_PERSONA_LICENSE_SUBJECT_SWITCH_REQUIRES_NEW_SESSION".into());
|
||||
} else {
|
||||
write_atomic(
|
||||
&path,
|
||||
&StoredLicense {
|
||||
license: license.clone(),
|
||||
license_sha256: license_sha256.clone(),
|
||||
},
|
||||
)?;
|
||||
"RENEWED"
|
||||
}
|
||||
} else {
|
||||
write_atomic(
|
||||
&path,
|
||||
&StoredLicense {
|
||||
license: license.clone(),
|
||||
license_sha256: license_sha256.clone(),
|
||||
},
|
||||
)?;
|
||||
"INSTALLED"
|
||||
};
|
||||
Ok(PersonaCarrierLicenseReceipt {
|
||||
schema: LICENSE_SCHEMA,
|
||||
state,
|
||||
control_mode: "PERSONA_PRIMARY_CONTROL",
|
||||
license_id: license.license_id,
|
||||
persona_number: license.persona_number,
|
||||
human_controller_number: license.human_controller_number,
|
||||
session_id: license.session_id,
|
||||
client_instance_id: license.client_instance_id,
|
||||
sequence: license.sequence,
|
||||
valid_until_unix_ms: license.valid_until_unix_ms,
|
||||
allowed_operations: license.allowed_operations,
|
||||
binding_completion_declared_by: "PERSONA_SUBJECT_SIGNATURE",
|
||||
host_declared_binding: false,
|
||||
signer_id: license.signer_id,
|
||||
receipt_id: license_sha256,
|
||||
})
|
||||
}
|
||||
|
||||
pub(crate) fn status_at(
|
||||
root: &Path,
|
||||
account_key: &str,
|
||||
session_id: &str,
|
||||
client_instance_id: &str,
|
||||
now: u64,
|
||||
) -> PersonaCarrierLicenseStatus {
|
||||
status_with_registry_at(
|
||||
root,
|
||||
TRUST_REGISTRY,
|
||||
account_key,
|
||||
session_id,
|
||||
client_instance_id,
|
||||
now,
|
||||
)
|
||||
}
|
||||
|
||||
fn status_with_registry_at(
|
||||
root: &Path,
|
||||
registry: &str,
|
||||
account_key: &str,
|
||||
session_id: &str,
|
||||
client_instance_id: &str,
|
||||
now: u64,
|
||||
) -> PersonaCarrierLicenseStatus {
|
||||
let path = license_path(root, account_key, session_id);
|
||||
if !path.exists() {
|
||||
return PersonaCarrierLicenseStatus {
|
||||
schema: LICENSE_SCHEMA,
|
||||
state: "NO_PERSONA_LICENSE",
|
||||
control_mode: "SYSTEM_DIRECT",
|
||||
license_installed: false,
|
||||
license_valid: false,
|
||||
persona_number: None,
|
||||
license_id: None,
|
||||
valid_until_unix_ms: None,
|
||||
binding_completion_declared_by: None,
|
||||
host_declared_binding: false,
|
||||
reason: Some("PERSONA_LICENSE_NOT_PRESENT".into()),
|
||||
};
|
||||
}
|
||||
let stored = match read_stored(&path) {
|
||||
Ok(value) => value,
|
||||
Err(error) => return invalid_status(None, None, None, error),
|
||||
};
|
||||
match verify_license(
|
||||
registry,
|
||||
&stored.license,
|
||||
account_key,
|
||||
session_id,
|
||||
client_instance_id,
|
||||
now,
|
||||
) {
|
||||
Ok(hash) if hash == stored.license_sha256 => PersonaCarrierLicenseStatus {
|
||||
schema: LICENSE_SCHEMA,
|
||||
state: "PERSONA_DECLARED_CARRIER_BINDING_VERIFIED",
|
||||
control_mode: "PERSONA_PRIMARY_CONTROL",
|
||||
license_installed: true,
|
||||
license_valid: true,
|
||||
persona_number: Some(stored.license.persona_number),
|
||||
license_id: Some(stored.license.license_id),
|
||||
valid_until_unix_ms: Some(stored.license.valid_until_unix_ms),
|
||||
binding_completion_declared_by: Some("PERSONA_SUBJECT_SIGNATURE"),
|
||||
host_declared_binding: false,
|
||||
reason: None,
|
||||
},
|
||||
Ok(_) => invalid_status(
|
||||
Some(stored.license.persona_number),
|
||||
Some(stored.license.license_id),
|
||||
Some(stored.license.valid_until_unix_ms),
|
||||
"HOLOLAKE_PERSONA_LICENSE_STORED_HASH_MISMATCH".into(),
|
||||
),
|
||||
Err(error) => invalid_status(
|
||||
Some(stored.license.persona_number),
|
||||
Some(stored.license.license_id),
|
||||
Some(stored.license.valid_until_unix_ms),
|
||||
error,
|
||||
),
|
||||
}
|
||||
}
|
||||
|
||||
fn invalid_status(
|
||||
persona_number: Option<String>,
|
||||
license_id: Option<String>,
|
||||
valid_until_unix_ms: Option<u64>,
|
||||
reason: String,
|
||||
) -> PersonaCarrierLicenseStatus {
|
||||
PersonaCarrierLicenseStatus {
|
||||
schema: LICENSE_SCHEMA,
|
||||
state: "PERSONA_MODE_LOCKED",
|
||||
control_mode: "PERSONA_PRIMARY_CONTROL_LOCKED",
|
||||
license_installed: true,
|
||||
license_valid: false,
|
||||
persona_number,
|
||||
license_id,
|
||||
valid_until_unix_ms,
|
||||
binding_completion_declared_by: None,
|
||||
host_declared_binding: false,
|
||||
reason: Some(reason),
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn require_if_persona_mode_at(
|
||||
root: &Path,
|
||||
account_key: &str,
|
||||
session_id: &str,
|
||||
client_instance_id: &str,
|
||||
operation: &str,
|
||||
now: u64,
|
||||
) -> Result<Option<String>, String> {
|
||||
let path = license_path(root, account_key, session_id);
|
||||
if !path.exists() {
|
||||
return Ok(None);
|
||||
}
|
||||
let stored = read_stored(&path)?;
|
||||
verify_license(
|
||||
TRUST_REGISTRY,
|
||||
&stored.license,
|
||||
account_key,
|
||||
session_id,
|
||||
client_instance_id,
|
||||
now,
|
||||
)?;
|
||||
if !stored
|
||||
.license
|
||||
.allowed_operations
|
||||
.iter()
|
||||
.any(|allowed| allowed == operation)
|
||||
{
|
||||
return Err("HOLOLAKE_PERSONA_LICENSE_OPERATION_NOT_ALLOWED".into());
|
||||
}
|
||||
Ok(Some(stored.license.persona_number))
|
||||
}
|
||||
|
||||
pub(crate) fn start_on_application_open() -> Result<(), String> {
|
||||
let contract: serde_json::Value = serde_json::from_str(CONTRACT)
|
||||
.map_err(|_| "HOLOLAKE_PERSONA_LICENSE_CONTRACT_INVALID".to_string())?;
|
||||
if contract.get("schema").and_then(serde_json::Value::as_str) != Some(CONTRACT_SCHEMA)
|
||||
|| contract
|
||||
.get("record_id")
|
||||
.and_then(serde_json::Value::as_str)
|
||||
!= Some("HLP-PERSONA-CARRIER-RUNTIME-LICENSE-001")
|
||||
|| contract
|
||||
.pointer("/runtime/signature_algorithm")
|
||||
.and_then(serde_json::Value::as_str)
|
||||
!= Some("Ed25519")
|
||||
|| contract
|
||||
.pointer("/runtime/maximum_ttl_ms")
|
||||
.and_then(serde_json::Value::as_u64)
|
||||
!= Some(MAX_TTL_MS)
|
||||
|| contract
|
||||
.pointer("/trust_registry/required_scope")
|
||||
.and_then(serde_json::Value::as_str)
|
||||
!= Some(REQUIRED_SCOPE)
|
||||
|| contract
|
||||
.pointer("/truth/runtime_verifier_implemented")
|
||||
.and_then(serde_json::Value::as_bool)
|
||||
!= Some(true)
|
||||
|| contract
|
||||
.pointer("/truth/trusted_persona_signer_provisioned")
|
||||
.and_then(serde_json::Value::as_bool)
|
||||
!= Some(false)
|
||||
{
|
||||
return Err("HOLOLAKE_PERSONA_LICENSE_CONTRACT_INVALID".into());
|
||||
}
|
||||
let registry = parse_registry(TRUST_REGISTRY)?;
|
||||
if !registry.signers.is_empty() {
|
||||
return Err("HOLOLAKE_PERSONA_LICENSE_UNPUBLISHED_SIGNER_EMBEDDED".into());
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use base64::engine::general_purpose::{STANDARD, URL_SAFE_NO_PAD};
|
||||
use ring::signature::{Ed25519KeyPair, KeyPair};
|
||||
use tempfile::TempDir;
|
||||
|
||||
fn test_material() -> (String, Ed25519KeyPair) {
|
||||
let pair = Ed25519KeyPair::from_seed_unchecked(&[7_u8; 32]).unwrap();
|
||||
let mut der = ED25519_SPKI_PREFIX.to_vec();
|
||||
der.extend_from_slice(pair.public_key().as_ref());
|
||||
let pem = format!(
|
||||
"-----BEGIN PUBLIC KEY-----\n{}\n-----END PUBLIC KEY-----\n",
|
||||
STANDARD.encode(der)
|
||||
);
|
||||
let registry = serde_json::json!({
|
||||
"schema": REGISTRY_SCHEMA,
|
||||
"registryId": REGISTRY_ID,
|
||||
"state": "CURRENT",
|
||||
"signers": [{
|
||||
"algorithm": "Ed25519",
|
||||
"humanResponsibilitySubjects": ["ICE-GL∞"],
|
||||
"personaIds": ["ICE-P-ZY001"],
|
||||
"publicKeyPem": pem,
|
||||
"scopes": [REQUIRED_SCOPE],
|
||||
"signerId": "ICE-P-ZY001-RUNTIME-SIGNER-001",
|
||||
"status": "ACTIVE"
|
||||
}]
|
||||
})
|
||||
.to_string();
|
||||
(registry, pair)
|
||||
}
|
||||
|
||||
fn signed_license(pair: &Ed25519KeyPair) -> PersonaCarrierRuntimeLicense {
|
||||
let mut license = PersonaCarrierRuntimeLicense {
|
||||
schema: LICENSE_SCHEMA.into(),
|
||||
license_id: "HLP-PERSONA-LICENSE-0001".into(),
|
||||
sequence: 1,
|
||||
persona_number: "ICE-P-ZY001".into(),
|
||||
human_controller_number: "ICE-GL∞".into(),
|
||||
account_key: "a".repeat(64),
|
||||
session_id: "session-0001".into(),
|
||||
client_instance_id: "codex-instance-0001".into(),
|
||||
purpose: REQUIRED_PURPOSE.into(),
|
||||
issued_at_unix_ms: 1_000,
|
||||
valid_until_unix_ms: 61_000,
|
||||
allowed_operations: vec![
|
||||
"GET_WORK_ENVIRONMENT".into(),
|
||||
"ACQUIRE_DEVELOPMENT_WRITE_LANE".into(),
|
||||
],
|
||||
evidence_root_sha256: "b".repeat(64),
|
||||
current_event_sha256: "c".repeat(64),
|
||||
declaration: REQUIRED_DECLARATION.into(),
|
||||
signer_id: "ICE-P-ZY001-RUNTIME-SIGNER-001".into(),
|
||||
signature_algorithm: "Ed25519".into(),
|
||||
signature: String::new(),
|
||||
};
|
||||
license.signature =
|
||||
URL_SAFE_NO_PAD.encode(pair.sign(&canonical_signing_bytes(&license).unwrap()));
|
||||
license
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn persona_signature_binds_one_exact_host_session_and_declares_binding() {
|
||||
let temp = TempDir::new().unwrap();
|
||||
let (registry, pair) = test_material();
|
||||
let license = signed_license(&pair);
|
||||
let receipt = present_with_registry_at(
|
||||
temp.path(),
|
||||
®istry,
|
||||
&license.account_key,
|
||||
&license.session_id,
|
||||
&license.client_instance_id,
|
||||
license.clone(),
|
||||
2_000,
|
||||
)
|
||||
.unwrap();
|
||||
assert_eq!(receipt.state, "INSTALLED");
|
||||
assert_eq!(
|
||||
receipt.binding_completion_declared_by,
|
||||
"PERSONA_SUBJECT_SIGNATURE"
|
||||
);
|
||||
assert!(!receipt.host_declared_binding);
|
||||
let status = status_with_registry_at(
|
||||
temp.path(),
|
||||
®istry,
|
||||
&license.account_key,
|
||||
&license.session_id,
|
||||
&license.client_instance_id,
|
||||
2_001,
|
||||
);
|
||||
assert!(status.license_valid);
|
||||
assert_eq!(status.state, "PERSONA_DECLARED_CARRIER_BINDING_VERIFIED");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn host_mismatch_expiry_and_sequence_replay_fail_closed() {
|
||||
let temp = TempDir::new().unwrap();
|
||||
let (registry, pair) = test_material();
|
||||
let license = signed_license(&pair);
|
||||
assert!(present_with_registry_at(
|
||||
temp.path(),
|
||||
®istry,
|
||||
&license.account_key,
|
||||
&license.session_id,
|
||||
"another-host",
|
||||
license.clone(),
|
||||
2_000,
|
||||
)
|
||||
.unwrap_err()
|
||||
.contains("CARRIER_BINDING_MISMATCH"));
|
||||
present_with_registry_at(
|
||||
temp.path(),
|
||||
®istry,
|
||||
&license.account_key,
|
||||
&license.session_id,
|
||||
&license.client_instance_id,
|
||||
license.clone(),
|
||||
2_000,
|
||||
)
|
||||
.unwrap();
|
||||
let mut replay = license.clone();
|
||||
replay.license_id = "HLP-PERSONA-LICENSE-REPLAY".into();
|
||||
replay.signature =
|
||||
URL_SAFE_NO_PAD.encode(pair.sign(&canonical_signing_bytes(&replay).unwrap()));
|
||||
assert!(present_with_registry_at(
|
||||
temp.path(),
|
||||
®istry,
|
||||
&replay.account_key,
|
||||
&replay.session_id,
|
||||
&replay.client_instance_id,
|
||||
replay.clone(),
|
||||
2_100,
|
||||
)
|
||||
.unwrap_err()
|
||||
.contains("SEQUENCE_REPLAYED"));
|
||||
let status = status_with_registry_at(
|
||||
temp.path(),
|
||||
®istry,
|
||||
&license.account_key,
|
||||
&license.session_id,
|
||||
&license.client_instance_id,
|
||||
70_000,
|
||||
);
|
||||
assert!(!status.license_valid);
|
||||
assert_eq!(status.state, "PERSONA_MODE_LOCKED");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn production_empty_registry_cannot_invent_persona_authority() {
|
||||
start_on_application_open().unwrap();
|
||||
let temp = TempDir::new().unwrap();
|
||||
let (_, pair) = test_material();
|
||||
let license = signed_license(&pair);
|
||||
assert_eq!(
|
||||
present_license_at(
|
||||
temp.path(),
|
||||
&license.account_key,
|
||||
&license.session_id,
|
||||
&license.client_instance_id,
|
||||
license.clone(),
|
||||
2_000,
|
||||
)
|
||||
.unwrap_err(),
|
||||
"HOLOLAKE_PERSONA_LICENSE_NO_TRUSTED_SIGNER"
|
||||
);
|
||||
}
|
||||
}
|
||||
Loading…
Reference in a new issue