Skip to content

Commit

Permalink
Remove k256, use jose methods for dpop request
Browse files Browse the repository at this point in the history
  • Loading branch information
sugyan committed Aug 31, 2024
1 parent 05911f7 commit acd81a7
Show file tree
Hide file tree
Showing 13 changed files with 155 additions and 200 deletions.
19 changes: 0 additions & 19 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

9 changes: 4 additions & 5 deletions atrium-oauth/oauth-client/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -19,13 +19,12 @@ atrium-api = { workspace = true, default-features = false }
atrium-xrpc.workspace = true
base64.workspace = true
chrono.workspace = true
ecdsa = { workspace = true, features = ["std", "signing", "verifying"] }
elliptic-curve = { workspace = true, features = ["jwk", "serde"] }
ecdsa = { workspace = true, features = ["signing"] }
elliptic-curve = { workspace = true }
jose-jwa.workspace = true
jose-jwk = { workspace = true, features = ["p256"] }
k256 = { workspace = true, features = ["ecdsa", "jwk", "serde"] }
p256 = { workspace = true, features = ["ecdsa", "jwk", "serde"] }
rand.workspace = true
p256 = { workspace = true, features = ["ecdsa"] }
rand = { workspace = true, features = ["small_rng"] }
reqwest = { workspace = true, optional = true }
serde = { workspace = true, features = ["derive"] }
serde_html_form.workspace = true
Expand Down
163 changes: 62 additions & 101 deletions atrium-oauth/oauth-client/src/http_client/dpop.rs
Original file line number Diff line number Diff line change
@@ -1,88 +1,66 @@
use crate::jose::create_signed_jwt;
use crate::jose::jws::RegisteredHeader;
use crate::jose::jwt::{Claims, PublicClaims, RegisteredClaims};
use crate::store::memory::MemorySimpleStore;
use crate::store::SimpleStore;
use crate::utils::get_random_values;
use atrium_xrpc::http::{Request, Response};
use atrium_xrpc::HttpClient;
use base64::engine::general_purpose::URL_SAFE_NO_PAD;
use base64::Engine;
use ecdsa::hazmat::{DigestPrimitive, SignPrimitive};
use ecdsa::{signature::SignerMut, Signature, SigningKey};
use ecdsa::{PrimeCurve, SignatureSize};
use elliptic_curve::generic_array::ArrayLength;
use elliptic_curve::ops::Invert;
use elliptic_curve::sec1::{FromEncodedPoint, ModulusSize, ToEncodedPoint};
use elliptic_curve::subtle::CtOption;
use elliptic_curve::{
AffinePoint, Curve, CurveArithmetic, FieldBytesSize, JwkEcKey, JwkParameters, Scalar, SecretKey,
};
use rand::rngs::ThreadRng;
use serde::{Deserialize, Serialize};
use jose_jwa::{Algorithm, Signing};
use jose_jwk::{crypto, EcCurves, Jwk, Key};
use rand::rngs::SmallRng;
use rand::{RngCore, SeedableRng};
use serde::Deserialize;
use std::sync::Arc;
use thiserror::Error;

#[derive(Error, Debug)]
const JWT_HEADER_TYP_DPOP: &str = "dpop+jwt";

#[derive(Deserialize)]
struct ErrorResponse {
error: String,
}

