Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
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
8 changes: 8 additions & 0 deletions contracts/certificate/src/errors.rs
Original file line number Diff line number Diff line change
Expand Up @@ -96,6 +96,12 @@ pub enum CertificateError {
InvalidProof = 100,
/// The zero-knowledge verification process failed.
VerificationFailed = 101,

// Tamper Detection
/// Certificate integrity check failed — the stored checksum does not match.
TamperDetected = 110,
/// Certificate has not been sealed yet; call seal_certificate first.
NotSealed = 111,
}

impl CertificateError {
Expand Down Expand Up @@ -139,6 +145,8 @@ impl CertificateError {
Self::RateLimitExceeded => "CERT-090",
Self::InvalidProof => "CERT-100",
Self::VerificationFailed => "CERT-101",
Self::TamperDetected => "CERT-110",
Self::NotSealed => "CERT-111",
}
}

Expand Down
86 changes: 86 additions & 0 deletions contracts/certificate/src/test.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1044,3 +1044,89 @@ fn test_batch_export_non_admin_fails() {
let result = client.try_batch_export_certificates(&non_admin, &ids);
assert!(result.is_err());
}

// ─────────────────────────────────────────────────────────────
// 18. Tamper Detection
// ─────────────────────────────────────────────────────────────

fn issue_cert(
env: &Env,
client: &CertificateContractClient,
admin: &Address,
id_byte: u8,
) -> BytesN<32> {
let student = Address::generate(env);
let cert_id = BytesN::from_array(env, &[id_byte; 32]);
let mut params_list: Vec<MintCertificateParams> = Vec::new(env);
params_list.push_back(MintCertificateParams {
certificate_id: cert_id.clone(),
course_id: String::from_str(env, "TAMPER_COURSE"),
student,
title: String::from_str(env, "Tamper Test Cert"),
description: String::from_str(env, "desc"),
metadata_uri: String::from_str(env, "https://example.com/t"),
expiry_date: 0,
});
client.batch_issue_certificates(admin, &params_list);
cert_id
}

#[test]
fn test_seal_and_verify_integrity_ok() {
let (env, client, admin) = setup_env();
let cert_id = issue_cert(&env, &client, &admin, 0xF1);

client.seal_certificate(&admin, &cert_id);

let verifier = Address::generate(&env);
let result = client.verify_integrity(&verifier, &cert_id);
assert_eq!(result, Ok(true));
}

#[test]
fn test_verify_integrity_not_sealed_fails() {
let (env, client, admin) = setup_env();
let cert_id = issue_cert(&env, &client, &admin, 0xF2);

let verifier = Address::generate(&env);
let result = client.try_verify_integrity(&verifier, &cert_id);
assert!(result.is_err());
}

#[test]
fn test_seal_non_existent_cert_fails() {
let (env, client, admin) = setup_env();
let fake_id = BytesN::from_array(&env, &[0xFFu8; 32]);
let result = client.try_seal_certificate(&admin, &fake_id);
assert!(result.is_err());
}

#[test]
fn test_get_tamper_record_after_seal() {
let (env, client, admin) = setup_env();
let cert_id = issue_cert(&env, &client, &admin, 0xF3);

client.seal_certificate(&admin, &cert_id);

let record = client.get_tamper_record(&cert_id);
assert!(record.is_some());
let r = record.unwrap();
assert!(!r.tampered);
assert_eq!(r.custody_log.len(), 1); // one "sealed" entry
}

#[test]
fn test_custody_log_grows_on_verify() {
let (env, client, admin) = setup_env();
let cert_id = issue_cert(&env, &client, &admin, 0xF4);

client.seal_certificate(&admin, &cert_id);

let verifier = Address::generate(&env);
client.verify_integrity(&verifier, &cert_id);
client.verify_integrity(&verifier, &cert_id);

let record = client.get_tamper_record(&cert_id).unwrap();
// 1 seal + 2 verify entries
assert_eq!(record.custody_log.len(), 3);
}
Loading