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 preliminary VC support #26

Merged
merged 61 commits into from
Aug 14, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
61 commits
Select commit Hold shift + click to select a range
ccf3fb6
Skeleton for W3C VC implementation
scouten-adobe Jul 18, 2024
341291a
Depend on the forthcoming openssl_ffi_mutex feature of c2pa-rs
scouten-adobe Jul 18, 2024
ec23e56
Copy over placeholder code from previous related efforts
scouten-adobe Jul 18, 2024
1c174a7
Merge branch 'main' into vc
scouten-adobe Jul 18, 2024
aeb74d1
Add dep on SSI crates
scouten-adobe Jul 18, 2024
d6fc252
Update to follow CAWG section title change
scouten-adobe Jul 18, 2024
42c9a6e
Alright, it's a start!
scouten-adobe Jul 18, 2024
fa214e3
Make it a VC
scouten-adobe Jul 18, 2024
4ecd098
Refactor to have a more flexible test harness
scouten-adobe Jul 18, 2024
e33e184
Add test case for missing credential issuer
scouten-adobe Jul 18, 2024
29035d6
Add test case for missing issuance date
scouten-adobe Jul 18, 2024
bec4496
Fix missing issuance date
scouten-adobe Jul 18, 2024
fa22d49
Add test case for missing proof
scouten-adobe Jul 18, 2024
9a55b03
Add VC proof from issuer
scouten-adobe Jul 18, 2024
6974321
Verify VC proof
scouten-adobe Jul 18, 2024
ea6d065
Sketch in JSON context handling for CAWG identity assertion
scouten-adobe Jul 19, 2024
ec52efd
Enforce of CAWG @type requirement
scouten-adobe Jul 19, 2024
e030b89
Update to c2pa 0.33.0 and use new `openssl_ffi_mutex` feature
scouten-adobe Jul 30, 2024
a240095
ssi crate 0.8.0
scouten-adobe Jul 30, 2024
cae73c1
Start reworking DID generation to use new SSI crate APIs
scouten-adobe Jul 30, 2024
5963b4a
Start working up CreatorIdentityAssertion type using new ssi crate APIs
scouten-adobe Jul 31, 2024
5c6a38d
Implement RequiredType
scouten-adobe Aug 1, 2024
c26102a
Implement validFrom requirement
scouten-adobe Aug 1, 2024
8cae818
Use linked-data derive macros to improve serialization of CAWG-specif…
scouten-adobe Aug 1, 2024
5c41ce7
Link test credential to test issuer
scouten-adobe Aug 1, 2024
7c19f22
Flesh out more of verifiedIdentity type
scouten-adobe Aug 1, 2024
3ca512a
Remove no-longer-needed warning about yaml-rust
scouten-adobe Aug 1, 2024
c0124d2
Fill in remainder of verified identity type from current draft spec
scouten-adobe Aug 1, 2024
75a9461
Experiment with JWT envelope
scouten-adobe Aug 1, 2024
2b0b9de
First cut at including signer_payload
scouten-adobe Aug 2, 2024
188b4a9
Scope out COSE spike. Looks pretty doable
scouten-adobe Aug 2, 2024
cc6633e
Looks close!
scouten-adobe Aug 2, 2024
8964f6b
Looks like we're generating a valid COSE-protected VC
scouten-adobe Aug 5, 2024
add4c82
VcSignatureHandler -> CoseVcSignatureHandler
scouten-adobe Aug 5, 2024
c96d697
temp_cose isn't that big; move it up to a single file with the rest o…
scouten-adobe Aug 5, 2024
93cc5e8
Sort
scouten-adobe Aug 5, 2024
dc59394
Yay, we've round-tripped VCs!
scouten-adobe Aug 5, 2024
74fa430
Signature validation works!
scouten-adobe Aug 7, 2024
3554956
Define CAWG context using an IRI and without trailing slash
scouten-adobe Aug 8, 2024
a0dcb13
Add a Q&D code path for DID method selection
scouten-adobe Aug 8, 2024
b6e177f
Enforce requirement for validFrom field to be populated
scouten-adobe Aug 12, 2024
3a0993b
Make verified_identities a NonEmptyVec
scouten-adobe Aug 12, 2024
9918646
Use NonEmptyString for VC items that must be non-empty strings
scouten-adobe Aug 12, 2024
1242169
VerifiedIdentity -> VcVerifiedIdentity
scouten-adobe Aug 12, 2024
0f2c48f
New trait VerifiedIdentity
scouten-adobe Aug 12, 2024
8c21c22
Start splitting up mod identity_assertion
scouten-adobe Aug 12, 2024
1c91eb8
Split out SignatureHandler into its own mod
scouten-adobe Aug 12, 2024
2a37914
Move HashedUri over to mod signer_payload
scouten-adobe Aug 12, 2024
a6f1fb1
Split out ValidationError into its own mod
scouten-adobe Aug 12, 2024
0da1289
Split out NamedActor and friends into a new module
scouten-adobe Aug 12, 2024
2b09996
Add NamedActor::verified_identities
scouten-adobe Aug 12, 2024
57c7401
Actually implement NamedActor::verified_identities
scouten-adobe Aug 13, 2024
0c23b9a
Temporarily allow panic and unwrap
scouten-adobe Aug 13, 2024
0856e22
Merge branch 'main' into vc
scouten-adobe Aug 14, 2024
9f62fb1
Clippy 1.80.1
scouten-adobe Aug 14, 2024
dcb10d4
More Clippy
scouten-adobe Aug 14, 2024
75229a3
Add exceptions for licenses and advisories incorporated via ssi crate
scouten-adobe Aug 14, 2024
ee2b8b4
Bump minimum versions of iref and serde_json to match other crates
scouten-adobe Aug 14, 2024
9ac1b7a
Remove unused dep on did-method-key
scouten-adobe Aug 14, 2024
0439143
Remove CreatorIdentityAssertionIssuer
scouten-adobe Aug 14, 2024
2568ac5
Dead strip
scouten-adobe Aug 14, 2024
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
17 changes: 15 additions & 2 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -5,16 +5,29 @@ edition = "2021"

