Skip to content

Commit

Permalink
feat: add support for OpenID4VP (#53)
Browse files Browse the repository at this point in the history
* feat: implement `Subject` responsible for singing and verifying data

* feat: add `oid4vp` support

* docs: update `/v1/authorization_request` in `openapi` file

* chore: update postman collection

* fix: set `presentation_definition` to contain `VerifiableCredential`

* fix: use slice directly

* style: rename endpoint deserialization helpers

* docs: add comment for `GenericAuthorizationResponse`

* style: change `AuthorizationRequestsRequest` to `AuthorizationRequestsEndpointRequest`
  • Loading branch information
nanderstabel authored May 13, 2024
1 parent fcd11b0 commit e581d45
Show file tree
Hide file tree
Showing 27 changed files with 590 additions and 170 deletions.
58 changes: 45 additions & 13 deletions Cargo.lock

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

9 changes: 5 additions & 4 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -18,10 +18,11 @@ rust-version = "1.76.0"

[workspace.dependencies]
did_manager = { git = "https://[email protected]/impierce/did-manager.git", rev = "c70c0f1" }
siopv2 = { git = "https://[email protected]/impierce/openid4vc.git", rev = "a932af7" }
oid4vci = { git = "https://[email protected]/impierce/openid4vc.git", rev = "a932af7" }
oid4vc-core = { git = "https://[email protected]/impierce/openid4vc.git", rev = "a932af7" }
oid4vc-manager = { git = "https://[email protected]/impierce/openid4vc.git", rev = "a932af7" }
siopv2 = { git = "https://[email protected]/impierce/openid4vc.git", rev = "ce4e3fd" }
oid4vci = { git = "https://[email protected]/impierce/openid4vc.git", rev = "ce4e3fd" }
oid4vc-core = { git = "https://[email protected]/impierce/openid4vc.git", rev = "ce4e3fd" }
oid4vc-manager = { git = "https://[email protected]/impierce/openid4vc.git", rev = "ce4e3fd" }
oid4vp = { git = "https://[email protected]/impierce/openid4vc.git", rev = "ce4e3fd" }

async-trait = "0.1"
axum = { version = "0.7", features = ["tracing"] }
Expand Down
1 change: 1 addition & 0 deletions agent_api_rest/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ http-api-problem = "0.57"
hyper = { version = "1.2" }
oid4vc-core.workspace = true
oid4vci.workspace = true
oid4vp.workspace = true
serde.workspace = true
serde_json.workspace = true
siopv2.workspace = true
Expand Down
6 changes: 6 additions & 0 deletions agent_api_rest/openapi.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -160,6 +160,12 @@ paths:
nonce:
type: string
example: "0d520cbe176ab9e1f7888c70888020d84a69672a4baabd3ce1c6aaad8f6420c0"
state:
type: string
example: "84266fdbd31d4c2c6d0665f7e8380fa3"
presentation_definition_id:
type: string
example: "presentation_definition"
required:
- nonce
responses:
Expand Down
5 changes: 3 additions & 2 deletions agent_api_rest/postman/ssi-agent.postman_collection.json
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,8 @@
"}",
""
],
"type": "text/javascript"
"type": "text/javascript",
"packages": {}
}
}
],
Expand Down Expand Up @@ -251,7 +252,7 @@
"header": [],
"body": {
"mode": "raw",
"raw": "{\n \"nonce\": \"this is a nonce\"\n}",
"raw": "{\n \"nonce\": \"this is a nonce\",\n \"presentation_definition_id\": \"presentation_definition\"\n}",
"options": {
"raw": {
"language": "json"
Expand Down
8 changes: 5 additions & 3 deletions agent_api_rest/src/issuance/credential_issuer/credential.rs
Original file line number Diff line number Diff line change
Expand Up @@ -120,7 +120,9 @@ mod tests {

use crate::{
app,
issuance::{credential_issuer::token::tests::token, credentials::CredentialsRequest, offers::tests::offers},
issuance::{
credential_issuer::token::tests::token, credentials::CredentialsEndpointRequest, offers::tests::offers,
},
tests::{BASE_URL, OFFER_ID},
};

Expand Down Expand Up @@ -170,14 +172,14 @@ mod tests {

// The 'backend' server can either opt for an already signed credential...
let credentials_endpoint_request = if is_self_signed {
CredentialsRequest {
CredentialsEndpointRequest {
offer_id: offer_id.clone(),
credential: json!("eyJ0eXAiOiJKV1QiLCJhbGciOiJFZERTQSIsImtpZCI6ImRpZDprZXk6ejZNa2lpZXlvTE1TVnNKQVp2N0pqZTV3V1NrREV5bVVna3lGOGtiY3JqWnBYM3FkI3o2TWtpaWV5b0xNU1ZzSkFadjdKamU1d1dTa0RFeW1VZ2t5RjhrYmNyalpwWDNxZCJ9.eyJpc3MiOiJkaWQ6a2V5Ono2TWtpaWV5b0xNU1ZzSkFadjdKamU1d1dTa0RFeW1VZ2t5RjhrYmNyalpwWDNxZCIsInN1YiI6ImRpZDprZXk6ejZNa2lpZXlvTE1TVnNKQVp2N0pqZTV3V1NrREV5bVVna3lGOGtiY3JqWnBYM3FkIiwiZXhwIjo5OTk5OTk5OTk5LCJpYXQiOjAsInZjIjp7IkBjb250ZXh0IjpbImh0dHBzOi8vd3d3LnczLm9yZy8yMDE4L2NyZWRlbnRpYWxzL3YxIiwiaHR0cHM6Ly9wdXJsLmltc2dsb2JhbC5vcmcvc3BlYy9vYi92M3AwL2NvbnRleHQtMy4wLjIuanNvbiJdLCJpZCI6Imh0dHA6Ly9leGFtcGxlLmNvbS9jcmVkZW50aWFscy8zNTI3IiwidHlwZSI6WyJWZXJpZmlhYmxlQ3JlZGVudGlhbCIsIk9wZW5CYWRnZUNyZWRlbnRpYWwiXSwiaXNzdWVyIjoiZGlkOmtleTp6Nk1raWlleW9MTVNWc0pBWnY3SmplNXdXU2tERXltVWdreUY4a2JjcmpacFgzcWQiLCJpc3N1YW5jZURhdGUiOiIyMDEwLTAxLTAxVDAwOjAwOjAwWiIsIm5hbWUiOiJUZWFtd29yayBCYWRnZSIsImNyZWRlbnRpYWxTdWJqZWN0Ijp7ImZpcnN0X25hbWUiOiJGZXJyaXMiLCJsYXN0X25hbWUiOiJSdXN0YWNlYW4iLCJpZCI6ImRpZDprZXk6ejZNa2lpZXlvTE1TVnNKQVp2N0pqZTV3V1NrREV5bVVna3lGOGtiY3JqWnBYM3FkIn19fQ.r7T_zOXP7E2k7eAPq5EF20shwrnPKK0mOCfNaB0phPEXVkYSG_sf6QygUDuJ8-P0yU4EEajgE0dxJuRfdMVDAQ"),
is_signed: true,
}
} else {
// ...or else, submitting the data that will be signed inside `UniCore`.
CredentialsRequest {
CredentialsEndpointRequest {
offer_id: offer_id.clone(),
credential: json!({
"credentialSubject": {
Expand Down
4 changes: 2 additions & 2 deletions agent_api_rest/src/issuance/credentials.rs
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ pub(crate) async fn get_credentials(State(state): State<IssuanceState>, Path(cre

#[derive(Deserialize, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct CredentialsRequest {
pub struct CredentialsEndpointRequest {
pub offer_id: String,
pub credential: Value,
#[serde(default)]
Expand All @@ -43,7 +43,7 @@ pub(crate) async fn credentials(
) -> Response {
info!("Request Body: {}", payload);

let Ok(CredentialsRequest {
let Ok(CredentialsEndpointRequest {
offer_id,
credential: data,
is_signed,
Expand Down
4 changes: 2 additions & 2 deletions agent_api_rest/src/issuance/offers.rs
Original file line number Diff line number Diff line change
Expand Up @@ -16,15 +16,15 @@ use tracing::info;

#[derive(Deserialize, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct OffersRequest {
pub struct OffersEndpointRequest {
pub offer_id: String,
}

#[axum_macros::debug_handler]
pub(crate) async fn offers(State(state): State<IssuanceState>, Json(payload): Json<Value>) -> Response {
info!("Request Body: {}", payload);

let Ok(OffersRequest { offer_id }) = serde_json::from_value(payload) else {
let Ok(OffersEndpointRequest { offer_id }) = serde_json::from_value(payload) else {
return (StatusCode::BAD_REQUEST, "invalid payload").into_response();
};

Expand Down
46 changes: 37 additions & 9 deletions agent_api_rest/src/verification/authorization_requests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ use axum::{
Json,
};
use hyper::header;
use serde::{Deserialize, Serialize};
use serde_json::Value;
use tracing::info;

Expand All @@ -24,32 +25,58 @@ pub(crate) async fn get_authorization_requests(
// Get the authorization request if it exists.
match query_handler(&authorization_request_id, &state.query.authorization_request).await {
Ok(Some(AuthorizationRequestView {
siopv2_authorization_request: Some(siopv2_authorization_request),
authorization_request: Some(authorization_request),
..
})) => (StatusCode::OK, Json(siopv2_authorization_request)).into_response(),
})) => (StatusCode::OK, Json(authorization_request)).into_response(),
Ok(None) => StatusCode::NOT_FOUND.into_response(),
_ => StatusCode::INTERNAL_SERVER_ERROR.into_response(),
}
}

#[derive(Deserialize, Serialize)]
pub struct AuthorizationRequestsEndpointRequest {
pub nonce: String,
pub state: Option<String>,
pub presentation_definition_id: Option<String>,
}

#[axum_macros::debug_handler]
pub(crate) async fn authorization_requests(
State(verification_state): State<VerificationState>,
Json(payload): Json<Value>,
) -> Response {
info!("Request Body: {}", payload);

let nonce = if let Some(nonce) = payload["nonce"].as_str() {
nonce
} else {
return (StatusCode::BAD_REQUEST, "nonce is required").into_response();
let Ok(AuthorizationRequestsEndpointRequest {
nonce,
state,
presentation_definition_id,
}) = serde_json::from_value(payload)
else {
return (StatusCode::BAD_REQUEST, "invalid payload").into_response();
};

let state = generate_random_string();
let state = state.unwrap_or(generate_random_string());

// TODO: This needs to be properly fixed instead of reading the presentation definitions from the file system
// everytime a request is made. `PresentationDefinition`'s should be implemented as a proper `Aggregate`. This
// current suboptimal solution requires the `./tmp:/app/agent_api_rest` volume to be mounted in the `docker-compose.yml`.
let presentation_definition = presentation_definition_id.map(|presentation_definition_id| {
let project_root_dir = env!("CARGO_MANIFEST_DIR");

serde_json::from_reader(
std::fs::File::open(format!(
"{project_root_dir}/../agent_verification/presentation_definitions/{presentation_definition_id}.json"
))
.unwrap(),
)
.unwrap()
});

let command = AuthorizationRequestCommand::CreateAuthorizationRequest {
nonce: nonce.to_string(),
state: state.clone(),
presentation_definition,
};

// Create the authorization request.
Expand All @@ -75,7 +102,7 @@ pub(crate) async fn authorization_requests(
// Return the credential.
match query_handler(&state, &verification_state.query.authorization_request).await {
Ok(Some(AuthorizationRequestView {
form_url_encoded_authorization_request,
form_url_encoded_authorization_request: Some(form_url_encoded_authorization_request),
..
})) => (
StatusCode::CREATED,
Expand Down Expand Up @@ -114,7 +141,8 @@ pub mod tests {
.header(http::header::CONTENT_TYPE, mime::APPLICATION_JSON.as_ref())
.body(Body::from(
serde_json::to_vec(&json!({
"nonce": "nonce"
"nonce": "nonce",
"presentation_definition_id": "presentation_definition"
}))
.unwrap(),
))
Expand Down
Loading

0 comments on commit e581d45

Please sign in to comment.