Skip to content

Commit

Permalink
Credential revocation status check + basic test
Browse files Browse the repository at this point in the history
  • Loading branch information
UMR1352 committed Nov 24, 2023
1 parent ca11ee8 commit dd1cb11
Show file tree
Hide file tree
Showing 11 changed files with 469 additions and 89 deletions.
7 changes: 6 additions & 1 deletion bindings/grpc/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,12 @@ prost = "0.12"
tokio = { version = "1.0", features = ["macros", "rt-multi-thread"] }
anyhow = "1.0.75"
tokio-stream = { version = "0.1.14", features = ["net"] }
identity_credential = { path = "../../identity_credential" }
identity_iota = { path = "../../identity_iota", features = ["resolver"] }
identity_storage = { path = "../../identity_storage", features = ["memstore"] }
iota-sdk = { version = "1.1.2", features = ["stronghold"] }
serde_json = { version = "1.0.108", features = ["alloc"] }
thiserror = "1.0.50"
rand = "0.8.5"

[build-dependencies]
tonic-build = "0.10"
52 changes: 10 additions & 42 deletions bindings/grpc/proto/credentials.proto
Original file line number Diff line number Diff line change
@@ -1,56 +1,24 @@
syntax = "proto3";
package credentials;

enum SubjectHolderRelationshipType {
ALWAYS_SUBJECT = 0;
SUBJECT_ON_NON_TRANSFERABLE = 1;
ANY = 2;
}

message SubjectHolderRelationship {
SubjectHolderRelationshipType type = 1;
string url = 2;
}

message CVOptions {
string expiry_date = 1;
string issuance_date = 2;
string status = 3;
SubjectHolderRelationship subject_holder_relationship = 4;
// TODO: Add JwsVerificationOptions
}

message CVRequest {
string jwt = 1;
CVOptions verification_options = 2;
}

enum CVErrorType {
JWS_DECODING_ERROR = 0;
}

message CVError {
CVErrorType type = 1;
string msg = 2;
}
// -- CREDENTIALS REVOCATION ---------------------------------------------

enum RevocationStatus {
REVOKED = 0;
SUSPENDED = 1;
VALID = 2;
}

