Skip to content

Commit

Permalink
create, update rpcs and tests
Browse files Browse the repository at this point in the history
  • Loading branch information
UMR1352 committed Mar 15, 2024
1 parent 0980069 commit d6e0e85
Show file tree
Hide file tree
Showing 10 changed files with 345 additions and 9 deletions.
2 changes: 1 addition & 1 deletion bindings/grpc/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ path = "src/main.rs"
[dependencies]
anyhow = "1.0.75"
identity_eddsa_verifier = { path = "../../identity_eddsa_verifier" }
identity_iota = { path = "../../identity_iota", features = ["resolver", "sd-jwt"] }
identity_iota = { path = "../../identity_iota", features = ["resolver", "sd-jwt", "status-list-2021"] }
identity_stronghold = { path = "../../identity_stronghold", features = ["send-sync-storage"] }
iota-sdk = { version = "1.1.2", features = ["stronghold"] }
prost = "0.12"
Expand Down
1 change: 1 addition & 0 deletions bindings/grpc/proto/credentials.proto
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ service Jwt {

message VcValidationRequest {
string credential_jwt = 1;
optional string status_list_credential_json = 2;
}

message VcValidationResponse {
Expand Down
31 changes: 31 additions & 0 deletions bindings/grpc/proto/status_list_2021.proto
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
syntax = "proto3";
package status_list_2021;

enum Purpose {
REVOCATION = 0;
SUSPENSION = 1;
}

message CreateRequest {
Purpose purpose = 1;
optional uint64 length = 2;
optional string id = 3;
optional string expiration_date = 4;
repeated string contexts = 5;
repeated string types = 6;
string issuer = 7;
}

message StatusListCredential {
string credential_json = 1;
}

message UpdateRequest {
string credential_json = 1;
map<uint64, bool> entries = 2;
}

service StatusList2021Svc {
rpc create(CreateRequest) returns(StatusListCredential);
rpc update(UpdateRequest) returns(StatusListCredential);
}
5 changes: 4 additions & 1 deletion bindings/grpc/src/services/credential/validation.rs
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,10 @@ impl VcValidation for VcValidator {
err,
)]
async fn validate(&self, req: Request<VcValidationRequest>) -> Result<Response<VcValidationResponse>, Status> {
let VcValidationRequest { credential_jwt } = req.into_inner();
let VcValidationRequest {
credential_jwt,
status_list_credential_json,
} = req.into_inner();
let jwt = Jwt::new(credential_jwt);
let issuer_did = JwtCredentialValidatorUtils::extract_issuer_from_jwt::<IotaDID>(&jwt)
.map_err(VcValidationError::JwtValidationError)?;
Expand Down
2 changes: 2 additions & 0 deletions bindings/grpc/src/services/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ pub mod credential;
pub mod document;
pub mod health_check;
pub mod sd_jwt;
pub mod status_list_2021;

use identity_stronghold::StrongholdStorage;
use iota_sdk::client::Client;
Expand All @@ -14,6 +15,7 @@ pub fn routes(client: &Client, stronghold: &StrongholdStorage) -> Routes {
credential::init_services(&mut routes, client, stronghold);
routes.add_service(sd_jwt::service(client));
routes.add_service(document::service(client, stronghold));
routes.add_service(status_list_2021::service());

routes.routes()
}
164 changes: 164 additions & 0 deletions bindings/grpc/src/services/status_list_2021.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,164 @@
use identity_iota::core::Context;
use identity_iota::core::FromJson;
use identity_iota::core::Timestamp;
use identity_iota::core::ToJson;
use identity_iota::core::Url;
use identity_iota::credential::status_list_2021::StatusList2021;
use identity_iota::credential::status_list_2021::StatusList2021Credential;
use identity_iota::credential::status_list_2021::StatusList2021CredentialBuilder;
use identity_iota::credential::status_list_2021::StatusList2021CredentialError;
use identity_iota::credential::status_list_2021::StatusPurpose;
use identity_iota::credential::Issuer;
use identity_iota::credential::{self};

use _status_list_2021::status_list2021_svc_server::StatusList2021Svc;
use _status_list_2021::status_list2021_svc_server::StatusList2021SvcServer;
use _status_list_2021::CreateRequest;
use _status_list_2021::Purpose;
use _status_list_2021::StatusListCredential;
use _status_list_2021::UpdateRequest;
use tonic::Code;
use tonic::Request;
use tonic::Response;
use tonic::Status;

mod _status_list_2021 {
use identity_iota::credential::status_list_2021::StatusPurpose;

tonic::include_proto!("status_list_2021");

impl From<Purpose> for StatusPurpose {
fn from(value: Purpose) -> Self {
match value {
Purpose::Revocation => StatusPurpose::Revocation,
Purpose::Suspension => StatusPurpose::Suspension,
}
}
}
}

#[derive(Debug, thiserror::Error)]
pub enum Error {
#[error("A valid status list must have at least 16KB entries")]
InvalidStatusListLength,
#[error("\"{0}\" is not a valid context")]
InvalidContext(String),
#[error("\"{0}\" is not a valid issuer")]
InvalidIssuer(String),
#[error("\"{0}\" is not a valid timestamp")]
InvalidTimestamp(String),
#[error("\"{0}\" is not a valid id")]
InvalidId(String),
#[error("Failed to deserialize into a valid StatusList2021Credential")]
CredentialDeserializationError(#[source] identity_iota::core::Error),
#[error(transparent)]
CredentialError(#[from] credential::Error),
#[error(transparent)]
StatusListError(StatusList2021CredentialError),
}

impl From<Error> for Status {
fn from(value: Error) -> Self {
let code = match &value {
Error::InvalidStatusListLength
| Error::InvalidContext(_)
| Error::InvalidIssuer(_)
| Error::InvalidTimestamp(_) => Code::InvalidArgument,
_ => Code::Internal,
};

Status::new(code, value.to_string())
}
}

pub struct StatusList2021Service;

#[tonic::async_trait]
impl StatusList2021Svc for StatusList2021Service {
#[tracing::instrument(
name = "create_status_list_credential",
skip_all,
fields(request = ?req.get_ref())
ret,
err,
)]
async fn create(&self, req: Request<CreateRequest>) -> Result<Response<StatusListCredential>, Status> {
let CreateRequest {
purpose,
length,
id,
expiration_date,
contexts,
types,
issuer,
} = req.into_inner();
let status_list = length
.map(|entries| StatusList2021::new(entries as usize))
.unwrap_or(Ok(StatusList2021::default()))
.map_err(|_| Error::InvalidStatusListLength)?;

let mut builder = StatusList2021CredentialBuilder::new(status_list);
for ctx in contexts {
let url = Url::parse(&ctx).map_err(move |_| Error::InvalidContext(ctx))?;
builder = builder.context(Context::Url(url));
}
for t in types {
builder = builder.add_type(t);
}
let issuer = Url::parse(&issuer)
.map_err(move |_| Error::InvalidIssuer(issuer))
.map(Issuer::Url)?;
builder = builder.issuer(issuer);
builder = builder.purpose(StatusPurpose::from(Purpose::try_from(purpose).unwrap()));
if let Some(exp) = expiration_date {
let exp = Timestamp::parse(&exp).map_err(move |_| Error::InvalidTimestamp(exp))?;
builder = builder.expiration_date(exp);
}
if let Some(id) = id {
let id = Url::parse(&id).map_err(move |_| Error::InvalidId(id))?;
builder = builder.subject_id(id);
}
let status_list_credential = builder.build().map_err(Error::CredentialError)?;
let res = StatusListCredential {
credential_json: status_list_credential.to_json().unwrap(),
};

Ok(Response::new(res))
}

#[tracing::instrument(
name = "update_status_list_credential",
skip_all,
fields(request = ?req.get_ref())
ret,
err,
)]
async fn update(&self, req: Request<UpdateRequest>) -> Result<Response<StatusListCredential>, Status> {
let UpdateRequest {
credential_json,
entries,
} = req.into_inner();
let mut status_list_credential =
StatusList2021Credential::from_json(&credential_json).map_err(Error::CredentialDeserializationError)?;

status_list_credential
.update(move |status_list| {
for (idx, value) in entries {
if let Err(e) = status_list.set_entry(idx as usize, value) {
return Err(e);
}
}

Ok(())
})
.map_err(Error::StatusListError)?;

Ok(Response::new(StatusListCredential {
credential_json: status_list_credential.to_json().unwrap(),
}))
}
}

