Skip to content

Commit

Permalink
feat: C2PA
Browse files Browse the repository at this point in the history
  • Loading branch information
ivan770 committed Jul 24, 2024
1 parent 034817e commit 2f555f1
Show file tree
Hide file tree
Showing 11 changed files with 1,603 additions and 426 deletions.
1,843 changes: 1,436 additions & 407 deletions Cargo.lock

Large diffs are not rendered by default.

13 changes: 7 additions & 6 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -10,12 +10,12 @@ repository = "https://github.com/vaultie/teddybear"
categories = ["cryptography", "wasm"]

[workspace.dependencies]
ssi-dids = { git = "https://github.com/vaultie/ssi", branch = "ssi-jws-features" }
ssi-json-ld = { git = "https://github.com/vaultie/ssi", branch = "ssi-jws-features" }
ssi-jwk = { git = "https://github.com/vaultie/ssi", branch = "ssi-jws-features", default-features = false, features = ["ed25519"] }
ssi-jws = { git = "https://github.com/vaultie/ssi", branch = "ssi-jws-features", default-features = false, features = ["ed25519"] }
ssi-ldp = { git = "https://github.com/vaultie/ssi", branch = "ssi-jws-features", default-features = false, features = ["ed25519"] }
ssi-vc = { git = "https://github.com/vaultie/ssi", branch = "ssi-jws-features" }
ssi-dids = { git = "https://github.com/vaultie/ssi", rev = "7ff8a7713e9394002ccdd23520ac51caf554d7c3" }
ssi-json-ld = { git = "https://github.com/vaultie/ssi", rev = "7ff8a7713e9394002ccdd23520ac51caf554d7c3" }
ssi-jwk = { git = "https://github.com/vaultie/ssi", rev = "7ff8a7713e9394002ccdd23520ac51caf554d7c3", default-features = false, features = ["ed25519"] }
ssi-jws = { git = "https://github.com/vaultie/ssi", rev = "7ff8a7713e9394002ccdd23520ac51caf554d7c3", default-features = false, features = ["ed25519"] }
ssi-ldp = { git = "https://github.com/vaultie/ssi", rev = "7ff8a7713e9394002ccdd23520ac51caf554d7c3", default-features = false, features = ["ed25519"] }
ssi-vc = { git = "https://github.com/vaultie/ssi", rev = "7ff8a7713e9394002ccdd23520ac51caf554d7c3" }

base64 = "0.21.7"
ed25519-dalek = "2.1.0"
Expand All @@ -26,6 +26,7 @@ serde_json = "1.0.111"
thiserror = "1.0.56"
tokio = { version = "1.35.1", features = ["macros"] }

teddybear-c2pa = { path = "crates/teddybear-c2pa" }
teddybear-crypto = { path = "crates/teddybear-crypto" }
teddybear-did-key = { path = "crates/teddybear-did-key" }
teddybear-jwe = { path = "crates/teddybear-jwe" }
Expand Down
11 changes: 11 additions & 0 deletions crates/teddybear-c2pa/Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
[package]
name = "teddybear-c2pa"
version.workspace = true
edition.workspace = true
license.workspace = true
repository.workspace = true
categories.workspace = true

[dependencies]
c2pa = { git = "https://github.com/vaultie/c2pa-rs", features = ["unstable_api"] }
ed25519-dalek = { workspace = true }
54 changes: 54 additions & 0 deletions crates/teddybear-c2pa/src/lib.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
use std::io::{Read, Seek, Write};

use c2pa::{Builder, Signer, SigningAlg};
use ed25519_dalek::{Signer as _, SigningKey};

pub use c2pa::ManifestDefinition;

pub struct Ed25519Signer {
key: SigningKey,
certificates: Vec<Vec<u8>>,
}

impl Ed25519Signer {
pub fn new(key: SigningKey, certificate: Vec<u8>) -> Self {
Self {
key,
certificates: vec![certificate],
}
}
}

