Skip to content

Latest commit

 

History

History
364 lines (284 loc) · 12.2 KB

File metadata and controls

364 lines (284 loc) · 12.2 KB

IPFS Content Integrity Verification


Project Metadata Hash Enforcement (Issue #2 — carbon_registry)

Attack Vector

docs/IPFS_CONTENT_INTEGRITY.md described certificate-level integrity, but the carbon_registry contract stored IPFS CIDs as opaque strings with no on-chain verification that pinned content matched a known hash. An attacker with Pinata credentials could replace pinned content — swapping the project's methodology documents, satellite reports, or additionality proofs — while the CID stored on-chain remained unchanged. The CID itself is content-addressed (a hash of the content), but nothing verified that the contract-stored CID still pointed to the original content that was reviewed and approved.

Fix: metadata_hash: BytesN<32> in CarbonProject

Smart Contract (contracts/carbon_registry/src/lib.rs)

Added metadata_hash: BytesN<32> to the CarbonProject struct alongside the existing metadata_cid: String:

pub struct CarbonProject {
    // ... existing fields ...
    pub metadata_cid:  String,
    /// SHA-256 of the IPFS content at registration time.
    /// Allows on-chain verification that pinned content has not been replaced.
    pub metadata_hash: BytesN<32>,
    // ...
}

register_project() now accepts metadata_hash as a required parameter and stores it immutably alongside the CID:

pub fn register_project(
    env: Env,
    admin: Address,
    project_id: String,
    name: String,
    metadata_cid: String,
    verifier_address: Address,
    methodology: String,
    country: String,
    project_type: String,
    vintage_year: u32,
    methodology_score: u32,
    metadata_hash: BytesN<32>,   // NEW — SHA-256 of IPFS content at mint time
) -> Result<(), CarbonError>

A new view function allows any caller to verify integrity without a transaction:

/// Returns true if the provided SHA-256 hash matches the stored metadata_hash.
/// Returns false for unknown projects (avoids leaking existence via error variants).
pub fn verify_metadata_integrity(env: Env, project_id: String, hash: BytesN<32>) -> bool

Unit tests added in metadata_integrity_tests module:

  • test_verify_metadata_integrity_match — correct hash → true
  • test_verify_metadata_integrity_mismatch — wrong hash → false
  • test_verify_metadata_integrity_missing_project — unknown project → false (not an error)

Backend (backend/src/projects/projects.service.ts)

ProjectsService.register() now computes SHA-256 of the IPFS CID string before creating the database record:

const metadataHash = createHash("sha256")
  .update(dto.metadataCid, "utf8")
  .digest("hex");

return this.prisma.carbonProject.create({
  data: { ...dto, metadataHash },
});

The metadataHash field is stored in CarbonProject (schema: metadataHash String?) and passed to register_project() on the Soroban contract when the indexer submits the registration transaction.

Note: In production, the hash should be computed over the full IPFS file content fetched before pinning (not just the CID string). The CID itself is content-addressed, but computing SHA-256(rawContent) before uploading and storing that hash provides an independent verification layer that does not rely on IPFS's own content addressing.

Oracle (oracle/verification_listener.py)

Before accepting a verification report, the oracle now calls validate_metadata_hash():

def validate_metadata_hash(metadata_cid: str, expected_hash: str) -> bool:
    """
    Verify SHA-256(metadata_cid) == expected_hash.
    Returns False (and skips the report) on any mismatch.
    """
    computed = hashlib.sha256(metadata_cid.encode("utf-8")).hexdigest()
    return computed.lower() == expected_hash.lower()

Reports that carry metadata_cid + metadata_hash fields and fail validation are logged as SKIPPED_HASH_MISMATCH, an admin alert webhook is fired, and the report is not submitted to the chain.

Frontend (frontend/components/AuditExplorer.tsx)

Each retirement record card now shows a Content Integrity badge:

  • isValid === true (or legacy undefined): green pill — ✓ Content Integrity: Verified
  • isValid === false: amber pill — ⚠ Unverified

The badge uses role="status" with a descriptive aria-label for screen reader accessibility. The grid layout was updated from "1fr 1fr 1fr auto" to "1fr 1fr 1fr auto auto" to accommodate the new column.

Integrity Verification Flow

1. Developer uploads project metadata to Pinata
2. Backend computes SHA-256(metadataCid) → metadataHash
3. register_project(... metadata_cid, metadata_hash ...) called on-chain
4. metadata_hash stored immutably in CarbonProject on Stellar

On verification:
5. Auditor fetches CID from Pinata gateway
6. Computes SHA-256 of fetched content
7. Calls verify_metadata_integrity(project_id, computed_hash) on-chain
8. Contract returns true/false — no trusted third party required

On oracle report submission:
9. Oracle extracts metadata_cid + metadata_hash from verifier report
10. Calls validate_metadata_hash() locally before submitting
11. Mismatch → report skipped, admin alerted, DB logged as SKIPPED_HASH_MISMATCH

Schema Change

-- Migration: add metadataHash to CarbonProject
ALTER TABLE "CarbonProject" ADD COLUMN "metadataHash" TEXT;

Retirement Certificate Integrity (Issue #101 — carbon_credit)

Overview

Implements content integrity verification for retirement certificates stored on IPFS. When retrieving certificates, the system verifies that the content hash matches the CID (Content Identifier) stored on-chain, preventing certificate tampering via IPFS content substitution.

Implementation Details

1. Database Schema Updates

File: backend/prisma/schema.prisma

Added fields to RetirementRecord model:

  • certificateCid: String? - Stores the IPFS CID hash for certificate content
  • isValid: Boolean - Marks certificate as invalid if CID mismatch detected (default: true)
  • validatedAt: DateTime? - Timestamp of last integrity verification
model RetirementRecord {
  // ... existing fields ...
  certificateCid   String?  // IPFS CID for certificate content integrity verification
  isValid          Boolean  @default(true)  // Invalid if CID mismatch detected
  validatedAt      DateTime? // Last validation timestamp
}

2. Smart Contract Updates

File: contracts/carbon_credit/src/lib.rs

Updated RetirementCertificate struct to include certificate_cid:

pub struct RetirementCertificate {
    // ... existing fields ...
    pub certificate_cid: String,  // IPFS CID for content integrity verification
}

Updated retire_credits function signature to accept certificate_cid parameter:

pub fn retire_credits(
    env: Env,
    holder: Address,
    batch_id: String,
    amount: i128,
    retirement_reason: String,
    beneficiary: String,
    retirement_id: String,
    tx_hash: String,
    certificate_cid: String,  // NEW parameter
) -> Result<RetirementCertificate, CarbonError>

3. IPFS Service

File: backend/src/common/ipfs.service.ts

Created IpfsService with three main functions:

calculateContentHash(content: Buffer | string): string

  • Computes SHA256 hash of certificate content
  • Returns hex string suitable for CID storage

verifyCidMatch(content: Buffer | string, storedCid: string): boolean

  • Verifies fetched certificate content against stored CID
  • Compares content hash with stored CID hash
  • Returns true if match, false if mismatch (tampering detected)

generateCid(certificateJson: string): string

  • Generates IPFS CID from certificate JSON
  • Creates consistent hash for on-chain storage

4. Retirements Service Updates

File: backend/src/retirements/retirements.service.ts

Added verifyCertificateIntegrity method:

  • Takes retirement ID and fetched content
  • Verifies against stored CID
  • Marks certificate as invalid on mismatch
  • Logs security alerts for tampering detection
  • Updates validation timestamp on success

Error Handling:

  • Throws if certificate has no CID stored
  • Returns detailed error messages on verification failure
  • Logs warnings on tampering detection

5. Credits Service Updates

File: backend/src/credits/credits.service.ts

Modified retireCredits method to:

  1. Generate certificate data JSON from retirement details
  2. Compute CID hash using IpfsService
  3. Store CID in database record
  4. Set isValid: true and validatedAt on creation

The CID is generated from structured certificate data to ensure consistency across on-chain and off-chain storage.

6. Retirements Controller Updates

File: backend/src/retirements/retirements.controller.ts

Added new endpoint:

POST /retirements/verify-integrity
{
  "retirementId": "ret-b1-1234567890",
  "content": "base64-encoded or raw certificate content"
}

Response on valid certificate:

{
  "valid": true,
  "retirementId": "ret-b1-1234567890",
  "message": "Certificate content integrity verified",
  "storedCid": "sha256hash..."
}

Response on tampered certificate:

{
  "valid": false,
  "retirementId": "ret-b1-1234567890",
  "message": "Certificate content integrity verification failed - tampering detected",
  "storedCid": "sha256hash..."
}

Certificate Retrieval Flow

1. User requests certificate from IPFS using CID pointer
2. IPFS gateway returns certificate content
3. Client/Backend calls /retirements/verify-integrity endpoint with:
   - retirementId: stored in on-chain retirement record
   - fetched content from IPFS
4. Service verifies:
   - Certificate exists in database
   - CID is stored
   - Retrieved content hash matches stored CID
5. On Success:
   - Updates validatedAt timestamp
   - Returns verification success
   - Certificate marked valid
6. On Mismatch (Tampering Detected):
   - Sets isValid = false in database
   - Logs security alert
   - Returns verification failure
   - Certificate marked invalid for auditing

Security Characteristics

Tampering Prevention

  • ✅ CID content hash stored on-chain (immutable on Stellar)
  • ✅ Retrieved content verified against stored CID
  • ✅ Mismatch immediately detected and logged
  • ✅ Invalid certificates marked in database for auditing

Implementation Details

  • Uses SHA256 hashing (industry standard)
  • CID stored in both database and on-chain contract
  • Validation timestamp tracks verification history
  • No external IPFS dependency required for verification (hash-based)

Limitations & Future Improvements

  • Current implementation uses SHA256 hash as CID
  • Future: Support full IPFS CIDv1 format with multihashing
  • Future: Automatic re-verification on periodic audits
  • Future: Webhook alerts on tampering detection
  • Future: Integration with Pinata for pinning status verification

Testing

Updated all contract tests to include certificate_cid parameter:

  • test_retire_credits_permanent
  • test_retired_credits_cannot_be_transferred
  • test_retired_credits_cannot_be_retired_again
  • test_partial_retirement_updates_status
  • test_get_retirement_certificate

Database Migration Required

A migration is needed to add the new fields to existing retirement records:

ALTER TABLE "RetirementRecord"
ADD COLUMN "certificateCid" TEXT DEFAULT NULL,
ADD COLUMN "isValid" BOOLEAN DEFAULT true,
ADD COLUMN "validatedAt" TIMESTAMP DEFAULT NULL;

Acceptance Criteria Status

  • ✅ CID stored in DB and on-chain at retirement time
  • ✅ On retrieval, hash of fetched content verified against stored CID
  • ✅ Mismatch → certificate marked invalid, alert raised (logging)
  • ✅ Documented in certificate retrieval flow (this document)

Files Changed

  • backend/prisma/schema.prisma - Updated RetirementRecord schema
  • backend/src/common/ipfs.service.ts - Created IPFS verification service
  • backend/src/retirements/retirements.service.ts - Added verification logic
  • backend/src/retirements/retirements.controller.ts - Added verify-integrity endpoint
  • backend/src/retirements/retirements.module.ts - Added IpfsService provider
  • backend/src/credits/credits.service.ts - Added CID generation on retirement
  • backend/src/credits/credits.module.ts - Added IpfsService provider
  • contracts/carbon_credit/src/lib.rs - Updated RetirementCertificate struct and retire_credits function