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

Add JOSE VC/VP signature format. #586

Merged
merged 3 commits into from
Jul 26, 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
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.4"
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