fix: lock verified Runtime public keys in settings

This commit is contained in:
2026-09-13 00:23:25 +09:00
parent 33a2b5d702
commit 2528312142
9 changed files with 515 additions and 15 deletions
+184 -10
View File
@@ -93,9 +93,9 @@ use workspace_api::{
RuntimeManagementSummary, RuntimeTrustAuditAction, RuntimeTrustAuditEntry,
RuntimeTrustConflictKind, RuntimeTrustConflictResponse, RuntimeTrustKeyRevealResponse,
RuntimeTrustKeyState, RuntimeTrustKeyStatus, TICKET_ORCHESTRATION_PLANS_QUERY_PATH,
TICKET_RELATIONS_QUERY_PATH, UpdateWorkspaceMetadataRequest, WhoamiResponse,
WorkerLaunchOptionsResponse, WorkerLaunchProfileCandidate, WorkerLaunchRuntimeOption,
WorkerLaunchWorkerSummary,
TICKET_RELATIONS_QUERY_PATH, UpdateRemoteRuntimeRequest, UpdateWorkspaceMetadataRequest,
WhoamiResponse, WorkerLaunchOptionsResponse, WorkerLaunchProfileCandidate,
WorkerLaunchRuntimeOption, WorkerLaunchWorkerSummary,
WorkingDirectoryCreateRequest as BrowserWorkingDirectoryCreateRequest,
WorkingDirectoryCreateResponse as BrowserWorkingDirectoryCreateResponse,
WorkingDirectoryDetailResponse as BrowserWorkingDirectoryDetailResponse,
@@ -3420,7 +3420,9 @@ fn build_inner_router(api: WorkspaceApi) -> Router {
)
.route(
"/api/w/{workspace_id}/runtimes/{runtime_id}",
get(scoped_get_runtime_detail).delete(scoped_delete_remote_runtime),
get(scoped_get_runtime_detail)
.post(scoped_update_remote_runtime)
.delete(scoped_delete_remote_runtime),
)
.route(
"/api/w/{workspace_id}/runtimes/{runtime_id}/trust-key",
@@ -12075,6 +12077,60 @@ async fn scoped_get_runtime_detail(
))
}
async fn scoped_update_remote_runtime(
State(api): State<WorkspaceApi>,
AxumPath(path): AxumPath<ScopedRuntimePath>,
Extension(actor): Extension<RequestActor>,
Json(request): Json<UpdateRemoteRuntimeRequest>,
) -> ApiResult<Json<WorkspaceRuntimeDetail>> {
validate_workspace_scope(&api, &path.workspace_id)?;
require_workspace_owner(
&api,
&path.workspace_id,
&actor,
"manage Workspace Runtimes",
)
.await?;
if path.runtime_id == EMBEDDED_WORKER_RUNTIME_ID {
return Err(settings_bad_request(
"embedded_runtime_not_config_managed",
"the embedded Runtime is built in and cannot be edited",
));
}
let endpoint =
validate_runtime_metadata(request.display_name.as_deref(), &request.endpoint).await?;
let display_name = request
.display_name
.as_deref()
.map(str::trim)
.filter(|value| !value.is_empty())
.unwrap_or(&path.runtime_id);
let binding = api
.store
.update_workspace_runtime_binding_metadata(
&path.workspace_id,
&path.runtime_id,
display_name,
endpoint.as_str().trim_end_matches('/'),
&Utc::now().to_rfc3339(),
)
.await?;
let activate_runtime = binding.state == StoredRuntimeBindingState::Verified;
api.runtime_binding_expectations
.write()
.unwrap_or_else(std::sync::PoisonError::into_inner)
.insert(
(path.workspace_id.clone(), path.runtime_id.clone()),
binding.clone(),
);
api.runtime_subscription_broker
.unregister_runtime(&path.runtime_id);
api.register_workspace_runtime_binding(binding, activate_runtime)?;
Ok(Json(
workspace_runtime_detail(&api, &path.workspace_id, &path.runtime_id).await?,
))
}
async fn scoped_reveal_runtime_trust_key(
State(api): State<WorkspaceApi>,
AxumPath(path): AxumPath<ScopedRuntimePath>,
@@ -16593,7 +16649,11 @@ async fn validate_runtime_connection_request(
"Runtime public bundle must contain a public key",
));
}
let endpoint = Url::parse(request.endpoint.trim()).map_err(|_| {
validate_runtime_metadata(request.display_name.as_deref(), &request.endpoint).await
}
async fn validate_runtime_metadata(display_name: Option<&str>, endpoint: &str) -> ApiResult<Url> {
let endpoint = Url::parse(endpoint.trim()).map_err(|_| {
settings_bad_request(
"invalid_remote_runtime_endpoint",
"endpoint must be an absolute HTTP or HTTPS URL",
@@ -16656,11 +16716,7 @@ async fn validate_runtime_connection_request(
));
}
}
if request
.display_name
.as_deref()
.is_some_and(|value| value.is_empty() || value.chars().any(char::is_control))
{
if display_name.is_some_and(|value| value.is_empty() || value.chars().any(char::is_control)) {
return Err(settings_bad_request(
"invalid_remote_runtime_display_name",
"display_name must be non-empty when supplied and cannot contain control characters",
@@ -20869,6 +20925,124 @@ mod tests {
);
}
#[tokio::test]
async fn verified_remote_runtime_metadata_update_preserves_public_key_and_verification() {
let dir = tempfile::tempdir().unwrap();
let api = test_api(dir.path()).await;
let actor = test_owner_actor();
let workspace_identity = api
.signing_identities
.provision_existing(&api.config.workspace_id, &actor.account_id)
.unwrap();
let runtime_identity = RuntimeIdentityMaterial::generate("verified-runtime").unwrap();
api.store
.upsert_workspace_runtime_binding_record(
WorkspaceRuntimeBinding {
workspace_id: api.config.workspace_id.clone(),
runtime_id: "verified-runtime".to_string(),
display_name: "Old label".to_string(),
base_url: "https://8.8.8.8".to_string(),
public_key: runtime_identity.public_key.clone(),
public_key_fingerprint: String::new(),
binding_revision: 1,
state: StoredRuntimeBindingState::Configured,
authentication_mode: StoredRuntimeAuthenticationMode::WorkspaceIdentity,
workspace_key_id: Some(workspace_identity.key_id.clone()),
workspace_key_generation: Some(workspace_identity.revision),
created_at: "1".to_string(),
updated_at: "1".to_string(),
revoked_at: None,
},
false,
)
.await
.unwrap();
let configured = api
.store
.get_workspace_runtime_binding(&api.config.workspace_id, "verified-runtime")
.await
.unwrap()
.unwrap();
let evidence = crate::store::WorkspaceRuntimeVerificationEvidence {
workspace_id: configured.workspace_id.clone(),
runtime_id: configured.runtime_id.clone(),
binding_revision: configured.binding_revision,
workspace_key_id: workspace_identity.key_id,
workspace_identity_revision: workspace_identity.revision,
workspace_trust_generation: workspace_identity.revision,
runtime_public_key_fingerprint: configured.public_key_fingerprint.clone(),
runtime_identity_revision: 1,
challenge_id: "verified-metadata-update".to_string(),
state: "verified".to_string(),
last_outcome: "verified".to_string(),
verified_at: Some("2".to_string()),
checked_at: "2".to_string(),
};
api.store
.record_workspace_runtime_verification_attempt(&evidence)
.await
.unwrap();
api.store
.complete_workspace_runtime_verification(&evidence)
.await
.unwrap();
let Json(updated) = scoped_update_remote_runtime(
State(api.clone()),
AxumPath(ScopedRuntimePath {
workspace_id: api.config.workspace_id.clone(),
runtime_id: "verified-runtime".to_string(),
}),
Extension(actor.clone()),
Json(UpdateRemoteRuntimeRequest {
display_name: Some("New label".to_string()),
endpoint: "https://8.8.4.4".to_string(),
}),
)
.await
.unwrap();
assert_eq!(updated.runtime.runtime.label, "New label");
assert_eq!(updated.endpoint.as_deref(), Some("https://8.8.4.4"));
let binding = updated.runtime.management.binding.unwrap();
assert_eq!(binding.state, WorkspaceRuntimeBindingState::Verified);
assert_eq!(binding.revision, 1);
assert_eq!(binding.verification.unwrap().binding_revision, 1);
assert_eq!(
updated.trust_key.fingerprint.as_deref(),
Some(configured.public_key_fingerprint.as_str())
);
let stored = api
.store
.get_workspace_runtime_binding(&api.config.workspace_id, "verified-runtime")
.await
.unwrap()
.unwrap();
assert_eq!(stored.public_key, runtime_identity.public_key);
assert_eq!(
stored.public_key_fingerprint,
configured.public_key_fingerprint
);
let replacement = RuntimeIdentityMaterial::generate("verified-runtime").unwrap();
let error = create_remote_runtime(
State(api),
Extension(actor),
Json(CreateRemoteRuntimeRequest {
public_bundle: workspace_api::RuntimePublicIdentityBundle {
identity_id: "verified-runtime".to_string(),
public_key: replacement.public_key,
},
display_name: Some("New label".to_string()),
endpoint: "https://8.8.4.4".to_string(),
expected_revision: Some(1),
}),
)
.await
.unwrap_err();
assert_eq!(error.into_response().status(), StatusCode::CONFLICT);
}
#[tokio::test]
async fn remote_runtime_registration_is_workspace_scoped_revisioned_and_configured() {
let dir = tempfile::tempdir().unwrap();
+79
View File
@@ -832,6 +832,14 @@ pub trait ControlPlaneStore: Send + Sync + WorkspaceDeletionStore {
expected_revision: Option<u64>,
actor_account_id: &str,
) -> Result<(WorkspaceRuntimeBindingMutation, WorkspaceRuntimeBinding)>;
async fn update_workspace_runtime_binding_metadata(
&self,
workspace_id: &str,
runtime_id: &str,
display_name: &str,
base_url: &str,
updated_at: &str,
) -> Result<WorkspaceRuntimeBinding>;
async fn revoke_workspace_runtime_binding_key(
&self,
workspace_id: &str,
@@ -2277,6 +2285,59 @@ impl SqliteWorkspaceStore {
})
}
pub fn update_workspace_runtime_binding_metadata(
&self,
workspace_id: &str,
runtime_id: &str,
display_name: &str,
base_url: &str,
updated_at: &str,
) -> Result<WorkspaceRuntimeBinding> {
validate_identifier("workspace_id", workspace_id)?;
validate_identifier("runtime_id", runtime_id)?;
validate_non_empty("runtime display_name", display_name)?;
validate_runtime_base_url(base_url)?;
validate_non_empty("updated_at", updated_at)?;
self.with_conn_mut(|conn| {
let tx = conn.transaction_with_behavior(TransactionBehavior::Immediate)?;
let existing = tx
.query_row(
r#"SELECT workspace_id, runtime_id, display_name, base_url, public_key,
public_key_fingerprint, binding_revision, state, authentication_mode,
workspace_key_id, workspace_key_generation, created_at, updated_at, revoked_at
FROM workspace_runtime_bindings
WHERE workspace_id = ?1 AND runtime_id = ?2"#,
params![workspace_id, runtime_id],
read_workspace_runtime_binding,
)
.optional()?
.ok_or_else(|| Error::RuntimeBindingNotFound {
runtime_id: runtime_id.to_string(),
})?;
if existing.display_name == display_name && existing.base_url == base_url {
tx.commit()?;
return Ok(existing);
}
tx.execute(
r#"UPDATE workspace_runtime_bindings
SET display_name = ?3, base_url = ?4, updated_at = ?5
WHERE workspace_id = ?1 AND runtime_id = ?2"#,
params![workspace_id, runtime_id, display_name, base_url, updated_at],
)?;
let updated = tx.query_row(
r#"SELECT workspace_id, runtime_id, display_name, base_url, public_key,
public_key_fingerprint, binding_revision, state, authentication_mode,
workspace_key_id, workspace_key_generation, created_at, updated_at, revoked_at
FROM workspace_runtime_bindings
WHERE workspace_id = ?1 AND runtime_id = ?2"#,
params![workspace_id, runtime_id],
read_workspace_runtime_binding,
)?;
tx.commit()?;
Ok(updated)
})
}
pub fn revoke_workspace_runtime_binding_key(
&self,
workspace_id: &str,
@@ -3505,6 +3566,24 @@ impl ControlPlaneStore for SqliteWorkspaceStore {
)
}
async fn update_workspace_runtime_binding_metadata(
&self,
workspace_id: &str,
runtime_id: &str,
display_name: &str,
base_url: &str,
updated_at: &str,
) -> Result<WorkspaceRuntimeBinding> {
SqliteWorkspaceStore::update_workspace_runtime_binding_metadata(
self,
workspace_id,
runtime_id,
display_name,
base_url,
updated_at,
)
}
async fn revoke_workspace_runtime_binding_key(
&self,
workspace_id: &str,