dpop.rs 3.0 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586
  1. //! Hand-rolled DPoP helpers for the E2E grant suite (extension payloads the
  2. //! `oauth2` crate cannot drive).
  3. //!
  4. //! Included only by the grant suite, which uses every item here.
  5. use super::harness::{TestError, ensure, fail};
  6. /// Base64url (no-pad) engine for JWK/JWT segments.
  7. const B64: base64::engine::GeneralPurpose = base64::engine::general_purpose::URL_SAFE_NO_PAD;
  8. /// A DPoP signing key plus its public coordinates (ES256, hand-rolled).
  9. pub struct DpopKey {
  10. /// PKCS#8 signer (fixed-width ES256 signatures).
  11. pair: ring::signature::EcdsaKeyPair,
  12. /// Base64url `x` coordinate.
  13. x: String,
  14. /// Base64url `y` coordinate.
  15. y: String,
  16. }
  17. impl DpopKey {
  18. /// Generate a fresh P-256 key.
  19. pub fn generate() -> Result<Self, TestError> {
  20. let rng = ring::rand::SystemRandom::new();
  21. let pkcs8 = ring::signature::EcdsaKeyPair::generate_pkcs8(
  22. &ring::signature::ECDSA_P256_SHA256_FIXED_SIGNING,
  23. &rng,
  24. )
  25. .map_err(|error| fail(format!("dpop keygen failed: {error}")))?;
  26. let pair = ring::signature::EcdsaKeyPair::from_pkcs8(
  27. &ring::signature::ECDSA_P256_SHA256_FIXED_SIGNING,
  28. pkcs8.as_ref(),
  29. &rng,
  30. )
  31. .map_err(|error| fail(format!("dpop key load failed: {error}")))?;
  32. let point: &[u8] = ring::signature::KeyPair::public_key(&pair).as_ref();
  33. ensure(point.len() == 65 && point[0] == 4, "unexpected P-256 point")?;
  34. let x = base64::Engine::encode(&B64, &point[1..33]);
  35. let y = base64::Engine::encode(&B64, &point[33..65]);
  36. Ok(Self { pair, x, y })
  37. }
  38. /// The public JWK for RFC 7638 thumbprint checks.
  39. pub fn jwk(&self) -> serde_json::Value {
  40. serde_json::json!({
  41. "kty": "EC",
  42. "crv": "P-256",
  43. "x": self.x,
  44. "y": self.y,
  45. })
  46. }
  47. /// Mint a DPoP proof JWT for `htm`/`htu` with a fresh `jti`.
  48. pub fn proof(&self, htm: &str, htu: &str) -> Result<String, TestError> {
  49. self.proof_with_jti(htm, htu, &uuid::Uuid::new_v4().to_string())
  50. }
  51. /// Mint a DPoP proof JWT with a caller-chosen `jti` (replay tests).
  52. pub fn proof_with_jti(&self, htm: &str, htu: &str, jti: &str) -> Result<String, TestError> {
  53. let header = format!(
  54. "{{\"typ\":\"dpop+jwt\",\"alg\":\"ES256\",\"jwk\":{{\"kty\":\"EC\",\"crv\":\"P-256\",\"x\":\"{}\",\"y\":\"{}\"}}}}",
  55. self.x, self.y
  56. );
  57. let iat = time::OffsetDateTime::now_utc().unix_timestamp();
  58. let claims = serde_json::json!({"htm": htm, "htu": htu, "iat": iat, "jti": jti});
  59. let signing_input = format!(
  60. "{}.{}",
  61. base64::Engine::encode(&B64, header.as_bytes()),
  62. base64::Engine::encode(
  63. &B64,
  64. serde_json::to_string(&claims)
  65. .map_err(|error| fail(format!("claims: {error}")))?
  66. .as_bytes()
  67. )
  68. );
  69. let rng = ring::rand::SystemRandom::new();
  70. let signature = self
  71. .pair
  72. .sign(&rng, signing_input.as_bytes())
  73. .map_err(|error| fail(format!("dpop sign failed: {error}")))?;
  74. Ok(format!(
  75. "{signing_input}.{}",
  76. base64::Engine::encode(&B64, signature.as_ref())
  77. ))
  78. }
  79. }