Skip to content
Closed
Show file tree
Hide file tree
Changes from 1 commit
Commits
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
7 changes: 6 additions & 1 deletion rust-src/concordium_base/src/random_oracle/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@
//! with the context used to produce the proof. Any verification of sub-proofs
//! needs to be performed in the same order as when producing the proof.

use crate::{common::*, curve_arithmetic::Curve};
use crate::{common::*, curve_arithmetic::Curve, web3id::IsChallenge};
use sha3::{Digest, Sha3_256};
use std::io::Write;

Expand Down Expand Up @@ -112,6 +112,11 @@ impl RandomOracle {
self.add(message)
}

/// Append a challenge to the state of the random oracle.
pub fn append_challenge<Challenge: IsChallenge>(&mut self, challenge: &Challenge) {
challenge.append_to_transcript(self)
}

/// Append all items from an iterator to the random oracle. Equivalent to
/// repeatedly calling append in sequence.
/// Returns the new state of the random oracle, consuming the initial state.
Expand Down
210 changes: 182 additions & 28 deletions rust-src/concordium_base/src/web3id/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ pub mod did;
// TODO:
// - Documentation.
use crate::curve_arithmetic::Pairing;
use crate::hashes::BlockHash;
use crate::id::identity_attributes_credentials;
use crate::id::types::{
ArIdentity, ArPublicKey, ChainArData, HasIdentityObjectFields, IdObjectUseData,
Expand Down Expand Up @@ -689,9 +690,144 @@ impl<P: Pairing, C: Curve<Scalar = P::ScalarField>, AttributeType: Attribute<C::
/// Used as a phantom type to indicate a Web3ID challenge.
pub enum Web3IdChallengeMarker {}

/// Challenge string that serves as a distinguishing context when requesting
/// Sha256 challenge string that serves as a distinguishing context when requesting
/// proofs.
pub type Challenge = HashBytes<Web3IdChallengeMarker>;
pub type Sha256Challenge = HashBytes<Web3IdChallengeMarker>;

/// V1 structured challenge that serves as a distinguishing context when requesting
/// proofs.
/// Also called `ContextInfo` in ADR.
#[derive(
Clone,
Eq,
PartialEq,
Ord,
PartialOrd,
serde::Deserialize,
serde::Serialize,
crate::common::Serial,
Debug,
)]
pub struct V1Challenge {
/// This part of the challenge is suppose to be provided by the dapp backend (e.g. merchant backend).
given: GivenContextInfo,
/// This part of the challenge is suppose to be provided by the wallet or ID app.
requested: RequestedContextInfo,
}

// This data is suppose to be provided by the dapp backend (e.g. merchant backend).
Copy link
Contributor Author

@allanbrondum allanbrondum Oct 15, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Docs on an item needs three slashes: ///

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Else it is just a comment

#[derive(
Clone,
Eq,
PartialEq,
Ord,
PartialOrd,
serde::Deserialize,
serde::Serialize,
crate::common::Serial,
Debug,
)]
struct GivenContextInfo {
// Randomly generated nonce. It is important that the nonce is freshly generated by the backend
// for each request so that the presentation request anchor on-chain truely looks random.
nonce: Sha256Challenge,
// Human readable string giving more context to the request.
context_string: String,
// The topic of the wallet connection as defined by `walletConnect`.
// The wallet or ID app use this value to check that the topic matches the current active connection.
// TODO: Should we decide on a fix-sized length type `e.g. a type that will represent the walletConntect topic`.
Comment on lines +730 to +738
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think the properties should be dynamically represented and not as hardcoded fields? The web3id module may not even need to know the different properties.

connection_id: String,
}

// This data is suppose to be provided by the wallet or ID app.
#[derive(
Clone,
Eq,
PartialEq,
Ord,
PartialOrd,
serde::Deserialize,
serde::Serialize,
crate::common::Serial,
Debug,
)]
struct RequestedContextInfo {
// The looked up block hash by the wallet or ID app which contains the presentation request anchor transaction.
block_hash: BlockHash,
// The website URL that the wallet is connected to.
// TODO: Should we decide on a type that represent a URL.
resource_id: String,
}

#[derive(serde::Deserialize, serde::Serialize)]
// The type is `untagged` to be backward compatible with old proofs and requests.
#[serde(untagged)]
#[derive(Clone, Eq, PartialEq, Ord, PartialOrd, Debug)]
pub enum Challenge {
Sha256(Sha256Challenge),
// Also called `ConcordiumContextInformationV1` in ADR.
V1(V1Challenge),
}

