This commit is contained in:
jake 2024-11-11 04:54:24 -05:00
commit 3656213c0c
10 changed files with 853 additions and 0 deletions

2
.gitignore vendored Normal file
View file

@ -0,0 +1,2 @@
*.asc
keyserver.conf

65
README.md Normal file
View file

@ -0,0 +1,65 @@
# jake's keyserver
This program pokes gpg when it receives a key. Then it does stuff to the output gpg produces and stores it in a postgres database.
## Features
* 'Secret' upload path.
* Disable upload for secret, normal path, or both. For when you don't want people to upload keys.
## Requirements
* Relevant Mojo/Mojolicious modules
* Postgresql server
* GnuPG
### Install on your distro
Maybe it is packaged by your distro maintainers?
* Debian
```
apt install libmojolicious-perl libmojo-pg-perl
```
(Not sure if you'll need to cpan Mojo::File and/or Mojo::Util)
### Install via cpan (or cpan-minus, considered way better by most)
```
cpanm Mojolicious Mojo::Pg Mojo::File Mojo::Util
```
Installing via cpan(m) will work because the Mojolicious devs are competent.
## To use
### Create the config file
```
cp keyserver.conf.example keyserver.conf
```
Note that hypnotoad/morbo looks for the config file in the same directory as it was called from. I have no idea how to change this as there isn't a --config-file option one can use with hypnotoad or morbo.
### Create relevant details for the database.
An example that you may follow:
```
sudo -u postgres psql
postgres=# create database jjakkekeyserverdb;
postgres=# create user jjakkekeyserver with encrypted password 'password';
postgres=# grant all privileges on database jjakkekeyserverdb to jjakkekeyserver;
postgres=# \c jjakkekeyserverdb;
postgres=# grant all privileges on schema public to jjakkekeyserver;
```
### Start Program
```
hypnotoad -f keyserver; # starts in foreground
```
### Proxy
It's a good idea to proxy this program behind another dedicated program that listens on relevant ports: no TLS, 11371 and 80; with TLS, 11372 and 443.
## Usage
### GnuPG examples
```
gpg --keyserver hkp://hostname --send-keys <keyid>
gpg --keyserver hkp://hostname --search-keys <search string>
gpg --keyserver hkp://hostname --recv-keys <keyid>
```
### Web browser
http://hostname

523
keyserver Executable file
View file

@ -0,0 +1,523 @@
#!/usr/bin/env perl
# this program should be ran as a seperate user but dedicated user
use Mojolicious::Lite -signatures;
use Mojo::Pg;
use Smart::Comments;
use Mojo::File qw(tempdir tempfile);
use Mojo::Util qw(url_escape url_unescape);
use List::Util qw(uniq);
#use Mojo::Cache;
#use Carp::Always;
## no critic (prototype)
app->plugin('Config');
my $VERSION = "0.0.1";
my $config = app->config;
#my $cache = Mojo::Cache->new(max_keys => 5);
my $start_time;
my $magic_delimiter = '||,;,||';
my $gpg_options = "--no-default-keyring --keyring /dev/null --dry-run";
my $tmpdir;
my $pg = Mojo::Pg->new("postgresql://$config->{pguser}:$config->{pgpass}\@$config->{pghost}/$config->{pgdb}");
# operations supported:
# get (SHOULD)
# index (MAY)
# stats
# operations to be added:
# vindex
# vfpget
# kidget
# hget
# x- implimentation specifiec
my %operations = (
get => sub {
#$cache->set(operations => ($cache->get('operations') + 1));
operation();
my ($params) = @_;
$params->{search}=~ s/^0x(.*)/$1/;
my $search = $params->{search};
my $key = $pg->db->select('gpg_key', ['armored','fingerprint'], {fingerprint => { -ilike =>[ "%$search"] }})->hash;
return $key->{armored},$key->{fingerprint} if ref $key eq 'HASH';
},
index => sub {
#$cache->set(operations => ($cache->get('operations') + 1));
operation();
my ($params) = @_;
exists $params->{search} and $params->{search} =~ m/[\w@\s.]+/ or return;
my $safe = url_escape $params->{search};
my $fmatches = $pg->db->select('gpg_key',['id'],{ fingerprint => { -ilike => ["%$safe%"] }})->arrays;
my $umatches = $pg->db->select('gpg_uid',['relatedto'],{ uid => { -ilike => ["%$safe%"] }})->arrays;
# ^^ returns a Mojo::Collection
my @matches = @{$fmatches};
push @matches, @{$umatches};
@matches = map {@$_} @matches;
@matches = sort @matches;
return if not @matches;
@matches = uniq @matches;
if (exists $params->{options} and $params->{options} and $params->{options} =~ m/mr/) { # machine readable
my @output;
my @keys;
my $keys = 0;
for my $id (@matches) {
push @keys, $pg->db->select('gpg_key',['blob','fingerprint','version','flags','id'],{ id => $id })->hashes->to_array;
}
@keys = map {@$_} @keys;
for my $i (@keys) {
for my $row (split /\n/, $i->{blob}) {
my $record = substr($row, 0, 3);
next if ($record ne 'pub');
my @data = split /:/, $row;
my $keylen = $data[2];
my $algo = $data[3];
my $create = $data[5];
my $expire = $data[6];
{
no warnings 'uninitialized';
# pub:<keyid>:<algorithm>:<keylen>:<creationdate>:<expirationdate>:<flags>:<version>
push @output, "pub:$i->{fingerprint}:$algo:$keylen:$create:$expire:$i->{flags}:$i->{version}";
}
$keys++;
my $uids = $pg->db->select('gpg_uid',['uid', 'creationdate', 'expirationdate','flags'], { relatedto => $i->{id} })->hashes;
for my $uid (@{ $uids->to_array}) {
my $uidstring = url_unescape($uid->{uid});
next unless $uidstring;
{
no warnings 'uninitialized';
# uid:<uidstring>:<creationdate>:<expirationdate>:<flags>
push @output, "uid:$uidstring:$uid->{creationdate}:$uid->{expirationdate}:$uid->{flags}";
}
}
}
}
unshift @output, "info:1:$keys";
push @output, "\n"; # sometimes `gpg --search-keys whatever` results in that last uid being cut off, this fixes that
return \@output;
}
else { # human
my $data = '';
my @keys;
for my $id (@matches) {
push @keys, $pg->db->select('gpg_key',['blob'],{ id => $id })->arrays->to_array;
}
@keys = map {@$_} @keys;
@keys = map {@$_} @keys;
return \@keys, 'ok'; # template will handle this data
}
},
stats => sub {
return stats();
}
);
# modern
# http://keys.example.com:11371/pks/lookup/v1/index/dshaw
get '/pks/lookup/v1/:op/:search' => sub ($c) {
# machine readable format returned
# 'stats' operation cannot be used here
return $c->render(text=>"Forbidden (try /pks/stats)", status=>403) if (fc $c->param('search') eq fc 'stats');
$c->res->headers->add('Access-Control-Allow-Origin' => '*');
if (($c->param('op') =~ m/[\w\d]+/)
and (exists $operations{$c->param('op')})
and ($c->param('search') =~ m/[\w\d]+/))
{
my $other_options = $c->params('options');
my ($res,$meta) = $operations{index}({ search => $c->param('search'), options => "mr,$other_options"});
if ($res) {
if (ref $res eq 'ARRAY') {
return $c->render(text=>join("\n", @$res));
}
else {
if (fc $c->param('op') eq fc 'get') {
$c->res->headers->content_type('application/pgp-keys');
$c->res->headers->content_disposition("attachment;filename=$meta.asc");
}
return $c->render(text=>$res);
}
}
else {
return $c->render(text=>"Not Found", status=>404);
}
}
if (not exists $operations{$c->param('op')}) {
return $c->render(text=>"Not Impliemented", status=>501);
}
};
# 'legacy'
# http://keys.example.com:11371/pks/lookup?search=dshaw&op=index
get '/pks/lookup' => sub ($c) {
$c->res->headers->add('Access-Control-Allow-Origin' => '*');
if (lc $c->param('op') eq 'stats') {
#$cache->set(operations => ($cache->get('operations') + 1));
operation();
return $c->render(text=>stats());
}
if ($c->param('op') =~ m/[\w\d]+/
and (exists $operations{lc $c->param('op')})
and ($c->param('search') =~ m/[\w\d]+/))
{
my ($res, $meta) = $operations{lc $c->param('op')}({ search => $c->param('search'), options => $c->param('options')});
if ($res) {
if (ref $res eq 'ARRAY') {
if (defined $c->param('options') and lc $c->param('options') eq 'mr') {
return $c->render(text=>join("\n", @$res));
}
$c->stash(mydata => $res);
return $c->render(layout=>'lookup');
}
else {
if (lc $c->param('op') eq 'get') {
$c->res->headers->content_type('application/pgp-keys');
$c->res->headers->content_disposition("attachment;filename=$meta.asc");
return $c->render(text=>$res);
}
$c->stash(mydata => $res);
return $c->render(layout=>'lookup');
}
}
else {
return $c->render(text=>"Not Found", status=>404);
}
}
return $c->render(text=>"Not Impliemented", status=>501);
};
sub stats {
#$cache->set(operations => ($cache->get('operations') + 1));
operation();
my $output = "jjakke's keyserver ($VERSION)\n\n";
$output .= `gpg $gpg_options --version`;
$output =~ s/Home: (.*)/Home: [redacted]/g;
$output .= "\n";
my $data = $pg->db->select('this_service', ['starttime', 'operations'])->array;
my $elapsed = time - $data->[0];
my $days = int($elapsed / 86400);
my $hours = int(($elapsed % 86400) / 3600);
my $minutes = int(($elapsed % 3600) / 60);
my $seconds = $elapsed % 60;
$output .= sprintf("uptime: %d days, %02d:%02d:%02d\n", $days, $hours, $minutes, $seconds);
$output .= "Operations performed since uptime: $data->[1]";
return $output;
}
get '/pks/stats' => sub ($c) {
return $c->render(text=>stats());
};
post '/pks/add' => sub ($c) {
if ($config->{'pksadd'} == 1) {
return add($c);
}
return $c->render(text=>'Not Accepting Keys', status => 403);
};
get $config->{secret_add} => sub ($c) {
if ($config->{secret_add_ok} == 1) {
return $c->render(layout => 'secretadd.html');
}
return $c->render(text=>'Not Accepting Keys', status => 403);
};
post $config->{secret_add} => sub ($c) {
if ($config->{secret_add_ok} == 1) {
return add($c);
}
return $c->render(text=>'Not Accepting Keys', status => 403);
};
sub add ($c) {
#$cache->set(operations => ($cache->get('operations') + 1));
operation();
# "keytext" must contain ASCII-armored keyring to add
# if not accepting keys over http return 403, OR 501 if not implitmented.
my $keytext = $c->param('keytext');
if (defined $keytext) {
if (ref $keytext) { # eg Mojo::Upload
return $c->render(text=>"not accepting (must be plain text, not a 'file upload')", status=>422);
}
} else { # not defined
return $c->render(text=>"not accepting (no value)", status=>422);
}
if ($keytext !~ m/^-----BEGIN PGP PUBLIC KEY BLOCK-----/) {
return $c->render(text=>"not accepting (mystery headers)", status=>422);
}
my @keytexts = split /-----END PGP PUBLIC KEY BLOCK-----\n/, $keytext; # only one key.
if (scalar @keytexts >= 2) {
return $c->render(text=>"not accepting (too many keys)", status=>422);
}
undef @keytexts;
my $tmpfile = tempfile("$tmpdir/XXXXXXXXXXX");
open my $fh, '>', $tmpfile;
syswrite $fh, $keytext, length $keytext;
close $fh;
my $res = `gpg $gpg_options --with-colons --with-fingerprint --with-fingerprint $tmpfile`;
if ($? ne 0) {
return $c->render(text=>"not accepting (gpg bugged out)", status=>422);
}
# need to get 'version number' for machine readable format and
# this is the only way that I can think of. I really hate this.
# get the output then plug it to a bunch of regexes.
my $version = `gpg $gpg_options --list-packets $tmpfile`;
my @versions;
my $record;
my $nonext = 0;
my $pub_count = 0;
for (split /\n/, $version) {
my $string = $_;
if ($string =~ m/^(:[\w\s]*:)/) {
$record = $1;
$nonext = 1;
next;
}
if ($nonext) {
$nonext = 0;
if ($string =~ m/(version (\d+))/) {
$record .= $2;
## strictly for machine readable format, so only public key info is needed.
if ($record =~ s/^:public key packet:/pub:/) {
$pub_count++;
if ($pub_count >= 2) {
return $c->render(text=>"not accepting (only one public key per upload)", status=>422);
}
}
#$record =~ s/:signature packet:/sig:/;
#$record =~ s/:public sub key packet:/sub:/;
#$record =~ s/:secret key packet:/sec:/;
#$record =~ s/:user [iI][dD] packet:/uid/; # version-less
push @versions, $record;
}
}
}
my @rows = split /\n/, $res;
my %ok_records = (
pub => 1, # public key
sub => 1, # subkey
uid => 1, # user id
uat => 1, # user id except field 10 (9 when 0-indexed)
sig => 1, # signature
fpr => 1, # fingerprint
fp2 => 1, # sha256 fingerprint
rev => 1, # revocation signature
rvk => 1, # revocation
pkd => 1, # public key data [*]
grp => 1, # Keygrip
crt => 1, # x.509 ????
tfs => 1, # TOFU satistics [*]
tru => 1, # Trust database information [*]
spk => 1, # Signature subpacket [*]
cfg => 1, # Configuration data [*]
## will never accept below
crs => 0, # x.509 and private key
sec => 0, # secret key, which we will not accpet.
ssb => 0, # secret subkey, will not accept.
);
my $reject = 0;
my $blob = '';
my $this_fullkey = '';
my @these_uids;
my $need_sig = 0;
my $this_pubs_flags = '';
my $this_version = '';
for my $row (@rows) {
my @data = split /:/, $row;
my $record = $data[0];
my $flag = $data[1];
my $keylen = $data[2];
my $algo = $data[3];
my $keyid = $data[4];
my $create = $data[5];
my $expire = $data[6];
my $signatureclass = $data[10];
my $userid = $data[9];
# uid: FName LName (comment) <email@domain>
# uat: attribute count, total size ( "3 120" )
# fpr: fingerprint
# fp2: sha256 fingerprint
# rvk: fingerprint
# grp: keygrips, delimieted by comma.
my $version;
my %version_records = (
pub => 1,
sig => 1,
sub => 1,
);
if (exists $version_records{$record}) {
$version = get_version($record, \@versions);
}
if ($record eq 'pub') {
$this_fullkey = 'next';
$this_pubs_flags = $flag;
$this_version = $version;
}
if ($this_fullkey eq 'next' and $record eq 'fpr') {
$this_fullkey = $userid;
}
if ($record eq 'uid') {
push @these_uids, $userid;
$need_sig = 1;
}
if ($need_sig and $record eq 'sig' and $signatureclass eq '[selfsig]') {
$need_sig = 0;
$these_uids[-1] .= "$magic_delimiter$create:$expire:$flag";
}
$version ? ($blob .= "$row,$version\n") : ($blob .= "$row\n");
exists $ok_records{$record} and $ok_records{$record} or $reject = 1;
}
if ($reject) {
return $c->render(text=>"not accepting (woah)", status=>422);
}
push_to_database($blob, $this_fullkey, $this_version, $this_pubs_flags, $keytext, @these_uids) if $blob;
return $c->render(text=>'key accepted', status=>200);
};
sub push_to_database($blob, $fullkey, $version, $flags, $armored, @these_uids) {
my $id = insert_gpg_key($fullkey, substr($fullkey, -16), $version, $flags, $armored, $blob);
insert_gpg_uid($id, @these_uids);
};
sub get_version($record, $versions) {
my @selector = split /:/, $versions->[0];
if ($record eq $selector[0]) {
shift @$versions;
return $selector[1];
}
return;
}
sub insert_gpg_key ($fingerprint, $keyid, $version, $flags, $armored, $blob) {
my $test = $pg->db->select('gpg_key', ['id'], {fingerprint => $fingerprint})->hash->{id};
if ($test) {
$pg->db->delete('gpg_key', {fingerprint => $fingerprint});
}
my $result = $pg->db->insert('gpg_key', {
fingerprint => $fingerprint,
keyid => $keyid,
version => $version,
flags => $flags,
armored => $armored,
blob => $blob,
},
{returning => 'id'}
)->hash->{id};
return $result;
}
sub insert_gpg_uid ($id, @these_uids) {
for my $uid (@these_uids) {
# 'magic_delimiter' but unfortunately has special meaning to split.
my @the_actual_uid = split /\|\|\,\;\,\|\|/, $uid; # 0 = the string
#my @the_actual_uid = split $magic_delimiter, $uid; # 0 = the string
$the_actual_uid[0] = url_escape $the_actual_uid[0];
my @meta = split /:/, $the_actual_uid[-1]; # 0 = creation, 1 = expiration, 2 = flags
$pg->db->insert('gpg_uid', {
uid => $the_actual_uid[0],
creationdate => $meta[0],
expirationdate => $meta[1],
flags => $meta[2],
relatedto => $id
});
}
}
get '/' => sub ($c) {
$c->render(template => 'index');
};
sub operation {
my $operations = $pg->db->select('this_service', ['operations'])->hash->{operations};
$pg->db->update('this_service', {operations => $operations + 1});
}
sub check_for_tables {
my $res = $pg->db->query('SELECT COUNT(*) FROM information_schema.tables WHERE table_schema = ?', 'public')->hash;
return $res->{count} > 0;
}
sub create_tables {
my $stmt = "
CREATE TABLE gpg_key (
id SERIAL PRIMARY KEY,
fingerprint VARCHAR(40) UNIQUE NOT NULL,
keyid VARCHAR(16),
version smallint,
flags VARCHAR(5),
armored text,
blob text);";
$pg->db->query($stmt);
# some keys have multiple uids
$stmt = "
CREATE TABLE gpg_uid (
id SERIAL PRIMARY KEY,
uid VARCHAR(1000),
creationdate INT,
expirationdate INT,
flags VARCHAR(5),
relatedto INT,
FOREIGN KEY (relatedto) REFERENCES gpg_key(id) ON DELETE CASCADE
);";
$pg->db->query($stmt);
$stmt = "
CREATE TABLE this_service (
starttime INT,
operations INT DEFAULT 0
);";
$pg->db->query($stmt);
}
if (check_for_tables()) {
say "There are tables in the database.";
$pg->db->update('this_service', {operations => 0, starttime => time});
}
else {
say "There are not tables in the database.";
create_tables();
$pg->db->insert('this_service',{
starttime => time,
operations => 0,
});
}
#app->helper( cache => sub { return $cache } );
app->helper( pg => sub { return $pg} );
app->hook(before_server_start => sub ($server, $app) {
$tmpdir = tempdir('/tmp/gpgtempXXXXXXXXXXXXXX');
});
app->start;

16
keyserver.conf.example Normal file
View file

@ -0,0 +1,16 @@
{
hypnotoad => {
listen => [
'http://127.0.0.55:8080'
],
workers => 6
},
pguser => 'jjakkekeyserver',
pgpass => 'password',
pghost => 'localhost:5432',
pgdb => 'jjakkekeyserverdb',
servermessage => '', # Message banner for http access (blank for no message banner)
pksadd => 1, # 1/0 for allowing/disallowing public key upload (eg with `gpg --send-keys`)
secret_add_ok => 1, # 1/0 for allowing/disallowing public key upload (secretly)
secret_add => '/secret/add', # route for frens and family (keep it loaded; NOT '' or '/' as these will collide)
};

21
templates/index.html.ep Normal file
View file

@ -0,0 +1,21 @@
% layout 'default';
% title 'Welcome';
<h1>Welcome to jjakke's keyserver!</h1>
<h2>Request a Public Key, using fingerprint</h2>
<form method="GET" action="/pks/lookup">
<label for="key_name_request">Fingerprint:</label>
<input type="hidden" id="op" name="op" value="get">
<input type="text" id="search" name="search" required><br>
<input type="submit" value="Request Key">
</form>
<h2>Search for keys</h2>
<form method="GET" action="/pks/lookup">
<label for="data">Search Data:</label>
<input type="hidden" id="op" name="op" value="index">
<!--<input type="hidden" id="options" name="options" value="mr">-->
<input type="text" id="search" name="search" required><br>
<input type="submit" value="Search">
</form>

View file

@ -0,0 +1,106 @@
<!DOCTYPE html>
<html>
<head>
<title><%= title %></title>
<style>
body {
font-family: Arial, sans-serif;
margin: 20px;
background-color: #f4f4f9;
}
h1 {
color: #2c3e50;
}
h2 {
color: #34495e;
}
form {
background-color: #ffffff;
padding: 20px;
border-radius: 8px;
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1);
margin-bottom: 30px;
}
label {
font-size: 14px;
font-weight: bold;
}
input[type="text"], textarea {
width: 97%;
padding: 10px;
font-size: 14px;
border: 1px solid #ccc;
border-radius: 4px;
margin-bottom: 15px;
}
input[type="submit"] {
background-color: #3498db;
color: white;
border: none;
padding: 10px 20px;
border-radius: 4px;
cursor: pointer;
}
input[type="submit"]:hover {
background-color: #2980b9;
}
.servermessage {
background-color: #f4f4f4;
border-radius: 5px;
border: 2px solid black;
color: black;
}
/* Basic styling for the <pre> block with the 'blob' class */
.blob {
background-color: #f4f4f4; /* Light gray background */
color: #333; /* Dark text color for readability */
font-family: monospace; /* Monospaced font for code-like appearance */
white-space: pre-wrap; /* Allows long lines to wrap */
word-wrap: break-word; /* Prevents long words from breaking layout */
line-height: 1.2; /* Increases line spacing for readability */
max-width: 100%; /* Ensures it takes up available width */
overflow-x: auto; /* Adds horizontal scrolling for very long lines */
padding-left: 3px;
border-radius: 5px; /* Slightly rounded corners */
border: 2px solid black;
}
/* Style for links inside the <pre> block */
.blob a {
color: #007bff; /* Blue color for links */
text-decoration: none; /* Remove underline */
}
.blob a:hover {
text-decoration: underline; /* Underline on hover for clarity */
}
/* Style for date and timestamp information */
.blob .date, .blob .fingerprint {
font-style: italic; /* Italics for dates and fingerprints */
color: #777; /* Gray color for less emphasis */
}
/* Highlighted text within the blob (e.g., "selfsig") */
.blob .highlight {
color: #e74c3c; /* Red color for highlights like "selfsig" */
font-weight: bold; /* Bold for emphasis */
}
/* Styling for signature information */
.blob .signature {
background-color: #eef9ff; /* Light blue background for signature info */
padding: 8px;
border-radius: 3px; /* Rounded corners for signature blocks */
margin-top: 5px; /* Space between signatures */
}
</style>
</head>
<body>
% if ($c->config->{servermessage}) {
%== "<div class=\"servermessage\"><p>" . $c->config->{servermessage} ."</p></div>"
% }
<%= content %>
<div><p>Operations performed since uptime: <%= $c->pg->db->select('this_service', ['operations'])->hash->{operations} %></p></div>
</body>
</html>

