DNS Witness Flow Schema

This example shows how to create a Rust application with rebase that uses TreeLDR to define a simple DNS witness flow VC. The program will consist of:

  • a TreeLDR schema defining the credential type,

  • an implementation of Rebase's SchemaType trait for the credential schema.

Project Creation

First, create a new Rust project using cargo:

cargo new dns-example
cd dns-example

The src directory will contain the sources of our application. For now, it only contains a main.rs file with a dummy main function definition.

Credential Definition in TreeLDR

Create a new src/dns.tldr file that will contain the VC definition:

base <https://example.com/>;
use <http://www.w3.org/2000/01/rdf-schema#> as rdfs;
use <http://www.w3.org/2001/XMLSchema#> as xs;
use <https://treeldr.org/> as tldr;
use <https://www.w3.org/2018/credentials#> as vc;

/// DNS Verification Message.
type DnsVerificationMessage {
	// Include the type inside the layout as `@type`.
	// This will force TreeLDR to generate a type scoped context
	// for `DnsVerificationMessage` when generating the
	// JSON-LD context.
	rdf:type as @type: required multiple &rdfs:Class,

	timestamp: required xs:dateTime,
	dnsServer: required xs:string
}

/// DNS Verification Subject.
layout DnsVerificationSubject for rdfs:Resource {
	tldr:self as id: required &rdfs:Resource,

	schema:sameAs: &rdfs:Resource
}

/// DNS Verification VC.
type DnsVerification =
	vc:VerifiableCredential &
	all vc:credentialSubject: (rdfs:Resource with DnsVerificationSubject) &
	all vc:evidence: DnsVerificationMessage;

The DnsVerification type is a vc:VerifiableCredential where the credential subject is any resource represented with the DnsVerificationSubject layout, and evidences are any DnsVerificationMessage.

Instead of defining the vc:VerifiableCredential type ourselves in this file, we will rely on an example file provided in the TreeLDR repository that contains VC type definitions. In the terminal, use the following command to download this file as src/vc.tldr:

curl https://raw.githubusercontent.com/spruceid/treeldr/main/examples/vc.tldr > src/vc.tldr

Credential Definition in TreeLDR

We will embed the previous schema definition into the Rust program as a type definition using the #[tldr] procedural macro attribute provided by the treeldr-rust-macros crate. This crate rely on the treeldr-rust-prelude crate. First, add those two crates to the dependencies of the Rust program by adding the following lines to the Cargo.toml file under the [dependencies] section:

treeldr-rust-macros = { git = "https://github.com/spruceid/treeldr.git" }
treeldr-rust-prelude = { git = "https://github.com/spruceid/treeldr.git" }

Then add the following module definition annotated with the #[tldr] procedural macro attribute to the src/main.rs file:

#[tldr(
	"src/dns.tldr"
)]
mod schema {
	#[prefix("http://www.w3.org/2001/XMLSchema#")]
	pub mod xs {}
	
	#[prefix("https://www.w3.org/2018/credentials#")]
	pub mod vc {}

	#[prefix("https://example.com/")]
	pub mod example {}
}

At compile time, this macro call will expand to add the following two type definitions in the schema::example module:

pub struct DnsVerificationMessage {
	type_: BTreeSet<treeldr_rust_prelude::Id>,
	timestamp: super::xs::DateTime,
	dns_server: super::xs::String
}

pub struct DnsVerificationSubject {
	id: treeldr_rust_prelude::Id,
	same_as: Option<treeldr_rust_prelude::Id>
}

Rebase's SchemaType Trait Implementation

Contrarily to the "Basic Post" example, SchemaType will not directly be implemented for the credential subject type (here DnsVerificationMessage). Instead, we create a dedicated Schema type that will generate the credential subjects and evidence. Add the following items in the src/main.rs file:

use ssi::{one_or_many::OneOrMany, vc::Evidence};
use rebase::schema::schema_type::{SchemaError, SchemaType};
use rebase::signer::signer::DID as SignerDID;

pub struct Schema {
	pub domain: String,
	pub key_type: SignerDID,
}

impl SchemaType for Schema {
	fn context(&self) -> Result<serde_json::Value, SchemaError> {
		todo!()
	}

	fn subject(&self) -> Result<serde_json::Value, SchemaError> {
		todo!()
	}

	fn evidence(&self) -> Result<Option<OneOrMany<Evidence>>, SchemaError> {
		todo!()
	}

	fn types(&self) -> Result<Vec<String>, SchemaError> {
		Ok(vec![
			"VerifiableCredential".to_string(),
			"DnsVerification".to_string(),
		])
	}
}

We now need to fill-in the blanks for the context, subject and evidence methods.

context Implementation

We can then generate the associated JSON-LD context using the following command:

tldrc -i src/vc.tldr -i src/dns.tldr json-ld-context -c https://www.w3.org/2018/credentials/v1 https://example.com/DnsVerificationMessage

Using the output of the command, replace the todo!() in the context method with:

Ok(serde_json::json!([
	"https://www.w3.org/2018/credentials/v1",
	{
		"sameAs": "http://schema.org/sameAs",
		"DnsVerification": "https://example.com/DnsVerification",
		"DnsVerificationMessage": {
			"@id": "https://example.com/DnsVerificationMessage",
			"@context": {
				"timestamp": {
					"@id": "https://example.com/timestamp",
					"@type": "http://www.w3.org/2001/XMLSchema#dateTime"
				},
				"dnsServer": "https://example.com/dnsServer"
			}
		}
	}
]))

subject Implementation

The subject of the VC, a DnsVerifiactionSubject, is derived from the key type. We use schema::example::DnsVerificationSubject to build the subject and convert it into a serde_json::Value. Replace the todo!() in the subject method with:

use treeldr_rust_prelude::{Id, IntoJsonLd};

let signer_type = SignerTypes::new(&self.key_type)?;
let signer_did = signer_type
	.did_id()
	.map_err(|e| SchemaError::BadSubject(e.to_string()))?;

let mut subject = schema::example::DnsVerificationSubject::new(Id::try_from(signer_did).unwrap());
subject.same_as.insert(Id::try_from(format!("dns:{}",  self.domain)).unwrap());

Ok(subject.into_json_ld().into())

evidence Implementation

The evidence is a DnsVerificationMessage, however we will not be able to use the schema::example::DnsVerificationMessage type defined by TreeLDR since rebase expect an ssi::vc::Evidence instance. Instead we will closely follow the structure of a DnsVerificationMessage to build the evidence by hand. Replace the todo!() in the evidence method with:

let mut evidence_map = HashMap::new();

evidence_map.insert(
	"timestamp".to_string(),
	serde_json::Value::String(Utc::now().to_rfc3339_opts(SecondsFormat::Millis, true)),
);

evidence_map.insert(
	"dnsServer".to_string(),
	serde_json::Value::String("https://cloudflare-dns.com/dns-query".to_string()),
);

let evidence = Evidence {
	id: None,
	type_: vec!["DnsVerificationMessage".to_string()],
	property_set: Some(evidence_map),
};

Ok(Some(OneOrMany::One(evidence)))

Last updated