Skip to main content

basil_core/service/
broker.rs

1// SPDX-FileCopyrightText: 2026 OpenBasil Contributors
2//
3// SPDX-License-Identifier: Apache-2.0
4
5//! gRPC broker service adapters.
6//!
7//! This module is intentionally a thin transport adapter over the existing
8//! [`BrokerState`] + [`BackendManager`] core. It preserves the same PDP gating
9//! model as the JSON handler: every key-scoped RPC authorizes the
10//! kernel-attested peer uid before dispatching.
11
12#![allow(clippy::result_large_err)]
13
14use std::pin::Pin;
15use std::sync::Arc;
16
17use futures::Stream;
18use std::sync::Mutex;
19use tonic::{Code, Request, Response, Status};
20
21use crate::actor::AuthenticatedActor;
22use crate::catalog::policy::Op;
23use crate::state::BrokerState;
24use crate::transport::{authorize, broker_status};
25
26pub(super) type GrpcResult<T> = Result<Response<T>, Status>;
27pub(super) type BoxStream<T> = Pin<Box<dyn Stream<Item = Result<T, Status>> + Send + 'static>>;
28
29/// Broker identity and response-signing key settings for sealed invocation.
30#[derive(Debug, Clone, PartialEq, Eq)]
31pub struct BrokerIdentityRuntimeConfig {
32    /// Stable broker audience / identity URI.
33    pub id: String,
34    /// Catalog key id used to sign invocation responses.
35    pub response_signing_key_id: String,
36}
37
38/// Runtime settings for the sealed invocation service.
39#[derive(Debug, Clone, PartialEq, Eq)]
40pub struct InvocationRuntimeConfig {
41    /// Whether `InvocationService.Invoke` accepts requests.
42    pub enabled: bool,
43    /// Broker identity and response-signing key. Required when enabled.
44    pub broker_identity: Option<BrokerIdentityRuntimeConfig>,
45    /// Accepted broker audiences. An omitted header audience may be derived only
46    /// when this contains exactly one value.
47    pub audiences: Vec<String>,
48    /// Catalog key id whose public half receives sealed invocation requests.
49    pub request_encryption_key_id: Option<String>,
50    /// Maximum accepted signed request TTL in seconds.
51    pub max_ttl_secs: u32,
52    /// Allowed clock skew in seconds for issue and expiry timestamps.
53    pub clock_skew_secs: u32,
54    /// Maximum replay-cache entries retained in memory.
55    pub replay_cache_capacity: usize,
56    /// Fixed current time override for deterministic tests. Leave unset in
57    /// production.
58    pub now_unix_override: Option<u32>,
59}
60
61impl Default for InvocationRuntimeConfig {
62    fn default() -> Self {
63        Self {
64            enabled: false,
65            broker_identity: None,
66            audiences: Vec::new(),
67            request_encryption_key_id: None,
68            max_ttl_secs: basil_proto::invocation::DEFAULT_EXPIRES_AFTER_SECS,
69            clock_skew_secs: 30,
70            replay_cache_capacity: 4096,
71            now_unix_override: None,
72        }
73    }
74}
75
76/// Shared implementation for all broker gRPC services.
77#[derive(Debug, Clone)]
78pub struct BrokerGrpc {
79    pub(super) state: Arc<BrokerState>,
80    pub(super) invocation: InvocationRuntimeConfig,
81    pub(super) invocation_replay_cache:
82        Arc<Mutex<crate::service::invocation::InvocationReplayCache>>,
83}
84
85impl BrokerGrpc {
86    /// Build a gRPC service adapter over shared broker state.
87    #[must_use]
88    pub fn new(state: Arc<BrokerState>) -> Self {
89        Self::new_with_invocation(state, false)
90    }
91
92    /// Build a gRPC service adapter and explicitly configure invocation serving.
93    #[must_use]
94    pub fn new_with_invocation(state: Arc<BrokerState>, invocation_enabled: bool) -> Self {
95        Self::new_with_invocation_config(
96            state,
97            InvocationRuntimeConfig {
98                enabled: invocation_enabled,
99                ..InvocationRuntimeConfig::default()
100            },
101        )
102    }
103
104    /// Build a gRPC service adapter with full invocation runtime settings.
105    #[must_use]
106    pub fn new_with_invocation_config(
107        state: Arc<BrokerState>,
108        invocation: InvocationRuntimeConfig,
109    ) -> Self {
110        let capacity = invocation.replay_cache_capacity;
111        Self {
112            state,
113            invocation,
114            invocation_replay_cache: Arc::new(Mutex::new(
115                crate::service::invocation::InvocationReplayCache::new(capacity),
116            )),
117        }
118    }
119
120    pub(super) fn invocation_now_unix(&self) -> u32 {
121        if let Some(now) = self.invocation.now_unix_override {
122            return now;
123        }
124        std::time::SystemTime::now()
125            .duration_since(std::time::UNIX_EPOCH)
126            .map_or(0, |duration| {
127                u32::try_from(duration.as_secs()).unwrap_or(u32::MAX)
128            })
129    }
130
131    pub(super) fn authorize<T>(
132        &self,
133        request: &Request<T>,
134        op: Op,
135        key: &str,
136    ) -> Result<AuthenticatedActor, Status> {
137        authorize(&self.state, request, op, key)
138    }
139
140    pub(super) fn visible(&self, actor: &AuthenticatedActor, key: &str) -> bool {
141        let generation = self.state.load_generation();
142        let pdp = generation.pdp();
143        pdp.decide(actor, Op::List, key).is_allow()
144            || pdp.decide(actor, Op::GetPublicKey, key).is_allow()
145            || pdp.decide(actor, Op::Get, key).is_allow()
146    }
147
148    pub(super) fn require_unix_uid(
149        actor: &AuthenticatedActor,
150        op: &'static str,
151    ) -> Result<u32, Status> {
152        actor.unix_uid().ok_or_else(|| {
153            broker_status(
154                Code::Unauthenticated,
155                "UNAUTHENTICATED",
156                op,
157                "operation requires local peer credentials",
158            )
159        })
160    }
161}
162
163#[cfg(test)]
164mod tests {
165    use std::collections::BTreeMap;
166    use std::sync::Arc;
167
168    use async_trait::async_trait;
169    use base64::Engine as _;
170    use basil_proto::KeyType;
171    use basil_proto::broker::v1 as pb;
172    use basil_proto::broker::v1::BrokerErrorInfo;
173    use basil_proto::broker::v1::admin_service_server::AdminService;
174    use basil_proto::broker::v1::minting_service_server::MintingService;
175    use basil_proto::broker::v1::nats_service_server::NatsService;
176    use basil_proto::broker::v1::signing_service_server::SigningService;
177    use basil_proto::google::rpc::Status as RpcStatus;
178    use nkeys::{KeyPair, XKey};
179    use prost::Message;
180    use prost_types::{Duration, Struct, Value as ProstValue, value::Kind as ProstKind};
181    use serde_json::Value as JsonValue;
182    use tonic::{Code, Request, Status};
183
184    use super::BrokerGrpc;
185    use crate::backend::{Backend, BackendError, KvValue, NewKey, PublicKey};
186    use crate::catalog::load;
187    use crate::manager::{BackendManager, ManagerError};
188    use crate::peer::PeerInfo;
189    use crate::service::minting::nats_mint_status;
190    use crate::service::shared::*;
191    use crate::state::BrokerState;
192    use zeroize::Zeroizing;
193
194    fn error_info(status: &Status) -> BrokerErrorInfo {
195        let rpc = RpcStatus::decode(status.details()).expect("status details decode");
196        let detail = rpc.details.first().expect("broker detail present");
197        BrokerErrorInfo::decode(detail.value.as_slice()).expect("broker error info decodes")
198    }
199
200    fn assert_status_omits(status: &Status, canaries: &[&str]) {
201        let info = error_info(status);
202        let visible = format!(
203            "{} {} {} {:?}",
204            status.message(),
205            info.reason,
206            info.op,
207            status.details()
208        );
209        for canary in canaries {
210            assert!(
211                !visible.contains(canary),
212                "status leaked secret canary `{canary}` in `{visible}`"
213            );
214        }
215    }
216
217    #[test]
218    fn unsupported_algorithm_maps_to_unimplemented_detail() {
219        let status = backend_status(
220            "encrypt",
221            &BackendError::UnsupportedAlgorithm(basil_proto::AeadAlgorithm::Aes256Gcm),
222        );
223        let info = error_info(&status);
224        assert_eq!(status.code(), Code::Unimplemented);
225        assert_eq!(info.reason, "UNSUPPORTED_ALGORITHM");
226        assert_eq!(info.op, "encrypt");
227    }
228
229    #[test]
230    fn invalid_request_maps_to_invalid_argument_detail() {
231        let status = key_type(0, "new_key").expect_err("unspecified key type is invalid");
232        let info = error_info(&status);
233        assert_eq!(status.code(), Code::InvalidArgument);
234        assert_eq!(info.reason, "INVALID_REQUEST");
235        assert_eq!(info.op, "new_key");
236    }
237
238    #[test]
239    fn pqc_key_types_map_to_unimplemented_detail() {
240        let status = key_type(pb::KeyType::MlDsa65.into(), "new_key")
241            .expect_err("ML-DSA generation is not implemented yet");
242        let info = error_info(&status);
243        assert_eq!(status.code(), Code::Unimplemented);
244        assert_eq!(info.reason, "UNSUPPORTED_ALGORITHM");
245        assert_eq!(info.op, "new_key");
246
247        let status = key_type(pb::KeyType::MlKem768.into(), "new_key")
248            .expect_err("ML-KEM generation is not implemented yet");
249        let info = error_info(&status);
250        assert_eq!(status.code(), Code::Unimplemented);
251        assert_eq!(info.reason, "UNSUPPORTED_ALGORITHM");
252        assert_eq!(info.op, "new_key");
253    }
254
255    #[test]
256    fn signing_algorithms_are_accepted_at_the_wire_layer() {
257        ensure_supported_signing_algorithm(pb::SigningAlgorithm::Ed25519.into(), "sign")
258            .expect("legacy signing remains supported");
259        // ML-DSA is now serviceable: the wire validation accepts it and the
260        // manager dispatches a software-custodied ML-DSA key through the provider.
261        for algorithm in [
262            pb::SigningAlgorithm::MlDsa44,
263            pb::SigningAlgorithm::MlDsa65,
264            pb::SigningAlgorithm::MlDsa87,
265        ] {
266            ensure_supported_signing_algorithm(algorithm.into(), "sign")
267                .expect("ML-DSA signing is serviceable");
268        }
269    }
270
271    #[test]
272    fn kem_envelope_algorithms_are_validated() {
273        // X25519 and ML-KEM are recognized KEMs for envelope unwrap. Broker-side
274        // wrap remains X25519-only and is rejected in the AEAD service.
275        ensure_supported_kem_algorithm(pb::KemAlgorithm::X25519.into(), "wrap_envelope")
276            .expect("X25519 KEM is serviceable");
277        ensure_supported_kem_algorithm(pb::KemAlgorithm::MlKem1024.into(), "unwrap_envelope")
278            .expect("ML-KEM unwrap is serviceable");
279        ensure_supported_envelope_algorithm(
280            pb::EnvelopeAlgorithm::Aes256Gcm.into(),
281            "wrap_envelope",
282        )
283        .expect("envelope AEAD contract value is recognized");
284
285        let status = ensure_supported_kem_algorithm(0, "wrap_envelope")
286            .expect_err("missing KEM algorithm is invalid");
287        let info = error_info(&status);
288        assert_eq!(status.code(), Code::InvalidArgument);
289        assert_eq!(info.reason, "INVALID_REQUEST");
290        assert_eq!(info.op, "wrap_envelope");
291    }
292
293    #[test]
294    fn payload_cap_maps_to_resource_exhausted_detail() {
295        let status = payload_too_large("set", "too large");
296        let info = error_info(&status);
297        assert_eq!(status.code(), Code::ResourceExhausted);
298        assert_eq!(info.reason, "PAYLOAD_TOO_LARGE");
299        assert_eq!(info.op, "set");
300    }
301
302    #[test]
303    fn backend_transport_maps_to_unavailable_detail() {
304        let status = backend_status("sign", &BackendError::Transport("down".to_string()));
305        let info = error_info(&status);
306        assert_eq!(status.code(), Code::Unavailable);
307        assert_eq!(status.message(), "backend unavailable");
308        assert_eq!(info.reason, "BACKEND_UNAVAILABLE");
309        assert_eq!(info.op, "sign");
310    }
311
312    #[test]
313    fn backend_status_omits_secret_bearing_upstream_details() {
314        let canaries = [
315            "vault-token-s.123",
316            "Authorization: Bearer secret",
317            "/run/credentials/basil/passphrase",
318            "-----BEGIN PRIVATE KEY-----",
319            "upstream-response-body-with-credential",
320        ];
321        for err in [
322            BackendError::Transport(canaries[1].to_string()),
323            BackendError::Backend(canaries[4].to_string()),
324            BackendError::Protocol(canaries[3].to_string()),
325        ] {
326            let status = backend_status("sign", &err);
327            assert_status_omits(&status, &canaries);
328        }
329    }
330
331    #[test]
332    fn decrypt_failure_is_fixed_opaque_invalid_argument() {
333        let status = backend_status("decrypt", &BackendError::DecryptFailed);
334        let info = error_info(&status);
335        assert_eq!(status.code(), Code::InvalidArgument);
336        assert_eq!(status.message(), "decrypt failed");
337        assert_eq!(info.reason, "DECRYPT_FAILED");
338        assert_eq!(info.op, "decrypt");
339    }
340
341    #[test]
342    fn unknown_key_on_gated_manager_path_is_unauthorized_not_not_found() {
343        let status = manager_status("sign", &ManagerError::UnknownKey("hidden.key".to_string()));
344        let info = error_info(&status);
345        assert_eq!(status.code(), Code::PermissionDenied);
346        assert_eq!(status.message(), "not authorized");
347        assert_eq!(info.reason, "UNAUTHORIZED");
348        assert_eq!(info.op, "sign");
349    }
350
351    #[test]
352    fn duration_conversion_rejects_negative_and_rounds_fraction_up() {
353        let ttl = ttl_seconds(
354            Some(&Duration {
355                seconds: 4,
356                nanos: 1,
357            }),
358            "mint",
359        )
360        .expect("ttl converts");
361        assert_eq!(ttl, Some(5));
362
363        let status = ttl_seconds(
364            Some(&Duration {
365                seconds: -1,
366                nanos: 0,
367            }),
368            "mint",
369        )
370        .expect_err("negative ttl rejected");
371        assert_eq!(status.code(), Code::InvalidArgument);
372        assert_eq!(error_info(&status).reason, "INVALID_REQUEST");
373    }
374
375    #[test]
376    fn struct_claims_convert_to_json_and_reject_non_finite_numbers() {
377        let claims = Struct {
378            fields: [
379                (
380                    "scope".to_string(),
381                    ProstValue {
382                        kind: Some(ProstKind::StringValue("read".to_string())),
383                    },
384                ),
385                (
386                    "version".to_string(),
387                    ProstValue {
388                        kind: Some(ProstKind::NumberValue(2.0)),
389                    },
390                ),
391                (
392                    "ratio".to_string(),
393                    ProstValue {
394                        kind: Some(ProstKind::NumberValue(2.5)),
395                    },
396                ),
397            ]
398            .into(),
399        };
400        let json = claims_json(Some(&claims), "mint").expect("claims convert");
401        assert_eq!(json["scope"], JsonValue::String("read".to_string()));
402        assert_eq!(json["version"], JsonValue::Number(2.into()));
403        assert_eq!(
404            json["ratio"],
405            JsonValue::Number(serde_json::Number::from_f64(2.5).expect("finite"))
406        );
407
408        let claims = Struct {
409            fields: [(
410                "bad".to_string(),
411                ProstValue {
412                    kind: Some(ProstKind::NumberValue(f64::NAN)),
413                },
414            )]
415            .into(),
416        };
417        let status = claims_json(Some(&claims), "mint").expect_err("nan rejected");
418        assert_eq!(status.code(), Code::InvalidArgument);
419        assert_eq!(error_info(&status).reason, "INVALID_REQUEST");
420    }
421
422    #[test]
423    fn bad_subject_nkey_maps_to_invalid_argument() {
424        let status = nats_mint_status(
425            "mint_nats_user",
426            &BackendError::Protocol("invalid subject user nkey: bad".to_string()),
427        );
428        let info = error_info(&status);
429        assert_eq!(status.code(), Code::InvalidArgument);
430        assert_eq!(info.reason, "INVALID_REQUEST");
431        assert_eq!(info.op, "mint_nats_user");
432    }
433
434    struct MintBackend;
435
436    #[async_trait]
437    impl Backend for MintBackend {
438        fn kind(&self) -> &'static str {
439            "mint-test"
440        }
441
442        async fn new_key(&self, key_type: KeyType) -> Result<NewKey, BackendError> {
443            let _ = key_type;
444            Err(BackendError::Unsupported("new_key"))
445        }
446
447        async fn public_key(&self, key_id: &str) -> Result<Vec<u8>, BackendError> {
448            let _ = key_id;
449            Ok(vec![7; 32])
450        }
451
452        async fn sign(&self, key_id: &str, message: &[u8]) -> Result<Vec<u8>, BackendError> {
453            let _ = (key_id, message);
454            Ok(vec![9; 64])
455        }
456
457        async fn verify(
458            &self,
459            key_id: &str,
460            message: &[u8],
461            signature: &[u8],
462        ) -> Result<bool, BackendError> {
463            let _ = (key_id, message, signature);
464            Ok(true)
465        }
466
467        async fn kv_get(
468            &self,
469            key_id: &str,
470            version: Option<u32>,
471        ) -> Result<KvValue, BackendError> {
472            let _ = version;
473            match key_id {
474                "nats/curve-box-public" => {
475                    let private = Zeroizing::new([0x55; 32]);
476                    Ok(KvValue {
477                        value: basil_nats::xkey_public_from_private(&private).to_vec(),
478                        version: 1,
479                    })
480                }
481                _ => Err(BackendError::KeyNotFound(key_id.to_string())),
482            }
483        }
484
485        async fn kv_get_secret(
486            &self,
487            key_id: &str,
488            version: Option<u32>,
489        ) -> Result<Zeroizing<Vec<u8>>, BackendError> {
490            let _ = version;
491            match key_id {
492                "nats/curve-box" => Ok(Zeroizing::new(vec![0x55; 32])),
493                _ => Err(BackendError::KeyNotFound(key_id.to_string())),
494            }
495        }
496    }
497
498    /// Real NATS-key signer used to prove `SigningService.Sign` can complete a
499    /// caller-assembled rich NATS JWT without exposing the issuer seed.
500    struct NatsSignBackend(KeyPair);
501
502    #[async_trait]
503    impl Backend for NatsSignBackend {
504        fn kind(&self) -> &'static str {
505            "nkey-sign-test"
506        }
507
508        async fn new_key(&self, key_type: KeyType) -> Result<NewKey, BackendError> {
509            let _ = key_type;
510            Err(BackendError::Unsupported("new_key"))
511        }
512
513        async fn public_key(&self, key_id: &str) -> Result<Vec<u8>, BackendError> {
514            let _ = key_id;
515            let (_, public) = basil_nats::decode_public(&self.0.public_key())
516                .map_err(|e| BackendError::Protocol(e.to_string()))?;
517            Ok(public.to_vec())
518        }
519
520        async fn public_key_with_meta(&self, key_id: &str) -> Result<PublicKey, BackendError> {
521            Ok(PublicKey {
522                public_key: self.public_key(key_id).await?,
523                key_type: KeyType::Ed25519Nkey,
524                version: 1,
525            })
526        }
527
528        async fn sign(&self, key_id: &str, message: &[u8]) -> Result<Vec<u8>, BackendError> {
529            let _ = key_id;
530            self.0
531                .sign(message)
532                .map_err(|e| BackendError::Backend(e.to_string()))
533        }
534
535        async fn verify(
536            &self,
537            key_id: &str,
538            message: &[u8],
539            signature: &[u8],
540        ) -> Result<bool, BackendError> {
541            let _ = key_id;
542            Ok(self.0.verify(message, signature).is_ok())
543        }
544    }
545
546    fn state_with_backend(backend: Box<dyn Backend>) -> Arc<BrokerState> {
547        let catalog = r#"{
548          "schemaVersion": 1,
549          "backends": { "bao": { "kind": "vault", "addr": "https://127.0.0.1:8200" } },
550          "keys": {
551            "issuer.account": {
552              "class": "asymmetric", "keyType": "ed25519-nkey", "backend": "bao",
553              "path": "issuer/account", "writable": true, "missing": "error",
554              "labels": ["nats_type=A"], "description": "NATS account issuer"
555            },
556            "issuer.operator": {
557              "class": "asymmetric", "keyType": "ed25519-nkey", "backend": "bao",
558              "path": "issuer/operator", "writable": true, "missing": "error",
559              "labels": ["nats_type=O"], "description": "NATS operator issuer"
560            },
561            "issuer.server": {
562              "class": "asymmetric", "keyType": "ed25519-nkey", "backend": "bao",
563              "path": "issuer/server", "writable": true, "missing": "error",
564              "labels": ["nats_type=N"], "description": "NATS server issuer"
565            },
566            "issuer.curve": {
567              "class": "asymmetric", "keyType": "ed25519-nkey", "backend": "bao",
568              "path": "issuer/curve", "writable": true, "missing": "error",
569              "labels": ["nats_type=X"], "description": "NATS curve issuer"
570            },
571            "nats.curve_box": {
572              "class": "sealing", "keyType": "x25519", "backend": "bao",
573              "path": "nats/curve-box", "publicPath": "nats/curve-box-public",
574              "writable": false, "missing": "error",
575              "description": "NATS xkey box custody"
576            }
577          }
578        }"#;
579        let policy = r#"{
580          "schemaVersion": 2,
581          "subjects": {
582            "svc.mint": { "allOf": [ { "kind": "unix", "uid": 42 } ] }
583          },
584          "roles": {
585            "minter": ["mint", "sign_nats_jwt", "validate_nats_jwt"],
586            "reader": ["get", "list", "get_public_key"],
587            "signer": ["sign", "verify"],
588            "nats_box": ["encrypt_nats_curve", "decrypt_nats_curve"]
589          },
590          "rules": [
591            { "id": "mint", "subjects": ["svc.mint"], "action": ["role:minter", "role:reader", "role:signer"], "target": ["issuer.*"] },
592            { "id": "nats-box", "subjects": ["svc.mint"], "action": ["role:nats_box"], "target": ["nats.curve_box"] }
593          ],
594          "config": {
595            "names": { "users": { "42": "svc-mint" }, "groups": {} },
596            "memberships": { "42": [42] }
597          }
598        }"#;
599        let (catalog, policy, config, warnings) = load(catalog, policy).expect("fixture loads");
600        assert!(warnings.is_empty());
601        let mut backends: BTreeMap<String, Box<dyn Backend>> = BTreeMap::new();
602        backends.insert("bao".to_string(), backend);
603        let manager = BackendManager::new(catalog.clone(), backends).expect("manager builds");
604        Arc::new(BrokerState::new(
605            catalog,
606            policy,
607            config,
608            manager,
609            "mint-test",
610        ))
611    }
612
613    fn mint_state() -> Arc<BrokerState> {
614        state_with_backend(Box::new(MintBackend))
615    }
616
617    fn authed_request<T>(body: T) -> Request<T> {
618        let mut request = Request::new(body);
619        request.extensions_mut().insert(PeerInfo {
620            uid: Some(42),
621            ..PeerInfo::default()
622        });
623        request
624    }
625
626    fn ttl() -> Duration {
627        Duration {
628            seconds: 60,
629            nanos: 0,
630        }
631    }
632
633    fn rich_account_import(exporting: &KeyPair) -> basil_nats::AccountImport {
634        basil_nats::AccountImport {
635            name: "control-read".to_string(),
636            subject: "$JS.API.CONSUMER.MSG.NEXT.control_delivery".to_string(),
637            account: exporting.public_key(),
638            token: String::new(),
639            to: String::new(),
640            local_subject: "R3.$JS.API.CONSUMER.MSG.NEXT.control_delivery".to_string(),
641            kind: basil_nats::ExportType::Service,
642            share: true,
643            allow_trace: true,
644        }
645    }
646
647    fn rich_account_export(revocations: BTreeMap<String, i64>) -> basil_nats::AccountExport {
648        basil_nats::AccountExport {
649            name: "device-messages".to_string(),
650            subject: "dev.*.*.>".to_string(),
651            kind: basil_nats::ExportType::Stream,
652            token_req: true,
653            revocations,
654            response_type: None,
655            response_threshold: 0,
656            service_latency: None,
657            account_token_position: 2,
658            advertise: true,
659            allow_trace: true,
660            description: "realm device delivery".to_string(),
661            info_url: "https://basil.example.test/nats".to_string(),
662        }
663    }
664
665    fn rich_account_limits() -> basil_nats::OperatorLimits {
666        basil_nats::OperatorLimits {
667            nats: basil_nats::NatsLimits {
668                subs: 512,
669                data: 1_048_576,
670                payload: 262_144,
671            },
672            account: basil_nats::AccountLimits {
673                imports: 32,
674                exports: 32,
675                wildcards: true,
676                disallow_bearer: true,
677                conn: 256,
678                leaf: 8,
679            },
680            jetstream: basil_nats::JetStreamLimits {
681                mem_storage: 67_108_864,
682                disk_storage: 1_073_741_824,
683                streams: 64,
684                consumer: 512,
685                max_ack_pending: 10_000,
686                mem_max_stream_bytes: 8_388_608,
687                disk_max_stream_bytes: 134_217_728,
688                max_bytes_required: true,
689            },
690            tiered_limits: BTreeMap::new(),
691        }
692    }
693
694    fn rich_account_default_permissions() -> basil_nats::Permissions {
695        basil_nats::Permissions {
696            publish: basil_nats::Permission {
697                allow: vec!["dev.realm.device.>".to_string()],
698                deny: vec!["dev.realm.device.private.>".to_string()],
699            },
700            sub: basil_nats::Permission {
701                allow: vec!["dist.>".to_string(), "dev.realm.device.>".to_string()],
702                deny: Vec::new(),
703            },
704            resp: Some(basil_nats::ResponsePermission {
705                max: 1,
706                ttl: 2_000_000_000,
707            }),
708        }
709    }
710
711    fn rich_account_claims(exporting: &KeyPair, user: &KeyPair) -> basil_nats::AccountClaims {
712        let mut revocations = BTreeMap::new();
713        revocations.insert(user.public_key(), 1_782_000_001);
714        let mut export_revocations = BTreeMap::new();
715        export_revocations.insert("*".to_string(), 1_782_000_002);
716        basil_nats::AccountClaims {
717            imports: vec![rich_account_import(exporting)],
718            exports: vec![rich_account_export(export_revocations)],
719            limits: rich_account_limits(),
720            signing_keys: Vec::new(),
721            revocations,
722            default_permissions: Some(rich_account_default_permissions()),
723            mappings: BTreeMap::new(),
724            authorization: basil_nats::ExternalAuthorization::default(),
725            trace: Some(basil_nats::MsgTrace {
726                dest: "trace.realm".to_string(),
727                sampling: 25,
728            }),
729            cluster_traffic: Some(basil_nats::ClusterTraffic::System),
730        }
731    }
732
733    fn rich_account_jwt(
734        issuer_nkey: String,
735        account: &KeyPair,
736        signing: &KeyPair,
737        exporting: &KeyPair,
738        user: &KeyPair,
739    ) -> basil_nats::AccountJwt {
740        basil_nats::AccountJwt {
741            issuer: issuer_nkey,
742            subject_account: account.public_key(),
743            name: "basil-realm".to_string(),
744            issued_at: 1_782_000_000,
745            expires: None,
746            signing_keys: vec![signing.public_key()],
747            claims: rich_account_claims(exporting, user),
748        }
749    }
750
751    #[tokio::test]
752    #[allow(clippy::too_many_lines)]
753    async fn grpc_minting_methods_return_credentials() {
754        let service = BrokerGrpc::new(mint_state());
755        let generic = service
756            .mint_jwt(authed_request(pb::MintJwtRequest {
757                key_id: "issuer.account".to_string(),
758                subject: Some("subject".to_string()),
759                ttl: Some(ttl()),
760                claims: None,
761            }))
762            .await
763            .expect("generic mint succeeds")
764            .into_inner();
765        assert!(!generic.token.is_empty());
766        assert!(generic.expires_at.is_some());
767
768        let user_nkey = basil_nats::encode_public(basil_nats::NkeyType::User, &[2; 32])
769            .expect("test user public key encodes");
770        let user = service
771            .mint_nats_user(authed_request(pb::MintNatsUserRequest {
772                key_id: "issuer.account".to_string(),
773                subject_user_nkey: user_nkey,
774                issuer_account: None,
775                name: "user".to_string(),
776                ttl: Some(ttl()),
777                pub_allow: Vec::new(),
778                pub_deny: Vec::new(),
779                sub_allow: Vec::new(),
780                sub_deny: Vec::new(),
781            }))
782            .await
783            .expect("user mint succeeds")
784            .into_inner();
785        assert!(!user.token.is_empty());
786
787        let signed_nats = service
788            .sign_nats_jwt(authed_request(pb::SignNatsJwtRequest {
789                key_id: "issuer.account".to_string(),
790                claims_json: serde_json::to_vec(&serde_json::json!({
791                    "sub": basil_nats::encode_public(basil_nats::NkeyType::User, &[8; 32])
792                        .expect("test user public key encodes"),
793                    "name": "rich-user",
794                    "nats": { "type": "user", "version": 2 }
795                }))
796                .expect("claims json"),
797                expected_type: pb::NatsJwtType::User.into(),
798                ttl: Some(ttl()),
799                expires_at: None,
800                issued_at: None,
801                jti_mode: pb::NatsJtiMode::RequireValid.into(),
802            }))
803            .await
804            .expect("validated nats jwt sign succeeds")
805            .into_inner();
806        assert!(!signed_nats.token.is_empty());
807        assert!(signed_nats.expires_at.is_some());
808
809        let account_nkey = basil_nats::encode_public(basil_nats::NkeyType::Account, &[3; 32])
810            .expect("test account public key encodes");
811        let account = service
812            .mint_nats_account(authed_request(pb::MintNatsAccountRequest {
813                key_id: "issuer.operator".to_string(),
814                subject_account_nkey: account_nkey,
815                name: "account".to_string(),
816                ttl: Some(ttl()),
817                signing_keys: Vec::new(),
818            }))
819            .await
820            .expect("account mint succeeds")
821            .into_inner();
822        assert!(!account.token.is_empty());
823
824        let operator = service
825            .mint_nats_operator(authed_request(pb::MintNatsOperatorRequest {
826                key_id: "issuer.operator".to_string(),
827                subject_operator_nkey: None,
828                name: "operator".to_string(),
829                ttl: Some(ttl()),
830                signing_keys: Vec::new(),
831                account_server_url: None,
832                system_account: None,
833            }))
834            .await
835            .expect("operator mint succeeds")
836            .into_inner();
837        assert!(!operator.token.is_empty());
838
839        let signer = service
840            .mint_nats_signer(authed_request(pb::MintNatsSignerRequest {
841                key_id: "issuer.account".to_string(),
842                subject_nkey: basil_nats::encode_public(basil_nats::NkeyType::Account, &[4; 32])
843                    .expect("test account signer public key encodes"),
844                name: "signer".to_string(),
845                ttl: Some(ttl()),
846            }))
847            .await
848            .expect("signer mint succeeds")
849            .into_inner();
850        assert!(!signer.token.is_empty());
851
852        let server = service
853            .mint_nats_server(authed_request(pb::MintNatsServerRequest {
854                key_id: "issuer.server".to_string(),
855                subject_server_nkey: basil_nats::encode_public(
856                    basil_nats::NkeyType::Server,
857                    &[5; 32],
858                )
859                .expect("test server public key encodes"),
860                name: "server".to_string(),
861                ttl: Some(ttl()),
862            }))
863            .await
864            .expect("server mint succeeds")
865            .into_inner();
866        assert!(!server.token.is_empty());
867
868        let curve = service
869            .mint_nats_curve(authed_request(pb::MintNatsCurveRequest {
870                key_id: "issuer.curve".to_string(),
871                subject_curve_nkey: basil_nats::encode_public(
872                    basil_nats::NkeyType::Curve,
873                    &[6; 32],
874                )
875                .expect("test curve public key encodes"),
876                name: "curve".to_string(),
877                ttl: Some(ttl()),
878            }))
879            .await
880            .expect("curve mint succeeds")
881            .into_inner();
882        assert!(!curve.token.is_empty());
883    }
884
885    #[tokio::test]
886    async fn sign_nats_jwt_ttl_uses_claim_iat_as_base() {
887        let account = KeyPair::new_account();
888        let user = KeyPair::new_user();
889        let service = BrokerGrpc::new(state_with_backend(Box::new(NatsSignBackend(account))));
890        let token = service
891            .sign_nats_jwt(authed_request(pb::SignNatsJwtRequest {
892                key_id: "issuer.account".to_string(),
893                claims_json: serde_json::to_vec(&serde_json::json!({
894                    "iat": 1_700_000_000_u64,
895                    "sub": user.public_key(),
896                    "nats": { "type": "user", "version": 2 }
897                }))
898                .expect("claims json"),
899                expected_type: pb::NatsJwtType::User.into(),
900                ttl: Some(Duration {
901                    seconds: 60,
902                    nanos: 0,
903                }),
904                expires_at: None,
905                issued_at: None,
906                jti_mode: pb::NatsJtiMode::RequireValid.into(),
907            }))
908            .await
909            .expect("sign succeeds")
910            .into_inner()
911            .token;
912
913        let parts: Vec<&str> = token.split('.').collect();
914        let claims: JsonValue = serde_json::from_slice(
915            &base64::engine::general_purpose::URL_SAFE_NO_PAD
916                .decode(parts[1])
917                .expect("claims decode"),
918        )
919        .expect("claims json");
920        assert_eq!(claims["iat"], 1_700_000_000_u64);
921        assert_eq!(claims["exp"], 1_700_000_060_u64);
922    }
923
924    #[tokio::test]
925    async fn grpc_nats_curve_encrypt_decrypt_interops_with_nkeys() {
926        let service = BrokerGrpc::new(mint_state());
927        let broker_private = [0x55; 32];
928        let broker_xkey = XKey::new_from_raw(broker_private);
929        let peer = XKey::new_from_raw([0x66; 32]);
930
931        let encrypted = service
932            .encrypt_nats_curve(authed_request(pb::EncryptNatsCurveRequest {
933                key_id: "nats.curve_box".to_string(),
934                recipient_public_xkey: peer.public_key(),
935                plaintext: b"broker-to-peer".to_vec(),
936            }))
937            .await
938            .expect("encrypt succeeds")
939            .into_inner()
940            .ciphertext;
941        let opened_by_peer = peer
942            .open(&encrypted, &broker_xkey)
943            .expect("nkeys opens broker ciphertext");
944        assert_eq!(opened_by_peer, b"broker-to-peer");
945
946        let peer_box = peer
947            .seal(b"peer-to-broker", &broker_xkey)
948            .expect("nkeys seals");
949        let decrypted = service
950            .decrypt_nats_curve(authed_request(pb::DecryptNatsCurveRequest {
951                key_id: "nats.curve_box".to_string(),
952                sender_public_xkey: peer.public_key(),
953                ciphertext: peer_box,
954            }))
955            .await
956            .expect("decrypt succeeds")
957            .into_inner()
958            .plaintext;
959        assert_eq!(decrypted, b"peer-to-broker");
960
961        let denied = service
962            .encrypt_nats_curve(authed_request(pb::EncryptNatsCurveRequest {
963                key_id: "issuer.account".to_string(),
964                recipient_public_xkey: peer.public_key(),
965                plaintext: b"wrong class".to_vec(),
966            }))
967            .await
968            .expect_err("policy denies ungranted target");
969        assert_eq!(denied.code(), Code::PermissionDenied);
970    }
971
972    #[tokio::test]
973    async fn grpc_sign_completes_externally_assembled_rich_nats_jwt() {
974        let operator = KeyPair::new_operator();
975        let operator_public = operator.public_key();
976        let account = KeyPair::new_account();
977        let signing = KeyPair::new_account();
978        let exporting = KeyPair::new_account();
979        let user = KeyPair::new_user();
980        let service = BrokerGrpc::new(state_with_backend(Box::new(NatsSignBackend(operator))));
981
982        let public = service
983            .get_public_key(authed_request(pb::GetPublicKeyRequest {
984                key_id: "issuer.operator".to_string(),
985                version: None,
986            }))
987            .await
988            .expect("public key read succeeds")
989            .into_inner()
990            .public_key;
991        let issuer_nkey = basil_nats::encode_public(basil_nats::NkeyType::Operator, &public)
992            .expect("issuer public encodes as operator nkey");
993        assert_eq!(issuer_nkey, operator_public);
994
995        let jwt = rich_account_jwt(issuer_nkey, &account, &signing, &exporting, &user);
996        let signing_input = jwt.signing_input().expect("rich signing input builds");
997
998        let signature = service
999            .sign(authed_request(pb::SignRequest {
1000                key_id: "issuer.operator".to_string(),
1001                message: signing_input.as_bytes().to_vec(),
1002                algorithm: pb::SigningAlgorithm::Ed25519Nkey.into(),
1003            }))
1004            .await
1005            .expect("broker signs NATS JWT input")
1006            .into_inner()
1007            .signature;
1008        let token = basil_nats::assemble(&signing_input, &signature);
1009
1010        let parts: Vec<&str> = token.split('.').collect();
1011        assert_eq!(parts.len(), 3);
1012        let claims: JsonValue = serde_json::from_slice(
1013            &base64::engine::general_purpose::URL_SAFE_NO_PAD
1014                .decode(parts[1])
1015                .expect("claims decode"),
1016        )
1017        .expect("claims json");
1018        let nats = &claims["nats"];
1019        assert_eq!(
1020            nats["imports"][0]["local_subject"],
1021            "R3.$JS.API.CONSUMER.MSG.NEXT.control_delivery"
1022        );
1023        assert_eq!(nats["exports"][0]["token_req"], true);
1024        assert_eq!(nats["limits"]["disk_storage"], 1_073_741_824);
1025        assert_eq!(nats["default_permissions"]["resp"]["max"], 1);
1026        assert_eq!(nats["trace"]["dest"], "trace.realm");
1027
1028        let issuer = KeyPair::from_public_key(claims["iss"].as_str().expect("issuer claim"))
1029            .expect("issuer public key parses");
1030        issuer
1031            .verify(signing_input.as_bytes(), &signature)
1032            .expect("Basil signature verifies under issuer nkey");
1033    }
1034
1035    #[tokio::test]
1036    #[allow(clippy::too_many_lines)]
1037    async fn grpc_validate_nats_jwt_reports_authoritative_reasons() {
1038        let account = KeyPair::new_account();
1039        let account_public = account.public_key();
1040        let user = KeyPair::new_user();
1041        let service = BrokerGrpc::new(state_with_backend(Box::new(NatsSignBackend(account))));
1042
1043        let token = service
1044            .sign_nats_jwt(authed_request(pb::SignNatsJwtRequest {
1045                key_id: "issuer.account".to_string(),
1046                claims_json: serde_json::to_vec(&serde_json::json!({
1047                    "sub": user.public_key(),
1048                    "name": "valid-user",
1049                    "nats": { "type": "user", "version": 2 }
1050                }))
1051                .expect("claims json"),
1052                expected_type: pb::NatsJwtType::User.into(),
1053                ttl: Some(ttl()),
1054                expires_at: None,
1055                issued_at: None,
1056                jti_mode: pb::NatsJtiMode::RequireValid.into(),
1057            }))
1058            .await
1059            .expect("sign succeeds")
1060            .into_inner()
1061            .token;
1062
1063        let by_key = service
1064            .validate_nats_jwt(authed_request(pb::ValidateNatsJwtRequest {
1065                jwt: token.clone(),
1066                allowed_signers: vec![pb::AllowedNatsSigner {
1067                    signer: Some(pb::allowed_nats_signer::Signer::KeyId(
1068                        "issuer.account".to_string(),
1069                    )),
1070                }],
1071                expected_type: pb::NatsJwtType::User.into(),
1072            }))
1073            .await
1074            .expect("validate by key succeeds")
1075            .into_inner();
1076        assert!(by_key.valid);
1077        assert_eq!(by_key.reason, i32::from(pb::NatsJwtValidationReason::Valid));
1078        assert_eq!(by_key.issuer, account_public.as_str());
1079        assert_eq!(by_key.subject, user.public_key());
1080        assert_eq!(by_key.matched_signer_key_id, "issuer.account");
1081
1082        let by_public_nkey = service
1083            .validate_nats_jwt(authed_request(pb::ValidateNatsJwtRequest {
1084                jwt: token.clone(),
1085                allowed_signers: vec![pb::AllowedNatsSigner {
1086                    signer: Some(pb::allowed_nats_signer::Signer::NatsPublicKey(
1087                        account_public.clone(),
1088                    )),
1089                }],
1090                expected_type: pb::NatsJwtType::User.into(),
1091            }))
1092            .await
1093            .expect("validate by nkey succeeds")
1094            .into_inner();
1095        assert!(by_public_nkey.valid);
1096        assert!(by_public_nkey.matched_signer_key_id.is_empty());
1097
1098        let wrong_type = service
1099            .validate_nats_jwt(authed_request(pb::ValidateNatsJwtRequest {
1100                jwt: token.clone(),
1101                allowed_signers: Vec::new(),
1102                expected_type: pb::NatsJwtType::Account.into(),
1103            }))
1104            .await
1105            .expect("wrong type is authoritative")
1106            .into_inner();
1107        assert!(!wrong_type.valid);
1108        assert_eq!(
1109            wrong_type.reason,
1110            i32::from(pb::NatsJwtValidationReason::WrongType)
1111        );
1112
1113        let unknown = service
1114            .validate_nats_jwt(authed_request(pb::ValidateNatsJwtRequest {
1115                jwt: token,
1116                allowed_signers: vec![pb::AllowedNatsSigner {
1117                    signer: Some(pb::allowed_nats_signer::Signer::NatsPublicKey(
1118                        KeyPair::new_operator().public_key(),
1119                    )),
1120                }],
1121                expected_type: pb::NatsJwtType::User.into(),
1122            }))
1123            .await
1124            .expect("unknown signer is authoritative")
1125            .into_inner();
1126        assert!(!unknown.valid);
1127        assert_eq!(
1128            unknown.reason,
1129            i32::from(pb::NatsJwtValidationReason::UnknownSigner)
1130        );
1131
1132        let malformed = service
1133            .validate_nats_jwt(authed_request(pb::ValidateNatsJwtRequest {
1134                jwt: "not-a-jwt".to_string(),
1135                allowed_signers: Vec::new(),
1136                expected_type: pb::NatsJwtType::Unspecified.into(),
1137            }))
1138            .await
1139            .expect("malformed is authoritative")
1140            .into_inner();
1141        assert!(!malformed.valid);
1142        assert_eq!(
1143            malformed.reason,
1144            i32::from(pb::NatsJwtValidationReason::Malformed)
1145        );
1146    }
1147
1148    #[tokio::test]
1149    async fn grpc_nats_unsupported_issuer_role_is_invalid_argument() {
1150        let service = BrokerGrpc::new(mint_state());
1151        let status = service
1152            .mint_nats_server(authed_request(pb::MintNatsServerRequest {
1153                key_id: "issuer.operator".to_string(),
1154                subject_server_nkey: basil_nats::encode_public(
1155                    basil_nats::NkeyType::Server,
1156                    &[5; 32],
1157                )
1158                .expect("test server public key encodes"),
1159                name: "server".to_string(),
1160                ttl: Some(ttl()),
1161            }))
1162            .await
1163            .expect_err("operator issuer is invalid for server mint");
1164        let info = error_info(&status);
1165        assert_eq!(status.code(), Code::InvalidArgument);
1166        assert_eq!(info.reason, "INVALID_REQUEST");
1167        assert_eq!(info.op, "mint_nats_server");
1168    }
1169
1170    #[tokio::test]
1171    async fn watch_requires_peer_uid() {
1172        let service = BrokerGrpc::new(mint_state());
1173        let Err(status) = service
1174            .watch(Request::new(pb::WatchRequest { kinds: Vec::new() }))
1175            .await
1176        else {
1177            panic!("missing uid should be rejected");
1178        };
1179        assert_eq!(status.code(), Code::Unauthenticated);
1180    }
1181
1182    #[tokio::test]
1183    async fn watch_filters_key_rotation_and_allows_public_events() {
1184        use futures::StreamExt as _;
1185
1186        let state = mint_state();
1187        let service = BrokerGrpc::new(Arc::clone(&state));
1188        let mut stream = service
1189            .watch(authed_request(pb::WatchRequest { kinds: Vec::new() }))
1190            .await
1191            .expect("watch opens")
1192            .into_inner();
1193
1194        state.events().key_rotated("hidden.key", 2);
1195        state.events().bundle_changed("example.org");
1196
1197        let event = tokio::time::timeout(std::time::Duration::from_secs(1), stream.next())
1198            .await
1199            .expect("event arrives")
1200            .expect("stream item")
1201            .expect("event ok");
1202        assert_eq!(event.kind, i32::from(pb::EventKind::BundleChanged));
1203        assert!(matches!(
1204            event.detail,
1205            Some(pb::event::Detail::BundleChanged(_))
1206        ));
1207
1208        state.events().key_rotated("issuer.account", 3);
1209        let event = tokio::time::timeout(std::time::Duration::from_secs(1), stream.next())
1210            .await
1211            .expect("event arrives")
1212            .expect("stream item")
1213            .expect("event ok");
1214        assert_eq!(event.kind, i32::from(pb::EventKind::KeyRotated));
1215        assert!(matches!(
1216            event.detail,
1217            Some(pb::event::Detail::KeyRotated(_))
1218        ));
1219    }
1220
1221    #[tokio::test]
1222    async fn watch_emits_revocation_events() {
1223        use futures::StreamExt as _;
1224
1225        let state = mint_state();
1226        let service = BrokerGrpc::new(Arc::clone(&state));
1227        let mut stream = service
1228            .watch(authed_request(pb::WatchRequest {
1229                kinds: vec![i32::from(pb::EventKind::Revoked)],
1230            }))
1231            .await
1232            .expect("watch opens")
1233            .into_inner();
1234
1235        state
1236            .revoke_jwt_svid(
1237                "example.org",
1238                "test-jti",
1239                jsonwebtoken::get_current_timestamp().saturating_add(300),
1240            )
1241            .await
1242            .expect("revoked jti stored");
1243
1244        let event = tokio::time::timeout(std::time::Duration::from_secs(1), stream.next())
1245            .await
1246            .expect("event arrives")
1247            .expect("stream item")
1248            .expect("event ok");
1249        assert_eq!(event.kind, i32::from(pb::EventKind::Revoked));
1250        let Some(pb::event::Detail::Revoked(revoked)) = event.detail else {
1251            panic!("revoked detail expected");
1252        };
1253        assert_eq!(revoked.trust_domain, "example.org");
1254        assert_eq!(revoked.id, "test-jti");
1255    }
1256}