pub fn service() -> StatusList2021SvcServer<StatusList2021Service> {
StatusList2021SvcServer::new(StatusList2021Service)
}
5 changes: 4 additions & 1 deletion bindings/grpc/tests/api/credential_validation.rs
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,10 @@ async fn credential_validation() -> anyhow::Result<()> {

let mut grpc_client = VcValidationClient::connect(server.endpoint()).await?;
let decoded_cred = grpc_client
.validate(VcValidationRequest { credential_jwt })
.validate(VcValidationRequest {
credential_jwt,
status_list_credential_json: None,
})
.await?
.into_inner()
.credential_json;
Expand Down
1 change: 1 addition & 0 deletions bindings/grpc/tests/api/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,3 +5,4 @@ mod health_check;
mod helpers;
mod jwt;
mod sd_jwt_validation;
mod status_list_2021;
91 changes: 91 additions & 0 deletions bindings/grpc/tests/api/status_list_2021.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
use crate::helpers::TestServer;
use _status_list_2021::status_list2021_svc_client::StatusList2021SvcClient;
use _status_list_2021::CreateRequest;
use _status_list_2021::Purpose;
use _status_list_2021::UpdateRequest;
use identity_iota::core::FromJson;
use identity_iota::core::ToJson;
use identity_iota::core::Url;
use identity_iota::credential::status_list_2021::StatusList2021;
use identity_iota::credential::status_list_2021::StatusList2021Credential;
use identity_iota::credential::status_list_2021::StatusList2021CredentialBuilder;
use identity_iota::credential::status_list_2021::StatusPurpose;
use identity_iota::credential::Issuer;
use tonic::Request;

mod _status_list_2021 {
tonic::include_proto!("status_list_2021");
}

#[tokio::test]
async fn status_list_2021_credential_creation() -> anyhow::Result<()> {
let server = TestServer::new().await;

let id = Url::parse("http://example.com/credentials/status/1").unwrap();
let issuer = Issuer::Url(Url::parse("http://example.com/issuers/1").unwrap());
let status_list_credential = StatusList2021CredentialBuilder::new(StatusList2021::default())
.purpose(StatusPurpose::Revocation)
.subject_id(id.clone())
.issuer(issuer.clone())
.build()
.unwrap();

let mut grpc_client = StatusList2021SvcClient::connect(server.endpoint()).await?;
let res = grpc_client
.create(Request::new(CreateRequest {
id: Some(id.into_string()),
issuer: issuer.url().to_string(),
purpose: Purpose::Revocation as i32,
length: None,
expiration_date: None,
contexts: vec![],
types: vec![],
}))
.await?
.into_inner()
.credential_json;
let grpc_credential = StatusList2021Credential::from_json(&res)?;

assert_eq!(status_list_credential, grpc_credential);
Ok(())
}

#[tokio::test]
async fn status_list_2021_credential_update() -> anyhow::Result<()> {
let server = TestServer::new().await;

let id = Url::parse("http://example.com/credentials/status/1").unwrap();
let issuer = Issuer::Url(Url::parse("http://example.com/issuers/1").unwrap());
let mut status_list_credential = StatusList2021CredentialBuilder::new(StatusList2021::default())
.purpose(StatusPurpose::Revocation)
.subject_id(id)
.issuer(issuer)
.build()
.unwrap();

let entries_to_set = [0_u64, 42, 420, 4200];
let entries = entries_to_set.iter().map(|i| (*i, true)).collect();

let mut grpc_client = StatusList2021SvcClient::connect(server.endpoint()).await?;
let grpc_credential = grpc_client
.update(Request::new(UpdateRequest {
credential_json: status_list_credential.to_json().unwrap(),
entries,
}))
.await
.map(|res| res.into_inner().credential_json)
.map(|credential_json| StatusList2021Credential::from_json(&credential_json).unwrap())
.unwrap();

status_list_credential.update(|status_list| {
for idx in entries_to_set {
if let Err(e) = status_list.set_entry(idx as usize, true) {
return Err(e);
}
}
Ok(())
})?;

assert_eq!(status_list_credential, grpc_credential);
Ok(())
}
Loading

0 comments on commit d6e0e85

Please sign in to comment.