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

JOSE-COSE: Securing with SD-JWT #611

Merged
merged 5 commits into from
Sep 19, 2024
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
6 changes: 6 additions & 0 deletions crates/claims/crates/jwt/src/claims/any.rs
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,12 @@ impl IntoIterator for AnyClaims {
}
}

impl FromIterator<(String, serde_json::Value)> for AnyClaims {
fn from_iter<T: IntoIterator<Item = (String, serde_json::Value)>>(iter: T) -> Self {
Self(BTreeMap::from_iter(iter))
}
}

impl ClaimSet for AnyClaims {
fn contains<C: Claim>(&self) -> bool {
self.contains(C::JWT_CLAIM_NAME)
Expand Down
6 changes: 4 additions & 2 deletions crates/claims/crates/sd-jwt/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ use ssi_claims_core::{
DateTimeProvider, ProofValidationError, ResolverProvider, SignatureError, ValidateClaims,
Verification,
};
use ssi_core::{BytesBuf, JsonPointer, JsonPointerBuf};
use ssi_core::BytesBuf;
use ssi_jwk::JWKResolver;
use ssi_jws::{DecodedJws, Jws, JwsPayload, JwsSignature, JwsSigner, ValidateJwsHeader};
use ssi_jwt::{AnyClaims, ClaimSet, DecodedJwt, JWTClaims};
Expand All @@ -48,6 +48,8 @@ use std::{
str::FromStr,
};

pub use ssi_core::{json_pointer, JsonPointer, JsonPointerBuf};

pub(crate) mod utils;
use utils::is_url_safe_base64_char;

Expand Down Expand Up @@ -339,7 +341,7 @@ impl<'de> serde::Deserialize<'de> for &'de SdJwt {
}

/// Owned SD-JWT.
#[derive(PartialEq, Eq, PartialOrd, Ord, Hash)]
#[derive(Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub struct SdJwtBuf(Vec<u8>);

impl SdJwtBuf {
Expand Down
2 changes: 2 additions & 0 deletions crates/claims/crates/vc-jose-cose/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@ documentation = "https://docs.rs/vc-jose-cose/"
[dependencies]
ssi-claims-core.workspace = true
ssi-jws.workspace = true
ssi-jwt.workspace = true
ssi-sd-jwt.workspace = true
ssi-cose.workspace = true
ssi-vc.workspace = true
ssi-json-ld.workspace = true
Expand Down
3 changes: 3 additions & 0 deletions crates/claims/crates/vc-jose-cose/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,3 +7,6 @@ pub use jose::*;

mod cose;
pub use cose::*;

mod sd_jwt;
pub use sd_jwt::*;
286 changes: 286 additions & 0 deletions crates/claims/crates/vc-jose-cose/src/sd_jwt/credential.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,286 @@
use std::borrow::Borrow;

use serde::{de::DeserializeOwned, Deserialize, Serialize};
use ssi_claims_core::{ClaimsValidity, DateTimeProvider, SignatureError, ValidateClaims};
use ssi_json_ld::{iref::Uri, syntax::Context};
use ssi_jws::JwsSigner;
use ssi_jwt::{ClaimSet, InfallibleClaimSet, JWTClaims};
use ssi_sd_jwt::{JsonPointer, RevealError, RevealedSdJwt, SdAlg, SdJwt, SdJwtBuf};
use ssi_vc::{
enveloped::EnvelopedVerifiableCredential,
v2::{Credential, CredentialTypes, JsonCredential},
MaybeIdentified,
};
use xsd_types::DateTimeStamp;

/// SD-JWT Verifiable Credential.
///
/// See: <https://w3c.github.io/vc-jose-cose/#securing-with-sd-jwt>
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
#[serde(transparent)]
pub struct SdJwtVc<T = JsonCredential>(pub T);

impl<T> SdJwtVc<T> {
/// Returns this credential as JWT claims.
///
/// These are the claims that will be encoded in the SD-JWT.
pub fn as_jwt_claims(&self) -> JWTClaims<&Self> {
JWTClaims {
registered: Default::default(),
private: self,
}
}

/// Turns this credential into JWT claims.
///
/// These claims can then be encoded in the SD-JWT.
pub fn into_jwt_claims(self) -> JWTClaims<Self> {
JWTClaims {
registered: Default::default(),
private: self,
}
}
}

impl<T: Serialize> SdJwtVc<T> {
/// Signs the credential into an SD-JWT without any concealed claims.
///
/// The generated SD-JWT will not have any disclosures.
///
/// Use [`Self::conceal_and_sign`] to select the claims to be concealed.
pub async fn sign(&self, signer: &impl JwsSigner) -> Result<SdJwtBuf, SignatureError> {
let pointers: [&JsonPointer; 0] = [];
self.conceal_and_sign(SdAlg::Sha256, &pointers, signer)
.await
}

/// Signs the credential while concealing the claims selected by the given
/// JSON pointers.
///
/// You can use [`Self::sign`] directly if you don't need to conceal
/// anything.
pub async fn conceal_and_sign(
&self,
sd_alg: SdAlg,
pointers: &[impl Borrow<JsonPointer>],
signer: &impl JwsSigner,
) -> Result<SdJwtBuf, SignatureError> {
SdJwtBuf::conceal_and_sign(&self.as_jwt_claims(), sd_alg, pointers, signer).await
}

/// Signs the credential into an enveloped verifiable credential (with an
/// SD-JWT identifier) without concealing any claim.
///
/// The generated SD-JWT, encoded in the credential identifier, will not
/// have any disclosures.
///
/// Use [`Self::conceal_and_sign_into_enveloped`] to select the claims to be
/// concealed.
pub async fn sign_into_enveloped(
&self,
signer: &impl JwsSigner,
) -> Result<EnvelopedVerifiableCredential, SignatureError> {
let pointers: [&JsonPointer; 0] = [];
self.conceal_and_sign_into_enveloped(SdAlg::Sha256, &pointers, signer)
.await
}

/// Signs the credential into an enveloped verifiable credential (with an
/// SD-JWT identifier) while concealing the claims selected by the given
/// JSON pointers.
///
/// The generated SD-JWT, encoded in the credential identifier, will not
/// have any disclosures.
///
/// Use [`Self::conceal_and_sign_into_enveloped`] to select the claims to be
/// concealed.
pub async fn conceal_and_sign_into_enveloped(
&self,
sd_alg: SdAlg,
pointers: &[impl Borrow<JsonPointer>],
signer: &impl JwsSigner,
) -> Result<EnvelopedVerifiableCredential, SignatureError> {
let sd_jwt = self.conceal_and_sign(sd_alg, pointers, signer).await?;
Ok(EnvelopedVerifiableCredential {
context: Context::iri_ref(ssi_vc::v2::CREDENTIALS_V2_CONTEXT_IRI.to_owned().into()),
id: format!("data:application/vc-ld+sd-jwt,{sd_jwt}")
.parse()
.unwrap(),
})
}
}

impl<T: DeserializeOwned> SdJwtVc<T> {
/// Decodes a SD-JWT VC, revealing its disclosed claims.
///
/// This function requires the `T` parameter, representing the credential
/// type, to be known. If you don't know what `T` you should use, use the
/// [`Self::decode_reveal_any`].
pub fn decode_reveal(sd_jwt: &SdJwt) -> Result<RevealedSdJwt<Self>, RevealError> {
sd_jwt.decode_reveal()
}
}

impl SdJwtVc {
/// Decodes a SD-JWT VC, revealing its disclosed claims.
///
/// This function uses [`JsonCredential`] as credential type. If you need
/// to use a custom credential type, use the [`Self::decode_reveal`]
/// function.
pub fn decode_reveal_any(sd_jwt: &SdJwt) -> Result<RevealedSdJwt<Self>, RevealError> {
sd_jwt.decode_reveal()
}
}

impl<T: MaybeIdentified> MaybeIdentified for SdJwtVc<T> {
fn id(&self) -> Option<&Uri> {
self.0.id()
}
}

impl<T: Credential> Credential for SdJwtVc<T> {
type Description = T::Description;
type Subject = T::Subject;
type Issuer = T::Issuer;
type Status = T::Status;
type Schema = T::Schema;
type RelatedResource = T::RelatedResource;
type RefreshService = T::RefreshService;
type TermsOfUse = T::TermsOfUse;
type Evidence = T::Evidence;

fn id(&self) -> Option<&Uri> {
Credential::id(&self.0)
}

fn additional_types(&self) -> &[String] {
self.0.additional_types()
}

fn types(&self) -> CredentialTypes {
self.0.types()
}

fn name(&self) -> Option<&str> {
self.0.name()
}

fn description(&self) -> Option<&Self::Description> {
self.0.description()
}

fn credential_subjects(&self) -> &[Self::Subject] {
self.0.credential_subjects()
}

fn issuer(&self) -> &Self::Issuer {
self.0.issuer()
}

fn valid_from(&self) -> Option<DateTimeStamp> {
self.0.valid_from()
}

fn valid_until(&self) -> Option<DateTimeStamp> {
self.0.valid_until()
}

fn credential_status(&self) -> &[Self::Status] {
self.0.credential_status()
}

fn credential_schemas(&self) -> &[Self::Schema] {
self.0.credential_schemas()
}

fn related_resources(&self) -> &[Self::RelatedResource] {
self.0.related_resources()
}

fn refresh_services(&self) -> &[Self::RefreshService] {
self.0.refresh_services()
}

fn terms_of_use(&self) -> &[Self::TermsOfUse] {
self.0.terms_of_use()
}

fn evidence(&self) -> &[Self::Evidence] {
self.0.evidence()
}

fn validate_credential<E>(&self, env: &E) -> ClaimsValidity
where
E: DateTimeProvider,
{
self.0.validate_credential(env)
}
}

impl<E, P, T: ValidateClaims<E, P>> ValidateClaims<E, P> for SdJwtVc<T> {
fn validate_claims(&self, environment: &E, proof: &P) -> ClaimsValidity {
self.0.validate_claims(environment, proof)
}
}

impl<T> ClaimSet for SdJwtVc<T> {}
impl<T> InfallibleClaimSet for SdJwtVc<T> {}

#[cfg(test)]
mod tests {
use serde_json::json;
use ssi_claims_core::VerificationParameters;
use ssi_jwk::JWK;
use ssi_sd_jwt::{json_pointer, SdAlg, SdJwt, SdJwtBuf};
use ssi_vc::v2::JsonCredential;

use crate::SdJwtVc;

async fn verify(input: &SdJwt, key: &JWK) {
let vc = SdJwtVc::decode_reveal_any(input).unwrap();
let params = VerificationParameters::from_resolver(key);
let result = vc.verify(params).await.unwrap();
assert_eq!(result, Ok(()))
}

#[async_std::test]
async fn sd_jwt_vc_roundtrip() {
let vc: JsonCredential = serde_json::from_value(json!({
"@context": [
"https://www.w3.org/ns/credentials/v2",
"https://www.w3.org/ns/credentials/examples/v2"
],
"id": "http://university.example/credentials/1872",
"type": [
"VerifiableCredential",
"ExampleAlumniCredential"
],
"issuer": "https://university.example/issuers/565049",
"validFrom": "2010-01-01T19:23:24Z",
"credentialSchema": {
"id": "https://example.org/examples/degree.json",
"type": "JsonSchema"
},
"credentialSubject": {
"id": "did:example:123",
"degree": {
"type": "BachelorDegree",
"name": "Bachelor of Science and Arts"
}
}
}))
.unwrap();

let key = JWK::generate_p256();
let enveloped = SdJwtVc(vc)
.conceal_and_sign_into_enveloped(
SdAlg::Sha256,
&[json_pointer!("/credentialSubject/id")],
&key,
)
.await
.unwrap();
let jws = SdJwtBuf::new(enveloped.id.decoded_data().unwrap().into_owned()).unwrap();
verify(&jws, &key).await
}
}
5 changes: 5 additions & 0 deletions crates/claims/crates/vc-jose-cose/src/sd_jwt/mod.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
mod credential;
pub use credential::*;

mod presentation;
pub use presentation::*;
Loading