1#![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#[derive(Debug, Clone, PartialEq, Eq)]
27pub struct BrokerIdentityRuntimeConfig {
28 pub id: String,
30 pub response_signing_key_id: String,
32}
33
34#[derive(Debug, Clone, PartialEq, Eq)]
36pub struct InvocationRuntimeConfig {
37 pub enabled: bool,
39 pub broker_identity: Option<BrokerIdentityRuntimeConfig>,
41 pub audiences: Vec<String>,
44 pub request_encryption_key_id: Option<String>,
46 pub max_ttl_secs: u32,
48 pub clock_skew_secs: u32,
50 pub replay_cache_capacity: usize,
52 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#[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 #[must_use]
84 pub fn new(state: Arc<BrokerState>) -> Self {
85 Self::new_with_invocation(state, false)
86 }
87
88 #[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 #[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 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 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 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}