use affinidi_bbs::PublicKey;
use affinidi_data_integrity::bbs_2023_transform as bbs_tx;
use affinidi_did_resolver_cache_sdk::DIDCacheClient;
use base64::Engine;
use base64::engine::general_purpose::URL_SAFE_NO_PAD;
use chrono::{DateTime, Utc};
use serde_json::Value;
use vti_common::error::AppError;
use vti_common::store::KeyspaceHandle;
use super::model::{
BBS_PROVER_NYM_TAG, BBS_SECRET_PROVER_BLIND_TAG, CredentialFormat, CredentialStatus,
StoredCredential,
};
use super::receive::{Provenance, di_temporal_valid, extract_types, infer_purpose};
use super::storage;
const BLS12381_G2_LEN: usize = 96;
pub fn g2_public_key(bytes: &[u8]) -> Result<PublicKey, AppError> {
let arr: [u8; BLS12381_G2_LEN] = bytes.try_into().map_err(|_| {
AppError::Validation(format!(
"BBS issuer key must be {BLS12381_G2_LEN} bytes (compressed G2), got {}",
bytes.len()
))
})?;
PublicKey::from_bytes(&arr)
.map_err(|e| AppError::Validation(format!("invalid BBS issuer key: {e}")))
}
pub fn g2_issuer_key_from_did_key(issuer_did: &str) -> Result<PublicKey, AppError> {
let bytes = affinidi_crypto::bls12381::did_key_to_g2_pub(issuer_did).map_err(|e| {
AppError::Validation(format!("issuer `{issuer_did}` is not a BBS did:key: {e}"))
})?;
g2_public_key(&bytes)
}
pub async fn resolve_bbs_issuer_key(
did_resolver: Option<&DIDCacheClient>,
credential: &Value,
) -> Result<[u8; BLS12381_G2_LEN], AppError> {
let issuer_did = crate::vault::di_verify::credential_issuer(credential)
.ok_or_else(|| AppError::Validation("bbs-2023 credential has no `issuer`".into()))?;
let vm = credential
.get("proof")
.and_then(|p| p.get("verificationMethod"))
.and_then(Value::as_str)
.ok_or_else(|| AppError::Validation("bbs-2023 proof has no `verificationMethod`".into()))?;
if vm.split('#').next().unwrap_or_default() != issuer_did {
return Err(AppError::Validation(format!(
"bbs-2023 proof verificationMethod `{vm}` is not under the credential issuer \
`{issuer_did}`"
)));
}
if issuer_did.starts_with("did:key:") {
return affinidi_crypto::bls12381::did_key_to_g2_pub(&issuer_did).map_err(|e| {
AppError::Validation(format!("issuer `{issuer_did}` is not a BBS did:key: {e}"))
});
}
let resolver = did_resolver.ok_or_else(|| {
AppError::Validation(format!(
"resolving issuer `{issuer_did}` needs a DID resolver for did:webvh / did:web BBS \
issuers"
))
})?;
let resolved = resolver.resolve(&issuer_did).await.map_err(|e| {
AppError::Validation(format!("issuer DID `{issuer_did}` did not resolve: {e}"))
})?;
let doc: Value = serde_json::to_value(&resolved.doc)
.map_err(|e| AppError::Internal(format!("issuer DID document serialise failed: {e}")))?;
let vms = doc
.get("verificationMethod")
.and_then(Value::as_array)
.ok_or_else(|| {
AppError::Validation(format!(
"issuer DID `{issuer_did}` has no verificationMethod array"
))
})?;
let relative = vm
.split_once('#')
.map(|(_, f)| format!("#{f}"))
.unwrap_or_default();
let entry = vms
.iter()
.find(|e| {
let id = e.get("id").and_then(Value::as_str).unwrap_or("");
id == vm || id == relative
})
.ok_or_else(|| {
AppError::Validation(format!(
"verificationMethod `{vm}` not found in issuer DID `{issuer_did}`"
))
})?;
let multibase = entry
.get("publicKeyMultibase")
.and_then(Value::as_str)
.ok_or_else(|| {
AppError::Validation(format!(
"verificationMethod `{vm}` has no publicKeyMultibase (BLS12-381 G2 Multikey)"
))
})?;
affinidi_crypto::bls12381::did_key_to_g2_pub(&format!("did:key:{multibase}")).map_err(|e| {
AppError::Validation(format!(
"verificationMethod `{vm}` is not a BLS12-381 G2 Multikey: {e}"
))
})
}
pub async fn receive_bbs(
vault: &KeyspaceHandle,
id: &str,
vc_json: &[u8],
issuer_pub: &[u8],
source: Provenance,
now: DateTime<Utc>,
) -> Result<StoredCredential, AppError> {
if id.trim().is_empty() {
return Err(AppError::Validation(
"credential id must be non-empty".to_string(),
));
}
let pk = g2_public_key(issuer_pub)?;
let vc: Value = serde_json::from_slice(vc_json)
.map_err(|e| AppError::Validation(format!("malformed BBS VC JSON: {e}")))?;
const BASE_CHECK_NONCE: &[u8] = b"vta-vault-base-check";
let full = bbs_tx::create_derived_proof(&vc, &[""], BASE_CHECK_NONCE, &pk)
.map_err(|e| AppError::Validation(format!("BBS base proof is malformed: {e}")))?;
if !bbs_tx::verify_derived_proof(&full, &pk)
.map_err(|e| AppError::Validation(format!("BBS base proof verification failed: {e}")))?
{
return Err(AppError::Validation(
"BBS issuer base proof did not verify".to_string(),
));
}
di_temporal_valid(&vc, now)?;
let cred = build_stored_bbs(&vc, id, vc_json, source, now);
storage::put(vault, &cred).await?;
Ok(cred)
}
fn build_stored_bbs(
vc: &Value,
id: &str,
vc_json: &[u8],
source: Provenance,
now: DateTime<Utc>,
) -> StoredCredential {
let types = extract_types(vc);
let subject_did = vc
.get("credentialSubject")
.and_then(|s| s.get("id"))
.and_then(Value::as_str)
.map(str::to_string);
let issuer_did = vc.get("issuer").and_then(|i| {
i.as_str()
.map(str::to_string)
.or_else(|| i.get("id").and_then(Value::as_str).map(str::to_string))
});
let purpose = infer_purpose(&types);
let valid_from = vc
.get("validFrom")
.and_then(Value::as_str)
.map(str::to_string);
let valid_until = vc
.get("validUntil")
.and_then(Value::as_str)
.map(str::to_string);
StoredCredential {
id: id.to_string(),
format: CredentialFormat::Bbs2023,
types,
schema_id: None,
community_did: None,
subject_did,
issuer_did,
purpose,
status: CredentialStatus::Valid,
valid_from,
valid_until,
received_at: now.to_rfc3339(),
source,
tags: std::collections::BTreeMap::new(),
body: vc_json.to_vec(),
lifecycle: vti_common::vault::VaultStatus::Active,
archived_at: None,
deleted_at: None,
grace_until: None,
}
}
#[allow(clippy::too_many_arguments)]
pub async fn receive_bbs_pseudonym(
vault: &KeyspaceHandle,
id: &str,
vc_json: &[u8],
issuer_pub: &[u8],
prover_nym: &[u8],
secret_prover_blind: &[u8],
source: Provenance,
now: DateTime<Utc>,
) -> Result<StoredCredential, AppError> {
if id.trim().is_empty() {
return Err(AppError::Validation(
"credential id must be non-empty".to_string(),
));
}
let pk = g2_public_key(issuer_pub)?;
let vc: Value = serde_json::from_slice(vc_json)
.map_err(|e| AppError::Validation(format!("malformed BBS VC JSON: {e}")))?;
const BASE_CHECK_NONCE: &[u8] = b"vta-vault-base-check";
const BASE_CHECK_VERIFIER: &str = "vta-vault-base-check";
let full = bbs_tx::create_pseudonym_derived_proof(
&vc,
&[""],
BASE_CHECK_NONCE,
&pk,
prover_nym,
secret_prover_blind,
BASE_CHECK_VERIFIER,
)
.map_err(|e| AppError::Validation(format!("BBS pseudonym base proof is malformed: {e}")))?;
if !bbs_tx::verify_pseudonym_derived_proof(&full, &pk, BASE_CHECK_VERIFIER).map_err(|e| {
AppError::Validation(format!("BBS pseudonym base proof verification failed: {e}"))
})? {
return Err(AppError::Validation(
"BBS issuer pseudonym base proof did not verify".to_string(),
));
}
di_temporal_valid(&vc, now)?;
let mut cred = build_stored_bbs(&vc, id, vc_json, source, now);
cred.tags.insert(
BBS_PROVER_NYM_TAG.to_string(),
URL_SAFE_NO_PAD.encode(prover_nym),
);
cred.tags.insert(
BBS_SECRET_PROVER_BLIND_TAG.to_string(),
URL_SAFE_NO_PAD.encode(secret_prover_blind),
);
storage::put(vault, &cred).await?;
Ok(cred)
}
#[allow(clippy::too_many_arguments)]
pub async fn present_bbs(
vault: &KeyspaceHandle,
credential_id: &str,
consent_record_id: &str,
issuer_pub: &[u8],
nonce: &str,
aud: &str,
status_resolver: Option<&dyn super::status::StatusListResolver>,
now: DateTime<Utc>,
) -> Result<String, AppError> {
let (cred, record) = super::present::gate_present(
vault,
credential_id,
consent_record_id,
aud,
status_resolver,
now,
)
.await?;
if cred.format != CredentialFormat::Bbs2023 {
return Err(AppError::Validation(format!(
"credential `{credential_id}` is not a bbs-2023 credential (format {:?}); \
cannot present via present_bbs",
cred.format
)));
}
let pk = g2_public_key(issuer_pub)?;
let vc: Value = serde_json::from_slice(&cred.body)
.map_err(|e| AppError::Validation(format!("stored BBS VC body is not JSON: {e}")))?;
let selective: Vec<String> = record
.process
.personal_data
.iter()
.map(|name| format!("/credentialSubject/{}", rfc6901_escape(name)))
.collect();
let selective_refs: Vec<&str> = selective.iter().map(String::as_str).collect();
let derived = match holder_pseudonym_secrets(&cred)? {
Some((prover_nym, secret_prover_blind)) => bbs_tx::create_pseudonym_derived_proof(
&vc,
&selective_refs,
nonce.as_bytes(),
&pk,
&prover_nym,
&secret_prover_blind,
aud,
)
.map_err(|e| AppError::Validation(format!("BBS holder-bound disclosure failed: {e}")))?,
None => bbs_tx::create_derived_proof(&vc, &selective_refs, nonce.as_bytes(), &pk)
.map_err(|e| AppError::Validation(format!("BBS selective disclosure failed: {e}")))?,
};
serde_json::to_string(&derived)
.map_err(|e| AppError::Internal(format!("serialise BBS presentation: {e}")))
}
fn holder_pseudonym_secrets(
cred: &StoredCredential,
) -> Result<Option<(Vec<u8>, Vec<u8>)>, AppError> {
let decode = |k: &str, v: &str| {
URL_SAFE_NO_PAD
.decode(v)
.map_err(|e| AppError::Validation(format!("bbs `{k}` tag is not base64url: {e}")))
};
match (
cred.tags.get(BBS_PROVER_NYM_TAG),
cred.tags.get(BBS_SECRET_PROVER_BLIND_TAG),
) {
(None, None) => Ok(None),
(Some(nym), Some(blind)) => Ok(Some((
decode(BBS_PROVER_NYM_TAG, nym)?,
decode(BBS_SECRET_PROVER_BLIND_TAG, blind)?,
))),
_ => Err(AppError::Validation(
"bbs-2023 credential has only one of the two pseudonym holder secrets — both \
`bbs:prover_nym` and `bbs:secret_prover_blind` are required for holder binding"
.into(),
)),
}
}
fn rfc6901_escape(token: &str) -> String {
token.replace('~', "~0").replace('/', "~1")
}
#[cfg(test)]
#[allow(clippy::too_many_arguments)]
pub(crate) fn issue_bbs_pseudonym_for_test(
vc: &Value,
mandatory: &[&str],
verification_method: &str,
created: &str,
sk: &affinidi_bbs::SecretKey,
pk: &PublicKey,
hmac_key: &[u8],
prover_nym_bytes: &[u8; 32],
signer_nym_entropy_bytes: &[u8; 32],
) -> (Value, Vec<u8>, Vec<u8>) {
use affinidi_bbs as bbs;
let prover_nym =
bbs::hash::scalar_from_bytes(prover_nym_bytes).expect("prover_nym is a valid scalar");
let (commitment_with_proof, secret_prover_blind) =
bbs::nym_commit(prover_nym, &[], bbs::Ciphersuite::default()).expect("nym_commit");
let secret_prover_blind_bytes = bbs::hash::scalar_to_bytes(&secret_prover_blind);
let context = vc.get("@context").cloned().expect("vc has @context");
let proof_config = serde_json::json!({
"type": "DataIntegrityProof",
"cryptosuite": "bbs-2023",
"created": created,
"verificationMethod": verification_method,
"proofPurpose": "assertionMethod",
"@context": context,
});
let proof_value = bbs_tx::create_pseudonym_base_proof_value(
vc,
&proof_config,
mandatory,
sk,
pk,
hmac_key,
&commitment_with_proof,
signer_nym_entropy_bytes,
)
.expect("create pseudonym base proof");
let mut proof = proof_config;
let obj = proof.as_object_mut().unwrap();
obj.remove("@context");
obj.insert("proofValue".to_string(), Value::String(proof_value));
let mut base = vc.clone();
base.as_object_mut()
.unwrap()
.insert("proof".to_string(), proof);
(
base,
prover_nym_bytes.to_vec(),
secret_prover_blind_bytes.to_vec(),
)
}
#[cfg(test)]
mod tests {
use super::*;
use affinidi_bbs as bbs;
use affinidi_data_integrity::bbs_2023_transform::sign_base_document;
use serde_json::json;
use vti_common::config::StoreConfig;
use vti_common::store::Store;
const MANDATORY: &[&str] = &["/@context", "/type", "/issuer", "/credentialSubject/id"];
const TEST_HMAC: &[u8; 32] = b"vta-bbs-test-hmac-key-32-bytes!!";
const TEST_CREATED: &str = "2020-01-01T00:00:00Z";
fn fresh_vault() -> (tempfile::TempDir, Store, KeyspaceHandle) {
let dir = tempfile::tempdir().expect("tempdir");
let store = Store::open(&StoreConfig {
data_dir: dir.path().to_path_buf(),
})
.expect("open store");
let ks = store
.keyspace(crate::keyspaces::VAULT)
.expect("vault keyspace");
(dir, store, ks)
}
fn issuer_keys() -> (bbs::SecretKey, bbs::PublicKey) {
let sk = bbs::keygen(b"vta-bbs-test-key-material-32byte", b"").unwrap();
let pk = bbs::sk_to_pk(&sk);
(sk, pk)
}
fn issuer_did(pk: &bbs::PublicKey) -> String {
affinidi_crypto::bls12381::g2_pub_to_did_key(&pk.to_bytes())
}
fn signed_bbs_vc(valid_until: Option<&str>) -> (Vec<u8>, bbs::PublicKey) {
let (sk, pk) = issuer_keys();
let did = issuer_did(&pk);
let mut vc = json!({
"@context": [
"https://www.w3.org/ns/credentials/v2",
"https://www.w3.org/ns/credentials/examples/v2"
],
"type": ["VerifiableCredential", "MembershipCredential"],
"issuer": did,
"validFrom": "2020-01-01T00:00:00Z",
"credentialSubject": { "id": "did:key:zMember", "givenName": "Alice", "memberLevel": "gold" }
});
if let Some(u) = valid_until {
vc["validUntil"] = json!(u);
}
let vm = format!("{did}#bbs-key-0");
let signed =
sign_base_document(&vc, MANDATORY, &vm, TEST_CREATED, &sk, &pk, TEST_HMAC).unwrap();
(serde_json::to_vec(&signed).unwrap(), pk)
}
#[tokio::test]
async fn receives_and_stores_a_valid_bbs_credential() {
let (_dir, _store, vault) = fresh_vault();
let (vc, pk) = signed_bbs_vc(Some("2100-01-01T00:00:00Z"));
let cred = receive_bbs(&vault, "bbs-1", &vc, &pk.to_bytes(), None, Utc::now())
.await
.expect("receive valid BBS VC");
assert_eq!(cred.format, CredentialFormat::Bbs2023);
assert_eq!(cred.subject_did.as_deref(), Some("did:key:zMember"));
assert!(cred.types.contains(&"MembershipCredential".to_string()));
assert!(
storage::get(&vault, "bbs-1").await.unwrap().is_some(),
"credential must be stored"
);
}
#[tokio::test]
async fn rejects_a_tampered_bbs_credential() {
let (_dir, _store, vault) = fresh_vault();
let (vc, pk) = signed_bbs_vc(None);
let mut v: Value = serde_json::from_slice(&vc).unwrap();
v["credentialSubject"]["memberLevel"] = json!("platinum"); let tampered = serde_json::to_vec(&v).unwrap();
let err = receive_bbs(&vault, "bbs-x", &tampered, &pk.to_bytes(), None, Utc::now())
.await
.expect_err("a tampered BBS VC must be rejected");
assert!(matches!(err, AppError::Validation(_)), "{err:?}");
assert!(
storage::get(&vault, "bbs-x").await.unwrap().is_none(),
"a rejected credential must not be stored"
);
}
#[test]
fn tampered_disclosure_is_rejected_upstream_381() {
let (sk, pk) = issuer_keys();
let did = issuer_did(&pk);
let sig = bbs::sign(&sk, &pk, b"hdr", &[b"m0".as_ref(), b"gold"]).unwrap();
let proof = bbs::proof_gen(
&pk,
&sig,
b"hdr",
b"ph",
&[b"m0".as_ref(), b"platinum"],
&[1],
)
.unwrap();
assert!(
!bbs::proof_verify(&pk, &proof, b"hdr", b"ph", &[b"platinum".as_ref()], &[1]).unwrap(),
"raw affinidi-bbs must reject a tampered disclosure"
);
let vc = json!({
"@context": [
"https://www.w3.org/ns/credentials/v2",
"https://www.w3.org/ns/credentials/examples/v2"
],
"type": ["VerifiableCredential", "ExampleMembershipCredential"],
"issuer": did,
"credentialSubject": { "id": "did:key:zMember", "memberLevel": "gold" }
});
let base = sign_base_document(
&vc,
MANDATORY,
&format!("{did}#bbs-key-0"),
TEST_CREATED,
&sk,
&pk,
TEST_HMAC,
)
.unwrap();
let honest =
bbs_tx::create_derived_proof(&base, &["/credentialSubject/memberLevel"], b"n", &pk)
.unwrap();
assert!(bbs_tx::verify_derived_proof(&honest, &pk).unwrap());
let mut tampered = base.clone();
tampered["credentialSubject"]["memberLevel"] = json!("platinum");
let forged =
bbs_tx::create_derived_proof(&tampered, &["/credentialSubject/memberLevel"], b"n", &pk)
.expect("derive (defined terms)");
assert!(
!bbs_tx::verify_derived_proof(&forged, &pk).unwrap_or(false),
"REGRESSION (affinidi-tdk-rs#381): bbs_2023_transform accepted a forged disclosed value"
);
let undefined = json!({
"@context": ["https://www.w3.org/ns/credentials/v2"],
"type": ["VerifiableCredential"],
"issuer": did,
"credentialSubject": { "id": "did:key:zMember", "memberLevel": "gold" }
});
assert!(
sign_base_document(
&undefined,
MANDATORY,
&format!("{did}#bbs-key-0"),
TEST_CREATED,
&sk,
&pk,
TEST_HMAC,
)
.is_err(),
"safe mode must refuse a credential with @context-undefined claim terms"
);
}
#[tokio::test]
async fn rejects_an_expired_bbs_credential() {
let (_dir, _store, vault) = fresh_vault();
let (vc, pk) = signed_bbs_vc(Some("2001-01-01T00:00:00Z"));
let err = receive_bbs(&vault, "bbs-exp", &vc, &pk.to_bytes(), None, Utc::now())
.await
.expect_err("an expired BBS VC must be rejected");
assert!(matches!(err, AppError::Validation(_)), "{err:?}");
}
#[tokio::test]
async fn rejects_a_wrong_issuer_key() {
let (_dir, _store, vault) = fresh_vault();
let (vc, _pk) = signed_bbs_vc(None);
let other = bbs::sk_to_pk(&bbs::keygen(b"another-bbs-key-material-32bytes", b"").unwrap());
let err = receive_bbs(&vault, "bbs-w", &vc, &other.to_bytes(), None, Utc::now())
.await
.expect_err("verification under the wrong issuer key must fail");
assert!(matches!(err, AppError::Validation(_)), "{err:?}");
}
#[test]
fn resolves_g2_issuer_key_from_did_key() {
let (_sk, pk) = issuer_keys();
let did = issuer_did(&pk);
let resolved = g2_issuer_key_from_did_key(&did).expect("resolve G2 did:key");
assert_eq!(resolved.to_bytes(), pk.to_bytes());
}
fn pseudonym_bbs_vc() -> (Vec<u8>, bbs::PublicKey, [u8; 32], Vec<u8>) {
let (sk, pk) = issuer_keys();
let did = issuer_did(&pk);
let vc = json!({
"@context": [
"https://www.w3.org/ns/credentials/v2",
"https://www.w3.org/ns/credentials/examples/v2"
],
"type": ["VerifiableCredential", "MembershipCredential"],
"issuer": did,
"validFrom": "2020-01-01T00:00:00Z",
"credentialSubject": { "id": "did:key:zMember", "givenName": "Alice", "memberLevel": "gold" }
});
let vm = format!("{did}#bbs-key-0");
let prover_nym = [0x11u8; 32];
let entropy = [0x22u8; 32];
let (base, nym, blind) = issue_bbs_pseudonym_for_test(
&vc,
MANDATORY,
&vm,
TEST_CREATED,
&sk,
&pk,
TEST_HMAC,
&prover_nym,
&entropy,
);
assert_eq!(nym, prover_nym.to_vec());
(serde_json::to_vec(&base).unwrap(), pk, prover_nym, blind)
}
#[tokio::test]
async fn receives_and_stores_a_pseudonym_bbs_credential() {
let (_dir, _store, vault) = fresh_vault();
let (vc, pk, nym, blind) = pseudonym_bbs_vc();
let cred = receive_bbs_pseudonym(
&vault,
"bbs-nym",
&vc,
&pk.to_bytes(),
&nym,
&blind,
None,
Utc::now(),
)
.await
.expect("receive pseudonym BBS VC");
assert_eq!(cred.format, CredentialFormat::Bbs2023);
assert!(
cred.tags.contains_key(BBS_PROVER_NYM_TAG)
&& cred.tags.contains_key(BBS_SECRET_PROVER_BLIND_TAG),
"holder pseudonym secrets must be persisted"
);
let stored = storage::get(&vault, "bbs-nym")
.await
.unwrap()
.expect("stored");
assert_eq!(
stored.tags.get(BBS_PROVER_NYM_TAG),
cred.tags.get(BBS_PROVER_NYM_TAG),
"secrets round-trip through the store"
);
assert_eq!(
holder_pseudonym_secrets(&stored).unwrap(),
Some((nym.to_vec(), blind))
);
}
#[tokio::test]
async fn rejects_pseudonym_base_with_wrong_holder_secret() {
let (_dir, _store, vault) = fresh_vault();
let (vc, pk, _nym, blind) = pseudonym_bbs_vc();
let wrong_nym = [0x33u8; 32];
let err = receive_bbs_pseudonym(
&vault,
"bbs-bad",
&vc,
&pk.to_bytes(),
&wrong_nym,
&blind,
None,
Utc::now(),
)
.await
.expect_err("a wrong holder link secret must fail the base check");
assert!(matches!(err, AppError::Validation(_)), "{err:?}");
assert!(
storage::get(&vault, "bbs-bad").await.unwrap().is_none(),
"a rejected credential must not be stored"
);
}
#[test]
fn half_a_pseudonym_secret_pair_is_an_error() {
let mut cred = StoredCredential {
id: "x".into(),
format: CredentialFormat::Bbs2023,
types: vec![],
schema_id: None,
community_did: None,
subject_did: None,
issuer_did: None,
purpose: None,
status: CredentialStatus::Valid,
valid_from: None,
valid_until: None,
received_at: Utc::now().to_rfc3339(),
source: None,
tags: std::collections::BTreeMap::new(),
body: vec![],
lifecycle: vti_common::vault::VaultStatus::Active,
archived_at: None,
deleted_at: None,
grace_until: None,
};
assert_eq!(holder_pseudonym_secrets(&cred).unwrap(), None);
cred.tags.insert(
BBS_PROVER_NYM_TAG.to_string(),
URL_SAFE_NO_PAD.encode([1u8; 32]),
);
assert!(holder_pseudonym_secrets(&cred).is_err());
}
}