#! /usr/bin/perl
# This file is part of pam-modules.
# Copyright (C) 2014-2015, 2018 Sergey Poznyakoff
#
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation; either version 3, or (at your option)
# any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License along
# with this program.  If not, see <http://www.gnu.org/licenses/>.

use strict;
use Net::LDAP;

=head1 NAME

ldappubkey - get user public ssh keys from the LDAP database

=head1 SYNOPSIS

B<ldappubkey> I<LOGIN>

=head1 DESCRIPTION

Produces on the standard output public ssh keys for the user I<LOGIN>, each
on a separate line.  The program is designed for use with B<sshd>(8) version
6.2p1 or higher.  Public keys are obtained from a LDAP database.  The
configuration is looked up in the following files: B</etc/ldap.conf>,
B</etc/ldap/ldap.conf> and B</etc/openldap/ldap.conf>.  These files are
tried in this order and the first one of them that exists is read.
If the environment variable B<LDAP_CONF> is defined, the file it points
to is tried first.    

The following configuration statements are used (all keywords are
case-insensitive):

=over 4

=item B<uri> B<ldap[si]://>[I<name>[:I<port>]] ...>

Specifies the URI of the LDAP server (or servers) to connect to.  The default
is B<ldap://127.0.0.1>.    
    
=item B<base> I<DN>

Specifies the default base DN to use when performing ldap operations.
The base must be specified as a Distinguished Name in LDAP format.

=item B<binddn> I<DN>

Specifies the default bind DN to use.

=item B<bindpw> I<PASS>

Specifies the password to use with B<binddn>.    

=item B<uid> I<ATTR>

Name of the attribute to use instead of B<uid>.  The LDAP record is searched
using the filter B<(&(objectClass=posixAccount)(I<ATTR>=I<LOGIN>))>.

=item B<ssl start_tls>

Use TLS

=item B<tls_cacert> I<FILE>

Specifies the file that contains certificates for all of the Certificate
Authorities the client will recognize. 

=item B<tls_cacertdir> I<DIR>

Path of a directory that contains Certificate Authority certificates in
separate individual files.  The B<tls_cacert> statement takes precedence
over B<tls_cacertdir>.

=item B<tls_cert> I<FILE>
    
Specifies the file that contains the client certificate.
    
=item B<tls_key> I<FILE>

Specifies the file that contains the private key that matches the
certificate stored in the B<tls_cert> file.    

=item B<tls_cipher_suite> I<SPEC>

Specifies acceptable cipher suite and preference order.    

=item B<tls_reqcert> I<LEVEL>

Specifies what checks to perform on server certificates in a TLS session.
I<LEVEL> is one of B<never>, B<allow>, B<try>, B<demand> or B<hard>.
    
=item B<publickeyattribute> I<ATTR>

Name of the attribute which holds the public key.  Default is B<grayPublicKey>.
    
=back

=head1 OPTIONS

=over 4

=item B<-h>

Show program usage.

=item B<--help>

Show detailed help page.    
    
=back    
    
=head1 ENVIRONMENT

=over 4

=item B<LDAP_CONF>

If defined, names the alternative configuration file to read.

=item B<GITCONFIG_TEMPLATE>

Name of the template file to use instead of the default B<.gitconfig>.
    
=back    

=head1 SEE ALSO

B<sshd>(8), B<sshd_config>(5), B<ldap.conf>(5).

=head1 BUGS

LDAP filter string is hardcoded.    
    
=head1 AUTHOR

Sergey Poznyakoff <gray@gnu.org>    
    
=cut

# ###################################
# Configuration file handling
# ###################################

my %config = ('uri' => 'ldap://127.0.0.1', 'uid' => 'uid',
	      'publickeyattribute' => 'grayPublicKey');

sub read_config_file($) {
    my $config_file = shift;
    my $file;
    my $line = 0;

    open($file, "<", $config_file) or die("cannot open $config_file: $!");
    while (<$file>) {
	++$line;
	chomp;
	s/^\s+//;
	s/\s+$//;
	        s/#.*//;
	next if ($_ eq "");
	my @kwp = split(/\s*\s+\s*/, $_, 2);
	$config{lc($kwp[0])} = $kwp[1];
    }
    close($file);
}

sub assert {
    my $mesg = shift;
    my $action = shift;
    die("An error occurred $action: ".$mesg->error) if ($mesg->code);
    return $mesg;
}

# ###################################
# MAIN
# ###################################

die "bad number of arguments; try perldoc $0 for more info"
    unless ($#ARGV == 0);

## Read configuration
my @config_files = ("/etc/ldap.conf", "/etc/ldap/ldap.conf",
		    "/etc/openldap/ldap.conf");
unshift @config_files, $ENV{LDAP_CONF} if defined($ENV{LDAP_CONF});

foreach my $file (@config_files) {
    if (-e $file) {
	read_config_file($file);
	last;
    }
}

my $ldap = Net::LDAP->new($config{'uri'})
    or die("Unable to connect to LDAP server $config{'uri'}: $!");

if ($config{ssl} eq 'start_tls') {
    my %args;
    
    $args{capath} = $config{tls_cacertdir}
        if (defined($config{tls_cacertdir}));
    $args{cafile} = $config{tls_cacert}
        if (defined($config{tls_cacert}));
    if ($config{tls_reqcert} eq 'none') {
	$args{verify} = 'never';
    } elsif ($config{tls_reqcert} eq 'allow') {
	$args{verify} = 'optional';
    } elsif ($config{tls_reqcert} eq 'demand'
	     or $config{tls_reqcert} eq 'hard') {
	$args{verify} = 'require';
    } elsif ($config{tls_reqcert} eq 'try') {
	$args{verify} = 'optional'; # FIXME: That's wrong
    }
    $args{clientcert} = $config{tls_cert}
        if (defined($config{tls_cert}));
    $args{clientkey} = $config{tls_key}
        if (defined($config{tls_key}));
    $args{ciphers} = $config{tls_cipher_suite}
        if (defined($config{tls_cipher_suite}));
    
    assert($ldap->start_tls, "TLS negotiation"); 
}

my @bindargs = ();
if (defined($config{'binddn'})) {
    push(@bindargs, $config{'binddn'});
    push(@bindargs, password => $config{'bindpw'})
	if defined($config{'bindpw'});
}
assert($ldap->bind(@bindargs), "binding to the server");

my $attr = $config{'publickeyattribute'};
my $filter = "(&(objectClass=posixAccount)($config{'uid'}=$ARGV[0]))";

my $res = assert($ldap->search(base => $config{'base'},
			       filter => $filter,
			       attr => [ $attr ] ),
		 "searching for $filter in $config{'base'}");

foreach my $entry ($res->entry(0)) {
    my $keyref = $entry->get_value($attr, asref => 1);
    for (@{$keyref}) {
	print "$_\n";
    }
}

# END


