$max) $v = mb_substr($v, 0, $max); return preg_replace('/[\x00-\x08\x0B\x0C\x0E-\x1F\x7F]/', '', $v); } /** * Clean a public key from any format to raw key content. * Handles: PEM (any type), SSH format, hex, base64, raw paste. */ function cleanPublicKey(string $raw): string { $key = trim($raw); if (empty($key)) return ''; // Strip PEM headers/footers (-----BEGIN ... -----, -----END ... -----) $key = preg_replace('/-----BEGIN [A-Z\s]+-----/', '', $key); $key = preg_replace('/-----END [A-Z\s]+-----/', '', $key); // Strip SSH prefix (ssh-ed25519, ssh-rsa, ecdsa-sha2-nistp256, etc.) $key = preg_replace('/^(ssh-\S+|ecdsa-\S+)\s+/', '', trim($key)); // Strip SSH trailing comment (user@host) $key = preg_replace('/\s+\S+@\S+\s*$/', '', $key); $key = preg_replace('/\s+[^\s=+\/]+\s*$/', '', $key); // Remove all whitespace and newlines $key = preg_replace('/\s+/', '', $key); return $key; } // ============================================ // IDENTICON GENERATION VIA CLI // ============================================ function generateIdenticonBase64(string $input, int $size): ?string { if (!is_executable(IDENTICONS_CLI)) return null; $cmd = IDENTICONS_CLI . ' -input ' . escapeshellarg($input) . ' -size ' . intval($size) . ' -transparent -format base64 2>&1'; $out = trim(shell_exec($cmd) ?? ''); if (empty($out) || !preg_match('/^[A-Za-z0-9+\/=]+$/', $out)) return null; return $out; } // ============================================ // FACE HEADER FOLDING (RFC 2822) // ============================================ function foldFaceHeader(string $base64): string { $maxFirst = 72 - strlen('Face: '); $maxCont = 72 - 1; $lines = ['Face: ' . substr($base64, 0, $maxFirst)]; $offset = $maxFirst; while ($offset < strlen($base64)) { $lines[] = ' ' . substr($base64, $offset, $maxCont); $offset += $maxCont; } return implode("\n", $lines); } // ============================================ // ED25519 KEYPAIR GENERATION // ============================================ function generateKeypair(): array { $kp = sodium_crypto_sign_keypair(); $sk = sodium_crypto_sign_secretkey($kp); $pk = sodium_crypto_sign_publickey($kp); $result = [ 'publicKey' => base64_encode($pk), 'secretKey' => base64_encode($sk), 'created' => gmdate('c'), ]; // Wipe sensitive material from memory sodium_memzero($sk); sodium_memzero($kp); return $result; } // ============================================ // ED25519 SIGNATURE VERIFICATION // ============================================ function verifySignature(string $message, string $signatureB64, string $pubkeyB64): bool { $sig = base64_decode($signatureB64, true); $pk = base64_decode($pubkeyB64, true); if ($sig === false || $pk === false) return false; if (strlen($sig) !== 64 || strlen($pk) !== 32) return false; return sodium_crypto_sign_verify_detached($sig, $message, $pk); } // ============================================ // STATIC IDENTITY PAGE GENERATOR // ============================================ function buildIdentityPageHTML( string $username, string $email, string $pubkey, string $hash, string $identicon256, string $faceBase64 ): string { $u = htmlspecialchars($username, ENT_QUOTES, 'UTF-8'); $e = htmlspecialchars($email, ENT_QUOTES, 'UTF-8'); $p = htmlspecialchars($pubkey, ENT_QUOTES, 'UTF-8'); $h = htmlspecialchars($hash, ENT_QUOTES, 'UTF-8'); $faceHeader = htmlspecialchars(foldFaceHeader($faceBase64), ENT_QUOTES, 'UTF-8'); $vfaceHeaders = htmlspecialchars( "From: {$username} <{$email}>\n" . "Ed25519-Pub: {$pubkey}\n" . "Ed25519-Sig: [sign message body with your private key]\n" . "Identity-Hash: {$hash}\n" . foldFaceHeader($faceBase64), ENT_QUOTES, 'UTF-8' ); return << VFACE Identity — {$u}

VFACE Identity

Identicon
Username
{$u}
Email
{$e}
Public Key
{$p}
Identity Hash
{$h}
VFACE Headers for Usenet / Email:
Copy these headers into your client. Replace the signature line after signing your message body.
{$vfaceHeaders}
How to verify this identity:

1. Concatenate this string:
Click to select all — then copy
2. Calculate SHA256 of that string — it should match the Identity Hash above.

3. Go to identicons.virebent.art and use the Analyze tab to verify the identicon, or the Create tab to regenerate it from the same input.

4. For signed messages: verify the Ed25519 signature with the public key above.

5. Check the .ots file alongside this page for Bitcoin timestamp proof (opentimestamps.org).
HTML; } // ============================================ // CH1FFR3PUNK PALETTES (for analysis) // ============================================ const PRIMARY_PALETTE = [ [0x00, 0xbf, 0x93], [0x2d, 0xcc, 0x70], [0x42, 0xe4, 0x53], [0xf1, 0xc4, 0x0f], [0xe6, 0x7f, 0x22], [0xff, 0x94, 0x4e], [0xe8, 0x4c, 0x3d], [0x35, 0x98, 0xdb], [0x9a, 0x59, 0xb5], [0xef, 0x3e, 0x96], [0xdf, 0x21, 0xb9], [0x7d, 0xc2, 0xd2], [0x16, 0xa0, 0x86], [0x27, 0xae, 0x61], [0x24, 0xc3, 0x33], [0x1c, 0xab, 0xbb], ]; const SECONDARY_PALETTE = [ [0x34, 0x49, 0x5e], [0x95, 0xa5, 0xa5], [0xd2, 0x54, 0x00], [0xc1, 0x39, 0x2b], [0x29, 0x7f, 0xb8], [0x8d, 0x44, 0xad], [0xbe, 0x12, 0x7e], [0xe5, 0x23, 0x83], [0x27, 0xae, 0x61], [0x24, 0xc3, 0x33], [0xd9, 0xd9, 0x21], [0xf3, 0x9c, 0x11], [0xff, 0x55, 0x00], [0x1c, 0xab, 0xbb], [0x23, 0x23, 0x23], [0x7e, 0x8c, 0x8d], ]; const BACKGROUNDS = [ [255, 255, 255], [243, 245, 247], [236, 240, 241], ]; function colorsMatch(array $a, array $b, int $tolerance = 5): bool { return abs($a['r'] - $b['r']) <= $tolerance && abs($a['g'] - $b['g']) <= $tolerance && abs($a['b'] - $b['b']) <= $tolerance; } function colorDistance(array $a, array $b): float { return sqrt(pow($a[0] - $b[0], 2) + pow($a[1] - $b[1], 2) + pow($a[2] - $b[2], 2)); } function analyzeIdenticon(string $filePath): array { $result = [ 'valid' => false, 'dimensions' => null, 'cell_size' => null, 'symmetric' => false, 'colors_found' => [], 'primary_color' => null, 'secondary_color' => null, 'background' => null, 'bg_match' => false, 'primary_match' => false, 'secondary_match' => false, 'palette_score' => 0, 'errors' => [], 'dataurl' => null, ]; $info = @getimagesize($filePath); if ($info === false) { $result['errors'][] = 'Not a valid image file.'; return $result; } if ($info[2] !== IMAGETYPE_PNG) { $result['errors'][] = 'Image must be PNG format.'; return $result; } $width = $info[0]; $height = $info[1]; $result['dimensions'] = "{$width}x{$height}"; if ($width !== $height) { $result['errors'][] = 'Image is not square.'; return $result; } if ($width < 10) { $result['errors'][] = 'Image too small.'; return $result; } $spriteSize = 5; $cellSize = intdiv($width, $spriteSize); $margin = intdiv($width - $spriteSize * $cellSize, 2); $result['cell_size'] = $cellSize; $img = @imagecreatefrompng($filePath); if (!$img) { $result['errors'][] = 'Failed to load PNG.'; return $result; } if (!imageistruecolor($img)) { imagepalettetotruecolor($img); } imagealphablending($img, false); imagesavealpha($img, true); ob_start(); imagepng($img); $pngData = ob_get_clean(); $result['dataurl'] = 'data:image/png;base64,' . base64_encode($pngData); $grid = []; $colorMap = []; for ($row = 0; $row < $spriteSize; $row++) { $grid[$row] = []; for ($col = 0; $col < $spriteSize; $col++) { $px = min($col * $cellSize + $margin + intdiv($cellSize, 2), $width - 1); $py = min($row * $cellSize + $margin + intdiv($cellSize, 2), $width - 1); $rgba = imagecolorat($img, $px, $py); $r = ($rgba >> 16) & 0xFF; $g = ($rgba >> 8) & 0xFF; $b = $rgba & 0xFF; $a = ($rgba >> 24) & 0x7F; $grid[$row][$col] = ['r' => $r, 'g' => $g, 'b' => $b, 'a' => $a]; if ($a < 64) { $colorMap["{$r},{$g},{$b}"] = [$r, $g, $b]; } } } $symmetryOk = true; for ($row = 0; $row < 5; $row++) { for ($pair = 0; $pair < 2; $pair++) { if (!colorsMatch($grid[$row][$pair], $grid[$row][4 - $pair], 5)) { $symmetryOk = false; break 2; } } } $result['symmetric'] = $symmetryOk; $result['colors_found'] = array_values($colorMap); $bgColor = null; $fgColors = []; $cornerColor = $grid[0][0]; foreach (BACKGROUNDS as $bg) { if (colorsMatch(['r'=>$bg[0],'g'=>$bg[1],'b'=>$bg[2],'a'=>0], $cornerColor, 5)) { $result['bg_match'] = true; $bgColor = $bg; break; } } $hasTransparent = false; for ($row = 0; $row < 5; $row++) { for ($col = 0; $col < 5; $col++) { if ($grid[$row][$col]['a'] >= 64) { $hasTransparent = true; break 2; } } } if ($hasTransparent) { $result['bg_match'] = true; $result['background'] = 'transparent'; } elseif ($bgColor) { $result['background'] = sprintf('#%02x%02x%02x', $bgColor[0], $bgColor[1], $bgColor[2]); } foreach ($colorMap as $key => $rgb) { if ($bgColor && colorsMatch(['r'=>$rgb[0],'g'=>$rgb[1],'b'=>$rgb[2],'a'=>0], ['r'=>$bgColor[0],'g'=>$bgColor[1],'b'=>$bgColor[2],'a'=>0], 5)) continue; $fgColors[] = $rgb; } foreach ($fgColors as $fg) { foreach (PRIMARY_PALETTE as $idx => $pal) { if (colorDistance($fg, $pal) < 10) { $result['primary_match'] = true; $result['primary_color'] = ['rgb'=>$fg, 'hex'=>sprintf('#%02x%02x%02x',$fg[0],$fg[1],$fg[2]), 'index'=>$idx]; break 2; } } } foreach ($fgColors as $fg) { if ($result['primary_color'] && colorDistance($fg, $result['primary_color']['rgb']) < 10) continue; foreach (SECONDARY_PALETTE as $idx => $pal) { if (colorDistance($fg, $pal) < 10) { $result['secondary_match'] = true; $result['secondary_color'] = ['rgb'=>$fg, 'hex'=>sprintf('#%02x%02x%02x',$fg[0],$fg[1],$fg[2]), 'index'=>$idx]; break 2; } } } $score = 0; if ($result['symmetric']) $score += 30; if ($result['bg_match']) $score += 20; if ($result['primary_match']) $score += 25; if ($result['secondary_match']) $score += 25; $result['palette_score'] = $score; $result['valid'] = ($result['symmetric'] && $result['bg_match'] && $result['primary_match'] && $result['secondary_match'] && count($colorMap) >= 2 && count($colorMap) <= 4); imagedestroy($img); return $result; } // ============================================ // PROCESS REQUESTS // ============================================ $tab = $_POST['tab'] ?? 'create'; $result = null; $verifyRes = null; $anaResult = null; $error = null; if ($_SERVER['REQUEST_METHOD'] === 'POST') { $tab = $_POST['tab'] ?? 'create'; // ---- TAB: CREATE ---- if ($tab === 'create') { $username = sanitize($_POST['username'] ?? '', 64); $email = sanitize($_POST['email'] ?? '', 254); $keyMode = $_POST['key_mode'] ?? 'generate'; $rawPubkey = sanitize($_POST['pubkey'] ?? '', 8192); if (empty($username)) { $error = 'Pick a username.'; } elseif (empty($email)) { $error = 'Enter an email address.'; } elseif ($keyMode === 'existing' && empty($rawPubkey)) { $error = 'Paste your public key.'; } if (!$error) { $keypair = null; if ($keyMode === 'generate') { if (!function_exists('sodium_crypto_sign_keypair')) { $error = 'Server does not support Ed25519 (sodium extension missing).'; } else { $keypair = generateKeypair(); $pubkey = $keypair['publicKey']; } } else { $pubkey = cleanPublicKey($rawPubkey); if (empty($pubkey)) { $error = 'Could not extract a valid key from your input.'; } } $keyCleaned = isset($pubkey) && $rawPubkey !== $pubkey; } if (!$error) { $input = $username . '|' . $email . '|' . $pubkey; $hash = hash('sha256', $input); $ico48 = generateIdenticonBase64($input, 48); $ico256 = generateIdenticonBase64($input, 256); if (!$ico48 || !$ico256) { $error = 'Failed to generate identicon (binary not found).'; } else { $result = [ 'username' => $username, 'email' => $email, 'pubkey' => $pubkey, 'keypair' => $keypair, 'keyCleaned' => $keyCleaned ?? false, 'input' => $input, 'hash' => $hash, 'ico48' => $ico48, 'ico256' => $ico256, 'face' => foldFaceHeader($ico48), 'headers' => "From: {$username} <{$email}>\n" . "Ed25519-Pub: {$pubkey}\n" . "Ed25519-Sig: [sign message body with your private key]\n" . "Identity-Hash: {$hash}\n" . foldFaceHeader($ico48), 'identityPage' => buildIdentityPageHTML( $username, $email, $pubkey, $hash, $ico256, $ico48 ), ]; } } // ---- TAB: VERIFY ---- } elseif ($tab === 'verify') { $vRawPubkey = sanitize($_POST['v_pubkey'] ?? '', 8192); $vSig = sanitize($_POST['v_signature'] ?? '', 1024); $vBody = $_POST['v_body'] ?? ''; $vUser = sanitize($_POST['v_username'] ?? '', 64); $vEmail = sanitize($_POST['v_email'] ?? '', 254); $vFace = sanitize($_POST['v_face'] ?? '', 8192); $vPubkey = cleanPublicKey($vRawPubkey); if (empty($vPubkey) || empty($vSig) || empty($vBody)) { $error = 'Public key, signature, and message body are required.'; } if (!$error) { $sigValid = verifySignature($vBody, $vSig, $vPubkey); $hashCheck = null; $icoCheck = null; $icoGenerated = null; if (!empty($vUser) && !empty($vEmail)) { $expectedInput = $vUser . '|' . $vEmail . '|' . $vPubkey; $expectedHash = hash('sha256', $expectedInput); $hashCheck = $expectedHash; $icoGenerated = generateIdenticonBase64($expectedInput, 48); if (!empty($vFace) && $icoGenerated) { $cleanFace = trim(preg_replace('/^Face:\s*/m', '', $vFace)); $cleanFace = preg_replace('/\s+/', '', $cleanFace); $icoCheck = ($cleanFace === $icoGenerated); } } $verifyRes = [ 'sig_valid' => $sigValid, 'hash_expected' => $hashCheck, 'ico_match' => $icoCheck, 'ico_generated' => $icoGenerated, ]; } // ---- TAB: ANALYZE ---- } elseif ($tab === 'analyze') { if (!isset($_FILES['identicon_file']) || $_FILES['identicon_file']['error'] !== UPLOAD_ERR_OK) { $uploadErr = $_FILES['identicon_file']['error'] ?? UPLOAD_ERR_NO_FILE; $error = match ($uploadErr) { UPLOAD_ERR_NO_FILE => 'No file uploaded.', UPLOAD_ERR_INI_SIZE, UPLOAD_ERR_FORM_SIZE => 'File too large.', default => 'Upload failed (error code: ' . $uploadErr . ').', }; } else { $file = $_FILES['identicon_file']; if ($file['size'] > MAX_UPLOAD_SIZE) { $error = 'File exceeds maximum size of ' . (MAX_UPLOAD_SIZE / 1024) . ' KB.'; } elseif ($file['size'] === 0) { $error = 'Uploaded file is empty.'; } else { $finfo = new finfo(FILEINFO_MIME_TYPE); $mime = $finfo->file($file['tmp_name']); if ($mime !== 'image/png') { $error = 'Only PNG files are accepted (detected: ' . htmlspecialchars($mime) . ').'; } else { $anaResult = analyzeIdenticon($file['tmp_name']); if (!empty($anaResult['errors'])) { $error = implode(' ', $anaResult['errors']); } // Optional: compare with identity $anaUser = sanitize($_POST['ana_username'] ?? '', 64); $anaEmail = sanitize($_POST['ana_email'] ?? '', 254); $anaRawKey = sanitize($_POST['ana_pubkey'] ?? '', 8192); $anaKey = cleanPublicKey($anaRawKey); if (!empty($anaUser) && !empty($anaEmail) && !empty($anaKey) && $anaResult && !$error) { $compareInput = $anaUser . '|' . $anaEmail . '|' . $anaKey; $compareHash = hash('sha256', $compareInput); // Read raw uploaded file and base64 it $uploadedB64 = base64_encode(file_get_contents($file['tmp_name'])); // Generate at both sizes and try matching $compareIco48 = generateIdenticonBase64($compareInput, 48); $compareIco256 = generateIdenticonBase64($compareInput, 256); $match = ($compareIco48 && $uploadedB64 === $compareIco48) || ($compareIco256 && $uploadedB64 === $compareIco256); $dims = explode('x', $anaResult['dimensions'] ?? '48x48'); $showSize = (intval($dims[0]) > 48) ? 256 : 48; $showIco = ($showSize === 256) ? $compareIco256 : $compareIco48; $anaResult['comparison'] = [ 'hash' => $compareHash, 'generated' => $showIco, 'match' => $match, ]; } } } } } } // Preserve form values $fUser = htmlspecialchars($_POST['username'] ?? '', ENT_QUOTES, 'UTF-8'); $fEmail = htmlspecialchars($_POST['email'] ?? '', ENT_QUOTES, 'UTF-8'); $fPubkey = htmlspecialchars($_POST['pubkey'] ?? '', ENT_QUOTES, 'UTF-8'); $fKeyMode = $_POST['key_mode'] ?? 'generate'; ?> VFACE — Your Identity, Cryptographically Yours

