diff --git a/pam_goatherd.c b/pam_goatherd.c new file mode 100644 index 0000000000000000000000000000000000000000..2f472b92091004edfb8c829d1c179586420abc9f --- /dev/null +++ b/pam_goatherd.c @@ -0,0 +1,299 @@ +#include <gnutls/gnutls.h> +#include <gnutls/x509.h> +#include <sys/socket.h> +#include <sys/types.h> +#include <netinet/in.h> +#include <netinet/tcp.h> +#include <netdb.h> + +#include <assert.h> +#include <stdio.h> +#include <stdlib.h> +#include <string.h> +#include <unistd.h> + +#define PAM_SM_AUTH +#include <security/pam_modules.h> + +struct cfg { + int debug; + const char *server; + const char *port; + const char *certs; +}; +static const char arg_server[] = "server="; +static const char arg_port[] = "port="; +static const char arg_certs[] = "certs="; + +#define dbgp(msg) do { \ + if (cfg.debug) fprintf(stderr, "[%s:%s:%i] %s\n", __FILE__, __FUNCTION__, __LINE__, msg); \ +} while(0) + +#define dbgp2(msg, msg1) do { \ + if (cfg.debug) fprintf(stderr, "[%s:%s:%i] %s %s\n", __FILE__, __FUNCTION__, __LINE__, msg, msg1); \ +} while(0) + + +static int verify_cb(gnutls_session_t session) { + int err; + struct cfg cfg = *(const struct cfg *)gnutls_session_get_ptr(session); + unsigned int status; + if ((err = gnutls_certificate_verify_peers2(session, &status)) < 0) + { + dbgp2("error in gnutls_certificate_verify_peers2:", gnutls_strerror(err)); + return -1; + } + + if (status != 0) + { + dbgp("cert verify failed"); + int type = gnutls_certificate_type_get(session); + gnutls_datum_t status_str; + if ((err = gnutls_certificate_verification_status_print(status, type, &status_str, 0)) < 0) + { + dbgp2("error in gnutls_certificate_verification_status_print:", gnutls_strerror(err)); + return -1; + } + + dbgp(status_str.data); + + return -1; + } + + return 0; +} + +static int check_hotp(struct cfg cfg, const char *user, const char *hotp) +{ + int err; + + // allocate all the gnutls stuff + dbgp("gnutls setup"); + gnutls_certificate_credentials_t creds; + gnutls_session_t session; + if ((err = gnutls_certificate_allocate_credentials(&creds)) < 0) + { + dbgp(gnutls_strerror(err)); + return PAM_AUTHINFO_UNAVAIL; + } + if ((err = gnutls_init(&session, GNUTLS_CLIENT)) < 0) + { + dbgp(gnutls_strerror(err)); + gnutls_certificate_free_credentials(creds); + return PAM_AUTHINFO_UNAVAIL; + } + gnutls_session_set_ptr(session, &cfg); + + // set up cert verification + dbgp("reading cert"); + if ((err = gnutls_certificate_set_x509_trust_file(creds, cfg.certs, GNUTLS_X509_FMT_PEM)) < 1) + { + dbgp2("error in gnutls_certificate_set_x509_trust_file:", gnutls_strerror(err)); + err = PAM_AUTHINFO_UNAVAIL; + goto out_gnutls; + } + gnutls_certificate_set_verify_function(creds, verify_cb); + + + // init gnutls session + const char *err_pos; + // XXX configurable prio + if ((err = gnutls_priority_set_direct(session, "NORMAL", &err_pos))) + { + dbgp2("error in gnutls_priority_set_direct:", gnutls_strerror(err)); + err = PAM_AUTHINFO_UNAVAIL; + goto out_gnutls; + } + if ((err = gnutls_credentials_set(session, GNUTLS_CRD_CERTIFICATE, creds))) + { + dbgp2("error in gnutls_credentials_set:", gnutls_strerror(err)); + err = PAM_AUTHINFO_UNAVAIL; + goto out_gnutls; + } + + // resolve name and connect + dbgp("connecting..."); + struct addrinfo hint = { .ai_socktype = SOCK_STREAM }; + struct addrinfo *addris_start; + if ((err = getaddrinfo(cfg.server, cfg.port, &hint, &addris_start)) != 0) + { + dbgp(gai_strerror(err)); + err = PAM_AUTHINFO_UNAVAIL; + goto out_gnutls; + } + + int sock; + struct addrinfo *addri; + for (addri = addris_start; addri != NULL; addri = addri->ai_next) + { + sock = socket(addri->ai_family, addri->ai_socktype, addri->ai_protocol); + if (sock < 0) { + continue; + } + + if (connect(sock, addri->ai_addr, addri->ai_addrlen) == 0) { + break; + } + + close(sock); + } + + freeaddrinfo(addris_start); + + if (!addri) + { + dbgp("Could not connect to server"); + err = PAM_AUTHINFO_UNAVAIL; + goto out_gnutls; + } + + + // we are connected, start TLS + dbgp("tls handshake"); + gnutls_transport_set_int(session, sock); + gnutls_handshake_set_timeout(session, GNUTLS_DEFAULT_HANDSHAKE_TIMEOUT); + + do { + err = gnutls_handshake(session); + } while (err < 0 && !gnutls_error_is_fatal(err)); + if (err < 0) + { + dbgp("handshake failed"); + err = PAM_AUTHINFO_UNAVAIL; + goto out; + } + + // talk + dbgp("authenticating"); + char ln = '\n'; + if ((err = gnutls_record_send(session, user, strlen(user))) < 0 + || (err = gnutls_record_send(session, &ln, 1)) < 0 + || (err = gnutls_record_send(session, hotp, strlen(hotp))) < 0 + || (err = gnutls_record_send(session, &ln, 1)) < 0) + { + dbgp2("error in send:", gnutls_strerror(err)); + err = PAM_AUTHINFO_UNAVAIL; + goto bye; + } + + char buf[5]; + if ((err = gnutls_record_recv(session, &buf, sizeof(buf) - 1)) < 0) + { + dbgp2("error in send", gnutls_strerror(err)); + err = PAM_AUTHINFO_UNAVAIL; + goto bye; + } + + // auth succeeded? + if (!strncmp(buf, "OK", 2)) { + dbgp("OK"); + err = PAM_SUCCESS; + } else if (!strncmp(buf, "FAIL", 4)) { + dbgp("FAIL"); + err = PAM_AUTH_ERR; + } else { + dbgp("Unexpected response"); + err = PAM_AUTHINFO_UNAVAIL; + } + +bye: + gnutls_bye(session, GNUTLS_SHUT_RDWR); + +out: + close(sock); + +out_gnutls: + gnutls_deinit(session); + gnutls_certificate_free_credentials(creds); + + return err; +} + + +PAM_EXTERN int +pam_sm_authenticate(pam_handle_t *pamh, int flags, int argc, const char **argv) +{ + int err; + struct cfg cfg = { 0 }; + + for (int i = 0; i < argc; i++) + { + if (!strcmp(argv[i], "debug")) + { + cfg.debug = 1; + } + else if (!strncmp(argv[i], arg_server, strlen(arg_server))) + { + cfg.server = &argv[i][strlen(arg_server)]; + } + else if (!strncmp(argv[i], arg_port, strlen(arg_port))) + { + cfg.port = &argv[i][strlen(arg_port)]; + } + else if (!strncmp(argv[i], arg_certs, strlen(arg_certs))) + { + cfg.certs = &argv[i][strlen(arg_certs)]; + } + } + + if (!cfg.server) + { + dbgp("Fatal: No server specified"); + return PAM_AUTHINFO_UNAVAIL; + } + if (!cfg.port) + { + dbgp("Fatal: No port specified"); + return PAM_AUTHINFO_UNAVAIL; + } + if (!cfg.certs) + { + dbgp("Fatal: No certs specified"); + return PAM_AUTHINFO_UNAVAIL; + } + + dbgp("retrieving user"); + const char *user; + if ((err = pam_get_user(pamh, &user, NULL)) != PAM_SUCCESS) + { + dbgp(pam_strerror(pamh, err)); + return err; + } + dbgp(user); + + dbgp("retrieving conv"); + const struct pam_conv *conv; + if ((err = pam_get_item(pamh, PAM_CONV, (const void **)&conv)) != PAM_SUCCESS) + { + dbgp(pam_strerror(pamh, err)); + return err; + } + + struct pam_message msg = { + .msg_style = PAM_PROMPT_ECHO_OFF, + .msg = "HOTP: " + }; + const struct pam_message *msgs = &msg; + + dbgp("asking for password"); + struct pam_response *resp; + if ((err = conv->conv(1, &msgs, &resp, conv->appdata_ptr)) != PAM_SUCCESS) + { + dbgp(pam_strerror(pamh, err)); + return err; + } + + char *hotp = resp[0].resp; + free(resp); + + err = check_hotp(cfg, user, hotp); + + free(hotp); + return err; +} + +PAM_EXTERN int +pam_sm_setcred(pam_handle_t * pamh, int flags, int argc, const char **argv) +{ + return PAM_SUCCESS; +}