impl Signer for Ed25519Signer {
#[inline]
fn sign(&self, data: &[u8]) -> c2pa::Result<Vec<u8>> {
Ok(self.key.sign(data).to_vec())
}

#[inline]
fn alg(&self) -> SigningAlg {
SigningAlg::Ed25519
}

#[inline]
fn certs(&self) -> c2pa::Result<Vec<Vec<u8>>> {
Ok(self.certificates.clone())
}

#[inline]
fn reserve_size(&self) -> usize {
2048
}
}

pub fn embed_manifest<R: Read + Seek + Send, W: Write + Read + Seek + Send, S: Signer>(
source: &mut R,
dest: &mut W,
format: &str,
definition: ManifestDefinition,
signer: &S,
) -> c2pa::Result<Vec<u8>> {
let mut builder = Builder::default();
builder.definition = definition;
builder.sign(signer, format, source, dest)
}
27 changes: 16 additions & 11 deletions crates/teddybear-crypto/src/lib.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,3 @@
use std::marker::PhantomData;

use ed25519_dalek::SigningKey;
use ssi_dids::{did_resolve::easy_resolve, DIDMethod, Document, Source, VerificationMethod};
use ssi_jwk::{Algorithm, Base64urlUInt, OctetParams, Params};
Expand Down Expand Up @@ -38,18 +36,20 @@ pub struct KeyInfo {
jwk: JWK,
}

#[derive(Copy, Clone, Debug)]
#[derive(Clone, Debug)]
pub struct Public;

#[derive(Copy, Clone, Debug)]
pub struct Private;
#[derive(Clone, Debug)]
pub struct Private {
signing_key: SigningKey,
}

#[derive(Clone, Debug)]
pub struct Ed25519<T> {
raw: T,
document: Document,
pub ed25519: KeyInfo,
pub x25519: KeyInfo,
__type: PhantomData<T>,
}

impl Ed25519<Private> {
Expand Down Expand Up @@ -99,6 +99,11 @@ impl Ed25519<Private> {
encode_sign_custom_header(payload, &self.ed25519.jwk, &header)
}

#[inline]
pub fn raw_signing_key(&self) -> &SigningKey {
&self.raw.signing_key
}

#[inline]
pub fn as_ed25519_private_jwk(&self) -> &JWK {
&self.ed25519.jwk
Expand All @@ -109,12 +114,12 @@ impl Ed25519<Private> {
&self.x25519.jwk
}

async fn from_signing_key(key: SigningKey, jwk: JWK) -> Result<Self, Error> {
async fn from_signing_key(signing_key: SigningKey, jwk: JWK) -> Result<Self, Error> {
let (document, ed25519, mut x25519) = Ed25519::<()>::parts_from_jwk(jwk).await?;

match &mut x25519.jwk.params {
Params::OKP(okp) => {
okp.private_key = Some(Base64urlUInt(key.to_scalar_bytes().to_vec()))
okp.private_key = Some(Base64urlUInt(signing_key.to_scalar_bytes().to_vec()))
}
_ => unreachable!("X25519 keys should always have OKP params"),
}
Expand All @@ -123,7 +128,7 @@ impl Ed25519<Private> {
document,
ed25519,
x25519,
__type: PhantomData,
raw: Private { signing_key },
})
}
}
Expand All @@ -136,7 +141,7 @@ impl Ed25519<Public> {
document,
ed25519,
x25519,
__type: PhantomData,
raw: Public,
})
}

Expand All @@ -161,7 +166,7 @@ impl Ed25519<Public> {
document,
ed25519,
x25519,
__type: PhantomData,
raw: Public,
})
}
}
Expand Down
1 change: 1 addition & 0 deletions crates/teddybear-js/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ crate-type = ["cdylib", "rlib"]
wasm-opt = ["--enable-simd", "-Oz"]

[dependencies]
teddybear-c2pa = { workspace = true }
teddybear-crypto = { workspace = true }
teddybear-jwe = { workspace = true }
teddybear-status-list = { workspace = true }
Expand Down
48 changes: 47 additions & 1 deletion crates/teddybear-js/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -125,12 +125,13 @@

extern crate alloc;

use std::collections::HashMap;
use std::{collections::HashMap, io::Cursor};

