diff options
Diffstat (limited to 'lacme-accountd')
-rwxr-xr-x | lacme-accountd | 71 |
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: $!"); } |