[dependencies]
async-trait = "0.1.78"
c2pa = "0.32.4"
c2pa = { version = "0.33.0", features = ["openssl_ffi_mutex"] }
chrono = "0.4.38"
ciborium = "0.2.2"
coset = "0.3.8"
hex-literal = "0.4.1"
iref = "3.2.2"
json-ld-syntax = "0.21.1"
jumbf = "0.4.0"
linked-data = { version = "0.1.2", features = ["derive"] }
multibase = "0.9.1"
non-empty-string = { version = "0.2.4", features = ["serde"] }
serde = { version = "1.0.197", features = ["derive"] }
serde_bytes = "0.11.14"
serde_json = "1.0.117"
ssi = "0.8.0"
ssi-dids = "0.2.0"
ssi-json-ld = "0.3.0"
static-iref = "3.0"
thiserror = "1.0.61"
xsd-types = "0.9.5"

[dev-dependencies]
actix = "0.13.3"
c2pa = { version = "0.32.4", features = ["file_io", "openssl_sign"] }
c2pa = { version = "0.33.0", features = ["file_io", "openssl_sign", "openssl_ffi_mutex"] }
serde = { version = "1.0.197", features = ["derive"] }
tempfile = "3.10.1"
15 changes: 11 additions & 4 deletions deny.toml
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,16 @@ yanked = "allow" # "deny" # TODO: Re-enable when possible.

ignore = [
"RUSTSEC-2021-0127", # serde_cbor
"RUSTSEC-2024-0320", # yaml-rust

# The following dependencies come in through ssi crate.
# See https://github.com/spruceid/ssi/issues/599 and
# https://github.com/scouten-adobe/cawg-identity-core/issues/29.
"RUSTSEC-2021-0139", # ansi_term (unmaintained)
"RUSTSEC-2021-0145", # atty (unaligned read, possibly unmaintained)
"RUSTSEC-2021-0141", # dotenv (meant for dev/testing only)
"RUSTSEC-2023-0055", # lexical 6.1.1 (soundness issues)
"RUSTSEC-2022-0040", # owning_ref 0.4.1 (soundness issues, unmaintained)
"RUSTSEC-2023-0071", # rsa 0.6.1 (Marvin key recovery attack)
]

