Skip to main content

basil_core/core/
minter.rs

1//! Credential minters.
2//!
3//! A minter assembles a format-specific credential (here, a NATS user JWT) by
4//! delegating the actual signature to the [`Backend`], so the signing key
5//! never leaves the vault. This is the generic "assemble in the broker, sign in
6//! the backend" pattern; SPIFFE-SVID and other formats can join later.
7
8use std::time::{SystemTime, UNIX_EPOCH};
9
10use base64::Engine;
11use base64::engine::general_purpose::URL_SAFE_NO_PAD;
12use p256::elliptic_curve::sec1::ToEncodedPoint as _;
13use rsa::pkcs1::DecodeRsaPublicKey;
14use rsa::pkcs8::DecodePublicKey;
15use rsa::traits::PublicKeyParts;
16use serde_json::{Value, json};
17use sha2::{Digest, Sha256};
18
19use basil_nats::{NkeyType, RoleKind};
20
21use crate::backend::{Backend, BackendError, SignOptions};
22
23/// Standard JWT claim keys the preset owns and a caller may NOT supply in
24/// `claims` (reserved-claim protection, §6). A collision is `invalid_request`.
25const RESERVED_GENERIC_CLAIMS: &[&str] = &["iss", "iat", "exp", "jti", "sub", "nbf"];
26
27/// The reserved claims for the `svid` preset: the generic set plus `aud`, which
28/// the preset owns (the required SVID audience, §6). A caller `claims.aud` is the
29/// *source* of the audience, but it is consumed by the preset, not merged as an
30/// arbitrary extra, so it is listed here and stripped before the merge.
31const RESERVED_SVID_CLAIMS: &[&str] = &["iss", "iat", "exp", "jti", "sub", "nbf", "aud"];
32
33/// NATS JWT claim kinds Basil can validate and sign.
34#[derive(Debug, Clone, Copy, PartialEq, Eq)]
35pub enum NatsJwtKind {
36    /// A NATS user JWT (`nats.type=user`).
37    User,
38    /// A NATS account JWT (`nats.type=account`).
39    Account,
40    /// A NATS operator JWT (`nats.type=operator`).
41    Operator,
42    /// A NATS signing-key JWT (`nats.type=signer`).
43    Signer,
44    /// A NATS server JWT (`nats.type=server`).
45    Server,
46    /// A NATS curve/xkey JWT (`nats.type=curve`).
47    Curve,
48}
49
50impl NatsJwtKind {
51    /// Parse the `nats.type` claim.
52    #[must_use]
53    pub fn parse(value: &str) -> Option<Self> {
54        match value {
55            "user" => Some(Self::User),
56            "account" => Some(Self::Account),
57            "operator" => Some(Self::Operator),
58            "signer" => Some(Self::Signer),
59            "server" => Some(Self::Server),
60            "curve" => Some(Self::Curve),
61            _ => None,
62        }
63    }
64
65    /// The canonical `nats.type` string.
66    #[must_use]
67    pub const fn as_str(self) -> &'static str {
68        match self {
69            Self::User => "user",
70            Self::Account => "account",
71            Self::Operator => "operator",
72            Self::Signer => "signer",
73            Self::Server => "server",
74            Self::Curve => "curve",
75        }
76    }
77}
78
79impl std::fmt::Display for NatsJwtKind {
80    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
81        f.write_str(self.as_str())
82    }
83}
84
85/// How [`sign_nats_jwt`] handles a supplied but incorrect NATS `jti`.
86#[derive(Debug, Clone, Copy, PartialEq, Eq)]
87pub enum NatsJtiMode {
88    /// Reject a supplied `jti` that does not match the NATS standard-claim hash.
89    RequireValid,
90    /// Replace a missing or mismatched `jti` with the computed value.
91    Rewrite,
92}
93
94/// Inputs for [`sign_nats_jwt`].
95pub struct SignNatsJwtSpec<'a> {
96    /// Catalog/backend key id to sign with.
97    pub signing_key_id: &'a str,
98    /// NATS role of the issuer key, from the catalog `nats_type` label.
99    pub issuer_role: NkeyType,
100    /// Caller-supplied NATS JWT claims.
101    pub claims: &'a Value,
102    /// Optional assertion against `claims.nats.type`.
103    pub expected_kind: Option<NatsJwtKind>,
104    /// Override/insert `iat` as a Unix timestamp.
105    pub issued_at: Option<u64>,
106    /// Override/insert `exp` as a Unix timestamp.
107    pub expires_at: Option<u64>,
108    /// Handling for a supplied but stale `jti`.
109    pub jti_mode: NatsJtiMode,
110}
111
112/// The JWS `alg` for a JWT-SVID, derived from the issuer key's catalog key type.
113///
114/// `EdDSA` is raw Ed25519 (RFC 8037); `RS256` is RSASSA-PKCS1-v1_5 over SHA-256;
115/// `ES256` is ECDSA P-256 over SHA-256 and `ES384` is ECDSA P-384 over SHA-384,
116/// both with raw fixed-width `r || s` signatures. In all cases the **backend**
117/// signs the signing input, so the private key never leaves the vault.
118#[derive(Debug, Clone, Copy, PartialEq, Eq)]
119pub enum SvidAlg {
120    /// Ed25519 issuer (`ed25519` / `ed25519-nkey`).
121    EdDsa,
122    /// RSA-2048 issuer (`rsa-2048`).
123    Rs256,
124    /// ECDSA P-256 issuer (`ecdsa-p256`).
125    Es256,
126    /// ECDSA P-384 issuer (`ecdsa-p384`).
127    Es384,
128}
129
130impl SvidAlg {
131    /// The JWS `alg` header string.
132    const fn header_alg(self) -> &'static str {
133        match self {
134            Self::EdDsa => "EdDSA",
135            Self::Rs256 => "RS256",
136            Self::Es256 => "ES256",
137            Self::Es384 => "ES384",
138        }
139    }
140
141    /// Backend signing options required for this JWS algorithm.
142    const fn sign_options(self) -> SignOptions {
143        match self {
144            Self::EdDsa => SignOptions::Default,
145            Self::Rs256 => SignOptions::Rs256Pkcs1v15Sha256,
146            Self::Es256 => SignOptions::Es256,
147            Self::Es384 => SignOptions::Es384,
148        }
149    }
150}
151
152/// Build JWKS bytes for a JWT-SVID issuer key held by `backend`.
153///
154/// The returned bytes are a UTF-8 JSON Web Key Set (`{"keys":[...]}`) suitable
155/// for a SPIFFE JWT bundle response. The `kid` is deterministic from
156/// `(alg, public_key)`, so issuer rotation changes the published key id without
157/// backend-specific metadata.
158pub async fn jwt_svid_jwks(
159    backend: &dyn Backend,
160    signing_key_id: &str,
161    alg: SvidAlg,
162) -> Result<Vec<u8>, BackendError> {
163    let public_key = backend.public_key(signing_key_id).await?;
164    jwt_svid_jwks_from_public_key(&public_key, alg)
165}
166
167/// Build grace-window JWKS bytes for a JWT-SVID issuer held by `backend`.
168///
169/// Reflects the **rotation grace window**: every version's public key still
170/// inside `[grace_floor ..= latest]` is published, so a verifier can validate a
171/// token signed by a recently rotated-away version, while versions below the
172/// floor are dropped.
173///
174/// Fetches the issuer's whole version→public-key map ([`Backend::public_keys`])
175/// in one round-trip and the latest version ([`Backend::public_keys`] keys), then
176/// applies `grace_floor` (the broker's [`crate::state::BrokerLimits::grace_floor`]
177/// for the latest version). This is the **single source of truth** used by both
178/// the gRPC Workload-API JWKS and the HTTP JWKS so they never diverge.
179pub async fn jwt_svid_jwks_grace(
180    backend: &dyn Backend,
181    signing_key_id: &str,
182    alg: SvidAlg,
183    grace_floor: impl Fn(u32) -> u32,
184) -> Result<Vec<u8>, BackendError> {
185    let versions = backend.public_keys(signing_key_id).await?;
186    let latest = versions.keys().copied().max().unwrap_or(1);
187    jwt_svid_jwks_grace_window(&versions, latest, grace_floor(latest), alg)
188}
189
190/// Build JWKS bytes from already-fetched JWT-SVID issuer public key bytes.
191///
192/// A single-key set for the supplied public half. The grace-window variant
193/// [`jwt_svid_jwks_grace_window`] publishes every in-window version.
194pub fn jwt_svid_jwks_from_public_key(
195    public_key: &[u8],
196    alg: SvidAlg,
197) -> Result<Vec<u8>, BackendError> {
198    let key = jwk_for_public_key(public_key, alg)?;
199    serde_json::to_vec(&json!({ "keys": [key] }))
200        .map_err(|e| BackendError::Protocol(format!("serializing jwks: {e}")))
201}
202
203/// Build grace-window JWKS bytes from a version→public-key map.
204///
205/// Reflects the **rotation grace window**: one JWK per key version in
206/// `[grace_floor ..= latest]` that has a published public half, so a verifier can
207/// validate a token signed by a recently-rotated-away version, and versions below
208/// the floor are absent.
209///
210/// `versions` is the backend's whole version→public-key map (e.g. transit's
211/// `data.keys`, fetched via [`Backend::public_keys`]); `grace_floor` is
212/// [`crate::state::BrokerLimits::grace_floor`] applied to `latest`. Each in-window
213/// version becomes a JWK with a distinct content-derived `kid` (two versions with
214/// identical public bytes would collide on `kid` and de-duplicate, which is
215/// correct). This is the **single source of truth** the gRPC Workload-API JWKS and
216/// the HTTP JWKS both use, so the two surfaces never diverge.
217///
218/// # Errors
219///
220/// Returns a [`BackendError`] if a version's public key cannot be parsed into a
221/// JWK or the set cannot be serialized.
222pub fn jwt_svid_jwks_grace_window(
223    versions: &std::collections::BTreeMap<u32, Vec<u8>>,
224    latest: u32,
225    grace_floor: u32,
226    alg: SvidAlg,
227) -> Result<Vec<u8>, BackendError> {
228    let mut keys: Vec<Value> = Vec::new();
229    let mut seen_kids: Vec<String> = Vec::new();
230    for (&version, public_key) in versions {
231        // Only versions inside the grace window are published; older ones are
232        // dropped so a token keyed to a pre-floor version no longer resolves.
233        if version < grace_floor || version > latest {
234            continue;
235        }
236        let kid = jwt_svid_jwk_kid(public_key, alg);
237        if seen_kids.iter().any(|k| k == &kid) {
238            continue;
239        }
240        seen_kids.push(kid);
241        keys.push(jwk_for_public_key(public_key, alg)?);
242    }
243    serde_json::to_vec(&json!({ "keys": keys }))
244        .map_err(|e| BackendError::Protocol(format!("serializing jwks: {e}")))
245}
246
247/// Build a single JWK [`Value`] (not a set) for one issuer public key + `alg`.
248/// Shared by the single-key and grace-window generators so the JWK shape
249/// (`kty`/`crv`/`n`/`e`/`use`/`alg`/`kid`) is defined in exactly one place.
250fn jwk_for_public_key(public_key: &[u8], alg: SvidAlg) -> Result<Value, BackendError> {
251    let kid = jwt_svid_jwk_kid(public_key, alg);
252    match alg {
253        SvidAlg::EdDsa => {
254            if public_key.len() != 32 {
255                return Err(BackendError::Protocol(format!(
256                    "Ed25519 JWT issuer public key must be 32 bytes, got {}",
257                    public_key.len()
258                )));
259            }
260            Ok(json!({
261                "kty": "OKP",
262                "crv": "Ed25519",
263                "x": URL_SAFE_NO_PAD.encode(public_key),
264                "use": "sig",
265                "alg": alg.header_alg(),
266                "kid": kid,
267            }))
268        }
269        SvidAlg::Rs256 => {
270            let key = decode_rsa_public_key(public_key)?;
271            Ok(json!({
272                "kty": "RSA",
273                "n": URL_SAFE_NO_PAD.encode(key.n().to_bytes_be()),
274                "e": URL_SAFE_NO_PAD.encode(key.e().to_bytes_be()),
275                "use": "sig",
276                "alg": alg.header_alg(),
277                "kid": kid,
278            }))
279        }
280        SvidAlg::Es256 => {
281            let key = decode_p256_public_key(public_key)?;
282            let encoded = key.to_encoded_point(false);
283            let x = encoded.x().ok_or_else(|| {
284                BackendError::Protocol("P-256 public key has no x coordinate".into())
285            })?;
286            let y = encoded.y().ok_or_else(|| {
287                BackendError::Protocol("P-256 public key has no y coordinate".into())
288            })?;
289            Ok(json!({
290                "kty": "EC",
291                "crv": "P-256",
292                "x": URL_SAFE_NO_PAD.encode(x),
293                "y": URL_SAFE_NO_PAD.encode(y),
294                "use": "sig",
295                "alg": alg.header_alg(),
296                "kid": kid,
297            }))
298        }
299        SvidAlg::Es384 => {
300            let key = decode_p384_public_key(public_key)?;
301            let encoded = key.to_encoded_point(false);
302            let x = encoded.x().ok_or_else(|| {
303                BackendError::Protocol("P-384 public key has no x coordinate".into())
304            })?;
305            let y = encoded.y().ok_or_else(|| {
306                BackendError::Protocol("P-384 public key has no y coordinate".into())
307            })?;
308            Ok(json!({
309                "kty": "EC",
310                "crv": "P-384",
311                "x": URL_SAFE_NO_PAD.encode(x),
312                "y": URL_SAFE_NO_PAD.encode(y),
313                "use": "sig",
314                "alg": alg.header_alg(),
315                "kid": kid,
316            }))
317        }
318    }
319}
320
321/// Deterministic key id for a JWT-SVID issuer public key.
322#[must_use]
323pub fn jwt_svid_jwk_kid(public_key: &[u8], alg: SvidAlg) -> String {
324    let mut hasher = Sha256::new();
325    hasher.update(alg.header_alg().as_bytes());
326    hasher.update([0]);
327    hasher.update(public_key);
328    URL_SAFE_NO_PAD.encode(hasher.finalize())
329}
330
331fn decode_rsa_public_key(public_key: &[u8]) -> Result<rsa::RsaPublicKey, BackendError> {
332    if let Ok(pem) = std::str::from_utf8(public_key)
333        && pem.trim_start().starts_with("-----BEGIN ")
334    {
335        return rsa::RsaPublicKey::from_public_key_pem(pem)
336            .or_else(|_| rsa::RsaPublicKey::from_pkcs1_pem(pem))
337            .map_err(|e| BackendError::Protocol(format!("RSA public key PEM is malformed: {e}")));
338    }
339
340    rsa::RsaPublicKey::from_public_key_der(public_key)
341        .or_else(|_| rsa::RsaPublicKey::from_pkcs1_der(public_key))
342        .map_err(|e| BackendError::Protocol(format!("RSA public key DER is malformed: {e}")))
343}
344
345fn decode_p256_public_key(public_key: &[u8]) -> Result<p256::PublicKey, BackendError> {
346    if let Ok(pem) = std::str::from_utf8(public_key)
347        && pem.trim_start().starts_with("-----BEGIN ")
348    {
349        return p256::PublicKey::from_public_key_pem(pem).map_err(|e| {
350            BackendError::Protocol(format!("P-256 public key PEM is malformed: {e}"))
351        });
352    }
353
354    p256::PublicKey::from_public_key_der(public_key)
355        .map_err(|e| BackendError::Protocol(format!("P-256 public key DER is malformed: {e}")))
356}
357
358fn decode_p384_public_key(public_key: &[u8]) -> Result<p384::PublicKey, BackendError> {
359    if let Ok(pem) = std::str::from_utf8(public_key)
360        && pem.trim_start().starts_with("-----BEGIN ")
361    {
362        return p384::PublicKey::from_public_key_pem(pem).map_err(|e| {
363            BackendError::Protocol(format!("P-384 public key PEM is malformed: {e}"))
364        });
365    }
366
367    p384::PublicKey::from_public_key_der(public_key)
368        .map_err(|e| BackendError::Protocol(format!("P-384 public key DER is malformed: {e}")))
369}
370
371/// A reserved-claim collision: the caller tried to set a preset-owned claim.
372/// The handler maps this onto the `invalid_request` wire code.
373#[derive(Debug)]
374pub struct ReservedClaim(pub String);
375
376impl std::fmt::Display for ReservedClaim {
377    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
378        write!(f, "claims may not override the reserved claim `{}`", self.0)
379    }
380}
381impl std::error::Error for ReservedClaim {}
382
383/// Current UNIX time in seconds.
384fn unix_now() -> Result<u64, BackendError> {
385    Ok(SystemTime::now()
386        .duration_since(UNIX_EPOCH)
387        .map_err(|e| BackendError::Backend(e.to_string()))?
388        .as_secs())
389}
390
391/// Mint a NATS user JWT whose issuing account key lives in the backend.
392///
393/// Steps: derive the issuer `NKey` (`A…`) from the signing key's Ed25519 public
394/// half, build the JWT signing input, have the backend sign it, and assemble the
395/// token. The seed/private key is never read out.
396///
397/// `issuer_account` sets the `nats.issuer_account` claim. It is **required** when
398/// `signing_key_id` is an account *signing* key rather than the account identity
399/// key: without it the minted user names no owning account and `nats-server`
400/// rejects the connection with an authorization violation. Pass the owning
401/// account's identity public `NKey` (`A…`); leave it `None` when the signing key
402/// itself *is* the account identity key. Basil cannot tell the two apart from the
403/// key alone, so naming the account is the caller's responsibility.
404#[allow(clippy::too_many_arguments)] // distinct scalar claim inputs; bundling them
405// into a struct would just move the arity to the call site without clarifying it.
406pub async fn mint_nats_user(
407    backend: &dyn Backend,
408    signing_key_id: &str,
409    issuer_role: NkeyType,
410    subject_user_nkey: &str,
411    issuer_account: Option<&str>,
412    name: &str,
413    expires_in_secs: Option<u64>,
414    permissions: basil_nats::UserPermissions,
415) -> Result<String, BackendError> {
416    ensure_issuer_role(issuer_role, &[NkeyType::Account], "user")?;
417    // The subject must be a well-formed user public NKey.
418    basil_nats::require_public_prefix(subject_user_nkey, NkeyType::User)
419        .map_err(|e| BackendError::Protocol(format!("invalid subject user nkey: {e}")))?;
420    // A supplied issuer account must be a well-formed account public NKey.
421    let issuer_account = match issuer_account {
422        Some(account) => {
423            basil_nats::require_public_prefix(account, NkeyType::Account)
424                .map_err(|e| BackendError::Protocol(format!("invalid issuer account nkey: {e}")))?;
425            Some(account.to_string())
426        }
427        None => None,
428    };
429
430    // Issuer (`iss`) NKey from the backend signing key's public half.
431    let public = backend.public_key(signing_key_id).await?;
432    let issuer = basil_nats::encode_public(issuer_role, &public)
433        .map_err(|e| BackendError::Protocol(format!("deriving issuer nkey: {e}")))?;
434
435    let now = unix_now()?;
436
437    let jwt = basil_nats::UserJwt {
438        issuer,
439        issuer_account,
440        subject_user: subject_user_nkey.to_string(),
441        name: name.to_string(),
442        issued_at: now,
443        expires: expires_in_secs.map(|s| now.saturating_add(s)),
444        permissions,
445    };
446    let signing_input = jwt
447        .signing_input()
448        .map_err(|e| BackendError::Protocol(format!("building nats jwt: {e}")))?;
449
450    // The backend performs the Ed25519 signature over the signing input.
451    let signature = backend
452        .sign(signing_key_id, signing_input.as_bytes())
453        .await?;
454
455    Ok(basil_nats::assemble(&signing_input, &signature))
456}
457
458/// Mint a NATS **account** JWT whose issuing operator key lives in the backend.
459///
460/// The issuer (`O…`) `NKey` is derived from the backend key's public half via
461/// the `nats_type` prefix letter (`O` for an operator issuer). The subject is
462/// the account public `NKey` (`A…`) being signed. The backend signs the signing
463/// input; the seed never leaves the vault.
464pub async fn mint_nats_account(
465    backend: &dyn Backend,
466    signing_key_id: &str,
467    issuer_role: NkeyType,
468    subject_account_nkey: &str,
469    name: &str,
470    expires_in_secs: Option<u64>,
471    signing_keys: Vec<String>,
472) -> Result<String, BackendError> {
473    ensure_issuer_role(
474        issuer_role,
475        &[NkeyType::Account, NkeyType::Operator],
476        "account",
477    )?;
478    basil_nats::require_public_prefix(subject_account_nkey, NkeyType::Account)
479        .map_err(|e| BackendError::Protocol(format!("invalid subject account nkey: {e}")))?;
480    for key in &signing_keys {
481        basil_nats::require_public_prefix(key, NkeyType::Account)
482            .map_err(|e| BackendError::Protocol(format!("invalid account signing key: {e}")))?;
483    }
484
485    let public = backend.public_key(signing_key_id).await?;
486    let issuer = basil_nats::encode_public(issuer_role, &public)
487        .map_err(|e| BackendError::Protocol(format!("deriving issuer nkey: {e}")))?;
488
489    let now = unix_now()?;
490    let jwt = basil_nats::AccountJwt {
491        issuer,
492        subject_account: subject_account_nkey.to_string(),
493        name: name.to_string(),
494        issued_at: now,
495        expires: expires_in_secs.map(|s| now.saturating_add(s)),
496        signing_keys,
497        // A minted account JWT must carry a limits block: nats-server treats a
498        // zero connection/subscription limit as deny-all, so without this the
499        // account rejects every connection. Default to the standard unlimited
500        // account (JetStream stays disabled).
501        claims: basil_nats::AccountClaims {
502            limits: basil_nats::OperatorLimits::unlimited(),
503            ..basil_nats::AccountClaims::default()
504        },
505    };
506    let signing_input = jwt
507        .signing_input()
508        .map_err(|e| BackendError::Protocol(format!("building nats account jwt: {e}")))?;
509    let signature = backend
510        .sign(signing_key_id, signing_input.as_bytes())
511        .await?;
512    Ok(basil_nats::assemble(&signing_input, &signature))
513}
514
515/// Mint a NATS **operator** JWT (usually self-signed).
516///
517/// The issuer (`O…`) `NKey` is derived from the backend operator key's public
518/// half; the subject defaults to the same operator unless an explicit
519/// `subject_operator_nkey` is given. The backend signs in place.
520#[allow(clippy::too_many_arguments)] // distinct scalar claim inputs; bundling them
521// into a struct would just move the arity to the call site without clarifying it.
522pub async fn mint_nats_operator(
523    backend: &dyn Backend,
524    signing_key_id: &str,
525    issuer_role: NkeyType,
526    subject_operator_nkey: Option<&str>,
527    name: &str,
528    expires_in_secs: Option<u64>,
529    signing_keys: Vec<String>,
530    account_server_url: String,
531    system_account: String,
532) -> Result<String, BackendError> {
533    ensure_issuer_role(issuer_role, &[NkeyType::Operator], "operator")?;
534    let public = backend.public_key(signing_key_id).await?;
535    let issuer = basil_nats::encode_public(issuer_role, &public)
536        .map_err(|e| BackendError::Protocol(format!("deriving issuer nkey: {e}")))?;
537
538    // An explicit subject must be a well-formed public NKey; otherwise the
539    // operator self-signs (sub == iss).
540    let subject_operator = match subject_operator_nkey {
541        Some(s) => {
542            basil_nats::require_public_prefix(s, NkeyType::Operator).map_err(|e| {
543                BackendError::Protocol(format!("invalid subject operator nkey: {e}"))
544            })?;
545            s.to_string()
546        }
547        None => issuer.clone(),
548    };
549    for key in &signing_keys {
550        basil_nats::require_public_prefix(key, NkeyType::Operator)
551            .map_err(|e| BackendError::Protocol(format!("invalid operator signing key: {e}")))?;
552    }
553    if !system_account.is_empty() {
554        basil_nats::require_public_prefix(&system_account, NkeyType::Account)
555            .map_err(|e| BackendError::Protocol(format!("invalid system account nkey: {e}")))?;
556    }
557
558    let now = unix_now()?;
559    let jwt = basil_nats::OperatorJwt {
560        issuer,
561        subject_operator,
562        name: name.to_string(),
563        issued_at: now,
564        expires: expires_in_secs.map(|s| now.saturating_add(s)),
565        signing_keys,
566        account_server_url,
567        system_account,
568        claims: basil_nats::OperatorClaims::default(),
569    };
570    let signing_input = jwt
571        .signing_input()
572        .map_err(|e| BackendError::Protocol(format!("building nats operator jwt: {e}")))?;
573    let signature = backend
574        .sign(signing_key_id, signing_input.as_bytes())
575        .await?;
576    Ok(basil_nats::assemble(&signing_input, &signature))
577}
578
579fn ensure_issuer_role(
580    issuer: NkeyType,
581    allowed: &[NkeyType],
582    claim_kind: &'static str,
583) -> Result<(), BackendError> {
584    if allowed.contains(&issuer) {
585        return Ok(());
586    }
587    let expected = allowed.iter().map(|role| role.letter()).collect::<String>();
588    Err(BackendError::Protocol(format!(
589        "unsupported issuer role {issuer} for nats {claim_kind}; expected one of {expected}"
590    )))
591}
592
593async fn mint_nats_role(
594    backend: &dyn Backend,
595    spec: RoleMintSpec<'_>,
596) -> Result<String, BackendError> {
597    let RoleMintSpec {
598        signing_key_id,
599        issuer_role,
600        subject_nkey,
601        subject_role,
602        name,
603        expires_in_secs,
604        kind,
605    } = spec;
606    ensure_issuer_role(issuer_role, &[subject_role], kind.as_str())?;
607    basil_nats::require_public_prefix(subject_nkey, subject_role).map_err(|e| {
608        BackendError::Protocol(format!("invalid subject {} nkey: {e}", kind.as_str()))
609    })?;
610
611    let public = backend.public_key(signing_key_id).await?;
612    let issuer = basil_nats::encode_public(issuer_role, &public)
613        .map_err(|e| BackendError::Protocol(format!("deriving issuer nkey: {e}")))?;
614    let now = unix_now()?;
615    let jwt = basil_nats::RoleJwt {
616        issuer,
617        subject: subject_nkey.to_string(),
618        name: name.to_string(),
619        issued_at: now,
620        expires: expires_in_secs.map(|s| now.saturating_add(s)),
621        kind,
622    };
623    let signing_input = jwt
624        .signing_input()
625        .map_err(|e| BackendError::Protocol(format!("building nats {} jwt: {e}", kind.as_str())))?;
626    let signature = backend
627        .sign(signing_key_id, signing_input.as_bytes())
628        .await?;
629    Ok(basil_nats::assemble(&signing_input, &signature))
630}
631
632struct RoleMintSpec<'a> {
633    signing_key_id: &'a str,
634    issuer_role: NkeyType,
635    subject_nkey: &'a str,
636    subject_role: NkeyType,
637    name: &'a str,
638    expires_in_secs: Option<u64>,
639    kind: RoleKind,
640}
641
642pub async fn mint_nats_signer(
643    backend: &dyn Backend,
644    signing_key_id: &str,
645    issuer_role: NkeyType,
646    subject_nkey: &str,
647    name: &str,
648    expires_in_secs: Option<u64>,
649) -> Result<String, BackendError> {
650    ensure_issuer_role(
651        issuer_role,
652        &[NkeyType::Account, NkeyType::Operator],
653        "signer",
654    )?;
655    // A signer's subject shares the issuer's role (an account signs an account
656    // signing key; an operator signs an operator signing key).
657    basil_nats::require_public_prefix(subject_nkey, issuer_role)
658        .map_err(|e| BackendError::Protocol(format!("invalid subject signer nkey: {e}")))?;
659
660    let public = backend.public_key(signing_key_id).await?;
661    let issuer = basil_nats::encode_public(issuer_role, &public)
662        .map_err(|e| BackendError::Protocol(format!("deriving issuer nkey: {e}")))?;
663    let now = unix_now()?;
664    let jwt = basil_nats::RoleJwt {
665        issuer,
666        subject: subject_nkey.to_string(),
667        name: name.to_string(),
668        issued_at: now,
669        expires: expires_in_secs.map(|s| now.saturating_add(s)),
670        kind: RoleKind::Signer,
671    };
672    let signing_input = jwt
673        .signing_input()
674        .map_err(|e| BackendError::Protocol(format!("building nats signer jwt: {e}")))?;
675    let signature = backend
676        .sign(signing_key_id, signing_input.as_bytes())
677        .await?;
678    Ok(basil_nats::assemble(&signing_input, &signature))
679}
680
681pub async fn mint_nats_server(
682    backend: &dyn Backend,
683    signing_key_id: &str,
684    issuer_role: NkeyType,
685    subject_server_nkey: &str,
686    name: &str,
687    expires_in_secs: Option<u64>,
688) -> Result<String, BackendError> {
689    mint_nats_role(
690        backend,
691        RoleMintSpec {
692            signing_key_id,
693            issuer_role,
694            subject_nkey: subject_server_nkey,
695            subject_role: NkeyType::Server,
696            name,
697            expires_in_secs,
698            kind: RoleKind::Server,
699        },
700    )
701    .await
702}
703
704pub async fn mint_nats_curve(
705    backend: &dyn Backend,
706    signing_key_id: &str,
707    issuer_role: NkeyType,
708    subject_curve_nkey: &str,
709    name: &str,
710    expires_in_secs: Option<u64>,
711) -> Result<String, BackendError> {
712    mint_nats_role(
713        backend,
714        RoleMintSpec {
715            signing_key_id,
716            issuer_role,
717            subject_nkey: subject_curve_nkey,
718            subject_role: NkeyType::Curve,
719            name,
720            expires_in_secs,
721            kind: RoleKind::Curve,
722        },
723    )
724    .await
725}
726
727/// Validate, normalize, and sign a caller-supplied NATS JWT claim document.
728///
729/// Basil derives the `iss` `NKey` from the catalog key's public half and
730/// `nats_type` label. A supplied `iss` must match; an omitted `iss` is inserted.
731/// `sub`, `name`, `iat`, and the `nats` block are validated before the backend
732/// signs the NATS `ed25519-nkey` signing input. Missing `iat` defaults to now,
733/// and `expires_at` overrides any supplied `exp`.
734pub async fn sign_nats_jwt(
735    backend: &dyn Backend,
736    spec: SignNatsJwtSpec<'_>,
737) -> Result<String, BackendError> {
738    let SignNatsJwtSpec {
739        signing_key_id,
740        issuer_role,
741        claims,
742        expected_kind,
743        issued_at,
744        expires_at,
745        jti_mode,
746    } = spec;
747    let mut claims = claims
748        .as_object()
749        .ok_or_else(|| BackendError::Protocol("invalid nats jwt claims: expected object".into()))?
750        .clone();
751
752    let public = backend.public_key(signing_key_id).await?;
753    let issuer = basil_nats::encode_public(issuer_role, &public)
754        .map_err(|e| BackendError::Protocol(format!("deriving issuer nkey: {e}")))?;
755    set_or_validate_string(&mut claims, "iss", &issuer)?;
756
757    let now = unix_now()?;
758    let iat = match issued_at {
759        Some(value) => {
760            claims.insert("iat".into(), Value::Number(value.into()));
761            value
762        }
763        None => {
764            if let Some(value) = claims.get("iat") {
765                claim_u64(value, "iat")?
766            } else {
767                claims.insert("iat".into(), Value::Number(now.into()));
768                now
769            }
770        }
771    };
772    let exp = match expires_at {
773        Some(value) => {
774            claims.insert("exp".into(), Value::Number(value.into()));
775            Some(value)
776        }
777        None => claims
778            .get("exp")
779            .map(|value| claim_u64(value, "exp"))
780            .transpose()?,
781    };
782
783    let sub = required_claim_string(&claims, "sub")?.to_string();
784    let name = optional_claim_string(&claims, "name")?;
785    let aud = optional_claim_string(&claims, "aud")?;
786    let nbf = claims
787        .get("nbf")
788        .map(|value| claim_u64(value, "nbf"))
789        .transpose()?;
790    let kind = validate_nats_claim(&claims, expected_kind)?;
791    validate_nats_roles(issuer_role, kind, &sub)?;
792
793    let computed_jti = basil_nats::jti_for_standard_claims(&issuer, &sub, name, iat, exp, aud, nbf)
794        .map_err(|e| BackendError::Protocol(format!("building nats jwt jti: {e}")))?;
795    match claims.get("jti") {
796        Some(value) if value.as_str() == Some(computed_jti.as_str()) => {}
797        Some(_) if jti_mode == NatsJtiMode::Rewrite => {
798            claims.insert("jti".into(), Value::String(computed_jti));
799        }
800        Some(_) => {
801            return Err(BackendError::Protocol(
802                "invalid nats jwt jti: supplied jti does not match standard claims".into(),
803            ));
804        }
805        None => {
806            claims.insert("jti".into(), Value::String(computed_jti));
807        }
808    }
809
810    let signing_input = basil_nats::signing_input_from_claims(&Value::Object(claims))
811        .map_err(|e| BackendError::Protocol(format!("building nats jwt: {e}")))?;
812    let signature = backend
813        .sign(signing_key_id, signing_input.as_bytes())
814        .await?;
815    Ok(basil_nats::assemble(&signing_input, &signature))
816}
817
818fn set_or_validate_string(
819    claims: &mut serde_json::Map<String, Value>,
820    field: &'static str,
821    expected: &str,
822) -> Result<(), BackendError> {
823    match claims.get(field) {
824        Some(value) if value.as_str() == Some(expected) => Ok(()),
825        Some(_) => Err(BackendError::Protocol(format!(
826            "invalid nats jwt {field}: does not match signing key"
827        ))),
828        None => {
829            claims.insert(field.into(), Value::String(expected.to_string()));
830            Ok(())
831        }
832    }
833}
834
835fn required_claim_string<'a>(
836    claims: &'a serde_json::Map<String, Value>,
837    field: &'static str,
838) -> Result<&'a str, BackendError> {
839    claims
840        .get(field)
841        .and_then(Value::as_str)
842        .filter(|value| !value.is_empty())
843        .ok_or_else(|| BackendError::Protocol(format!("invalid nats jwt {field}: required string")))
844}
845
846fn optional_claim_string<'a>(
847    claims: &'a serde_json::Map<String, Value>,
848    field: &'static str,
849) -> Result<Option<&'a str>, BackendError> {
850    let Some(value) = claims.get(field) else {
851        return Ok(None);
852    };
853    value
854        .as_str()
855        .map(|value| (!value.is_empty()).then_some(value))
856        .ok_or_else(|| BackendError::Protocol(format!("invalid nats jwt {field}: required string")))
857}
858
859fn claim_u64(value: &Value, field: &'static str) -> Result<u64, BackendError> {
860    if let Some(value) = value.as_u64() {
861        return Ok(value);
862    }
863    Err(BackendError::Protocol(format!(
864        "invalid nats jwt {field}: required u64"
865    )))
866}
867
868fn validate_nats_claim(
869    claims: &serde_json::Map<String, Value>,
870    expected_kind: Option<NatsJwtKind>,
871) -> Result<NatsJwtKind, BackendError> {
872    let nats = claims
873        .get("nats")
874        .and_then(Value::as_object)
875        .ok_or_else(|| BackendError::Protocol("invalid nats jwt nats: required object".into()))?;
876    let kind = nats
877        .get("type")
878        .and_then(Value::as_str)
879        .and_then(NatsJwtKind::parse)
880        .ok_or_else(|| {
881            BackendError::Protocol("invalid nats jwt nats.type: unsupported or missing".into())
882        })?;
883    if let Some(expected) = expected_kind
884        && expected != kind
885    {
886        return Err(BackendError::Protocol(format!(
887            "invalid nats jwt nats.type: expected {expected}, got {kind}"
888        )));
889    }
890    let version = nats
891        .get("version")
892        .ok_or_else(|| BackendError::Protocol("invalid nats jwt nats.version: expected 2".into()))
893        .and_then(|value| claim_u64(value, "nats.version"))?;
894    if version != 2 {
895        return Err(BackendError::Protocol(
896            "invalid nats jwt nats.version: expected 2".into(),
897        ));
898    }
899    Ok(kind)
900}
901
902fn validate_nats_roles(
903    issuer_role: NkeyType,
904    kind: NatsJwtKind,
905    subject_nkey: &str,
906) -> Result<(), BackendError> {
907    let allowed_issuers: &[NkeyType] = match kind {
908        NatsJwtKind::User => &[NkeyType::Account],
909        NatsJwtKind::Account | NatsJwtKind::Signer => &[NkeyType::Account, NkeyType::Operator],
910        NatsJwtKind::Operator => &[NkeyType::Operator],
911        NatsJwtKind::Server => &[NkeyType::Server],
912        NatsJwtKind::Curve => &[NkeyType::Curve],
913    };
914    ensure_issuer_role(issuer_role, allowed_issuers, kind.as_str())?;
915
916    let subject_role = match kind {
917        NatsJwtKind::User => NkeyType::User,
918        NatsJwtKind::Account | NatsJwtKind::Signer => NkeyType::Account,
919        NatsJwtKind::Operator => NkeyType::Operator,
920        NatsJwtKind::Server => NkeyType::Server,
921        NatsJwtKind::Curve => NkeyType::Curve,
922    };
923    let subject_role = if kind == NatsJwtKind::Signer {
924        issuer_role
925    } else {
926        subject_role
927    };
928    basil_nats::require_public_prefix(subject_nkey, subject_role)
929        .map_err(|e| BackendError::Protocol(format!("invalid nats jwt sub: {e}")))?;
930    Ok(())
931}
932
933/// Build the **generic** JWT signing input (`base64url(header).base64url(claims)`)
934/// for the issuer's key type, then have the backend sign it and assemble the
935/// compact JWS.
936///
937/// The preset owns `iss` (the dotted issuer name), `iat`, `exp` (from `ttl`),
938/// `jti` (a deterministic, RNG-free `base64url` SHA-512/256 over the standard
939/// claims, a stable unique-per-token id), and `sub`. Caller `extra` claims may
940/// only add **non-reserved** keys: a collision is a [`ReservedClaim`] error the
941/// handler maps to `invalid_request`.
942///
943/// The JWS `alg` is derived from the issuer key type, matching the JWT-SVID
944/// path: Ed25519 issuers produce `EdDSA` tokens; ECDSA P-256 issuers `ES256` and
945/// P-384 issuers `ES384`; RSA issuers `RS256`, each with a `kid` matching the
946/// JWKS entry for the same public key.
947pub async fn mint_generic(
948    backend: &dyn Backend,
949    signing_key_id: &str,
950    issuer_name: &str,
951    subject: &str,
952    expires_in_secs: Option<u64>,
953    extra: &Value,
954) -> Result<String, GenericMintError> {
955    // Start from the preset-owned standard claims, then merge non-reserved extras.
956    let now = unix_now()?;
957    let exp = expires_in_secs.map(|s| now.saturating_add(s));
958
959    let mut claims = serde_json::Map::new();
960    claims.insert("iss".into(), Value::String(issuer_name.to_string()));
961    claims.insert("sub".into(), Value::String(subject.to_string()));
962    claims.insert("iat".into(), Value::Number(now.into()));
963    if let Some(exp) = exp {
964        claims.insert("exp".into(), Value::Number(exp.into()));
965    }
966
967    // `jti`: a deterministic, URL-safe SHA-512/256 over the standard claims so
968    // far (stable, no RNG dependency, a unique-per-token identifier).
969    claims.insert("jti".into(), Value::String(jti_for(&claims)?));
970
971    let public_key = backend.public_key(signing_key_id).await?;
972    let alg = jws_alg_for_public_key(&public_key)?;
973
974    // Merge caller extras, then build the signing input and have the backend sign.
975    merge_extras(&mut claims, extra, RESERVED_GENERIC_CLAIMS)?;
976    sign_jwt_claims(backend, signing_key_id, alg, claims).await
977}
978
979/// Mint a SPIFFE **JWT-SVID** (§6, `svid` preset) whose issuer key lives in the
980/// backend.
981///
982/// The seed never leaves the vault: the backend signs the signing input and the
983/// raw signature is `base64url`-assembled into the compact JWS.
984///
985/// The preset owns `iss` (`issuer_id`), `sub` (`subject_spiffe_id`, a validated
986/// `spiffe://…` id templated by the handler from the request or the attested
987/// caller), `aud` (the **required** SVID audience), `iat`, `exp` (from `ttl`),
988/// and `jti`. The `alg` is the issuer's SPIFFE JWT-SVID profile signing
989/// algorithm (`RS256` for RSA-2048; `ES256` for ECDSA P-256).
990/// `EdDSA`/Ed25519 is not a JWT-SVID profile alg and is rejected at catalog
991/// load, so a JWT-SVID issuer never signs with `EdDSA`. Caller `extra` claims
992/// may only add **non-reserved**
993/// keys (a collision, including `aud`, which the preset consumes, is a
994/// [`ReservedClaim`] the handler maps to `invalid_request`).
995#[allow(clippy::too_many_arguments)] // distinct preset-owned inputs; bundling them
996// into a struct would just move the arity to the call site without clarifying it.
997pub async fn mint_svid(
998    backend: &dyn Backend,
999    signing_key_id: &str,
1000    issuer_id: &str,
1001    alg: SvidAlg,
1002    subject_spiffe_id: &str,
1003    audience: &str,
1004    expires_in_secs: Option<u64>,
1005    extra: &Value,
1006) -> Result<String, GenericMintError> {
1007    let now = unix_now()?;
1008    let exp = expires_in_secs.map(|s| now.saturating_add(s));
1009
1010    let mut claims = serde_json::Map::new();
1011    claims.insert("iss".into(), Value::String(issuer_id.to_string()));
1012    claims.insert("sub".into(), Value::String(subject_spiffe_id.to_string()));
1013    claims.insert("aud".into(), Value::String(audience.to_string()));
1014    claims.insert("iat".into(), Value::Number(now.into()));
1015    if let Some(exp) = exp {
1016        claims.insert("exp".into(), Value::Number(exp.into()));
1017    }
1018    claims.insert("jti".into(), Value::String(jti_for(&claims)?));
1019
1020    // Caller extras may add non-reserved keys only; `aud` is preset-owned (the
1021    // handler already lifted it out of `extra`), so a leftover `aud` is rejected.
1022    merge_extras(&mut claims, extra, RESERVED_SVID_CLAIMS)?;
1023    sign_jwt_claims(backend, signing_key_id, alg, claims).await
1024}
1025
1026/// Merge caller `extra` claims into `claims`, rejecting any key in `reserved`.
1027///
1028/// A non-object, non-null `extra` is itself a malformed request (mapped to
1029/// `invalid_request`).
1030fn merge_extras(
1031    claims: &mut serde_json::Map<String, Value>,
1032    extra: &Value,
1033    reserved: &[&str],
1034) -> Result<(), GenericMintError> {
1035    if let Some(obj) = extra.as_object() {
1036        for (k, v) in obj {
1037            if reserved.contains(&k.as_str()) {
1038                return Err(GenericMintError::Reserved(ReservedClaim(k.clone())));
1039            }
1040            claims.insert(k.clone(), v.clone());
1041        }
1042        Ok(())
1043    } else if extra.is_null() {
1044        Ok(())
1045    } else {
1046        Err(GenericMintError::Reserved(ReservedClaim(
1047            "<claims must be a JSON object>".into(),
1048        )))
1049    }
1050}
1051
1052/// A deterministic, RNG-free `jti`: `base64url(SHA-512/256(serialized claims))`.
1053/// Stable and unique-per-token without a random dependency (§6).
1054fn jti_for(claims: &serde_json::Map<String, Value>) -> Result<String, BackendError> {
1055    use sha2::{Digest, Sha512_256};
1056    let bytes = serde_json::to_vec(claims)
1057        .map_err(|e| BackendError::Protocol(format!("serializing claims: {e}")))?;
1058    let mut hasher = Sha512_256::new();
1059    hasher.update(&bytes);
1060    Ok(URL_SAFE_NO_PAD.encode(hasher.finalize()))
1061}
1062
1063fn jws_alg_for_public_key(public_key: &[u8]) -> Result<SvidAlg, BackendError> {
1064    if public_key.len() == 32 {
1065        return Ok(SvidAlg::EdDsa);
1066    }
1067    // ECDSA curves are probed before RSA: an EC `SubjectPublicKeyInfo` carries the
1068    // `id-ecPublicKey` algorithm OID, so the RSA decoders reject it (and vice
1069    // versa). There is no ambiguous key that satisfies both.
1070    if decode_p256_public_key(public_key).is_ok() {
1071        return Ok(SvidAlg::Es256);
1072    }
1073    if decode_p384_public_key(public_key).is_ok() {
1074        return Ok(SvidAlg::Es384);
1075    }
1076    if decode_rsa_public_key(public_key).is_ok() {
1077        return Ok(SvidAlg::Rs256);
1078    }
1079    Err(BackendError::Protocol(format!(
1080        "JWT issuer public key must be Ed25519 raw public bytes, ECDSA P-256/P-384 PEM/DER, or RSA PEM/DER, got {} bytes",
1081        public_key.len()
1082    )))
1083}
1084
1085/// Build the compact-JWS signing input (`base64url(header).base64url(claims)`)
1086/// for `alg`, have the **backend** sign it, and assemble the token. Shared by
1087/// [`mint_generic`] and [`mint_svid`]: the only per-preset difference is the
1088/// header `alg` and the (already-built) claim set.
1089async fn sign_jwt_claims(
1090    backend: &dyn Backend,
1091    signing_key_id: &str,
1092    alg: SvidAlg,
1093    claims: serde_json::Map<String, Value>,
1094) -> Result<String, GenericMintError> {
1095    // The `kid` header MUST match the published JWKS key id so a SPIFFE verifier
1096    // (and the SPIFFE JWT-SVID profile, which requires `kid`) can select the
1097    // signing key. It is deterministic over (alg, public key), the same value
1098    // `jwt_svid_jwks` publishes for the bundle.
1099    let public_key = backend.public_key(signing_key_id).await?;
1100    let kid = jwt_svid_jwk_kid(&public_key, alg);
1101    let header_bytes = serde_json::to_vec(&json!({
1102        "typ": "JWT",
1103        "alg": alg.header_alg(),
1104        "kid": kid,
1105    }))
1106    .map_err(|e| BackendError::Protocol(format!("serializing JWS header: {e}")))?;
1107    let claims_bytes = serde_json::to_vec(&claims)
1108        .map_err(|e| BackendError::Protocol(format!("serializing claims: {e}")))?;
1109    let signing_input = format!(
1110        "{}.{}",
1111        URL_SAFE_NO_PAD.encode(&header_bytes),
1112        URL_SAFE_NO_PAD.encode(&claims_bytes)
1113    );
1114    let signature = backend
1115        .sign_with_options(signing_key_id, signing_input.as_bytes(), alg.sign_options())
1116        .await?;
1117    Ok(format!(
1118        "{signing_input}.{}",
1119        URL_SAFE_NO_PAD.encode(&signature)
1120    ))
1121}
1122
1123/// The error returned by [`mint_generic`]: either a reserved-claim collision
1124/// (→ `invalid_request`) or an underlying backend error (mapped per [`backend`]).
1125///
1126/// [`backend`]: crate::backend
1127#[derive(Debug)]
1128pub enum GenericMintError {
1129    /// A caller `claims` key collided with a preset-owned reserved claim.
1130    Reserved(ReservedClaim),
1131    /// A backend/serialization failure while building or signing the token.
1132    Backend(BackendError),
1133}
1134
1135impl std::fmt::Display for GenericMintError {
1136    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1137        match self {
1138            Self::Reserved(e) => write!(f, "{e}"),
1139            Self::Backend(e) => write!(f, "{e}"),
1140        }
1141    }
1142}
1143impl std::error::Error for GenericMintError {}
1144impl From<BackendError> for GenericMintError {
1145    fn from(e: BackendError) -> Self {
1146        Self::Backend(e)
1147    }
1148}
1149
1150#[cfg(test)]
1151mod tests {
1152    use super::*;
1153    use async_trait::async_trait;
1154    use base64::engine::general_purpose::URL_SAFE_NO_PAD;
1155    use nkeys::KeyPair;
1156
1157    use crate::backend::NewKey;
1158    use basil_proto::KeyType;
1159
1160    /// A signing backend backed by a real `nkeys::KeyPair`, so the minters
1161    /// produce signatures that verify under the issuer `NKey` (exactly what a
1162    /// NATS server checks). Only the methods the minters call are implemented.
1163    struct KeyPairBackend(KeyPair);
1164
1165    #[async_trait]
1166    impl Backend for KeyPairBackend {
1167        fn kind(&self) -> &'static str {
1168            "nkey-test"
1169        }
1170        async fn new_key(&self, _key_type: KeyType) -> Result<NewKey, BackendError> {
1171            Err(BackendError::Unsupported("new_key"))
1172        }
1173        async fn public_key(&self, _key_id: &str) -> Result<Vec<u8>, BackendError> {
1174            // The raw 32-byte Ed25519 public half from the keypair's NKey.
1175            let (_, raw) = basil_nats::decode_public(&self.0.public_key())
1176                .map_err(|e| BackendError::Protocol(e.to_string()))?;
1177            Ok(raw.to_vec())
1178        }
1179        async fn sign(&self, _key_id: &str, message: &[u8]) -> Result<Vec<u8>, BackendError> {
1180            self.0
1181                .sign(message)
1182                .map_err(|e| BackendError::Backend(e.to_string()))
1183        }
1184        async fn verify(
1185            &self,
1186            _key_id: &str,
1187            message: &[u8],
1188            signature: &[u8],
1189        ) -> Result<bool, BackendError> {
1190            Ok(self.0.verify(message, signature).is_ok())
1191        }
1192    }
1193
1194    /// Split a compact JWT, returning `(header_json, claims_json, signing_input,
1195    /// signature_bytes)`.
1196    fn parse_token(token: &str) -> (Value, Value, String, Vec<u8>) {
1197        let parts: Vec<&str> = token.split('.').collect();
1198        assert_eq!(parts.len(), 3, "compact JWS has 3 parts");
1199        let header: Value =
1200            serde_json::from_slice(&URL_SAFE_NO_PAD.decode(parts[0]).unwrap()).unwrap();
1201        let claims: Value =
1202            serde_json::from_slice(&URL_SAFE_NO_PAD.decode(parts[1]).unwrap()).unwrap();
1203        let signing_input = format!("{}.{}", parts[0], parts[1]);
1204        let sig = URL_SAFE_NO_PAD.decode(parts[2]).unwrap();
1205        (header, claims, signing_input, sig)
1206    }
1207
1208    /// Distinct `kid`s in a JWKS body (in published order).
1209    fn jwks_kids(body: &[u8]) -> Vec<String> {
1210        let parsed: Value = serde_json::from_slice(body).expect("jwks json");
1211        parsed["keys"]
1212            .as_array()
1213            .expect("keys array")
1214            .iter()
1215            .filter_map(|k| k["kid"].as_str().map(str::to_string))
1216            .collect()
1217    }
1218
1219    /// A distinct 32-byte Ed25519 public key per `seed` (the JWK shape only needs
1220    /// 32 bytes; `EdDsa`'s JWK builder accepts any 32-byte value). Two calls with
1221    /// the same `seed` produce byte-identical keys (for the dedup boundary test).
1222    fn ed_public(seed: u8) -> Vec<u8> {
1223        vec![seed; 32]
1224    }
1225
1226    // ---- grace-window boundary fn (direct unit coverage) --------------------
1227
1228    #[test]
1229    fn grace_window_includes_floor_and_latest_excludes_outside() {
1230        // versions 1..=4 with distinct public keys; window is [2 ..= 3].
1231        let versions = std::collections::BTreeMap::from([
1232            (1u32, ed_public(1)),
1233            (2, ed_public(2)),
1234            (3, ed_public(3)),
1235            (4, ed_public(4)),
1236        ]);
1237        let body = jwt_svid_jwks_grace_window(&versions, 3, 2, SvidAlg::EdDsa).expect("jwks");
1238        let kids = jwks_kids(&body);
1239        // v1 (< floor) and v4 (> latest) are excluded; v2 (== floor) and v3
1240        // (== latest) are included.
1241        assert_eq!(kids.len(), 2, "only the in-window versions: {kids:?}");
1242        assert!(kids.contains(&jwt_svid_jwk_kid(&ed_public(2), SvidAlg::EdDsa)));
1243        assert!(kids.contains(&jwt_svid_jwk_kid(&ed_public(3), SvidAlg::EdDsa)));
1244        assert!(!kids.contains(&jwt_svid_jwk_kid(&ed_public(1), SvidAlg::EdDsa)));
1245        assert!(!kids.contains(&jwt_svid_jwk_kid(&ed_public(4), SvidAlg::EdDsa)));
1246    }
1247
1248    #[test]
1249    fn grace_window_floor_equals_latest_yields_single_key() {
1250        let versions = std::collections::BTreeMap::from([
1251            (1u32, ed_public(1)),
1252            (2, ed_public(2)),
1253            (3, ed_public(3)),
1254        ]);
1255        // floor == latest == 3 -> only v3.
1256        let body = jwt_svid_jwks_grace_window(&versions, 3, 3, SvidAlg::EdDsa).expect("jwks");
1257        let kids = jwks_kids(&body);
1258        assert_eq!(kids, vec![jwt_svid_jwk_kid(&ed_public(3), SvidAlg::EdDsa)]);
1259    }
1260
1261    #[test]
1262    fn grace_window_empty_version_map_yields_empty_set_no_panic() {
1263        let versions: std::collections::BTreeMap<u32, Vec<u8>> = std::collections::BTreeMap::new();
1264        let body = jwt_svid_jwks_grace_window(&versions, 1, 1, SvidAlg::EdDsa).expect("jwks");
1265        assert!(jwks_kids(&body).is_empty(), "no versions -> empty key set");
1266    }
1267
1268    #[test]
1269    fn grace_window_floor_zero_publishes_every_version_up_to_latest() {
1270        // grace_floor == 0: every version is `>= 0`, so all versions up to `latest`
1271        // are published (the documented `[grace_floor ..= latest]` window with a
1272        // floor at the bottom of the range). v4 is still excluded (> latest).
1273        let versions = std::collections::BTreeMap::from([
1274            (1u32, ed_public(1)),
1275            (2, ed_public(2)),
1276            (3, ed_public(3)),
1277            (4, ed_public(4)),
1278        ]);
1279        let body = jwt_svid_jwks_grace_window(&versions, 3, 0, SvidAlg::EdDsa).expect("jwks");
1280        let kids = jwks_kids(&body);
1281        assert_eq!(kids.len(), 3, "floor 0 -> v1..=v3, v4 excluded: {kids:?}");
1282        assert!(!kids.contains(&jwt_svid_jwk_kid(&ed_public(4), SvidAlg::EdDsa)));
1283    }
1284
1285    #[test]
1286    fn grace_window_dedups_identical_public_keys_by_kid() {
1287        // Two in-window versions with IDENTICAL public bytes collide on the
1288        // content-derived kid and de-duplicate to ONE JWK.
1289        let shared = ed_public(7);
1290        let versions =
1291            std::collections::BTreeMap::from([(1u32, shared.clone()), (2, shared.clone())]);
1292        let body = jwt_svid_jwks_grace_window(&versions, 2, 1, SvidAlg::EdDsa).expect("jwks");
1293        let kids = jwks_kids(&body);
1294        assert_eq!(
1295            kids,
1296            vec![jwt_svid_jwk_kid(&shared, SvidAlg::EdDsa)],
1297            "identical public bytes dedup to one kid"
1298        );
1299    }
1300
1301    #[tokio::test]
1302    async fn jwt_svid_jwks_publishes_eddsa_issuer_key() {
1303        let kp = KeyPair::new_account();
1304        let backend = KeyPairBackend(KeyPair::from_seed(&kp.seed().unwrap()).unwrap());
1305
1306        let bytes = jwt_svid_jwks(&backend, "issuer-key", SvidAlg::EdDsa)
1307            .await
1308            .expect("jwks");
1309        let jwks: Value = serde_json::from_slice(&bytes).expect("jwks json");
1310        let key = jwks["keys"].as_array().unwrap().first().unwrap();
1311
1312        let (_, raw_public) = basil_nats::decode_public(&kp.public_key()).expect("public key");
1313        assert_eq!(key["kty"], "OKP");
1314        assert_eq!(key["crv"], "Ed25519");
1315        assert_eq!(key["alg"], "EdDSA");
1316        assert_eq!(key["use"], "sig");
1317        assert_eq!(key["x"], URL_SAFE_NO_PAD.encode(raw_public.as_slice()));
1318        assert_eq!(key["kid"], jwt_svid_jwk_kid(&raw_public, SvidAlg::EdDsa));
1319    }
1320
1321    #[test]
1322    fn jwt_svid_jwks_publishes_rs256_issuer_key() {
1323        use rsa::pkcs8::EncodePublicKey;
1324
1325        let mut rng = rand::thread_rng();
1326        let private = rsa::RsaPrivateKey::new(&mut rng, 2048).expect("rsa keygen");
1327        let public = rsa::RsaPublicKey::from(&private);
1328        let public_der = public.to_public_key_der().expect("public der");
1329
1330        let bytes =
1331            jwt_svid_jwks_from_public_key(public_der.as_bytes(), SvidAlg::Rs256).expect("jwks");
1332        let jwks: Value = serde_json::from_slice(&bytes).expect("jwks json");
1333        let key = jwks["keys"].as_array().unwrap().first().unwrap();
1334
1335        assert_eq!(key["kty"], "RSA");
1336        assert_eq!(key["alg"], "RS256");
1337        assert_eq!(key["use"], "sig");
1338        assert_eq!(key["n"], URL_SAFE_NO_PAD.encode(public.n().to_bytes_be()));
1339        assert_eq!(key["e"], URL_SAFE_NO_PAD.encode(public.e().to_bytes_be()));
1340        assert_eq!(
1341            key["kid"],
1342            jwt_svid_jwk_kid(public_der.as_bytes(), SvidAlg::Rs256)
1343        );
1344    }
1345
1346    #[test]
1347    fn jwt_svid_jwks_publishes_es256_issuer_key() {
1348        use p256::pkcs8::EncodePublicKey as _;
1349
1350        let signing_key = p256::ecdsa::SigningKey::random(&mut rand::thread_rng());
1351        let public = signing_key.verifying_key();
1352        let public_der = public.to_public_key_der().expect("public der");
1353        let encoded = public.to_encoded_point(false);
1354        let x = encoded.x().expect("x coordinate");
1355        let y = encoded.y().expect("y coordinate");
1356
1357        let bytes =
1358            jwt_svid_jwks_from_public_key(public_der.as_bytes(), SvidAlg::Es256).expect("jwks");
1359        let jwks: Value = serde_json::from_slice(&bytes).expect("jwks json");
1360        let key = jwks["keys"].as_array().unwrap().first().unwrap();
1361
1362        assert_eq!(key["kty"], "EC");
1363        assert_eq!(key["crv"], "P-256");
1364        assert_eq!(key["alg"], "ES256");
1365        assert_eq!(key["use"], "sig");
1366        assert_eq!(key["x"], URL_SAFE_NO_PAD.encode(x));
1367        assert_eq!(key["y"], URL_SAFE_NO_PAD.encode(y));
1368        assert_eq!(
1369            key["kid"],
1370            jwt_svid_jwk_kid(public_der.as_bytes(), SvidAlg::Es256)
1371        );
1372    }
1373
1374    #[test]
1375    fn jwt_svid_jwks_publishes_es384_issuer_key() {
1376        use p384::pkcs8::EncodePublicKey as _;
1377
1378        let signing_key = p384::ecdsa::SigningKey::random(&mut rand::thread_rng());
1379        let public = signing_key.verifying_key();
1380        let public_der = public.to_public_key_der().expect("public der");
1381        let encoded = public.to_encoded_point(false);
1382        let x = encoded.x().expect("x coordinate");
1383        let y = encoded.y().expect("y coordinate");
1384
1385        let bytes =
1386            jwt_svid_jwks_from_public_key(public_der.as_bytes(), SvidAlg::Es384).expect("jwks");
1387        let jwks: Value = serde_json::from_slice(&bytes).expect("jwks json");
1388        let key = jwks["keys"].as_array().unwrap().first().unwrap();
1389
1390        assert_eq!(key["kty"], "EC");
1391        assert_eq!(key["crv"], "P-384");
1392        assert_eq!(key["alg"], "ES384");
1393        assert_eq!(key["use"], "sig");
1394        assert_eq!(key["x"], URL_SAFE_NO_PAD.encode(x));
1395        assert_eq!(key["y"], URL_SAFE_NO_PAD.encode(y));
1396        assert_eq!(
1397            key["kid"],
1398            jwt_svid_jwk_kid(public_der.as_bytes(), SvidAlg::Es384)
1399        );
1400    }
1401
1402    #[tokio::test]
1403    async fn generic_mint_round_trips_and_verifies_under_issuer_key() {
1404        let kp = KeyPair::new_account();
1405        let backend = KeyPairBackend(KeyPair::from_seed(&kp.seed().unwrap()).unwrap());
1406
1407        let token = mint_generic(
1408            &backend,
1409            "issuer-key",
1410            "spire.issuer",
1411            "spiffe://example/sa/web",
1412            Some(60),
1413            &serde_json::json!({ "aud": "api", "role": "reader" }),
1414        )
1415        .await
1416        .expect("mint");
1417
1418        let (header, claims, signing_input, sig) = parse_token(&token);
1419        assert_eq!(header["typ"], "JWT");
1420        assert_eq!(header["alg"], "EdDSA");
1421        assert_eq!(claims["iss"], "spire.issuer");
1422        assert_eq!(claims["sub"], "spiffe://example/sa/web");
1423        assert_eq!(claims["aud"], "api");
1424        assert_eq!(claims["role"], "reader");
1425        assert!(claims["exp"].is_number());
1426        assert!(claims["jti"].is_string());
1427        // The signature verifies under the issuer's Ed25519 key.
1428        kp.verify(signing_input.as_bytes(), &sig)
1429            .expect("signature verifies");
1430    }
1431
1432    #[tokio::test]
1433    async fn generic_mint_rs256_derives_alg_and_verifies_under_issuer_key() {
1434        let (backend, public_der, public_pem) = rsa_backend();
1435
1436        let token = mint_generic(
1437            &backend,
1438            "rsa-issuer",
1439            "spire.issuer",
1440            "spiffe://example.org/api",
1441            Some(120),
1442            &serde_json::json!({ "aud": "api" }),
1443        )
1444        .await
1445        .expect("mint generic rs256");
1446
1447        let parts: Vec<&str> = token.split('.').collect();
1448        assert_eq!(parts.len(), 3);
1449        let (header, claims, _signing_input, _sig) = parse_token(&token);
1450        assert_eq!(header["typ"], "JWT");
1451        assert_eq!(header["alg"], "RS256");
1452        assert_eq!(header["kid"], jwt_svid_jwk_kid(&public_der, SvidAlg::Rs256));
1453        assert_eq!(claims["iss"], "spire.issuer");
1454        assert_eq!(claims["sub"], "spiffe://example.org/api");
1455        assert_eq!(claims["aud"], "api");
1456
1457        let signing_input = format!("{}.{}", parts[0], parts[1]);
1458        let decoding_key =
1459            jsonwebtoken::DecodingKey::from_rsa_pem(public_pem.as_bytes()).expect("decoding key");
1460        let valid = jsonwebtoken::crypto::verify(
1461            parts[2],
1462            signing_input.as_bytes(),
1463            &decoding_key,
1464            jsonwebtoken::Algorithm::RS256,
1465        )
1466        .expect("verify");
1467        assert!(valid, "generic RS256 signature verifies under issuer key");
1468    }
1469
1470    #[tokio::test]
1471    async fn generic_mint_es256_derives_alg_and_verifies_under_issuer_key() {
1472        let backend = p256_backend();
1473
1474        let token = mint_generic(
1475            &backend,
1476            "ecdsa-issuer",
1477            "spire.issuer",
1478            "spiffe://example.org/api",
1479            Some(120),
1480            &serde_json::json!({ "aud": "api" }),
1481        )
1482        .await
1483        .expect("mint generic es256");
1484
1485        let parts: Vec<&str> = token.split('.').collect();
1486        assert_eq!(parts.len(), 3);
1487        let (header, claims, _signing_input, sig) = parse_token(&token);
1488        assert_eq!(header["typ"], "JWT");
1489        assert_eq!(header["alg"], "ES256");
1490        assert_eq!(
1491            header["kid"],
1492            jwt_svid_jwk_kid(&backend.public_der, SvidAlg::Es256),
1493            "generic JWS kid matches the published JWKS key id"
1494        );
1495        assert_eq!(claims["iss"], "spire.issuer");
1496        assert_eq!(claims["sub"], "spiffe://example.org/api");
1497        assert_eq!(claims["aud"], "api");
1498        assert_eq!(sig.len(), 64, "ES256 signatures are raw fixed r||s");
1499
1500        let signing_input = format!("{}.{}", parts[0], parts[1]);
1501        let decoding_key = jsonwebtoken::DecodingKey::from_ec_pem(backend.public_pem.as_bytes())
1502            .expect("decoding key");
1503        let valid = jsonwebtoken::crypto::verify(
1504            parts[2],
1505            signing_input.as_bytes(),
1506            &decoding_key,
1507            jsonwebtoken::Algorithm::ES256,
1508        )
1509        .expect("verify");
1510        assert!(valid, "generic ES256 signature verifies under issuer key");
1511    }
1512
1513    #[tokio::test]
1514    async fn generic_mint_es384_derives_alg_and_verifies_under_issuer_key() {
1515        let backend = p384_backend();
1516
1517        let token = mint_generic(
1518            &backend,
1519            "ecdsa384-issuer",
1520            "spire.issuer",
1521            "spiffe://example.org/api",
1522            Some(120),
1523            &serde_json::Value::Null,
1524        )
1525        .await
1526        .expect("mint generic es384");
1527
1528        let parts: Vec<&str> = token.split('.').collect();
1529        assert_eq!(parts.len(), 3);
1530        let (header, _claims, _signing_input, sig) = parse_token(&token);
1531        assert_eq!(header["alg"], "ES384");
1532        assert_eq!(
1533            header["kid"],
1534            jwt_svid_jwk_kid(&backend.public_der, SvidAlg::Es384)
1535        );
1536        assert_eq!(sig.len(), 96, "ES384 signatures are raw fixed r||s");
1537
1538        let signing_input = format!("{}.{}", parts[0], parts[1]);
1539        let decoding_key = jsonwebtoken::DecodingKey::from_ec_pem(backend.public_pem.as_bytes())
1540            .expect("decoding key");
1541        let valid = jsonwebtoken::crypto::verify(
1542            parts[2],
1543            signing_input.as_bytes(),
1544            &decoding_key,
1545            jsonwebtoken::Algorithm::ES384,
1546        )
1547        .expect("verify");
1548        assert!(valid, "generic ES384 signature verifies under issuer key");
1549    }
1550
1551    #[tokio::test]
1552    async fn generic_mint_rejects_reserved_claim() {
1553        let backend = KeyPairBackend(KeyPair::new_account());
1554        for reserved in ["iss", "iat", "exp", "jti", "sub", "nbf"] {
1555            let err = mint_generic(
1556                &backend,
1557                "p",
1558                "issuer",
1559                "sub",
1560                None,
1561                &serde_json::json!({ reserved: "x" }),
1562            )
1563            .await
1564            .expect_err("reserved claim rejected");
1565            assert!(
1566                // ubs false positive: test
1567                /* ubs:ignore */
1568                matches!(err, GenericMintError::Reserved(ReservedClaim(k)) if k == reserved),
1569                "expected reserved {reserved}"
1570            );
1571        }
1572    }
1573
1574    #[tokio::test]
1575    async fn generic_mint_without_ttl_omits_exp() {
1576        let backend = KeyPairBackend(KeyPair::new_account());
1577        let token = mint_generic(
1578            &backend,
1579            "p",
1580            "issuer",
1581            "sub",
1582            None,
1583            &serde_json::Value::Null,
1584        )
1585        .await
1586        .expect("mint");
1587        let (_, claims, _, _) = parse_token(&token);
1588        assert!(claims.get("exp").is_none(), "no ttl => no exp claim");
1589    }
1590
1591    #[tokio::test]
1592    async fn account_mint_iss_is_operator_nkey_and_verifies() {
1593        // Issuer is an operator key; the Operator role derives an O… iss.
1594        let operator = KeyPair::new_operator();
1595        let backend = KeyPairBackend(KeyPair::from_seed(&operator.seed().unwrap()).unwrap());
1596        let account = KeyPair::new_account();
1597
1598        let token = mint_nats_account(
1599            &backend,
1600            "operator-key",
1601            NkeyType::Operator,
1602            &account.public_key(),
1603            "acme",
1604            Some(3600),
1605            vec![KeyPair::new_account().public_key()],
1606        )
1607        .await
1608        .expect("mint");
1609
1610        let (_, claims, signing_input, sig) = parse_token(&token);
1611        assert_eq!(claims["nats"]["type"], "account");
1612        assert_eq!(claims["sub"], account.public_key());
1613        assert!(claims["iss"].as_str().unwrap().starts_with('O'));
1614        operator
1615            .verify(signing_input.as_bytes(), &sig)
1616            .expect("verifies under operator key");
1617    }
1618
1619    #[tokio::test]
1620    async fn user_mint_sets_issuer_account_for_signing_key() {
1621        // The backend key stands in for an account *signing* key: its public half
1622        // becomes `iss`, while the caller-supplied account identity becomes
1623        // `nats.issuer_account` so nats-server can bind the user to its account.
1624        let signing = KeyPair::new_account();
1625        let backend = KeyPairBackend(KeyPair::from_seed(&signing.seed().unwrap()).unwrap());
1626        let account_identity = KeyPair::new_account();
1627        let user = KeyPair::new_user();
1628
1629        let token = mint_nats_user(
1630            &backend,
1631            "account-signing-key",
1632            NkeyType::Account,
1633            &user.public_key(),
1634            Some(&account_identity.public_key()),
1635            "svc-user",
1636            Some(3600),
1637            basil_nats::UserPermissions::default(),
1638        )
1639        .await
1640        .expect("mint user");
1641
1642        let (_, claims, signing_input, sig) = parse_token(&token);
1643        assert_eq!(claims["nats"]["type"], "user");
1644        assert_eq!(
1645            claims["iss"],
1646            signing.public_key(),
1647            "iss is the signing key"
1648        );
1649        assert_eq!(
1650            claims["nats"]["issuer_account"],
1651            account_identity.public_key(),
1652            "issuer_account names the owning account identity"
1653        );
1654        signing
1655            .verify(signing_input.as_bytes(), &sig)
1656            .expect("verifies under the signing key");
1657    }
1658
1659    #[tokio::test]
1660    async fn user_mint_without_issuer_account_omits_the_claim() {
1661        let account = KeyPair::new_account();
1662        let backend = KeyPairBackend(KeyPair::from_seed(&account.seed().unwrap()).unwrap());
1663        let user = KeyPair::new_user();
1664
1665        let token = mint_nats_user(
1666            &backend,
1667            "account-key",
1668            NkeyType::Account,
1669            &user.public_key(),
1670            None,
1671            "svc-user",
1672            None,
1673            basil_nats::UserPermissions::default(),
1674        )
1675        .await
1676        .expect("mint user");
1677
1678        let (_, claims, _, _) = parse_token(&token);
1679        assert_eq!(claims["iss"], account.public_key());
1680        assert!(
1681            claims["nats"].get("issuer_account").is_none(),
1682            "no issuer_account claim when the account identity key signs"
1683        );
1684    }
1685
1686    #[tokio::test]
1687    async fn user_mint_rejects_malformed_issuer_account() {
1688        let backend = KeyPairBackend(KeyPair::new_account());
1689        let user = KeyPair::new_user();
1690        let err = mint_nats_user(
1691            &backend,
1692            "account-key",
1693            NkeyType::Account,
1694            &user.public_key(),
1695            Some("not-an-account-nkey"),
1696            "svc-user",
1697            None,
1698            basil_nats::UserPermissions::default(),
1699        )
1700        .await
1701        .expect_err("malformed issuer account rejected");
1702        assert!(matches!(err, BackendError::Protocol(_)));
1703    }
1704
1705    #[tokio::test]
1706    async fn sign_nats_jwt_derives_issuer_jti_and_verifies() {
1707        let account = KeyPair::new_account();
1708        let backend = KeyPairBackend(KeyPair::from_seed(&account.seed().unwrap()).unwrap());
1709        let user = KeyPair::new_user();
1710        let token = sign_nats_jwt(
1711            &backend,
1712            SignNatsJwtSpec {
1713                signing_key_id: "account-key",
1714                issuer_role: NkeyType::Account,
1715                claims: &serde_json::json!({
1716                    "sub": user.public_key(),
1717                    "name": "rich-user",
1718                    "nats": { "type": "user", "version": 2 }
1719                }),
1720                expected_kind: Some(NatsJwtKind::User),
1721                issued_at: Some(1_700_000_000),
1722                expires_at: Some(1_700_003_600),
1723                jti_mode: NatsJtiMode::RequireValid,
1724            },
1725        )
1726        .await
1727        .expect("sign nats jwt");
1728
1729        let (header, claims, signing_input, sig) = parse_token(&token);
1730        assert_eq!(header["alg"], "ed25519-nkey");
1731        assert_eq!(claims["iss"], account.public_key());
1732        assert_eq!(claims["sub"], user.public_key());
1733        assert_eq!(claims["iat"], 1_700_000_000);
1734        assert_eq!(claims["exp"], 1_700_003_600);
1735        assert_eq!(claims["nats"]["type"], "user");
1736        assert!(claims["jti"].as_str().is_some_and(|jti| !jti.is_empty()));
1737        account
1738            .verify(signing_input.as_bytes(), &sig)
1739            .expect("verifies under account key");
1740    }
1741
1742    #[tokio::test]
1743    async fn sign_nats_jwt_allows_missing_name() {
1744        let account = KeyPair::new_account();
1745        let backend = KeyPairBackend(KeyPair::from_seed(&account.seed().unwrap()).unwrap());
1746        let user = KeyPair::new_user();
1747        let token = sign_nats_jwt(
1748            &backend,
1749            SignNatsJwtSpec {
1750                signing_key_id: "account-key",
1751                issuer_role: NkeyType::Account,
1752                claims: &serde_json::json!({
1753                    "sub": user.public_key(),
1754                    "nats": { "type": "user", "version": 2 }
1755                }),
1756                expected_kind: Some(NatsJwtKind::User),
1757                issued_at: Some(1_700_000_000),
1758                expires_at: None,
1759                jti_mode: NatsJtiMode::RequireValid,
1760            },
1761        )
1762        .await
1763        .expect("sign nats jwt without name");
1764
1765        let (_, claims, _, _) = parse_token(&token);
1766        assert!(claims.get("name").is_none());
1767        assert!(claims["jti"].as_str().is_some_and(|jti| !jti.is_empty()));
1768    }
1769
1770    #[tokio::test]
1771    async fn sign_nats_jwt_accepts_jti_with_aud_and_nbf() {
1772        let account = KeyPair::new_account();
1773        let backend = KeyPairBackend(KeyPair::from_seed(&account.seed().unwrap()).unwrap());
1774        let user = KeyPair::new_user();
1775        let issuer = account.public_key();
1776        let subject = user.public_key();
1777        let jti = basil_nats::jti_for_standard_claims(
1778            &issuer,
1779            &subject,
1780            Some("rich-user"),
1781            1_700_000_000,
1782            Some(1_700_003_600),
1783            Some("orders"),
1784            Some(1_699_999_900),
1785        )
1786        .expect("computed jti");
1787        let token = sign_nats_jwt(
1788            &backend,
1789            SignNatsJwtSpec {
1790                signing_key_id: "account-key",
1791                issuer_role: NkeyType::Account,
1792                claims: &serde_json::json!({
1793                    "aud": "orders",
1794                    "exp": 1_700_003_600_u64,
1795                    "iat": 1_700_000_000_u64,
1796                    "iss": issuer.clone(),
1797                    "jti": jti.clone(),
1798                    "name": "rich-user",
1799                    "nbf": 1_699_999_900_u64,
1800                    "sub": subject.clone(),
1801                    "nats": { "type": "user", "version": 2 }
1802                }),
1803                expected_kind: Some(NatsJwtKind::User),
1804                issued_at: None,
1805                expires_at: None,
1806                jti_mode: NatsJtiMode::RequireValid,
1807            },
1808        )
1809        .await
1810        .expect("sign nats jwt with standard claims");
1811
1812        let (_, claims, _, _) = parse_token(&token);
1813        assert_eq!(claims["jti"], jti);
1814        assert_eq!(claims["aud"], "orders");
1815        assert_eq!(claims["nbf"], 1_699_999_900_u64);
1816    }
1817
1818    #[tokio::test]
1819    async fn sign_nats_jwt_rejects_mismatched_issuer() {
1820        let backend = KeyPairBackend(KeyPair::new_account());
1821        let user = KeyPair::new_user();
1822        let err = sign_nats_jwt(
1823            &backend,
1824            SignNatsJwtSpec {
1825                signing_key_id: "account-key",
1826                issuer_role: NkeyType::Account,
1827                claims: &serde_json::json!({
1828                    "iss": KeyPair::new_account().public_key(),
1829                    "sub": user.public_key(),
1830                    "name": "bad-iss",
1831                    "nats": { "type": "user", "version": 2 }
1832                }),
1833                expected_kind: Some(NatsJwtKind::User),
1834                issued_at: None,
1835                expires_at: None,
1836                jti_mode: NatsJtiMode::RequireValid,
1837            },
1838        )
1839        .await
1840        .expect_err("mismatched issuer rejects");
1841        assert!(matches!(
1842            err,
1843            BackendError::Protocol(message)
1844                if message.starts_with("invalid nats jwt iss")
1845        ));
1846    }
1847
1848    #[tokio::test]
1849    async fn account_mint_rejects_bad_subject_nkey() {
1850        let backend = KeyPairBackend(KeyPair::new_operator());
1851        let err = mint_nats_account(
1852            &backend,
1853            "p",
1854            NkeyType::Operator,
1855            "not-an-nkey",
1856            "n",
1857            None,
1858            vec![],
1859        )
1860        .await
1861        .expect_err("bad subject");
1862        assert!(matches!(err, BackendError::Protocol(_)));
1863    }
1864
1865    #[tokio::test]
1866    async fn account_mint_rejects_disallowed_issuer_role() {
1867        // The role is type-safe now, but a valid role that is not allowed to
1868        // issue accounts (only Account/Operator may) is a clean Protocol error.
1869        let backend = KeyPairBackend(KeyPair::new_user());
1870        let subject = KeyPair::new_account().public_key();
1871        let err = mint_nats_account(&backend, "p", NkeyType::User, &subject, "n", None, vec![])
1872            .await
1873            .expect_err("disallowed issuer role");
1874        assert!(matches!(err, BackendError::Protocol(_)));
1875    }
1876
1877    #[tokio::test]
1878    async fn operator_mint_self_signs_and_verifies() {
1879        let operator = KeyPair::new_operator();
1880        let backend = KeyPairBackend(KeyPair::from_seed(&operator.seed().unwrap()).unwrap());
1881
1882        let token = mint_nats_operator(
1883            &backend,
1884            "operator-key",
1885            NkeyType::Operator,
1886            None, // self-signed: sub == iss
1887            "root-op",
1888            None,
1889            vec![],
1890            "nats://localhost:4222".into(),
1891            KeyPair::new_account().public_key(),
1892        )
1893        .await
1894        .expect("mint");
1895
1896        let (_, claims, signing_input, sig) = parse_token(&token);
1897        assert_eq!(claims["nats"]["type"], "operator");
1898        let iss = claims["iss"].as_str().unwrap();
1899        assert!(iss.starts_with('O'));
1900        assert_eq!(claims["sub"], iss, "self-signed: sub == iss");
1901        assert_eq!(
1902            claims["nats"]["account_server_url"],
1903            "nats://localhost:4222"
1904        );
1905        assert!(claims.get("exp").is_none(), "no ttl => no exp");
1906        operator
1907            .verify(signing_input.as_bytes(), &sig)
1908            .expect("verifies under operator key");
1909    }
1910
1911    #[tokio::test]
1912    async fn signer_mint_requires_matching_account_or_operator_subject() {
1913        let account = KeyPair::new_account();
1914        let backend = KeyPairBackend(KeyPair::from_seed(&account.seed().unwrap()).unwrap());
1915        let signing = KeyPair::new_account();
1916
1917        let token = mint_nats_signer(
1918            &backend,
1919            "account-key",
1920            NkeyType::Account,
1921            &signing.public_key(),
1922            "account-signer",
1923            Some(60),
1924        )
1925        .await
1926        .expect("mint");
1927
1928        let (_, claims, signing_input, sig) = parse_token(&token);
1929        assert_eq!(claims["nats"]["type"], "signer");
1930        assert_eq!(claims["sub"], signing.public_key());
1931        assert!(claims["iss"].as_str().unwrap().starts_with('A'));
1932        account
1933            .verify(signing_input.as_bytes(), &sig)
1934            .expect("verifies under account key");
1935
1936        let err = mint_nats_signer(
1937            &backend,
1938            "account-key",
1939            NkeyType::Account,
1940            &KeyPair::new_operator().public_key(),
1941            "wrong-signer",
1942            None,
1943        )
1944        .await
1945        .expect_err("wrong subject role");
1946        assert!(matches!(err, BackendError::Protocol(_)));
1947    }
1948
1949    #[tokio::test]
1950    async fn server_and_curve_mints_verify_expected_prefixes() {
1951        let server = KeyPair::new_server();
1952        let server_backend = KeyPairBackend(KeyPair::from_seed(&server.seed().unwrap()).unwrap());
1953
1954        let server_token = mint_nats_server(
1955            &server_backend,
1956            "server-key",
1957            NkeyType::Server,
1958            &server.public_key(),
1959            "nats-server",
1960            None,
1961        )
1962        .await
1963        .expect("server mint");
1964        let (_, claims, signing_input, sig) = parse_token(&server_token);
1965        assert_eq!(claims["nats"]["type"], "server");
1966        assert!(claims["iss"].as_str().unwrap().starts_with('N'));
1967        assert!(claims["sub"].as_str().unwrap().starts_with('N'));
1968        server
1969            .verify(signing_input.as_bytes(), &sig)
1970            .expect("verifies under server key");
1971
1972        let curve_source = KeyPair::new_account();
1973        let curve_backend =
1974            KeyPairBackend(KeyPair::from_seed(&curve_source.seed().unwrap()).unwrap());
1975        let (_, raw_curve) = basil_nats::decode_public(&curve_source.public_key())
1976            .expect("curve source public decodes");
1977        let curve_public =
1978            basil_nats::encode_public(NkeyType::Curve, &raw_curve).expect("curve nkey encodes");
1979        let curve_token = mint_nats_curve(
1980            &curve_backend,
1981            "curve-key",
1982            NkeyType::Curve,
1983            &curve_public,
1984            "curve-key",
1985            None,
1986        )
1987        .await
1988        .expect("curve mint");
1989        let (_, claims, signing_input, sig) = parse_token(&curve_token);
1990        assert_eq!(claims["nats"]["type"], "curve");
1991        assert!(claims["iss"].as_str().unwrap().starts_with('X'));
1992        assert!(claims["sub"].as_str().unwrap().starts_with('X'));
1993        curve_source
1994            .verify(signing_input.as_bytes(), &sig)
1995            .expect("verifies under encoded curve issuer bytes");
1996    }
1997
1998    #[tokio::test]
1999    async fn unsupported_issuer_role_is_protocol_error() {
2000        let backend = KeyPairBackend(KeyPair::new_operator());
2001        let server = KeyPair::new_server();
2002        let err = mint_nats_server(
2003            &backend,
2004            "operator-key",
2005            NkeyType::Operator,
2006            &server.public_key(),
2007            "n",
2008            None,
2009        )
2010        .await
2011        .expect_err("operator cannot mint server role");
2012        assert!(matches!(
2013            err,
2014            BackendError::Protocol(message) if message.starts_with("unsupported issuer role")
2015        ));
2016    }
2017
2018    // ---- svid preset (vault-d0z) --------------------------------------------
2019
2020    #[tokio::test]
2021    async fn svid_eddsa_mints_a_verifiable_jwt_svid() {
2022        let kp = KeyPair::new_account();
2023        let backend = KeyPairBackend(KeyPair::from_seed(&kp.seed().unwrap()).unwrap());
2024
2025        let token = mint_svid(
2026            &backend,
2027            "issuer-key",
2028            "spire.svid_issuer",
2029            SvidAlg::EdDsa,
2030            "spiffe://example.org/web-01",
2031            "vault",
2032            Some(300),
2033            &serde_json::json!({ "role": "ingest" }),
2034        )
2035        .await
2036        .expect("mint svid");
2037
2038        let (header, claims, signing_input, sig) = parse_token(&token);
2039        assert_eq!(header["typ"], "JWT");
2040        assert_eq!(header["alg"], "EdDSA");
2041        assert_eq!(claims["iss"], "spire.svid_issuer");
2042        assert_eq!(claims["sub"], "spiffe://example.org/web-01");
2043        assert_eq!(claims["aud"], "vault", "aud is required + preset-owned");
2044        assert!(claims["iat"].is_number());
2045        assert!(claims["exp"].is_number());
2046        assert!(claims["jti"].is_string());
2047        assert_eq!(claims["role"], "ingest", "non-reserved extra merged");
2048        // The backend's Ed25519 signature verifies under the issuer key.
2049        kp.verify(signing_input.as_bytes(), &sig)
2050            .expect("svid signature verifies under issuer key");
2051    }
2052
2053    #[tokio::test]
2054    async fn svid_without_ttl_omits_exp() {
2055        let backend = KeyPairBackend(KeyPair::new_account());
2056        let token = mint_svid(
2057            &backend,
2058            "k",
2059            "iss",
2060            SvidAlg::EdDsa,
2061            "spiffe://example.org/svc",
2062            "aud",
2063            None,
2064            &serde_json::Value::Null,
2065        )
2066        .await
2067        .expect("mint");
2068        let (_, claims, _, _) = parse_token(&token);
2069        assert!(claims.get("exp").is_none(), "no ttl => no exp");
2070    }
2071
2072    #[tokio::test]
2073    async fn svid_rejects_reserved_claims_including_aud() {
2074        let backend = KeyPairBackend(KeyPair::new_account());
2075        // `aud` is preset-owned for svid (the handler lifts the real audience out
2076        // before merging); a residual reserved key in the extras is rejected.
2077        for reserved in ["iss", "iat", "exp", "jti", "sub", "nbf", "aud"] {
2078            let err = mint_svid(
2079                &backend,
2080                "k",
2081                "iss",
2082                SvidAlg::EdDsa,
2083                "spiffe://example.org/svc",
2084                "aud",
2085                None,
2086                &serde_json::json!({ reserved: "x" }),
2087            )
2088            .await
2089            .expect_err("reserved claim rejected");
2090            assert!(
2091                // ubs false positive: test
2092                /* ubs:ignore */
2093                matches!(err, GenericMintError::Reserved(ReservedClaim(k)) if k == reserved),
2094                "expected reserved {reserved}"
2095            );
2096        }
2097    }
2098
2099    /// A backend whose `sign` is JWS `RS256` (RSASSA-PKCS1-v1_5 / SHA-256). It
2100    /// signs the signing input with `jsonwebtoken` (the same RS256 path the rest
2101    /// of the crate uses) and returns the raw signature bytes, so the minter's
2102    /// `base64url` reassembly reproduces a token verifiable under the public key.
2103    struct RsaBackend {
2104        encoding_key: jsonwebtoken::EncodingKey,
2105        /// SPKI DER of the RSA public half, returned by `public_key` so the
2106        /// minter can derive the deterministic JWS `kid` header.
2107        public_der: Vec<u8>,
2108    }
2109
2110    #[async_trait]
2111    impl Backend for RsaBackend {
2112        fn kind(&self) -> &'static str {
2113            "rsa-test"
2114        }
2115        async fn new_key(&self, _key_type: KeyType) -> Result<NewKey, BackendError> {
2116            Err(BackendError::Unsupported("new_key"))
2117        }
2118        async fn public_key(&self, _key_id: &str) -> Result<Vec<u8>, BackendError> {
2119            Ok(self.public_der.clone())
2120        }
2121        async fn sign(&self, _key_id: &str, input: &[u8]) -> Result<Vec<u8>, BackendError> {
2122            self.rs256_sign(input)
2123        }
2124        async fn sign_with_options(
2125            &self,
2126            _key_id: &str,
2127            input: &[u8],
2128            options: SignOptions,
2129        ) -> Result<Vec<u8>, BackendError> {
2130            if options != SignOptions::Rs256Pkcs1v15Sha256 {
2131                return Err(BackendError::Unsupported("rsa test sign options"));
2132            }
2133            self.rs256_sign(input)
2134        }
2135        async fn verify(
2136            &self,
2137            _key_id: &str,
2138            _message: &[u8],
2139            _signature: &[u8],
2140        ) -> Result<bool, BackendError> {
2141            Err(BackendError::Unsupported("verify"))
2142        }
2143    }
2144
2145    impl RsaBackend {
2146        fn rs256_sign(&self, input: &[u8]) -> Result<Vec<u8>, BackendError> {
2147            // jsonwebtoken returns a base64url signature string; decode it to raw
2148            // bytes so the minter re-encodes the identical 3rd JWS segment.
2149            let b64 = jsonwebtoken::crypto::sign(
2150                input,
2151                &self.encoding_key,
2152                jsonwebtoken::Algorithm::RS256,
2153            )
2154            .map_err(|e| BackendError::Backend(e.to_string()))?;
2155            URL_SAFE_NO_PAD
2156                .decode(b64)
2157                .map_err(|e| BackendError::Backend(e.to_string()))
2158        }
2159    }
2160
2161    fn rsa_backend() -> (RsaBackend, Vec<u8>, String) {
2162        use rsa::pkcs1::EncodeRsaPrivateKey;
2163        use rsa::pkcs8::{EncodePublicKey, LineEnding};
2164
2165        // RSA-2048 (jsonwebtoken's ring backend rejects keys below 2048 bits).
2166        let mut rng = rand::thread_rng();
2167        let private = rsa::RsaPrivateKey::new(&mut rng, 2048).expect("rsa keygen");
2168        let public = rsa::RsaPublicKey::from(&private);
2169        let private_pem = private.to_pkcs1_pem(LineEnding::LF).expect("pkcs1 pem");
2170        let public_pem = public.to_public_key_pem(LineEnding::LF).expect("spki pem");
2171        let public_der = public
2172            .to_public_key_der()
2173            .expect("spki der")
2174            .as_bytes()
2175            .to_vec();
2176        let backend = RsaBackend {
2177            encoding_key: jsonwebtoken::EncodingKey::from_rsa_pem(private_pem.as_bytes())
2178                .expect("encoding key"),
2179            public_der: public_der.clone(),
2180        };
2181        (backend, public_der, public_pem)
2182    }
2183
2184    /// A backend whose `sign_with_options` is JWS `ES256`. It mirrors the
2185    /// transit boundary: the backend returns raw fixed-width `r || s` signature
2186    /// bytes over the compact JWS signing input.
2187    struct P256Backend {
2188        encoding_key: jsonwebtoken::EncodingKey,
2189        public_der: Vec<u8>,
2190        public_pem: String,
2191    }
2192
2193    #[async_trait]
2194    impl Backend for P256Backend {
2195        fn kind(&self) -> &'static str {
2196            "p256-test"
2197        }
2198        async fn new_key(&self, _key_type: KeyType) -> Result<NewKey, BackendError> {
2199            Err(BackendError::Unsupported("new_key"))
2200        }
2201        async fn public_key(&self, _key_id: &str) -> Result<Vec<u8>, BackendError> {
2202            Ok(self.public_der.clone())
2203        }
2204        async fn sign(&self, key_id: &str, input: &[u8]) -> Result<Vec<u8>, BackendError> {
2205            self.sign_with_options(key_id, input, SignOptions::Es256)
2206                .await
2207        }
2208        async fn sign_with_options(
2209            &self,
2210            _key_id: &str,
2211            input: &[u8],
2212            options: SignOptions,
2213        ) -> Result<Vec<u8>, BackendError> {
2214            if options != SignOptions::Es256 {
2215                return Err(BackendError::Unsupported("p256 test sign options"));
2216            }
2217            let b64 = jsonwebtoken::crypto::sign(
2218                input,
2219                &self.encoding_key,
2220                jsonwebtoken::Algorithm::ES256,
2221            )
2222            .map_err(|e| BackendError::Backend(e.to_string()))?;
2223            URL_SAFE_NO_PAD
2224                .decode(b64)
2225                .map_err(|e| BackendError::Backend(e.to_string()))
2226        }
2227        async fn verify(
2228            &self,
2229            _key_id: &str,
2230            _message: &[u8],
2231            _signature: &[u8],
2232        ) -> Result<bool, BackendError> {
2233            Err(BackendError::Unsupported("verify"))
2234        }
2235    }
2236
2237    fn p256_backend() -> P256Backend {
2238        use p256::pkcs8::{EncodePrivateKey as _, EncodePublicKey as _, LineEnding};
2239
2240        let signing_key = p256::ecdsa::SigningKey::random(&mut rand::thread_rng());
2241        let private_der = signing_key.to_pkcs8_der().expect("pkcs8 der");
2242        let public = signing_key.verifying_key();
2243        let public_der = public
2244            .to_public_key_der()
2245            .expect("spki der")
2246            .as_bytes()
2247            .to_vec();
2248        let public_pem = public.to_public_key_pem(LineEnding::LF).expect("spki pem");
2249        P256Backend {
2250            encoding_key: jsonwebtoken::EncodingKey::from_ec_der(private_der.as_bytes()),
2251            public_der,
2252            public_pem,
2253        }
2254    }
2255
2256    /// A backend whose `sign_with_options` is JWS `ES384`.
2257    struct P384Backend {
2258        encoding_key: jsonwebtoken::EncodingKey,
2259        public_der: Vec<u8>,
2260        public_pem: String,
2261    }
2262
2263    #[async_trait]
2264    impl Backend for P384Backend {
2265        fn kind(&self) -> &'static str {
2266            "p384-test"
2267        }
2268        async fn new_key(&self, _key_type: KeyType) -> Result<NewKey, BackendError> {
2269            Err(BackendError::Unsupported("new_key"))
2270        }
2271        async fn public_key(&self, _key_id: &str) -> Result<Vec<u8>, BackendError> {
2272            Ok(self.public_der.clone())
2273        }
2274        async fn sign(&self, key_id: &str, input: &[u8]) -> Result<Vec<u8>, BackendError> {
2275            self.sign_with_options(key_id, input, SignOptions::Es384)
2276                .await
2277        }
2278        async fn sign_with_options(
2279            &self,
2280            _key_id: &str,
2281            input: &[u8],
2282            options: SignOptions,
2283        ) -> Result<Vec<u8>, BackendError> {
2284            if options != SignOptions::Es384 {
2285                return Err(BackendError::Unsupported("p384 test sign options"));
2286            }
2287            let b64 = jsonwebtoken::crypto::sign(
2288                input,
2289                &self.encoding_key,
2290                jsonwebtoken::Algorithm::ES384,
2291            )
2292            .map_err(|e| BackendError::Backend(e.to_string()))?;
2293            URL_SAFE_NO_PAD
2294                .decode(b64)
2295                .map_err(|e| BackendError::Backend(e.to_string()))
2296        }
2297        async fn verify(
2298            &self,
2299            _key_id: &str,
2300            _message: &[u8],
2301            _signature: &[u8],
2302        ) -> Result<bool, BackendError> {
2303            Err(BackendError::Unsupported("verify"))
2304        }
2305    }
2306
2307    fn p384_backend() -> P384Backend {
2308        use p384::pkcs8::{EncodePrivateKey as _, EncodePublicKey as _, LineEnding};
2309
2310        let signing_key = p384::ecdsa::SigningKey::random(&mut rand::thread_rng());
2311        let private_der = signing_key.to_pkcs8_der().expect("pkcs8 der");
2312        let public = signing_key.verifying_key();
2313        let public_der = public
2314            .to_public_key_der()
2315            .expect("spki der")
2316            .as_bytes()
2317            .to_vec();
2318        let public_pem = public.to_public_key_pem(LineEnding::LF).expect("spki pem");
2319        P384Backend {
2320            encoding_key: jsonwebtoken::EncodingKey::from_ec_der(private_der.as_bytes()),
2321            public_der,
2322            public_pem,
2323        }
2324    }
2325
2326    #[tokio::test]
2327    async fn svid_rs256_mints_a_verifiable_jwt_svid() {
2328        let (backend, public_der, public_pem) = rsa_backend();
2329
2330        let token = mint_svid(
2331            &backend,
2332            "rsa-issuer",
2333            "spiffe://example.org",
2334            SvidAlg::Rs256,
2335            "spiffe://example.org/db-01",
2336            "vault",
2337            Some(300),
2338            &serde_json::Value::Null,
2339        )
2340        .await
2341        .expect("mint rs256 svid");
2342
2343        let parts: Vec<&str> = token.split('.').collect();
2344        assert_eq!(parts.len(), 3);
2345        let (header, claims, _signing_input, _sig) = parse_token(&token);
2346        assert_eq!(header["alg"], "RS256");
2347        assert_eq!(claims["sub"], "spiffe://example.org/db-01");
2348        // The `kid` header is required by the SPIFFE JWT-SVID profile and MUST
2349        // match the JWKS key id published for the bundle (so a verifier can
2350        // select the key). Same deterministic id from the same public half.
2351        assert_eq!(
2352            header["kid"],
2353            jwt_svid_jwk_kid(&public_der, SvidAlg::Rs256),
2354            "JWS kid matches the published JWKS key id"
2355        );
2356        // The RS256 signature (3rd segment) verifies under the issuer's RSA key.
2357        let signing_input = format!("{}.{}", parts[0], parts[1]);
2358        let decoding_key =
2359            jsonwebtoken::DecodingKey::from_rsa_pem(public_pem.as_bytes()).expect("decoding key");
2360        let valid = jsonwebtoken::crypto::verify(
2361            parts[2],
2362            signing_input.as_bytes(),
2363            &decoding_key,
2364            jsonwebtoken::Algorithm::RS256,
2365        )
2366        .expect("verify");
2367        assert!(valid, "RS256 signature verifies under issuer key");
2368    }
2369
2370    #[tokio::test]
2371    async fn svid_es256_mints_a_verifiable_jwt_svid() {
2372        let backend = p256_backend();
2373
2374        let token = mint_svid(
2375            &backend,
2376            "ecdsa-issuer",
2377            "spiffe://example.org",
2378            SvidAlg::Es256,
2379            "spiffe://example.org/db-01",
2380            "vault",
2381            Some(300),
2382            &serde_json::Value::Null,
2383        )
2384        .await
2385        .expect("mint es256 svid");
2386
2387        let parts: Vec<&str> = token.split('.').collect();
2388        assert_eq!(parts.len(), 3);
2389        let (header, claims, _signing_input, sig) = parse_token(&token);
2390        assert_eq!(header["alg"], "ES256");
2391        assert_eq!(claims["sub"], "spiffe://example.org/db-01");
2392        assert_eq!(
2393            header["kid"],
2394            jwt_svid_jwk_kid(&backend.public_der, SvidAlg::Es256),
2395            "JWS kid matches the published JWKS key id"
2396        );
2397        assert_eq!(sig.len(), 64, "ES256 signatures are raw fixed r||s");
2398
2399        let signing_input = format!("{}.{}", parts[0], parts[1]);
2400        let decoding_key = jsonwebtoken::DecodingKey::from_ec_pem(backend.public_pem.as_bytes())
2401            .expect("decoding key");
2402        let valid = jsonwebtoken::crypto::verify(
2403            parts[2],
2404            signing_input.as_bytes(),
2405            &decoding_key,
2406            jsonwebtoken::Algorithm::ES256,
2407        )
2408        .expect("verify");
2409        assert!(valid, "ES256 signature verifies under issuer key");
2410    }
2411
2412    #[tokio::test]
2413    async fn svid_es384_mints_a_verifiable_jwt_svid() {
2414        let backend = p384_backend();
2415
2416        let token = mint_svid(
2417            &backend,
2418            "ecdsa384-issuer",
2419            "spiffe://example.org",
2420            SvidAlg::Es384,
2421            "spiffe://example.org/db-01",
2422            "vault",
2423            Some(300),
2424            &serde_json::Value::Null,
2425        )
2426        .await
2427        .expect("mint es384 svid");
2428
2429        let parts: Vec<&str> = token.split('.').collect();
2430        assert_eq!(parts.len(), 3);
2431        let (header, claims, _signing_input, sig) = parse_token(&token);
2432        assert_eq!(header["alg"], "ES384");
2433        assert_eq!(claims["sub"], "spiffe://example.org/db-01");
2434        assert_eq!(
2435            header["kid"],
2436            jwt_svid_jwk_kid(&backend.public_der, SvidAlg::Es384),
2437            "JWS kid matches the published JWKS key id"
2438        );
2439        assert_eq!(sig.len(), 96, "ES384 signatures are raw fixed r||s");
2440
2441        let signing_input = format!("{}.{}", parts[0], parts[1]);
2442        let decoding_key = jsonwebtoken::DecodingKey::from_ec_pem(backend.public_pem.as_bytes())
2443            .expect("decoding key");
2444        let valid = jsonwebtoken::crypto::verify(
2445            parts[2],
2446            signing_input.as_bytes(),
2447            &decoding_key,
2448            jsonwebtoken::Algorithm::ES384,
2449        )
2450        .expect("verify");
2451        assert!(valid, "ES384 signature verifies under issuer key");
2452    }
2453}