siopv2

Crates.iosiopv2
lib.rssiopv2
version0.1.0
sourcesrc
created_at2023-06-12 12:16:55.933545
updated_at2023-06-12 12:16:55.933545
descriptionRust implementation for the OpenID Connect Self-Issued OpenID Provider v2 standard
homepagehttps://www.impierce.com/
repositoryhttps://github.com/impierce/openid4vc
max_upload_size
id888134
size96,956
Nander Stabel (nanderstabel)

documentation

README

openid4vc

This library aims to support all the specifications under the OpenID for Verifiable Credentials works.

OpenID for Verifiable Credentials (OID4VC) consists of the following specifications:

Description

Currently the Implicit Flow is consists of four major parts:

  • A Provider that can accept a AuthorizationRequest and generate a AuthorizationResponse by creating an IdToken, adding its key identifier to the header of the id_token, signing the id_token and wrap it into a AuthorizationResponse. It can also send the AuthorizationResponse using the redirect_uri parameter.
  • A RelyingParty struct which can validate a AuthorizationResponse by validating its IdToken using a key identifier (which is extracted from the id_token) and its public key.
  • The Subject trait can be implemented on a custom struct representing the signing logic of a DID method. A Provider can ingest an object that implements the Subject trait so that during generation of a AuthorizationResponse the DID method syntax, key identifier and signing method of the specific Subject can be used.
  • The Validator trait can be implemented on a custom struct representing the validating logic of a DID method. When ingested by a RelyingParty, it can resolve the public key that is needed for validating an IdToken.

Example

use anyhow::Result;
use async_trait::async_trait;
use chrono::{Duration, Utc};
use ed25519_dalek::{Keypair, Signature, Signer};
use lazy_static::lazy_static;
use siopv2::{
    claims::{ClaimRequests, ClaimValue, IndividualClaimRequest},
    request::ResponseType,
    Provider, Registration, RelyingParty, RequestUrl, AuthorizationResponse, Scope, AuthorizationRequest, StandardClaims, Subject, Validator,
};
use rand::rngs::OsRng;
use wiremock::{
    http::Method,
    matchers::{method, path},
    Mock, MockServer, ResponseTemplate,
};

lazy_static! {
    pub static ref MOCK_KEYPAIR: Keypair = Keypair::generate(&mut OsRng);
}

// A Subject type that can be ingested by a Provider
#[derive(Default)]
pub struct MySubject;

impl MySubject {
    pub fn new() -> Self {
        MySubject {}
    }
}

#[async_trait]
impl Subject for MySubject {
    fn did(&self) -> Result<did_url::DID> {
        Ok(did_url::DID::parse("did:mymethod:subject")?)
    }

    fn key_identifier(&self) -> Option<String> {
        Some("key_identifier".to_string())
    }

    async fn sign<'a>(&self, message: &'a str) -> Result<Vec<u8>> {
        let signature: Signature = MOCK_KEYPAIR.sign(message.as_bytes());
        Ok(signature.to_bytes().to_vec())
    }
}

#[async_trait]
impl Validator for MySubject {
    async fn public_key<'a>(&self, _kid: &'a str) -> Result<Vec<u8>> {
        Ok(MOCK_KEYPAIR.public.to_bytes().to_vec())
    }
}

// A Validator type that can be ingested by a RelyingParty
#[derive(Default)]
pub struct MyValidator;

#[async_trait]
impl Validator for MyValidator {
    async fn public_key<'a>(&self, _kid: &'a str) -> Result<Vec<u8>> {
        Ok(MOCK_KEYPAIR.public.to_bytes().to_vec())
    }
}

