use ark_babyjubjub::EdwardsAffine;
use eddsa_babyjubjub::{EdDSAPrivateKey, EdDSAPublicKey, EdDSASignature};
use rand::Rng;
use ruint::aliases::U256;
use serde::{Deserialize, Deserializer, Serialize, Serializer, de};
use crate::{FieldElement, PrimitiveError, sponge::hash_bytes_to_field_element};
const ASSOCIATED_DATA_COMMITMENT_DS_TAG: &[u8] = b"ASSOCIATED_DATA_HASH_V1";
const CLAIMS_HASH_DS_TAG: &[u8] = b"CLAIMS_HASH_V1";
const SUB_DS_TAG: &[u8] = b"H_CS(id, r)";
#[derive(Default, Debug, PartialEq, Eq, Hash, Copy, Clone, Serialize, Deserialize)]
#[repr(u8)]
pub enum CredentialVersion {
#[default]
V1 = 1,
}
#[derive(Debug, Serialize, Deserialize, Clone)]
pub struct Credential {
pub id: u64,
pub version: CredentialVersion,
pub issuer_schema_id: u64,
pub sub: FieldElement,
pub genesis_issued_at: u64,
pub expires_at: u64,
pub claims: Vec<FieldElement>,
#[serde(alias = "associated_data_hash")]
pub associated_data_commitment: FieldElement,
#[serde(serialize_with = "serialize_signature")]
#[serde(deserialize_with = "deserialize_signature")]
#[serde(default)]
pub signature: Option<EdDSASignature>,
#[serde(serialize_with = "serialize_public_key")]
#[serde(deserialize_with = "deserialize_public_key")]
pub issuer: EdDSAPublicKey,
}
impl Credential {
pub const MAX_CLAIMS: usize = 16;
#[must_use]
pub fn new() -> Self {
let mut rng = rand::thread_rng();
Self {
id: rng.r#gen(),
version: CredentialVersion::V1,
issuer_schema_id: 0,
sub: FieldElement::ZERO,
genesis_issued_at: 0,
expires_at: 0,
claims: vec![FieldElement::ZERO; Self::MAX_CLAIMS],
associated_data_commitment: FieldElement::ZERO,
signature: None,
issuer: EdDSAPublicKey {
pk: EdwardsAffine::default(),
},
}
}
#[must_use]
pub const fn id(mut self, id: u64) -> Self {
self.id = id;
self
}
#[must_use]
pub const fn version(mut self, version: CredentialVersion) -> Self {
self.version = version;
self
}
#[must_use]
pub const fn issuer_schema_id(mut self, issuer_schema_id: u64) -> Self {
self.issuer_schema_id = issuer_schema_id;
self
}
#[must_use]
pub const fn subject(mut self, sub: FieldElement) -> Self {
self.sub = sub;
self
}
#[must_use]
pub const fn genesis_issued_at(mut self, genesis_issued_at: u64) -> Self {
self.genesis_issued_at = genesis_issued_at;
self
}
#[must_use]
pub const fn expires_at(mut self, expires_at: u64) -> Self {
self.expires_at = expires_at;
self
}
pub fn claim_hash(mut self, index: usize, claim: U256) -> Result<Self, PrimitiveError> {
if index >= self.claims.len() {
return Err(PrimitiveError::OutOfBounds);
}
self.claims[index] = claim.try_into().map_err(|_| PrimitiveError::NotInField)?;
Ok(self)
}
pub fn claim(mut self, index: usize, claim: &[u8]) -> Result<Self, PrimitiveError> {
if index >= self.claims.len() {
return Err(PrimitiveError::OutOfBounds);
}
self.claims[index] = hash_bytes_to_field_element(CLAIMS_HASH_DS_TAG, claim)?;
Ok(self)
}
pub fn associated_data_commitment(
mut self,
associated_data_commitment: U256,
) -> Result<Self, PrimitiveError> {
self.associated_data_commitment = associated_data_commitment
.try_into()
.map_err(|_| PrimitiveError::NotInField)?;
Ok(self)
}
pub fn associated_data_commitment_from_raw_bytes(
mut self,
data: &[u8],
) -> Result<Self, PrimitiveError> {
self.associated_data_commitment =
hash_bytes_to_field_element(ASSOCIATED_DATA_COMMITMENT_DS_TAG, data)?;
Ok(self)
}
#[must_use]
pub fn get_cred_ds(&self) -> FieldElement {
match self.version {
CredentialVersion::V1 => FieldElement::from_be_bytes_mod_order(b"POSEIDON2+EDDSA-BJJ"),
}
}
pub fn claims_hash(&self) -> Result<FieldElement, eyre::Error> {
if self.claims.len() > Self::MAX_CLAIMS {
eyre::bail!("There can be at most {} claims", Self::MAX_CLAIMS);
}
let mut input = [*FieldElement::ZERO; Self::MAX_CLAIMS];
for (i, claim) in self.claims.iter().enumerate() {
input[i] = **claim;
}
poseidon2::bn254::t16::permutation_in_place(&mut input);
Ok(input[1].into())
}
pub fn hash(&self) -> Result<FieldElement, eyre::Error> {
match self.version {
CredentialVersion::V1 => {
let mut input = [
*self.get_cred_ds(),
self.issuer_schema_id.into(),
*self.sub,
self.genesis_issued_at.into(),
self.expires_at.into(),
*self.claims_hash()?,
*self.associated_data_commitment,
self.id.into(),
];
poseidon2::bn254::t8::permutation_in_place(&mut input);
Ok(input[1].into())
}
}
}
pub fn sign(self, signer: &EdDSAPrivateKey) -> Result<Self, eyre::Error> {
let mut credential = self;
credential.signature = Some(signer.sign(*credential.hash()?));
credential.issuer = signer.public();
Ok(credential)
}
pub fn verify_signature(
&self,
expected_issuer_pubkey: &EdDSAPublicKey,
) -> Result<bool, eyre::Error> {
if &self.issuer != expected_issuer_pubkey {
return Err(eyre::eyre!(
"Issuer public key does not match expected public key"
));
}
if let Some(signature) = &self.signature {
return Ok(self.issuer.verify(*self.hash()?, signature));
}
Err(eyre::eyre!("Credential not signed"))
}
#[must_use]
pub fn compute_sub(leaf_index: u64, blinding_factor: FieldElement) -> FieldElement {
let mut input = [
*FieldElement::from_be_bytes_mod_order(SUB_DS_TAG),
leaf_index.into(),
*blinding_factor,
];
poseidon2::bn254::t3::permutation_in_place(&mut input);
input[1].into()
}
}
impl Default for Credential {
fn default() -> Self {
Self::new()
}
}
#[expect(clippy::ref_option)]
fn serialize_signature<S>(
signature: &Option<EdDSASignature>,
serializer: S,
) -> Result<S::Ok, S::Error>
where
S: Serializer,
{
let Some(signature) = signature else {
return serializer.serialize_none();
};
let sig = signature
.to_compressed_bytes()
.map_err(serde::ser::Error::custom)?;
if serializer.is_human_readable() {
serializer.serialize_str(&hex::encode(sig))
} else {
serializer.serialize_bytes(&sig)
}
}
fn deserialize_signature<'de, D>(deserializer: D) -> Result<Option<EdDSASignature>, D::Error>
where
D: Deserializer<'de>,
{
let bytes: Option<Vec<u8>> = if deserializer.is_human_readable() {
Option::<String>::deserialize(deserializer)?
.map(|s| hex::decode(s).map_err(de::Error::custom))
.transpose()?
} else {
Option::<Vec<u8>>::deserialize(deserializer)?
};
let Some(bytes) = bytes else {
return Ok(None);
};
if bytes.len() != 64 {
return Err(de::Error::custom("Invalid signature. Expected 64 bytes."));
}
let mut arr = [0u8; 64];
arr.copy_from_slice(&bytes);
EdDSASignature::from_compressed_bytes(arr)
.map(Some)
.map_err(de::Error::custom)
}
fn serialize_public_key<S>(public_key: &EdDSAPublicKey, serializer: S) -> Result<S::Ok, S::Error>
where
S: Serializer,
{
let pk = public_key
.to_compressed_bytes()
.map_err(serde::ser::Error::custom)?;
if serializer.is_human_readable() {
serializer.serialize_str(&hex::encode(pk))
} else {
serializer.serialize_bytes(&pk)
}
}
fn deserialize_public_key<'de, D>(deserializer: D) -> Result<EdDSAPublicKey, D::Error>
where
D: Deserializer<'de>,
{
let bytes: Vec<u8> = if deserializer.is_human_readable() {
hex::decode(String::deserialize(deserializer)?).map_err(de::Error::custom)?
} else {
Vec::<u8>::deserialize(deserializer)?
};
if bytes.len() != 32 {
return Err(de::Error::custom("Invalid public key. Expected 32 bytes."));
}
let mut arr = [0u8; 32];
arr.copy_from_slice(&bytes);
EdDSAPublicKey::from_compressed_bytes(arr).map_err(de::Error::custom)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_associated_data_matches_direct_hash() {
let data = vec![1u8, 2, 3, 4, 5, 6, 7, 8, 9, 10];
let credential = Credential::new()
.associated_data_commitment_from_raw_bytes(&data)
.unwrap();
let direct_hash =
hash_bytes_to_field_element(ASSOCIATED_DATA_COMMITMENT_DS_TAG, &data).unwrap();
assert_eq!(credential.associated_data_commitment, direct_hash);
}
#[test]
fn test_associated_data_method() {
let data = vec![1u8, 2, 3, 4, 5, 6, 7, 8];
let credential = Credential::new()
.associated_data_commitment_from_raw_bytes(&data)
.unwrap();
assert_ne!(credential.associated_data_commitment, FieldElement::ZERO);
}
#[test]
fn test_claim_matches_direct_hash() {
let data = vec![1u8, 2, 3, 4, 5, 6, 7, 8, 9, 10];
let credential = Credential::new().claim(0, &data).unwrap();
let direct_hash = hash_bytes_to_field_element(CLAIMS_HASH_DS_TAG, &data).unwrap();
assert_eq!(credential.claims[0], direct_hash);
}
#[test]
fn test_claim_method() {
let data = vec![1u8, 2, 3, 4, 5, 6, 7, 8];
let credential = Credential::new().claim(1, &data).unwrap();
assert_ne!(credential.claims[1], FieldElement::ZERO);
}
}