pkjwt.rs 3.2 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889
  1. //! `private_key_jwt` client-auth fixture for `tests/e2e_edge_matrix.rs`.
  2. //!
  3. //! The harness `seed_client_bound` hardcodes `jwks: None`, so this module
  4. //! seeds the row directly with an inline JWKS and signs assertions through the
  5. //! same Ed25519 path the container suite uses.
  6. use base64::Engine as _;
  7. use d_oauth::db::{NewClient, TokenEndpointAuthMethod, client_repo};
  8. use jsonwebtoken::{Algorithm, EncodingKey, Header, encode};
  9. use ring::signature::{Ed25519KeyPair, KeyPair as _};
  10. use serde_json::{Value, json};
  11. use super::harness::{TestError, World, fail};
  12. /// Base64url (no pad) for the Ed25519 public key.
  13. const B64: base64::engine::GeneralPurpose = base64::engine::general_purpose::URL_SAFE_NO_PAD;
  14. /// Directly seeded `private_key_jwt` client.
  15. pub const CLIENT_ID: &str = "e2e-edge-pkjwt";
  16. /// An Ed25519 assertion signer plus the matching inline JWKS.
  17. pub struct AssertionKey {
  18. /// PKCS#8 signer.
  19. encoding: EncodingKey,
  20. /// Public JWK set stored on the client row.
  21. jwks: Value,
  22. }
  23. impl AssertionKey {
  24. /// Generate one key pair and its inline JWKS.
  25. pub fn generate(kid: &str) -> Result<Self, TestError> {
  26. let rng = ring::rand::SystemRandom::new();
  27. let pkcs8 = Ed25519KeyPair::generate_pkcs8(&rng).map_err(|_| fail("ed25519 keygen failed"))?;
  28. let pair =
  29. Ed25519KeyPair::from_pkcs8(pkcs8.as_ref()).map_err(|_| fail("ed25519 reload failed"))?;
  30. let x = B64.encode(pair.public_key().as_ref());
  31. Ok(Self {
  32. encoding: EncodingKey::from_ed_der(pkcs8.as_ref()),
  33. jwks: json!({"keys": [{"kty": "OKP", "crv": "Ed25519", "x": x, "kid": kid}]}),
  34. })
  35. }
  36. /// The inline JWKS to store on the client row.
  37. #[must_use]
  38. pub fn jwks(&self) -> Value {
  39. self.jwks.clone()
  40. }
  41. /// Sign one assertion for `client_id`/`audience` with a fixed `jti`.
  42. pub fn mint(&self, client_id: &str, audience: &str, jti: &str) -> Result<String, TestError> {
  43. let now = time::OffsetDateTime::now_utc().unix_timestamp();
  44. let claims = json!({
  45. "iss": client_id,
  46. "sub": client_id,
  47. "aud": audience,
  48. "jti": jti,
  49. "exp": now + 300,
  50. "iat": now,
  51. });
  52. encode(&Header::new(Algorithm::EdDSA), &claims, &self.encoding)
  53. .map_err(|error| fail(format!("assertion mint failed: {error}")))
  54. }
  55. }
  56. /// Seed a `private_key_jwt` client row with an inline `jwks`.
  57. pub async fn seed_assertion_client(world: &World, jwks: Value) -> Result<(), TestError> {
  58. let new = NewClient {
  59. id: uuid::Uuid::new_v4(),
  60. client_id: CLIENT_ID.to_owned(),
  61. client_secret_hash: None,
  62. client_secret_expires_at: None,
  63. client_name: "edge private_key_jwt".to_owned(),
  64. grant_types: vec!["client_credentials".to_owned()],
  65. response_types: vec!["token".to_owned()],
  66. scope: String::new(),
  67. token_endpoint_auth_method: TokenEndpointAuthMethod::PrivateKeyJwt.as_str().to_owned(),
  68. jwks: Some(jwks),
  69. jwks_uri: None,
  70. tls_client_auth_subject_dn: None,
  71. tls_client_auth_cert_thumbprint: None,
  72. request_uris: None,
  73. contacts: None,
  74. software_id: None,
  75. dpop_bound_access_tokens: false,
  76. access_token_format: "opaque".to_owned(),
  77. registration_access_token_hash: None,
  78. };
  79. client_repo::insert(&world.pool, &world.state.keys, false, &new).await?;
  80. Ok(())
  81. }