Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Bring about a plugin-oriented generic server #54

Merged
merged 16 commits into from
Oct 10, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
25 changes: 18 additions & 7 deletions .github/workflows/rust.yml
Original file line number Diff line number Diff line change
Expand Up @@ -22,12 +22,23 @@ jobs:
- name: Build and Test Did Utils
run: |
cd did-utils
cargo build --verbose
cargo test --verbose
cargo build
cargo test

- name: Build and Test Sample Pop Server
- name: Build and Test Did Endpoint
run: |
cd sample-pop-server
cargo build --verbose
cargo run --bin didgen
cargo test --verbose
cd did-endpoint
cargo build
cargo test

- name: Build and Test Generic Server
run: |
cd generic-server
cargo build
cargo test

- name: Build and Mediator Server
run: |
cd mediator-server
cargo build
cargo test
2 changes: 2 additions & 0 deletions did-endpoint/.env
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
STORAGE_DIRPATH="target/storage"
SERVER_PUBLIC_DOMAIN="example.com"
28 changes: 28 additions & 0 deletions did-endpoint/Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
[package]
name = "did-endpoint"
version = "0.1.0"
edition = "2021"

# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html

[dependencies]
axum = { version = "0.6.20" }
chrono = { version = "0.4.26" }
did-utils = { path = "../did-utils"}
dotenv-flow = "0.15.0"
hyper = { version = "0.14.27", features = ["full"] }
multibase = { version = "0.8.0" } # earlier version due to 'did-utils'
serde_json = "1.0.104"
thiserror = "1.0.49"
tokio = { version = "1.30.0", features = ["full"] }
tracing = "0.1.37"
url = { version = "2.4.0" }
uuid = { version = "1.4.1", features = ["v4"] }
zeroize = { version = "1.6.0" }

# Plugins traits
server-plugin = { path = "../server-plugin" }

[dev-dependencies]
json-canon = "0.1.3"
tower = { version = "0.4.13", features = ["util"] }
328 changes: 328 additions & 0 deletions did-endpoint/src/didgen.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,328 @@
use crate::util::{didweb, KeyStore};
use did_utils::{
didcore::{
AssertionMethod, Authentication, Document, Jwk, KeyAgreement, KeyFormat, Service,
VerificationMethod,
},
ldmodel::Context,
};
use std::path::Path;

#[derive(Debug, thiserror::Error)]
pub enum Error {
#[error("KeyGenerationError")]
KeyGenerationError,
#[error("MissingServerPublicDomain")]
MissingServerPublicDomain,
#[error("DidAddressDerivationError")]
DidAddressDerivationError,
#[error("PersistenceError")]
PersistenceError,
#[error("Generic: {0}")]
Generic(String),
}

/// Generates keys and forward them for DID generation
///
/// All persistence is handled at `storage_dirpath`.
pub fn didgen(storage_dirpath: &str, server_public_domain: &str) -> Result<Document, Error> {
// Create a new store, which is timestamp-aware
let mut store = KeyStore::new(storage_dirpath);
tracing::info!("keystore: {}", store.path());

// Generate authentication key
tracing::debug!("generating authentication key");
let authentication_key = store
.gen_ed25519_jwk()
.map_err(|_| Error::KeyGenerationError)?;

// Generate assertion key
tracing::debug!("generating assertion key");
let assertion_key = store
.gen_ed25519_jwk()
.map_err(|_| Error::KeyGenerationError)?;

// Generate agreement key
tracing::debug!("generating agreement key");
let agreement_key = store
.gen_x25519_jwk()
.map_err(|_| Error::KeyGenerationError)?;

// Build DID document
let diddoc = gen_diddoc(
storage_dirpath,
server_public_domain,
authentication_key,
assertion_key,
agreement_key,
)?;

// Mark successful completion
tracing::debug!("successful completion");
Ok(diddoc)
}

/// Builds and persists DID document
fn gen_diddoc(
storage_dirpath: &str,
server_public_domain: &str,
authentication_key: Jwk,
assertion_key: Jwk,
agreement_key: Jwk,
) -> Result<Document, Error> {
tracing::info!("building DID document");

// Prepare DID address

let did = didweb::url_to_did_web_id(server_public_domain)
.map_err(|_| Error::DidAddressDerivationError)?;

// Prepare authentication verification method

let authentication_method = VerificationMethod {
public_key: Some(KeyFormat::Jwk(authentication_key)),
..VerificationMethod::new(
did.clone() + "#keys-1",
String::from("JsonWebKey2020"),
did.clone(),
)
};

// Prepare assertion verification method

let assertion_method = VerificationMethod {
public_key: Some(KeyFormat::Jwk(assertion_key)),
..VerificationMethod::new(
did.clone() + "#keys-2",
String::from("JsonWebKey2020"),
did.clone(),
)
};

// Prepare key agreement verification method

let agreement_method = VerificationMethod {
public_key: Some(KeyFormat::Jwk(agreement_key)),
..VerificationMethod::new(
did.clone() + "#keys-3",
String::from("JsonWebKey2020"),
did.clone(),
)
};

// Prepare service endpoint

let service = Service::new(
did.clone() + "#pop-domain",
String::from("LinkedDomains"),
format!("{server_public_domain}/.well-known/did/pop.json"),
);

// Build document

let context = Context::SetOfString(vec![
String::from("https://www.w3.org/ns/did/v1"),
String::from("https://w3id.org/security/suites/jws-2020/v1"),
]);

let diddoc = Document {
authentication: Some(vec![Authentication::Reference(
authentication_method.id.clone(), //
)]),
assertion_method: Some(vec![AssertionMethod::Reference(
assertion_method.id.clone(), //
)]),
key_agreement: Some(vec![KeyAgreement::Reference(
agreement_method.id.clone(), //
)]),
verification_method: Some(vec![
authentication_method,
assertion_method,
agreement_method,
]),
service: Some(vec![service]),
..Document::new(context, did)
};

// Serialize and persist to file

let did_json = serde_json::to_string_pretty(&diddoc).unwrap();

std::fs::create_dir_all(storage_dirpath).map_err(|_| Error::PersistenceError)?;
std::fs::write(format!("{storage_dirpath}/did.json"), did_json)
.map_err(|_| Error::PersistenceError)?;

tracing::info!("persisted DID document to disk");
Ok(diddoc)
}