impl crate::common::Serial for Challenge {
fn serial<B: crate::common::Buffer>(&self, out: &mut B) {
match self {
Challenge::Sha256(hash_bytes) => {
// No tag is added to be backward compatible with old proofs and requests.
hash_bytes.serial(out)
}
Challenge::V1(context_info) => {
// TODO: We should probably add the tag of the enum to the serialization to represent the versions of challenge for future
// extendability of the challenge enum.
context_info.serial(out)
}
}
}
}

/// A trait that represents a challenge in a proof response request protocol.
/// Types implementing this trait define how they can be incorporated into a cryptographic transcript (random oracle).
pub trait IsChallenge:
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If we need a polymorphic behaviour for different types of challenges, I think we should settle on one approach. Either an enum, or a trait that you implement. Having both seems redundant?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

(enum is for variation of a fixed set of behaviours, while the trait is open for extension, not sure which one is best here)

Clone + crate::common::Serial + serde::Serialize + for<'de> serde::Deserialize<'de>
{
/// Appends this challenge to the provided transcript (random oracle),
/// ensuring it contributes uniquely to the proof context.
///
/// Implementations should use deterministic versions, as well as labels and ensure every part of the challenge
/// is accounted for when appending the transcript. No two types implementing this trait should contribute in
/// the same way to the transcript.
fn append_to_transcript(&self, transcript: &mut RandomOracle);
}

impl IsChallenge for Challenge {
fn append_to_transcript(&self, transcript: &mut RandomOracle) {
match self {
Challenge::Sha256(hash_bytes) => hash_bytes.append_to_transcript(transcript),
Challenge::V1(context_info) => context_info.append_to_transcript(transcript),
}
}
}

impl IsChallenge for Sha256Challenge {
fn append_to_transcript(&self, transcript: &mut RandomOracle) {
// No tag `V0` is added to be backward compatible with old proofs and requests.
transcript.add_bytes(self);
}
}

impl IsChallenge for V1Challenge {
fn append_to_transcript(&self, transcript: &mut RandomOracle) {
// The tag `V1` is added for the challenge version.
transcript.add_bytes(b"V1");
transcript.add_bytes(b"given");
transcript.append_message(b"nonce", &self.given.nonce);
transcript.append_message(b"contextString", &self.given.context_string);
transcript.append_message(b"connectionID", &self.given.connection_id);
transcript.add_bytes(b"requested");
transcript.append_message(b"blockHash", &self.requested.block_hash);
transcript.append_message(b"resourceID", &self.requested.resource_id);
}
}

#[derive(Clone, serde::Serialize, serde::Deserialize, PartialEq, Eq, Debug)]
#[serde(rename_all = "camelCase")]
Expand All @@ -701,7 +837,7 @@ pub type Challenge = HashBytes<Web3IdChallengeMarker>;
))]
/// A request for a proof. This is the statement and challenge. The secret data
/// comes separately.
pub struct Request<C: Curve, AttributeType: Attribute<C::Scalar>> {
pub struct Request<Challenge: IsChallenge, C: Curve, AttributeType: Attribute<C::Scalar>> {
pub challenge: Challenge,
pub credential_statements: Vec<CredentialStatement<C, AttributeType>>,
}
Expand Down Expand Up @@ -876,6 +1012,7 @@ pub type CredentialHolderId = Ed25519PublicKey<CredentialHolderIdRole>;
/// statements, ownership proof for all Web3 credentials, and a context. The
/// only missing part to verify the proof are the public commitments.
pub struct Presentation<
Challenge: IsChallenge,
P: Pairing,
C: Curve<Scalar = P::ScalarField>,
AttributeType: Attribute<C::Scalar>,
Expand All @@ -902,8 +1039,12 @@ pub enum PresentationVerificationError {
InvalidCredential,
}

impl<P: Pairing, C: Curve<Scalar = P::ScalarField>, AttributeType: Attribute<C::Scalar>>
Presentation<P, C, AttributeType>
impl<
Challenge: IsChallenge,
P: Pairing,
C: Curve<Scalar = P::ScalarField>,
AttributeType: Attribute<C::Scalar>,
> Presentation<Challenge, P, C, AttributeType>
{
/// Get an iterator over the metadata for each of the verifiable credentials
/// in the order they appear in the presentation.
Expand All @@ -924,19 +1065,19 @@ impl<P: Pairing, C: Curve<Scalar = P::ScalarField>, AttributeType: Attribute<C::
&self,
params: &GlobalContext<C>,
public: impl ExactSizeIterator<Item = &'a CredentialsInputs<P, C>>,
) -> Result<Request<C, AttributeType>, PresentationVerificationError> {
) -> Result<Request<Challenge, C, AttributeType>, PresentationVerificationError> {
let mut transcript = RandomOracle::domain("ConcordiumWeb3ID");
transcript.add_bytes(self.presentation_context);
transcript.append_challenge(&self.presentation_context);
transcript.append_message(b"ctx", &params);

let mut request = Request {
challenge: self.presentation_context,
challenge: self.presentation_context.clone(),
credential_statements: Vec::new(),
};

// Compute the data that the linking proof signed.
let to_sign =
linking_proof_message_to_sign(self.presentation_context, &self.verifiable_credential);
linking_proof_message_to_sign(&self.presentation_context, &self.verifiable_credential);

let mut linking_proof_iter = self.linking_proof.proof_value.iter();

Expand Down Expand Up @@ -968,8 +1109,12 @@ impl<P: Pairing, C: Curve<Scalar = P::ScalarField>, AttributeType: Attribute<C::
}
}

