Skip to content

Commit

Permalink
Add JOSE VC/VP signature format. (#586)
Browse files Browse the repository at this point in the history
  • Loading branch information
timothee-haudebourg committed Jul 26, 2024
1 parent 04720d4 commit d6d7737
Show file tree
Hide file tree
Showing 13 changed files with 457 additions and 22 deletions.
3 changes: 2 additions & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@ ssi-jws = { path = "./crates/claims/crates/jws", version = "0.2", default-featur
ssi-jwt = { path = "./crates/claims/crates/jwt", version = "0.2", default-features = false }
ssi-sd-jwt = { path = "./crates/claims/crates/sd-jwt", version = "0.2", default-features = false }
ssi-vc = { path = "./crates/claims/crates/vc", version = "0.3", default-features = false }
ssi-vc-jose-cose = { path = "./crates/claims/crates/vc-jose-cose", version = "0.1", default-features = false }
ssi-data-integrity-core = { path = "./crates/claims/crates/data-integrity/core", version = "0.1", default-features = false }
ssi-di-sd-primitives = { path = "./crates/claims/crates/data-integrity/sd-primitives", version = "0.1", default-features = false }
ssi-data-integrity-suites = { path = "./crates/claims/crates/data-integrity/suites", version = "0.1", default-features = false }
Expand Down Expand Up @@ -83,7 +84,7 @@ async-std = "1.9"
async-trait = "0.1.68"
thiserror = "1.0.40"
chrono = "0.4.24"
iref = "3.1.2"
iref = "3.2.2"
static-iref = "3.0"
rdf-types = "0.22.3"
xsd-types = "0.9.5"
Expand Down
1 change: 1 addition & 0 deletions crates/claims/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,7 @@ ssi-jws.workspace = true
ssi-jwt.workspace = true
ssi-sd-jwt.workspace = true
ssi-vc.workspace = true
ssi-vc-jose-cose.workspace = true
ssi-data-integrity.workspace = true
ssi-dids-core.workspace = true
ssi-eip712 = { workspace = true, optional = true }
Expand Down
10 changes: 7 additions & 3 deletions crates/claims/crates/jws/src/compact/bytes.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,9 +9,13 @@ use std::{borrow::Cow, ops::Deref};
pub struct CompactJWS([u8]);

impl CompactJWS {
pub fn new(data: &[u8]) -> Result<&Self, InvalidCompactJWS<&[u8]>> {
if Self::check(data) {
Ok(unsafe { Self::new_unchecked(data) })
pub fn new<T>(data: &T) -> Result<&Self, InvalidCompactJWS<&T>>
where
T: ?Sized + AsRef<[u8]>,
{
let bytes = data.as_ref();
if Self::check(bytes) {
Ok(unsafe { Self::new_unchecked(bytes) })
} else {
Err(InvalidCompactJWS(data))
}
Expand Down
26 changes: 9 additions & 17 deletions crates/claims/crates/jws/src/signature.rs
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,14 @@ pub trait JWSPayload {
/// JWS type.
///
/// Value of the `typ` field in the JWS header.
fn typ(&self) -> Option<&'static str>;
fn typ(&self) -> Option<&str> {
None
}

/// JWS cty header value.
fn cty(&self) -> Option<&str> {
None
}

fn payload_bytes(&self) -> Cow<[u8]>;

Expand All @@ -23,40 +30,24 @@ pub trait JWSPayload {
}

impl JWSPayload for [u8] {
fn typ(&self) -> Option<&'static str> {
None
}

fn payload_bytes(&self) -> Cow<[u8]> {
Cow::Borrowed(self)
}
}

impl JWSPayload for Vec<u8> {
fn typ(&self) -> Option<&'static str> {
None
}

fn payload_bytes(&self) -> Cow<[u8]> {
Cow::Borrowed(self)
}
}

impl JWSPayload for str {
fn typ(&self) -> Option<&'static str> {
None
}

fn payload_bytes(&self) -> Cow<[u8]> {
Cow::Borrowed(self.as_bytes())
}
}

impl JWSPayload for String {
fn typ(&self) -> Option<&'static str> {
None
}

fn payload_bytes(&self) -> Cow<[u8]> {
Cow::Borrowed(self.as_bytes())
}
Expand Down Expand Up @@ -89,6 +80,7 @@ pub trait JWSSigner {
let header = Header {
algorithm: info.algorithm,
key_id: info.key_id,
content_type: payload.cty().map(ToOwned::to_owned),
type_: payload.typ().map(ToOwned::to_owned),
..Default::default()
};
Expand Down
24 changes: 24 additions & 0 deletions crates/claims/crates/vc-jose-cose/Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
[package]
name = "ssi-vc-jose-cose"
version = "0.1.0"
edition = "2021"
authors = ["Spruce Systems, Inc."]
license = "Apache-2.0"
description = "Securing Verifiable Credentials using JOSE and COSE with the `ssi` library."
repository = "https://github.com/spruceid/ssi/"
documentation = "https://docs.rs/vc-jose-cose/"

[dependencies]
ssi-claims-core.workspace = true
ssi-jws.workspace = true
ssi-vc.workspace = true
ssi-json-ld.workspace = true
xsd-types.workspace = true
serde.workspace = true
serde_json.workspace = true
thiserror.workspace = true

[dev-dependencies]
ssi-jws = { workspace = true, features = ["secp256r1"] }
ssi-jwk.workspace = true
async-std.workspace = true
211 changes: 211 additions & 0 deletions crates/claims/crates/vc-jose-cose/src/jose/credential.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,211 @@
use super::JoseDecodeError;
use serde::{de::DeserializeOwned, Serialize};
use ssi_claims_core::{ClaimsValidity, DateTimeProvider, SignatureError, ValidateClaims};
use ssi_json_ld::{iref::Uri, syntax::Context};
use ssi_jws::{CompactJWS, DecodedJWS, JWSPayload, JWSSigner, ValidateJWSHeader};
use ssi_vc::{
enveloped::EnvelopedVerifiableCredential,
v2::{Credential, CredentialTypes, JsonCredential},
MaybeIdentified,
};
use std::borrow::Cow;
use xsd_types::DateTimeStamp;

/// Payload of a JWS-secured Verifiable Credential.
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub struct JoseVc<T = JsonCredential>(pub T);

impl<T: Serialize> JoseVc<T> {
/// Sign a JOSE VC into an enveloped verifiable credential.
pub async fn sign_into_enveloped(
&self,
signer: &impl JWSSigner,
) -> Result<EnvelopedVerifiableCredential, SignatureError> {
let jws = JWSPayload::sign(self, signer).await?;
Ok(EnvelopedVerifiableCredential {
context: Context::iri_ref(ssi_vc::v2::CREDENTIALS_V2_CONTEXT_IRI.to_owned().into()),
id: format!("data:application/vc-ld+jwt,{jws}").parse().unwrap(),
})
}
}

impl<T: DeserializeOwned> JoseVc<T> {
/// Decode a JOSE VC.
pub fn decode(jws: &CompactJWS) -> Result<DecodedJWS<Self>, JoseDecodeError> {
jws.to_decoded()?
.try_map(|payload| serde_json::from_slice(&payload).map(Self))
.map_err(Into::into)
}
}

impl JoseVc {
/// Decode a JOSE VC with an arbitrary credential type.
pub fn decode_any(jws: &CompactJWS) -> Result<DecodedJWS<Self>, JoseDecodeError> {
Self::decode(jws)
}
}

impl<T: Serialize> JWSPayload for JoseVc<T> {
fn typ(&self) -> Option<&str> {
Some("vc-ld+jwt")
}

fn cty(&self) -> Option<&str> {
Some("vc")
}

fn payload_bytes(&self) -> Cow<[u8]> {
Cow::Owned(serde_json::to_vec(&self.0).unwrap())
}
}

impl<E, T> ValidateJWSHeader<E> for JoseVc<T> {
fn validate_jws_header(&self, _env: &E, _header: &ssi_jws::Header) -> ClaimsValidity {
// There are no formal obligations about `typ` and `cty`.
// It SHOULD be `vc-ld+jwt` and `vc`, but it does not MUST.
Ok(())
}
}

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

impl<T: Credential> Credential for JoseVc<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 JoseVc<T> {
fn validate_claims(&self, environment: &E, proof: &P) -> ClaimsValidity {
self.0.validate_claims(environment, proof)
}
}

#[cfg(test)]
mod tests {
use serde_json::json;
use ssi_claims_core::VerificationParameters;
use ssi_jwk::JWK;
use ssi_jws::{CompactJWS, CompactJWSBuf};
use ssi_vc::v2::JsonCredential;

use crate::JoseVc;

async fn verify(input: &CompactJWS, key: &JWK) {
let vc = JoseVc::decode_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 jose_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 = JoseVc(vc).sign_into_enveloped(&key).await.unwrap();
let jws = CompactJWSBuf::new(enveloped.id.decoded_data().unwrap().into_owned()).unwrap();
verify(&jws, &key).await
}
}
17 changes: 17 additions & 0 deletions crates/claims/crates/vc-jose-cose/src/jose/mod.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
mod credential;
pub use credential::*;

mod presentation;
pub use presentation::*;

/// Error that can occur when decoding a JOSE VC or VP.
#[derive(Debug, thiserror::Error)]
pub enum JoseDecodeError {
/// JWS error.
#[error(transparent)]
JWS(#[from] ssi_jws::DecodeError),

/// JSON payload error.
#[error(transparent)]
JSON(#[from] serde_json::Error),
}
Loading

0 comments on commit d6d7737

Please sign in to comment.