Skip to content

Commit

Permalink
Add e2e test and update API
Browse files Browse the repository at this point in the history
  • Loading branch information
cobward committed Jul 31, 2024
1 parent 321434a commit 6b0d643
Show file tree
Hide file tree
Showing 25 changed files with 798 additions and 442 deletions.
12 changes: 9 additions & 3 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -8,14 +8,18 @@ description = "OpenID Connect for Verifiable Presentations"
repository = "https://github.com/spruceid/oidc4vp-rs/"
documentation = "https://docs.rs/oid4vp/"

[features]
reqwest = ["dep:reqwest"]
p256 = ["dep:p256"]

[dependencies]
anyhow = "1.0.75"
async-trait = "0.1.73"
base64 = "0.21.4"
did-web = "0.2.2"
josekit = { git = "https://github.com/cobward/josekit-rs", rev = "635c8a7" }
p256 = { version = "0.13.2", features = ["jwk"] }
reqwest = "0.11.20"
http = "1.1.0"
p256 = { version = "0.13.2", features = ["jwk"], optional = true }
reqwest = { version = "0.12.5", features = ["rustls-tls"], optional = true }
serde = "1.0.188"
serde_cbor = "0.11.2"
serde_json = "1.0.107"
Expand All @@ -31,6 +35,8 @@ x509-cert = "0.2.4"
[dev-dependencies]
serde_path_to_error = "0.1.8"
tokio = { version = "1.32.0", features = ["macros"] }
did-method-key = "0.2"
oid4vp = { path = ".", features = ["p256"] }

