GIT THIS
This commit is contained in:
commit
3656213c0c
10 changed files with 853 additions and 0 deletions
2
.gitignore
vendored
Normal file
2
.gitignore
vendored
Normal file
|
@ -0,0 +1,2 @@
|
||||||
|
*.asc
|
||||||
|
keyserver.conf
|
65
README.md
Normal file
65
README.md
Normal 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
523
keyserver
Executable 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
16
keyserver.conf.example
Normal 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
21
templates/index.html.ep
Normal 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>
|
||||||
|
|
106
templates/layouts/default.html.ep
Normal file
106
templates/layouts/default.html.ep
Normal 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>
|
71
templates/pkslookup.html.ep
Normal file
71
templates/pkslookup.html.ep
Normal 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/>/>/g if $safe;
|
||||||
|
$safe =~ s/</</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:
|
||||||
|
%>
|
13
templates/secretadd.html.ep
Normal file
13
templates/secretadd.html.ep
Normal 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
1
todo
Normal file
|
@ -0,0 +1 @@
|
||||||
|
check /pks/add for warnings
|
35
upload_key.pl
Executable file
35
upload_key.pl
Executable 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";
|
||||||
|
}
|
Loading…
Reference in a new issue