1#![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#[derive(Debug, Clone, PartialEq, Eq)]
31pub struct BrokerIdentityRuntimeConfig {
32 pub id: String,
34 pub response_signing_key_id: String,
36}
37
38#[derive(Debug, Clone, PartialEq, Eq)]
40pub struct InvocationRuntimeConfig {
41 pub enabled: bool,
43 pub broker_identity: Option<BrokerIdentityRuntimeConfig>,
45 pub audiences: Vec<String>,
48 pub request_encryption_key_id: Option<String>,
50 pub max_ttl_secs: u32,
52 pub clock_skew_secs: u32,
54 pub replay_cache_capacity: usize,
56 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#[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 #[must_use]
88 pub fn new(state: Arc<BrokerState>) -> Self {
89 Self::new_with_invocation(state, false)
90 }
91
92 #[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 #[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 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 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 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}