Simple Public Key Directory: Store and verify public keys on a blockchain

Introduction

Project Name: KeyChain: Decentralized Public Key Directory

Concept: Traditional Public Key Infrastructure (PKI) relies on centralized Certificate Authorities (CAs) that can be compromised or issue fraudulent certificates. This project creates a decentralized, tamper-proof public key directory using blockchain technology, where users can register their public keys and anyone can verify ownership without relying on a central authority.

The Blockchain Solution: Instead of trusting a central certificate authority, we store public key mappings on an immutable blockchain. Each entry contains a username/identifier and their associated public key, cryptographically linked to previous entries. This creates an auditable, transparent, and tamper-proof directory where key ownership can be verified by anyone at any time.

Features

  • 🔐 Decentralized Trust: No single point of failure or central authority
  • 📝 Key Registration: Users can register their public keys with a unique identifier
  • ✅ Key Verification: Verify if a public key belongs to a claimed identity
  • ⛓️ Immutable History: All key registrations are permanently recorded
  • 🔍 Key Lookup: Search and retrieve public keys by username
  • 🕒 Timestamp Proof: Each registration includes cryptographic timestamp
  • 🔄 Key Rotation: Support for updating keys with version history
  • 📊 Directory Explorer: Browse all registered public keys

Project File Structure

public-key-directory/ │ ├── index.php # Main application UI ├── style.css # Custom styling ├── script.js # Frontend JavaScript ├── api/ │ ├── register_key.php # Register a new public key │ ├── lookup_key.php # Lookup public key by username │ ├── verify_key.php # Verify key ownership │ ├── get_all_keys.php # Get all registered keys │ └── get_chain_info.php # Get blockchain statistics ├── includes/ │ ├── config.php # Database configuration │ └── KeyChain.php # Core blockchain logic for keys ├── database/ │ └── schema.sql # Database schema └── README.md # Project documentation

Database Setup (MySQL)

1. Create a database named keychain_db.

2. Run the database/schema.sql script:

CREATE DATABASE IF NOT EXISTS keychain_db; USE keychain_db; CREATE TABLE IF NOT EXISTS key_chain ( id INT AUTO_INCREMENT PRIMARY KEY, block_index INT NOT NULL, username VARCHAR(100) NOT NULL, public_key TEXT NOT NULL, key_type VARCHAR(20) DEFAULT 'RSA', email VARCHAR(255), timestamp BIGINT NOT NULL, previous_hash VARCHAR(64) NOT NULL, hash VARCHAR(64) NOT NULL UNIQUE, nonce INT NOT NULL, created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, INDEX idx_username (username), INDEX idx_block_index (block_index), UNIQUE KEY unique_username (username, block_index) ); CREATE TABLE IF NOT EXISTS key_history ( id INT AUTO_INCREMENT PRIMARY KEY, username VARCHAR(100) NOT NULL, public_key TEXT NOT NULL, block_index INT NOT NULL, action VARCHAR(20) DEFAULT 'REGISTER', timestamp BIGINT NOT NULL, FOREIGN KEY (block_index) REFERENCES key_chain(block_index) ); -- Insert Genesis Block INSERT INTO key_chain (block_index, username, public_key, key_type, email, timestamp, previous_hash, hash, nonce) VALUES (0, 'GENESIS', 'Genesis Key Directory Block', 'SYSTEM', '[email protected]', UNIX_TIMESTAMP(), '0', SHA2(CONCAT('0', 'GENESIS', 'Genesis Key Directory Block', 'SYSTEM', '[email protected]', UNIX_TIMESTAMP(), '0', '0'), 256), 0);

Code Implementation

1. includes/config.php (Database Configuration)

<?php define('DB_HOST', 'localhost'); define('DB_USER', 'root'); define('DB_PASS', ''); define('DB_NAME', 'keychain_db'); function getDBConnection() { $conn = new mysqli(DB_HOST, DB_USER, DB_PASS, DB_NAME); if ($conn->connect_error) { die(json_encode(['success' => false, 'message' => 'Database connection failed: ' . $conn->connect_error])); } return $conn; } // Set timezone date_default_timezone_set('UTC'); ?>

2. includes/KeyChain.php (Core Blockchain Logic)

