commit 3656213c0c52ae09cc7f57e8be31add0d743ba52 Author: jake Date: Mon Nov 11 04:54:24 2024 -0500 GIT THIS diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..b2a3945 --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +*.asc +keyserver.conf diff --git a/README.md b/README.md new file mode 100644 index 0000000..8641429 --- /dev/null +++ b/README.md @@ -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 +gpg --keyserver hkp://hostname --search-keys +gpg --keyserver hkp://hostname --recv-keys +``` + +### Web browser +http://hostname + diff --git a/keyserver b/keyserver new file mode 100755 index 0000000..8d80e9d --- /dev/null +++ b/keyserver @@ -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::::::: + 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:::: + 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) + # 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; diff --git a/keyserver.conf.example b/keyserver.conf.example new file mode 100644 index 0000000..adceba5 --- /dev/null +++ b/keyserver.conf.example @@ -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) +}; diff --git a/templates/index.html.ep b/templates/index.html.ep new file mode 100644 index 0000000..d8dc3fd --- /dev/null +++ b/templates/index.html.ep @@ -0,0 +1,21 @@ +% layout 'default'; +% title 'Welcome'; +

Welcome to jjakke's keyserver!

+

Request a Public Key, using fingerprint

+ +
+ + +
+ +
+ +

Search for keys

+
+ + + +
+ +
+ diff --git a/templates/layouts/default.html.ep b/templates/layouts/default.html.ep new file mode 100644 index 0000000..96a7d21 --- /dev/null +++ b/templates/layouts/default.html.ep @@ -0,0 +1,106 @@ + + + + <%= title %> + + + +% if ($c->config->{servermessage}) { +%== "

" . $c->config->{servermessage} ."

" +% } + <%= content %> +

Operations performed since uptime: <%= $c->pg->db->select('this_service', ['operations'])->hash->{operations} %>

+ + diff --git a/templates/pkslookup.html.ep b/templates/pkslookup.html.ep new file mode 100644 index 0000000..10cb2c7 --- /dev/null +++ b/templates/pkslookup.html.ep @@ -0,0 +1,71 @@ +% layout 'default'; +% title 'Lookup'; +

Welcome to jjakke's keyserver!

+

index

+

Search results:

+% for my $item (@{ $c->stash('mydata') }) { +% my $pubkey_fpr = 0; +% my $string; +% my @row = split /\n/, $item; +
+<%
+	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: " . gmtime($create) . " -> " . ( $expire ? gmtime($expire) : 'n/a' ) . " | flags: $flag\n"
+			}
+		}
+		elsif ($record eq 'fpr') {
+			if (not $pubkey_fpr) {
+				$string .= "fingerprint: $userid\n";
+				$pubkey_fpr = 1;
+			} 
+			else {
+				$string .= "fingerprint: $userid\n";
+			}
+		}
+		elsif ($record eq 'uid') {
+			my $safe = $userid;
+			$safe =~ s/>/>/g if $safe;
+			$safe =~ s/$safe\n" if $safe;
+		}
+		elsif ($record eq 'sig') {
+			$string .= "signature: $keyid | " . gmtime($create) . " -> ". ( $expire ? gmtime($expire) : 'n/a' ) . " | $signatureclass\n";
+		}
+		elsif ($record eq 'sub') {
+			$string .= "\nsub key:    " . gmtime($create) . " -> " . ( $expire ? gmtime($expire) : 'n/a' ) . " | flags: $flag\n"
+		}
+		elsif ($record eq 'rev') {
+			$string .= "revocation: $keyid | " . gmtime($create) .  " | $signatureclass \n";
+		}
+	}
+%>
+%== $string
+
+<% +} +#rev:::1:95660BB822BAC934:1388248001:::::[selfsig]::20x: + +#pub:u:255:22:9904E01052985080:1730221618:1856365618::u:,4 +#fpr:::::::::50B750CC829A462D016AD9679904E01052985080: +#uid:::::::::Jake Thoughts (jjakke) : +#sig:::22:9904E01052985080:1730221618:::::[selfsig]::13x: +#sub:u:255:18:190FCA50206CE28A:1730221618:1856365618::: +#fpr:::::::::B99D79A47141D942494EBCF2190FCA50206CE28A: +#sig:::22:9904E01052985080:1730221618:::::[keybind]::18x: +%> diff --git a/templates/secretadd.html.ep b/templates/secretadd.html.ep new file mode 100644 index 0000000..20e5f90 --- /dev/null +++ b/templates/secretadd.html.ep @@ -0,0 +1,13 @@ +% layout 'default'; +% title 'Send a public key'; +

Welcome to jjakke's keyserver!

+

index

+

Send a public key (secretly)

+ +
+ + +
+ +
+ diff --git a/todo b/todo new file mode 100644 index 0000000..352cc22 --- /dev/null +++ b/todo @@ -0,0 +1 @@ +check /pks/add for warnings diff --git a/upload_key.pl b/upload_key.pl new file mode 100755 index 0000000..a7807c4 --- /dev/null +++ b/upload_key.pl @@ -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 \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"; +}