VFACE

Your pseudonym, cryptographically yours.
VFACE gives you a persistent identity that nobody can steal or fake. Pick a name, get a unique visual fingerprint tied to your key. Anyone can verify it's really you — without knowing who you are.
How does it work? Read the full explanation →
Reset
Use something@example.invalid if you want a fictional address.
Works with any key type or format: Ed25519, RSA, PEM, SSH, hex strings. Headers like -----BEGIN PUBLIC KEY----- and SSH prefixes are stripped automatically.
YubiKey users: paste your public key — the private key stays in the hardware. This is the recommended setup. Compatible with yubicrypt and yubisigner.
A fresh Ed25519 key pair will be generated for you. You'll download it as a JSON file — keep it safe, it's your identity.

Your VFACE Identity

Your identicon
This is your face.
Name
Email
Public Key
Your key was normalized — headers, prefixes, and formatting were removed. The cleaned version above is what defines your identity. Always use this exact value when verifying.
Identity Hash
Save your key pair now. This is the only time you'll see your private key. Download the file below and store it somewhere safe. If you lose it, this identity is gone forever. Nobody — including this server — has a copy.
 'Ed25519',
                'publicKey' => $result['keypair']['publicKey'],
                'secretKey' => $result['keypair']['secretKey'],
                'created'   => $result['keypair']['created'],
                'username'  => $result['username'],
                'email'     => $result['email'],
                'identityHash' => $result['hash'],
            ], JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES)) ?>