[target.'cfg(target_arch = "wasm32")'.dependencies]
uuid = { version = "1.2", features = ["v4", "serde", "js"] }
Expand Down
92 changes: 54 additions & 38 deletions src/core/authorization_request/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ use self::{

use super::{
object::{ParsingErrorContext, UntypedObject},
util::default_http_client,
util::{base_request, HttpClient},
};

pub mod parameters;
Expand Down Expand Up @@ -65,25 +65,42 @@ impl AuthorizationRequest {
/// [RequestObject].
///
/// Custom wallet metadata can be provided, otherwise the default metadata for this profile is used.
pub async fn validate_with_http_client<W: Wallet + ?Sized>(
pub async fn validate<W: Wallet + ?Sized>(
self,
wallet: &W,
http_client: &reqwest::Client,
) -> Result<AuthorizationRequestObject> {
let jwt = match self.request_indirection {
RequestIndirection::ByValue(jwt) => jwt,
RequestIndirection::ByReference(url) => http_client
.get(url.clone())
.send()
.await
.context(format!("failed to GET {url}"))?
.error_for_status()
.context(format!("failed to GET {url}"))?
.text()
.await
.context(format!("failed to parse data from {url}"))?,
RequestIndirection::ByReference(url) => {
let request = base_request()
.method("GET")
.uri(url.to_string())
.body(vec![])
.context("failed to build authorization request request")?;

let response = wallet
.http_client()
.execute(request)
.await
.context(format!(
"failed to make authorization request request at {url}"
))?;

let status = response.status();
let Ok(body) = String::from_utf8(response.into_body()) else {
bail!("failed to parse authorization request response as UTF-8 from {url} (status: {status})")
};

if !status.is_success() {
bail!(
"authorization request request was unsuccessful (status: {status}): {body}"
)
}

body
}
};
let aro = verify_request(wallet, jwt, http_client)
let aro = verify_request(wallet, jwt)
.await
.context("unable to validate Authorization Request")?;
if self.client_id.as_str() != aro.client_id().0.as_str() {
Expand Down Expand Up @@ -187,40 +204,39 @@ impl AuthorizationRequestObject {
&self.2
}

pub async fn resolve_presentation_definition_with_http_client(
pub async fn resolve_presentation_definition<H: HttpClient>(
&self,
http_client: reqwest::Client,
http_client: &H,
) -> Result<PresentationDefinition> {
match &self.5 {
PresentationDefinitionIndirection::ByValue(by_value) => Ok(by_value.clone()),
PresentationDefinitionIndirection::ByReference(by_reference) => {
let value: Json = http_client
.get(by_reference.clone())
.send()
.await
.context(format!(
"failed to GET Presentation Definition from '{by_reference}'"
))?
.error_for_status()
.context(format!(
"failed to GET Presentation Definition from '{by_reference}'"
))?
.json()
.await
let request = base_request()
.method("GET")
.uri(by_reference.to_string())
.body(vec![])
.context("failed to build presentation definition request")?;

let response = http_client.execute(request).await.context(format!(
"failed to make presentation definition request at {by_reference}"
))?;

let status = response.status();

if !status.is_success() {
bail!("presentation definition request was unsuccessful (status: {status})")
}

serde_json::from_slice::<Json>(response.body())
.context(format!(
"response received from '{by_reference}' was not JSON"
))?;
value.try_into()
"failed to parse presentation definition response as JSON from {by_reference} (status: {status})"
))?
.try_into()
.context("failed to parse presentation definition from JSON")
}
}
}

/// Uses the default library http client.
pub async fn resolve_presentation_definition(&self) -> Result<PresentationDefinition> {
self.resolve_presentation_definition_with_http_client(default_http_client()?)
.await
}

pub fn is_id_token_requested(&self) -> Option<bool> {
match self.4 {
ResponseType::VpToken => Some(false),
Expand Down
53 changes: 26 additions & 27 deletions src/core/authorization_request/parameters.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ use std::fmt;

use crate::core::{
object::{ParsingErrorContext, TypedParameter, UntypedObject},
util::default_http_client,
util::{base_request, HttpClient},
};
use anyhow::{bail, Context, Error, Ok};
use serde::{Deserialize, Serialize};
Expand Down Expand Up @@ -130,40 +130,39 @@ impl ClientMetadata {
///
/// If the client metadata is not passed by reference or value if the Authorization Request Object,
/// then this function will return an error.
///
/// Uses the library's default http client.
pub async fn resolve(request: &AuthorizationRequestObject) -> Result<Self, Error> {
Self::resolve_with_http_client(request, &default_http_client()?).await
}

/// Resolves the client metadata from the Authorization Request Object.
///
/// If the client metadata is not passed by reference or value if the Authorization Request Object,
/// then this function will return an error.
pub async fn resolve_with_http_client(
pub async fn resolve<H: HttpClient>(
request: &AuthorizationRequestObject,
http_client: &reqwest::Client,
http_client: &H,
) -> Result<Self, Error> {
if let Some(metadata) = request.get() {
return metadata;
}

if let Some(metadata_uri) = request.get::<ClientMetadataUri>() {
let uri = metadata_uri.parsing_error()?;
return http_client
.get(uri.0.clone())
.send()
.await
.context(format!("failed to GET {}", uri.0))?
.error_for_status()
.context(format!("failed to GET {}", uri.0))?
.json()
let uri = metadata_uri.parsing_error()?.0;
let request = base_request()
.method("GET")
.uri(uri.to_string())
.body(vec![])
.context("failed to build client metadata request")?;

let response = http_client
.execute(request)
.await
.map(ClientMetadata)
.context(format!("failed to make client metadata request at {uri}"))?;

let status = response.status();

if !status.is_success() {
bail!("client metadata request was unsuccessful (status: {status})")
}

return serde_json::from_slice::<Json>(response.body())
.context(format!(
"could not parse response from GET '{}' as JSON",
uri.0
));
"failed to parse client metadata response as JSON from {uri} (status: {status})"
))?
.try_into()
.context("failed to parse client metadata from JSON");
}

bail!("the client metadata was not passed by reference or value")
Expand Down Expand Up @@ -281,7 +280,7 @@ impl TryFrom<Json> for ResponseUri {
const DIRECT_POST: &str = "direct_post";
const DIRECT_POST_JWT: &str = "direct_post.jwt";

#[derive(Debug, Clone, Serialize, Deserialize)]
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, PartialOrd, Ord)]
#[serde(into = "String", from = "String")]
pub enum ResponseMode {
/// The `direct_post` response mode as defined in OID4VP.
Expand Down
31 changes: 14 additions & 17 deletions src/core/authorization_request/verification/mod.rs
Original file line number Diff line number Diff line change
@@ -1,14 +1,11 @@
use crate::{
core::{
metadata::{
parameters::{
verifier::{AuthorizationEncryptedResponseAlg, AuthorizationEncryptedResponseEnc},
wallet::{
AuthorizationEncryptionAlgValuesSupported,
AuthorizationEncryptionEncValuesSupported,
},
metadata::parameters::{
verifier::{AuthorizationEncryptedResponseAlg, AuthorizationEncryptedResponseEnc},
wallet::{
AuthorizationEncryptionAlgValuesSupported,
AuthorizationEncryptionEncValuesSupported, ClientIdSchemesSupported,
},
WalletMetadata,
},
object::{ParsingErrorContext, TypedParameter, UntypedObject},
},
Expand All @@ -23,8 +20,8 @@ use super::{
};

pub mod did;
pub mod x509_san_dns;
pub mod x509_san_uri;
pub mod verifier;
pub mod x509_san;

/// Verifies Authorization Request Objects.
#[allow(unused_variables)]
Expand Down Expand Up @@ -107,13 +104,12 @@ pub trait RequestVerifier {
pub(crate) async fn verify_request<W: Wallet + ?Sized>(
wallet: &W,
jwt: String,
http_client: &reqwest::Client,
) -> Result<AuthorizationRequestObject> {
let request: AuthorizationRequestObject = ssi::jwt::decode_unverified::<UntypedObject>(&jwt)
.context("unable to decode Authorization Request Object JWT")?
.try_into()?;

validate_request_against_metadata(wallet.wallet_metadata(), &request, http_client).await?;
validate_request_against_metadata(wallet, &request).await?;

let client_id_scheme = request.client_id_scheme();

Expand All @@ -131,14 +127,15 @@ pub(crate) async fn verify_request<W: Wallet + ?Sized>(
Ok(request)
}

pub(crate) async fn validate_request_against_metadata(
wallet_metadata: &WalletMetadata,
pub(crate) async fn validate_request_against_metadata<W: Wallet + ?Sized>(
wallet: &W,
request: &AuthorizationRequestObject,
http_client: &reqwest::Client,
) -> Result<(), Error> {
let wallet_metadata = wallet.metadata();

let client_id_scheme = request.client_id_scheme();
if !wallet_metadata
.client_id_schemes_supported()
.get_or_default::<ClientIdSchemesSupported>()?
.0
.contains(client_id_scheme)
{
Expand All @@ -148,7 +145,7 @@ pub(crate) async fn validate_request_against_metadata(
)
}

let client_metadata = ClientMetadata::resolve_with_http_client(request, http_client)
let client_metadata = ClientMetadata::resolve(request, wallet.http_client())
.await?
.0;

Expand Down
35 changes: 35 additions & 0 deletions src/core/authorization_request/verification/verifier.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
use anyhow::Result;
#[cfg(feature = "p256")]
use anyhow::{bail, Error};
#[cfg(feature = "p256")]
use p256::ecdsa::signature::Verifier as _;
use x509_cert::spki::SubjectPublicKeyInfoRef;

pub trait Verifier: Sized {
/// Construct a [Verifier] from [SubjectPublicKeyInfoRef].
///
/// ## Params
/// * `spki` - the public key information necessary to construct a [Verifier].
/// * `algorithm` - the value taken from the `alg` header of the request, to hint at what curve should be used by the [Verifier].
fn from_spki(spki: SubjectPublicKeyInfoRef<'_>, algorithm: String) -> Result<Self>;
fn verify(&self, payload: &[u8], signature: &[u8]) -> Result<()>;
}

#[cfg(feature = "p256")]
#[derive(Debug, Clone)]
pub struct P256Verifier(p256::ecdsa::VerifyingKey);

#[cfg(feature = "p256")]
impl Verifier for P256Verifier {
fn from_spki(spki: SubjectPublicKeyInfoRef<'_>, algorithm: String) -> Result<Self> {
if algorithm != "ES256" {
bail!("P256Verifier cannot verify requests signed with '{algorithm}'")
}
spki.try_into().map(Self).map_err(Error::from)
}

fn verify(&self, payload: &[u8], signature: &[u8]) -> Result<()> {
let signature = p256::ecdsa::Signature::from_slice(signature)?;
self.0.verify(payload, &signature).map_err(Error::from)
}
}
Loading

0 comments on commit 6b0d643

Please sign in to comment.