#[tokio::main]
async fn main() {
    // Create a new mock server and retreive it's url.
    let mock_server = MockServer::start().await;
    let server_url = mock_server.uri();

    // Create a new validator.
    let validator = MySubject::default();

    // Create a new relying party.
    let relying_party = RelyingParty::new(validator);

    // Create a new RequestUrl with response mode `post` for cross-device communication.
    let request: AuthorizationRequest = RequestUrl::builder()
        .response_type(ResponseType::IdToken)
        .client_id("did:mymethod:relyingparty".to_string())
        .scope(Scope::openid())
        .redirect_uri(format!("{server_url}/redirect_uri"))
        .response_mode("post".to_string())
        .registration(
            Registration::default()
                .with_subject_syntax_types_supported(vec!["did:mymethod".to_string()])
                .with_id_token_signing_alg_values_supported(vec!["EdDSA".to_string()]),
        )
        .claims(ClaimRequests {
            id_token: Some(StandardClaims {
                name: Some(IndividualClaimRequest::default()),
                ..Default::default()
            }),
            ..Default::default()
        })
        .exp((Utc::now() + Duration::minutes(10)).timestamp())
        .nonce("n-0S6_WzA2Mj".to_string())
        .build()
        .and_then(TryInto::try_into)
        .unwrap();

    // Create a new `request_uri` endpoint on the mock server and load it with the JWT encoded `AuthorizationRequest`.
    Mock::given(method("GET"))
        .and(path("/request_uri"))
        .respond_with(ResponseTemplate::new(200).set_body_string(relying_party.encode(&request).await.unwrap()))
        .mount(&mock_server)
        .await;

    // Create a new `redirect_uri` endpoint on the mock server where the `Provider` will send the `AuthorizationResponse`.
    Mock::given(method("POST"))
        .and(path("/redirect_uri"))
        .respond_with(ResponseTemplate::new(200))
        .mount(&mock_server)
        .await;

    // Create a new subject.
    let subject = MySubject::default();

    // Create a new provider.
    let provider = Provider::new(subject).await.unwrap();

    // Create a new RequestUrl which includes a `request_uri` pointing to the mock server's `request_uri` endpoint.
    let request_url = RequestUrl::builder()
        .request_uri(format!("{server_url}/request_uri"))
        .build()
        .unwrap();

    // The Provider obtains the reuquest url either by a deeplink or by scanning a QR code. It then validates the
    // request. Since in this case the request is a JWT, the provider will fetch the request by sending a GET
    // request to mock server's `request_uri` endpoint.
    let request = provider.validate_request(request_url).await.unwrap();

    // Assert that the request was successfully received by the mock server at the `request_uri` endpoint.
    let get_request = mock_server.received_requests().await.unwrap()[0].clone();
    assert_eq!(get_request.method, Method::Get);
    assert_eq!(get_request.url.path(), "/request_uri");

    // Let the provider generate a response based on the validated request. The response is an `IdToken` which is
    // encoded as a JWT.
    let response = provider
        .generate_response(
            request,
            StandardClaims {
                name: Some(ClaimValue("Jane Doe".to_string())),
                ..Default::default()
            },
        )
        .await
        .unwrap();

    // The provider sends it's response to the mock server's `redirect_uri` endpoint.
    provider.send_response(response).await.unwrap();

    // Assert that the AuthorizationResponse was successfully received by the mock server at the expected endpoint.
    let post_request = mock_server.received_requests().await.unwrap()[1].clone();
    assert_eq!(post_request.method, Method::Post);
    assert_eq!(post_request.url.path(), "/redirect_uri");
    let response: AuthorizationResponse = serde_urlencoded::from_bytes(post_request.body.as_slice()).unwrap();

    // The `RelyingParty` then validates the response by decoding the header of the id_token, by fetching the public
    // key corresponding to the key identifier and finally decoding the id_token using the public key and by
    // validating the signature.
    let id_token = relying_party.validate_response(&response).await.unwrap();
    assert_eq!(
        id_token.standard_claims(),
        &StandardClaims {
            name: Some(ClaimValue("Jane Doe".to_string())),
            ..Default::default()
        }
    );
}
Commit count: 27

cargo fmt