View file

@ -0,0 +1,71 @@
% layout 'default';
% title 'Lookup';
<h1>Welcome to jjakke's keyserver!</h1>
<p><a href="/">index</a></p>
<h2>Search results:</h2>
% for my $item (@{ $c->stash('mydata') }) {
% my $pubkey_fpr = 0;
% my $string;
% my @row = split /\n/, $item;
<pre class="blob">
<%
for my $row (@row) {
my @data = split /:/, $row;
my $record = $data[0];
my $flag = $data[1];
my $keylen = $data[2];
my $algo = $data[3];
my $keyid = $data[4];
my $create = $data[5];
my $expire = $data[6];
my $userid = $data[9];
my $signatureclass = $data[10];
if ($record eq 'pub') {
if ($expire) {
$string .= "public key: " . gmtime($create) . " -> " . ( $expire ? gmtime($expire) : 'n/a' ) . " | flags: $flag\n"
}
else {
$string .= "public key: <span class=\"date\">" . gmtime($create) . " -> " . ( $expire ? gmtime($expire) : 'n/a' ) . "</span> | flags: $flag\n"
}
}
elsif ($record eq 'fpr') {
if (not $pubkey_fpr) {
$string .= "fingerprint: <a href=\"/pks/lookup?op=get&search=$userid\">$userid</a>\n";
$pubkey_fpr = 1;
}
else {
$string .= "<span class=\"fingerprint\">fingerprint: $userid</span>\n";
}
}
elsif ($record eq 'uid') {
my $safe = $userid;
$safe =~ s/>/&gt;/g if $safe;
$safe =~ s/</&lt;/g if $safe;
$string .= "\n<strong>$safe</strong>\n" if $safe;
}
elsif ($record eq 'sig') {
$string .= "signature: $keyid | <span class=\"date\">" . gmtime($create) . " -> ". ( $expire ? gmtime($expire) : 'n/a' ) . "</span> | $signatureclass\n";
}
elsif ($record eq 'sub') {
$string .= "\nsub key: " . gmtime($create) . " -> " . ( $expire ? gmtime($expire) : 'n/a' ) . " | flags: $flag\n"
}
elsif ($record eq 'rev') {
$string .= "<span class=\"highlight\">revocation: $keyid </span>| <span class=\"date\">" . gmtime($create) . "</span> | $signatureclass \n";
}
}
%>
%== $string
</pre>
<%
}
#rev:::1:95660BB822BAC934:1388248001:::::[selfsig]::20x:
#pub:u:255:22:9904E01052985080:1730221618:1856365618::u:,4
#fpr:::::::::50B750CC829A462D016AD9679904E01052985080:
#uid:::::::::Jake Thoughts (jjakke) <jake@jjakke.com>:
#sig:::22:9904E01052985080:1730221618:::::[selfsig]::13x:
#sub:u:255:18:190FCA50206CE28A:1730221618:1856365618:::
#fpr:::::::::B99D79A47141D942494EBCF2190FCA50206CE28A:
#sig:::22:9904E01052985080:1730221618:::::[keybind]::18x:
%>

