Skip to content

Commit b6fb107

Browse files
authored
Adapt Identity Create / Deanonymize and Identity Proofs (#745)
1 parent 9ea232c commit b6fb107

File tree

29 files changed

+931
-435
lines changed

29 files changed

+931
-435
lines changed

CHANGELOG.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,12 @@
1616
* Change document max file size to 10 MB and max files on bill to 20
1717
* Add request deadlines to BillHistoryBlock
1818
* Remove `identity_proof` API and adapt and move to new email confirmation API
19+
* Add dev mode flag `disable_mandatory_email_confirmations`, to make it easier for testing
20+
* Identity Confirmation via Email
21+
* Add persistence
22+
* Adapt `create_identity` and `deanonymize` to require a confirmed email for identified users
23+
* Add endpoints to `confirm`, `verify` an email address and to `get_email_confirmations`
24+
* Adapt `IdentityProof` Block to include the email confirmation signed by the mint
1925

2026
# 0.4.12
2127

crates/bcr-ebill-api/src/external/email.rs

Lines changed: 79 additions & 68 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,15 @@
11
use async_trait::async_trait;
22
use bcr_common::core::{BillId, NodeId};
33
use bcr_ebill_core::application::ServiceTraitBounds;
4-
use bcr_ebill_core::protocol::{Email, event::bill_events::BillEventType, mint::MintSignature};
5-
use bitcoin::{XOnlyPublicKey, base58};
4+
use bcr_ebill_core::protocol::Sha256Hash;
5+
use bcr_ebill_core::protocol::{
6+
Email, EmailIdentityProofData, SchnorrSignature, SignedIdentityProof,
7+
crypto::Error as CryptoError, event::bill_events::BillEventType,
8+
};
9+
use bitcoin::base58;
610
use borsh_derive::BorshSerialize;
7-
use nostr::hashes::Hash;
8-
use nostr::hashes::sha256;
9-
use nostr::util::SECP256K1;
1011
use secp256k1::schnorr::Signature;
11-
use secp256k1::{Keypair, Message, SecretKey};
12+
use secp256k1::{Keypair, Message, SECP256K1, SecretKey};
1213
use serde::{Deserialize, Serialize};
1314
use thiserror::Error;
1415

@@ -27,16 +28,15 @@ pub enum Error {
2728
/// all hex errors
2829
#[error("External Email Base58 Error: {0}")]
2930
Base58(#[from] base58::InvalidCharacterError),
30-
/// all signature errors
31-
#[error("External Email Signature Error: {0}")]
32-
Signature(#[from] secp256k1::Error),
3331
/// all borsh errors
3432
#[error("External Email Borsh Error")]
3533
Borsh(#[from] borsh::io::Error),
3634
#[error("External Email Invalid Mint Id Error")]
3735
InvalidMintId,
3836
#[error("External Email Invalid Mint Signature Error")]
3937
InvalidMintSignature,
38+
#[error("External Email crypto Error")]
39+
Crypto(#[from] CryptoError),
4040
}
4141

4242
#[cfg(test)]
@@ -66,7 +66,7 @@ pub trait EmailClientApi: ServiceTraitBounds {
6666
company_node_id: &Option<NodeId>,
6767
confirmation_code: &str,
6868
private_key: &SecretKey,
69-
) -> Result<MintSignature>;
69+
) -> Result<(SignedIdentityProof, EmailIdentityProofData)>;
7070
/// Send a bill notification email
7171
async fn send_bill_notification(
7272
&self,
@@ -100,38 +100,54 @@ impl EmailClient {
100100
}
101101
}
102102

103-
async fn get_eic_challenge(&self, mint_url: &url::Url, node_id: &NodeId) -> Result<String> {
104-
let req = StartEmailRegisterRequest {
105-
node_id: node_id.to_owned(),
106-
};
107-
108-
let resp: StartEmailRegisterResponse = self
109-
.cl
110-
.post(to_url(mint_url, "v1/eic/challenge")?)
111-
.json(&req)
112-
.send()
113-
.await?
114-
.json()
115-
.await?;
103+
async fn get_eic_challenge(
104+
&self,
105+
mint_url: &url::Url,
106+
node_id: &NodeId,
107+
private_key: &SecretKey,
108+
) -> Result<Signature> {
109+
self.get_challenge("eic", mint_url, node_id, private_key)
110+
.await
111+
}
116112

117-
Ok(resp.challenge)
113+
async fn get_ens_challenge(
114+
&self,
115+
mint_url: &url::Url,
116+
node_id: &NodeId,
117+
private_key: &SecretKey,
118+
) -> Result<Signature> {
119+
self.get_challenge("ens", mint_url, node_id, private_key)
120+
.await
118121
}
119122

120-
async fn get_ens_challenge(&self, mint_url: &url::Url, node_id: &NodeId) -> Result<String> {
123+
async fn get_challenge(
124+
&self,
125+
prefix: &str,
126+
mint_url: &url::Url,
127+
node_id: &NodeId,
128+
private_key: &SecretKey,
129+
) -> Result<Signature> {
121130
let req = StartEmailRegisterRequest {
122131
node_id: node_id.to_owned(),
123132
};
124133

125134
let resp: StartEmailRegisterResponse = self
126135
.cl
127-
.post(to_url(mint_url, "v1/ens/challenge")?)
136+
.post(to_url(mint_url, &format!("v1/{}/challenge", prefix))?)
128137
.json(&req)
129138
.send()
130139
.await?
131140
.json()
132141
.await?;
133142

134-
Ok(resp.challenge)
143+
let decoded_challenge = base58::decode(&resp.challenge).map_err(Error::Base58)?;
144+
145+
let key_pair = Keypair::from_secret_key(SECP256K1, private_key);
146+
let msg = Message::from_digest_slice(&decoded_challenge)
147+
.map_err(|e| Error::Crypto(CryptoError::Signature(e.to_string())))?;
148+
let signature = SECP256K1.sign_schnorr(&msg, &key_pair);
149+
150+
Ok(signature)
135151
}
136152
}
137153

@@ -151,12 +167,9 @@ impl EmailClientApi for EmailClient {
151167
email: &Email,
152168
private_key: &SecretKey,
153169
) -> Result<()> {
154-
let challenge = self.get_eic_challenge(mint_url, node_id).await?;
155-
let decoded_challenge = base58::decode(&challenge).map_err(Error::Base58)?;
156-
157-
let key_pair = Keypair::from_secret_key(SECP256K1, private_key);
158-
let msg = Message::from_digest_slice(&decoded_challenge).map_err(Error::Signature)?;
159-
let signed_challenge = SECP256K1.sign_schnorr(&msg, &key_pair);
170+
let signed_challenge = self
171+
.get_eic_challenge(mint_url, node_id, private_key)
172+
.await?;
160173

161174
let req = RegisterEmailRequest {
162175
node_id: node_id.to_owned(),
@@ -183,18 +196,22 @@ impl EmailClientApi for EmailClient {
183196
company_node_id: &Option<NodeId>,
184197
confirmation_code: &str,
185198
private_key: &SecretKey,
186-
) -> Result<MintSignature> {
199+
) -> Result<(SignedIdentityProof, EmailIdentityProofData)> {
187200
let pl = EmailConfirmPayload {
188201
node_id: node_id.to_owned(),
189202
company_node_id: company_node_id.to_owned(),
190203
confirmation_code: confirmation_code.to_owned(),
191204
};
192205

193206
let serialized = borsh::to_vec(&pl).map_err(Error::Borsh)?;
194-
let signature = sign_payload(&serialized, private_key);
207+
let hash = Sha256Hash::from_bytes(&serialized);
208+
let signature = SchnorrSignature::sign(&hash, private_key).map_err(Error::Crypto)?;
195209
let payload = base58::encode(&serialized);
196210

197-
let req = EmailConfirmRequest { payload, signature };
211+
let req = EmailConfirmRequest {
212+
payload,
213+
signature: signature.as_sig(),
214+
};
198215

199216
let res: EmailConfirmResponse = self
200217
.cl
@@ -210,18 +227,25 @@ impl EmailClientApi for EmailClient {
210227
}
211228

212229
let decoded_mint_sig = base58::decode(&res.payload).map_err(Error::Base58)?;
230+
let hash = Sha256Hash::from_bytes(&decoded_mint_sig);
231+
232+
let signature = SchnorrSignature::from(res.signature);
213233

214-
if !verify_request(
215-
&decoded_mint_sig,
216-
&res.signature,
217-
&mint_node_id.pub_key().x_only_public_key().0,
218-
)? {
234+
if !signature
235+
.verify(&hash, &mint_node_id.pub_key())
236+
.map_err(Error::Crypto)?
237+
{
219238
return Err(Error::InvalidMintSignature.into());
220239
}
221240

222-
let mint_sig: MintSignature = borsh::from_slice(&decoded_mint_sig).map_err(Error::Borsh)?;
241+
let proof = SignedIdentityProof {
242+
signature,
243+
witness: res.mint_node_id,
244+
};
245+
let data: EmailIdentityProofData =
246+
borsh::from_slice(&decoded_mint_sig).map_err(Error::Borsh)?;
223247

224-
Ok(mint_sig)
248+
Ok((proof, data))
225249
}
226250

227251
async fn send_bill_notification(
@@ -243,13 +267,17 @@ impl EmailClientApi for EmailClient {
243267
};
244268

245269
let serialized = borsh::to_vec(&pl).map_err(Error::Borsh)?;
246-
let signature = sign_payload(&serialized, private_key);
270+
let hash = Sha256Hash::from_bytes(&serialized);
271+
let signature = SchnorrSignature::sign(&hash, private_key).map_err(Error::Crypto)?;
247272
let payload = base58::encode(&serialized);
248273

249-
let req = NotificationSendRequest { payload, signature };
274+
let req = NotificationSendRequest {
275+
payload,
276+
signature: signature.as_sig(),
277+
};
250278

251279
self.cl
252-
.post(to_url(mint_url, "notifications/v1/send")?)
280+
.post(to_url(mint_url, "v1/ens/email/send")?)
253281
.json(&req)
254282
.send()
255283
.await?
@@ -265,12 +293,9 @@ impl EmailClientApi for EmailClient {
265293
company_node_id: &Option<NodeId>,
266294
private_key: &SecretKey,
267295
) -> Result<url::Url> {
268-
let challenge = self.get_ens_challenge(mint_url, node_id).await?;
269-
let decoded_challenge = base58::decode(&challenge).map_err(Error::Base58)?;
270-
271-
let key_pair = Keypair::from_secret_key(SECP256K1, private_key);
272-
let msg = Message::from_digest_slice(&decoded_challenge).map_err(Error::Signature)?;
273-
let signed_challenge = SECP256K1.sign_schnorr(&msg, &key_pair);
296+
let signed_challenge = self
297+
.get_ens_challenge(mint_url, node_id, private_key)
298+
.await?;
274299

275300
let req = GetEmailPreferencesLinkRequest {
276301
node_id: node_id.to_owned(),
@@ -291,20 +316,6 @@ impl EmailClientApi for EmailClient {
291316
}
292317
}
293318

294-
pub fn sign_payload(req: &[u8], private_key: &SecretKey) -> Signature {
295-
let key_pair = Keypair::from_secret_key(SECP256K1, private_key);
296-
let hash: sha256::Hash = sha256::Hash::hash(req);
297-
let req = Message::from_digest(*hash.as_ref());
298-
299-
SECP256K1.sign_schnorr(&req, &key_pair)
300-
}
301-
302-
pub fn verify_request(payload: &[u8], signature: &Signature, key: &XOnlyPublicKey) -> Result<bool> {
303-
let hash = sha256::Hash::hash(payload);
304-
let msg = Message::from_digest(*hash.as_ref());
305-
Ok(SECP256K1.verify_schnorr(signature, &msg, key).is_ok())
306-
}
307-
308319
#[derive(Debug, Serialize)]
309320
pub struct StartEmailRegisterRequest {
310321
pub node_id: NodeId,
@@ -313,7 +324,7 @@ pub struct StartEmailRegisterRequest {
313324
#[derive(Debug, Deserialize)]
314325
pub struct StartEmailRegisterResponse {
315326
pub challenge: String,
316-
pub ttl_seconds: u32,
327+
pub ttl: u32,
317328
}
318329

319330
#[derive(Debug, Serialize)]
@@ -341,7 +352,7 @@ pub struct EmailConfirmPayload {
341352

342353
#[derive(Debug, Deserialize)]
343354
pub struct EmailConfirmResponse {
344-
/// A borsh-encoded MintSignature
355+
/// A borsh-encoded EmailIdentityProofData
345356
pub payload: String,
346357
/// The mint signature of the payload
347358
pub signature: Signature,

crates/bcr-ebill-api/src/lib.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,8 @@ pub struct CourtConfig {
7272
pub struct DevModeConfig {
7373
/// Whether dev mode is on
7474
pub on: bool,
75+
/// Whether mandatory email confirmations should be enabled (disable for easier testing)
76+
pub disable_mandatory_email_confirmations: bool,
7577
}
7678

7779
/// Payment specific configuration

0 commit comments

Comments
 (0)