impl<P: Pairing, C: Curve<Scalar = P::ScalarField>, AttributeType: Attribute<C::Scalar>>
crate::common::Serial for Presentation<P, C, AttributeType>
impl<
Challenge: IsChallenge,
P: Pairing,
C: Curve<Scalar = P::ScalarField>,
AttributeType: Attribute<C::Scalar>,
> crate::common::Serial for Presentation<Challenge, P, C, AttributeType>
{
fn serial<B: crate::common::Buffer>(&self, out: &mut B) {
self.presentation_context.serial(out);
Expand All @@ -979,10 +1124,11 @@ impl<P: Pairing, C: Curve<Scalar = P::ScalarField>, AttributeType: Attribute<C::
}

impl<
Challenge: IsChallenge,
P: Pairing,
C: Curve<Scalar = P::ScalarField>,
AttributeType: Attribute<C::Scalar> + DeserializeOwned,
> TryFrom<serde_json::Value> for Presentation<P, C, AttributeType>
> TryFrom<serde_json::Value> for Presentation<Challenge, P, C, AttributeType>
{
type Error = anyhow::Error;

Expand All @@ -1003,10 +1149,11 @@ impl<
}

impl<
Challenge: IsChallenge,
P: Pairing,
C: Curve<Scalar = P::ScalarField>,
AttributeType: Attribute<C::Scalar> + serde::Serialize,
> serde::Serialize for Presentation<P, C, AttributeType>
> serde::Serialize for Presentation<Challenge, P, C, AttributeType>
{
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
Expand Down Expand Up @@ -1778,11 +1925,12 @@ impl<C: Curve, AttributeType: Attribute<C::Scalar>> CredentialStatement<C, Attri
}

fn linking_proof_message_to_sign<
Challenge: IsChallenge,
P: Pairing,
C: Curve<Scalar = P::ScalarField>,
AttributeType: Attribute<C::Scalar>,
>(
challenge: Challenge,
challenge: &Challenge,
proofs: &[CredentialProof<P, C, AttributeType>],
) -> Vec<u8> {
use crate::common::Serial;
Expand All @@ -1796,20 +1944,22 @@ fn linking_proof_message_to_sign<
msg
}

impl<C: Curve, AttributeType: Attribute<C::Scalar>> Request<C, AttributeType> {
impl<Challenge: IsChallenge, C: Curve, AttributeType: Attribute<C::Scalar>>
Request<Challenge, C, AttributeType>
{
/// Construct a proof for the [`Request`] using the provided cryptographic
/// parameters and secrets.
pub fn prove<'a, P: Pairing<ScalarField = C::Scalar>, Signer: 'a + Web3IdSigner>(
self,
params: &GlobalContext<C>,
attrs: impl ExactSizeIterator<Item = CommitmentInputs<'a, P, C, AttributeType, Signer>>,
) -> Result<Presentation<P, C, AttributeType>, ProofError>
) -> Result<Presentation<Challenge, P, C, AttributeType>, ProofError>
where
AttributeType: 'a,
{
let mut proofs = Vec::with_capacity(attrs.len());
let mut transcript = RandomOracle::domain("ConcordiumWeb3ID");
transcript.add_bytes(self.challenge);
transcript.append_challenge(&self.challenge);
transcript.append_message(b"ctx", &params);
let mut csprng = rand::thread_rng();
if self.credential_statements.len() != attrs.len() {
Expand All @@ -1823,7 +1973,7 @@ impl<C: Curve, AttributeType: Attribute<C::Scalar>> Request<C, AttributeType> {
let proof = cred_statement.prove(params, &mut transcript, &mut csprng, attributes)?;
proofs.push(proof);
}
let to_sign = linking_proof_message_to_sign(self.challenge, &proofs);
let to_sign = linking_proof_message_to_sign(&self.challenge, &proofs);
// Linking proof
let mut proof_value = Vec::new();
for signer in signers {
Expand Down Expand Up @@ -2080,7 +2230,7 @@ mod tests {
/// JSON serialization of requests and presentations is also tested.
fn test_web3_only() -> anyhow::Result<()> {
let mut rng = rand::thread_rng();
let challenge = Challenge::new(rng.gen());
let challenge = Sha256Challenge::new(rng.gen());
let signer_1 = ed25519_dalek::SigningKey::generate(&mut rng);
let signer_2 = ed25519_dalek::SigningKey::generate(&mut rng);
let issuer_1 = ed25519_dalek::SigningKey::generate(&mut rng);
Expand Down Expand Up @@ -2178,7 +2328,7 @@ mod tests {
},
];

let request = Request::<ArCurve, Web3IdAttribute> {
let request = Request::<Sha256Challenge, ArCurve, Web3IdAttribute> {
challenge,
credential_statements,
};
Expand Down Expand Up @@ -2274,14 +2424,16 @@ mod tests {

let data = serde_json::to_string_pretty(&proof)?;
assert!(
serde_json::from_str::<Presentation<IpPairing, ArCurve, Web3IdAttribute>>(&data)
.is_ok(),
serde_json::from_str::<
Presentation<Sha256Challenge, IpPairing, ArCurve, Web3IdAttribute>,
>(&data)
.is_ok(),
"Cannot deserialize proof correctly."
);

let data = serde_json::to_string_pretty(&request)?;
assert_eq!(
serde_json::from_str::<Request<ArCurve, Web3IdAttribute>>(&data)?,
serde_json::from_str::<Request<Sha256Challenge, ArCurve, Web3IdAttribute>>(&data)?,
request,
"Cannot deserialize request correctly."
);
Expand All @@ -2296,7 +2448,7 @@ mod tests {
/// JSON serialization of requests and presentations is also tested.
fn test_mixed() -> anyhow::Result<()> {
let mut rng = rand::thread_rng();
let challenge = Challenge::new(rng.gen());
let challenge = Sha256Challenge::new(rng.gen());
let params = GlobalContext::generate("Test".into());
let cred_id_exp = ArCurve::generate_scalar(&mut rng);
let cred_id = CredentialRegistrationID::from_exponent(&params, cred_id_exp);
Expand Down Expand Up @@ -2368,7 +2520,7 @@ mod tests {
},
];

let request = Request::<ArCurve, Web3IdAttribute> {
let request = Request::<Sha256Challenge, ArCurve, Web3IdAttribute> {
challenge,
credential_statements,
};
Expand Down Expand Up @@ -2462,14 +2614,16 @@ mod tests {

let data = serde_json::to_string_pretty(&proof)?;
assert!(
serde_json::from_str::<Presentation<IpPairing, ArCurve, Web3IdAttribute>>(&data)
.is_ok(),
serde_json::from_str::<
Presentation<Sha256Challenge, IpPairing, ArCurve, Web3IdAttribute>,
>(&data)
.is_ok(),
"Cannot deserialize proof correctly."
);

let data = serde_json::to_string_pretty(&request)?;
assert_eq!(
serde_json::from_str::<Request<ArCurve, Web3IdAttribute>>(&data)?,
serde_json::from_str::<Request<Sha256Challenge, ArCurve, Web3IdAttribute>>(&data)?,
request,
"Cannot deserialize request correctly."
);
Expand Down
36 changes: 35 additions & 1 deletion rust-src/wallet_library/resources/web3_id_request.json

Large diffs are not rendered by default.

Loading