Skip to main content

basil_core/core/
minter.rs

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