SpruceID
Search…
⌃K
Links

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_keys 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.