Skip to main content

basil_core/service/
broker.rs

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