Skip to content

Commit

Permalink
Initial commit of ssi-sd-jwt
Browse files Browse the repository at this point in the history
  • Loading branch information
tristanmiller-spruceid committed Sep 11, 2023
1 parent db906df commit ce6c962
Show file tree
Hide file tree
Showing 9 changed files with 959 additions and 0 deletions.
1 change: 1 addition & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,7 @@ members = [
"ssi-dids",
"ssi-jws",
"ssi-jwt",
"ssi-sd-jwt",
"ssi-tzkey",
"ssi-ssh",
"ssi-ldp",
Expand Down
23 changes: 23 additions & 0 deletions ssi-sd-jwt/Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
[package]
name = "ssi-sd-jwt"
version = "0.1.0"
edition = "2021"
authors = ["Spruce Systems, Inc."]
license = "Apache-2.0"
description = "Implementation of SD-JWT for the ssi library."
repository = "https://github.com/spruceid/ssi/"
documentation = "https://docs.rs/ssi-sd-jwt/"

[dependencies]
jose-b64 = { version = "0.1", features = ["json"] }
rand = { version = "0.8" }
serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0"
sha2 = "0.10"
ssi-jwk = { path = "../ssi-jwk", version = "0.1" }
ssi-jws = { path = "../ssi-jws", version = "0.1" }
ssi-jwt = { path = "../ssi-jwt", version = "0.1" }
thiserror = "1.0"

[dev-dependencies]
hex-literal = "0.4.1"
197 changes: 197 additions & 0 deletions ssi-sd-jwt/src/decode.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,197 @@
use serde::de::DeserializeOwned;
use serde::Deserialize;
use ssi_jwk::JWK;
use ssi_jwt::NumericDate;
use std::collections::BTreeMap;

use crate::verify::{DecodedDisclosure, DisclosureKind};
use crate::*;

#[derive(Debug, Deserialize, PartialEq)]
pub struct ValidityClaims {
pub nbf: Option<NumericDate>,
pub iat: Option<NumericDate>,
pub exp: Option<NumericDate>,
}

pub fn decode_verify<Claims: DeserializeOwned>(
jwt: &str,
key: &JWK,
disclosures: &[&str],
) -> Result<(ValidityClaims, Claims), Error> {
let mut payload_claims: serde_json::Value = ssi_jwt::decode_verify(jwt, key)?;

let validity_claims: ValidityClaims = serde_json::from_value(payload_claims.clone())?;

let sd_alg = sd_alg(&payload_claims)?;
let _ = payload_claims
.as_object_mut()
.unwrap()
.remove(SD_ALG_CLAIM_NAME);

let mut disclosures = translate_to_in_progress_disclosures(disclosures, sd_alg)?;

visit_claims(&mut payload_claims, &mut disclosures)?;

Ok((validity_claims, serde_json::from_value(payload_claims)?))
}

fn sd_alg(claims: &serde_json::Value) -> Result<SdAlg, Error> {
let alg_name = claims[SD_ALG_CLAIM_NAME]
.as_str()
.ok_or(Error::MissingSdAlg)?;

SdAlg::try_from(alg_name)
}

fn translate_to_in_progress_disclosures(
disclosures: &[&str],
sd_alg: SdAlg,
) -> Result<BTreeMap<String, InProgressDisclosure>, Error> {
let disclosure_vec: Result<Vec<_>, Error> = disclosures
.iter()
.map(|disclosure| InProgressDisclosure::new(disclosure, sd_alg))
.collect();

let disclosure_vec = disclosure_vec?;

let mut disclosure_map = BTreeMap::new();
for disclosure in disclosure_vec {
let prev = disclosure_map.insert(disclosure.hash.clone(), disclosure);

if prev.is_some() {
return Err(Error::MultipleDisclosuresWithSameHash);
}
}

Ok(disclosure_map)
}

struct InProgressDisclosure {
decoded: DecodedDisclosure,
hash: String,
found: bool,
}

impl InProgressDisclosure {
fn new(disclosure: &str, sd_alg: SdAlg) -> Result<Self, Error> {
Ok(InProgressDisclosure {
decoded: DecodedDisclosure::new(disclosure)?,
hash: hash_encoded_disclosure(sd_alg, disclosure),
found: false,
})
}
}

fn visit_claims(
payload_claims: &mut serde_json::Value,
disclosures: &mut BTreeMap<String, InProgressDisclosure>,
) -> Result<(), Error> {
let payload_claims = match payload_claims.as_object_mut() {
Some(obj) => obj,
None => return Ok(()),
};

// Visit children
for (_, child_claim) in payload_claims.iter_mut() {
visit_claims(child_claim, disclosures)?
}

// Process _sd claim
let new_claims = if let Some(sd) = payload_claims[SD_CLAIM_NAME].as_array() {
decode_sd_claims(sd, disclosures)?
} else {
vec![]
};

if payload_claims.contains_key(SD_CLAIM_NAME) {
payload_claims.remove(SD_CLAIM_NAME);
}

for (new_claim_name, mut new_claim_value) in new_claims {
visit_claims(&mut new_claim_value, disclosures)?;

let prev = payload_claims.insert(new_claim_name, new_claim_value);

if prev.is_some() {
return Err(Error::DisclosureClaimCollidesWithJwtClaim);
}
}

// Process array claims
for (_, item) in payload_claims.iter_mut() {
if let Some(array) = item.as_array_mut() {
let new_array_items = decode_array_claims(array, disclosures)?;
*array = new_array_items;
}
}

Ok(())
}

fn decode_sd_claims(
sd_claims: &Vec<serde_json::Value>,
disclosures: &mut BTreeMap<String, InProgressDisclosure>,
) -> Result<Vec<(String, serde_json::Value)>, Error> {
let mut found_disclosures = vec![];
for disclosure_hash in sd_claims {
let disclosure_hash = disclosure_hash.as_str().ok_or(Error::SdClaimNotString)?;

if let Some(in_progress_disclosure) = disclosures.get_mut(disclosure_hash) {
if in_progress_disclosure.found {
return Err(Error::DisclosureUsedMultipleTimes);
}
in_progress_disclosure.found = true;
match in_progress_disclosure.decoded.kind {
DisclosureKind::ArrayItem(_) => {
return Err(Error::ArrayDisclosureWhenExpectingProperty)
}
DisclosureKind::Property {
ref name,
ref value,
} => found_disclosures.push((name.clone(), value.clone())),
}
}
}

Ok(found_disclosures)
}

fn decode_array_claims(
array: &[serde_json::Value],
disclosures: &mut BTreeMap<String, InProgressDisclosure>,
) -> Result<Vec<serde_json::Value>, Error> {
let mut new_items = vec![];
for item in array.iter() {
if let Some(hash) = array_item_is_disclosure(item) {
if let Some(in_progress_disclosure) = disclosures.get_mut(hash) {
if in_progress_disclosure.found {
return Err(Error::DisclosureUsedMultipleTimes);
}
in_progress_disclosure.found = true;
match in_progress_disclosure.decoded.kind {
DisclosureKind::ArrayItem(ref value) => {
new_items.push(value.clone());
}
DisclosureKind::Property { .. } => {
return Err(Error::PropertyDisclosureWhenExpectingArray)
}
}
}
} else {
new_items.push(item.clone());
}
}

Ok(new_items)
}

fn array_item_is_disclosure(item: &serde_json::Value) -> Option<&str> {
let obj = item.as_object()?;

if obj.len() != 1 {
return None;
}

obj.get(ARRAY_CLAIM_ITEM_PROPERTY_NAME)?.as_str()
}
63 changes: 63 additions & 0 deletions ssi-sd-jwt/src/digest.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
use jose_b64::base64ct::{Base64UrlUnpadded, Encoding};
use sha2::Digest;

use crate::Error;

#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord)]
pub enum SdAlg {
Sha256,
}

impl SdAlg {
const SHA256_STR: &'static str = "sha-256";
}

impl SdAlg {
pub fn to_str(&self) -> &'static str {
match self {
SdAlg::Sha256 => Self::SHA256_STR,
}
}
}

impl TryFrom<&str> for SdAlg {
type Error = Error;

fn try_from(value: &str) -> Result<Self, Self::Error> {
Ok(match value {
Self::SHA256_STR => SdAlg::Sha256,
other => return Err(Error::UnknownSdAlg(other.to_owned())),
})
}
}

impl From<SdAlg> for &'static str {
fn from(value: SdAlg) -> Self {
value.to_str()
}
}

pub fn hash_encoded_disclosure(digest_algo: SdAlg, disclosure: &str) -> String {
match digest_algo {
SdAlg::Sha256 => {
let digest = sha2::Sha256::digest(disclosure.as_bytes());
Base64UrlUnpadded::encode_string(&digest)
}
}
}

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

#[test]
fn test_disclosure_hashing() {
assert_eq!(
hash_encoded_disclosure(
SdAlg::Sha256,
"WyI2cU1RdlJMNWhhaiIsICJmYW1pbHlfbmFtZSIsICJNw7ZiaXVzIl0"
),
"uutlBuYeMDyjLLTpf6Jxi7yNkEF35jdyWMn9U7b_RYY",
);
}
}
Loading

0 comments on commit ce6c962

Please sign in to comment.