/// Validates the integrity of the persisted diddoc
pub fn validate_diddoc(storage_dirpath: &str) -> Result<(), String> {
// Validate that did.json exists

let didpath = format!("{storage_dirpath}/did.json");
if !Path::new(&didpath).exists() {
return Err(String::from("Missing did.json"));
};

// Validate that keystore exists

let store = KeyStore::latest(storage_dirpath);
if store.is_none() {
return Err(String::from("Missing keystore"));
}

// Validate that did.json matches keystore

let store = store.unwrap();

let diddoc: Document = match std::fs::read_to_string(didpath) {
Err(_) => return Err(String::from("Unreadable did.json")),
Ok(content) => {
serde_json::from_str(&content).map_err(|_| String::from("Unparseable did.json"))?
}
};

for method in diddoc.verification_method.unwrap_or(vec![]) {
let pubkey = method.public_key.ok_or(String::from("Missing key"))?;
let pubkey = match pubkey {
KeyFormat::Jwk(jwk) => jwk,
_ => return Err(String::from("Unsupported key format")),
};

store
.find_keypair(&pubkey)
.ok_or(String::from("Keystore mismatch"))?;
}

Ok(())
}

#[cfg(test)]
mod tests {
use super::*;
use crate::util::dotenv_flow_read;

fn setup() -> (String, String) {
let storage_dirpath = dotenv_flow_read("STORAGE_DIRPATH")
.map(|p| format!("{}/{}", p, uuid::Uuid::new_v4()))
.unwrap();

let server_public_domain = dotenv_flow_read("SERVER_PUBLIC_DOMAIN").unwrap();

(storage_dirpath, server_public_domain)
}

fn cleanup(storage_dirpath: &str) {
std::fs::remove_dir_all(storage_dirpath).unwrap();
}

// Verifies that the didgen function returns a DID document.
// Does not validate the DID document.
#[test]
fn test_didgen() {
let (storage_dirpath, server_public_domain) = setup();

let diddoc = didgen(&storage_dirpath, &server_public_domain).unwrap();
assert_eq!(diddoc.id, "did:web:example.com");

cleanup(&storage_dirpath);
}

// Produces did doc from keys and validate that corresponding verification methods are present.
#[test]
fn test_gen_diddoc() {
let (storage_dirpath, server_public_domain) = setup();

let authentication_key = Jwk {
key_id: None,
key_type: String::from("OKP"),
curve: String::from("Ed25519"),
x: Some(String::from(
"d75a980182b10ab2463c5b1be1b4d97e06ec21ebac8552059996bd962d77f259",
)),
y: None,
d: None,
};

let assertion_key = Jwk {
key_id: None,
key_type: String::from("OKP"),
curve: String::from("Ed25519"),
x: Some(String::from(
"d75a980182b10ab2463c5b1be1b4d97e06ec21ebac8552059996bd962d77f259",
)),
y: None,
d: None,
};

let agreement_key = Jwk {
key_id: None,
key_type: String::from("OKP"),
curve: String::from("X25519"),
x: Some(String::from(
"d75a980182b10ab2463c5b1be1b4d97e06ec21ebac8552059996bd962d77f259",
)),
y: None,
d: None,
};

let diddoc = gen_diddoc(
&storage_dirpath,
&server_public_domain,
authentication_key.clone(),
assertion_key.clone(),
agreement_key.clone(),
)
.unwrap();

// Verify that the DID contains exactly the defined verification methods.
let expected_verification_methods = vec![
VerificationMethod {
id: "did:web:example.com#keys-1".to_string(),
public_key: Some(KeyFormat::Jwk(authentication_key)),
..VerificationMethod::new(
"did:web:example.com#keys-1".to_string(),
String::from("JsonWebKey2020"),
"did:web:example.com".to_string(),
)
},
VerificationMethod {
id: "did:web:example.com#keys-2".to_string(),
public_key: Some(KeyFormat::Jwk(assertion_key)),
..VerificationMethod::new(
"did:web:example.com#keys-2".to_string(),
String::from("JsonWebKey2020"),
"did:web:example.com".to_string(),
)
},
VerificationMethod {
id: "did:web:example.com#keys-3".to_string(),
public_key: Some(KeyFormat::Jwk(agreement_key)),
..VerificationMethod::new(
"did:web:example.com#keys-3".to_string(),
String::from("JsonWebKey2020"),
"did:web:example.com".to_string(),
)
},
];

let actual_verification_methods = diddoc.verification_method.unwrap();

let actual = json_canon::to_string(&actual_verification_methods).unwrap();
let expected = json_canon::to_string(&expected_verification_methods).unwrap();
assert_eq!(expected, actual);

cleanup(&storage_dirpath);
}

#[test]
fn test_validate_diddoc() {
let (storage_dirpath, server_public_domain) = setup();

didgen(&storage_dirpath, &server_public_domain).unwrap();
assert!(validate_diddoc(&storage_dirpath).is_ok());

cleanup(&storage_dirpath);
}
}
5 changes: 5 additions & 0 deletions did-endpoint/src/lib.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
pub mod didgen;
pub mod web;
pub mod plugin;

mod util;
Loading
Loading