<?php require_once 'config.php'; class KeyChain { private $conn; private $difficulty = 2; // Proof of work difficulty public function __construct() { $this->conn = getDBConnection(); } /** * Calculate hash for a key block */ public static function calculateHash($index, $username, $publicKey, $keyType, $email, $timestamp, $previousHash, $nonce) { $data = $index . $username . $publicKey . $keyType . $email . $timestamp . $previousHash . $nonce; return hash('sha256', $data); } /** * Get the last block from the chain */ public function getLastBlock() { $sql = "SELECT * FROM key_chain ORDER BY block_index DESC LIMIT 1"; $result = $this->conn->query($sql); if ($result && $result->num_rows > 0) { return $result->fetch_assoc(); } return null; } /** * Get all registered keys */ public function getAllKeys() { $sql = "SELECT * FROM key_chain WHERE username != 'GENESIS' ORDER BY block_index DESC"; $result = $this->conn->query($sql); $keys = []; if ($result) { while ($row = $result->fetch_assoc()) { // Truncate public key for display $row['public_key_short'] = substr($row['public_key'], 0, 50) . '...'; $row['formatted_time'] = date('Y-m-d H:i:s', $row['timestamp']); $keys[] = $row; } } return $keys; } /** * Get key by username */ public function getKeyByUsername($username) { $sql = "SELECT * FROM key_chain WHERE username = ? ORDER BY block_index DESC LIMIT 1"; $stmt = $this->conn->prepare($sql); $stmt->bind_param("s", $username); $stmt->execute(); $result = $stmt->get_result(); if ($result && $result->num_rows > 0) { return $result->fetch_assoc(); } return null; } /** * Get key history for a username */ public function getKeyHistory($username) { $sql = "SELECT * FROM key_history WHERE username = ? ORDER BY block_index DESC"; $stmt = $this->conn->prepare($sql); $stmt->bind_param("s", $username); $stmt->execute(); $result = $stmt->get_result(); $history = []; if ($result) { while ($row = $result->fetch_assoc()) { $row['formatted_time'] = date('Y-m-d H:i:s', $row['timestamp']); $history[] = $row; } } return $history; } /** * Proof of Work */ public function proofOfWork($index, $username, $publicKey, $keyType, $email, $timestamp, $previousHash) { $nonce = 0; $prefix = str_repeat('0', $this->difficulty); while (true) { $hash = $this->calculateHash($index, $username, $publicKey, $keyType, $email, $timestamp, $previousHash, $nonce); if (substr($hash, 0, $this->difficulty) === $prefix) { return ['nonce' => $nonce, 'hash' => $hash]; } $nonce++; if ($nonce > 1000000) { throw new Exception("Proof of work took too long"); } } } /** * Register a new public key */ public function registerKey($username, $publicKey, $keyType = 'RSA', $email = '') { try { // Validate inputs if (empty($username) || empty($publicKey)) { return ['success' => false, 'message' => 'Username and public key are required']; } // Check if username already exists (optional - can allow updates) $existing = $this->getKeyByUsername($username); $action = $existing ? 'UPDATE' : 'REGISTER'; // Get last block $lastBlock = $this->getLastBlock(); if (!$lastBlock) { return ['success' => false, 'message' => 'Key chain corrupted. Genesis block missing.']; } $newIndex = $lastBlock['block_index'] + 1; $timestamp = time(); $previousHash = $lastBlock['hash']; // Perform Proof of Work $powResult = $this->proofOfWork($newIndex, $username, $publicKey, $keyType, $email, $timestamp, $previousHash); // Start transaction $this->conn->begin_transaction(); // Insert new block $sql = "INSERT INTO key_chain (block_index, username, public_key, key_type, email, timestamp, previous_hash, hash, nonce) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)"; $stmt = $this->conn->prepare($sql); $stmt->bind_param("issssisss", $newIndex, $username, $publicKey, $keyType, $email, $timestamp, $previousHash, $powResult['hash'], $powResult['nonce'] ); if (!$stmt->execute()) { throw new Exception("Failed to insert key: " . $stmt->error); } // Add to history $sql2 = "INSERT INTO key_history (username, public_key, block_index, action, timestamp) VALUES (?, ?, ?, ?, ?)"; $stmt2 = $this->conn->prepare($sql2); $stmt2->bind_param("ssiss", $username, $publicKey, $newIndex, $action, $timestamp); if (!$stmt2->execute()) { throw new Exception("Failed to record history: " . $stmt2->error); } // Commit transaction $this->conn->commit(); return [ 'success' => true, 'message' => $action == 'REGISTER' ? 'Key registered successfully' : 'Key updated successfully', 'key' => [ 'index' => $newIndex, 'username' => $username, 'key_type' => $keyType, 'email' => $email, 'timestamp' => $timestamp, 'hash' => $powResult['hash'], 'previous_hash' => $previousHash, 'nonce' => $powResult['nonce'] ] ]; } catch (Exception $e) { $this->conn->rollback(); return ['success' => false, 'message' => 'Error registering key: ' . $e->getMessage()]; } } /** * Verify key ownership */ public function verifyKey($username, $publicKey) { $key = $this->getKeyByUsername($username); if (!$key) { return [ 'success' => false, 'message' => 'Username not found in directory', 'verified' => false ]; } // Compare public keys $verified = ($key['public_key'] === $publicKey); return [ 'success' => true, 'verified' => $verified, 'message' => $verified ? '✅ Key verified successfully' : '❌ Key does not match registered key', 'key_info' => [ 'username' => $key['username'], 'registered' => date('Y-m-d H:i:s', $key['timestamp']), 'block_index' => $key['block_index'], 'key_type' => $key['key_type'] ] ]; } /** * Get blockchain statistics */ public function getStats() { $stats = [ 'total_blocks' => 0, 'total_keys' => 0, 'latest_block' => null, 'chain_valid' => true ]; // Count blocks $result = $this->conn->query("SELECT COUNT(*) as count FROM key_chain"); if ($result) { $stats['total_blocks'] = $result->fetch_assoc()['count']; } // Count keys (excluding genesis) $result = $this->conn->query("SELECT COUNT(*) as count FROM key_chain WHERE username != 'GENESIS'"); if ($result) { $stats['total_keys'] = $result->fetch_assoc()['count']; } // Get latest block $result = $this->conn->query("SELECT * FROM key_chain ORDER BY block_index DESC LIMIT 1"); if ($result && $result->num_rows > 0) { $latest = $result->fetch_assoc(); $stats['latest_block'] = [ 'index' => $latest['block_index'], 'username' => $latest['username'], 'time' => date('Y-m-d H:i:s', $latest['timestamp']) ]; } // Verify chain integrity $stats['chain_valid'] = $this->verifyChainIntegrity(); return $stats; } /** * Verify the integrity of the entire chain */ private function verifyChainIntegrity() { $sql = "SELECT * FROM key_chain ORDER BY block_index ASC"; $result = $this->conn->query($sql); if (!$result || $result->num_rows == 0) { return false; } $blocks = []; while ($row = $result->fetch_assoc()) { $blocks[] = $row; } // Verify each block for ($i = 1; $i < count($blocks); $i++) { $current = $blocks[$i]; $previous = $blocks[$i - 1]; // Check if previous hash matches if ($current['previous_hash'] !== $previous['hash']) { return false; } // Verify current block's hash $expectedHash = $this->calculateHash( $current['block_index'], $current['username'], $current['public_key'], $current['key_type'], $current['email'], $current['timestamp'], $current['previous_hash'], $current['nonce'] ); if ($expectedHash !== $current['hash']) { return false; } } return true; } public function __destruct() { $this->conn->close(); } } ?>

3. api/register_key.php (Register Key Endpoint)

<?php header('Content-Type: application/json'); header('Access-Control-Allow-Origin: *'); header('Access-Control-Allow-Methods: POST'); header('Access-Control-Allow-Headers: Content-Type'); require_once '../includes/KeyChain.php'; if ($_SERVER['REQUEST_METHOD'] !== 'POST') { http_response_code(405); echo json_encode(['success' => false, 'message' => 'Method not allowed']); exit; } $input = json_decode(file_get_contents('php://input'), true); if (!$input) { $username = $_POST['username'] ?? ''; $publicKey = $_POST['public_key'] ?? ''; $keyType = $_POST['key_type'] ?? 'RSA'; $email = $_POST['email'] ?? ''; } else { $username = $input['username'] ?? ''; $publicKey = $input['public_key'] ?? ''; $keyType = $input['key_type'] ?? 'RSA'; $email = $input['email'] ?? ''; } // Basic validation if (empty($username) || empty($publicKey)) { echo json_encode(['success' => false, 'message' => 'Username and public key are required']); exit; } // Validate key type $validTypes = ['RSA', 'ECDSA', 'Ed25519', 'DSA']; if (!in_array($keyType, $validTypes)) { $keyType = 'RSA'; } $keychain = new KeyChain(); $result = $keychain->registerKey($username, $publicKey, $keyType, $email); echo json_encode($result); ?>

4. api/lookup_key.php (Lookup Key Endpoint)

<?php header('Content-Type: application/json'); header('Access-Control-Allow-Origin: *'); require_once '../includes/KeyChain.php'; $username = $_GET['username'] ?? ''; if (empty($username)) { echo json_encode(['success' => false, 'message' => 'Username is required']); exit; } $keychain = new KeyChain(); $key = $keychain->getKeyByUsername($username); if ($key) { // Don't return full key in some cases for privacy $showFullKey = $_GET['full'] ?? false; $response = [ 'success' => true, 'found' => true, 'username' => $key['username'], 'key_type' => $key['key_type'], 'email' => $key['email'], 'registered' => date('Y-m-d H:i:s', $key['timestamp']), 'block_index' => $key['block_index'], 'hash' => $key['hash'] ]; if ($showFullKey) { $response['public_key'] = $key['public_key']; } else { $response['public_key_preview'] = substr($key['public_key'], 0, 100) . '...'; } // Get history $response['history'] = $keychain->getKeyHistory($username); echo json_encode($response); } else { echo json_encode([ 'success' => true, 'found' => false, 'message' => 'Username not found in directory' ]); } ?>

5. api/verify_key.php (Verify Key Endpoint)

<?php header('Content-Type: application/json'); header('Access-Control-Allow-Origin: *'); header('Access-Control-Allow-Methods: POST'); header('Access-Control-Allow-Headers: Content-Type'); require_once '../includes/KeyChain.php'; if ($_SERVER['REQUEST_METHOD'] !== 'POST') { http_response_code(405); echo json_encode(['success' => false, 'message' => 'Method not allowed']); exit; } $input = json_decode(file_get_contents('php://input'), true); if (!$input) { $username = $_POST['username'] ?? ''; $publicKey = $_POST['public_key'] ?? ''; } else { $username = $input['username'] ?? ''; $publicKey = $input['public_key'] ?? ''; } if (empty($username) || empty($publicKey)) { echo json_encode(['success' => false, 'message' => 'Username and public key are required']); exit; } $keychain = new KeyChain(); $result = $keychain->verifyKey($username, $publicKey); echo json_encode($result); ?>

6. api/get_all_keys.php (Get All Keys)

<?php header('Content-Type: application/json'); header('Access-Control-Allow-Origin: *'); require_once '../includes/KeyChain.php'; $keychain = new KeyChain(); $keys = $keychain->getAllKeys(); echo json_encode([ 'success' => true, 'total' => count($keys), 'keys' => $keys ]); ?>

7. api/get_chain_info.php (Chain Statistics)

<?php header('Content-Type: application/json'); header('Access-Control-Allow-Origin: *'); require_once '../includes/KeyChain.php'; $keychain = new KeyChain(); $stats = $keychain->getStats(); echo json_encode([ 'success' => true, 'stats' => $stats ]); ?>

8. index.php (Main Application UI)

<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>KeyChain | Decentralized Public Key Directory</title> <script src="https://cdn.tailwindcss.com"></script> <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0/css/all.min.css"> <style> @import url('https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&display=swap'); body { font-family: 'Inter', sans-serif; background: linear-gradient(135deg, #0b1120 0%, #1a2234 100%); } .key-card { transition: all 0.3s ease; border: 1px solid rgba(255, 255, 255, 0.1); } .key-card:hover { transform: translateY(-2px); border-color: #3b82f6; box-shadow: 0 10px 30px -10px rgba(59, 130, 246, 0.5); } .hash-value { font-family: 'Monaco', 'Menlo', monospace; font-size: 0.75rem; } .glow { animation: glow 2s ease-in-out infinite; } @keyframes glow { 0%, 100% { box-shadow: 0 0 10px rgba(59, 130, 246, 0.3); } 50% { box-shadow: 0 0 20px rgba(59, 130, 246, 0.6); } } .chain-valid { background: linear-gradient(90deg, #10b981, #059669); } .chain-invalid { background: linear-gradient(90deg, #ef4444, #dc2626); } </style> </head> <body class="text-gray-100 min-h-screen"> <div class="max-w-7xl mx-auto px-4 py-8"> <!-- Header --> <div class="text-center mb-12"> <div class="flex items-center justify-center mb-4"> <i class="fas fa-link text-5xl text-blue-400 mr-3"></i> <h1 class="text-5xl font-bold bg-gradient-to-r from-blue-400 to-indigo-400 bg-clip-text text-transparent"> KeyChain </h1> </div> <p class="text-xl text-gray-300 mb-2">Decentralized Public Key Directory</p> <p class="text-gray-400 max-w-2xl mx-auto"> Store and verify public keys on an immutable blockchain. No central authority, no single point of failure. </p> </div> <!-- Stats Dashboard --> <div class="grid grid-cols-1 md:grid-cols-4 gap-4 mb-8"> <div class="bg-gray-800 bg-opacity-50 rounded-lg p-4 border border-gray-700"> <div class="flex items-center"> <i class="fas fa-cubes text-2xl text-blue-400 mr-3"></i> <div> <p class="text-sm text-gray-400">Total Blocks</p> <p id="totalBlocks" class="text-2xl font-bold">-</p> </div> </div> </div> <div class="bg-gray-800 bg-opacity-50 rounded-lg p-4 border border-gray-700"> <div class="flex items-center"> <i class="fas fa-key text-2xl text-blue-400 mr-3"></i> <div> <p class="text-sm text-gray-400">Registered Keys</p> <p id="totalKeys" class="text-2xl font-bold">-</p> </div> </div> </div> <div class="bg-gray-800 bg-opacity-50 rounded-lg p-4 border border-gray-700"> <div class="flex items-center"> <i class="fas fa-shield-alt text-2xl text-blue-400 mr-3"></i> <div> <p class="text-sm text-gray-400">Chain Status</p> <p id="chainStatus" class="text-lg font-semibold text-green-400">✓ Valid</p> </div> </div> </div> <div class="bg-gray-800 bg-opacity-50 rounded-lg p-4 border border-gray-700"> <div class="flex items-center"> <i class="fas fa-clock text-2xl text-blue-400 mr-3"></i> <div> <p class="text-sm text-gray-400">Last Block</p> <p id="lastBlock" class="text-sm font-semibold">-</p> </div> </div> </div> </div> <!-- Main Content --> <div class="grid grid-cols-1 lg:grid-cols-3 gap-8"> <!-- Left Column: Registration Form --> <div class="lg:col-span-1"> <div class="bg-gray-800 bg-opacity-50 rounded-xl p-6 border border-gray-700 sticky top-6"> <h2 class="text-xl font-bold mb-6 flex items-center"> <i class="fas fa-pen-fancy text-blue-400 mr-2"></i> Register Public Key </h2> <form id="registerForm" class="space-y-4"> <div> <label class="block text-sm font-medium text-gray-300 mb-2">Username</label> <input type="text" id="username" required class="w-full bg-gray-700 border border-gray-600 rounded-lg px-4 py-2 text-white focus:outline-none focus:ring-2 focus:ring-blue-500" placeholder="e.g., alice, bob, company"> </div> <div> <label class="block text-sm font-medium text-gray-300 mb-2">Email (Optional)</label> <input type="email" id="email" class="w-full bg-gray-700 border border-gray-600 rounded-lg px-4 py-2 text-white focus:outline-none focus:ring-2 focus:ring-blue-500" placeholder="[email protected]"> </div> <div> <label class="block text-sm font-medium text-gray-300 mb-2">Key Type</label> <select id="keyType" class="w-full bg-gray-700 border border-gray-600 rounded-lg px-4 py-2 text-white focus:outline-none focus:ring-2 focus:ring-blue-500"> <option value="RSA">RSA (2048+ bits)</option> <option value="ECDSA">ECDSA</option> <option value="Ed25519">Ed25519</option> <option value="DSA">DSA</option> </select> </div> <div> <label class="block text-sm font-medium text-gray-300 mb-2">Public Key</label> <textarea id="publicKey" rows="6" required class="w-full bg-gray-700 border border-gray-600 rounded-lg px-4 py-2 text-white font-mono text-xs focus:outline-none focus:ring-2 focus:ring-blue-500" placeholder="-----BEGIN PUBLIC KEY-----\nMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8A...\n-----END PUBLIC KEY-----"></textarea> </div> <div class="flex space-x-3"> <button type="submit" class="flex-1 bg-blue-600 hover:bg-blue-700 text-white font-bold py-3 px-4 rounded-lg transition duration-300 flex items-center justify-center"> <i class="fas fa-link mr-2"></i> Register Key </button> <button type="button" onclick="generateSampleKey()" class="bg-gray-700 hover:bg-gray-600 text-white px-4 rounded-lg transition" title="Generate Sample Key"> <i class="fas fa-magic"></i> </button> </div> </form> <!-- Quick Actions --> <div class="mt-6 pt-6 border-t border-gray-700"> <h3 class="text-sm font-medium text-gray-400 mb-3">Quick Actions</h3> <div class="space-y-2"> <button onclick="refreshDirectory()" class="w-full bg-gray-700 hover:bg-gray-600 text-gray-300 text-sm py-2 px-3 rounded-lg transition flex items-center"> <i class="fas fa-sync-alt mr-2"></i> Refresh Directory </button> <button onclick="verifyKeyForm()" class="w-full bg-gray-700 hover:bg-gray-600 text-gray-300 text-sm py-2 px-3 rounded-lg transition flex items-center"> <i class="fas fa-check-circle mr-2"></i> Verify a Key </button> </div> </div> </div> </div> <!-- Right Column: Key Directory --> <div class="lg:col-span-2"> <div class="bg-gray-800 bg-opacity-50 rounded-xl border border-gray-700 overflow-hidden"> <div class="p-4 border-b border-gray-700 flex justify-between items-center"> <h2 class="text-xl font-bold flex items-center"> <i class="fas fa-book text-blue-400 mr-2"></i> Public Key Directory </h2> <div class="flex space-x-2"> <span class="text-xs bg-gray-700 px-3 py-1 rounded-full text-gray-300" id="keyCount">0 keys</span> </div> </div> <div id="keyContainer" class="p-4 space-y-4 max-h-[600px] overflow-y-auto"> <!-- Keys will be loaded here --> <div class="text-center text-gray-500 py-8"> <i class="fas fa-spinner fa-spin text-3xl mb-2"></i> <p>Loading directory...</p> </div> </div> </div> </div> </div> <!-- Verification Modal --> <div id="verifyModal" class="fixed inset-0 bg-black bg-opacity-75 hidden items-center justify-center z-50"> <div class="bg-gray-800 rounded-xl p-6 max-w-md w-full mx-4 border border-gray-700"> <h3 class="text-xl font-bold mb-4 flex items-center"> <i class="fas fa-check-circle text-blue-400 mr-2"></i> Verify Key Ownership </h3> <form id="verifyForm" class="space-y-4"> <div> <label class="block text-sm font-medium text-gray-300 mb-2">Username</label> <input type="text" id="verifyUsername" required class="w-full bg-gray-700 border border-gray-600 rounded-lg px-4 py-2 text-white focus:outline-none focus:ring-2 focus:ring-blue-500"> </div> <div> <label class="block text-sm font-medium text-gray-300 mb-2">Public Key to Verify</label> <textarea id="verifyPublicKey" rows="4" required class="w-full bg-gray-700 border border-gray-600 rounded-lg px-4 py-2 text-white font-mono text-xs focus:outline-none focus:ring-2 focus:ring-blue-500"></textarea> </div> <button type="submit" class="w-full bg-blue-600 hover:bg-blue-700 text-white font-bold py-3 px-4 rounded-lg transition"> Verify </button> </form> <div id="verifyResult" class="mt-4 p-3 rounded-lg hidden"></div> <button onclick="closeVerifyModal()" class="mt-3 w-full bg-gray-700 hover:bg-gray-600 text-white py-2 rounded-lg transition"> Close </button> </div> </div> </div> <script src="script.js"></script> </body> </html>

9. script.js (Frontend JavaScript)

// script.js // Load data on page load document.addEventListener('DOMContentLoaded', function() { refreshDirectory(); loadChainStats(); // Set up form submission document.getElementById('registerForm').addEventListener('submit', function(e) { e.preventDefault(); registerKey(); }); document.getElementById('verifyForm').addEventListener('submit', function(e) { e.preventDefault(); verifyKey(); }); // Auto-refresh every 30 seconds setInterval(refreshDirectory, 30000); }); // Register a new public key async function registerKey() { const username = document.getElementById('username').value; const email = document.getElementById('email').value; const keyType = document.getElementById('keyType').value; const publicKey = document.getElementById('publicKey').value; if (!username || !publicKey) { showNotification('Please fill in all required fields', 'error'); return; } const formData = { username: username, email: email, key_type: keyType, public_key: publicKey }; try { const response = await fetch('api/register_key.php', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(formData) }); const data = await response.json(); if (data.success) { showNotification(data.message, 'success'); document.getElementById('registerForm').reset(); refreshDirectory(); loadChainStats(); } else { showNotification('Error: ' + data.message, 'error'); } } catch (error) { console.error('Error:', error); showNotification('Failed to register key', 'error'); } } // Refresh the key directory async function refreshDirectory() { try { const response = await fetch('api/get_all_keys.php'); const data = await response.json(); if (data.success) { displayKeys(data.keys); document.getElementById('keyCount').textContent = data.total + ' keys'; } } catch (error) { console.error('Error:', error); } } // Display keys in the directory function displayKeys(keys) { const container = document.getElementById('keyContainer'); if (!keys || keys.length === 0) { container.innerHTML = '<div class="text-center text-gray-500 py-8">No keys registered yet</div>'; return; } let html = ''; keys.forEach(key => { const timeStr = key.formatted_time; const keyTypeColor = getKeyTypeColor(key.key_type); html += ` <div class="key-card bg-gray-700 bg-opacity-30 rounded-lg p-4"> <div class="flex items-start justify-between mb-3"> <div class="flex items-center space-x-3"> <div class="w-10 h-10 rounded-full bg-blue-600 bg-opacity-20 flex items-center justify-center"> <i class="fas fa-key text-blue-400"></i> </div> <div> <h3 class="font-bold text-lg">${escapeHtml(key.username)}</h3> <p class="text-xs text-gray-400">${key.email || 'No email'}</p> </div> </div> <span class="text-xs ${keyTypeColor} px-2 py-1 rounded-full">${key.key_type}</span> </div> <div class="mb-3"> <p class="text-xs text-gray-400 mb-1">Public Key (preview):</p> <div class="bg-gray-800 rounded p-2 font-mono text-xs text-green-400 overflow-x-auto"> ${escapeHtml(key.public_key_short)} </div> </div> <div class="grid grid-cols-2 gap-2 text-xs"> <div> <span class="text-gray-500">Block:</span> <span class="text-gray-300 ml-1">#${key.block_index}</span> </div> <div> <span class="text-gray-500">Registered:</span> <span class="text-gray-300 ml-1">${timeStr}</span> </div> </div> <div class="mt-2 hash-value text-gray-500"> <span class="text-gray-500">Hash:</span> <span class="text-gray-400 ml-1">${key.hash.substring(0, 20)}...</span> </div> <div class="mt-3 flex space-x-2"> <button onclick="lookupKey('${key.username}')" class="flex-1 bg-gray-600 hover:bg-gray-500 text-white text-xs py-1 px-2 rounded transition"> <i class="fas fa-search mr-1"></i> Details </button> <button onclick="copyKey('${key.public_key.replace(/'/g, "\\'")}')" class="bg-gray-600 hover:bg-gray-500 text-white text-xs py-1 px-2 rounded transition"> <i class="fas fa-copy"></i> </button> </div> </div> `; }); container.innerHTML = html; } // Load chain statistics async function loadChainStats() { try { const response = await fetch('api/get_chain_info.php'); const data = await response.json(); if (data.success) { const stats = data.stats; document.getElementById('totalBlocks').textContent = stats.total_blocks; document.getElementById('totalKeys').textContent = stats.total_keys; const statusEl = document.getElementById('chainStatus'); if (stats.chain_valid) { statusEl.innerHTML = '<span class="text-green-400">✓ Valid</span>'; } else { statusEl.innerHTML = '<span class="text-red-400">✗ Invalid</span>'; } if (stats.latest_block) { document.getElementById('lastBlock').innerHTML = `#${stats.latest_block.index}<br><span class="text-xs text-gray-400">${stats.latest_block.username}</span>`; } } } catch (error) { console.error('Error loading stats:', error); } } // Lookup key details async function lookupKey(username) { try { const response = await fetch(`api/lookup_key.php?username=${encodeURIComponent(username)}&full=true`); const data = await response.json(); if (data.success && data.found) { showKeyDetails(data); } else { showNotification('Key not found', 'error'); } } catch (error) { console.error('Error:', error); } } // Show key details modal function showKeyDetails(key) { const modal = document.createElement('div'); modal.className = 'fixed inset-0 bg-black bg-opacity-75 flex items-center justify-center z-50'; modal.innerHTML = ` <div class="bg-gray-800 rounded-xl p-6 max-w-2xl w-full mx-4 border border-gray-700 max-h-[80vh] overflow-y-auto"> <h3 class="text-xl font-bold mb-4 flex items-center"> <i class="fas fa-key text-blue-400 mr-2"></i> Key Details: ${escapeHtml(key.username)} </h3> <div class="space-y-4"> <div class="grid grid-cols-2 gap-4"> <div> <p class="text-sm text-gray-400">Key Type</p> <p class="font-semibold">${key.key_type}</p> </div> <div> <p class="text-sm text-gray-400">Email</p> <p class="font-semibold">${key.email || 'Not provided'}</p> </div> <div> <p class="text-sm text-gray-400">Registered</p> <p class="font-semibold">${key.registered}</p> </div> <div> <p class="text-sm text-gray-400">Block Index</p> <p class="font-semibold">#${key.block_index}</p> </div> </div> <div> <p class="text-sm text-gray-400 mb-2">Full Public Key</p> <pre class="bg-gray-900 p-3 rounded-lg text-xs text-green-400 overflow-x-auto">${escapeHtml(key.public_key)}</pre> </div> <div> <p class="text-sm text-gray-400 mb-2">Block Hash</p> <pre class="bg-gray-900 p-3 rounded-lg text-xs text-blue-400 break-all">${key.hash}</pre> </div> ${key.history && key.history.length > 0 ? ` <div> <p class="text-sm text-gray-400 mb-2">Key History</p> <div class="space-y-2"> ${key.history.map(h => ` <div class="bg-gray-700 p-2 rounded text-xs"> <span class="text-gray-400">${h.action}</span> - <span class="text-gray-300">Block #${h.block_index}</span> <span class="text-gray-500 ml-2">${h.formatted_time}</span> </div> `).join('')} </div> </div> ` : ''} </div> <button onclick="this.closest('.fixed').remove()" class="mt-6 w-full bg-blue-600 hover:bg-blue-700 text-white py-2 rounded-lg transition"> Close </button> </div> `; document.body.appendChild(modal); } // Show verification form function verifyKeyForm() { document.getElementById('verifyModal').classList.remove('hidden'); } // Close verification modal function closeVerifyModal() { document.getElementById('verifyModal').classList.add('hidden'); document.getElementById('verifyForm').reset(); document.getElementById('verifyResult').classList.add('hidden'); } // Verify a key async function verifyKey() { const username = document.getElementById('verifyUsername').value; const publicKey = document.getElementById('verifyPublicKey').value; if (!username || !publicKey) { showNotification('Please fill in all fields', 'error'); return; } const formData = { username: username, public_key: publicKey }; try { const response = await fetch('api/verify_key.php', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(formData) }); const data = await response.json(); const resultDiv = document.getElementById('verifyResult'); resultDiv.classList.remove('hidden'); if (data.verified) { resultDiv.className = 'mt-4 p-3 rounded-lg bg-green-600 bg-opacity-20 border border-green-600 text-green-400'; resultDiv.innerHTML = ` <div class="flex items-center"> <i class="fas fa-check-circle mr-2"></i> <div> <p class="font-bold">✅ Verified!</p> <p class="text-sm">This key belongs to ${data.key_info.username}</p> <p class="text-xs mt-1">Registered: ${data.key_info.registered}</p> </div> </div> `; } else { resultDiv.className = 'mt-4 p-3 rounded-lg bg-red-600 bg-opacity-20 border border-red-600 text-red-400'; resultDiv.innerHTML = ` <div class="flex items-center"> <i class="fas fa-times-circle mr-2"></i> <div> <p class="font-bold">❌ Verification Failed</p> <p class="text-sm">${data.message}</p> </div> </div> `; } } catch (error) { console.error('Error:', error); showNotification('Verification failed', 'error'); } } // Generate a sample RSA public key for demonstration function generateSampleKey() { const sampleKeys = [ `-----BEGIN PUBLIC KEY----- MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAu1SU1LfVLPHCYZM6Gj1l X1pVqJqXoJ4lYQhYQjKXxqYxKqGqQwYxKqGqQwYxKqGqQwYxKqGqQwYxKqGqQw== -----END PUBLIC KEY-----`, `-----BEGIN PUBLIC KEY----- MCowBQYDK2VwAyEADd6j0hVYwTQfYj9XhQkYyMZpLqWgXzQvQKqGqQwYxKqGqQ== -----END PUBLIC KEY-----` ]; const randomKey = sampleKeys[Math.floor(Math.random() * sampleKeys.length)]; document.getElementById('publicKey').value = randomKey; } // Copy key to clipboard function copyKey(key) { navigator.clipboard.writeText(key).then(() => { showNotification('Key copied to clipboard!', 'success'); }).catch(() => { showNotification('Failed to copy', 'error'); }); } // Helper: Get color for key type function getKeyTypeColor(type) { const colors = { 'RSA': 'bg-blue-600 text-blue-100', 'ECDSA': 'bg-green-600 text-green-100', 'Ed25519': 'bg-purple-600 text-purple-100', 'DSA': 'bg-yellow-600 text-yellow-100' }; return colors[type] || 'bg-gray-600 text-gray-100'; } // Helper: Escape HTML function escapeHtml(text) { if (!text) return ''; const div = document.createElement('div'); div.textContent = text; return div.innerHTML; } // Helper: Show notification function showNotification(message, type = 'info') { const notification = document.createElement('div'); notification.className = `fixed top-4 right-4 px-6 py-3 rounded-lg shadow-lg z-50 animate-bounce ${ type === 'success' ? 'bg-green-600' : type === 'error' ? 'bg-red-600' : 'bg-blue-600' } text-white`; notification.textContent = message; document.body.appendChild(notification); setTimeout(() => { notification.remove(); }, 3000); }

10. style.css (Additional Styles)

/* style.css - Additional custom styles */ /* Custom scrollbar */ #keyContainer::-webkit-scrollbar { width: 8px; } #keyContainer::-webkit-scrollbar-track { background: #1f2937; border-radius: 4px; } #keyContainer::-webkit-scrollbar-thumb { background: #3b82f6; border-radius: 4px; } #keyContainer::-webkit-scrollbar-thumb:hover { background: #60a5fa; } /* Key card animations */ .key-card { position: relative; overflow: hidden; } .key-card::before { content: ''; position: absolute; top: 0; left: 0; right: 0; bottom: 0; background: linear-gradient(90deg, transparent, rgba(59, 130, 246, 0.1), transparent); transform: translateX(-100%); transition: transform 0.5s ease; } .key-card:hover::before { transform: translateX(100%); } /* Hash display */ .hash-display { font-family: 'Monaco', 'Menlo', monospace; font-size: 0.7rem; line-height: 1.2; word-break: break-all; } /* Chain link visualization */ .chain-link { border-left: 2px solid #3b82f6; margin-left: 12px; padding-left: 12px; } /* Loading spinner */ .loading-spinner { border: 3px solid rgba(59, 130, 246, 0.3); border-radius: 50%; border-top: 3px solid #3b82f6; width: 40px; height: 40px; animation: spin 1s linear infinite; } @keyframes spin { 0% { transform: rotate(0deg); } 100% { transform: rotate(360deg); } } /* Key type badges */ .key-type-badge { font-size: 0.65rem; padding: 0.2rem 0.5rem; border-radius: 9999px; font-weight: 500; } /* Timestamp style */ .timestamp { font-size: 0.7rem; color: #9ca3af; } /* Verification result animations */ @keyframes verifyPulse { 0%, 100% { opacity: 1; } 50% { opacity: 0.7; } } .verify-success { animation: verifyPulse 2s ease-in-out infinite; }

How to Run

  1. Setup Environment:
  • Ensure XAMPP/WAMP/Laragon is installed with PHP 7.4+ and MySQL
  • Start Apache and MySQL services
  1. Database Setup:
  • Open phpMyAdmin (http://localhost/phpmyadmin)
  • Create database keychain_db
  • Import database/schema.sql
  1. Configure Database:
  • Update includes/config.php with your database credentials
  1. Place Files:
  • Create folder public-key-directory in web root
  • Copy all files maintaining the structure
  1. Access Application:
  • Open browser and navigate to: http://localhost/public-key-directory/

Testing the Public Key Directory

  1. Register a Key:
  • Enter a username (e.g., "alice")
  • Optional email
  • Select key type
  • Paste a public key (or use the "Generate Sample" button)
  • Click "Register Key"
  1. Browse Directory:
  • View all registered keys
  • See key previews and metadata
  • Click "Details" for full key information
  1. Verify a Key:
  • Click "Verify a Key" button
  • Enter username and public key
  • See verification result
  1. Test Key Rotation:
  • Register same username with different key
  • View history to see all versions

Security Features

  • Immutable Chain: Each key registration is permanently recorded
  • Proof of Work: Mining adds computational cost to prevent spam
  • Chain Verification: Automatic integrity checking
  • Key History: Track all key changes over time
  • Tamper Detection: Any modification breaks the chain

Use Cases

  • Developer Tools: Store and verify GPG keys for code signing
  • Email Encryption: Lookup public keys for secure communication
  • Blockchain Identity: Link blockchain addresses to public keys
  • IoT Device Authentication: Register device public keys
  • Code Signing: Verify software authenticity

This project demonstrates a decentralized alternative to traditional Certificate Authorities, providing transparent and verifiable public key storage without central points of failure.

Leave a Reply

Your email address will not be published. Required fields are marked *


Macro Nepal Helper