View file

@ -0,0 +1,13 @@
% layout 'default';
% title 'Send a public key';
<h1>Welcome to jjakke's keyserver!</h1>
<p><a href="/">index</a></p>
<h2>Send a public key (secretly)</h2>
<form method="POST" action="<%= $c->config->{secret_add} %>" target="_blank">
<label for="">Armored keytext:</label>
<textarea rows="20" cols="60" type="t" id="keytext" name="keytext" placeholder="----- BEGIN PGP PUBLIC KEY BLOCK -----" required></textarea>
<br>
<input type="submit" value="Send Key">
</form>

1
todo Normal file
View file

@ -0,0 +1 @@
check /pks/add for warnings

35
upload_key.pl Executable file
View file

@ -0,0 +1,35 @@
#!/usr/bin/env perl
use strict;
use warnings;
use 5.010;
use LWP::UserAgent;
use HTTP::Request::Common qw(POST);
# Check for command line arguments
if (@ARGV != 2) {
die "Usage: perl upload_key.pl <key_file.asc> <keyserver_url>\n";
}
my ($key_file, $keyserver_url) = @ARGV;
# Read the armored key file
open my $fh, '<', $key_file or die "Could not open '$key_file': $!";
my $key_text = do { local $/; <$fh> }; # Read the entire file
close $fh;
# Create a user agent
my $ua = LWP::UserAgent->new;
# Prepare the POST request
my $request = POST $keyserver_url, Content => [ keytext => $key_text ];
# Send the request
my $response = $ua->request($request);
# Check the response
if ($response->is_success) {
say "Key uploaded successfully: " . $response->decoded_content . "\n";
} else {
die "Failed to upload key: " . $response->status_line . "\n\n" . $response->decoded_content . "\n";
}