use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, ::jumo_derive::Jumo)]
#[jumo(
kind = "message",
role = "command",
domain = "Control",
module = "Control.Gateway.Supervision"
)]
#[serde(deny_unknown_fields)]
pub struct RegisterGateway {
pub enrollment_token: String,
pub instance_id: String,
pub certificate_signing_request: String,
pub requested_at: String,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, ::jumo_derive::Jumo)]
#[jumo(
kind = "struct",
domain = "Control",
module = "Control.Gateway.Security"
)]
#[serde(deny_unknown_fields)]
pub struct GatewayClientCertificate {
pub certificate_id: String,
pub gateway_id: String,
pub serial: String,
pub certificate: String,
pub ca_bundle: Option<String>,
pub issued_at: String,
pub not_before: Option<String>,
pub not_after: Option<String>,
pub status: GatewayClientCertificateStatus,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, ::jumo_derive::Jumo)]
#[jumo(
kind = "state",
domain = "Control",
module = "Control.Gateway.Security"
)]
pub enum GatewayClientCertificateStatus {
#[serde(rename = "active")]
Active,
#[serde(rename = "expired")]
Expired,
#[serde(rename = "revoked")]
Revoked,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, ::jumo_derive::Jumo)]
#[jumo(
kind = "struct",
domain = "Control",
module = "Control.Gateway.Security"
)]
#[serde(deny_unknown_fields)]
pub struct GatewayCredentialBundle {
pub credential_id: String,
pub gateway_id: String,
pub instance_id: Option<String>,
pub certificate: String,
pub ca_bundle: Option<String>,
pub issued_at: String,
pub not_before: Option<String>,
pub not_after: Option<String>,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, ::jumo_derive::Jumo)]
#[jumo(
kind = "struct",
domain = "Control",
module = "Control.Gateway.Security"
)]
#[serde(deny_unknown_fields)]
pub struct GatewayCredentialVerificationResult {
pub gateway_id: String,
pub certificate_serial: String,
pub status: String,
pub verified_at: String,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct GatewayEnrollmentResult {
pub status: String,
pub gateway_id: String,
pub instance_id: String,
pub credential_id: String,
pub initial_config: String,
pub credential_bundle: GatewayCredentialBundle,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, ::jumo_derive::Jumo)]
#[jumo(
kind = "message",
role = "command",
domain = "Control",
module = "Control.Gateway.Supervision"
)]
#[serde(deny_unknown_fields)]
pub struct RenewGatewayCredential {
pub gateway_id: String,
pub current_certificate_serial: String,
pub certificate_signing_request: String,
pub requested_at: String,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, ::jumo_derive::Jumo)]
#[jumo(
kind = "message",
role = "command",
domain = "Control",
module = "Control.Gateway.Supervision"
)]
#[serde(deny_unknown_fields)]
pub struct VerifyGatewayCredential {
pub gateway_id: String,
pub certificate_serial: String,
}
#[cfg(test)]
mod tests {
use super::{
GatewayClientCertificate, GatewayClientCertificateStatus, GatewayCredentialBundle,
GatewayCredentialVerificationResult, RegisterGateway, RenewGatewayCredential,
VerifyGatewayCredential,
};
fn bundle() -> GatewayCredentialBundle {
GatewayCredentialBundle {
credential_id: "cred-1".to_string(),
gateway_id: "gw-1".to_string(),
instance_id: None,
certificate: "CERT".to_string(),
ca_bundle: None,
issued_at: "2026-10-04T00:00:00Z".to_string(),
not_before: None,
not_after: Some("2026-11-04T00:00:00Z".to_string()),
}
}
#[test]
fn gateway_credential_bundle_carries_only_the_client_certificate() {
let json = serde_json::to_string(&bundle()).expect("encode");
assert!(!json.contains("bearer_token"), "{json}");
assert!(!json.contains("auth_scheme"), "{json}");
let legacy = r#"{"credential_id":"c","gateway_id":"g","instance_id":null,"auth_scheme":"bearer","bearer_token":"rt_x","certificate":"CERT","ca_bundle":null,"issued_at":"t","not_before":null,"not_after":null}"#;
assert!(serde_json::from_str::<GatewayCredentialBundle>(legacy).is_err());
let no_certificate = r#"{"credential_id":"c","gateway_id":"g","instance_id":null,"ca_bundle":null,"issued_at":"t","not_before":null,"not_after":null}"#;
assert!(serde_json::from_str::<GatewayCredentialBundle>(no_certificate).is_err());
}
#[test]
fn register_requires_a_csr_and_rejects_unknown_fields() {
let request = RegisterGateway {
enrollment_token: "reg-1".to_string(),
instance_id: "gw-1/inst-1".to_string(),
certificate_signing_request: "-----BEGIN CERTIFICATE REQUEST-----\nA\n".to_string(),
requested_at: "2026-10-04T00:00:00Z".to_string(),
};
let json = serde_json::to_string(&request).expect("encode");
assert!(json.contains("certificate_signing_request"));
assert_eq!(
serde_json::from_str::<RegisterGateway>(&json).expect("decode"),
request
);
let mut without_csr = serde_json::to_value(&request).expect("encode");
without_csr
.as_object_mut()
.expect("object")
.remove("certificate_signing_request");
assert!(serde_json::from_value::<RegisterGateway>(without_csr).is_err());
let mutated = json.replacen('{', "{\"extra\":1,", 1);
assert!(serde_json::from_str::<RegisterGateway>(&mutated).is_err());
}
#[test]
fn renewal_and_verification_use_certificate_fields() {
let renewal = RenewGatewayCredential {
gateway_id: "gw-1".to_string(),
current_certificate_serial: "01".to_string(),
certificate_signing_request: "CSR".to_string(),
requested_at: "2026-10-04T00:00:00Z".to_string(),
};
let json = serde_json::to_string(&renewal).expect("encode");
assert!(json.contains("current_certificate_serial"));
assert!(json.contains("certificate_signing_request"));
let verify = VerifyGatewayCredential {
gateway_id: "gw-1".to_string(),
certificate_serial: "01".to_string(),
};
assert!(
serde_json::to_string(&verify)
.expect("encode")
.contains("certificate_serial")
);
let result = GatewayCredentialVerificationResult {
gateway_id: "gw-1".to_string(),
certificate_serial: "01".to_string(),
status: "valid".to_string(),
verified_at: "2026-10-04T00:00:00Z".to_string(),
};
let json = serde_json::to_string(&result).expect("encode");
assert!(json.contains("certificate_serial"));
assert!(!json.contains("credential_id"), "{json}");
}
#[test]
fn client_certificate_status_uses_wire_names() {
assert_eq!(
serde_json::to_string(&GatewayClientCertificateStatus::Active).unwrap(),
"\"active\""
);
assert_eq!(
serde_json::to_string(&GatewayClientCertificateStatus::Revoked).unwrap(),
"\"revoked\""
);
assert!(serde_json::from_str::<GatewayClientCertificateStatus>("\"unknown\"").is_err());
let cert = GatewayClientCertificate {
certificate_id: "cert-1".to_string(),
gateway_id: "gw-1".to_string(),
serial: "01".to_string(),
certificate: "PEM".to_string(),
ca_bundle: None,
issued_at: "2026-10-04T00:00:00Z".to_string(),
not_before: None,
not_after: None,
status: GatewayClientCertificateStatus::Active,
};
let back: GatewayClientCertificate =
serde_json::from_str(&serde_json::to_string(&cert).unwrap()).unwrap();
assert_eq!(back, cert);
}
}