use std::collections::HashMap;
use chrono::Utc;
use bsv::auth::certificates::certificate::AuthCertificate;
use bsv::auth::certificates::MasterCertificate;
use bsv::auth::utils::nonce::{create_nonce, verify_nonce};
use bsv::primitives::public_key::PublicKey;
use bsv::wallet::interfaces::{
AcquireCertificateArgs, Certificate as SdkCertificate, CertificateType, GetPublicKeyArgs,
KeyringRevealer, ProveCertificateArgs, ProveCertificateResult, SerialNumber, VerifyHmacArgs,
WalletInterface,
};
use bsv::wallet::types::{Counterparty, CounterpartyType, Protocol};
use crate::error::{WalletError, WalletResult};
use crate::storage::find_args::{
CertificateFieldPartial, CertificatePartial, FindCertificateFieldsArgs, FindCertificatesArgs,
Paged,
};
use crate::storage::manager::WalletStorageManager;
use crate::tables::{Certificate, CertificateField};
use crate::wallet::types::AuthId;
#[derive(Debug, Clone)]
pub struct AcquireCertificateResult {
pub cert_type: String,
pub subject: String,
pub serial_number: String,
pub certifier: String,
pub revocation_outpoint: String,
pub signature: String,
pub fields: HashMap<String, String>,
}
pub async fn acquire_direct_certificate<W: WalletInterface + ?Sized>(
storage: &WalletStorageManager,
wallet: &W,
auth: &AuthId,
args: &AcquireCertificateArgs,
) -> WalletResult<AcquireCertificateResult> {
let now = Utc::now().naive_utc();
let verifier = match &args.keyring_revealer {
Some(KeyringRevealer::Certifier) => Some(args.certifier.to_der_hex()),
Some(KeyringRevealer::PubKey(pk)) => Some(pk.to_der_hex()),
None => None,
};
let cert_type_str = cert_type_to_string(&args.cert_type);
let serial_number_str = serial_number_to_string(
args.serial_number
.as_ref()
.unwrap_or(&SerialNumber([0u8; 32])),
);
let certifier_str = args.certifier.to_der_hex();
let subject_str = get_identity_key(wallet).await?;
let revocation_outpoint_str = args.revocation_outpoint.clone().unwrap_or_default();
let signature_str = args
.signature
.as_ref()
.map(|s| hex_encode(s))
.unwrap_or_default();
let active = storage
.active()
.ok_or_else(|| WalletError::InvalidOperation("No active storage provider".to_string()))?;
let user = active
.find_user_by_identity_key(&auth.identity_key)
.await?
.ok_or_else(|| WalletError::Unauthorized("User not found".to_string()))?;
let new_cert = Certificate {
created_at: now,
updated_at: now,
certificate_id: 0, user_id: user.user_id,
cert_type: cert_type_str.clone(),
serial_number: serial_number_str.clone(),
certifier: certifier_str.clone(),
subject: subject_str.clone(),
verifier,
revocation_outpoint: revocation_outpoint_str.clone(),
signature: signature_str.clone(),
is_deleted: false,
};
let cert_id = active.insert_certificate_storage(&new_cert).await?;
let keyring = args.keyring_for_subject.clone().unwrap_or_default();
for (field_name, field_value) in &args.fields {
let master_key = keyring.get(field_name).cloned().unwrap_or_default();
let field_record = CertificateField {
created_at: now,
updated_at: now,
user_id: user.user_id,
certificate_id: cert_id,
field_name: field_name.clone(),
field_value: field_value.clone(),
master_key,
};
active
.insert_certificate_field_storage(&field_record)
.await?;
}
Ok(AcquireCertificateResult {
cert_type: cert_type_str,
subject: subject_str,
serial_number: serial_number_str,
certifier: certifier_str,
revocation_outpoint: revocation_outpoint_str,
signature: signature_str,
fields: args.fields.clone(),
})
}
pub async fn prove_certificate<W: WalletInterface + ?Sized>(
storage: &WalletStorageManager,
wallet: &W,
auth: &AuthId,
args: &ProveCertificateArgs,
) -> WalletResult<ProveCertificateResult> {
let active = storage
.active()
.ok_or_else(|| WalletError::InvalidOperation("No active storage provider".to_string()))?;
let user = active
.find_user_by_identity_key(&auth.identity_key)
.await?
.ok_or_else(|| WalletError::Unauthorized("User not found".to_string()))?;
let cert_type =
args.certificate
.cert_type
.as_ref()
.ok_or_else(|| WalletError::InvalidParameter {
parameter: "certificate.cert_type".to_string(),
must_be: "provided".to_string(),
})?;
let serial_number =
args.certificate
.serial_number
.as_ref()
.ok_or_else(|| WalletError::InvalidParameter {
parameter: "certificate.serial_number".to_string(),
must_be: "provided".to_string(),
})?;
let certifier =
args.certificate
.certifier
.as_ref()
.ok_or_else(|| WalletError::InvalidParameter {
parameter: "certificate.certifier".to_string(),
must_be: "provided".to_string(),
})?;
let cert_type_str = cert_type_to_string(cert_type);
let serial_number_str = serial_number_to_string(serial_number);
let certifier_str = certifier.to_der_hex();
let find_args = FindCertificatesArgs {
partial: CertificatePartial {
user_id: Some(user.user_id),
cert_type: Some(cert_type_str),
serial_number: Some(serial_number_str),
certifier: Some(certifier_str),
is_deleted: Some(false),
..Default::default()
},
paged: Some(Paged {
limit: 2,
offset: 0,
}),
..Default::default()
};
let certs = active.find_certificates_storage(&find_args).await?;
if certs.len() != 1 {
return Err(WalletError::InvalidParameter {
parameter: "args".to_string(),
must_be: "a unique certificate match".to_string(),
});
}
let storage_cert = &certs[0];
let fields = active
.find_certificate_fields(&FindCertificateFieldsArgs {
partial: CertificateFieldPartial {
certificate_id: Some(storage_cert.certificate_id),
user_id: Some(user.user_id),
..Default::default()
},
..Default::default()
})
.await?;
let field_map: HashMap<String, String> = fields
.iter()
.map(|f| (f.field_name.clone(), f.field_value.clone()))
.collect();
let keyring: HashMap<String, String> = fields
.iter()
.map(|f| (f.field_name.clone(), f.master_key.clone()))
.collect();
let certifier_pk = PublicKey::from_string(&storage_cert.certifier)
.map_err(|e| WalletError::Internal(format!("Invalid certifier key: {}", e)))?;
let subject =
args.certificate
.subject
.as_ref()
.ok_or_else(|| WalletError::InvalidParameter {
parameter: "certificate.subject".to_string(),
must_be: "provided".to_string(),
})?;
let sdk_cert = SdkCertificate {
cert_type: cert_type.clone(),
serial_number: serial_number.clone(),
subject: subject.clone(),
certifier: certifier_pk.clone(),
revocation_outpoint: Some(storage_cert.revocation_outpoint.clone()),
fields: Some(field_map),
signature: args.certificate.signature.clone(),
};
let master_cert = MasterCertificate::new(sdk_cert, keyring)
.map_err(|e| WalletError::Internal(format!("Failed to create MasterCertificate: {}", e)))?;
let keyring_for_verifier = master_cert
.create_keyring_for_verifier(
&args.verifier,
&args.fields_to_reveal,
&certifier_pk,
wallet,
)
.await
.map_err(|e| {
WalletError::Internal(format!("Failed to create keyring for verifier: {}", e))
})?;
Ok(ProveCertificateResult {
keyring_for_verifier,
certificate: None,
verifier: None,
})
}
pub async fn acquire_issuance_certificate<W: WalletInterface + ?Sized>(
storage: &WalletStorageManager,
wallet: &W,
auth: &AuthId,
args: &AcquireCertificateArgs,
) -> WalletResult<AcquireCertificateResult> {
let certifier_url =
args.certifier_url
.as_deref()
.ok_or_else(|| WalletError::InvalidParameter {
parameter: "certifierUrl".to_string(),
must_be: "required for issuance protocol".to_string(),
})?;
let client_nonce = create_nonce(wallet)
.await
.map_err(|e| WalletError::Internal(format!("Failed to create nonce: {}", e)))?;
let certifier_pk = &args.certifier;
let (certificate_fields, master_keyring) =
MasterCertificate::create_certificate_fields(&args.fields, wallet, certifier_pk)
.await
.map_err(|e| {
WalletError::Internal(format!("Failed to create certificate fields: {}", e))
})?;
let cert_type_b64 = base64_encode(&args.cert_type.0);
let request_body = serde_json::json!({
"clientNonce": client_nonce,
"type": cert_type_b64,
"fields": certificate_fields,
"masterKeyring": master_keyring,
});
let url = format!("{}/signCertificate", certifier_url.trim_end_matches('/'));
let http_client = reqwest::Client::new();
let response = http_client
.post(&url)
.header("Content-Type", "application/json")
.json(&request_body)
.send()
.await
.map_err(|e| WalletError::Internal(format!("HTTP request to certifier failed: {}", e)))?;
let identity_header = response
.headers()
.get("x-bsv-auth-identity-key")
.and_then(|v| v.to_str().ok())
.map(|s| s.to_string());
let expected_certifier_hex = certifier_pk.to_der_hex();
if let Some(ref header_key) = identity_header {
if header_key != &expected_certifier_hex {
return Err(WalletError::Internal(format!(
"Invalid certifier! Expected: {}, Received: {}",
expected_certifier_hex, header_key
)));
}
}
let response_body: serde_json::Value = response
.json()
.await
.map_err(|e| WalletError::Internal(format!("Failed to parse certifier response: {}", e)))?;
let certificate_json = response_body.get("certificate").ok_or_else(|| {
WalletError::Internal("No certificate received from certifier!".to_string())
})?;
let server_nonce = response_body
.get("serverNonce")
.and_then(|v| v.as_str())
.ok_or_else(|| {
WalletError::Internal("No serverNonce received from certifier!".to_string())
})?
.to_string();
let nonce_valid = verify_nonce(wallet, &server_nonce)
.await
.map_err(|e| WalletError::Internal(format!("Failed to verify server nonce: {}", e)))?;
if !nonce_valid {
return Err(WalletError::Internal(
"Server nonce verification failed".to_string(),
));
}
let resp_type = certificate_json
.get("type")
.and_then(|v| v.as_str())
.unwrap_or_default();
let resp_serial = certificate_json
.get("serialNumber")
.and_then(|v| v.as_str())
.unwrap_or_default();
let resp_subject = certificate_json
.get("subject")
.and_then(|v| v.as_str())
.unwrap_or_default();
let resp_certifier = certificate_json
.get("certifier")
.and_then(|v| v.as_str())
.unwrap_or_default();
let resp_revocation = certificate_json
.get("revocationOutpoint")
.and_then(|v| v.as_str())
.unwrap_or_default();
let resp_signature = certificate_json
.get("signature")
.and_then(|v| v.as_str())
.unwrap_or_default();
let resp_fields: HashMap<String, String> = certificate_json
.get("fields")
.and_then(|v| serde_json::from_value(v.clone()).ok())
.unwrap_or_default();
let serial_bytes = base64_decode(resp_serial).unwrap_or_default();
let nonce_data = {
let mut d = base64_decode(&client_nonce).unwrap_or_default();
d.extend(base64_decode(&server_nonce).unwrap_or_default());
d
};
let hmac_result = wallet
.verify_hmac(
VerifyHmacArgs {
hmac: serial_bytes,
data: nonce_data,
protocol_id: Protocol {
security_level: 2,
protocol: "certificate issuance".to_string(),
},
key_id: format!("{}{}", server_nonce, client_nonce),
counterparty: Counterparty {
counterparty_type: CounterpartyType::Other,
public_key: Some(certifier_pk.clone()),
},
privileged: false,
privileged_reason: None,
seek_permission: None,
},
None,
)
.await
.map_err(sdk_err)?;
if !hmac_result.valid {
return Err(WalletError::Internal(
"Invalid serialNumber HMAC".to_string(),
));
}
if resp_type != cert_type_b64 {
return Err(WalletError::Internal(format!(
"Invalid certificate type! Expected: {}, Received: {}",
cert_type_b64, resp_type
)));
}
let identity_key = get_identity_key(wallet).await?;
if resp_subject != identity_key {
return Err(WalletError::Internal(format!(
"Invalid certificate subject! Expected: {}, Received: {}",
identity_key, resp_subject
)));
}
if resp_certifier != expected_certifier_hex {
return Err(WalletError::Internal(format!(
"Invalid certifier! Expected: {}, Received: {}",
expected_certifier_hex, resp_certifier
)));
}
if resp_revocation.is_empty() {
return Err(WalletError::Internal(
"Invalid revocationOutpoint!".to_string(),
));
}
if resp_fields.len() != certificate_fields.len() {
return Err(WalletError::Internal(
"Fields mismatch! Objects have different numbers of keys.".to_string(),
));
}
for (field_name, expected_value) in &certificate_fields {
match resp_fields.get(field_name) {
None => {
return Err(WalletError::Internal(format!(
"Missing field: {} in certificate.fields",
field_name
)));
}
Some(actual_value) => {
if actual_value != expected_value {
return Err(WalletError::Internal(format!(
"Invalid field! Expected: {}, Received: {}",
expected_value, actual_value
)));
}
}
}
}
let subject_pk = PublicKey::from_string(resp_subject)
.map_err(|e| WalletError::Internal(format!("Invalid subject in response: {}", e)))?;
let certifier_resp_pk = PublicKey::from_string(resp_certifier)
.map_err(|e| WalletError::Internal(format!("Invalid certifier in response: {}", e)))?;
let serial_number_arr = {
let decoded = base64_decode(resp_serial).unwrap_or_default();
let mut arr = [0u8; 32];
let len = decoded.len().min(32);
arr[..len].copy_from_slice(&decoded[..len]);
SerialNumber(arr)
};
let signature_bytes = if resp_signature.is_empty() {
None
} else {
Some(hex_decode_bytes(resp_signature))
};
let signed_cert = SdkCertificate {
cert_type: args.cert_type.clone(),
serial_number: serial_number_arr.clone(),
subject: subject_pk,
certifier: certifier_resp_pk,
revocation_outpoint: Some(resp_revocation.to_string()),
fields: Some(resp_fields.clone()),
signature: signature_bytes,
};
let sig_valid = AuthCertificate::verify(&signed_cert, wallet)
.await
.map_err(|e| WalletError::Internal(format!("Certificate verification failed: {}", e)))?;
if !sig_valid {
return Err(WalletError::Internal(
"Certificate signature verification failed".to_string(),
));
}
let master_cert = MasterCertificate::new(signed_cert.clone(), master_keyring.clone())
.map_err(|e| WalletError::Internal(format!("Failed to build MasterCertificate: {}", e)))?;
let _decrypted = master_cert
.decrypt_fields(wallet, certifier_pk)
.await
.map_err(|e| WalletError::Internal(format!("Field decryption test failed: {}", e)))?;
let store_args = AcquireCertificateArgs {
cert_type: args.cert_type.clone(),
certifier: args.certifier.clone(),
acquisition_protocol: args.acquisition_protocol.clone(),
fields: resp_fields,
serial_number: Some(serial_number_arr),
revocation_outpoint: Some(resp_revocation.to_string()),
signature: signed_cert.signature.clone(),
certifier_url: args.certifier_url.clone(),
keyring_revealer: Some(KeyringRevealer::Certifier),
keyring_for_subject: Some(master_keyring),
privileged: args.privileged,
privileged_reason: args.privileged_reason.clone(),
};
acquire_direct_certificate(storage, wallet, auth, &store_args).await
}
async fn get_identity_key<W: WalletInterface + ?Sized>(wallet: &W) -> WalletResult<String> {
let result = wallet
.get_public_key(
GetPublicKeyArgs {
identity_key: true,
protocol_id: None,
key_id: None,
counterparty: None,
privileged: false,
privileged_reason: None,
for_self: None,
seek_permission: None,
},
None,
)
.await
.map_err(|e| WalletError::Internal(format!("get_public_key failed: {}", e)))?;
Ok(result.public_key.to_der_hex())
}
fn sdk_err(e: bsv::wallet::error::WalletError) -> WalletError {
WalletError::Internal(e.to_string())
}
fn cert_type_to_string(ct: &CertificateType) -> String {
base64_encode(&ct.0)
}
fn serial_number_to_string(sn: &SerialNumber) -> String {
base64_encode(&sn.0)
}
fn hex_encode(data: &[u8]) -> String {
let mut s = String::with_capacity(data.len() * 2);
for byte in data {
s.push_str(&format!("{:02x}", byte));
}
s
}
fn hex_decode_bytes(hex: &str) -> Vec<u8> {
let mut bytes = Vec::with_capacity(hex.len() / 2);
let hex_bytes = hex.as_bytes();
let mut i = 0;
while i + 1 < hex_bytes.len() {
let hi = hex_nibble(hex_bytes[i]);
let lo = hex_nibble(hex_bytes[i + 1]);
bytes.push((hi << 4) | lo);
i += 2;
}
bytes
}
fn hex_nibble(c: u8) -> u8 {
match c {
b'0'..=b'9' => c - b'0',
b'a'..=b'f' => c - b'a' + 10,
b'A'..=b'F' => c - b'A' + 10,
_ => 0,
}
}
fn base64_encode(data: &[u8]) -> String {
const CHARS: &[u8] = b"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
let mut result = String::new();
for chunk in data.chunks(3) {
let b0 = chunk[0] as u32;
let b1 = if chunk.len() > 1 { chunk[1] as u32 } else { 0 };
let b2 = if chunk.len() > 2 { chunk[2] as u32 } else { 0 };
let triple = (b0 << 16) | (b1 << 8) | b2;
result.push(CHARS[((triple >> 18) & 0x3F) as usize] as char);
result.push(CHARS[((triple >> 12) & 0x3F) as usize] as char);
if chunk.len() > 1 {
result.push(CHARS[((triple >> 6) & 0x3F) as usize] as char);
} else {
result.push('=');
}
if chunk.len() > 2 {
result.push(CHARS[(triple & 0x3F) as usize] as char);
} else {
result.push('=');
}
}
result
}
fn base64_decode(s: &str) -> Result<Vec<u8>, WalletError> {
let mut result = Vec::new();
let bytes = s.as_bytes();
let mut i = 0;
while i < bytes.len() {
if bytes[i] == b'=' {
break;
}
let a = b64_val(bytes[i])?;
let b = if i + 1 < bytes.len() && bytes[i + 1] != b'=' {
b64_val(bytes[i + 1])?
} else {
0
};
let c = if i + 2 < bytes.len() && bytes[i + 2] != b'=' {
b64_val(bytes[i + 2])?
} else {
0
};
let d = if i + 3 < bytes.len() && bytes[i + 3] != b'=' {
b64_val(bytes[i + 3])?
} else {
0
};
let triple = ((a as u32) << 18) | ((b as u32) << 12) | ((c as u32) << 6) | (d as u32);
result.push(((triple >> 16) & 0xFF) as u8);
if i + 2 < bytes.len() && bytes[i + 2] != b'=' {
result.push(((triple >> 8) & 0xFF) as u8);
}
if i + 3 < bytes.len() && bytes[i + 3] != b'=' {
result.push((triple & 0xFF) as u8);
}
i += 4;
}
Ok(result)
}
fn b64_val(c: u8) -> Result<u8, WalletError> {
match c {
b'A'..=b'Z' => Ok(c - b'A'),
b'a'..=b'z' => Ok(c - b'a' + 26),
b'0'..=b'9' => Ok(c - b'0' + 52),
b'+' => Ok(62),
b'/' => Ok(63),
_ => Err(WalletError::Internal(format!(
"invalid base64 character: {}",
c as char
))),
}
}
#[cfg(test)]
mod tests {
use super::*;
async fn _check_direct<W: WalletInterface>(
s: &WalletStorageManager,
w: &W,
a: &AuthId,
args: &AcquireCertificateArgs,
) -> WalletResult<AcquireCertificateResult> {
acquire_direct_certificate(s, w, a, args).await
}
async fn _check_prove<W: WalletInterface>(
s: &WalletStorageManager,
w: &W,
a: &AuthId,
args: &ProveCertificateArgs,
) -> WalletResult<ProveCertificateResult> {
prove_certificate(s, w, a, args).await
}
async fn _check_issuance<W: WalletInterface>(
s: &WalletStorageManager,
w: &W,
a: &AuthId,
args: &AcquireCertificateArgs,
) -> WalletResult<AcquireCertificateResult> {
acquire_issuance_certificate(s, w, a, args).await
}
#[tokio::test]
async fn test_certificate_functions_are_callable() {
}
}