use js_sys::{Object, Uint8Array};
use serde::Serialize;
use serde_json::json;
use serde_wasm_bindgen::Serializer;
use teddybear_c2pa::{Ed25519Signer, ManifestDefinition};
use teddybear_crypto::{Ed25519, Private, Public, JWK as InnerJWK};
use teddybear_jwe::{add_recipient, decrypt, A256Gcm, XC20P};
use teddybear_status_list::{
Expand Down Expand Up @@ -180,6 +181,24 @@ extern "C" {
pub type Jwe;
}

#[wasm_bindgen]
pub struct C2PASignatureResult(Vec<u8>, Vec<u8>);

#[wasm_bindgen(js_class = "C2PASignatureResult")]
impl C2PASignatureResult {
/// Payload with C2PA manifest embedded within.
#[wasm_bindgen(getter, js_name = "signedPayload")]
pub fn signed_payload(&self) -> Uint8Array {
self.0.as_slice().into()
}

/// C2PA manifest value.
#[wasm_bindgen(getter)]
pub fn manifest(&self) -> Uint8Array {
self.1.as_slice().into()
}
}

/// A public/private Ed25519/X25519 keypair.
#[wasm_bindgen]
pub struct PrivateEd25519(Ed25519<Private>);
Expand Down Expand Up @@ -336,6 +355,33 @@ impl PrivateEd25519 {
.await?;
Ok(presentation.serialize(&OBJECT_SERIALIZER)?.into())
}

#[wasm_bindgen(js_name = "embedC2PAManifest")]
pub fn embed_c2pa_manifest(
&self,
certificate: Uint8Array,
source: Uint8Array,
format: &str,
manifest_definition: Object,
) -> Result<C2PASignatureResult, JsError> {
let manifest_definition: ManifestDefinition =
serde_wasm_bindgen::from_value(manifest_definition.into())?;

let mut source = Cursor::new(source.to_vec());
let mut dest = source.clone();

let signer = Ed25519Signer::new(self.0.raw_signing_key().clone(), certificate.to_vec());

let manifest = teddybear_c2pa::embed_manifest(
&mut source,
&mut dest,
format,
manifest_definition,
&signer,
)?;

Ok(C2PASignatureResult(dest.into_inner(), manifest))
}
}

/// A public Ed25519/X25519 keypair.
Expand Down
Binary file added nix/data/crt.der
Binary file not shown.
8 changes: 8 additions & 0 deletions nix/node-testing.nix
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
{
src,
stdenvNoCC,
fetchurl,
fetchYarnDeps,
fixup-yarn-lock,
nodejs-slim,
Expand All @@ -25,6 +26,13 @@ stdenvNoCC.mkDerivation {
fixup-yarn-lock
];

placeholderImage = fetchurl {
url = "https://placehold.co/4/jpg";
hash = "sha256-2PSe5tyaj6dmakMkZGGJq/HZhCqtvb2KHPFKvEcZkq4=";
};

certificate = ./data/crt.der;

postPatch = ''
export HOME=$(mktemp -d)
Expand Down
2 changes: 1 addition & 1 deletion tests/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
"license": "MIT OR Apache-2.0",
"scripts": {
"pretty": "ts-standard --fix \"src/**/*.test.ts\"",
"test": "vitest run"
"test": "vitest run --reporter=basic"
},
"devDependencies": {
"@types/node": "^20.14.5",
Expand Down
22 changes: 22 additions & 0 deletions tests/src/c2pa.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import { PrivateEd25519 } from '@vaultie/teddybear'
import { readFileSync } from 'fs'
import { describe, it } from 'vitest'

const image = readFileSync(process.env.placeholderImage!)
const certificate = readFileSync(process.env.certificate!)

describe('can execute C2PA operations', () => {
it('can sign a C2PA manifest', async () => {
// This key should correspond to the certificate private key
const keyBytes = Buffer.from("5ff5e2393a44256abe197c82742366ff2f998f6822980e726f8fd16d6bd07eb1", "hex");

const key = await PrivateEd25519.fromBytes(new Uint8Array(keyBytes));

key.embedC2PAManifest(new Uint8Array(certificate), new Uint8Array(image), 'image/jpeg', {
title: 'Hello World',
claim_generator_info: [
{ name: "Teddybear" }
]
})
})
})

0 comments on commit 2f555f1

Please sign in to comment.