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;
+}