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