VFACE Headers

Copy these into your Usenet/email client. Replace the signature line after signing your message body.

What's next?

1
Save your key pair (if generated above).
Store the JSON file on an encrypted drive. Consider a YubiKey for maximum security.
2
Publish your identity page.
Host the static HTML page below on your website or .onion service. It's your public identity card.
3
Timestamp it on Bitcoin.
Run ots stamp identity-page.html to anchor your identity on the blockchain. This proves when you first claimed this name — nobody can backdate a fake.
4
Start signing messages.
Use your private key to sign every message you send. Recipients verify with your public key.

Download Identity Page

A ready-to-publish static HTML page. Upload it to your web server and timestamp it with OpenTimestamps for first-claim proof.
About VFACE → How identicons work →
Upload an identicon image to check if it's a valid Ch1ffr3punk identicon. Optionally enter identity details to check if the image belongs to a specific identity.
Any square PNG: 48×48, 256×256, etc. Max KB.
Optional — enter identity details to check if this identicon belongs to a specific person.

Analysis

= 50) { $verdictClass = 'unknown'; $verdictText = 'Partial match — some identicon properties detected (score: ' . $score . '/100)'; } else { $verdictClass = 'fail'; $verdictText = 'Not a valid Ch1ffr3punk identicon (score: ' . $score . '/100)'; } ?>
Uploaded image
  • Horizontal symmetry (5×5 grid, mirrored columns)
  • Background:
  • Primary color: (palette index ) no match in primary palette
  • Secondary color: (palette index ) no match in secondary palette
Dimensions
Cell Size px
Unique Colors

Identity Comparison

This identicon matches the identity provided.
This identicon does NOT match the identity provided.
Identity Hash
Uploaded
Uploaded
Expected
Expected
Advanced — verify an Ed25519 signed message. Paste the details from the message headers to check if the signature is genuine.
Optional — add identity details for full VFACE verification.

Verification Result

Signature is valid — this message is authentic.
Signature is INVALID — this message may be forged.
Identity Hash
Face header matches the expected identicon.
Face header does NOT match — possible impersonation.
Expected identicon
Expected identicon for this identity