aboutsummaryrefslogtreecommitdiffstats
path: root/lacme-accountd
diff options
context:
space:
mode:
Diffstat (limited to 'lacme-accountd')
-rwxr-xr-xlacme-accountd71
1 files changed, 46 insertions, 25 deletions
diff --git a/lacme-accountd b/lacme-accountd
index 0f5deb2..a9f5469 100755
--- a/lacme-accountd
+++ b/lacme-accountd
@@ -23,7 +23,7 @@ use v5.14.2;
use strict;
use warnings;
-our $VERSION = '0.8.0';
+our $VERSION = '0.8.1';
my $PROTOCOL_VERSION = 1;
my $NAME = 'lacme-accountd';
@@ -64,18 +64,21 @@ sub usage(;$$) {
usage(1) unless GetOptions(\%OPTS, qw/config=s privkey=s socket=s stdio quiet|q debug help|h/);
usage(0) if $OPTS{help};
-my $LOG;
+my ($LOG, $LOGLEVEL);
+my ($LOG_INFO, $LOG_VERBOSE, $LOG_DEBUG) = (0,1,2);
sub logmsg($@) {
- my $lvl = shift // "all";
- if (defined $LOG) {
+ my $lvl = shift;
+ if (defined $LOG and ($lvl <= $LOGLEVEL or $lvl <= $LOG_VERBOSE)) {
+ # --quiet flag hides verbose-level messages from the standard
+ # error but we add them to the logfile nonetheless
my $now = localtime;
$LOG->printflush("[", $now, "] ", @_, "\n") or warn "print: $!";
}
- unless (($lvl eq "debug" and !$OPTS{debug}) or ($lvl eq "noquiet" and $OPTS{quiet})) {
+ if ($lvl <= $LOGLEVEL) {
print STDERR @_, "\n" or warn "print: $!";
}
}
-sub info(@) { logmsg(all => @_); }
+sub info(@) { logmsg($LOG_INFO => @_); }
sub error(@) {
my @msg = ("Error: ", @_);
info(@msg);
@@ -83,7 +86,8 @@ sub error(@) {
}
sub panic(@) {
my @loc = caller;
- my @msg = (@_, " at line $loc[2] in $loc[1]");
+ my @msg = ("PANIC at line $loc[2] in $loc[1]");
+ push @msg, ": ", @_ if @_;
info(@msg);
exit 255;
}
@@ -133,7 +137,7 @@ do {
print STDERR "Ignoring missing configuration file at default location $conffile\n" if $OPTS{debug};
}
- $OPTS{quiet} = 0 if $OPTS{debug};
+ $LOGLEVEL = $OPTS{debug} ? $LOG_DEBUG : $OPTS{quiet} ? $LOG_INFO : $LOG_VERBOSE;
error("'privkey' is not specified") unless defined $OPTS{privkey};
};
@@ -211,9 +215,9 @@ unless (defined $OPTS{stdio}) {
my @stat = stat($dirname) or error("stat($dirname): $!");
error("Insecure permissions on $dirname") if ($stat[2] & 0022) != 0;
- my $umask = umask(0177) // panic("umask: $!");
+ my $umask = umask(0177) // panic();
- logmsg(noquiet => "Starting lacme Account Key Manager at $sockname");
+ logmsg($LOG_VERBOSE => "Starting lacme Account Key Manager at $sockname");
socket(my $sock, PF_UNIX, SOCK_STREAM, 0) or panic("socket: $!");
my $sockaddr = Socket::sockaddr_un($sockname) // panic();
bind($sock, $sockaddr) or panic("bind: $!");
@@ -221,7 +225,7 @@ unless (defined $OPTS{stdio}) {
($SOCKNAME, $S) = ($sockname, $sock);
listen($S, 1) or panic("listen: $!");
- umask($umask) // panic("umask: $!");
+ umask($umask) // panic();
};
@@ -234,26 +238,43 @@ sub conn($$$) {
$out->printflush( "$PROTOCOL_VERSION OK ", $EXTRA_GREETING_STR, "\r\n",
$JWK_STR, "\r\n" ) or warn "print: $!";
- # sign whatever comes in
while (defined (my $data = $in->getline())) {
$data =~ s/\r\n\z// or panic();
+ # validate JWS Signing Input from RFC 7515:
+ # ASCII(BASE64URL(UTF8(JWS Protected Header)) || '.' || BASE64URL(JWS Payload))
my ($header, $payload) = split(/\./, $data, 2);
- unless (defined $header and $header =~ /\A[A-Za-z0-9\-_]+\z/) {
- info("[$id] >>> Error: Refusing to sign request: Malformed protected header");
+ if (defined $header and $header =~ /\A[A-Za-z0-9\-_]+\z/) {
+ $header = decode_base64url($header);
+ } else {
+ info("[$id] NOSIGN [malformed JWS Protected Header]");
last;
}
- unless (defined $payload and $payload =~ /\A[A-Za-z0-9\-_]*\z/) {
- # POST-as-GET yields an empty payload
- info("[$id] >>> Error: Refusing to sign request: Malformed payload");
+ if (defined $payload and $payload =~ /\A[A-Za-z0-9\-_]*\z/) {
+ # empty payloads are valid, and used for POST-as-GET (RFC 8555 sec. 6.3)
+ $payload = decode_base64url($payload);
+ } else {
+ info("[$id] NOSIGN [malformed JWS Payload]");
last;
}
- logmsg(noquiet => "[$id] >>> OK signing request: ",
- "header=base64url(", decode_base64url($header), "); ",
- "playload=base64url(", decode_base64url($payload), ")");
+ my $req = "header=base64url($header) playload=base64url($payload)";
+
+ eval { $header = JSON::->new->decode($header); };
+ if ($@ or # couldn't decode (parse error)
+ # RFC 7515: not a JSON object
+ !defined($header) or ref($header) ne "HASH" or
+ # RFC 8555 sec. 6.2: the protected Header MUST include all these fields
+ grep !defined, @$header{qw/alg nonce url/} or
+ # RFC 8555 sec. 6.2: the protected header MUST include any of these fields
+ !grep defined, @$header{qw/jwk kid/}) {
+ info("[$id] NOSIGN [invalid JWS Protected Header] ", $req);
+ last;
+ }
- my $sig = $SIGN->($data);
+ my $sig = eval { $SIGN->($data) };
+ panic($@) if $@ or !defined $sig;
+ logmsg($LOG_VERBOSE => "[$id] SIGNED ", $req);
$out->printflush( encode_base64url($sig), "\r\n" ) or warn "print: $!";
}
}
@@ -267,9 +288,9 @@ if (defined $OPTS{stdio}) {
next if $! == EINTR; # try again if accept(2) was interrupted by a signal
panic("accept: $!");
};
- logmsg(noquiet => "[$count] >>> Accepted new connection");
+ logmsg($LOG_VERBOSE => "[$count] Accepted new connection");
conn($conn, $conn, $count);
- logmsg(noquiet => "[$count] >>> Connection terminated");
+ logmsg($LOG_VERBOSE => "[$count] Connection terminated");
$conn->close() or warn "close: $!";
}
}
@@ -279,11 +300,11 @@ if (defined $OPTS{stdio}) {
#
END {
if (defined $SOCKNAME and -S $SOCKNAME) {
- logmsg(debug => "Unlinking $SOCKNAME");
+ logmsg($LOG_DEBUG => "Unlinking $SOCKNAME");
unlink $SOCKNAME or info("Error: unlink($SOCKNAME): $!");
}
if (defined $S) {
- logmsg(noquiet => "Shutting down and closing lacme Account Key Manager");
+ logmsg($LOG_VERBOSE => "Shutting down and closing lacme Account Key Manager");
shutdown($S, SHUT_RDWR) or info("Error: shutdown: $!");
close $S or info("Error: close: $!");
}