# Deny multiple versions unless explicitly skipped.
Expand All @@ -37,6 +46,7 @@ allow = [
"MIT",
"MPL-2.0",
"Unicode-DFS-2016",
"W3C-20150513", # needs review
"Zlib",
]
confidence-threshold = 0.9
Expand All @@ -52,6 +62,3 @@ license-files = [
unknown-registry = "deny"
unknown-git = "deny"
allow-registry = ["https://github.com/rust-lang/crates.io-index"]
allow-git = [
"https://github.com/contentauth/c2pa-rs.git"
]
219 changes: 28 additions & 191 deletions src/identity_assertion.rs → src/identity_assertion/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -11,19 +11,28 @@
// specific language governing permissions and limitations under
// each license.

use std::{
collections::HashSet,
fmt::{Debug, Formatter},
};
use std::fmt::{Debug, Formatter};

use async_trait::async_trait;
use serde::{Deserialize, Serialize};
use serde_bytes::ByteBuf;

use crate::{
builder::IdentityAssertionBuilder, internal, internal::debug_byte_slice::DebugByteSlice,
};

pub(crate) mod named_actor;
use named_actor::NamedActor;
pub use named_actor::VerifiedIdentities;

pub(crate) mod signer_payload;
use signer_payload::{HashedUri, SignerPayload};

pub(crate) mod signature_handler;
use signature_handler::SignatureHandler;

pub(crate) mod validation_error;
use validation_error::{ValidationError, ValidationResult};

/// This struct represents the raw content of the identity assertion.
///
/// Use [`IdentityAssertionBuilder`] and [`ManifestBuilder`] to
Expand Down Expand Up @@ -173,6 +182,20 @@
}
}

// TO DO: Allow configuration of signature handler list.
// For now, we hard-code the VC/creator identity assertion signature handler.

let vc_handler = crate::w3c_vc::CoseVcSignatureHandler {};
if let Ok(named_actor) = vc_handler
.check_signature(signer_payload, &self.signature)
.await

Check warning on line 191 in src/identity_assertion/mod.rs

View check run for this annotation

Codecov / codecov/patch

src/identity_assertion/mod.rs#L191

Added line #L191 was not covered by tests
{
return Ok(IdentityAssertionReport {
signer_payload,
named_actor,
});
}

Check warning on line 198 in src/identity_assertion/mod.rs

View check run for this annotation

Codecov / codecov/patch

src/identity_assertion/mod.rs#L197-L198

Added lines #L197 - L198 were not covered by tests
Err(ValidationError::UnknownSignatureType(
self.signer_payload.sig_type.clone(),
))
Expand Down Expand Up @@ -217,192 +240,6 @@
}
}

/// The set of data to be signed by the credential holder.
#[derive(Clone, Debug, Deserialize, Eq, Serialize, PartialEq)]
pub struct SignerPayload {
/// List of assertions referenced by this credential signature
pub referenced_assertions: Vec<HashedUri>,

/// A string identifying the data type of the `signature` field
pub sig_type: String,
}

impl SignerPayload {
fn check_against_manifest(&self, manifest: &c2pa::Manifest) -> ValidationResult<()> {
// All assertions mentioned in referenced_assertions
// also need to be referenced in the claim.

for ref_assertion in self.referenced_assertions.iter() {
if let Some(claim_assertion) = manifest
.assertion_references()
.find(|a| a.url() == ref_assertion.url)
{
if claim_assertion.hash() != ref_assertion.hash {
return Err(ValidationError::AssertionMismatch(
ref_assertion.url.to_owned(),
));
}
if let Some(alg) = claim_assertion.alg().as_ref() {
if Some(alg) != ref_assertion.alg.as_ref() {
return Err(ValidationError::AssertionMismatch(
ref_assertion.url.to_owned(),
));
}
} else {
return Err(ValidationError::AssertionMismatch(
ref_assertion.url.to_owned(),
));
}
} else {
return Err(ValidationError::AssertionNotInClaim(
ref_assertion.url.to_owned(),
));
}
}

// Ensure that a hard binding assertion is present.

let ref_assertion_labels: Vec<String> = self
.referenced_assertions
.iter()
.map(|ra| ra.url.to_owned())
.collect();

if !ref_assertion_labels.iter().any(|ra| {
if let Some((_jumbf_prefix, label)) = ra.rsplit_once('/') {
label.starts_with("c2pa.hash.")
} else {
false
}
}) {
return Err(ValidationError::NoHardBindingAssertion);
}

// Make sure no assertion references are duplicated.

let mut labels = HashSet::<String>::new();

for label in &ref_assertion_labels {
let label = label.clone();
if labels.contains(&label) {
return Err(ValidationError::MultipleAssertionReferenced(label));
}
labels.insert(label);
}

Ok(())
}
}

