-
Notifications
You must be signed in to change notification settings - Fork 61
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Add JOSE VC/VP signature format. (#586)
- Loading branch information
1 parent
04720d4
commit d6d7737
Showing
13 changed files
with
457 additions
and
22 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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
211
crates/claims/crates/vc-jose-cose/src/jose/credential.rs
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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), | ||
} |
Oops, something went wrong.