message CVResponseSuccess {
string credential = 1;
RevocationStatus status = 2;
message RevocationCheckRequest {
string type = 1;
string url = 2;
map<string, string> properties = 3;
}

message CVResponse {
oneof response {
CVResponseSuccess success = 1;
CVError error = 2;
}
message RevocationCheckResponse {
RevocationStatus status = 1;
}

service CredentialVerification {
rpc VerifyCredential(CVRequest) returns (CVResponse);
service CredentialRevocation {
rpc check(RevocationCheckRequest) returns (RevocationCheckResponse);
}
19 changes: 12 additions & 7 deletions bindings/grpc/src/main.rs
Original file line number Diff line number Diff line change
@@ -1,15 +1,20 @@
use anyhow::Result;
use tonic::transport::Server;
use identity_grpc::server::GRpcServer;
use iota_sdk::client::Client;

use identity_grpc::services;
const API_ENDPOINT: &str = "http://127.0.0.1:14265";

#[tokio::main]
async fn main() -> Result<()> {
let addr = "[::1]:50051".parse()?;
async fn main() -> anyhow::Result<()> {
let client: Client = Client::builder()
.with_primary_node(API_ENDPOINT, None)?
.finish()
.await?;

let addr = "127.0.0.1:50051".parse()?;
println!("gRPC server listening on {}", addr);

Server::builder().add_routes(services::routes()).serve(addr).await?;
GRpcServer::new(client)
.serve(addr)
.await?;

Ok(())
}
23 changes: 21 additions & 2 deletions bindings/grpc/src/server.rs
Original file line number Diff line number Diff line change
@@ -1,7 +1,26 @@
use std::net::SocketAddr;

use tonic::transport::server::{Router, Server};
use iota_sdk::client::Client;

use crate::services;

pub fn make_server() -> Router {
Server::builder().add_routes(services::routes())
#[derive(Debug)]
pub struct GRpcServer {
router: Router,
}

impl GRpcServer {
pub fn new(client: Client) -> Self {
let router = Server::builder().add_routes(services::routes(client));
Self {
router,
}
}
pub async fn serve(self, addr: SocketAddr) -> Result<(), tonic::transport::Error> {
self.router.serve(addr).await
}
pub fn into_router(self) -> Router {
self.router
}
}
114 changes: 114 additions & 0 deletions bindings/grpc/src/services/credential.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,114 @@
use credential_verification::{
credential_revocation_server::{CredentialRevocation, CredentialRevocationServer},
RevocationCheckRequest, RevocationCheckResponse, RevocationStatus,
};
use identity_iota::{
credential::{JwtCredentialValidatorUtils, JwtValidationError, RevocationBitmapStatus, self},
prelude::{IotaDocument, Resolver},
};
use iota_sdk::client::Client;

use thiserror::Error;
use tonic::{self, Request, Response};

mod credential_verification {
use super::RevocationCheckError;
use identity_iota::credential::{RevocationBitmapStatus, Status};

tonic::include_proto!("credentials");

impl TryFrom<RevocationCheckRequest> for Status {
type Error = RevocationCheckError;
fn try_from(req: RevocationCheckRequest) -> Result<Self, Self::Error> {
use identity_iota::core::{Object, Url};

if req.r#type.as_str() != RevocationBitmapStatus::TYPE {
Err(Self::Error::UnknownRevocationType(req.r#type))
} else {
let parsed_url = req
.url
.parse::<Url>()
.map_err(|_| Self::Error::InvalidRevocationUrl(req.url))?;
let properties = req
.properties
.into_iter()
.map(|(k, v)| serde_json::to_value(v).map(|v| (k, v)))
.collect::<Result<Object, _>>()
.map_err(|_| Self::Error::MalformedPropertiesObject)?;

Ok(Status {
id: parsed_url,
type_: req.r#type,
properties,
})
}
}
}
}

#[derive(Debug, Error)]
pub enum RevocationCheckError {
#[error("Unknown revocation type {0}")]
UnknownRevocationType(String),
#[error("Could not parse {0} into a valid URL")]
InvalidRevocationUrl(String),
#[error("Properties isn't a valid JSON object")]
MalformedPropertiesObject,
}

impl From<RevocationCheckError> for tonic::Status {
fn from(e: RevocationCheckError) -> Self {
Self::from_error(Box::new(e))
}
}

#[derive(Debug)]
pub struct CredentialVerifier {
resolver: Resolver<IotaDocument>,
}

impl CredentialVerifier {
pub fn new(client: &Client) -> Self {
let mut resolver = Resolver::new();
resolver.attach_iota_handler(client.clone());
Self { resolver }
}
}

#[tonic::async_trait]
impl CredentialRevocation for CredentialVerifier {
async fn check(
&self,
req: Request<RevocationCheckRequest>,
) -> Result<Response<RevocationCheckResponse>, tonic::Status> {
let credential_revocation_status = {
let revocation_status = credential::Status::try_from(req.into_inner())?;
RevocationBitmapStatus::try_from(revocation_status).map_err(|e| tonic::Status::from_error(Box::new(e)))?
};
let issuer_did = credential_revocation_status.id().unwrap(); // Safety: already parsed as a valid URL
let issuer_doc = self
.resolver
.resolve(issuer_did.did())
.await
.map_err(|e| tonic::Status::from_error(Box::new(e)))?;

if let Err(e) =
JwtCredentialValidatorUtils::check_revocation_bitmap_status(&issuer_doc, credential_revocation_status)
{
match &e {
JwtValidationError::Revoked => Ok(Response::new(RevocationCheckResponse {
status: RevocationStatus::Revoked.into(),
})),
_ => Err(tonic::Status::from_error(Box::new(e))),
}
} else {
Ok(Response::new(RevocationCheckResponse {
status: RevocationStatus::Valid.into(),
}))
}
}
}

pub fn service(client: &Client) -> CredentialRevocationServer<CredentialVerifier> {
CredentialRevocationServer::new(CredentialVerifier::new(client))
}
31 changes: 0 additions & 31 deletions bindings/grpc/src/services/credential_verification.rs

This file was deleted.

7 changes: 4 additions & 3 deletions bindings/grpc/src/services/mod.rs
Original file line number Diff line number Diff line change
@@ -1,12 +1,13 @@
mod health_check;
mod credential_verification;
mod credential;

use iota_sdk::client::Client;
use tonic::transport::server::{Routes, RoutesBuilder};

pub fn routes() -> Routes {
pub fn routes(client: Client) -> Routes {
let mut routes = RoutesBuilder::default();
routes.add_service(health_check::service());
routes.add_service(credential_verification::service());
routes.add_service(credential::service(&client));

routes.routes()
}
60 changes: 60 additions & 0 deletions bindings/grpc/tests/api/credential_revocation_check.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
use credentials::{credential_revocation_client::CredentialRevocationClient, RevocationStatus};
use identity_iota::{
credential::{self, RevocationBitmap, RevocationBitmapStatus, StatusCheck},
did::DID,
};

use crate::{
credential_revocation_check::credentials::RevocationCheckRequest,
helpers::{Entity, TestServer},
};

mod credentials {
tonic::include_proto!("credentials");
}

#[tokio::test]
async fn checking_status_of_valid_credential_works() -> anyhow::Result<()> {
let server = TestServer::new().await;
let client = server.client();
let mut issuer = Entity::new();
issuer.create_did(client).await?;

let mut subject = Entity::new();
subject.create_did(client).await?;

let service_id = issuer
.document()
.unwrap() // Safety: `create_did` didn't fail
.id()
.to_url()
.join("#my-revocation-service")?;

// Add a revocation service to the issuer's DID document
issuer
.update_document(client, |mut doc| {
let service = RevocationBitmap::new().to_service(service_id.clone()).unwrap();

doc.insert_service(service).ok().map(|_| doc)
})
.await?;

let credential_status: credential::Status = RevocationBitmapStatus::new(service_id, 3).into();

let mut grpc_client = CredentialRevocationClient::connect(server.endpoint()).await?;
let req = RevocationCheckRequest {
r#type: credential_status.type_,
url: credential_status.id.into_string(),
properties: credential_status
.properties
.into_iter()
.map(|(k, v)| (k, v.to_string().trim_matches('"').to_owned()))
.collect(),
};
dbg!(&req);
let res = grpc_client.check(tonic::Request::new(req)).await?.into_inner();

assert_eq!(res.status(), RevocationStatus::Valid);

Ok(())
}
Loading

0 comments on commit dd1cb11

Please sign in to comment.