-
Notifications
You must be signed in to change notification settings - Fork 90
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Credential revocation status check + basic test
- Loading branch information
Showing
11 changed files
with
469 additions
and
89 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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); | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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(()) | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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)) | ||
} |
This file was deleted.
Oops, something went wrong.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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() | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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(()) | ||
} |
Oops, something went wrong.