Flow
A
Flow
is defined in terms of Content
, Statement
, and Proof
(which is parameterized by the same Content
). The underlying Flow
implementation includes any information needed to validate the Proof, including things like api_key
s and max look up limits. A struct implementing Flow
is expected to be Deserializable
for use in WASM based libraries.#[async_trait(?Send)]
pub trait Flow<C, S, P>
where
C: Content,
S: Statement,
P: Proof<C>,
{
// NOTE: IMPLEMENTED VIA COMPOSITION
async fn credential<I: Issuer>(&self, proof: &P, issuer: &I) -> Result<Credential, FlowError> {
/* NOTE: THIS IS AUTOMATICALLY IMPLEMENTED! */
}
fn instructions(&self) -> Result<Instructions, FlowError>;
// NOTE: IMPLEMENTED VIA COMPOSITION
async fn jwt<I: Issuer>(&self, proof: &P, issuer: &I) -> Result<String, FlowError> {
/* NOTE: THIS IS AUTOMATICALLY IMPLEMENTED! */
}
async fn statement<I: Issuer>(&self, statement: &S, issuer: &I) -> Result<FlowResponse, FlowError>;
// NOTE: IMPLEMENTED VIA COMPOSITION
async fn unsigned_credential<Subj: Subject, I: Issuer>(
&self,
proof: &P,
subj: &Subj,
issuer: &I,
) -> Result<Credential, FlowError> {
/* NOTE: THIS IS AUTOMATICALLY IMPLEMENTED! */
}
async fn validate_proof<I: Issuer>(&self, proof: &P, issuer: &I) -> Result<C, FlowError>;
}
The
FlowResponse
struct returned by statement
looks like:#[derive(Deserialize, Serialize)]
pub struct FlowResponse {
pub statement: String,
pub delimitor: Option<String>,
}
With
delimitor
representing the string seperator for the statement
and signature
if the user is expected to post these things in a public place. In cases where the user doesn't post, or only posts a signature, delimitor is returned as None
.An implementor of
Flow
needs to provide three functions, the first, Instructions
is just a set of human readable instructions on how to get through the flow and some automatically derived JSON Schemas to help the clients send the right format back.Similarly
statement
often just takes the statement
arguement, calls statement.generate_statement()
and returns it. However, in some cases, like e-mail, the witness also generates a challenge at this point. Because we have access to the issuer at this stage, we can use it generate a cryptograpic challenge, then later validate it statelessly.Finally,
validate_proof
takes a proof and returns the proof's associated content assuming the proof is valid. Proofs can't check their own validity because in the case of things where API access is needed, every proof would have to contain a copy of an API key. Using the Flow
abstraction to validate the proofs, the Flow
struct can contain the API key. Additionally, if the Proof
passed as an argument here contians a challenge generated in the statement
step, it can be checked here using the same Issuer
.The implementor of
Flow
used to generate the second example credential looks like:#[async_trait(?Send)]
impl Flow<Ctnt, Stmt, Prf, PostResponse> for GitHubFlow {
fn instructions(&self) -> Result<Instructions, FlowError> {
Ok(Instructions {
statement: "Enter your GitHub account handle to verify and include in a signed message using your wallet.".to_string(),
statement_schema: schema_for!(Stmt),
signature: "Sign the message presented to you containing your email address and additional information.".to_string(),
witness: "Find the email sent from the witness and copy the code and challenge into the respective form fields.".to_string(),
witness_schema: schema_for!(Prf)
})
}
async fn statement<I: Issuer>(
&self,
statement: &Stmt,
_issuer: &I,
) -> Result<PostResponse, FlowError> {
Ok(PostResponse {
delimitor: self.delimitor.to_owned(),
statement: statement.generate_statement()?,
})
}
async fn validate_proof<I: Issuer>(&self, proof: &Prf, _issuer: &I) -> Result<Ctnt, FlowError> {
let client = Client::new();
let request_url = format!("https://api.github.com/gists/{}", proof.gist_id);
let re = Regex::new(r"^[a-zA-Z0-9]{32}$")
.map_err(|_| FlowError::BadLookup("could not generate gist id regex".to_string()))?;
if !re.is_match(&proof.gist_id) {
return Err(FlowError::BadLookup("gist id invalid".to_string()));
}
let mut headers = HeaderMap::new();
headers.insert(
USER_AGENT,
format!("{}", self.user_agent).parse().map_err(|_| {
FlowError::BadLookup("could not generate header for lookup".to_string())
})?,
);
let res: GitHubResponse = client
.get(Url::parse(&request_url).map_err(|e| FlowError::BadLookup(e.to_string()))?)
.headers(headers)
.send()
.await
.map_err(|e| FlowError::BadLookup(e.to_string()))?
.json()
.await
.map_err(|e| FlowError::BadLookup(e.to_string()))?;
if proof.statement.handle.to_lowercase() != res.owner.login.to_lowercase() {
return Err(FlowError::BadLookup(format!(
"handle mismatch, expected: {}, got: {}",
proof.statement.handle.to_lowercase(),
res.owner.login.to_lowercase()
)));
};
let s = serde_json::to_string(&res.files)
.map_err(|e| FlowError::BadLookup(e.to_string()))?;
for (_k, v) in res.files {
let object = match v.as_object() {
None => continue,
Some(x) => x,
};
let str_val = match object.get("content") {
None => continue,
Some(x) => x,
};
let p = match str_val.as_str() {
None => continue,
Some(x) => x,
};
let mut a = p.split(&self.delimitor);
let txt = a.next();
let txt_sig = a.next();
match (txt, txt_sig) {
(Some(stmt), Some(sig)) => {
proof.statement.subject.valid_signature(stmt, sig).await?;
return Ok(proof.to_content(stmt, sig)?)
}
_ => continue
}
}
Err(FlowError::BadLookup(
format!("Failed to find files in: {}", s),
))
}
}
This implementation of
validate_proof
looks up the gist, validates the username matches the name in the statement, validates the signature is the statement signed by the subject. It uses it's own internal delimitor
property to split the gists to find statment and signature. Similiarly internal properties like api_key
or email_address
can be used to handle all sort of flows.Some flows don't even require a post, like the email flow, which statelessly creates and validates challenges to prove email address ownership.
Last modified 5mo ago