Skip to main content

basil_nats/
lib.rs

1// SPDX-FileCopyrightText: 2026 OpenBasil Contributors
2//
3// SPDX-License-Identifier: Apache-2.0
4
5//! `basil-nats`: build NATS JWTs (the `ed25519-nkey` JWS profile) **without
6//! holding the signing key**.
7//!
8//! NATS credentials (`nsc`-style) are JWTs signed by an Ed25519 `NKey`. This crate
9//! produces the exact wire bytes a NATS server expects, but *splits signing out*:
10//! you build the **signing input**, hand it to whatever holds the key (an HSM, a
11//! `nkeys::KeyPair`, or a Vault transit engine) to produce a raw 64-byte
12//! Ed25519 signature, then [`assemble`] the final token. The key never has to
13//! enter this process, which is the whole point of using it from a broker.
14//!
15//! Wire format (matches `nats-io/jwt` v2 / `nsc`):
16//! - header: `{"typ":"JWT","alg":"ed25519-nkey"}`
17//! - `jti`: base32 (no pad) of **SHA-512/256** over the *standard* claims only
18//!   (`aud,exp,jti="",iat,iss,name,nbf,sub`). The `nats` object is excluded.
19//! - header & claims: `base64url` no-pad; signing input is `header.claims`.
20//! - signature: raw 64-byte Ed25519, `base64url` no-pad, appended as the 3rd part.
21//!
22//! Claim *field order* does not affect validity (servers parse JSON), but the
23//! `jti` hash is order-sensitive, so the hash struct mirrors `nats-io/jwt`.
24
25#![no_std]
26// Index/slice in test code is fine (fixed test vectors); the no-panic
27// `indexing_slicing` gate has no test-allow config option, unlike unwrap/expect.
28#![cfg_attr(test, allow(clippy::indexing_slicing))]
29
30extern crate alloc;
31
32use alloc::collections::BTreeMap;
33use alloc::format;
34use alloc::string::String;
35use alloc::vec::Vec;
36use base64::Engine;
37use base64::engine::general_purpose::URL_SAFE_NO_PAD;
38use crypto_secretbox::aead::{Aead, KeyInit};
39use crypto_secretbox::{Key as SecretboxKey, Nonce as SecretboxNonce, XSalsa20Poly1305};
40use ed25519_dalek::{Signature, Verifier, VerifyingKey};
41use rand_core::TryRng;
42use salsa20::cipher::consts::{U10, U16};
43use salsa20::hsalsa;
44use serde::de::DeserializeOwned;
45use serde::{Deserialize, Serialize};
46use serde_json::Value;
47use sha2::{Digest, Sha512_256};
48use x25519_dalek::{PublicKey as X25519PublicKey, StaticSecret};
49use zeroize::Zeroizing;
50
51/// The fixed JWS header for NATS tokens (`typ` before `alg`, as `nsc` emits).
52const HEADER_JSON: &str = r#"{"typ":"JWT","alg":"ed25519-nkey"}"#;
53const XKEY_VERSION_V1: &[u8; 4] = b"xkv1";
54const XKEY_NONCE_LEN: usize = 24;
55const XKEY_TAG_LEN: usize = 16;
56
57/// The role of a public NATS `NKey`, identified by its single base32 prefix
58/// letter (the first character of an encoded key string).
59///
60/// # Wire derivation
61///
62/// A public key's wire form is `prefix_byte || 32-byte key || crc16`, base32
63/// encoded. base32 packs 5 bits per output character, so the first character is
64/// exactly the top 5 bits of `prefix_byte`. NATS chooses the role value so that
65/// first character *is* the role letter: `prefix_byte = base32_index(letter) << 3`,
66/// which lands the letter's 5-bit alphabet index in the high 5 bits and leaves
67/// the low 3 bits zero. Those 3 bits just roll into the *second* base32 character
68/// with the top 2 bits of the key. (base32 alphabet
69/// `ABCDEFGHIJKLMNOPQRSTUVWXYZ234567`: A=0 C=2 N=13 O=14 U=20 X=23.)
70///
71/// (SEED keys look different, `SU`/`SA`/`SO`, because a seed is *not*
72/// `role << 3`: it OR-packs an `S` marker with the role and spills the role's low
73/// bits into a second header byte. Only *public* keys are encoded here, so the
74/// simple `role << 3` form holds.)
75#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
76pub enum NkeyType {
77    /// Account key (`A…`).
78    Account,
79    /// Cluster key (`C…`).
80    Cluster,
81    /// Server key (`N…`).
82    Server,
83    /// Operator key (`O…`).
84    Operator,
85    /// User key (`U…`).
86    User,
87    /// Curve / x25519 (`xkey`) key (`X…`); also NATS's catch-all role.
88    Curve,
89}
90
91impl NkeyType {
92    /// Every supported public role, in prefix-letter order: the lookup table
93    /// backing the letter/byte conversions.
94    const ALL: [Self; 6] = [
95        Self::Account,
96        Self::Cluster,
97        Self::Server,
98        Self::Operator,
99        Self::User,
100        Self::Curve,
101    ];
102
103    /// The role's `NKey` prefix letter (the first character of an encoded key).
104    #[must_use]
105    pub const fn letter(self) -> char {
106        match self {
107            Self::Account => 'A',
108            Self::Cluster => 'C',
109            Self::Server => 'N',
110            Self::Operator => 'O',
111            Self::User => 'U',
112            Self::Curve => 'X',
113        }
114    }
115
116    /// The wire prefix byte (`base32_index(letter) << 3`) this role encodes to.
117    #[must_use]
118    pub const fn prefix_byte(self) -> u8 {
119        // base32 index of `letter`, shifted into the high 5 bits (low 3 zero).
120        match self {
121            Self::Account => 0,        // 'A' (index 0; 0 << 3 == 0)
122            Self::Cluster => 2 << 3,   // 'C'
123            Self::Server => 13 << 3,   // 'N'
124            Self::Operator => 14 << 3, // 'O'
125            Self::User => 20 << 3,     // 'U'
126            Self::Curve => 23 << 3,    // 'X'
127        }
128    }
129
130    /// Parse a role from its prefix letter, or `None` if `letter` is not one of
131    /// the supported public roles (`A`/`C`/`N`/`O`/`U`/`X`).
132    #[must_use]
133    pub fn from_letter(letter: char) -> Option<Self> {
134        Self::ALL.into_iter().find(|role| role.letter() == letter)
135    }
136
137    /// Parse a role from a decoded wire prefix byte, or `None` if it is not a
138    /// known public role prefix.
139    #[must_use]
140    fn from_prefix_byte(byte: u8) -> Option<Self> {
141        Self::ALL
142            .into_iter()
143            .find(|role| role.prefix_byte() == byte)
144    }
145}
146
147impl core::fmt::Display for NkeyType {
148    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
149        write!(f, "{}", self.letter())
150    }
151}
152
153#[derive(Debug)]
154pub enum Error {
155    /// A public `NKey` was not valid base32-nopad text.
156    BadEncoding,
157    /// A public key was not the expected 32 bytes.
158    BadPublicKeyLen(usize),
159    /// A decoded `NKey` carried a prefix letter that is not a known public
160    /// role. The supported public-key roles are `A` account, `C` cluster, `N`
161    /// server, `O` operator, `U` user, and `X` curve/x25519.
162    UnsupportedPrefix(char),
163    /// A public `NKey` decoded cleanly, but its [`NkeyType`] did not match the
164    /// role required for the claim shape being minted.
165    UnexpectedPrefix {
166        expected: NkeyType,
167        actual: NkeyType,
168    },
169    /// JSON serialization failed (should not happen for these types).
170    Json(serde_json::Error),
171    /// A caller-supplied claim document is not a NATS JWT claim object.
172    InvalidClaims(String),
173    /// A compact JWT was not a valid NATS JWT.
174    MalformedJwt(String),
175    /// A compact JWT signature segment did not decode to a 64-byte Ed25519
176    /// signature.
177    BadSignatureLen(usize),
178    /// A NATS xkey operation was given a non-curve public `NKey`.
179    UnexpectedXKeyPrefix(NkeyType),
180    /// A NATS xkey ciphertext did not carry the `xkv1` version prefix.
181    BadXKeyVersion,
182    /// A NATS xkey ciphertext was too short to contain version, nonce, and tag.
183    BadXKeyCiphertextLen(usize),
184    /// The caller-supplied RNG failed while generating a NATS xkey nonce.
185    XKeyRandomnessFailed,
186    /// NATS xkey encryption failed.
187    XKeySealFailed,
188    /// NATS xkey authentication failed on open.
189    XKeyOpenFailed,
190}
191
192impl core::fmt::Display for Error {
193    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
194        match self {
195            Self::BadEncoding => f.write_str("invalid NKey base32 encoding"),
196            Self::BadPublicKeyLen(n) => write!(f, "expected a 32-byte Ed25519 public key, got {n}"),
197            Self::UnsupportedPrefix(c) => write!(f, "unsupported NKey prefix letter: {c}"),
198            Self::UnexpectedPrefix { expected, actual } => {
199                write!(f, "expected NKey role {expected}, got {actual}")
200            }
201            Self::Json(e) => write!(f, "json error: {e}"),
202            Self::InvalidClaims(message) | Self::MalformedJwt(message) => f.write_str(message),
203            Self::BadSignatureLen(n) => write!(f, "expected a 64-byte Ed25519 signature, got {n}"),
204            Self::UnexpectedXKeyPrefix(actual) => {
205                write!(f, "expected curve xkey prefix X, got {actual}")
206            }
207            Self::BadXKeyVersion => f.write_str("NATS xkey ciphertext has unsupported version"),
208            Self::BadXKeyCiphertextLen(n) => {
209                write!(f, "NATS xkey ciphertext is too short: {n} bytes")
210            }
211            Self::XKeyRandomnessFailed => f.write_str("NATS xkey randomness failed"),
212            Self::XKeySealFailed => f.write_str("NATS xkey seal failed"),
213            Self::XKeyOpenFailed => f.write_str("NATS xkey open failed"),
214        }
215    }
216}
217impl core::error::Error for Error {}
218impl From<serde_json::Error> for Error {
219    fn from(e: serde_json::Error) -> Self {
220        Self::Json(e)
221    }
222}
223
224// ---------------------------------------------------------------------------
225// NKey encoding
226// ---------------------------------------------------------------------------
227
228/// CRC-16/XMODEM (poly `0x1021`, init `0x0000`), the checksum NATS `NKeys` use.
229fn crc16(data: &[u8]) -> u16 {
230    let mut crc: u16 = 0;
231    for &b in data {
232        crc ^= u16::from(b) << 8;
233        for _ in 0..8 {
234            crc = if crc & 0x8000 != 0 {
235                (crc << 1) ^ 0x1021
236            } else {
237                crc << 1
238            };
239        }
240    }
241    crc
242}
243
244fn encode_nkey(prefix: u8, public: &[u8]) -> Result<String, Error> {
245    if public.len() != 32 {
246        return Err(Error::BadPublicKeyLen(public.len()));
247    }
248    let mut raw = Vec::with_capacity(1 + 32 + 2);
249    raw.push(prefix);
250    raw.extend_from_slice(public);
251    let crc = crc16(&raw);
252    raw.push((crc & 0xff) as u8); // little-endian
253    raw.push((crc >> 8) as u8);
254    Ok(data_encoding::BASE32_NOPAD.encode(&raw))
255}
256
257/// Encode a raw 32-byte public key as the public `NKey` for `role`.
258///
259/// This is the catalog-driven path: an issuer key's `nats_type` label maps to an
260/// [`NkeyType`], and the broker encodes its backend public half accordingly. The
261/// bytes are encoded as-is. For [`NkeyType::Curve`] the caller is responsible
262/// for the key actually being an x25519/curve key (the role only sets the prefix
263/// letter, not the algorithm).
264///
265/// # Errors
266///
267/// [`Error::BadPublicKeyLen`] if `public` is not 32 bytes.
268pub fn encode_public(role: NkeyType, public: &[u8]) -> Result<String, Error> {
269    encode_nkey(role.prefix_byte(), public)
270}
271
272/// Decode any public `NKey` string back to its [`NkeyType`] role and raw 32-byte
273/// Ed25519 key, verifying the CRC.
274///
275/// # Errors
276///
277/// - [`Error::BadEncoding`] if `nkey` is not valid base32-nopad text.
278/// - [`Error::BadPublicKeyLen`] if `nkey` does not decode to a valid public
279///   `NKey` payload length or CRC.
280/// - [`Error::UnsupportedPrefix`] if the decoded prefix is not a known public
281///   role.
282pub fn decode_public(nkey: &str) -> Result<(NkeyType, [u8; 32]), Error> {
283    let raw = data_encoding::BASE32_NOPAD
284        .decode(nkey.as_bytes())
285        .map_err(|_| Error::BadEncoding)?;
286    // Destructure the 35-byte layout (1 prefix + 32 key + 2 CRC) into
287    // fixed-size chunks. `split_at_checked` proves each length to the compiler
288    // so no slice index can panic; the CRC covers prefix+key (`body`).
289    let Some((body, crc_bytes)) = raw.split_at_checked(33) else {
290        return Err(Error::BadPublicKeyLen(raw.len()));
291    };
292    let (Ok(crc_bytes), Some((&prefix, key))) =
293        (<[u8; 2]>::try_from(crc_bytes), body.split_first())
294    else {
295        return Err(Error::BadPublicKeyLen(raw.len()));
296    };
297    let Ok(key) = <[u8; 32]>::try_from(key) else {
298        return Err(Error::BadPublicKeyLen(raw.len()));
299    };
300    let crc = u16::from_le_bytes(crc_bytes);
301    if crc != crc16(body) {
302        return Err(Error::BadPublicKeyLen(raw.len()));
303    }
304    let role = NkeyType::from_prefix_byte(prefix)
305        .ok_or_else(|| Error::UnsupportedPrefix(nkey.chars().next().unwrap_or('?')))?;
306    Ok((role, key))
307}
308
309/// Decode a public `NKey` and require a specific [`NkeyType`] role.
310///
311/// # Errors
312///
313/// - [`Error::BadEncoding`] if `nkey` is not valid base32-nopad text.
314/// - [`Error::BadPublicKeyLen`] if `nkey` does not decode to a valid public
315///   `NKey` payload length or CRC.
316/// - [`Error::UnsupportedPrefix`] if its prefix is not a supported public role.
317/// - [`Error::UnexpectedPrefix`] if the key is valid but has the wrong role.
318pub fn require_public_prefix(nkey: &str, expected: NkeyType) -> Result<[u8; 32], Error> {
319    let (actual, key) = decode_public(nkey)?;
320    if actual != expected {
321        return Err(Error::UnexpectedPrefix { expected, actual });
322    }
323    Ok(key)
324}
325
326/// Verify a raw Ed25519 signature against a public `NKey`.
327///
328/// This is the sealed-invocation trust-anchor path: policy stores the public
329/// `NKey`, the caller supplies the signed transcript bytes and raw 64-byte
330/// signature, and Basil verifies without parsing a JWT.
331///
332/// # Errors
333///
334/// Returns [`Error`] when `nkey` is malformed or `signature` is not a 64-byte
335/// Ed25519 signature.
336pub fn verify_public_signature(
337    nkey: &str,
338    message: &[u8],
339    signature: &[u8],
340) -> Result<bool, Error> {
341    let (_, public_key) = decode_public(nkey)?;
342    let signature =
343        <[u8; 64]>::try_from(signature).map_err(|_| Error::BadSignatureLen(signature.len()))?;
344    Ok(verify_ed25519(&public_key, message, &signature))
345}
346
347/// Derive a public xkey from raw 32-byte X25519 private material.
348#[must_use]
349pub fn xkey_public_from_private(private: &Zeroizing<[u8; 32]>) -> [u8; 32] {
350    let secret = StaticSecret::from(**private);
351    X25519PublicKey::from(&secret).to_bytes()
352}
353
354/// Encrypt a small payload using the NATS xkey authenticated box format.
355///
356/// The wire format matches `nats-io/nkeys`: `xkv1 || 24-byte nonce ||
357/// XSalsa20-Poly1305 ciphertext`. The sender private is materialized by the
358/// caller and zeroized there; this function keeps ECDH and derived-key
359/// intermediates in zeroizing buffers. The 24-byte nonce is drawn from the
360/// caller-supplied `rng`, so the crate stays `no_std` and does not choose an
361/// ambient OS or thread-local RNG for the caller. Pass a CSPRNG; nonce reuse
362/// with the same sender/recipient key pair breaks the xkey confidentiality and
363/// authenticity assumptions.
364///
365/// # Errors
366///
367/// Returns [`Error::UnexpectedXKeyPrefix`] when `recipient_public_xkey` is not an
368/// `X...` public key, [`Error::BadEncoding`] / [`Error::BadPublicKeyLen`] for
369/// malformed nkeys, or
370/// [`Error::XKeyRandomnessFailed`] when the caller-supplied RNG fails, or
371/// [`Error::XKeySealFailed`] for low-order keys or an AEAD failure.
372pub fn seal_nats_curve(
373    sender_private: &Zeroizing<[u8; 32]>,
374    recipient_public_xkey: &str,
375    plaintext: &[u8],
376    rng: &mut impl TryRng,
377) -> Result<Vec<u8>, Error> {
378    let recipient_public = decode_xkey_public(recipient_public_xkey)?;
379    let mut nonce = [0u8; XKEY_NONCE_LEN];
380    rng.try_fill_bytes(&mut nonce)
381        .map_err(|_| Error::XKeyRandomnessFailed)?;
382    let ciphertext = box_crypt(
383        sender_private,
384        &recipient_public,
385        &nonce,
386        plaintext,
387        BoxMode::Seal,
388    )?;
389    let mut out = Vec::with_capacity(XKEY_VERSION_V1.len() + nonce.len() + ciphertext.len());
390    out.extend_from_slice(XKEY_VERSION_V1);
391    out.extend_from_slice(&nonce);
392    out.extend_from_slice(&ciphertext);
393    Ok(out)
394}
395
396/// Decrypt a NATS xkey authenticated box.
397///
398/// # Errors
399///
400/// Returns [`Error::BadXKeyVersion`] / [`Error::BadXKeyCiphertextLen`] for
401/// malformed wire bytes, [`Error::UnexpectedXKeyPrefix`] for a non-`X...` sender
402/// key, or [`Error::XKeyOpenFailed`] for authentication failure.
403pub fn open_nats_curve(
404    recipient_private: &Zeroizing<[u8; 32]>,
405    sender_public_xkey: &str,
406    ciphertext: &[u8],
407) -> Result<Zeroizing<Vec<u8>>, Error> {
408    let sender_public = decode_xkey_public(sender_public_xkey)?;
409    let Some(rest) = ciphertext.strip_prefix(XKEY_VERSION_V1) else {
410        return Err(Error::BadXKeyVersion);
411    };
412    // An authentic box of the empty plaintext is exactly nonce + tag bytes.
413    if rest.len() < XKEY_NONCE_LEN + XKEY_TAG_LEN {
414        return Err(Error::BadXKeyCiphertextLen(ciphertext.len()));
415    }
416    let Some((nonce, body)) = rest.split_at_checked(XKEY_NONCE_LEN) else {
417        return Err(Error::BadXKeyCiphertextLen(ciphertext.len()));
418    };
419    let nonce: [u8; XKEY_NONCE_LEN] = nonce
420        .try_into()
421        .map_err(|_| Error::BadXKeyCiphertextLen(ciphertext.len()))?;
422    box_crypt(
423        recipient_private,
424        &sender_public,
425        &nonce,
426        body,
427        BoxMode::Open,
428    )
429    .map(Zeroizing::new)
430}
431
432fn decode_xkey_public(nkey: &str) -> Result<[u8; 32], Error> {
433    let (actual, key) = decode_public(nkey)?;
434    if actual != NkeyType::Curve {
435        return Err(Error::UnexpectedXKeyPrefix(actual));
436    }
437    Ok(key)
438}
439
440#[derive(Clone, Copy)]
441enum BoxMode {
442    Seal,
443    Open,
444}
445
446fn box_crypt(
447    private: &Zeroizing<[u8; 32]>,
448    peer_public: &[u8; 32],
449    nonce: &[u8; XKEY_NONCE_LEN],
450    input: &[u8],
451    mode: BoxMode,
452) -> Result<Vec<u8>, Error> {
453    let private = StaticSecret::from(**private);
454    let peer = X25519PublicKey::from(*peer_public);
455    let shared_secret = private.diffie_hellman(&peer);
456    if !shared_secret.was_contributory() {
457        return match mode {
458            BoxMode::Seal => Err(Error::XKeySealFailed),
459            BoxMode::Open => Err(Error::XKeyOpenFailed),
460        };
461    }
462    let shared = Zeroizing::new(shared_secret.to_bytes());
463    let key = nats_box_key(&shared);
464    let cipher = XSalsa20Poly1305::new(&key);
465    let nonce = SecretboxNonce::from(*nonce);
466    match mode {
467        BoxMode::Seal => cipher
468            .encrypt(&nonce, input)
469            .map_err(|_| Error::XKeySealFailed),
470        BoxMode::Open => cipher
471            .decrypt(&nonce, input)
472            .map_err(|_| Error::XKeyOpenFailed),
473    }
474}
475
476#[allow(deprecated)]
477fn nats_box_key(shared: &Zeroizing<[u8; 32]>) -> Zeroizing<SecretboxKey> {
478    let input = crypto_secretbox::aead::generic_array::GenericArray::<u8, U16>::default();
479    let key = Zeroizing::new(SecretboxKey::clone_from_slice(shared.as_slice()));
480    Zeroizing::new(hsalsa::<U10>(&key, &input))
481}
482
483// ---------------------------------------------------------------------------
484// NATS JWT decoding and verification
485// ---------------------------------------------------------------------------
486
487#[derive(Deserialize)]
488struct CompactHeader {
489    typ: String,
490    alg: String,
491}
492
493#[derive(Deserialize)]
494struct CompactClaims {
495    iss: String,
496    sub: String,
497    #[serde(default)]
498    iat: Option<u64>,
499    #[serde(default)]
500    exp: Option<u64>,
501    #[serde(default)]
502    nbf: Option<u64>,
503    #[serde(default)]
504    nats: Option<CompactNatsClaims>,
505}
506
507#[derive(Deserialize)]
508struct CompactNatsClaims {
509    #[serde(rename = "type")]
510    kind: Option<String>,
511}
512
513/// Standard claims extracted from a decoded NATS JWT.
514#[derive(Debug, Clone, PartialEq, Eq)]
515pub struct NatsJwtClaims {
516    /// The `iss` claim. This is the public `NKey` whose Ed25519 key signed the
517    /// token.
518    pub issuer: String,
519    /// The `sub` claim. This is the public `NKey` the token describes.
520    pub subject: String,
521    /// Issued-at time (`iat`), Unix seconds.
522    pub issued_at: Option<u64>,
523    /// Expiry time (`exp`), Unix seconds.
524    pub expires_at: Option<u64>,
525    /// Not-before time (`nbf`), Unix seconds.
526    pub not_before: Option<u64>,
527    /// The `nats.type` claim, if present.
528    pub nats_type: Option<String>,
529    /// Full JSON claim object for callers that need fields outside the standard
530    /// broker validation surface.
531    pub raw: Value,
532}
533
534/// A decoded compact NATS JWT.
535#[derive(Debug, Clone, PartialEq, Eq)]
536pub struct DecodedNatsJwt {
537    signing_input: String,
538    signature: [u8; 64],
539    claims: NatsJwtClaims,
540    issuer_role: NkeyType,
541    issuer_public_key: [u8; 32],
542}
543
544impl DecodedNatsJwt {
545    /// The literal `header.payload` bytes that were signed.
546    #[must_use]
547    pub fn signing_input(&self) -> &str {
548        &self.signing_input
549    }
550
551    /// The decoded raw Ed25519 signature.
552    #[must_use]
553    pub const fn signature(&self) -> &[u8; 64] {
554        &self.signature
555    }
556
557    /// Extracted NATS JWT claims.
558    #[must_use]
559    pub const fn claims(&self) -> &NatsJwtClaims {
560        &self.claims
561    }
562
563    /// The role prefix carried by the embedded issuer `NKey`.
564    #[must_use]
565    pub const fn issuer_role(&self) -> NkeyType {
566        self.issuer_role
567    }
568
569    /// Raw 32-byte Ed25519 public key decoded from the embedded issuer `NKey`.
570    #[must_use]
571    pub const fn issuer_public_key(&self) -> &[u8; 32] {
572        &self.issuer_public_key
573    }
574
575    /// Verify the signature with an already-resolved Ed25519 public key.
576    #[must_use]
577    pub fn verify_signature(&self, public_key: &[u8; 32]) -> bool {
578        verify_ed25519(public_key, self.signing_input.as_bytes(), &self.signature)
579    }
580
581    /// Validate this token against the supplied candidate signer set.
582    ///
583    /// The candidate set is the trust boundary: the token's `iss` is
584    /// self-asserted, so at least one candidate must match it before the
585    /// signature and time claims are authoritative.
586    ///
587    /// # Errors
588    ///
589    /// Returns [`Error`] when a caller-supplied candidate public `NKey` is
590    /// malformed.
591    pub fn verify_with_candidates<'a, I>(
592        &self,
593        candidates: I,
594        now_unix: u64,
595    ) -> Result<NatsJwtValidation, Error>
596    where
597        I: IntoIterator<Item = CandidateSigner<'a>>,
598    {
599        for candidate in candidates {
600            let resolved = self.resolve_candidate(candidate)?;
601            if let Some(matched) = resolved {
602                return Ok(self.validation_for_match(matched, now_unix));
603            }
604        }
605        Ok(NatsJwtValidation {
606            reason: NatsJwtValidationReason::UnknownSigner,
607            matched_signer: None,
608        })
609    }
610
611    fn resolve_candidate(
612        &self,
613        candidate: CandidateSigner<'_>,
614    ) -> Result<Option<MatchedNatsSigner>, Error> {
615        match candidate {
616            CandidateSigner::Nkey(nkey) => {
617                let (role, public_key) = decode_public(nkey)?;
618                if role == self.issuer_role && public_key == self.issuer_public_key {
619                    Ok(Some(MatchedNatsSigner {
620                        role: Some(role),
621                        public_key,
622                    }))
623                } else {
624                    Ok(None)
625                }
626            }
627            CandidateSigner::RawPublicKey(public_key) => {
628                if public_key == &self.issuer_public_key {
629                    Ok(Some(MatchedNatsSigner {
630                        role: None,
631                        public_key: *public_key,
632                    }))
633                } else {
634                    Ok(None)
635                }
636            }
637        }
638    }
639
640    fn validation_for_match(
641        &self,
642        matched_signer: MatchedNatsSigner,
643        now_unix: u64,
644    ) -> NatsJwtValidation {
645        if !self.verify_signature(&matched_signer.public_key) {
646            return NatsJwtValidation {
647                reason: NatsJwtValidationReason::BadSignature,
648                matched_signer: None,
649            };
650        }
651        if let Some(exp) = self.claims.expires_at
652            && exp <= now_unix
653        {
654            return NatsJwtValidation {
655                reason: NatsJwtValidationReason::Expired,
656                matched_signer: None,
657            };
658        }
659        if let Some(nbf) = self.claims.not_before
660            && nbf > now_unix
661        {
662            return NatsJwtValidation {
663                reason: NatsJwtValidationReason::NotYetValid,
664                matched_signer: None,
665            };
666        }
667        NatsJwtValidation {
668            reason: NatsJwtValidationReason::Valid,
669            matched_signer: Some(matched_signer),
670        }
671    }
672}
673
674/// A caller-supplied signer candidate for NATS JWT validation.
675#[derive(Debug, Clone, Copy)]
676pub enum CandidateSigner<'a> {
677    /// Public `NKey` string (`A…`, `O…`, `U…`, or another supported public role).
678    Nkey(&'a str),
679    /// Raw 32-byte Ed25519 public key.
680    RawPublicKey(&'a [u8; 32]),
681}
682
683/// The signer that matched a token's embedded `iss`.
684#[derive(Debug, Clone, Copy, PartialEq, Eq)]
685pub struct MatchedNatsSigner {
686    /// The public `NKey` role, if the caller supplied this candidate as an
687    /// encoded `NKey`. Raw public-key candidates have no role prefix.
688    pub role: Option<NkeyType>,
689    /// Raw 32-byte Ed25519 public key used for verification.
690    pub public_key: [u8; 32],
691}
692
693/// Result of validating a NATS JWT against a candidate signer set.
694#[derive(Debug, Clone, Copy, PartialEq, Eq)]
695pub struct NatsJwtValidation {
696    /// Machine-readable validation result.
697    pub reason: NatsJwtValidationReason,
698    /// The verified signer. `Some` only when [`Self::is_valid`] is true.
699    pub matched_signer: Option<MatchedNatsSigner>,
700}
701
702impl NatsJwtValidation {
703    /// Whether the token signature and time claims are valid.
704    #[must_use]
705    pub const fn is_valid(self) -> bool {
706        matches!(self.reason, NatsJwtValidationReason::Valid)
707    }
708}
709
710/// Authoritative validation result for a decoded NATS JWT.
711#[derive(Debug, Clone, Copy, PartialEq, Eq)]
712pub enum NatsJwtValidationReason {
713    /// Signature and time claims are valid.
714    Valid,
715    /// No supplied signer matched the token's embedded `iss`.
716    UnknownSigner,
717    /// The signer matched, but the Ed25519 signature did not verify over the
718    /// literal compact-JWT `header.payload` bytes.
719    BadSignature,
720    /// The token's `exp` is at or before the supplied validation time.
721    Expired,
722    /// The token's `nbf` is after the supplied validation time.
723    NotYetValid,
724}
725
726/// Decode a compact NATS JWT and expose its claims plus literal signing input.
727///
728/// # Errors
729///
730/// Returns [`Error::MalformedJwt`] for malformed compact JWTs, invalid base64url
731/// segments, non-JSON header/claims, missing required claims, unsupported NATS
732/// JWS header values, or invalid issuer `NKey`; returns
733/// [`Error::BadSignatureLen`] when the signature segment is not 64 bytes.
734pub fn decode_nats_jwt(token: &str) -> Result<DecodedNatsJwt, Error> {
735    let mut parts = token.split('.');
736    let Some(header_segment) = parts.next() else {
737        return Err(Error::MalformedJwt("jwt is empty".into()));
738    };
739    let Some(claims_segment) = parts.next() else {
740        return Err(Error::MalformedJwt(
741            "jwt must contain header, claims, and signature".into(),
742        ));
743    };
744    let Some(signature_segment) = parts.next() else {
745        return Err(Error::MalformedJwt(
746            "jwt must contain header, claims, and signature".into(),
747        ));
748    };
749    if parts.next().is_some() {
750        return Err(Error::MalformedJwt(
751            "jwt must contain exactly three segments".into(),
752        ));
753    }
754
755    let header: CompactHeader = decode_url_json(header_segment, "header")?;
756    if header.typ != "JWT" || header.alg != "ed25519-nkey" {
757        return Err(Error::MalformedJwt(
758            "jwt header must be typ=JWT and alg=ed25519-nkey".into(),
759        ));
760    }
761
762    let claims_raw = decode_url_bytes(claims_segment, "claims")?;
763    let claims_value: Value = serde_json::from_slice(&claims_raw)
764        .map_err(|e| Error::MalformedJwt(format!("invalid jwt claims json: {e}")))?;
765    let claims: CompactClaims = serde_json::from_value(claims_value.clone())
766        .map_err(|e| Error::MalformedJwt(format!("invalid jwt claims: {e}")))?;
767    let (issuer_role, issuer_public_key) = decode_public(&claims.iss)
768        .map_err(|e| Error::MalformedJwt(format!("invalid jwt issuer nkey: {e}")))?;
769
770    let signature = decode_url_bytes(signature_segment, "signature")?;
771    let signature_len = signature.len();
772    let Ok(signature) = <[u8; 64]>::try_from(signature) else {
773        return Err(Error::BadSignatureLen(signature_len));
774    };
775
776    Ok(DecodedNatsJwt {
777        signing_input: format!("{header_segment}.{claims_segment}"),
778        signature,
779        claims: NatsJwtClaims {
780            issuer: claims.iss,
781            subject: claims.sub,
782            issued_at: claims.iat,
783            expires_at: claims.exp,
784            not_before: claims.nbf,
785            nats_type: claims.nats.and_then(|nats| nats.kind),
786            raw: claims_value,
787        },
788        issuer_role,
789        issuer_public_key,
790    })
791}
792
793fn decode_url_json<T>(segment: &str, name: &str) -> Result<T, Error>
794where
795    T: DeserializeOwned,
796{
797    let bytes = decode_url_bytes(segment, name)?;
798    serde_json::from_slice(&bytes)
799        .map_err(|e| Error::MalformedJwt(format!("invalid jwt {name} json: {e}")))
800}
801
802fn decode_url_bytes(segment: &str, name: &str) -> Result<Vec<u8>, Error> {
803    URL_SAFE_NO_PAD
804        .decode(segment)
805        .map_err(|e| Error::MalformedJwt(format!("invalid jwt {name} base64url: {e}")))
806}
807
808fn verify_ed25519(public_key: &[u8; 32], message: &[u8], signature: &[u8; 64]) -> bool {
809    let signature = Signature::from_bytes(signature);
810    VerifyingKey::from_bytes(public_key).is_ok_and(|key| key.verify(message, &signature).is_ok())
811}
812
813// ---------------------------------------------------------------------------
814// Claim shapes (mirrors nats-io/jwt v2)
815// ---------------------------------------------------------------------------
816
817/// A subject allow/deny list for one direction (publish or subscribe). An empty
818/// `allow` means "no allow restriction"; `deny` always subtracts.
819#[derive(Serialize, Default, Clone)]
820pub struct Permission {
821    /// Subjects explicitly permitted (empty = unrestricted for this direction).
822    #[serde(skip_serializing_if = "Vec::is_empty")]
823    pub allow: Vec<String>,
824    /// Subjects explicitly denied (subtracted from whatever `allow` permits).
825    #[serde(skip_serializing_if = "Vec::is_empty")]
826    pub deny: Vec<String>,
827}
828
829#[derive(Serialize)]
830struct NatsUser {
831    #[serde(rename = "pub")]
832    publish: Permission,
833    sub: Permission,
834    subs: i64,
835    data: i64,
836    payload: i64,
837    // The owning account identity when the token is issued by an account signing
838    // key (mirrors `nats-io/jwt`'s `User.issuer_account`, `omitempty`).
839    #[serde(skip_serializing_if = "Option::is_none")]
840    issuer_account: Option<String>,
841    #[serde(rename = "type")]
842    kind: &'static str,
843    version: i64,
844}
845
846/// Claims hashed to produce `jti`: the standard claims only, in `nats-io/jwt`
847/// `ClaimsData` field order, with `omitempty` semantics (and `jti` always empty
848/// so it is omitted).
849#[derive(Serialize)]
850struct ClaimsHash<'a> {
851    #[serde(skip_serializing_if = "Option::is_none")]
852    aud: Option<&'a str>,
853    #[serde(skip_serializing_if = "Option::is_none")]
854    exp: Option<u64>,
855    #[serde(skip_serializing_if = "Option::is_none")]
856    jti: Option<&'a str>,
857    iat: u64,
858    iss: &'a str,
859    #[serde(skip_serializing_if = "Option::is_none")]
860    name: Option<&'a str>,
861    #[serde(skip_serializing_if = "Option::is_none")]
862    nbf: Option<u64>,
863    sub: &'a str,
864}
865
866/// A pub/sub permission pair as the account `default_permissions` block carries
867/// it (`{"pub":{…},"sub":{…}}`).
868#[derive(Serialize, Default, Clone)]
869pub struct Permissions {
870    /// Publish permissions (serialized as `pub`).
871    #[serde(rename = "pub")]
872    pub publish: Permission,
873    /// Subscribe permissions.
874    pub sub: Permission,
875    /// Optional limit on auto-generated response-subject permissions.
876    #[serde(skip_serializing_if = "Option::is_none")]
877    pub resp: Option<ResponsePermission>,
878}
879
880/// Bounds on the implicit reply-subject permissions a request grants its
881/// responder.
882#[derive(Serialize, Clone)]
883pub struct ResponsePermission {
884    /// Maximum number of responses allowed.
885    pub max: i64,
886    /// How long (nanoseconds) the response permission stays valid.
887    pub ttl: i64,
888}
889
890/// Whether an import/export is a one-way `stream` or a request/reply `service`.
891#[derive(Serialize, Clone)]
892#[serde(rename_all = "lowercase")]
893pub enum ExportType {
894    /// One-way publish stream.
895    Stream,
896    /// Request/reply service.
897    Service,
898}
899
900/// An account-level import of another account's stream or service.
901#[derive(Serialize, Clone)]
902pub struct AccountImport {
903    /// Human-readable import name.
904    #[serde(skip_serializing_if = "str::is_empty")]
905    pub name: String,
906    /// Exported subject being imported.
907    #[serde(skip_serializing_if = "str::is_empty")]
908    pub subject: String,
909    /// Public `NKey` (`A…`) of the exporting account.
910    #[serde(skip_serializing_if = "str::is_empty")]
911    pub account: String,
912    /// Activation token for a token-gated export (empty if not required).
913    #[serde(skip_serializing_if = "str::is_empty")]
914    pub token: String,
915    /// Local subject the import is remapped *to* (legacy `to` form).
916    #[serde(skip_serializing_if = "str::is_empty")]
917    pub to: String,
918    /// Local subject the import is remapped to (with `$N` capture groups).
919    #[serde(skip_serializing_if = "str::is_empty")]
920    pub local_subject: String,
921    /// Whether this imports a `stream` or a `service` (serialized as `type`).
922    #[serde(rename = "type")]
923    pub kind: ExportType,
924    /// Whether latency/connection info may be shared with the exporter.
925    #[serde(skip_serializing_if = "is_false")]
926    pub share: bool,
927    /// Whether message-trace propagation is allowed across this import.
928    #[serde(skip_serializing_if = "is_false")]
929    pub allow_trace: bool,
930}
931
932/// Latency-sampling configuration for an exported service.
933#[derive(Serialize, Clone)]
934pub struct ServiceLatency {
935    /// Sampling rate (`headers` or a percentage).
936    pub sampling: SamplingRate,
937    /// Subject latency measurements are published to.
938    pub results: String,
939}
940
941/// How often an exported service samples latency.
942#[derive(Clone)]
943pub enum SamplingRate {
944    /// Sample only when the request carries the latency header (serialized as
945    /// the string `"headers"`).
946    Headers,
947    /// Sample this percentage (1–100) of requests.
948    Percent(u8),
949}
950
951impl Serialize for SamplingRate {
952    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
953    where
954        S: serde::Serializer,
955    {
956        match self {
957            Self::Headers => serializer.serialize_str("headers"),
958            Self::Percent(percent) => serializer.serialize_u8(*percent),
959        }
960    }
961}
962
963/// An account-level export of a stream or service that other accounts may
964/// import.
965#[derive(Serialize, Clone)]
966pub struct AccountExport {
967    /// Human-readable export name.
968    #[serde(skip_serializing_if = "str::is_empty")]
969    pub name: String,
970    /// Subject (with wildcards) being exported.
971    #[serde(skip_serializing_if = "str::is_empty")]
972    pub subject: String,
973    /// Whether this exports a `stream` or a `service` (serialized as `type`).
974    #[serde(rename = "type")]
975    pub kind: ExportType,
976    /// Whether importers need an activation token (private export).
977    #[serde(skip_serializing_if = "is_false")]
978    pub token_req: bool,
979    /// Revoked activation tokens, keyed by account public key → revocation time.
980    #[serde(skip_serializing_if = "BTreeMap::is_empty")]
981    pub revocations: BTreeMap<String, i64>,
982    /// Reply cardinality for a service export (`singleton`/`stream`/`chunked`).
983    #[serde(skip_serializing_if = "Option::is_none")]
984    pub response_type: Option<ResponseType>,
985    /// Latency-tracking response threshold (nanoseconds).
986    #[serde(skip_serializing_if = "is_zero_i64")]
987    pub response_threshold: i64,
988    /// Optional latency-sampling configuration for a service export.
989    #[serde(skip_serializing_if = "Option::is_none")]
990    pub service_latency: Option<ServiceLatency>,
991    /// Token position for a dynamically-named (templated) export subject.
992    #[serde(skip_serializing_if = "is_zero_u32")]
993    pub account_token_position: u32,
994    /// Whether the export is advertised to other accounts.
995    #[serde(skip_serializing_if = "is_false")]
996    pub advertise: bool,
997    /// Whether message-trace propagation is allowed across this export.
998    #[serde(skip_serializing_if = "is_false")]
999    pub allow_trace: bool,
1000    /// Free-form description.
1001    #[serde(skip_serializing_if = "str::is_empty")]
1002    pub description: String,
1003    /// URL with more information about the export.
1004    #[serde(skip_serializing_if = "str::is_empty")]
1005    pub info_url: String,
1006}
1007
1008/// Reply cardinality of a service export.
1009#[derive(Serialize, Clone)]
1010pub enum ResponseType {
1011    /// Exactly one reply per request.
1012    Singleton,
1013    /// A stream of replies per request.
1014    Stream,
1015    /// A chunked single logical reply.
1016    Chunked,
1017}
1018
1019/// Per-connection NATS limits (a `-1` value means unlimited).
1020#[derive(Serialize, Default, Clone)]
1021pub struct NatsLimits {
1022    /// Maximum subscriptions.
1023    #[serde(skip_serializing_if = "is_zero_i64")]
1024    pub subs: i64,
1025    /// Maximum data in bytes.
1026    #[serde(skip_serializing_if = "is_zero_i64")]
1027    pub data: i64,
1028    /// Maximum message payload in bytes.
1029    #[serde(skip_serializing_if = "is_zero_i64")]
1030    pub payload: i64,
1031}
1032
1033/// Account-wide limits (a `-1` value means unlimited).
1034#[derive(Serialize, Default, Clone)]
1035pub struct AccountLimits {
1036    /// Maximum number of imports.
1037    #[serde(skip_serializing_if = "is_zero_i64")]
1038    pub imports: i64,
1039    /// Maximum number of exports.
1040    #[serde(skip_serializing_if = "is_zero_i64")]
1041    pub exports: i64,
1042    /// Whether wildcard export subjects are allowed.
1043    #[serde(skip_serializing_if = "is_false")]
1044    pub wildcards: bool,
1045    /// Whether bearer (non-`NKey`) user tokens are disallowed.
1046    #[serde(skip_serializing_if = "is_false")]
1047    pub disallow_bearer: bool,
1048    /// Maximum active client connections.
1049    #[serde(skip_serializing_if = "is_zero_i64")]
1050    pub conn: i64,
1051    /// Maximum leaf-node connections.
1052    #[serde(skip_serializing_if = "is_zero_i64")]
1053    pub leaf: i64,
1054}
1055
1056/// `JetStream` resource limits for an account or a named tier (a `-1` value
1057/// means unlimited).
1058#[derive(Serialize, Default, Clone)]
1059pub struct JetStreamLimits {
1060    /// Maximum in-memory storage in bytes.
1061    #[serde(skip_serializing_if = "is_zero_i64")]
1062    pub mem_storage: i64,
1063    /// Maximum on-disk storage in bytes.
1064    #[serde(skip_serializing_if = "is_zero_i64")]
1065    pub disk_storage: i64,
1066    /// Maximum number of streams.
1067    #[serde(skip_serializing_if = "is_zero_i64")]
1068    pub streams: i64,
1069    /// Maximum number of consumers.
1070    #[serde(skip_serializing_if = "is_zero_i64")]
1071    pub consumer: i64,
1072    /// Maximum unacknowledged messages per consumer.
1073    #[serde(skip_serializing_if = "is_zero_i64")]
1074    pub max_ack_pending: i64,
1075    /// Maximum bytes for a single memory stream.
1076    #[serde(skip_serializing_if = "is_zero_i64")]
1077    pub mem_max_stream_bytes: i64,
1078    /// Maximum bytes for a single disk stream.
1079    #[serde(skip_serializing_if = "is_zero_i64")]
1080    pub disk_max_stream_bytes: i64,
1081    /// Whether streams must declare a maximum byte size.
1082    #[serde(skip_serializing_if = "is_false")]
1083    pub max_bytes_required: bool,
1084}
1085
1086/// The full operator-limits block on an account JWT: the `nats`, `account`, and
1087/// `jetstream` limit groups (flattened into one JSON object) plus per-tier
1088/// `JetStream` limits.
1089#[derive(Serialize, Default, Clone)]
1090pub struct OperatorLimits {
1091    /// Per-connection NATS limits.
1092    #[serde(flatten)]
1093    pub nats: NatsLimits,
1094    /// Account-wide limits.
1095    #[serde(flatten)]
1096    pub account: AccountLimits,
1097    /// Default-tier `JetStream` limits.
1098    #[serde(flatten)]
1099    pub jetstream: JetStreamLimits,
1100    /// `JetStream` limits per named tier (`R1`, `R3`, …).
1101    #[serde(skip_serializing_if = "BTreeMap::is_empty")]
1102    pub tiered_limits: BTreeMap<String, JetStreamLimits>,
1103}
1104
1105impl OperatorLimits {
1106    /// Standard **unlimited** operator limits for an account, matching what
1107    /// `nsc` writes for a default account: every per-connection and account-wide
1108    /// limit is `-1` (unlimited) and wildcard exports are allowed. `JetStream`
1109    /// limits stay `0` (disabled); enabling `JetStream` is a separate, explicit
1110    /// grant.
1111    ///
1112    /// A minted account JWT **must** carry a limits block: `nats-server` reads a
1113    /// zero connection/subscription limit as **deny-all** (only `-1` is
1114    /// unlimited), so an account with no limits rejects every connection.
1115    #[must_use]
1116    pub fn unlimited() -> Self {
1117        Self {
1118            nats: NatsLimits {
1119                subs: -1,
1120                data: -1,
1121                payload: -1,
1122            },
1123            account: AccountLimits {
1124                imports: -1,
1125                exports: -1,
1126                wildcards: true,
1127                conn: -1,
1128                leaf: -1,
1129                ..AccountLimits::default()
1130            },
1131            ..Self::default()
1132        }
1133    }
1134}
1135
1136/// One weighted target in a subject mapping (for traffic splitting / canaries).
1137#[derive(Serialize, Clone)]
1138pub struct WeightedMapping {
1139    /// Destination subject.
1140    pub subject: String,
1141    /// Weight 0–100; the share of traffic routed to `subject`.
1142    #[serde(skip_serializing_if = "is_zero_u8")]
1143    pub weight: u8,
1144    /// Restrict this mapping to a named cluster (empty = all clusters).
1145    #[serde(skip_serializing_if = "str::is_empty")]
1146    pub cluster: String,
1147}
1148
1149/// External (delegated) authorization config for an account: auth is performed
1150/// by an external service rather than by per-user `NKey`s.
1151#[derive(Serialize, Default, Clone)]
1152pub struct ExternalAuthorization {
1153    /// User public `NKey`s (`U…`) the external auth callout runs as.
1154    #[serde(skip_serializing_if = "Vec::is_empty")]
1155    pub auth_users: Vec<String>,
1156    /// Account public `NKey`s (`A…`) the callout may place users into.
1157    #[serde(skip_serializing_if = "Vec::is_empty")]
1158    pub allowed_accounts: Vec<String>,
1159    /// Curve (`X…`) xkey used to encrypt the auth-callout request (empty = none).
1160    #[serde(skip_serializing_if = "str::is_empty")]
1161    pub xkey: String,
1162}
1163
1164/// Message-trace configuration: where traced messages are reported and how
1165/// often they are sampled.
1166#[derive(Serialize, Clone)]
1167pub struct MsgTrace {
1168    /// Subject trace events are delivered to.
1169    #[serde(skip_serializing_if = "str::is_empty")]
1170    pub dest: String,
1171    /// Sampling percentage (1–100).
1172    #[serde(skip_serializing_if = "is_zero_u8")]
1173    pub sampling: u8,
1174}
1175
1176/// Which account a cluster's system traffic is attributed to.
1177#[derive(Serialize, Clone)]
1178#[serde(rename_all = "lowercase")]
1179pub enum ClusterTraffic {
1180    /// The system account.
1181    System,
1182    /// The owning account.
1183    Owner,
1184}
1185
1186/// Account-specific fields inside a NATS account JWT (the `nats` claim block for
1187/// `type=account`). All fields are optional; empty ones are omitted from the
1188/// serialized claims.
1189#[derive(Serialize, Default, Clone)]
1190pub struct AccountClaims {
1191    /// Streams/services this account imports from others.
1192    #[serde(skip_serializing_if = "Vec::is_empty")]
1193    pub imports: Vec<AccountImport>,
1194    /// Streams/services this account exports to others.
1195    #[serde(skip_serializing_if = "Vec::is_empty")]
1196    pub exports: Vec<AccountExport>,
1197    /// Resource limits applied to the account.
1198    #[serde(skip_serializing_if = "operator_limits_is_empty")]
1199    pub limits: OperatorLimits,
1200    /// Account signing keys (`A…`) authorized to sign on the account's behalf.
1201    /// If left empty, [`AccountJwt::signing_keys`] is used.
1202    #[serde(rename = "signing_keys", skip_serializing_if = "Vec::is_empty")]
1203    pub signing_keys: Vec<String>,
1204    /// Revoked user keys, keyed by user public key → revocation time.
1205    #[serde(skip_serializing_if = "BTreeMap::is_empty")]
1206    pub revocations: BTreeMap<String, i64>,
1207    /// Default pub/sub permissions applied to users lacking their own.
1208    #[serde(skip_serializing_if = "Option::is_none")]
1209    pub default_permissions: Option<Permissions>,
1210    /// Subject mappings, keyed by source subject → weighted destinations.
1211    #[serde(skip_serializing_if = "BTreeMap::is_empty")]
1212    pub mappings: BTreeMap<String, Vec<WeightedMapping>>,
1213    /// External (delegated) authorization configuration.
1214    #[serde(skip_serializing_if = "external_authorization_is_empty")]
1215    pub authorization: ExternalAuthorization,
1216    /// Account-level message-trace configuration.
1217    #[serde(skip_serializing_if = "Option::is_none")]
1218    pub trace: Option<MsgTrace>,
1219    /// Which account this account's cluster traffic is attributed to.
1220    #[serde(skip_serializing_if = "Option::is_none")]
1221    pub cluster_traffic: Option<ClusterTraffic>,
1222}
1223
1224#[derive(Serialize)]
1225struct NatsAccount {
1226    #[serde(flatten)]
1227    claims: AccountClaims,
1228    #[serde(rename = "type")]
1229    kind: &'static str,
1230    version: i64,
1231}
1232
1233/// Operator-specific fields inside a NATS operator JWT (the `nats` claim block
1234/// for `type=operator`). All fields are optional; empty ones are omitted.
1235#[derive(Serialize, Default, Clone)]
1236pub struct OperatorClaims {
1237    /// Operator signing keys (`O…`) authorized to sign accounts. If left empty,
1238    /// [`OperatorJwt::signing_keys`] is used.
1239    #[serde(rename = "signing_keys", skip_serializing_if = "Vec::is_empty")]
1240    pub signing_keys: Vec<String>,
1241    /// Account-resolver / account-server URL. If empty,
1242    /// [`OperatorJwt::account_server_url`] is used.
1243    #[serde(rename = "account_server_url", skip_serializing_if = "str::is_empty")]
1244    pub account_server_url: String,
1245    /// Advertised operator service (NATS) URLs.
1246    #[serde(skip_serializing_if = "Vec::is_empty")]
1247    pub operator_service_urls: Vec<String>,
1248    /// System account public `NKey` (`A…`). If empty,
1249    /// [`OperatorJwt::system_account`] is used.
1250    #[serde(rename = "system_account", skip_serializing_if = "str::is_empty")]
1251    pub system_account: String,
1252    /// Minimum NATS server version this operator asserts (empty = none).
1253    #[serde(skip_serializing_if = "str::is_empty")]
1254    pub assert_server_version: String,
1255    /// Whether accounts must be signed by a dedicated signing key (not the
1256    /// operator identity key).
1257    #[serde(skip_serializing_if = "is_false")]
1258    pub strict_signing_key_usage: bool,
1259}
1260
1261#[derive(Serialize)]
1262struct NatsOperator {
1263    #[serde(flatten)]
1264    claims: OperatorClaims,
1265    #[serde(rename = "type")]
1266    kind: &'static str,
1267    version: i64,
1268}
1269
1270#[allow(clippy::trivially_copy_pass_by_ref)]
1271const fn is_false(value: &bool) -> bool {
1272    !*value
1273}
1274
1275#[allow(clippy::trivially_copy_pass_by_ref)]
1276const fn is_zero_i64(value: &i64) -> bool {
1277    *value == 0
1278}
1279
1280#[allow(clippy::trivially_copy_pass_by_ref)]
1281const fn is_zero_u32(value: &u32) -> bool {
1282    *value == 0
1283}
1284
1285#[allow(clippy::trivially_copy_pass_by_ref)]
1286const fn is_zero_u8(value: &u8) -> bool {
1287    *value == 0
1288}
1289
1290fn external_authorization_is_empty(value: &ExternalAuthorization) -> bool {
1291    value.auth_users.is_empty() && value.allowed_accounts.is_empty() && value.xkey.is_empty()
1292}
1293
1294fn operator_limits_is_empty(value: &OperatorLimits) -> bool {
1295    value.nats.subs == 0
1296        && value.nats.data == 0
1297        && value.nats.payload == 0
1298        && value.account.imports == 0
1299        && value.account.exports == 0
1300        && !value.account.wildcards
1301        && !value.account.disallow_bearer
1302        && value.account.conn == 0
1303        && value.account.leaf == 0
1304        && value.jetstream.mem_storage == 0
1305        && value.jetstream.disk_storage == 0
1306        && value.jetstream.streams == 0
1307        && value.jetstream.consumer == 0
1308        && value.jetstream.max_ack_pending == 0
1309        && value.jetstream.mem_max_stream_bytes == 0
1310        && value.jetstream.disk_max_stream_bytes == 0
1311        && !value.jetstream.max_bytes_required
1312        && value.tiered_limits.is_empty()
1313}
1314
1315/// Minimal NATS claim block for role tokens that do not have additional
1316/// modeled fields in the broker API yet.
1317#[derive(Serialize)]
1318struct NatsRole {
1319    #[serde(rename = "type")]
1320    kind: &'static str,
1321    version: i64,
1322}
1323
1324/// Generic token claims parameterized over the `nats` block type, so the
1325/// account and operator builders share the standard-claim envelope + `jti`
1326/// hashing with the user builder.
1327#[derive(Serialize)]
1328struct TokenClaimsGeneric<'a, N>
1329where
1330    N: Serialize,
1331{
1332    jti: String,
1333    iat: u64,
1334    iss: &'a str,
1335    #[serde(skip_serializing_if = "str::is_empty")]
1336    name: &'a str,
1337    #[serde(skip_serializing_if = "Option::is_none")]
1338    exp: Option<u64>,
1339    #[serde(skip_serializing_if = "Option::is_none")]
1340    nbf: Option<u64>,
1341    nats: N,
1342    sub: &'a str,
1343}
1344
1345/// Compute the `jti` (base32-nopad SHA-512/256 over the standard claims, with
1346/// `jti=""` excluded) shared by every NATS JWT shape, and encode the
1347/// `header.claims` signing input. The `nats` block is excluded from the hash by
1348/// construction (same as `nats-io/jwt`).
1349fn build_signing_input<N: Serialize>(
1350    iss: &str,
1351    sub: &str,
1352    name: &str,
1353    iat: u64,
1354    exp: Option<u64>,
1355    nats: N,
1356) -> Result<String, Error> {
1357    let jti = jti_for_standard_claims(iss, sub, Some(name), iat, exp, None, None)?;
1358
1359    let claims = TokenClaimsGeneric {
1360        jti,
1361        iat,
1362        iss,
1363        name,
1364        exp,
1365        nbf: None,
1366        nats,
1367        sub,
1368    };
1369    let header = URL_SAFE_NO_PAD.encode(HEADER_JSON.as_bytes());
1370    let payload = URL_SAFE_NO_PAD.encode(serde_json::to_vec(&claims)?);
1371    Ok(format!("{header}.{payload}"))
1372}
1373
1374/// Compute the NATS JWT `jti`: base32-nopad SHA-512/256 over the standard
1375/// claims with `jti` itself omitted.
1376///
1377/// This is public for callers that accept a rich NATS claim document and need to
1378/// validate or refresh `jti` before signing.
1379///
1380/// # Errors
1381///
1382/// [`Error::Json`] if the standard-claim hash input cannot be serialized.
1383pub fn jti_for_standard_claims(
1384    iss: &str,
1385    sub: &str,
1386    name: Option<&str>,
1387    iat: u64,
1388    exp: Option<u64>,
1389    aud: Option<&str>,
1390    nbf: Option<u64>,
1391) -> Result<String, Error> {
1392    let hash_src = ClaimsHash {
1393        aud: aud.filter(|value| !value.is_empty()),
1394        exp: exp.filter(|value| *value != 0),
1395        jti: None,
1396        iat,
1397        iss,
1398        name: name.filter(|value| !value.is_empty()),
1399        nbf: nbf.filter(|value| *value != 0),
1400        sub,
1401    };
1402    let mut hasher = Sha512_256::new();
1403    hasher.update(serde_json::to_vec(&hash_src)?);
1404    Ok(data_encoding::BASE32_NOPAD.encode(&hasher.finalize()))
1405}
1406
1407/// Build the NATS JWS signing input from a fully validated claim document.
1408///
1409/// The claim document must be a JSON object. This function only applies the
1410/// NATS fixed header and serialization; semantic checks for `iss`, `sub`, and
1411/// the `nats` block belong at the authorization boundary.
1412///
1413/// # Errors
1414///
1415/// [`Error::InvalidClaims`] if `claims` is not an object, or [`Error::Json`] if
1416/// serialization fails.
1417pub fn signing_input_from_claims(claims: &Value) -> Result<String, Error> {
1418    if !claims.is_object() {
1419        return Err(Error::InvalidClaims(
1420            "nats jwt claims must be a JSON object".into(),
1421        ));
1422    }
1423    let header = URL_SAFE_NO_PAD.encode(HEADER_JSON.as_bytes());
1424    let payload = URL_SAFE_NO_PAD.encode(serde_json::to_vec(claims)?);
1425    Ok(format!("{header}.{payload}"))
1426}
1427
1428// ---------------------------------------------------------------------------
1429// User JWT builder
1430// ---------------------------------------------------------------------------
1431
1432/// Publish / subscribe permission lists for a user (empty = unrestricted within
1433/// the account).
1434#[derive(Default, Clone)]
1435pub struct UserPermissions {
1436    /// Subjects the user may publish to (empty = all).
1437    pub pub_allow: Vec<String>,
1438    /// Subjects the user may not publish to.
1439    pub pub_deny: Vec<String>,
1440    /// Subjects the user may subscribe to (empty = all).
1441    pub sub_allow: Vec<String>,
1442    /// Subjects the user may not subscribe to.
1443    pub sub_deny: Vec<String>,
1444}
1445
1446/// A NATS user JWT to be signed by its issuing account key.
1447pub struct UserJwt {
1448    /// Issuer (`iss`) = the public `NKey` (`A…`) of the key that signs the token:
1449    /// either the account identity key or an account signing key.
1450    pub issuer: String,
1451    /// `nats.issuer_account` = the owning account's identity public `NKey` (`A…`).
1452    /// Set this when [`issuer`](Self::issuer) is an account *signing* key, so
1453    /// `nats-server` can bind the user to its account; leave `None` when `issuer`
1454    /// is the account identity key itself.
1455    pub issuer_account: Option<String>,
1456    /// Subject = the user public `NKey` (`U…`) the credential is for.
1457    pub subject_user: String,
1458    /// Human-readable user name (the `name` claim).
1459    pub name: String,
1460    /// Issued-at time (`iat`), Unix seconds.
1461    pub issued_at: u64,
1462    /// Optional expiry (`exp`), Unix seconds; `None` mints a non-expiring token.
1463    pub expires: Option<u64>,
1464    /// Publish/subscribe permissions embedded in the user claims.
1465    pub permissions: UserPermissions,
1466}
1467
1468impl UserJwt {
1469    /// Produce the JWS **signing input** (`base64url(header).base64url(claims)`).
1470    /// Hand the bytes to the key holder, then pass the resulting 64-byte
1471    /// signature to [`assemble`].
1472    pub fn signing_input(&self) -> Result<String, Error> {
1473        let nats = NatsUser {
1474            publish: Permission {
1475                allow: self.permissions.pub_allow.clone(),
1476                deny: self.permissions.pub_deny.clone(),
1477            },
1478            sub: Permission {
1479                allow: self.permissions.sub_allow.clone(),
1480                deny: self.permissions.sub_deny.clone(),
1481            },
1482            subs: -1,
1483            data: -1,
1484            payload: -1,
1485            issuer_account: self.issuer_account.clone(),
1486            kind: "user",
1487            version: 2,
1488        };
1489        build_signing_input(
1490            &self.issuer,
1491            &self.subject_user,
1492            &self.name,
1493            self.issued_at,
1494            self.expires,
1495            nats,
1496        )
1497    }
1498}
1499
1500// ---------------------------------------------------------------------------
1501// Account JWT builder
1502// ---------------------------------------------------------------------------
1503
1504/// A NATS **account** JWT to be signed by its issuing operator key.
1505///
1506/// Uses the same [`signing_input`](AccountJwt::signing_input) / [`assemble`]
1507/// split as [`UserJwt`]: the operator (or operator-signing) key signs the input
1508/// in the vault and the seed never materializes.
1509pub struct AccountJwt {
1510    /// Issuer = the operator (or operator-signing) public `NKey` (`O…`) signing.
1511    pub issuer: String,
1512    /// Subject = the account public `NKey` (`A…`) the JWT is for.
1513    pub subject_account: String,
1514    /// Human-readable account name (the `name` claim).
1515    pub name: String,
1516    /// Issued-at time (`iat`), Unix seconds.
1517    pub issued_at: u64,
1518    /// Optional expiry (`exp`), Unix seconds; `None` mints a non-expiring token.
1519    pub expires: Option<u64>,
1520    /// Account signing keys (`A…`) authorized to sign on behalf of the account.
1521    /// Used only when [`AccountClaims::signing_keys`] is empty.
1522    pub signing_keys: Vec<String>,
1523    /// Additional account claim fields from `nats-io/jwt` v2.
1524    pub claims: AccountClaims,
1525}
1526
1527impl AccountJwt {
1528    /// Produce the JWS **signing input** (`base64url(header).base64url(claims)`).
1529    ///
1530    /// # Errors
1531    ///
1532    /// [`Error::Json`] if claim serialization fails (does not happen for these
1533    /// types in practice).
1534    pub fn signing_input(&self) -> Result<String, Error> {
1535        let nats = NatsAccount {
1536            claims: {
1537                let mut claims = self.claims.clone();
1538                if claims.signing_keys.is_empty() {
1539                    claims.signing_keys.clone_from(&self.signing_keys);
1540                }
1541                claims
1542            },
1543            kind: "account",
1544            version: 2,
1545        };
1546        build_signing_input(
1547            &self.issuer,
1548            &self.subject_account,
1549            &self.name,
1550            self.issued_at,
1551            self.expires,
1552            nats,
1553        )
1554    }
1555}
1556
1557// ---------------------------------------------------------------------------
1558// Operator JWT builder
1559// ---------------------------------------------------------------------------
1560
1561/// A NATS **operator** JWT to be signed by its operator key (usually
1562/// self-signed: `iss == sub`). Same `signing_input`/[`assemble`] split as the
1563/// others.
1564pub struct OperatorJwt {
1565    /// Issuer = the operator public `NKey` (`O…`) signing the token.
1566    pub issuer: String,
1567    /// Subject = the operator public `NKey` (`O…`) the JWT describes (self-signed
1568    /// operators set this equal to `issuer`).
1569    pub subject_operator: String,
1570    /// Human-readable operator name (the `name` claim).
1571    pub name: String,
1572    /// Issued-at time (`iat`), Unix seconds.
1573    pub issued_at: u64,
1574    /// Optional expiry (`exp`), Unix seconds; `None` mints a non-expiring token.
1575    pub expires: Option<u64>,
1576    /// Operator signing keys (`O…`). Used only when
1577    /// [`OperatorClaims::signing_keys`] is empty.
1578    pub signing_keys: Vec<String>,
1579    /// The account-resolver / account-server URL (empty = omitted). Used only
1580    /// when [`OperatorClaims::account_server_url`] is empty.
1581    pub account_server_url: String,
1582    /// The system account public `NKey` (`A…`, empty = omitted). Used only when
1583    /// [`OperatorClaims::system_account`] is empty.
1584    pub system_account: String,
1585    /// Additional operator claim fields from `nats-io/jwt` v2.
1586    pub claims: OperatorClaims,
1587}
1588
1589impl OperatorJwt {
1590    /// Produce the JWS **signing input** (`base64url(header).base64url(claims)`).
1591    ///
1592    /// # Errors
1593    ///
1594    /// [`Error::Json`] if claim serialization fails.
1595    pub fn signing_input(&self) -> Result<String, Error> {
1596        let nats = NatsOperator {
1597            claims: {
1598                let mut claims = self.claims.clone();
1599                if claims.signing_keys.is_empty() {
1600                    claims.signing_keys.clone_from(&self.signing_keys);
1601                }
1602                if claims.account_server_url.is_empty() {
1603                    claims
1604                        .account_server_url
1605                        .clone_from(&self.account_server_url);
1606                }
1607                if claims.system_account.is_empty() {
1608                    claims.system_account.clone_from(&self.system_account);
1609                }
1610                claims
1611            },
1612            kind: "operator",
1613            version: 2,
1614        };
1615        build_signing_input(
1616            &self.issuer,
1617            &self.subject_operator,
1618            &self.name,
1619            self.issued_at,
1620            self.expires,
1621            nats,
1622        )
1623    }
1624}
1625
1626// ---------------------------------------------------------------------------
1627// Minimal role JWT builder
1628// ---------------------------------------------------------------------------
1629
1630/// The `nats.type` claim of a minimal role JWT minted via [`RoleJwt`]. These are
1631/// the roles the broker mints through the generic role path; account, user, and
1632/// operator have their own dedicated builders.
1633#[derive(Debug, Clone, Copy, PartialEq, Eq)]
1634pub enum RoleKind {
1635    /// A NATS server JWT (`type=server`).
1636    Server,
1637    /// A NATS curve / x25519 xkey JWT (`type=curve`).
1638    Curve,
1639    /// An account- or operator-signing-key JWT (`type=signer`).
1640    Signer,
1641}
1642
1643impl RoleKind {
1644    /// The `nats.type` claim string for this role.
1645    #[must_use]
1646    pub const fn as_str(self) -> &'static str {
1647        match self {
1648            Self::Server => "server",
1649            Self::Curve => "curve",
1650            Self::Signer => "signer",
1651        }
1652    }
1653}
1654
1655impl core::fmt::Display for RoleKind {
1656    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
1657        f.write_str(self.as_str())
1658    }
1659}
1660
1661/// A minimal NATS JWT for a role whose current broker RPC carries only issuer,
1662/// subject, name, and expiry (`server`, `curve`, account-`signer`). The `nats`
1663/// claim block is just `{type, version}`.
1664pub struct RoleJwt {
1665    /// Issuer = the public `NKey` of the signing key.
1666    pub issuer: String,
1667    /// Subject = the public `NKey` the credential is for.
1668    pub subject: String,
1669    /// Human-readable name (the `name` claim).
1670    pub name: String,
1671    /// Issued-at time (`iat`), Unix seconds.
1672    pub issued_at: u64,
1673    /// Optional expiry (`exp`), Unix seconds; `None` mints a non-expiring token.
1674    pub expires: Option<u64>,
1675    /// The role this token is for, setting its `nats.type` claim.
1676    pub kind: RoleKind,
1677}
1678
1679impl RoleJwt {
1680    /// Produce the JWS **signing input** (`base64url(header).base64url(claims)`).
1681    ///
1682    /// # Errors
1683    ///
1684    /// [`Error::Json`] if claim serialization fails.
1685    pub fn signing_input(&self) -> Result<String, Error> {
1686        let nats = NatsRole {
1687            kind: self.kind.as_str(),
1688            version: 2,
1689        };
1690        build_signing_input(
1691            &self.issuer,
1692            &self.subject,
1693            &self.name,
1694            self.issued_at,
1695            self.expires,
1696            nats,
1697        )
1698    }
1699}
1700
1701/// Append a raw 64-byte Ed25519 `signature` to a `signing_input`, producing the
1702/// final compact JWT.
1703#[must_use]
1704pub fn assemble(signing_input: &str, signature: &[u8]) -> String {
1705    format!("{signing_input}.{}", URL_SAFE_NO_PAD.encode(signature))
1706}
1707
1708/// Render a `nsc`-style `NATS` user `.creds` document from a compact user JWT
1709/// and user `NKey` seed.
1710///
1711/// The result embeds the seed, so it is returned in a [`Zeroizing`] owner and
1712/// must be handled as a secret by callers. Inputs are trimmed and must each fit
1713/// on one line.
1714///
1715/// # Errors
1716///
1717/// Returns [`Error::InvalidClaims`] when `jwt` or `seed` is empty or contains an
1718/// embedded line break.
1719pub fn format_user_creds(jwt: &str, seed: &str) -> Result<Zeroizing<String>, Error> {
1720    let jwt = single_line_creds_field("NATS user JWT", jwt)?;
1721    let seed = single_line_creds_field("NATS user NKey seed", seed)?;
1722    Ok(Zeroizing::new(format!(
1723        "-----BEGIN NATS USER JWT-----\n{jwt}\n------END NATS USER JWT------\n\n************************* IMPORTANT *************************\nNKEY Seed printed below can be used to sign and prove identity.\nNKEYs are sensitive and should be treated as secrets.\n\n-----BEGIN USER NKEY SEED-----\n{seed}\n------END USER NKEY SEED------\n\n*************************************************************\n"
1724    )))
1725}
1726
1727fn single_line_creds_field<'a>(name: &str, value: &'a str) -> Result<&'a str, Error> {
1728    let trimmed = value.trim();
1729    if trimmed.is_empty() {
1730        return Err(Error::InvalidClaims(format!("{name} must not be empty")));
1731    }
1732    if trimmed.contains('\n') || trimmed.contains('\r') {
1733        return Err(Error::InvalidClaims(format!(
1734            "{name} must be a single line"
1735        )));
1736    }
1737    Ok(trimmed)
1738}
1739
1740#[cfg(test)]
1741mod tests {
1742    use super::*;
1743    use alloc::string::ToString;
1744    use alloc::vec;
1745    use nkeys::{KeyPair, XKey};
1746    use serde_json::json;
1747
1748    #[derive(Default)]
1749    struct TestRng(u64);
1750
1751    impl rand_core::TryRng for TestRng {
1752        type Error = core::convert::Infallible;
1753
1754        fn try_next_u32(&mut self) -> Result<u32, Self::Error> {
1755            let mut bytes = [0u8; 4];
1756            self.try_fill_bytes(&mut bytes)?;
1757            Ok(u32::from_le_bytes(bytes))
1758        }
1759
1760        fn try_next_u64(&mut self) -> Result<u64, Self::Error> {
1761            let mut bytes = [0u8; 8];
1762            self.try_fill_bytes(&mut bytes)?;
1763            Ok(u64::from_le_bytes(bytes))
1764        }
1765
1766        fn try_fill_bytes(&mut self, dst: &mut [u8]) -> Result<(), Self::Error> {
1767            for byte in dst {
1768                *byte = self.0.to_le_bytes()[0].wrapping_mul(37).wrapping_add(0x5a);
1769                self.0 = self.0.wrapping_add(1);
1770            }
1771            Ok(())
1772        }
1773    }
1774
1775    fn signed_token(signing_input: &str, signer: &KeyPair) -> String {
1776        let signature = signer.sign(signing_input.as_bytes()).expect("sign");
1777        assemble(signing_input, &signature)
1778    }
1779
1780    #[test]
1781    fn format_user_creds_renders_canonical_nsc_document() {
1782        let creds = format_user_creds(" jwt.token.sig ", " SUUSERSEED ")
1783            .expect("credentials document must render");
1784        assert_eq!(
1785            creds.as_str(),
1786            "-----BEGIN NATS USER JWT-----\njwt.token.sig\n------END NATS USER JWT------\n\n************************* IMPORTANT *************************\nNKEY Seed printed below can be used to sign and prove identity.\nNKEYs are sensitive and should be treated as secrets.\n\n-----BEGIN USER NKEY SEED-----\nSUUSERSEED\n------END USER NKEY SEED------\n\n*************************************************************\n"
1787        );
1788    }
1789
1790    #[test]
1791    fn format_user_creds_rejects_empty_or_multiline_fields() {
1792        assert!(format_user_creds("", "SUUSERSEED").is_err());
1793        assert!(format_user_creds("jwt.token.sig", "").is_err());
1794        assert!(format_user_creds("jwt\nsecond", "SUUSERSEED").is_err());
1795        assert!(format_user_creds("jwt.token.sig", "SU\nseed").is_err());
1796    }
1797
1798    fn assert_valid_token(token: &str, signer: &KeyPair, expected_kind: &str) {
1799        let signer_public = signer.public_key();
1800        let decoded = decode_nats_jwt(token).expect("decode jwt");
1801        assert_eq!(decoded.claims().issuer, signer_public);
1802        assert_eq!(decoded.claims().nats_type.as_deref(), Some(expected_kind));
1803        assert!(decoded.verify_signature(decoded.issuer_public_key()));
1804
1805        let validation = decoded
1806            .verify_with_candidates(
1807                [CandidateSigner::Nkey(&signer_public)],
1808                decoded.claims().issued_at.unwrap_or_default(),
1809            )
1810            .expect("validate");
1811        assert_eq!(validation.reason, NatsJwtValidationReason::Valid);
1812        assert!(validation.is_valid());
1813        assert!(validation.matched_signer.is_some());
1814
1815        let raw_validation = decoded
1816            .verify_with_candidates(
1817                [CandidateSigner::RawPublicKey(decoded.issuer_public_key())],
1818                decoded.claims().issued_at.unwrap_or_default(),
1819            )
1820            .expect("validate raw");
1821        assert_eq!(raw_validation.reason, NatsJwtValidationReason::Valid);
1822        assert!(raw_validation.is_valid());
1823        assert!(raw_validation.matched_signer.is_some());
1824    }
1825
1826    #[test]
1827    fn account_nkey_encoding_matches_nkeys() {
1828        // A real account public from nkeys, decoded to raw bytes, must re-encode
1829        // identically through our encoder, proving prefix byte + CRC + base32.
1830        let account = KeyPair::new_account();
1831        let expected = account.public_key(); // "A..."
1832        let (role, raw) = decode_public(&expected).expect("decode");
1833        assert_eq!(role, NkeyType::Account);
1834        assert_eq!(encode_public(NkeyType::Account, &raw).unwrap(), expected);
1835    }
1836
1837    #[test]
1838    fn decode_public_reports_bad_encoding_separately_from_bad_length() {
1839        assert!(matches!(
1840            decode_public("not-an-nkey"),
1841            Err(Error::BadEncoding)
1842        ));
1843        let short = data_encoding::BASE32_NOPAD.encode(&[0]);
1844        assert!(matches!(
1845            decode_public(&short),
1846            Err(Error::BadPublicKeyLen(_))
1847        ));
1848    }
1849
1850    #[test]
1851    fn user_jwt_verifies_under_issuer_account_key() {
1852        // Build a user JWT issued by an account key, sign it with that key
1853        // (standing in for the vault), then verify the signature decodes from
1854        // the `iss` NKey, i.e. the token is a valid NATS JWT.
1855        let account = KeyPair::new_account();
1856        let user = KeyPair::new_user();
1857
1858        let jwt = UserJwt {
1859            issuer: account.public_key(),
1860            issuer_account: None,
1861            subject_user: user.public_key(),
1862            name: "bob".into(),
1863            issued_at: 1_782_000_000,
1864            expires: None,
1865            permissions: UserPermissions::default(),
1866        };
1867
1868        let signing_input = jwt.signing_input().expect("signing input");
1869        let signature = account.sign(signing_input.as_bytes()).expect("sign");
1870        let token = assemble(&signing_input, &signature);
1871
1872        // Three compact-JWT segments.
1873        let parts: Vec<&str> = token.split('.').collect();
1874        assert_eq!(parts.len(), 3);
1875
1876        // Header is exactly the NATS header.
1877        let header = URL_SAFE_NO_PAD.decode(parts[0]).unwrap();
1878        assert_eq!(header, HEADER_JSON.as_bytes());
1879
1880        // Claims carry the expected iss/sub/nats.type.
1881        let claims: serde_json::Value =
1882            serde_json::from_slice(&URL_SAFE_NO_PAD.decode(parts[1]).unwrap()).unwrap();
1883        assert_eq!(claims["iss"], account.public_key());
1884        assert_eq!(claims["sub"], user.public_key());
1885        assert_eq!(claims["nats"]["type"], "user");
1886        assert_eq!(claims["nats"]["version"], 2);
1887        assert!(claims["jti"].as_str().unwrap().len() >= 52);
1888
1889        // The signature verifies under the issuer account NKey, exactly what a
1890        // NATS server checks.
1891        let issuer = KeyPair::from_public_key(claims["iss"].as_str().unwrap()).unwrap();
1892        issuer
1893            .verify(signing_input.as_bytes(), &signature)
1894            .expect("signature must verify under iss account key");
1895    }
1896
1897    #[test]
1898    fn user_jwt_sets_nats_issuer_account_when_signing_key() {
1899        // A user issued by an account *signing* key: `iss` is the signing key and
1900        // `nats.issuer_account` names the owning account identity so nats-server
1901        // can bind the user to its account.
1902        let signing = KeyPair::new_account();
1903        let account_identity = KeyPair::new_account();
1904        let user = KeyPair::new_user();
1905
1906        let jwt = UserJwt {
1907            issuer: signing.public_key(),
1908            issuer_account: Some(account_identity.public_key()),
1909            subject_user: user.public_key(),
1910            name: "svc".into(),
1911            issued_at: 1_782_000_000,
1912            expires: None,
1913            permissions: UserPermissions::default(),
1914        };
1915        let signing_input = jwt.signing_input().expect("signing input");
1916        let parts: Vec<&str> = signing_input.split('.').collect();
1917        assert_eq!(parts.len(), 2);
1918        let claims: serde_json::Value =
1919            serde_json::from_slice(&URL_SAFE_NO_PAD.decode(parts[1]).unwrap()).unwrap();
1920        assert_eq!(claims["iss"], signing.public_key());
1921        assert_eq!(
1922            claims["nats"]["issuer_account"],
1923            account_identity.public_key()
1924        );
1925
1926        // With no issuer_account the claim is omitted entirely.
1927        let plain = UserJwt {
1928            issuer: signing.public_key(),
1929            issuer_account: None,
1930            subject_user: user.public_key(),
1931            name: "svc".into(),
1932            issued_at: 1_782_000_000,
1933            expires: None,
1934            permissions: UserPermissions::default(),
1935        };
1936        let plain_input = plain.signing_input().expect("signing input");
1937        let plain_parts: Vec<&str> = plain_input.split('.').collect();
1938        let plain_claims: serde_json::Value =
1939            serde_json::from_slice(&URL_SAFE_NO_PAD.decode(plain_parts[1]).unwrap()).unwrap();
1940        assert!(plain_claims["nats"].get("issuer_account").is_none());
1941    }
1942
1943    #[test]
1944    fn encode_public_round_trips_each_role() {
1945        // Each role encodes to a key string starting with its prefix letter, and
1946        // decode_public recovers the same role and raw bytes.
1947        let account = KeyPair::new_account();
1948        let (_, raw) = decode_public(&account.public_key()).unwrap();
1949        for role in [
1950            NkeyType::Account,
1951            NkeyType::Cluster,
1952            NkeyType::Server,
1953            NkeyType::Operator,
1954            NkeyType::User,
1955            NkeyType::Curve,
1956        ] {
1957            let encoded = encode_public(role, &raw).unwrap();
1958            assert!(
1959                encoded.starts_with(role.letter()),
1960                "{role} key should start with {}, got {encoded}",
1961                role.letter()
1962            );
1963            let (decoded_role, decoded_raw) = decode_public(&encoded).unwrap();
1964            assert_eq!(decoded_role, role);
1965            assert_eq!(decoded_raw, raw);
1966        }
1967    }
1968
1969    #[test]
1970    fn nats_curve_box_round_trips_and_validates_wire_shape() {
1971        let sender_private = Zeroizing::new([0x11; 32]);
1972        let receiver_private = Zeroizing::new([0x22; 32]);
1973        let sender_public =
1974            encode_public(NkeyType::Curve, &xkey_public_from_private(&sender_private))
1975                .expect("sender xkey encodes");
1976        let receiver_public = encode_public(
1977            NkeyType::Curve,
1978            &xkey_public_from_private(&receiver_private),
1979        )
1980        .expect("receiver xkey encodes");
1981
1982        let mut rng = TestRng::default();
1983        let boxed = seal_nats_curve(&sender_private, &receiver_public, b"payload", &mut rng)
1984            .expect("seal succeeds");
1985        assert!(boxed.starts_with(XKEY_VERSION_V1));
1986        assert_eq!(
1987            boxed.len(),
1988            XKEY_VERSION_V1.len() + XKEY_NONCE_LEN + XKEY_TAG_LEN + b"payload".len()
1989        );
1990
1991        let opened =
1992            open_nats_curve(&receiver_private, &sender_public, &boxed).expect("open succeeds");
1993        assert_eq!(opened.as_slice(), b"payload");
1994
1995        let mut tampered = boxed;
1996        if let Some(byte) = tampered.last_mut() {
1997            *byte ^= 0x80;
1998        }
1999        assert!(matches!(
2000            open_nats_curve(&receiver_private, &sender_public, &tampered),
2001            Err(Error::XKeyOpenFailed)
2002        ));
2003    }
2004
2005    #[test]
2006    fn nats_curve_box_empty_payload_round_trips() {
2007        let sender_private = Zeroizing::new([0x55; 32]);
2008        let receiver_private = Zeroizing::new([0x66; 32]);
2009        let sender_public =
2010            encode_public(NkeyType::Curve, &xkey_public_from_private(&sender_private))
2011                .expect("sender xkey encodes");
2012        let receiver_public = encode_public(
2013            NkeyType::Curve,
2014            &xkey_public_from_private(&receiver_private),
2015        )
2016        .expect("receiver xkey encodes");
2017
2018        let mut rng = TestRng::default();
2019        let boxed = seal_nats_curve(&sender_private, &receiver_public, b"", &mut rng)
2020            .expect("seal of empty payload succeeds");
2021        assert_eq!(
2022            boxed.len(),
2023            XKEY_VERSION_V1.len() + XKEY_NONCE_LEN + XKEY_TAG_LEN
2024        );
2025
2026        let opened = open_nats_curve(&receiver_private, &sender_public, &boxed)
2027            .expect("open of authentic empty box succeeds");
2028        assert!(opened.is_empty());
2029
2030        // One byte short of nonce + tag is still malformed.
2031        let (_, truncated) = boxed.split_last().expect("box is non-empty");
2032        assert!(matches!(
2033            open_nats_curve(&receiver_private, &sender_public, truncated),
2034            Err(Error::BadXKeyCiphertextLen(_))
2035        ));
2036
2037        // Interop: nkeys opens basil's empty box, and basil opens nkeys' one.
2038        let sender = XKey::new_from_raw(*sender_private);
2039        let receiver = XKey::new_from_raw(*receiver_private);
2040        let nkeys_opened = receiver
2041            .open(&boxed, &sender)
2042            .expect("nkeys opens basil empty box");
2043        assert!(nkeys_opened.is_empty());
2044        let nkeys_box = sender
2045            .seal(b"", &receiver)
2046            .expect("nkeys seals empty payload");
2047        let basil_opened = open_nats_curve(&receiver_private, &sender.public_key(), &nkeys_box)
2048            .expect("basil opens nkeys empty box");
2049        assert!(basil_opened.is_empty());
2050    }
2051
2052    #[test]
2053    fn nats_curve_box_interops_with_nkeys_xkey_both_directions() {
2054        let sender_private = Zeroizing::new([0x33; 32]);
2055        let receiver_private = Zeroizing::new([0x44; 32]);
2056        let sender = XKey::new_from_raw(*sender_private);
2057        let receiver = XKey::new_from_raw(*receiver_private);
2058
2059        let mut rng = TestRng::default();
2060        let basil_box = seal_nats_curve(
2061            &sender_private,
2062            &receiver.public_key(),
2063            b"from basil",
2064            &mut rng,
2065        )
2066        .expect("basil seal succeeds");
2067        let nkeys_opened = receiver
2068            .open(&basil_box, &sender)
2069            .expect("nkeys opens basil box");
2070        assert_eq!(nkeys_opened, b"from basil");
2071
2072        let nkeys_box = sender
2073            .seal(b"from nkeys", &receiver)
2074            .expect("nkeys seal succeeds");
2075        let basil_opened = open_nats_curve(&receiver_private, &sender.public_key(), &nkeys_box)
2076            .expect("basil opens nkeys box");
2077        assert_eq!(basil_opened.as_slice(), b"from nkeys");
2078    }
2079
2080    #[test]
2081    fn nkey_type_letter_round_trips() {
2082        for role in NkeyType::ALL {
2083            assert_eq!(NkeyType::from_letter(role.letter()), Some(role));
2084        }
2085        // Unknown letters (including the seed marker `S`) are not public roles.
2086        assert_eq!(NkeyType::from_letter('Z'), None);
2087        assert_eq!(NkeyType::from_letter('S'), None);
2088    }
2089
2090    #[test]
2091    fn require_public_prefix_rejects_wrong_role() {
2092        let user = KeyPair::new_user();
2093        let err =
2094            require_public_prefix(&user.public_key(), NkeyType::Account).expect_err("wrong role");
2095        assert!(matches!(
2096            err,
2097            Error::UnexpectedPrefix {
2098                expected: NkeyType::Account,
2099                actual: NkeyType::User
2100            }
2101        ));
2102        require_public_prefix(&user.public_key(), NkeyType::User).expect("user role");
2103    }
2104
2105    #[test]
2106    fn account_jwt_verifies_under_issuer_operator_key() {
2107        // An account JWT issued (signed) by an operator key. Verifies under the
2108        // operator `iss` NKey, carries nats.type=account, and round-trips signing.
2109        let operator = KeyPair::new_operator();
2110        let account = KeyPair::new_account();
2111        let signing = KeyPair::new_account();
2112
2113        let jwt = AccountJwt {
2114            issuer: operator.public_key(),
2115            subject_account: account.public_key(),
2116            name: "acme".into(),
2117            issued_at: 1_782_000_000,
2118            expires: Some(1_782_003_600),
2119            signing_keys: vec![signing.public_key()],
2120            claims: AccountClaims::default(),
2121        };
2122
2123        let signing_input = jwt.signing_input().expect("signing input");
2124        let signature = operator.sign(signing_input.as_bytes()).expect("sign");
2125        let token = assemble(&signing_input, &signature);
2126
2127        let parts: Vec<&str> = token.split('.').collect();
2128        assert_eq!(parts.len(), 3);
2129        assert_eq!(
2130            URL_SAFE_NO_PAD.decode(parts[0]).unwrap(),
2131            HEADER_JSON.as_bytes()
2132        );
2133        let claims: serde_json::Value =
2134            serde_json::from_slice(&URL_SAFE_NO_PAD.decode(parts[1]).unwrap()).unwrap();
2135        assert_eq!(claims["iss"], operator.public_key());
2136        assert_eq!(claims["sub"], account.public_key());
2137        assert_eq!(claims["nats"]["type"], "account");
2138        assert_eq!(claims["nats"]["version"], 2);
2139        assert_eq!(claims["nats"]["signing_keys"][0], signing.public_key());
2140        assert_eq!(claims["exp"], 1_782_003_600u64);
2141
2142        let issuer = KeyPair::from_public_key(claims["iss"].as_str().unwrap()).unwrap();
2143        issuer
2144            .verify(signing_input.as_bytes(), &signature)
2145            .expect("must verify under iss operator key");
2146    }
2147
2148    #[test]
2149    fn operator_jwt_self_signed_verifies_and_omits_empty_fields() {
2150        // A self-signed operator (iss == sub). With no optional fields set, the
2151        // nats block is just {type,version}; with them set they appear.
2152        let operator = KeyPair::new_operator();
2153        let sys = KeyPair::new_account();
2154
2155        let jwt = OperatorJwt {
2156            issuer: operator.public_key(),
2157            subject_operator: operator.public_key(),
2158            name: "root-op".into(),
2159            issued_at: 1_782_000_000,
2160            expires: None,
2161            signing_keys: Vec::new(),
2162            account_server_url: "nats://localhost:4222".into(),
2163            system_account: sys.public_key(),
2164            claims: OperatorClaims::default(),
2165        };
2166
2167        let signing_input = jwt.signing_input().expect("signing input");
2168        let signature = operator.sign(signing_input.as_bytes()).expect("sign");
2169        let token = assemble(&signing_input, &signature);
2170
2171        let parts: Vec<&str> = token.split('.').collect();
2172        let claims: serde_json::Value =
2173            serde_json::from_slice(&URL_SAFE_NO_PAD.decode(parts[1]).unwrap()).unwrap();
2174        assert_eq!(claims["iss"], operator.public_key());
2175        assert_eq!(claims["sub"], operator.public_key());
2176        assert_eq!(claims["nats"]["type"], "operator");
2177        assert_eq!(
2178            claims["nats"]["account_server_url"],
2179            "nats://localhost:4222"
2180        );
2181        assert_eq!(claims["nats"]["system_account"], sys.public_key());
2182        // Non-expiring: exp omitted, not null.
2183        assert!(claims.get("exp").is_none());
2184        // Empty signing_keys omitted.
2185        assert!(claims["nats"].get("signing_keys").is_none());
2186
2187        let issuer = KeyPair::from_public_key(claims["iss"].as_str().unwrap()).unwrap();
2188        issuer
2189            .verify(signing_input.as_bytes(), &signature)
2190            .expect("must verify under iss operator key");
2191    }
2192
2193    #[test]
2194    #[allow(clippy::too_many_lines)]
2195    fn account_jwt_serializes_rich_claim_surface() {
2196        let operator = KeyPair::new_operator();
2197        let account = KeyPair::new_account();
2198        let signing = KeyPair::new_account();
2199        let exporting = KeyPair::new_account();
2200        let user = KeyPair::new_user();
2201
2202        let mut revocations = BTreeMap::new();
2203        revocations.insert(user.public_key(), 1_782_000_001);
2204        let mut export_revocations = BTreeMap::new();
2205        export_revocations.insert("*".to_string(), 1_782_000_002);
2206        let mut tiered_limits = BTreeMap::new();
2207        tiered_limits.insert(
2208            "gold".to_string(),
2209            JetStreamLimits {
2210                mem_storage: 64,
2211                disk_storage: 128,
2212                streams: 3,
2213                consumer: 4,
2214                max_ack_pending: 5,
2215                mem_max_stream_bytes: 6,
2216                disk_max_stream_bytes: 7,
2217                max_bytes_required: true,
2218            },
2219        );
2220
2221        let jwt = AccountJwt {
2222            issuer: operator.public_key(),
2223            subject_account: account.public_key(),
2224            name: "tenant".into(),
2225            issued_at: 1_782_000_000,
2226            expires: None,
2227            signing_keys: vec![signing.public_key()],
2228            claims: AccountClaims {
2229                imports: vec![AccountImport {
2230                    name: "orders-in".into(),
2231                    subject: "orders.*".into(),
2232                    account: exporting.public_key(),
2233                    /* ubs false positive: not a hardcoded secret */
2234                    /* ubs:ignore */
2235                    token: "activation.jwt".into(),
2236                    to: String::new(),
2237                    local_subject: "tenant.$1".into(),
2238                    kind: ExportType::Stream,
2239                    share: false,
2240                    allow_trace: true,
2241                }],
2242                exports: vec![AccountExport {
2243                    name: "lookup".into(),
2244                    subject: "svc.lookup".into(),
2245                    kind: ExportType::Service,
2246                    token_req: true,
2247                    revocations: export_revocations,
2248                    response_type: Some(ResponseType::Stream),
2249                    response_threshold: 1_000_000,
2250                    service_latency: Some(ServiceLatency {
2251                        sampling: SamplingRate::Headers,
2252                        results: "latency.results".into(),
2253                    }),
2254                    account_token_position: 0,
2255                    advertise: true,
2256                    allow_trace: true,
2257                    description: "lookup service".into(),
2258                    info_url: "https://example.test/lookup".into(),
2259                }],
2260                limits: OperatorLimits {
2261                    nats: NatsLimits {
2262                        subs: 100,
2263                        data: 1_024,
2264                        payload: 256,
2265                    },
2266                    account: AccountLimits {
2267                        imports: 10,
2268                        exports: 11,
2269                        wildcards: true,
2270                        disallow_bearer: true,
2271                        conn: 12,
2272                        leaf: 13,
2273                    },
2274                    jetstream: JetStreamLimits::default(),
2275                    tiered_limits,
2276                },
2277                signing_keys: Vec::new(),
2278                revocations,
2279                default_permissions: Some(Permissions {
2280                    publish: Permission {
2281                        allow: vec!["pub.>".into()],
2282                        deny: vec!["pub.secret".into()],
2283                    },
2284                    sub: Permission {
2285                        allow: vec!["sub.>".into()],
2286                        deny: Vec::new(),
2287                    },
2288                    resp: Some(ResponsePermission {
2289                        max: 1,
2290                        ttl: 2_000_000_000,
2291                    }),
2292                }),
2293                mappings: BTreeMap::from([(
2294                    "legacy.>".into(),
2295                    vec![WeightedMapping {
2296                        subject: "modern.>".into(),
2297                        weight: 50,
2298                        cluster: "edge".into(),
2299                    }],
2300                )]),
2301                authorization: ExternalAuthorization {
2302                    auth_users: vec![user.public_key()],
2303                    allowed_accounts: vec![account.public_key()],
2304                    xkey: encode_public(NkeyType::Curve, &[9; 32]).unwrap(),
2305                },
2306                trace: Some(MsgTrace {
2307                    dest: "trace.out".into(),
2308                    sampling: 25,
2309                }),
2310                cluster_traffic: Some(ClusterTraffic::System),
2311            },
2312        };
2313
2314        let signing_input = jwt.signing_input().expect("signing input");
2315        let parts: Vec<&str> = signing_input.split('.').collect();
2316        let claims: serde_json::Value =
2317            serde_json::from_slice(&URL_SAFE_NO_PAD.decode(parts[1]).unwrap()).unwrap();
2318        let nats = &claims["nats"];
2319
2320        assert_eq!(nats["type"], "account");
2321        assert_eq!(nats["imports"][0]["type"], "stream");
2322        assert_eq!(nats["imports"][0]["local_subject"], "tenant.$1");
2323        assert_eq!(nats["exports"][0]["type"], "service");
2324        assert_eq!(nats["exports"][0]["response_type"], "Stream");
2325        assert_eq!(nats["exports"][0]["service_latency"]["sampling"], "headers");
2326        assert_eq!(nats["limits"]["imports"], 10);
2327        assert_eq!(nats["limits"]["wildcards"], true);
2328        assert_eq!(nats["limits"]["tiered_limits"]["gold"]["mem_storage"], 64);
2329        assert_eq!(nats["revocations"][user.public_key()], 1_782_000_001);
2330        assert_eq!(nats["default_permissions"]["pub"]["allow"][0], "pub.>");
2331        assert_eq!(nats["default_permissions"]["resp"]["ttl"], 2_000_000_000i64);
2332        assert_eq!(nats["mappings"]["legacy.>"][0]["subject"], "modern.>");
2333        assert_eq!(nats["authorization"]["auth_users"][0], user.public_key());
2334        assert_eq!(nats["trace"]["dest"], "trace.out");
2335        assert_eq!(nats["cluster_traffic"], "system");
2336        assert_eq!(nats["signing_keys"][0], signing.public_key());
2337    }
2338
2339    #[test]
2340    fn unlimited_operator_limits_serialize_as_a_real_not_deny_all_limits_block() {
2341        // Regression for the deny-all account bug: an empty `OperatorLimits`
2342        // block is skipped entirely, and `nats-server` reads a missing/zero
2343        // connection or subscription limit as deny-all (only `-1` is unlimited).
2344        // `OperatorLimits::unlimited()` must serialize a present limits block with
2345        // `-1` connection/subscription limits, matching a default `nsc` account.
2346        let operator = KeyPair::new_operator();
2347        let account = KeyPair::new_account();
2348        let jwt = AccountJwt {
2349            issuer: operator.public_key(),
2350            subject_account: account.public_key(),
2351            name: "tenant".into(),
2352            issued_at: 1_782_000_000,
2353            expires: None,
2354            signing_keys: Vec::new(),
2355            claims: AccountClaims {
2356                limits: OperatorLimits::unlimited(),
2357                ..AccountClaims::default()
2358            },
2359        };
2360
2361        let signing_input = jwt.signing_input().expect("signing input");
2362        let parts: Vec<&str> = signing_input.split('.').collect();
2363        let claims: serde_json::Value =
2364            serde_json::from_slice(&URL_SAFE_NO_PAD.decode(parts[1]).unwrap()).unwrap();
2365        let limits = &claims["nats"]["limits"];
2366
2367        // Present, not skipped as empty (the deny-all bug omitted this object).
2368        assert!(limits.is_object());
2369        assert_eq!(limits["conn"], -1);
2370        assert_eq!(limits["subs"], -1);
2371        assert_eq!(limits["leaf"], -1);
2372        assert_eq!(limits["data"], -1);
2373        assert_eq!(limits["payload"], -1);
2374        assert_eq!(limits["imports"], -1);
2375        assert_eq!(limits["exports"], -1);
2376        assert_eq!(limits["wildcards"], true);
2377        // JetStream stays disabled: zero limits are skipped, so no such key.
2378        assert!(limits["mem_storage"].is_null());
2379    }
2380
2381    #[test]
2382    fn operator_jwt_serializes_service_urls_and_strict_signing_keys() {
2383        let operator = KeyPair::new_operator();
2384        let signing = KeyPair::new_operator();
2385
2386        let jwt = OperatorJwt {
2387            issuer: operator.public_key(),
2388            subject_operator: operator.public_key(),
2389            name: "root".into(),
2390            issued_at: 1_782_000_000,
2391            expires: None,
2392            signing_keys: vec![signing.public_key()],
2393            account_server_url: "https://accounts.example.test/jwt/v1".into(),
2394            system_account: String::new(),
2395            claims: OperatorClaims {
2396                operator_service_urls: vec![
2397                    "nats://nats.example.test:4222".into(),
2398                    "tls://nats.example.test:4443".into(),
2399                ],
2400                assert_server_version: "2.11.0".into(),
2401                strict_signing_key_usage: true,
2402                ..OperatorClaims::default()
2403            },
2404        };
2405
2406        let signing_input = jwt.signing_input().expect("signing input");
2407        let parts: Vec<&str> = signing_input.split('.').collect();
2408        let claims: serde_json::Value =
2409            serde_json::from_slice(&URL_SAFE_NO_PAD.decode(parts[1]).unwrap()).unwrap();
2410        let nats = &claims["nats"];
2411
2412        assert_eq!(nats["type"], "operator");
2413        assert_eq!(nats["signing_keys"][0], signing.public_key());
2414        assert_eq!(
2415            nats["account_server_url"],
2416            "https://accounts.example.test/jwt/v1"
2417        );
2418        assert_eq!(
2419            nats["operator_service_urls"][0],
2420            "nats://nats.example.test:4222"
2421        );
2422        assert_eq!(nats["assert_server_version"], "2.11.0");
2423        assert_eq!(nats["strict_signing_key_usage"], true);
2424    }
2425
2426    #[test]
2427    fn role_jwt_verifies_and_carries_kind() {
2428        let operator = KeyPair::new_operator();
2429        let server = KeyPair::new_server();
2430        let jwt = RoleJwt {
2431            issuer: operator.public_key(),
2432            subject: server.public_key(),
2433            name: "nats-server".into(),
2434            issued_at: 1_782_000_000,
2435            expires: Some(1_782_003_600),
2436            kind: RoleKind::Server,
2437        };
2438
2439        let signing_input = jwt.signing_input().expect("signing input");
2440        let signature = operator.sign(signing_input.as_bytes()).expect("sign");
2441        let token = assemble(&signing_input, &signature);
2442
2443        let parts: Vec<&str> = token.split('.').collect();
2444        let claims: serde_json::Value =
2445            serde_json::from_slice(&URL_SAFE_NO_PAD.decode(parts[1]).unwrap()).unwrap();
2446        assert_eq!(claims["iss"], operator.public_key());
2447        assert_eq!(claims["sub"], server.public_key());
2448        assert_eq!(claims["nats"]["type"], "server");
2449        assert_eq!(claims["nats"]["version"], 2);
2450
2451        let issuer = KeyPair::from_public_key(claims["iss"].as_str().unwrap()).unwrap();
2452        issuer
2453            .verify(signing_input.as_bytes(), &signature)
2454            .expect("must verify under iss operator key");
2455    }
2456
2457    #[test]
2458    fn decode_and_validate_known_good_user_account_operator_tokens() {
2459        let account = KeyPair::new_account();
2460        let user = KeyPair::new_user();
2461        let user_jwt = UserJwt {
2462            issuer: account.public_key(),
2463            issuer_account: None,
2464            subject_user: user.public_key(),
2465            name: "bob".into(),
2466            issued_at: 1_782_000_000,
2467            expires: Some(1_782_003_600),
2468            permissions: UserPermissions::default(),
2469        };
2470        let user_input = user_jwt.signing_input().expect("user input");
2471        let user_token = signed_token(&user_input, &account);
2472        assert_valid_token(&user_token, &account, "user");
2473
2474        let operator = KeyPair::new_operator();
2475        let tenant = KeyPair::new_account();
2476        let account_jwt = AccountJwt {
2477            issuer: operator.public_key(),
2478            subject_account: tenant.public_key(),
2479            name: "tenant".into(),
2480            issued_at: 1_782_000_000,
2481            expires: Some(1_782_003_600),
2482            signing_keys: Vec::new(),
2483            claims: AccountClaims::default(),
2484        };
2485        let account_input = account_jwt.signing_input().expect("account input");
2486        let account_token = signed_token(&account_input, &operator);
2487        assert_valid_token(&account_token, &operator, "account");
2488
2489        let operator_jwt = OperatorJwt {
2490            issuer: operator.public_key(),
2491            subject_operator: operator.public_key(),
2492            name: "root".into(),
2493            issued_at: 1_782_000_000,
2494            expires: Some(1_782_003_600),
2495            signing_keys: Vec::new(),
2496            account_server_url: String::new(),
2497            system_account: String::new(),
2498            claims: OperatorClaims::default(),
2499        };
2500        let operator_input = operator_jwt.signing_input().expect("operator input");
2501        let operator_token = signed_token(&operator_input, &operator);
2502        assert_valid_token(&operator_token, &operator, "operator");
2503    }
2504
2505    #[test]
2506    fn validation_rejects_expired_and_not_yet_valid_tokens() {
2507        let account = KeyPair::new_account();
2508        let user = KeyPair::new_user();
2509        let expired = UserJwt {
2510            issuer: account.public_key(),
2511            issuer_account: None,
2512            subject_user: user.public_key(),
2513            name: "old".into(),
2514            issued_at: 100,
2515            expires: Some(200),
2516            permissions: UserPermissions::default(),
2517        };
2518        let expired_input = expired.signing_input().expect("expired input");
2519        let expired_token = signed_token(&expired_input, &account);
2520        let decoded_expired = decode_nats_jwt(&expired_token).expect("decode expired");
2521        let account_public = account.public_key();
2522        let expired_validation = decoded_expired
2523            .verify_with_candidates([CandidateSigner::Nkey(&account_public)], 200)
2524            .expect("validate expired");
2525        assert_eq!(expired_validation.reason, NatsJwtValidationReason::Expired);
2526        assert!(!expired_validation.is_valid());
2527        assert!(expired_validation.matched_signer.is_none());
2528
2529        let claims = json!({
2530            "jti": "manual",
2531            "iat": 100_u64,
2532            "iss": account.public_key(),
2533            "name": "future",
2534            "nbf": 300_u64,
2535            "nats": {"type": "user", "version": 2},
2536            "sub": user.public_key(),
2537        });
2538        let future_input = signing_input_from_claims(&claims).expect("future input");
2539        let future_token = signed_token(&future_input, &account);
2540        let decoded_future = decode_nats_jwt(&future_token).expect("decode future");
2541        let future_validation = decoded_future
2542            .verify_with_candidates([CandidateSigner::Nkey(&account_public)], 299)
2543            .expect("validate future");
2544        assert_eq!(
2545            future_validation.reason,
2546            NatsJwtValidationReason::NotYetValid
2547        );
2548        assert!(!future_validation.is_valid());
2549        assert!(future_validation.matched_signer.is_none());
2550    }
2551
2552    #[test]
2553    fn validation_rejects_tampered_signature_and_unknown_signer() {
2554        let account = KeyPair::new_account();
2555        let other = KeyPair::new_account();
2556        let user = KeyPair::new_user();
2557        let jwt = UserJwt {
2558            issuer: account.public_key(),
2559            issuer_account: None,
2560            subject_user: user.public_key(),
2561            name: "bob".into(),
2562            issued_at: 1_782_000_000,
2563            expires: None,
2564            permissions: UserPermissions::default(),
2565        };
2566        let input = jwt.signing_input().expect("input");
2567        let mut signature = account.sign(input.as_bytes()).expect("sign");
2568        signature[0] ^= 1;
2569        let tampered = assemble(&input, &signature);
2570
2571        let account_public = account.public_key();
2572        let decoded_tampered = decode_nats_jwt(&tampered).expect("decode tampered");
2573        let bad_signature = decoded_tampered
2574            .verify_with_candidates([CandidateSigner::Nkey(&account_public)], 1_782_000_000)
2575            .expect("validate tampered");
2576        assert_eq!(bad_signature.reason, NatsJwtValidationReason::BadSignature);
2577        assert!(!bad_signature.is_valid());
2578        assert!(bad_signature.matched_signer.is_none());
2579
2580        let valid = signed_token(&input, &account);
2581        let decoded_valid = decode_nats_jwt(&valid).expect("decode valid");
2582        let other_public = other.public_key();
2583        let unknown = decoded_valid
2584            .verify_with_candidates([CandidateSigner::Nkey(&other_public)], 1_782_000_000)
2585            .expect("validate unknown");
2586        assert_eq!(unknown.reason, NatsJwtValidationReason::UnknownSigner);
2587        assert!(!unknown.is_valid());
2588        assert!(unknown.matched_signer.is_none());
2589    }
2590
2591    #[test]
2592    fn public_nkey_verifies_raw_signature() {
2593        let user = KeyPair::new_user();
2594        let message = b"basil sealed invocation digest";
2595        let signature = user.sign(message).expect("sign");
2596        assert!(
2597            verify_public_signature(&user.public_key(), message, &signature)
2598                .expect("valid public nkey")
2599        );
2600
2601        let other = KeyPair::new_user();
2602        assert!(
2603            !verify_public_signature(&other.public_key(), message, &signature)
2604                .expect("valid other public nkey")
2605        );
2606        assert!(matches!(
2607            verify_public_signature(&user.public_key(), message, &[0_u8; 63]),
2608            Err(Error::BadSignatureLen(63))
2609        ));
2610        assert!(matches!(
2611            verify_public_signature("not-an-nkey", message, &signature),
2612            Err(Error::BadEncoding | Error::BadPublicKeyLen(_) | Error::UnsupportedPrefix(_))
2613        ));
2614    }
2615
2616    #[test]
2617    fn decode_rejects_malformed_tokens() {
2618        assert!(matches!(
2619            decode_nats_jwt("not-a-jwt"),
2620            Err(Error::MalformedJwt(_))
2621        ));
2622
2623        let claims = URL_SAFE_NO_PAD.encode(br#"{"iss":"O","sub":"U"}"#);
2624        let bad_header = format!(
2625            "{}.{}.{}",
2626            URL_SAFE_NO_PAD.encode(br#"{"alg":"none"}"#),
2627            claims,
2628            ""
2629        );
2630        assert!(matches!(
2631            decode_nats_jwt(&bad_header),
2632            Err(Error::MalformedJwt(_))
2633        ));
2634
2635        let header = URL_SAFE_NO_PAD.encode(HEADER_JSON.as_bytes());
2636        let bad_signature = format!("{header}.{claims}.{}", URL_SAFE_NO_PAD.encode([1_u8, 2]));
2637        assert!(matches!(
2638            decode_nats_jwt(&bad_signature),
2639            Err(Error::MalformedJwt(_) | Error::BadSignatureLen(_))
2640        ));
2641    }
2642}