/// A `SignatureHandler` can read one kind of signature from an identity
/// assertion, assess the validity of the signature, and return information
/// about the corresponding credential subject.
#[async_trait]
pub trait SignatureHandler {
/// Returns true if this handler can process a signature with
/// the given `sig_type` code.
fn can_handle_sig_type(sig_type: &str) -> bool;

/// Check the signature, returning an instance of [`NamedActor`] if
/// the signature is valid.
///
/// Will only be called if `can_handle_sig_type` returns `true`
/// for this signature.
async fn check_signature<'a>(
&self,
signer_payload: &SignerPayload,
signature: &'a [u8],
) -> ValidationResult<Box<dyn NamedActor<'a>>>;
}

/// A `NamedActor` is the actor named by a signature in an identity
/// assertion.
pub trait NamedActor<'a>: Debug {
/// Return the name of the subject suitable for user experience display.
fn display_name(&self) -> Option<String>;

/// Return `true` if the subject's credentials chain up to a suitable trust
/// list for this kind of signature.
fn is_trusted(&self) -> bool;
}

/// A `HashedUri` provides a reference to content available within the same
/// manifest store.
///
/// This is described in §8.3, “[URI References],” of the C2PA Technical
/// Specification.
///
/// [URI References]: https://c2pa.org/specifications/specifications/2.0/specs/C2PA_Specification.html#_uri_references
#[derive(Clone, Deserialize, Eq, PartialEq, Serialize)]
pub struct HashedUri {
/// JUMBF URI reference
pub url: String,

/// A string identifying the cryptographic hash algorithm used to compute
/// the hash
#[serde(skip_serializing_if = "Option::is_none")]
pub alg: Option<String>,

/// Byte string containing the hash value
#[serde(with = "serde_bytes")]
pub hash: Vec<u8>,
}

impl Debug for HashedUri {
fn fmt(&self, f: &mut Formatter<'_>) -> Result<(), std::fmt::Error> {
f.debug_struct("HashedUri")
.field("url", &self.url)
.field("alg", &self.alg)
.field("hash", &DebugByteSlice(&self.hash))
.finish()
}
}

/// Describes the ways in which a CAWG identity
/// assertion can fail validation as described in
/// [§7. Validating the identity assertion].
///
/// [§7. Validating the identity assertion]: https://creator-assertions.github.io/identity/1.0-draft/#_validating_the_identity_assertion
/// [`IdentityAssertion`]: crate::IdentityAssertion
#[derive(Clone, Debug, Eq, thiserror::Error, PartialEq)]
pub enum ValidationError {
/// The named assertion could not be found in the claim.
#[error("No assertion with the label {0:#?} in the claim")]
AssertionNotInClaim(String),

/// The named assertion exists in the claim, but the hash does not match.
#[error("The assertion with the label {0:#?} is not the same as in the claim")]
AssertionMismatch(String),

/// The named assertion was referenced more than once in the identity
/// assertion.
#[error("The assertion with the label {0:#?} is referenced multiple times")]
MultipleAssertionReferenced(String),

/// No hard-binding assertion was referenced in the identity assertion.
#[error("No hard binding assertion is referenced")]
NoHardBindingAssertion,

/// The `sig_type` field is not recognized.
#[error("Unable to parse a signature of type {0:#?}")]
UnknownSignatureType(String),

/// The signature is not valid.
#[error("Signature is invalid")]
InvalidSignature,

/// The `pad1` or `pad2` fields contain values other than 0x00 bytes.
#[error("Invalid padding")]
InvalidPadding,

/// Unexpected error while parsing or validating the identity assertion.
#[error("Unexpected error")]
UnexpectedError,
}

/// Result type for validation operations.
pub type ValidationResult<T> = std::result::Result<T, ValidationError>;

/// This struct is returned when the data in an identity assertion is deemed
/// valid.
#[derive(Debug)]
Expand Down
Loading
Loading