#[derive(Error, Debug)]
pub enum Error {
#[error("unsupported curve: {0}")]
UnsupportedCurve(String),
#[error("crypto error: {0:?}")]
JwkCrypto(crypto::Error),
#[error("key does not match any alg supported by the server")]
UnsupportedKey,
#[error(transparent)]
EC(#[from] elliptic_curve::Error),
#[error(transparent)]
SerdeJson(#[from] serde_json::Error),
#[error(transparent)]
SystemTime(#[from] std::time::SystemTimeError),
}

type Result<T> = core::result::Result<T, Error>;

#[derive(Serialize)]
enum JwtHeaderType {
#[serde(rename = "dpop+jwt")]
DpopJwt,
}

#[derive(Serialize)]
struct JwtHeader {
alg: String,
typ: JwtHeaderType,
jwk: JwkEcKey,
}

#[derive(Serialize)]
struct JwtClaims {
iss: String,
iat: u64,
jti: String,
htm: String,
htu: String,
#[serde(skip_serializing_if = "Option::is_none")]
nonce: Option<String>,
}

pub struct DpopClient<T, S = MemorySimpleStore<String, String>>
where
S: SimpleStore<String, String>,
{
inner: Arc<T>,
key: JwkEcKey,
key: Key,
#[allow(dead_code)]
iss: String,
nonces: S,
}

impl<T> DpopClient<T> {
pub fn new(
key: JwkEcKey,
key: Key,
iss: String,
supported_algs: Option<Vec<String>>,
http_client: Arc<T>,
supported_algs: &Option<Vec<String>>,
) -> Result<Self> {
if let Some(algs) = supported_algs {
let alg = String::from(match key.crv() {
k256::Secp256k1::CRV => "ES256K",
p256::NistP256::CRV => "ES256",
_ => return Err(Error::UnsupportedCurve(key.crv().to_string())),
let alg = String::from(match &key {
Key::Ec(ec) => match &ec.crv {
EcCurves::P256 => "ES256",
_ => unimplemented!(),
},
_ => unimplemented!(),
});
if !algs.contains(&alg) {
return Err(Error::UnsupportedKey);
Expand All @@ -97,63 +75,39 @@ impl<T> DpopClient<T> {
})
}
fn build_proof(&self, htm: String, htu: String, nonce: Option<String>) -> Result<String> {
Ok(match self.key.crv() {
k256::Secp256k1::CRV => {
self.create_jwk::<k256::Secp256k1>(htm, htu, String::from("ES256K"), nonce)?
}
p256::NistP256::CRV => {
self.create_jwk::<p256::NistP256>(htm, htu, String::from("ES256K"), nonce)?
match crypto::Key::try_from(&self.key).map_err(Error::JwkCrypto)? {
crypto::Key::P256(crypto::Kind::Secret(secret_key)) => {
let mut header = RegisteredHeader::from(Algorithm::Signing(Signing::Es256));
header.typ = Some(JWT_HEADER_TYP_DPOP.into());
header.jwk = Some(Jwk {
key: Key::from(&crypto::Key::from(secret_key.public_key())),
prm: Default::default(),
});
let claims = Claims {
registered: RegisteredClaims {
jti: Some(Self::generate_jti()),
iat: Some(
std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)?
.as_secs(),
),
..Default::default()
},
public: PublicClaims {
htm: Some(htm),
htu: Some(htu),
nonce,
..Default::default()
},
};
Ok(create_signed_jwt(secret_key.into(), header.into(), claims)?)
}
_ => return Err(Error::UnsupportedCurve(self.key.crv().to_string())),
})
}
fn create_jwk<C>(
&self,
htm: String,
htu: String,
alg: String,
nonce: Option<String>,
) -> Result<String>
where
C: Curve + JwkParameters + PrimeCurve + CurveArithmetic + DigestPrimitive,
AffinePoint<C>: FromEncodedPoint<C> + ToEncodedPoint<C>,
FieldBytesSize<C>: ModulusSize,
Scalar<C>: Invert<Output = CtOption<Scalar<C>>> + SignPrimitive<C>,
SignatureSize<C>: ArrayLength<u8>,
{
let key = SecretKey::<C>::from_jwk(&self.key)?;
let iat = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)?
.as_secs();
let header = JwtHeader {
alg,
typ: JwtHeaderType::DpopJwt,
jwk: key.public_key().to_jwk(),
};
let payload = JwtClaims {
iss: self.iss.clone(),
iat,
jti: URL_SAFE_NO_PAD.encode(get_random_values::<_, 16>(&mut ThreadRng::default())),
htm,
htu,
nonce,
};
let header = URL_SAFE_NO_PAD.encode(serde_json::to_vec(&header)?);
let payload = URL_SAFE_NO_PAD.encode(serde_json::to_vec(&payload)?);
let mut signing_key = SigningKey::<C>::from(key);
let signature: Signature<_> = signing_key.sign(format!("{header}.{payload}").as_bytes());
Ok(format!(
"{header}.{payload}.{}",
URL_SAFE_NO_PAD.encode(signature.to_bytes())
))
_ => unimplemented!(),
}
}
fn is_use_dpop_nonce_error(&self, response: &Response<Vec<u8>>) -> bool {
// is auth server?
if response.status() == 400 {
#[derive(Deserialize)]
struct ErrorResponse {
error: String,
}
if let Ok(res) = serde_json::from_slice::<ErrorResponse>(response.body()) {
return res.error == "use_dpop_nonce";
};
Expand All @@ -162,6 +116,13 @@ impl<T> DpopClient<T> {

false
}
// https://datatracker.ietf.org/doc/html/rfc9449#section-4.2
fn generate_jti() -> String {
let mut rng = SmallRng::from_entropy();
let mut bytes = [0u8; 12];
rng.fill_bytes(&mut bytes);
URL_SAFE_NO_PAD.encode(bytes)
}
}

#[cfg_attr(target_arch = "wasm32", async_trait::async_trait(?Send))]
Expand Down
18 changes: 9 additions & 9 deletions atrium-oauth/oauth-client/src/jose.rs
Original file line number Diff line number Diff line change
Expand Up @@ -14,15 +14,15 @@ pub enum Header {

#[cfg(test)]
mod tests {
use jose_jwa::{Algorithm, Signing};
use jws::RegisteredHeader;

use super::*;

// #[test]
// fn test_create_jwt() {
// let secret_key = SecretKey::<p256::NistP256>::from_slice(&[
// 178, 249, 128, 41, 213, 198, 33, 120, 72, 132, 129, 161, 128, 134, 36, 120, 199, 128,
// 234, 73, 217, 232, 94, 120, 78, 231, 64, 117, 105, 239, 160, 251,
// ])
// .expect("failed to create secret key");
// panic!("{:?}", create_jwt(&secret_key.into(), JwtClaims::default()));
// }
#[test]
fn test_serialize_claims() {
let header = Header::from(RegisteredHeader::from(Algorithm::Signing(Signing::Es256)));
let json = serde_json::to_string(&header).expect("failed to serialize header");
assert_eq!(json, r#"{"alg":"ES256"}"#);
}
}
Loading

0 comments on commit acd81a7

Please sign in to comment.