Skip to content

Commit

Permalink
Use vc-derive-credential to create a derived credential with selectiv…
Browse files Browse the repository at this point in the history
…ely disclosed claims and a BBS+ signature

vc-derive-credential, when combined with --nonce, now verifies a BBS+ derived credential

Check hashes that are revealed from BBS+ derived credential verification with the claims in the derived credential. Turn off canonicalization for now if using BBS+.

Update to_nquads_vec to into_nquads_vec, following new conventions
  • Loading branch information
thefireskater committed Apr 19, 2023
1 parent f77ec9d commit ee3dfe6
Show file tree
Hide file tree
Showing 16 changed files with 584 additions and 23 deletions.
4 changes: 3 additions & 1 deletion did-key/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -197,7 +197,9 @@ impl DIDResolver for DIDKey {
&& data[1] == DID_KEY_BLS12381_G2_PREFIX[1]
{
{
if data.len() - 2 != 96 {
// A BBS+ public key may have more than one generator; hence, the length may
// exceed 96
if data.len() - 2 < 96 {
return (
ResolutionMetadata::from_error(ERROR_INVALID_DID),
None,
Expand Down
1 change: 1 addition & 0 deletions ssi-json-ld/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ futures = "0.3"
lazy_static = "1.4"
combination = "0.1"
grdf = "0.16.2"
hex = "0.4"
ssi-contexts = { version = "0.1.3", path = "../contexts/" }
ssi-crypto = { path = "../ssi-crypto", version = "0.1" }

Expand Down
3 changes: 3 additions & 0 deletions ssi-json-ld/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -487,6 +487,7 @@ where
L::Context: Into<json_ld::syntax::context::Value<Span>>,
L::ContextError: Send,
{
eprintln!("json_to_dataset: enter");
use json_ld::JsonLdProcessor;

let options = Options {
Expand All @@ -500,10 +501,12 @@ where
let doc = json_ld::RemoteDocument::new(None, None, json);
let mut generator =
rdf_types::generator::Blank::new_with_prefix("b".to_string()).with_default_metadata();
eprintln!("json_to_dataset: 1");
let mut to_rdf = doc
.to_rdf_using(&mut generator, loader, options)
.await
.map_err(Box::new)?;
eprintln!("json_to_dataset: 2");
Ok(to_rdf
.cloned_quads()
.map(|q| {
Expand Down
11 changes: 11 additions & 0 deletions ssi-json-ld/src/rdf.rs
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ pub type DataSet =
/// See <https://www.w3.org/TR/n-quads/>.
pub trait IntoNQuads {
fn into_nquads(self) -> String;
fn into_nquads_vec(self) -> Vec<String>;
}

impl<Q: IntoIterator> IntoNQuads for Q
Expand All @@ -27,6 +28,16 @@ where
lines.dedup();
lines.join("")
}

fn into_nquads_vec(self) -> Vec<String> {
let mut lines = self
.into_iter()
.map(|quad| NQuadsStatement(quad.borrow()).to_string())
.collect::<Vec<String>>();
lines.sort();
lines.dedup();
lines
}
}

/// Wrapper to display an RDF Quad as an N-Quads statement.
Expand Down
2 changes: 2 additions & 0 deletions ssi-jwk/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,8 @@ tezos = ["blake2b_simd", "secp256k1", "secp256r1", "bs58"]
ring = ["dep:ring"]

[dependencies]
# todo make bbs optional based on bbs feature
bbs = { version = "0.4" }
num-bigint = "0.4"
simple_asn1 = "^0.5.2"
zeroize = { version = "1.5", features = ["zeroize_derive"] }
Expand Down
20 changes: 20 additions & 0 deletions ssi-jwk/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ use std::convert::TryFrom;
use std::result::Result;
use zeroize::Zeroize;
pub mod error;
use bbs::prelude::*;
pub use error::Error;

#[cfg(feature = "ripemd-160")]
Expand Down Expand Up @@ -255,6 +256,7 @@ pub enum Algorithm {
ESKeccakKR,
ESBlake2b,
ESBlake2bK,
BLS12381G2,
#[doc(hidden)]
AleoTestnet1Signature,
// Per the specs it should only be `none` but `None` is kept for backwards compatibility
Expand Down Expand Up @@ -337,6 +339,21 @@ impl JWK {
crate::aleo::generate_private_key_jwk().map_err(Error::AleoGeneratePrivateKey)
}

//#[cfg(feature = "bbs")]
pub fn generate_bls12381_2020() -> Result<JWK, Error> {
let (pk, sk) = Issuer::new_keys(100).unwrap();
let pk_bytes = pk.to_bytes_compressed_form();
let sk_bytes = sk.to_bytes_compressed_form().to_vec();

let params = Params::OKP(OctetParams {
curve: "Bls12381G2".to_string(),
public_key: Base64urlUInt(pk_bytes),
private_key: Some(Base64urlUInt(sk_bytes)),
});

Ok(JWK::from(params))
}

pub fn get_algorithm(&self) -> Option<Algorithm> {
if let Some(algorithm) = self.algorithm {
return Some(algorithm);
Expand All @@ -348,6 +365,9 @@ impl JWK {
Params::OKP(okp_params) if okp_params.curve == "Ed25519" => {
return Some(Algorithm::EdDSA);
}
Params::OKP(okp_params) if okp_params.curve == "Bls12381G2" => {
return Some(Algorithm::BLS12381G2);
}
#[cfg(feature = "aleo")]
Params::OKP(okp_params) if okp_params.curve == crate::aleo::OKP_CURVE => {
return Some(Algorithm::AleoTestnet1Signature);
Expand Down
1 change: 1 addition & 0 deletions ssi-jws/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ tezos = ["ssi-jwk/tezos", "secp256k1", "secp256r1", "ed25519"]
ring = ["ssi-jwk/ring", "dep:ring", "rand", "blake2"]

[dependencies]
bbs = { version = "0.4.1" }
thiserror = "1.0"
serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0"
Expand Down
210 changes: 207 additions & 3 deletions ssi-jws/src/lib.rs
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
#![cfg_attr(docsrs, feature(doc_auto_cfg))]

// TODO reinstate Error::MissingFeatures ?

pub mod error;
use bbs::prelude::*;
pub use error::Error;
use serde::{Deserialize, Serialize};
use ssi_crypto::hashes::sha256::sha256;
use ssi_jwk::{Algorithm, Base64urlUInt, Params as JWKParams, JWK};
use std::collections::BTreeMap;
use std::convert::TryFrom;
Expand Down Expand Up @@ -72,6 +73,62 @@ fn base64_encode_json<T: Serialize>(object: &T) -> Result<String, Error> {
Ok(base64::encode_config(json, base64::URL_SAFE_NO_PAD))
}

pub fn create_bbs_sig_input(payload: &JWSPayload) -> Vec<SignatureMessage> {
let mut messages: Vec<SignatureMessage> = Vec::new();
messages.push(SignatureMessage::hash(payload.header.as_bytes()));
messages.push(SignatureMessage::hash(payload.sigopts_digest.as_ref()));

for i in 0..payload.messages.len() {
let message = payload.messages[i].as_bytes();
messages.push(SignatureMessage::hash(message));
}

let mut num_messages = payload.messages.len() + 2;
while num_messages < 100 {
// todo 100 is hardcoded; use config
messages.push(SignatureMessage::hash(b""));
num_messages += 1;
}
messages
}

pub fn sign_bytes_v2(
algorithm: Algorithm,
key: &JWK,
payload: &JWSPayload,
) -> Result<Vec<u8>, Error> {
match &key.params {
JWKParams::OKP(okp) => {
match algorithm {
Algorithm::BLS12381G2 => {
let messages = create_bbs_sig_input(payload);

let Base64urlUInt(pk_bytes) = &okp.public_key;
let Base64urlUInt(sk_bytes) = okp.private_key.as_ref().unwrap();
let pk = bbs::prelude::PublicKey::try_from(pk_bytes.as_slice()).unwrap();
let sk = bbs::prelude::SecretKey::try_from(sk_bytes.as_slice()).unwrap();

let signature = Signature::new(messages.as_slice(), &sk, &pk).unwrap();
return Ok(signature.to_bytes_compressed_form().to_vec());
}
_ => (),
}
}
_ => (),
}

let messages_str = payload.messages.join("");
let messages_hash = sha256(messages_str.as_bytes());
sign_bytes(algorithm, &messages_hash, key)
}

pub fn generate_proof_nonce() -> String {
let proof_nonce = Verifier::generate_proof_nonce();
let proof_nonce_bytes = proof_nonce.to_bytes_compressed_form();
let proof_nonce_str = base64::encode(proof_nonce_bytes.as_ref());
return proof_nonce_str;
}

pub fn sign_bytes(algorithm: Algorithm, data: &[u8], key: &JWK) -> Result<Vec<u8>, Error> {
let signature = match &key.params {
#[cfg(feature = "ring")]
Expand Down Expand Up @@ -116,7 +173,9 @@ pub fn sign_bytes(algorithm: Algorithm, data: &[u8], key: &JWK) -> Result<Vec<u8
#[cfg(any(feature = "ring", feature = "ed25519"))]
JWKParams::OKP(okp) => {
use blake2::digest::{consts::U32, Digest};
if algorithm != Algorithm::EdDSA && algorithm != Algorithm::EdBlake2b {
if algorithm != Algorithm::EdDSA
&& algorithm != Algorithm::EdBlake2b
{
return Err(Error::UnsupportedAlgorithm);
}
if okp.curve != *"Ed25519" {
Expand Down Expand Up @@ -237,6 +296,119 @@ pub fn sign_bytes_b64(algorithm: Algorithm, data: &[u8], key: &JWK) -> Result<St
Ok(sig_b64)
}

pub fn sign_bytes_b64_v2(
algorithm: Algorithm,
key: &JWK,
payload: &JWSPayload,
) -> Result<String, Error> {
let signature = sign_bytes_v2(algorithm, key, payload)?;
let sig_b64 = base64::encode_config(signature, base64::URL_SAFE_NO_PAD);
Ok(sig_b64)
}

pub fn verify_payload(
algorithm: Algorithm,
key: &JWK,
payload: &JWSPayload,
signature: &[u8],
disclosed_message_indices: &[usize],
nonce: Option<&String>,
) -> Result<VerificationWarnings, Error> {
// todo ensure algorithm is bbs
let mut warnings = VerificationWarnings::default();

match algorithm {
Algorithm::BLS12381G2 => (),
_ => {
return Err(Error::UnsupportedAlgorithm);
}
}

match &key.params {
JWKParams::OKP(okp) => {
match nonce {
Some(n) => {
let proof = SignatureProof::try_from(signature).unwrap(); // todo error handling

let Base64urlUInt(pk_bytes) = &okp.public_key;
let issuer_pk = PublicKey::try_from(pk_bytes.as_slice()).unwrap();
let proof_request = Verifier::new_proof_request(disclosed_message_indices, &issuer_pk).unwrap();
let proof_nonce_bytes = base64::decode(n).unwrap();
assert!(proof_nonce_bytes.len() == 32);
let mut proof_nonce_bytes_sized: [u8; 32] = [0; 32];
proof_nonce_bytes_sized.clone_from_slice(proof_nonce_bytes.as_slice());
let proof_nonce = ProofNonce::from(proof_nonce_bytes_sized);

let result = Verifier::verify_signature_pok(&proof_request, &proof, &proof_nonce);
match result {
Ok(message_hashes) => {
let mut i = 0;
let mut credential_subject_id = "";
while i < payload.messages.len() {
let m = payload.messages[i].as_str();
if m.contains("<https://www.w3.org/2018/credentials#credentialSubject>") {
let m_parts: Vec<&str> = m.split(" ").collect();
credential_subject_id = m_parts[2];
break;
}
i += 1;
}
assert!(credential_subject_id != "", "credentialSubject node not found");

let mut first_claim_found = false;
while i < payload.messages.len() {
let m = payload.messages[i].as_str();
if m.starts_with(credential_subject_id) {
first_claim_found = true;
break;
}
i += 1;
}
assert!(first_claim_found, "No claims in derived credential");

for j in 0..message_hashes.len() {
let revealed_hash = message_hashes[j];
let target_hash = SignatureMessage::hash(payload.messages[i].as_bytes());
if revealed_hash != target_hash {
return Err(Error::InvalidSignature);
}

i += 1;
}
},
Err(_) => {
return Err(Error::InvalidSignature);
}
}
},
None => {
if signature.len() != 112 {
return Err(Error::InvalidSignature);
} else {
let mut signature_sized: [u8; 112] = [0; 112];
signature_sized.clone_from_slice(signature);
let bbs_sig = bbs::prelude::Signature::from(&signature_sized);

let messages = create_bbs_sig_input(payload);
let Base64urlUInt(pk_bytes) = &okp.public_key;
let pk = bbs::prelude::PublicKey::try_from(pk_bytes.as_slice()).unwrap();
let result = bbs_sig.verify(messages.as_slice(), &pk).unwrap();

if !result {
return Err(Error::InvalidSignature);
}
}
}
}
}
_ => {
return Err(Error::UnsupportedAlgorithm);
}
}

Ok(warnings)
}

pub fn verify_bytes_warnable(
algorithm: Algorithm,
data: &[u8],
Expand Down Expand Up @@ -297,6 +469,7 @@ pub fn verify_bytes_warnable(
#[cfg(any(feature = "ring", feature = "ed25519"))]
JWKParams::OKP(okp) => {
use blake2::digest::{consts::U32, Digest};
// todo error if BLS12-381 G2
if okp.curve != *"Ed25519" {
return Err(ssi_jwk::Error::CurveNotImplemented(okp.curve.to_string()).into());
}
Expand All @@ -310,7 +483,8 @@ pub fn verify_bytes_warnable(
{
use ring::signature::UnparsedPublicKey;
let verification_algorithm = &ring::signature::ED25519;
let public_key = UnparsedPublicKey::new(verification_algorithm, &okp.public_key.0);
let public_key =
UnparsedPublicKey::new(verification_algorithm, &okp.public_key.0);
public_key.verify(&hash, signature)?;
}
#[cfg(feature = "ed25519")]
Expand Down Expand Up @@ -531,6 +705,30 @@ pub fn detached_sign_unencoded_payload(
Ok(jws)
}

pub fn generate_header(algorithm: Algorithm, key: &JWK) -> Result<(Header, String), Error> {
let header = Header {
algorithm,
key_id: key.key_id.clone(),
critical: Some(vec!["b64".to_string()]),
base64urlencode_payload: Some(false),
..Default::default()
};
let header_str = base64_encode_json(&header)?;
Ok((header, header_str))
}

pub fn detached_sign_unencoded_payload_v2(
algorithm: Algorithm,
payload: &mut JWSPayload,
key: &JWK,
) -> Result<String, Error> {
let (header, header_b64) = generate_header(algorithm, key)?;
payload.header = header_b64;
let sig_b64 = sign_bytes_b64_v2(header.algorithm, &key, payload)?;
let jws = payload.header.clone() + ".." + &sig_b64;
Ok(jws)
}

pub fn prepare_detached_unencoded_payload(
algorithm: Algorithm,
payload: &[u8],
Expand Down Expand Up @@ -610,6 +808,12 @@ pub struct DecodedJWS {
pub signature: Vec<u8>,
}

pub struct JWSPayload {
pub header: String,
pub messages: Vec<String>,
pub sigopts_digest: [u8; 32],
}

/// Decode JWS parts (JOSE header, payload, and signature) into useful values.
/// The payload argument is bytes since it may be unencoded if the b64:false header parameter is used; otherwise it must be a base64url-encoded string. Header and signature are always expected to be base64url-encoded.
/// "crit" (critical) header parameters are checked and disallowed if unrecognized/unsupported.
Expand Down
Loading

0 comments on commit ee3dfe6

Please sign in to comment.