fix: authenticate Runtime requests without persisted verification
Use the current Workspace signing identity for each request and remove binding key copies and verification handshake gates. Preserve explicit trust and revocation, migrate historical evidence, and align Runtime settings and connection tests. Retain request scope and durable replay protection with regression coverage.
This commit is contained in:
@@ -817,17 +817,12 @@ mod current_authority {
|
||||
|
||||
fn binding_store() -> (SqliteWorkspaceStore, WorkspaceRuntimeBinding) {
|
||||
let store = SqliteWorkspaceStore::in_memory().unwrap();
|
||||
let workspace_key =
|
||||
worker_runtime::auth::RuntimeIdentityMaterial::generate("workspace-key").unwrap();
|
||||
let runtime_key =
|
||||
worker_runtime::auth::RuntimeIdentityMaterial::generate("runtime-key").unwrap();
|
||||
let workspace_fingerprint = normalize_runtime_public_key(&workspace_key.public_key)
|
||||
.unwrap()
|
||||
.1;
|
||||
store.with_conn(|conn| {
|
||||
conn.execute_batch("INSERT INTO accounts(account_id,kind,handle,display_name,created_at,updated_at) VALUES('owner','user','owner','Owner','created','updated');
|
||||
INSERT INTO workspaces(workspace_id,owner_account_id,display_name,state,created_at,updated_at) VALUES('space','owner','Space','active','created','updated');")?;
|
||||
conn.execute("INSERT INTO workspace_signing_identities(workspace_id,key_id,algorithm,public_key,public_key_fingerprint,private_material_ref,state,created_at,provisioned_at,updated_at) VALUES('space','workspace-key','ed25519',?1,?2,'material','active','created','provisioned','updated')", params![workspace_key.public_key,workspace_fingerprint])?;
|
||||
INSERT INTO workspaces(workspace_id,owner_account_id,display_name,state,created_at,updated_at) VALUES('space','owner','Space','active','created','updated');
|
||||
INSERT INTO workspace_signing_identities(workspace_id,key_id,algorithm,public_key,public_key_fingerprint,private_material_ref,state,created_at,provisioned_at,updated_at) VALUES('space','workspace-key','ed25519','public','sha256:workspace','material','active','created','provisioned','updated');")?;
|
||||
Ok(())
|
||||
}).unwrap();
|
||||
let request = WorkspaceRuntimeBinding {
|
||||
@@ -838,11 +833,7 @@ mod current_authority {
|
||||
public_key: runtime_key.public_key,
|
||||
public_key_fingerprint: String::new(),
|
||||
binding_id: String::new(),
|
||||
state: WorkspaceRuntimeBindingState::Configured,
|
||||
authentication_mode: WorkspaceRuntimeAuthenticationMode::WorkspaceIdentity,
|
||||
workspace_key_id: Some("workspace-key".into()),
|
||||
workspace_public_key_fingerprint: Some(workspace_fingerprint),
|
||||
workspace_trust_id: Some("runtime-issued-trust".into()),
|
||||
created_at: "created".into(),
|
||||
updated_at: "updated".into(),
|
||||
revoked_at: None,
|
||||
@@ -853,36 +844,10 @@ mod current_authority {
|
||||
(store, binding)
|
||||
}
|
||||
|
||||
fn evidence(binding: &WorkspaceRuntimeBinding) -> WorkspaceRuntimeVerificationEvidence {
|
||||
WorkspaceRuntimeVerificationEvidence {
|
||||
workspace_id: binding.workspace_id.clone(),
|
||||
runtime_id: binding.runtime_id.clone(),
|
||||
binding_id: binding.binding_id.clone(),
|
||||
workspace_key_id: binding.workspace_key_id.clone().unwrap(),
|
||||
workspace_public_key_fingerprint: binding
|
||||
.workspace_public_key_fingerprint
|
||||
.clone()
|
||||
.unwrap(),
|
||||
workspace_trust_id: binding.workspace_trust_id.clone().unwrap(),
|
||||
runtime_public_key_fingerprint: binding.public_key_fingerprint.clone(),
|
||||
challenge_id: "challenge".into(),
|
||||
state: "verified".into(),
|
||||
last_outcome: "verified".into(),
|
||||
verified_at: Some("verified-at".into()),
|
||||
checked_at: "checked-at".into(),
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn same_key_reactivation_fences_old_cas_and_verification_evidence() {
|
||||
fn same_key_reactivation_fences_old_config_and_admin_mutations() {
|
||||
let (store, binding) = binding_store();
|
||||
let old_proof = evidence(&binding);
|
||||
store
|
||||
.record_workspace_runtime_verification_attempt(&old_proof)
|
||||
.unwrap();
|
||||
store
|
||||
.complete_workspace_runtime_verification(&old_proof)
|
||||
.unwrap();
|
||||
assert!(store.workspace_runtime_binding_matches(&binding).unwrap());
|
||||
let (_, revoked) = store
|
||||
.revoke_workspace_runtime_binding_key(
|
||||
"space",
|
||||
@@ -893,16 +858,22 @@ mod current_authority {
|
||||
)
|
||||
.unwrap();
|
||||
assert_ne!(binding.binding_id, revoked.binding_id);
|
||||
let mut replacement = binding.clone();
|
||||
replacement.state = WorkspaceRuntimeBindingState::Configured;
|
||||
assert!(!store.workspace_runtime_binding_matches(&binding).unwrap());
|
||||
assert!(!store.workspace_runtime_binding_matches(&revoked).unwrap());
|
||||
let (_, reactivated) = store
|
||||
.put_workspace_runtime_binding_key(replacement, Some(&revoked.binding_id), "owner")
|
||||
.put_workspace_runtime_binding_key(binding.clone(), Some(&revoked.binding_id), "owner")
|
||||
.unwrap();
|
||||
assert_eq!(
|
||||
binding.public_key_fingerprint,
|
||||
reactivated.public_key_fingerprint
|
||||
);
|
||||
assert_ne!(binding.binding_id, reactivated.binding_id);
|
||||
assert!(
|
||||
store
|
||||
.workspace_runtime_binding_matches(&reactivated)
|
||||
.unwrap()
|
||||
);
|
||||
assert!(!store.workspace_runtime_binding_matches(&binding).unwrap());
|
||||
assert!(matches!(
|
||||
store.put_workspace_runtime_binding_key(
|
||||
reactivated.clone(),
|
||||
@@ -921,42 +892,27 @@ mod current_authority {
|
||||
),
|
||||
Err(Error::RuntimeBindingIdConflict { .. })
|
||||
));
|
||||
assert!(
|
||||
store
|
||||
.record_workspace_runtime_verification_attempt(&old_proof)
|
||||
.is_err()
|
||||
);
|
||||
assert!(
|
||||
store
|
||||
.complete_workspace_runtime_verification(&old_proof)
|
||||
.is_err()
|
||||
);
|
||||
assert!(
|
||||
!store
|
||||
.workspace_runtime_verification_matches(
|
||||
&reactivated,
|
||||
&old_proof.workspace_public_key_fingerprint,
|
||||
&old_proof.workspace_trust_id
|
||||
)
|
||||
.unwrap()
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn endpoint_aba_and_workspace_key_change_cannot_reuse_verification_authority() {
|
||||
fn endpoint_aba_fences_old_config_without_copying_workspace_signing_identity() {
|
||||
let (store, binding) = binding_store();
|
||||
let proof = evidence(&binding);
|
||||
store
|
||||
.record_workspace_runtime_verification_attempt(&proof)
|
||||
.unwrap();
|
||||
store
|
||||
.complete_workspace_runtime_verification(&proof)
|
||||
let renamed = store
|
||||
.update_workspace_runtime_binding_metadata(
|
||||
"space",
|
||||
"remote",
|
||||
"Renamed",
|
||||
&binding.base_url,
|
||||
"renamed-at",
|
||||
)
|
||||
.unwrap();
|
||||
assert_eq!(binding.binding_id, renamed.binding_id);
|
||||
assert!(!store.workspace_runtime_binding_matches(&binding).unwrap());
|
||||
let moved = store
|
||||
.update_workspace_runtime_binding_metadata(
|
||||
"space",
|
||||
"remote",
|
||||
"Remote",
|
||||
"Renamed",
|
||||
"https://other.test",
|
||||
"changed",
|
||||
)
|
||||
@@ -965,35 +921,166 @@ mod current_authority {
|
||||
.update_workspace_runtime_binding_metadata(
|
||||
"space",
|
||||
"remote",
|
||||
"Remote",
|
||||
"Renamed",
|
||||
"https://remote.test",
|
||||
"changed-back",
|
||||
)
|
||||
.unwrap();
|
||||
assert_ne!(moved.binding_id, binding.binding_id);
|
||||
assert_ne!(returned.binding_id, binding.binding_id);
|
||||
assert!(
|
||||
store
|
||||
.record_workspace_runtime_verification_attempt(&proof)
|
||||
.is_err()
|
||||
);
|
||||
let current = evidence(&returned);
|
||||
store
|
||||
.record_workspace_runtime_verification_attempt(¤t)
|
||||
.unwrap();
|
||||
store.with_conn(|conn| { conn.execute("UPDATE workspace_signing_identities SET public_key_fingerprint='sha256:replacement' WHERE workspace_id='space'",[])?; Ok(()) }).unwrap();
|
||||
assert!(
|
||||
store
|
||||
.complete_workspace_runtime_verification(¤t)
|
||||
.is_err()
|
||||
);
|
||||
assert!(!store.workspace_runtime_binding_matches(&renamed).unwrap());
|
||||
store.with_conn(|conn| { conn.execute("UPDATE workspace_signing_identities SET public_key_fingerprint='sha256:replacement' WHERE workspace_id='space'", [])?; Ok(()) }).unwrap();
|
||||
assert_eq!(
|
||||
store
|
||||
.get_workspace_runtime_binding("space", "remote")
|
||||
.unwrap(),
|
||||
Some(returned.clone())
|
||||
);
|
||||
assert!(store.workspace_runtime_binding_matches(&returned).unwrap());
|
||||
}
|
||||
|
||||
fn rows(conn: &Connection, sql: &str) -> Vec<Vec<rusqlite::types::Value>> {
|
||||
let mut stmt = conn.prepare(sql).unwrap();
|
||||
let count = stmt.column_count();
|
||||
stmt.query_map([], |row| (0..count).map(|i| row.get(i)).collect())
|
||||
.unwrap()
|
||||
.collect::<rusqlite::Result<Vec<_>>>()
|
||||
.unwrap()
|
||||
}
|
||||
|
||||
fn schema89_bindings() -> Connection {
|
||||
let conn = legacy_binding_database();
|
||||
migrate_runtime_bindings_v87_to_v88(&conn).unwrap();
|
||||
conn.execute(
|
||||
"INSERT INTO __yoi_schema_migrations VALUES(89,'descriptive legacy evidence columns')",
|
||||
[],
|
||||
)
|
||||
.unwrap();
|
||||
conn.execute_batch("INSERT INTO workspace_runtime_bindings VALUES
|
||||
('space','configured-trust','Configured','https://configured.test','key-c','fp-c','binding-c','configured','workspace_identity','workspace-key','sha256:workspace','trust-c','created-c','updated-c',NULL),
|
||||
('space','verified','Verified','https://verified.test','key-v','fp-v','binding-v','verified','workspace_identity','workspace-key','sha256:workspace','trust-v','created-v','updated-v',NULL),
|
||||
('space','revoked-null-trust','Revoked','https://revoked.test','key-r','fp-r','binding-r','revoked','workspace_identity','workspace-key',NULL,NULL,'created-r','updated-r','revoked-r'),
|
||||
('space','legacy-verified','Legacy','https://legacy.test','key-l','fp-l','binding-l','verified','legacy_server_issuer',NULL,NULL,NULL,'created-l','updated-l',NULL),
|
||||
('space','legacy-revoked','Legacy revoked','https://legacy-r.test','key-lr','fp-lr','binding-lr','revoked','legacy_server_issuer',NULL,NULL,NULL,'created-lr','updated-lr','revoked-lr');
|
||||
INSERT INTO workspace_runtime_verifications VALUES
|
||||
('space','verified','binding-v','workspace-key','sha256:workspace','trust-v','fp-v','challenge-v','verified','verified','verified-at','checked-v'),
|
||||
('space','configured-trust','binding-c','workspace-key','sha256:workspace','trust-c','fp-c','challenge-c','pending','challenge_issued',NULL,'checked-c'),
|
||||
('space','revoked-null-trust','binding-r','workspace-key','sha256:workspace','old-trust','fp-r','challenge-r','failed','verification_failed',NULL,'checked-r');").unwrap();
|
||||
conn
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn schema90_preserves_connections_and_archives_all_auth_metadata_without_reenrollment() {
|
||||
let conn = schema89_bindings();
|
||||
let connections = rows(
|
||||
&conn,
|
||||
"SELECT workspace_id,runtime_id,display_name,base_url,public_key,public_key_fingerprint,binding_id,authentication_mode,created_at,updated_at,revoked_at FROM workspace_runtime_bindings ORDER BY runtime_id",
|
||||
);
|
||||
let metadata = rows(
|
||||
&conn,
|
||||
"SELECT workspace_id,runtime_id,binding_id,state,workspace_key_id,workspace_public_key_fingerprint,workspace_trust_id,updated_at FROM workspace_runtime_bindings ORDER BY runtime_id",
|
||||
);
|
||||
let proofs = rows(
|
||||
&conn,
|
||||
"SELECT * FROM workspace_runtime_verifications ORDER BY runtime_id",
|
||||
);
|
||||
let identities = rows(&conn, "SELECT * FROM workspace_signing_identities");
|
||||
let audit = rows(&conn, "SELECT * FROM workspace_runtime_binding_audit");
|
||||
let removal = rows(&conn, "SELECT * FROM runtime_removal_operations");
|
||||
migrate_runtime_binding_connections_v89_to_v90(&conn).unwrap();
|
||||
assert_eq!(current_schema_version(&conn).unwrap(), 90);
|
||||
assert_eq!(
|
||||
rows(
|
||||
&conn,
|
||||
"SELECT * FROM workspace_runtime_bindings ORDER BY runtime_id"
|
||||
),
|
||||
connections
|
||||
);
|
||||
assert_eq!(
|
||||
rows(
|
||||
&conn,
|
||||
"SELECT * FROM workspace_runtime_binding_verification_archive ORDER BY runtime_id"
|
||||
),
|
||||
metadata
|
||||
);
|
||||
assert_eq!(
|
||||
rows(
|
||||
&conn,
|
||||
"SELECT * FROM workspace_runtime_verification_archive ORDER BY runtime_id"
|
||||
),
|
||||
proofs
|
||||
);
|
||||
assert_eq!(
|
||||
rows(&conn, "SELECT * FROM workspace_signing_identities"),
|
||||
identities
|
||||
);
|
||||
assert_eq!(
|
||||
rows(&conn, "SELECT * FROM workspace_runtime_binding_audit"),
|
||||
audit
|
||||
);
|
||||
assert_eq!(
|
||||
rows(&conn, "SELECT * FROM runtime_removal_operations"),
|
||||
removal
|
||||
);
|
||||
verify_runtime_binding_connection_schema(&conn).unwrap();
|
||||
assert!(!table_exists(&conn, "workspace_runtime_verifications").unwrap());
|
||||
assert!(
|
||||
conn.query_row("PRAGMA foreign_keys", [], |r| r.get::<_, bool>(0))
|
||||
.unwrap()
|
||||
);
|
||||
assert_eq!(
|
||||
rows(&conn, "SELECT * FROM pragma_foreign_key_check"),
|
||||
Vec::<Vec<rusqlite::types::Value>>::new()
|
||||
);
|
||||
conn.execute_batch("UPDATE workspace_runtime_bindings SET base_url='https://moved.test',binding_id='binding-new' WHERE runtime_id='verified';
|
||||
UPDATE workspace_runtime_bindings SET revoked_at='admin-revoked' WHERE runtime_id='configured-trust';
|
||||
DELETE FROM workspace_runtime_bindings WHERE runtime_id='legacy-revoked';").unwrap();
|
||||
assert_eq!(
|
||||
rows(
|
||||
&conn,
|
||||
"SELECT * FROM workspace_runtime_binding_verification_archive ORDER BY runtime_id"
|
||||
),
|
||||
metadata
|
||||
);
|
||||
assert_eq!(
|
||||
rows(
|
||||
&conn,
|
||||
"SELECT * FROM workspace_runtime_verification_archive ORDER BY runtime_id"
|
||||
),
|
||||
proofs
|
||||
);
|
||||
conn.execute(
|
||||
"UPDATE runtime_removal_operations SET state='cleanup_pending'",
|
||||
[],
|
||||
)
|
||||
.unwrap();
|
||||
assert!(conn.execute("UPDATE workspace_runtime_bindings SET display_name='Blocked' WHERE runtime_id='remote'", []).unwrap_err().to_string().contains("runtime_removal_in_progress"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn schema90_failed_rebuild_rolls_back_connections_archives_and_schema_marker() {
|
||||
let conn = schema89_bindings();
|
||||
// An incompatible preexisting archive is not permission to discard historical evidence.
|
||||
conn.execute_batch("CREATE TABLE workspace_runtime_verification_archive(existing TEXT);")
|
||||
.unwrap();
|
||||
let before = rows(
|
||||
&conn,
|
||||
"SELECT * FROM workspace_runtime_bindings ORDER BY runtime_id",
|
||||
);
|
||||
assert!(migrate_runtime_binding_connections_v89_to_v90(&conn).is_err());
|
||||
assert_eq!(current_schema_version(&conn).unwrap(), 89);
|
||||
assert_eq!(
|
||||
rows(
|
||||
&conn,
|
||||
"SELECT * FROM workspace_runtime_bindings ORDER BY runtime_id"
|
||||
),
|
||||
before
|
||||
);
|
||||
assert!(!table_exists(&conn, "workspace_runtime_binding_verification_archive").unwrap());
|
||||
assert!(table_exists(&conn, "workspace_runtime_verifications").unwrap());
|
||||
assert!(
|
||||
conn.query_row("PRAGMA foreign_keys", [], |r| r.get::<_, bool>(0))
|
||||
.unwrap()
|
||||
.state,
|
||||
WorkspaceRuntimeBindingState::Configured
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,9 +2,7 @@ use crate::Error;
|
||||
use crate::resource_broker::BackendResourceBroker;
|
||||
#[cfg(test)]
|
||||
use crate::resource_broker::BackendResourceTarget;
|
||||
use crate::store::{
|
||||
ControlPlaneStore, WorkspaceRuntimeAuthenticationMode, WorkspaceRuntimeBindingState,
|
||||
};
|
||||
use crate::store::{ControlPlaneStore, WorkspaceRuntimeAuthenticationMode};
|
||||
use crate::workspace_signing_identity::WorkspaceSigningIdentityService;
|
||||
use chrono::Utc;
|
||||
use protocol::Segment;
|
||||
@@ -66,11 +64,7 @@ use worker_runtime::ssh_host_key_probe::{
|
||||
SSH_HOST_KEY_PROBE_OPERATION, SSH_HOST_KEY_PROBE_PATH, SshHostKeyProbeRequest,
|
||||
SshHostKeyProbeResponse,
|
||||
};
|
||||
use worker_runtime::workspace_issuer::{
|
||||
WorkspaceCapabilityClaims, WorkspaceRuntimeVerificationAcknowledgement,
|
||||
WorkspaceRuntimeVerificationChallenge, WorkspaceRuntimeVerificationReceipt,
|
||||
WorkspaceRuntimeVerificationResponse, workspace_request_body_digest,
|
||||
};
|
||||
use worker_runtime::workspace_issuer::{WorkspaceCapabilityClaims, workspace_request_body_digest};
|
||||
|
||||
pub const EMBEDDED_RUNTIME_ID: &str = "embedded-worker-runtime";
|
||||
const EMBEDDED_HOST_KIND: &str = "embedded-worker-runtime-host";
|
||||
@@ -925,32 +919,6 @@ pub trait WorkspaceWorkerRuntime: Send + Sync {
|
||||
})
|
||||
}
|
||||
|
||||
fn activate_workspace_authorization(&self, _binding: crate::store::WorkspaceRuntimeBinding) {}
|
||||
|
||||
fn send_workspace_verification_challenge(
|
||||
&self,
|
||||
_challenge: &WorkspaceRuntimeVerificationChallenge,
|
||||
_bearer_token: &str,
|
||||
) -> Result<WorkspaceRuntimeVerificationResponse, RuntimePingFailure> {
|
||||
Err(RuntimePingFailure::new(
|
||||
RuntimePingFailureKind::Unsupported,
|
||||
"runtime_workspace_verification_unsupported",
|
||||
"Workspace Runtime verification is unavailable for this Runtime provider",
|
||||
))
|
||||
}
|
||||
|
||||
fn send_workspace_verification_acknowledgement(
|
||||
&self,
|
||||
_acknowledgement: &WorkspaceRuntimeVerificationAcknowledgement,
|
||||
_bearer_token: &str,
|
||||
) -> Result<WorkspaceRuntimeVerificationReceipt, RuntimePingFailure> {
|
||||
Err(RuntimePingFailure::new(
|
||||
RuntimePingFailureKind::Unsupported,
|
||||
"runtime_workspace_verification_unsupported",
|
||||
"Workspace Runtime verification is unavailable for this Runtime provider",
|
||||
))
|
||||
}
|
||||
|
||||
fn list_hosts(&self, limit: usize) -> RuntimeList<InternalHostSummary>;
|
||||
|
||||
fn list_workers(&self, limit: usize) -> RuntimeList<InternalWorkerSummary>;
|
||||
@@ -2277,48 +2245,6 @@ impl RuntimeRegistry {
|
||||
runtime.ping()
|
||||
}
|
||||
|
||||
pub fn activate_workspace_authorization(
|
||||
&self,
|
||||
runtime_id: &str,
|
||||
binding: crate::store::WorkspaceRuntimeBinding,
|
||||
) -> Result<(), RuntimeRegistryError> {
|
||||
self.runtime(runtime_id)?
|
||||
.activate_workspace_authorization(binding);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn send_workspace_verification_challenge(
|
||||
&self,
|
||||
runtime_id: &str,
|
||||
challenge: &WorkspaceRuntimeVerificationChallenge,
|
||||
bearer_token: &str,
|
||||
) -> Result<WorkspaceRuntimeVerificationResponse, RuntimePingFailure> {
|
||||
let runtime = self.runtime(runtime_id).map_err(|_| {
|
||||
RuntimePingFailure::new(
|
||||
RuntimePingFailureKind::Configuration,
|
||||
"runtime_verification_registration_unavailable",
|
||||
"Registered Runtime binding is unavailable",
|
||||
)
|
||||
})?;
|
||||
runtime.send_workspace_verification_challenge(challenge, bearer_token)
|
||||
}
|
||||
|
||||
pub fn send_workspace_verification_acknowledgement(
|
||||
&self,
|
||||
runtime_id: &str,
|
||||
acknowledgement: &WorkspaceRuntimeVerificationAcknowledgement,
|
||||
bearer_token: &str,
|
||||
) -> Result<WorkspaceRuntimeVerificationReceipt, RuntimePingFailure> {
|
||||
let runtime = self.runtime(runtime_id).map_err(|_| {
|
||||
RuntimePingFailure::new(
|
||||
RuntimePingFailureKind::Configuration,
|
||||
"runtime_verification_registration_unavailable",
|
||||
"Registered Runtime binding is unavailable",
|
||||
)
|
||||
})?;
|
||||
runtime.send_workspace_verification_acknowledgement(acknowledgement, bearer_token)
|
||||
}
|
||||
|
||||
fn runtimes_snapshot(&self) -> Vec<Arc<dyn WorkspaceWorkerRuntime>> {
|
||||
self.runtimes
|
||||
.read()
|
||||
@@ -3518,7 +3444,9 @@ pub struct WorkspaceRuntimeAuthorization {
|
||||
store: Arc<dyn ControlPlaneStore>,
|
||||
signing_identities: WorkspaceSigningIdentityService,
|
||||
backend_url: String,
|
||||
binding: Arc<RwLock<Option<crate::store::WorkspaceRuntimeBinding>>>,
|
||||
// The connection selected for this client. Compare it with the current
|
||||
// administrative configuration before sending, never with a past auth result.
|
||||
binding: crate::store::WorkspaceRuntimeBinding,
|
||||
}
|
||||
|
||||
impl std::fmt::Debug for WorkspaceRuntimeAuthorization {
|
||||
@@ -3535,19 +3463,13 @@ impl WorkspaceRuntimeAuthorization {
|
||||
store: Arc<dyn ControlPlaneStore>,
|
||||
signing_identities: WorkspaceSigningIdentityService,
|
||||
backend_url: impl Into<String>,
|
||||
binding: Option<crate::store::WorkspaceRuntimeBinding>,
|
||||
binding: crate::store::WorkspaceRuntimeBinding,
|
||||
) -> Self {
|
||||
Self {
|
||||
store,
|
||||
signing_identities,
|
||||
backend_url: backend_url.into(),
|
||||
binding: Arc::new(RwLock::new(binding)),
|
||||
}
|
||||
}
|
||||
|
||||
fn activate(&self, binding: crate::store::WorkspaceRuntimeBinding) {
|
||||
if let Ok(mut current) = self.binding.write() {
|
||||
*current = Some(binding);
|
||||
binding,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3559,30 +3481,14 @@ impl WorkspaceRuntimeAuthorization {
|
||||
worker_id: Option<&str>,
|
||||
body: &[u8],
|
||||
) -> Result<String, RuntimeDiagnostic> {
|
||||
let binding = self
|
||||
.binding
|
||||
.read()
|
||||
.map_err(|_| {
|
||||
diagnostic(
|
||||
"workspace_runtime_authorization_unavailable",
|
||||
HostDiagnosticSeverity::Error,
|
||||
"Workspace Runtime authorization is unavailable".to_string(),
|
||||
)
|
||||
})?
|
||||
.clone()
|
||||
.ok_or_else(|| {
|
||||
diagnostic(
|
||||
"workspace_runtime_verification_required",
|
||||
HostDiagnosticSeverity::Error,
|
||||
"Workspace Runtime binding is not verified".to_string(),
|
||||
)
|
||||
})?;
|
||||
if binding.state != WorkspaceRuntimeBindingState::Verified
|
||||
|| binding.authentication_mode != WorkspaceRuntimeAuthenticationMode::WorkspaceIdentity
|
||||
let binding = &self.binding;
|
||||
// A removed/replaced endpoint must not be used by an old client. This
|
||||
// compares connection configuration, not cached authentication success.
|
||||
if binding.authentication_mode != WorkspaceRuntimeAuthenticationMode::WorkspaceIdentity
|
||||
|| binding.revoked_at.is_some()
|
||||
|| !self
|
||||
.store
|
||||
.workspace_runtime_binding_matches(&binding)
|
||||
.workspace_runtime_binding_matches(binding)
|
||||
.map_err(|error| {
|
||||
diagnostic(
|
||||
"workspace_runtime_authorization_unavailable",
|
||||
@@ -3594,7 +3500,7 @@ impl WorkspaceRuntimeAuthorization {
|
||||
return Err(diagnostic(
|
||||
"workspace_runtime_authorization_stale",
|
||||
HostDiagnosticSeverity::Error,
|
||||
"Workspace Runtime binding changed or was revoked".to_string(),
|
||||
"Workspace Runtime connection changed or was revoked".to_string(),
|
||||
));
|
||||
}
|
||||
let identity = self
|
||||
@@ -3607,63 +3513,26 @@ impl WorkspaceRuntimeAuthorization {
|
||||
error.to_string(),
|
||||
)
|
||||
})?;
|
||||
let workspace_key_id = binding.workspace_key_id.as_deref().ok_or_else(|| {
|
||||
diagnostic(
|
||||
"workspace_runtime_authorization_invalid",
|
||||
HostDiagnosticSeverity::Error,
|
||||
"Workspace Runtime binding is missing its Workspace key".to_string(),
|
||||
)
|
||||
})?;
|
||||
let workspace_public_key_fingerprint = binding
|
||||
.workspace_public_key_fingerprint
|
||||
.as_deref()
|
||||
.ok_or_else(|| {
|
||||
diagnostic(
|
||||
"workspace_runtime_authorization_invalid",
|
||||
HostDiagnosticSeverity::Error,
|
||||
"Workspace Runtime binding is missing its Workspace public key fingerprint"
|
||||
.to_string(),
|
||||
)
|
||||
})?;
|
||||
let workspace_trust_id = binding.workspace_trust_id.as_deref().ok_or_else(|| {
|
||||
diagnostic(
|
||||
"workspace_runtime_authorization_invalid",
|
||||
HostDiagnosticSeverity::Error,
|
||||
"Workspace Runtime binding is missing its Workspace trust ID".to_string(),
|
||||
)
|
||||
})?;
|
||||
if identity.state != "active"
|
||||
|| identity.key_id != workspace_key_id
|
||||
|| identity.public_key_fingerprint.as_deref() != Some(workspace_public_key_fingerprint)
|
||||
|| !self
|
||||
.store
|
||||
.workspace_runtime_verification_matches(
|
||||
&binding,
|
||||
workspace_public_key_fingerprint,
|
||||
workspace_trust_id,
|
||||
)
|
||||
.map_err(|error| {
|
||||
diagnostic(
|
||||
"workspace_runtime_authorization_unavailable",
|
||||
HostDiagnosticSeverity::Error,
|
||||
error.to_string(),
|
||||
)
|
||||
})?
|
||||
{
|
||||
if identity.state != "active" {
|
||||
return Err(diagnostic(
|
||||
"workspace_runtime_authorization_stale",
|
||||
"workspace_signing_identity_unavailable",
|
||||
HostDiagnosticSeverity::Error,
|
||||
"Workspace signing identity no longer matches the verified binding".to_string(),
|
||||
"Workspace signing identity is not active".to_string(),
|
||||
));
|
||||
}
|
||||
let fingerprint = identity.public_key_fingerprint.ok_or_else(|| {
|
||||
diagnostic(
|
||||
"workspace_signing_identity_unavailable",
|
||||
HostDiagnosticSeverity::Error,
|
||||
"Workspace signing identity has no public key fingerprint".to_string(),
|
||||
)
|
||||
})?;
|
||||
let now = Utc::now().timestamp();
|
||||
let claims = WorkspaceCapabilityClaims {
|
||||
issuer: self.backend_url.clone(),
|
||||
issuer_workspace_id: binding.workspace_id.clone(),
|
||||
issuer_key_id: identity.key_id,
|
||||
issuer_public_key_fingerprint: workspace_public_key_fingerprint.to_string(),
|
||||
trust_id: workspace_trust_id.to_string(),
|
||||
binding_id: binding.binding_id.clone(),
|
||||
issuer_public_key_fingerprint: fingerprint,
|
||||
runtime_id: binding.runtime_id.clone(),
|
||||
worker_id: worker_id.map(str::to_string),
|
||||
operation: operation.to_string(),
|
||||
@@ -3674,6 +3543,8 @@ impl WorkspaceRuntimeAuthorization {
|
||||
exp: now.saturating_add(60),
|
||||
jti: uuid::Uuid::now_v7().to_string(),
|
||||
};
|
||||
// The signer validates that these claims still name the selected current
|
||||
// identity; a concurrent rotation cannot silently sign mismatched claims.
|
||||
self.signing_identities
|
||||
.issue_workspace_capability(&binding.workspace_id, &claims)
|
||||
.map_err(|error| {
|
||||
@@ -4251,49 +4122,6 @@ impl RemoteWorkerRuntime {
|
||||
format!("{base}/v1/workers/{worker_id}/protocol/ws")
|
||||
}
|
||||
|
||||
fn post_bearer_json<T, U>(
|
||||
&self,
|
||||
path: &str,
|
||||
body: &T,
|
||||
bearer_token: &str,
|
||||
) -> Result<U, RuntimePingFailure>
|
||||
where
|
||||
T: Serialize + ?Sized,
|
||||
U: DeserializeOwned,
|
||||
{
|
||||
let response = self
|
||||
.http
|
||||
.post(self.endpoint(path))
|
||||
.bearer_auth(bearer_token)
|
||||
.json(body)
|
||||
.send()
|
||||
.map_err(|error| {
|
||||
RuntimePingFailure::new(
|
||||
RuntimePingFailureKind::NetworkUnreachable,
|
||||
"runtime_workspace_verification_unreachable",
|
||||
format!("Runtime verification request failed: {error}"),
|
||||
)
|
||||
})?;
|
||||
let status = response.status();
|
||||
if !status.is_success() {
|
||||
return Err(RuntimePingFailure::new(
|
||||
RuntimePingFailureKind::MalformedResponse,
|
||||
"runtime_workspace_verification_rejected",
|
||||
format!(
|
||||
"Runtime verification request returned HTTP {}",
|
||||
status.as_u16()
|
||||
),
|
||||
));
|
||||
}
|
||||
response.json::<U>().map_err(|error| {
|
||||
RuntimePingFailure::new(
|
||||
RuntimePingFailureKind::MalformedResponse,
|
||||
"runtime_workspace_verification_invalid_response",
|
||||
format!("Runtime verification response was invalid: {error}"),
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
fn get_json<T>(&self, path: &str) -> Result<T, RuntimeDiagnostic>
|
||||
where
|
||||
T: DeserializeOwned + Send + 'static,
|
||||
@@ -4702,36 +4530,6 @@ impl WorkspaceWorkerRuntime for RemoteWorkerRuntime {
|
||||
})
|
||||
}
|
||||
|
||||
fn activate_workspace_authorization(&self, binding: crate::store::WorkspaceRuntimeBinding) {
|
||||
if let Some(authorization) = &self.workspace_authorization {
|
||||
authorization.activate(binding);
|
||||
}
|
||||
}
|
||||
|
||||
fn send_workspace_verification_challenge(
|
||||
&self,
|
||||
challenge: &WorkspaceRuntimeVerificationChallenge,
|
||||
bearer_token: &str,
|
||||
) -> Result<WorkspaceRuntimeVerificationResponse, RuntimePingFailure> {
|
||||
self.post_bearer_json(
|
||||
worker_runtime::workspace_issuer::WORKSPACE_VERIFICATION_CHALLENGE_PATH,
|
||||
challenge,
|
||||
bearer_token,
|
||||
)
|
||||
}
|
||||
|
||||
fn send_workspace_verification_acknowledgement(
|
||||
&self,
|
||||
acknowledgement: &WorkspaceRuntimeVerificationAcknowledgement,
|
||||
bearer_token: &str,
|
||||
) -> Result<WorkspaceRuntimeVerificationReceipt, RuntimePingFailure> {
|
||||
self.post_bearer_json(
|
||||
worker_runtime::workspace_issuer::WORKSPACE_VERIFICATION_ACK_PATH,
|
||||
acknowledgement,
|
||||
bearer_token,
|
||||
)
|
||||
}
|
||||
|
||||
fn list_hosts(&self, limit: usize) -> RuntimeList<InternalHostSummary> {
|
||||
if limit == 0 {
|
||||
return RuntimeList::new(Vec::new(), Vec::new());
|
||||
@@ -6536,6 +6334,7 @@ pub fn placeholder_spawn_response(host_id: impl Into<String>) -> WorkerSpawnResu
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
mod restore_auth_tests;
|
||||
mod workspace_auth_tests;
|
||||
|
||||
use super::*;
|
||||
use serde_json::json;
|
||||
|
||||
@@ -9,36 +9,18 @@ use worker_runtime::{
|
||||
auth::RuntimeIdentityMaterial,
|
||||
http_server::{WorkspaceRuntimeHttpAuth, runtime_http_router_with_workspace_auth},
|
||||
workspace_issuer::{
|
||||
InMemoryWorkspaceClaimReplayProtection, InMemoryWorkspaceRuntimeVerificationAuthority,
|
||||
RuntimeVerificationSigner, WorkspaceCapabilityVerifier, WorkspaceIssuerTrustRecord,
|
||||
WorkspaceIssuerTrustState, WorkspaceRuntimeVerificationAuthority,
|
||||
WorkspaceRuntimeVerificationRecord, issue_workspace_capability_token,
|
||||
InMemoryWorkspaceClaimReplayProtection, WorkspaceCapabilityVerifier,
|
||||
WorkspaceIssuerTrustRecord, WorkspaceIssuerTrustState, issue_workspace_capability_token,
|
||||
},
|
||||
};
|
||||
|
||||
#[tokio::test]
|
||||
async fn restore_capabilities_authorize_runtime_routes_without_relaxing_request_binding() {
|
||||
let workspace_identity = RuntimeIdentityMaterial::generate("workspace-key").unwrap();
|
||||
let runtime_identity = RuntimeIdentityMaterial::generate("runtime-test").unwrap();
|
||||
let signer = RuntimeVerificationSigner::from_identity(&runtime_identity).unwrap();
|
||||
let now = Utc::now().timestamp();
|
||||
let workspace_public_key_fingerprint =
|
||||
crate::workspace_signing_identity::public_key_fingerprint(&workspace_identity.public_key)
|
||||
.unwrap();
|
||||
let binding_id = "binding-restore-test".to_string();
|
||||
let verifications = Arc::new(InMemoryWorkspaceRuntimeVerificationAuthority::default());
|
||||
verifications
|
||||
.record(WorkspaceRuntimeVerificationRecord {
|
||||
workspace_id: "workspace-test".into(),
|
||||
runtime_id: "runtime-test".into(),
|
||||
binding_id: binding_id.clone(),
|
||||
workspace_trust_id: "trust-restore-test".to_string(),
|
||||
workspace_key_id: "workspace-key".into(),
|
||||
workspace_public_key_fingerprint: workspace_public_key_fingerprint.clone(),
|
||||
runtime_public_key_fingerprint: signer.public_key_fingerprint().to_owned(),
|
||||
verified_at: now,
|
||||
})
|
||||
.unwrap();
|
||||
let public_key =
|
||||
worker_runtime::auth::decode_public_key(&workspace_identity.public_key).unwrap();
|
||||
let verifier = WorkspaceCapabilityVerifier::new(
|
||||
@@ -69,8 +51,7 @@ async fn restore_capabilities_authorize_runtime_routes_without_relaxing_request_
|
||||
None,
|
||||
WorkspaceRuntimeHttpAuth {
|
||||
verifier,
|
||||
signer,
|
||||
verifications,
|
||||
runtime_id: "runtime-test".into(),
|
||||
},
|
||||
);
|
||||
let worker_id = EmbeddedWorkerId::from_legacy_u64(1).to_string();
|
||||
@@ -93,8 +74,6 @@ async fn restore_capabilities_authorize_runtime_routes_without_relaxing_request_
|
||||
"wrong-worker",
|
||||
"wrong-body",
|
||||
"wrong-key-fingerprint",
|
||||
"wrong-binding-id",
|
||||
"wrong-trust-id",
|
||||
] {
|
||||
let claims = WorkspaceCapabilityClaims {
|
||||
issuer: "https://backend.test".into(),
|
||||
@@ -111,16 +90,6 @@ async fn restore_capabilities_authorize_runtime_routes_without_relaxing_request_
|
||||
} else {
|
||||
workspace_public_key_fingerprint.clone()
|
||||
},
|
||||
binding_id: if case == "wrong-binding-id" {
|
||||
"binding-retired".to_string()
|
||||
} else {
|
||||
binding_id.clone()
|
||||
},
|
||||
trust_id: if case == "wrong-trust-id" {
|
||||
"trust-retired".to_string()
|
||||
} else {
|
||||
"trust-restore-test".to_string()
|
||||
},
|
||||
runtime_id: "runtime-test".into(),
|
||||
worker_id: if case == "wrong-worker" {
|
||||
Some(EmbeddedWorkerId::from_legacy_u64(2).to_string())
|
||||
@@ -176,27 +145,16 @@ async fn restore_capabilities_authorize_runtime_routes_without_relaxing_request_
|
||||
} else {
|
||||
assert_eq!(
|
||||
status,
|
||||
if case == "wrong-binding-id" {
|
||||
StatusCode::FORBIDDEN
|
||||
} else {
|
||||
StatusCode::UNAUTHORIZED
|
||||
},
|
||||
StatusCode::UNAUTHORIZED,
|
||||
"{path} {case}: {}",
|
||||
error.error.message
|
||||
);
|
||||
assert_eq!(
|
||||
error.error.code,
|
||||
if case == "wrong-binding-id" {
|
||||
"workspace_runtime_verification_stale"
|
||||
} else {
|
||||
"unauthorized"
|
||||
}
|
||||
);
|
||||
assert_eq!(error.error.code, "unauthorized");
|
||||
let expected = match case {
|
||||
"wrong-operation" => Some("does not authorize this operation"),
|
||||
"wrong-worker" => Some("targets another Worker"),
|
||||
"wrong-body" => Some("does not bind this request body"),
|
||||
"wrong-key-fingerprint" | "wrong-binding-id" | "wrong-trust-id" => None,
|
||||
"wrong-key-fingerprint" => None,
|
||||
_ => unreachable!(),
|
||||
};
|
||||
if let Some(expected) = expected {
|
||||
|
||||
@@ -0,0 +1,316 @@
|
||||
use super::*;
|
||||
use crate::store::{
|
||||
AccountRecord, SqliteWorkspaceStore, WorkspaceRecord, WorkspaceRuntimeBinding,
|
||||
WorkspaceSigningIdentityRecord,
|
||||
};
|
||||
use crate::workspace_signing_identity::{
|
||||
InMemoryWorkspaceSigningMaterialStore, WorkspaceSigningMaterialStore,
|
||||
WorkspaceSigningPrivateMaterial, public_key_fingerprint,
|
||||
};
|
||||
use worker_runtime::auth::RuntimeIdentityMaterial;
|
||||
use worker_runtime::workspace_issuer::{
|
||||
InMemoryWorkspaceClaimReplayProtection, VerifiedWorkspaceCapability,
|
||||
WorkspaceCapabilityExpectation, WorkspaceCapabilityVerificationError,
|
||||
WorkspaceCapabilityVerifier, WorkspaceIssuerTrustRecord, WorkspaceIssuerTrustState,
|
||||
};
|
||||
|
||||
const WORKSPACE: &str = "workspace-auth-test";
|
||||
const RUNTIME: &str = "runtime-auth-test";
|
||||
const BACKEND: &str = "https://backend.test";
|
||||
const PATH: &str = "/v1/ping?probe=1";
|
||||
|
||||
struct Fixture {
|
||||
_temp: tempfile::TempDir,
|
||||
store: Arc<SqliteWorkspaceStore>,
|
||||
materials: Arc<InMemoryWorkspaceSigningMaterialStore>,
|
||||
service: WorkspaceSigningIdentityService,
|
||||
authorizer: WorkspaceRuntimeAuthorization,
|
||||
binding: WorkspaceRuntimeBinding,
|
||||
identity: WorkspaceSigningIdentityRecord,
|
||||
}
|
||||
|
||||
impl Fixture {
|
||||
async fn new() -> Self {
|
||||
let temp = tempfile::tempdir().unwrap();
|
||||
let store = Arc::new(SqliteWorkspaceStore::open(temp.path().join("server.db")).unwrap());
|
||||
store
|
||||
.upsert_account(&AccountRecord {
|
||||
account_id: "owner".into(),
|
||||
kind: "user".into(),
|
||||
handle: "owner".into(),
|
||||
display_name: "Owner".into(),
|
||||
created_at: "1".into(),
|
||||
updated_at: "1".into(),
|
||||
})
|
||||
.unwrap();
|
||||
store
|
||||
.upsert_workspace(&WorkspaceRecord {
|
||||
workspace_id: WORKSPACE.into(),
|
||||
owner_account_id: "owner".into(),
|
||||
display_name: "Workspace".into(),
|
||||
state: "active".into(),
|
||||
created_at: "1".into(),
|
||||
updated_at: "1".into(),
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
let materials = Arc::new(InMemoryWorkspaceSigningMaterialStore::default());
|
||||
let service = WorkspaceSigningIdentityService::new(store.clone(), materials.clone());
|
||||
let identity = service.provision_existing(WORKSPACE, "owner").unwrap();
|
||||
let runtime_identity = RuntimeIdentityMaterial::generate(RUNTIME).unwrap();
|
||||
store
|
||||
.upsert_workspace_runtime_binding(
|
||||
WorkspaceRuntimeBinding {
|
||||
workspace_id: WORKSPACE.into(),
|
||||
runtime_id: RUNTIME.into(),
|
||||
display_name: "Runtime".into(),
|
||||
base_url: "https://runtime.test".into(),
|
||||
public_key_fingerprint: public_key_fingerprint(&runtime_identity.public_key)
|
||||
.unwrap(),
|
||||
public_key: runtime_identity.public_key,
|
||||
binding_id: String::new(),
|
||||
authentication_mode: WorkspaceRuntimeAuthenticationMode::WorkspaceIdentity,
|
||||
created_at: "1".into(),
|
||||
updated_at: "1".into(),
|
||||
revoked_at: None,
|
||||
},
|
||||
false,
|
||||
)
|
||||
.unwrap();
|
||||
let binding = store
|
||||
.get_workspace_runtime_binding(WORKSPACE, RUNTIME)
|
||||
.unwrap()
|
||||
.unwrap();
|
||||
let authorizer = WorkspaceRuntimeAuthorization::new(
|
||||
store.clone(),
|
||||
service.clone(),
|
||||
BACKEND,
|
||||
binding.clone(),
|
||||
);
|
||||
Self {
|
||||
_temp: temp,
|
||||
store,
|
||||
materials,
|
||||
service,
|
||||
authorizer,
|
||||
binding,
|
||||
identity,
|
||||
}
|
||||
}
|
||||
|
||||
fn issue(&self) -> Result<String, RuntimeDiagnostic> {
|
||||
self.authorizer
|
||||
.issue("GET", PATH, RUNTIME_PING_PERMISSION, None, b"")
|
||||
}
|
||||
|
||||
fn verifier(&self) -> WorkspaceCapabilityVerifier {
|
||||
WorkspaceCapabilityVerifier::new(
|
||||
vec![WorkspaceIssuerTrustRecord {
|
||||
workspace_id: WORKSPACE.into(),
|
||||
backend_url: BACKEND.into(),
|
||||
key_id: self.identity.key_id.clone(),
|
||||
algorithm: "ed25519".into(),
|
||||
public_key: self.identity.public_key.clone().unwrap(),
|
||||
public_key_fingerprint: self.identity.public_key_fingerprint.clone().unwrap(),
|
||||
trust_id: uuid::Uuid::now_v7().to_string(),
|
||||
state: WorkspaceIssuerTrustState::Active,
|
||||
registered_at_unix: Utc::now().timestamp(),
|
||||
updated_at_unix: Utc::now().timestamp(),
|
||||
}],
|
||||
Arc::new(InMemoryWorkspaceClaimReplayProtection::default()),
|
||||
)
|
||||
.unwrap()
|
||||
}
|
||||
|
||||
// Fixture-only rotation: deliberately no production rotation API or live database.
|
||||
fn rotate_identity(&mut self) {
|
||||
let key_id = "WK-rotated";
|
||||
let material_ref = format!("workspace-signing/{WORKSPACE}/{key_id}");
|
||||
let material = WorkspaceSigningPrivateMaterial::generate(WORKSPACE, key_id).unwrap();
|
||||
let public_key = material.validate_and_public_key(WORKSPACE, key_id).unwrap();
|
||||
let fingerprint = public_key_fingerprint(&public_key).unwrap();
|
||||
self.materials
|
||||
.put_if_absent(&material_ref, &material)
|
||||
.unwrap();
|
||||
self.store.with_conn(|conn| {
|
||||
conn.execute("UPDATE workspace_signing_identities SET key_id = ?1, public_key = ?2, public_key_fingerprint = ?3, private_material_ref = ?4 WHERE workspace_id = ?5",
|
||||
rusqlite::params![key_id, public_key, fingerprint, material_ref, WORKSPACE])?;
|
||||
Ok(())
|
||||
}).unwrap();
|
||||
self.identity = self.service.get_validated(WORKSPACE).unwrap();
|
||||
}
|
||||
}
|
||||
|
||||
fn verify(
|
||||
verifier: &WorkspaceCapabilityVerifier,
|
||||
token: &str,
|
||||
) -> Result<VerifiedWorkspaceCapability, WorkspaceCapabilityVerificationError> {
|
||||
verifier.verify(
|
||||
token,
|
||||
&WorkspaceCapabilityExpectation {
|
||||
workspace_id: WORKSPACE,
|
||||
runtime_id: RUNTIME,
|
||||
worker_id: None,
|
||||
operation: RUNTIME_PING_PERMISSION,
|
||||
method: "GET",
|
||||
path_and_query: PATH,
|
||||
body_digest: &workspace_request_body_digest(b""),
|
||||
now_unix: Utc::now().timestamp(),
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn authorizer_token_authenticates_actual_request_without_prior_verification_record() {
|
||||
let fixture = Fixture::new().await;
|
||||
let token = fixture.issue().unwrap();
|
||||
let verifier = fixture.verifier();
|
||||
let authenticated = verify(&verifier, &token).unwrap();
|
||||
assert_eq!(authenticated.workspace_id, WORKSPACE);
|
||||
assert_eq!(authenticated.issuer_key_id, fixture.identity.key_id);
|
||||
assert_eq!(
|
||||
verify(&verifier, &token).unwrap_err(),
|
||||
WorkspaceCapabilityVerificationError::Replay
|
||||
);
|
||||
let token = fixture.issue().unwrap();
|
||||
let wrong_request = WorkspaceCapabilityExpectation {
|
||||
workspace_id: WORKSPACE,
|
||||
runtime_id: RUNTIME,
|
||||
worker_id: None,
|
||||
operation: RUNTIME_PING_PERMISSION,
|
||||
method: "GET",
|
||||
path_and_query: "/v1/ping?probe=2",
|
||||
body_digest: &workspace_request_body_digest(b""),
|
||||
now_unix: Utc::now().timestamp(),
|
||||
};
|
||||
assert_eq!(
|
||||
verifier.verify(&token, &wrong_request).unwrap_err(),
|
||||
WorkspaceCapabilityVerificationError::WrongPathAndQuery
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn existing_authorizer_selects_rotated_workspace_key_and_new_runtime_trust() {
|
||||
let mut fixture = Fixture::new().await;
|
||||
let old_verifier = fixture.verifier();
|
||||
let old_token = fixture.issue().unwrap();
|
||||
fixture.rotate_identity();
|
||||
let new_token = fixture.issue().unwrap();
|
||||
let new_verifier = fixture.verifier();
|
||||
assert_eq!(
|
||||
verify(&new_verifier, &new_token).unwrap().issuer_key_id,
|
||||
"WK-rotated"
|
||||
);
|
||||
assert_eq!(
|
||||
verify(&new_verifier, &old_token).unwrap_err(),
|
||||
WorkspaceCapabilityVerificationError::WrongIssuerKey
|
||||
);
|
||||
assert_eq!(
|
||||
verify(&old_verifier, &new_token).unwrap_err(),
|
||||
WorkspaceCapabilityVerificationError::WrongIssuerKey
|
||||
);
|
||||
// Changing Workspace authority did not replace or copy metadata into the connection.
|
||||
assert_eq!(
|
||||
fixture
|
||||
.store
|
||||
.get_workspace_runtime_binding(WORKSPACE, RUNTIME)
|
||||
.unwrap()
|
||||
.unwrap(),
|
||||
fixture.binding
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn replaced_or_revoked_connection_rejects_stale_authorizer() {
|
||||
for revoked in [false, true] {
|
||||
let fixture = Fixture::new().await;
|
||||
fixture.issue().unwrap();
|
||||
if revoked {
|
||||
fixture
|
||||
.store
|
||||
.revoke_workspace_runtime_binding(WORKSPACE, RUNTIME, "2")
|
||||
.unwrap();
|
||||
} else {
|
||||
let mut replacement = fixture.binding.clone();
|
||||
replacement.base_url = "https://replacement-runtime.test".into();
|
||||
fixture
|
||||
.store
|
||||
.upsert_workspace_runtime_binding(replacement, true)
|
||||
.unwrap();
|
||||
}
|
||||
assert_eq!(
|
||||
fixture.issue().unwrap_err().code,
|
||||
"workspace_runtime_authorization_stale"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn missing_workspace_signing_material_rejects_authorization_without_fallback() {
|
||||
let fixture = Fixture::new().await;
|
||||
fixture
|
||||
.materials
|
||||
.delete(&fixture.identity.private_material_ref)
|
||||
.unwrap();
|
||||
let error = fixture.issue().unwrap_err();
|
||||
assert_eq!(error.code, "workspace_runtime_authorization_unavailable");
|
||||
assert!(
|
||||
error.message.contains("private material is missing"),
|
||||
"{}",
|
||||
error.message
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn capability_claims_must_match_current_workspace_identity_before_signing() {
|
||||
let mut fixture = Fixture::new().await;
|
||||
let now = Utc::now().timestamp();
|
||||
let mut claims = WorkspaceCapabilityClaims {
|
||||
issuer: BACKEND.into(),
|
||||
issuer_workspace_id: WORKSPACE.into(),
|
||||
issuer_key_id: fixture.identity.key_id.clone(),
|
||||
issuer_public_key_fingerprint: fixture.identity.public_key_fingerprint.clone().unwrap(),
|
||||
runtime_id: RUNTIME.into(),
|
||||
worker_id: None,
|
||||
operation: RUNTIME_PING_PERMISSION.into(),
|
||||
method: "GET".into(),
|
||||
path_and_query: PATH.into(),
|
||||
body_digest: workspace_request_body_digest(b""),
|
||||
iat: now,
|
||||
exp: now + 60,
|
||||
jti: uuid::Uuid::now_v7().to_string(),
|
||||
};
|
||||
let token = fixture
|
||||
.service
|
||||
.issue_workspace_capability(WORKSPACE, &claims)
|
||||
.unwrap();
|
||||
verify(&fixture.verifier(), &token).unwrap();
|
||||
for field in ["workspace", "key", "fingerprint"] {
|
||||
let mut mismatched = claims.clone();
|
||||
match field {
|
||||
"workspace" => mismatched.issuer_workspace_id = "another-workspace".into(),
|
||||
"key" => mismatched.issuer_key_id = "WK-another".into(),
|
||||
"fingerprint" => {
|
||||
mismatched.issuer_public_key_fingerprint = format!("sha256:{}", "0".repeat(64))
|
||||
}
|
||||
_ => unreachable!(),
|
||||
}
|
||||
assert!(
|
||||
matches!(fixture.service.issue_workspace_capability(WORKSPACE, &mismatched),
|
||||
Err(Error::WorkspaceSigningIdentity { ref code, .. }) if code == "workspace_capability_identity_mismatch"),
|
||||
"{field}"
|
||||
);
|
||||
}
|
||||
fixture.rotate_identity();
|
||||
assert!(
|
||||
matches!(fixture.service.issue_workspace_capability(WORKSPACE, &claims),
|
||||
Err(Error::WorkspaceSigningIdentity { ref code, .. }) if code == "workspace_capability_identity_mismatch")
|
||||
);
|
||||
claims.issuer_key_id = fixture.identity.key_id.clone();
|
||||
claims.issuer_public_key_fingerprint = fixture.identity.public_key_fingerprint.clone().unwrap();
|
||||
let token = fixture
|
||||
.service
|
||||
.issue_workspace_capability(WORKSPACE, &claims)
|
||||
.unwrap();
|
||||
verify(&fixture.verifier(), &token).unwrap();
|
||||
}
|
||||
@@ -442,49 +442,43 @@ CREATE TABLE workspace_runtime_bindings (
|
||||
public_key TEXT NOT NULL,
|
||||
public_key_fingerprint TEXT NOT NULL,
|
||||
binding_id TEXT NOT NULL CHECK (length(binding_id) > 0),
|
||||
state TEXT NOT NULL CHECK (state IN ('configured', 'verified', 'revoked')),
|
||||
authentication_mode TEXT NOT NULL CHECK (authentication_mode IN ('legacy_server_issuer', 'workspace_identity')),
|
||||
workspace_key_id TEXT,
|
||||
workspace_public_key_fingerprint TEXT,
|
||||
workspace_trust_id TEXT CHECK (length(workspace_trust_id) > 0),
|
||||
created_at TEXT NOT NULL,
|
||||
updated_at TEXT NOT NULL,
|
||||
revoked_at TEXT,
|
||||
PRIMARY KEY (workspace_id, runtime_id),
|
||||
UNIQUE (workspace_id, public_key_fingerprint),
|
||||
FOREIGN KEY(workspace_id) REFERENCES workspaces(workspace_id) ON DELETE RESTRICT,
|
||||
CHECK (
|
||||
(authentication_mode = 'legacy_server_issuer' AND workspace_key_id IS NULL AND workspace_public_key_fingerprint IS NULL AND workspace_trust_id IS NULL)
|
||||
OR
|
||||
(authentication_mode = 'workspace_identity' AND workspace_key_id IS NOT NULL AND (state != 'verified' OR (workspace_trust_id IS NOT NULL AND workspace_public_key_fingerprint IS NOT NULL)))
|
||||
),
|
||||
CHECK (
|
||||
(state = 'revoked' AND revoked_at IS NOT NULL)
|
||||
OR
|
||||
(state != 'revoked' AND revoked_at IS NULL)
|
||||
)
|
||||
FOREIGN KEY(workspace_id) REFERENCES workspaces(workspace_id) ON DELETE RESTRICT
|
||||
);
|
||||
CREATE TABLE workspace_runtime_verifications (
|
||||
-- Historical authentication metadata only, never current admission authority.
|
||||
CREATE TABLE workspace_runtime_binding_verification_archive (
|
||||
workspace_id TEXT NOT NULL,
|
||||
runtime_id TEXT NOT NULL,
|
||||
binding_id TEXT NOT NULL CHECK(length(binding_id) > 0),
|
||||
binding_id TEXT NOT NULL,
|
||||
state TEXT NOT NULL,
|
||||
workspace_key_id TEXT,
|
||||
workspace_public_key_fingerprint TEXT,
|
||||
workspace_trust_id TEXT,
|
||||
updated_at TEXT NOT NULL,
|
||||
PRIMARY KEY (workspace_id, runtime_id, binding_id),
|
||||
FOREIGN KEY(workspace_id) REFERENCES workspaces(workspace_id) ON DELETE CASCADE
|
||||
);
|
||||
CREATE TABLE workspace_runtime_verification_archive (
|
||||
workspace_id TEXT NOT NULL,
|
||||
runtime_id TEXT NOT NULL,
|
||||
binding_id TEXT NOT NULL,
|
||||
workspace_key_id TEXT NOT NULL,
|
||||
workspace_public_key_fingerprint TEXT NOT NULL CHECK(length(workspace_public_key_fingerprint) > 0),
|
||||
workspace_trust_id TEXT NOT NULL CHECK(length(workspace_trust_id) > 0),
|
||||
workspace_public_key_fingerprint TEXT NOT NULL,
|
||||
workspace_trust_id TEXT NOT NULL,
|
||||
runtime_public_key_fingerprint TEXT NOT NULL,
|
||||
challenge_id TEXT NOT NULL,
|
||||
state TEXT NOT NULL CHECK(state IN ('pending', 'verified', 'failed')),
|
||||
state TEXT NOT NULL,
|
||||
last_outcome TEXT NOT NULL,
|
||||
verified_at TEXT,
|
||||
checked_at TEXT NOT NULL,
|
||||
PRIMARY KEY(workspace_id, runtime_id),
|
||||
FOREIGN KEY(workspace_id, runtime_id)
|
||||
REFERENCES workspace_runtime_bindings(workspace_id, runtime_id) ON DELETE CASCADE,
|
||||
CHECK((state = 'verified' AND verified_at IS NOT NULL)
|
||||
OR (state != 'verified' AND verified_at IS NULL))
|
||||
PRIMARY KEY (workspace_id, runtime_id),
|
||||
FOREIGN KEY(workspace_id) REFERENCES workspaces(workspace_id) ON DELETE CASCADE
|
||||
);
|
||||
CREATE INDEX workspace_runtime_verifications_state_idx
|
||||
ON workspace_runtime_verifications(workspace_id, state, checked_at DESC);
|
||||
CREATE TABLE workspace_runtime_binding_audit (
|
||||
workspace_id TEXT NOT NULL,
|
||||
runtime_id TEXT NOT NULL,
|
||||
|
||||
@@ -48,7 +48,7 @@ fn evidence_column_cutover_preserves_all_values_and_current_schema_names() {
|
||||
let upgraded = SqliteWorkspaceStore::open(&path).unwrap();
|
||||
upgraded
|
||||
.with_conn(|conn| {
|
||||
assert_eq!(current_schema_version(conn)?, 89);
|
||||
assert_eq!(current_schema_version(conn)?, LATEST_SCHEMA_VERSION);
|
||||
assert_eq!(evidence_rows(conn, "artifacts"), artifacts);
|
||||
assert_eq!(
|
||||
evidence_rows(conn, "repository_secret_legacy_receipts"),
|
||||
|
||||
@@ -10,7 +10,7 @@ use yoi_workspace_server::hosts::{
|
||||
};
|
||||
use yoi_workspace_server::store::{
|
||||
AccountRecord, ApiTokenRecord, SqliteWorkspaceStore, UserRecord,
|
||||
WorkspaceRuntimeAuthenticationMode, WorkspaceRuntimeBinding, WorkspaceRuntimeBindingState,
|
||||
WorkspaceRuntimeAuthenticationMode, WorkspaceRuntimeBinding,
|
||||
};
|
||||
use yoi_workspace_server::{
|
||||
ControlPlaneStore, ResolvedWorkspaceBackendConfig, ServerConfig, ServerHostConfigFile,
|
||||
@@ -456,7 +456,7 @@ fn append_workspace_runtime_sources(
|
||||
.into_iter()
|
||||
.filter(|binding| {
|
||||
binding.runtime_id != yoi_workspace_server::hosts::EMBEDDED_RUNTIME_ID
|
||||
&& binding.state == WorkspaceRuntimeBindingState::Verified
|
||||
&& binding.revoked_at.is_none()
|
||||
})
|
||||
.collect::<Vec<_>>()
|
||||
})
|
||||
@@ -787,11 +787,7 @@ mod tests {
|
||||
public_key: "unused".to_owned(),
|
||||
public_key_fingerprint: "unused".to_owned(),
|
||||
binding_id: "binding-test".to_owned(),
|
||||
state: WorkspaceRuntimeBindingState::Verified,
|
||||
authentication_mode: WorkspaceRuntimeAuthenticationMode::LegacyServerIssuer,
|
||||
workspace_key_id: None,
|
||||
workspace_public_key_fingerprint: None,
|
||||
workspace_trust_id: None,
|
||||
created_at: "2026-09-01T00:00:00Z".to_owned(),
|
||||
updated_at: "2026-09-01T00:00:00Z".to_owned(),
|
||||
revoked_at: None,
|
||||
@@ -815,11 +811,7 @@ mod tests {
|
||||
public_key: "unused".to_owned(),
|
||||
public_key_fingerprint: "unused".to_owned(),
|
||||
binding_id: "binding-test".to_owned(),
|
||||
state: WorkspaceRuntimeBindingState::Verified,
|
||||
authentication_mode: WorkspaceRuntimeAuthenticationMode::WorkspaceIdentity,
|
||||
workspace_key_id: Some("WK-test".to_owned()),
|
||||
workspace_public_key_fingerprint: Some("sha256:workspace-key".to_owned()),
|
||||
workspace_trust_id: Some("trust-test".to_owned()),
|
||||
created_at: "2026-09-01T00:00:00Z".to_owned(),
|
||||
updated_at: "2026-09-01T00:00:00Z".to_owned(),
|
||||
revoked_at: None,
|
||||
|
||||
@@ -53,7 +53,8 @@ fn fresh_and_upgraded_state_contract_schemas_enforce_the_same_constraints() {
|
||||
"flow_source_contents",
|
||||
"repositories",
|
||||
"workspace_runtime_bindings",
|
||||
"workspace_runtime_verifications",
|
||||
"workspace_runtime_binding_verification_archive",
|
||||
"workspace_runtime_verification_archive",
|
||||
"workspace_runtime_binding_audit",
|
||||
"workspace_signing_identities",
|
||||
"workspace_signing_identity_provisioning_operations",
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -111,11 +111,9 @@ impl RealDriveFixture {
|
||||
public_key: identity.public_key.clone(),
|
||||
public_key_fingerprint: String::new(),
|
||||
binding_id: "binding-drive-test".to_string(),
|
||||
state: StoredRuntimeBindingState::Verified,
|
||||
|
||||
authentication_mode: StoredRuntimeAuthenticationMode::LegacyServerIssuer,
|
||||
workspace_key_id: None,
|
||||
workspace_public_key_fingerprint: None,
|
||||
workspace_trust_id: None,
|
||||
|
||||
created_at: "1".into(),
|
||||
updated_at: "1".into(),
|
||||
revoked_at: None,
|
||||
|
||||
@@ -160,12 +160,10 @@ impl LaunchFixture {
|
||||
if remote {
|
||||
fixture.runtime_id = "ticket-worker-real-http".to_string();
|
||||
use worker_runtime::workspace_issuer::{
|
||||
InMemoryWorkspaceClaimReplayProtection,
|
||||
InMemoryWorkspaceRuntimeVerificationAuthority, RuntimeVerificationSigner,
|
||||
WorkspaceCapabilityVerifier, WorkspaceIssuerTrustRecord, WorkspaceIssuerTrustState,
|
||||
InMemoryWorkspaceClaimReplayProtection, WorkspaceCapabilityVerifier,
|
||||
WorkspaceIssuerTrustRecord, WorkspaceIssuerTrustState,
|
||||
};
|
||||
let runtime_identity = RuntimeIdentityMaterial::generate(&fixture.runtime_id).unwrap();
|
||||
let signer = RuntimeVerificationSigner::from_identity(&runtime_identity).unwrap();
|
||||
let workspace_identity = fixture
|
||||
.api
|
||||
.signing_identities
|
||||
@@ -204,13 +202,11 @@ impl LaunchFixture {
|
||||
display_name: "Ticket Worker real HTTP Runtime".into(),
|
||||
base_url: endpoint.clone(),
|
||||
public_key: runtime_identity.public_key.clone(),
|
||||
public_key_fingerprint: signer.public_key_fingerprint().to_string(),
|
||||
public_key_fingerprint: String::new(),
|
||||
binding_id: "binding-launch-test".to_string(),
|
||||
state: StoredRuntimeBindingState::Configured,
|
||||
|
||||
authentication_mode: StoredRuntimeAuthenticationMode::WorkspaceIdentity,
|
||||
workspace_key_id: Some(workspace_identity.key_id),
|
||||
workspace_public_key_fingerprint: workspace_identity.public_key_fingerprint.clone(),
|
||||
workspace_trust_id: Some("trust-launch-test".to_string()),
|
||||
|
||||
created_at: TEST_CREATED_AT.into(),
|
||||
updated_at: TEST_CREATED_AT.into(),
|
||||
revoked_at: None,
|
||||
@@ -228,6 +224,7 @@ impl LaunchFixture {
|
||||
.await
|
||||
.unwrap()
|
||||
.unwrap();
|
||||
let runtime_id = fixture.runtime_id.clone();
|
||||
fixture.runtime_server = Some(tokio::spawn(async move {
|
||||
worker_runtime::http_server::serve_runtime_http_with_workspace_auth(
|
||||
runtime,
|
||||
@@ -235,10 +232,7 @@ impl LaunchFixture {
|
||||
None,
|
||||
worker_runtime::http_server::WorkspaceRuntimeHttpAuth {
|
||||
verifier,
|
||||
signer,
|
||||
verifications: Arc::new(
|
||||
InMemoryWorkspaceRuntimeVerificationAuthority::default(),
|
||||
),
|
||||
runtime_id,
|
||||
},
|
||||
)
|
||||
.await
|
||||
@@ -258,27 +252,12 @@ impl LaunchFixture {
|
||||
cached_status: "active".into(),
|
||||
timeout: std::time::Duration::from_secs(5),
|
||||
};
|
||||
fixture.api.runtime.register_or_replace(
|
||||
RemoteWorkerRuntime::new(
|
||||
remote_config.clone(),
|
||||
TEST_WORKSPACE_ID.to_string(),
|
||||
backend_url.clone(),
|
||||
)
|
||||
.unwrap(),
|
||||
);
|
||||
let verified = perform_workspace_runtime_verification(
|
||||
&fixture.api,
|
||||
fixture.api.runtime.clone(),
|
||||
&binding,
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
remote_config.workspace_authorization =
|
||||
Some(crate::hosts::WorkspaceRuntimeAuthorization::new(
|
||||
fixture.api.store.clone(),
|
||||
fixture.api.signing_identities.clone(),
|
||||
backend_url.clone(),
|
||||
Some(verified),
|
||||
binding,
|
||||
));
|
||||
fixture.api.runtime.register_or_replace(
|
||||
RemoteWorkerRuntime::new(remote_config, TEST_WORKSPACE_ID.to_string(), backend_url)
|
||||
|
||||
@@ -694,7 +694,6 @@ async fn revoked_runtime_and_persisted_workspace_fence_reject_new_requests() {
|
||||
.unwrap()
|
||||
.unwrap();
|
||||
binding.revoked_at = Some("revoked".into());
|
||||
binding.state = StoredRuntimeBindingState::Revoked;
|
||||
f.api
|
||||
.store
|
||||
.upsert_workspace_runtime_binding_record(binding, true)
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -99,7 +99,8 @@ const WORKSPACE_DELETION_PURGE_TABLES: &[&str] = &[
|
||||
"workspace_resource_keys",
|
||||
"workspace_runtime_binding_audit",
|
||||
"workspace_runtime_bindings",
|
||||
"workspace_runtime_verifications",
|
||||
"workspace_runtime_binding_verification_archive",
|
||||
"workspace_runtime_verification_archive",
|
||||
"workspace_signing_identities",
|
||||
"workspace_signing_identity_audit",
|
||||
"workspace_signing_identity_provisioning_operations",
|
||||
|
||||
@@ -228,36 +228,7 @@ impl WorkspaceSigningIdentityService {
|
||||
}
|
||||
|
||||
pub fn sign(&self, workspace_id: &str, payload: &[u8]) -> Result<Vec<u8>> {
|
||||
let identity = self.get_validated(workspace_id)?;
|
||||
if identity.state != "active" {
|
||||
return Err(identity_error(
|
||||
"workspace_signing_identity_not_provisioned",
|
||||
"Workspace signing identity is not provisioned",
|
||||
));
|
||||
}
|
||||
let material = self
|
||||
.materials
|
||||
.load(&identity.private_material_ref)?
|
||||
.ok_or_else(|| {
|
||||
identity_error(
|
||||
"workspace_signing_identity_material_missing",
|
||||
"Workspace signing private material is missing",
|
||||
)
|
||||
})?;
|
||||
let fingerprint = identity.public_key_fingerprint.as_deref().ok_or_else(|| {
|
||||
identity_error(
|
||||
"workspace_signing_identity_material_mismatch",
|
||||
"Workspace signing public key fingerprint is missing",
|
||||
)
|
||||
})?;
|
||||
let signing_key = material.signing_key(workspace_id, &identity.key_id)?;
|
||||
let public_key = encode_public_key(signing_key.public_key().as_ref());
|
||||
if public_key_fingerprint(&public_key)? != fingerprint {
|
||||
return Err(identity_error(
|
||||
"workspace_signing_identity_material_mismatch",
|
||||
"Workspace signing private material does not match public metadata",
|
||||
));
|
||||
}
|
||||
let (_, signing_key) = self.select_signing_key(workspace_id)?;
|
||||
Ok(signing_key.sign(payload).as_ref().to_vec())
|
||||
}
|
||||
|
||||
@@ -266,9 +237,20 @@ impl WorkspaceSigningIdentityService {
|
||||
workspace_id: &str,
|
||||
claims: &WorkspaceCapabilityClaims,
|
||||
) -> Result<String> {
|
||||
let (identity, signing_key) = self.select_signing_key(workspace_id)?;
|
||||
if claims.issuer_workspace_id != identity.workspace_id
|
||||
|| claims.issuer_key_id != identity.key_id
|
||||
|| Some(claims.issuer_public_key_fingerprint.as_str())
|
||||
!= identity.public_key_fingerprint.as_deref()
|
||||
{
|
||||
return Err(identity_error(
|
||||
"workspace_capability_identity_mismatch",
|
||||
"Workspace capability claims do not match the current signing identity",
|
||||
));
|
||||
}
|
||||
let input = workspace_capability_signing_input(claims).map_err(capability_error)?;
|
||||
let signature = self.sign(workspace_id, input.bytes())?;
|
||||
assemble_workspace_capability_token(input, &signature).map_err(capability_error)
|
||||
let signature = signing_key.sign(input.bytes());
|
||||
assemble_workspace_capability_token(input, signature.as_ref()).map_err(capability_error)
|
||||
}
|
||||
|
||||
pub fn delete_material(&self, workspace_id: &str) -> Result<()> {
|
||||
@@ -306,7 +288,40 @@ impl WorkspaceSigningIdentityService {
|
||||
})
|
||||
}
|
||||
|
||||
fn select_signing_key(
|
||||
&self,
|
||||
workspace_id: &str,
|
||||
) -> Result<(
|
||||
WorkspaceSigningIdentityRecord,
|
||||
ring::signature::Ed25519KeyPair,
|
||||
)> {
|
||||
let identity = self
|
||||
.store
|
||||
.get_workspace_signing_identity(workspace_id)?
|
||||
.ok_or_else(|| {
|
||||
identity_error(
|
||||
"workspace_signing_identity_metadata_missing",
|
||||
"Workspace signing identity metadata is missing",
|
||||
)
|
||||
})?;
|
||||
if identity.state != "active" {
|
||||
return Err(identity_error(
|
||||
"workspace_signing_identity_not_provisioned",
|
||||
"Workspace signing identity is not provisioned",
|
||||
));
|
||||
}
|
||||
let signing_key = self.validated_signing_key(&identity)?;
|
||||
Ok((identity, signing_key))
|
||||
}
|
||||
|
||||
fn validate_active_material(&self, identity: &WorkspaceSigningIdentityRecord) -> Result<()> {
|
||||
self.validated_signing_key(identity).map(|_| ())
|
||||
}
|
||||
|
||||
fn validated_signing_key(
|
||||
&self,
|
||||
identity: &WorkspaceSigningIdentityRecord,
|
||||
) -> Result<ring::signature::Ed25519KeyPair> {
|
||||
let material = self
|
||||
.materials
|
||||
.load(&identity.private_material_ref)?
|
||||
@@ -316,8 +331,8 @@ impl WorkspaceSigningIdentityService {
|
||||
"Workspace signing private material is missing",
|
||||
)
|
||||
})?;
|
||||
let public_key =
|
||||
material.validate_and_public_key(&identity.workspace_id, &identity.key_id)?;
|
||||
let signing_key = material.signing_key(&identity.workspace_id, &identity.key_id)?;
|
||||
let public_key = encode_public_key(signing_key.public_key().as_ref());
|
||||
let fingerprint = public_key_fingerprint(&public_key)?;
|
||||
if identity.public_key.as_deref() != Some(public_key.as_str())
|
||||
|| identity.public_key_fingerprint.as_deref() != Some(fingerprint.as_str())
|
||||
@@ -327,7 +342,7 @@ impl WorkspaceSigningIdentityService {
|
||||
"Workspace signing private material does not match public metadata",
|
||||
));
|
||||
}
|
||||
Ok(())
|
||||
Ok(signing_key)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -843,6 +858,14 @@ mod tests {
|
||||
loads: std::sync::atomic::AtomicUsize::new(0),
|
||||
}),
|
||||
);
|
||||
let selected_signature = swapping_service.sign("workspace-1", payload).unwrap();
|
||||
ring::signature::UnparsedPublicKey::new(
|
||||
&ring::signature::ED25519,
|
||||
worker_runtime::auth::decode_public_key(provisioned.public_key.as_deref().unwrap())
|
||||
.unwrap(),
|
||||
)
|
||||
.verify(payload, &selected_signature)
|
||||
.unwrap();
|
||||
assert!(matches!(
|
||||
swapping_service.sign("workspace-1", payload),
|
||||
Err(Error::WorkspaceSigningIdentity { ref code, .. })
|
||||
|
||||
Reference in New Issue
Block a user