mandos-1.6.0/0000775000175000017500000000000011767455003013255 5ustar teddyteddy00000000000000mandos-1.6.0/plugins.d/0000775000175000017500000000000011767455003015160 5ustar teddyteddy00000000000000mandos-1.6.0/plugins.d/mandos-client.c0000664000175000017500000017570311767455003020076 0ustar teddyteddy00000000000000/* -*- coding: utf-8 -*- */ /* * Mandos-client - get and decrypt data from a Mandos server * * This program is partly derived from an example program for an Avahi * service browser, downloaded from * . This * includes the following functions: "resolve_callback", * "browse_callback", and parts of "main". * * Everything else is * Copyright © 2008-2012 Teddy Hogeborn * Copyright © 2008-2012 Björn Påhlsson * * 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 of the * License, 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 * . * * Contact the authors at . */ /* Needed by GPGME, specifically gpgme_data_seek() */ #ifndef _LARGEFILE_SOURCE #define _LARGEFILE_SOURCE #endif #ifndef _FILE_OFFSET_BITS #define _FILE_OFFSET_BITS 64 #endif #define _GNU_SOURCE /* TEMP_FAILURE_RETRY(), asprintf() */ #include /* fprintf(), stderr, fwrite(), stdout, ferror(), remove() */ #include /* uint16_t, uint32_t, intptr_t */ #include /* NULL, size_t, ssize_t */ #include /* free(), EXIT_SUCCESS, srand(), strtof(), abort() */ #include /* bool, false, true */ #include /* memset(), strcmp(), strlen(), strerror(), asprintf(), strcpy() */ #include /* ioctl */ #include /* socket(), inet_pton(), sockaddr, sockaddr_in6, PF_INET6, SOCK_STREAM, uid_t, gid_t, open(), opendir(), DIR */ #include /* open(), S_ISREG */ #include /* socket(), struct sockaddr_in6, inet_pton(), connect() */ #include /* open() */ #include /* opendir(), struct dirent, readdir() */ #include /* PRIu16, PRIdMAX, intmax_t, strtoimax() */ #include /* perror(), errno, program_invocation_short_name */ #include /* nanosleep(), time(), sleep() */ #include /* ioctl, ifreq, SIOCGIFFLAGS, IFF_UP, SIOCSIFFLAGS, if_indextoname(), if_nametoindex(), IF_NAMESIZE */ #include /* IN6_IS_ADDR_LINKLOCAL, INET_ADDRSTRLEN, INET6_ADDRSTRLEN */ #include /* close(), SEEK_SET, off_t, write(), getuid(), getgid(), seteuid(), setgid(), pause(), _exit() */ #include /* inet_pton(), htons, inet_ntop() */ #include /* not, or, and */ #include /* struct argp_option, error_t, struct argp_state, struct argp, argp_parse(), ARGP_KEY_ARG, ARGP_KEY_END, ARGP_ERR_UNKNOWN */ #include /* sigemptyset(), sigaddset(), sigaction(), SIGTERM, sig_atomic_t, raise() */ #include /* EX_OSERR, EX_USAGE, EX_UNAVAILABLE, EX_NOHOST, EX_IOERR, EX_PROTOCOL */ #include /* waitpid(), WIFEXITED(), WEXITSTATUS(), WTERMSIG() */ #include /* setgroups() */ #include /* argz_add_sep(), argz_next(), argz_delete(), argz_append(), argz_stringify(), argz_add(), argz_count() */ #ifdef __linux__ #include /* klogctl() */ #endif /* __linux__ */ /* Avahi */ /* All Avahi types, constants and functions Avahi*, avahi_*, AVAHI_* */ #include #include #include #include #include #include /* GnuTLS */ #include /* All GnuTLS types, constants and functions: gnutls_* init_gnutls_session(), GNUTLS_* */ #include /* gnutls_certificate_set_openpgp_key_file(), GNUTLS_OPENPGP_FMT_BASE64 */ /* GPGME */ #include /* All GPGME types, constants and functions: gpgme_* GPGME_PROTOCOL_OpenPGP, GPG_ERR_NO_* */ #define BUFFER_SIZE 256 #define PATHDIR "/conf/conf.d/mandos" #define SECKEY "seckey.txt" #define PUBKEY "pubkey.txt" #define HOOKDIR "/lib/mandos/network-hooks.d" bool debug = false; static const char mandos_protocol_version[] = "1"; const char *argp_program_version = "mandos-client " VERSION; const char *argp_program_bug_address = ""; static const char sys_class_net[] = "/sys/class/net"; char *connect_to = NULL; const char *hookdir = HOOKDIR; uid_t uid = 65534; gid_t gid = 65534; /* Doubly linked list that need to be circularly linked when used */ typedef struct server{ const char *ip; in_port_t port; AvahiIfIndex if_index; int af; struct timespec last_seen; struct server *next; struct server *prev; } server; /* Used for passing in values through the Avahi callback functions */ typedef struct { AvahiServer *server; gnutls_certificate_credentials_t cred; unsigned int dh_bits; gnutls_dh_params_t dh_params; const char *priority; gpgme_ctx_t ctx; server *current_server; char *interfaces; size_t interfaces_size; } mandos_context; /* global so signal handler can reach it*/ AvahiSimplePoll *simple_poll; sig_atomic_t quit_now = 0; int signal_received = 0; /* Function to use when printing errors */ void perror_plus(const char *print_text){ int e = errno; fprintf(stderr, "Mandos plugin %s: ", program_invocation_short_name); errno = e; perror(print_text); } __attribute__((format (gnu_printf, 2, 3))) int fprintf_plus(FILE *stream, const char *format, ...){ va_list ap; va_start (ap, format); TEMP_FAILURE_RETRY(fprintf(stream, "Mandos plugin %s: ", program_invocation_short_name)); return TEMP_FAILURE_RETRY(vfprintf(stream, format, ap)); } /* * Make additional room in "buffer" for at least BUFFER_SIZE more * bytes. "buffer_capacity" is how much is currently allocated, * "buffer_length" is how much is already used. */ size_t incbuffer(char **buffer, size_t buffer_length, size_t buffer_capacity){ if(buffer_length + BUFFER_SIZE > buffer_capacity){ *buffer = realloc(*buffer, buffer_capacity + BUFFER_SIZE); if(buffer == NULL){ return 0; } buffer_capacity += BUFFER_SIZE; } return buffer_capacity; } /* Add server to set of servers to retry periodically */ bool add_server(const char *ip, in_port_t port, AvahiIfIndex if_index, int af, server **current_server){ int ret; server *new_server = malloc(sizeof(server)); if(new_server == NULL){ perror_plus("malloc"); return false; } *new_server = (server){ .ip = strdup(ip), .port = port, .if_index = if_index, .af = af }; if(new_server->ip == NULL){ perror_plus("strdup"); return false; } /* Special case of first server */ if(*current_server == NULL){ new_server->next = new_server; new_server->prev = new_server; *current_server = new_server; /* Place the new server last in the list */ } else { new_server->next = *current_server; new_server->prev = (*current_server)->prev; new_server->prev->next = new_server; (*current_server)->prev = new_server; } ret = clock_gettime(CLOCK_MONOTONIC, &(*current_server)->last_seen); if(ret == -1){ perror_plus("clock_gettime"); return false; } return true; } /* * Initialize GPGME. */ static bool init_gpgme(const char *seckey, const char *pubkey, const char *tempdir, mandos_context *mc){ gpgme_error_t rc; gpgme_engine_info_t engine_info; /* * Helper function to insert pub and seckey to the engine keyring. */ bool import_key(const char *filename){ int ret; int fd; gpgme_data_t pgp_data; fd = (int)TEMP_FAILURE_RETRY(open(filename, O_RDONLY)); if(fd == -1){ perror_plus("open"); return false; } rc = gpgme_data_new_from_fd(&pgp_data, fd); if(rc != GPG_ERR_NO_ERROR){ fprintf_plus(stderr, "bad gpgme_data_new_from_fd: %s: %s\n", gpgme_strsource(rc), gpgme_strerror(rc)); return false; } rc = gpgme_op_import(mc->ctx, pgp_data); if(rc != GPG_ERR_NO_ERROR){ fprintf_plus(stderr, "bad gpgme_op_import: %s: %s\n", gpgme_strsource(rc), gpgme_strerror(rc)); return false; } ret = (int)TEMP_FAILURE_RETRY(close(fd)); if(ret == -1){ perror_plus("close"); } gpgme_data_release(pgp_data); return true; } if(debug){ fprintf_plus(stderr, "Initializing GPGME\n"); } /* Init GPGME */ gpgme_check_version(NULL); rc = gpgme_engine_check_version(GPGME_PROTOCOL_OpenPGP); if(rc != GPG_ERR_NO_ERROR){ fprintf_plus(stderr, "bad gpgme_engine_check_version: %s: %s\n", gpgme_strsource(rc), gpgme_strerror(rc)); return false; } /* Set GPGME home directory for the OpenPGP engine only */ rc = gpgme_get_engine_info(&engine_info); if(rc != GPG_ERR_NO_ERROR){ fprintf_plus(stderr, "bad gpgme_get_engine_info: %s: %s\n", gpgme_strsource(rc), gpgme_strerror(rc)); return false; } while(engine_info != NULL){ if(engine_info->protocol == GPGME_PROTOCOL_OpenPGP){ gpgme_set_engine_info(GPGME_PROTOCOL_OpenPGP, engine_info->file_name, tempdir); break; } engine_info = engine_info->next; } if(engine_info == NULL){ fprintf_plus(stderr, "Could not set GPGME home dir to %s\n", tempdir); return false; } /* Create new GPGME "context" */ rc = gpgme_new(&(mc->ctx)); if(rc != GPG_ERR_NO_ERROR){ fprintf_plus(stderr, "Mandos plugin mandos-client: " "bad gpgme_new: %s: %s\n", gpgme_strsource(rc), gpgme_strerror(rc)); return false; } if(not import_key(pubkey) or not import_key(seckey)){ return false; } return true; } /* * Decrypt OpenPGP data. * Returns -1 on error */ static ssize_t pgp_packet_decrypt(const char *cryptotext, size_t crypto_size, char **plaintext, mandos_context *mc){ gpgme_data_t dh_crypto, dh_plain; gpgme_error_t rc; ssize_t ret; size_t plaintext_capacity = 0; ssize_t plaintext_length = 0; if(debug){ fprintf_plus(stderr, "Trying to decrypt OpenPGP data\n"); } /* Create new GPGME data buffer from memory cryptotext */ rc = gpgme_data_new_from_mem(&dh_crypto, cryptotext, crypto_size, 0); if(rc != GPG_ERR_NO_ERROR){ fprintf_plus(stderr, "bad gpgme_data_new_from_mem: %s: %s\n", gpgme_strsource(rc), gpgme_strerror(rc)); return -1; } /* Create new empty GPGME data buffer for the plaintext */ rc = gpgme_data_new(&dh_plain); if(rc != GPG_ERR_NO_ERROR){ fprintf_plus(stderr, "Mandos plugin mandos-client: " "bad gpgme_data_new: %s: %s\n", gpgme_strsource(rc), gpgme_strerror(rc)); gpgme_data_release(dh_crypto); return -1; } /* Decrypt data from the cryptotext data buffer to the plaintext data buffer */ rc = gpgme_op_decrypt(mc->ctx, dh_crypto, dh_plain); if(rc != GPG_ERR_NO_ERROR){ fprintf_plus(stderr, "bad gpgme_op_decrypt: %s: %s\n", gpgme_strsource(rc), gpgme_strerror(rc)); plaintext_length = -1; if(debug){ gpgme_decrypt_result_t result; result = gpgme_op_decrypt_result(mc->ctx); if(result == NULL){ fprintf_plus(stderr, "gpgme_op_decrypt_result failed\n"); } else { fprintf_plus(stderr, "Unsupported algorithm: %s\n", result->unsupported_algorithm); fprintf_plus(stderr, "Wrong key usage: %u\n", result->wrong_key_usage); if(result->file_name != NULL){ fprintf_plus(stderr, "File name: %s\n", result->file_name); } gpgme_recipient_t recipient; recipient = result->recipients; while(recipient != NULL){ fprintf_plus(stderr, "Public key algorithm: %s\n", gpgme_pubkey_algo_name (recipient->pubkey_algo)); fprintf_plus(stderr, "Key ID: %s\n", recipient->keyid); fprintf_plus(stderr, "Secret key available: %s\n", recipient->status == GPG_ERR_NO_SECKEY ? "No" : "Yes"); recipient = recipient->next; } } } goto decrypt_end; } if(debug){ fprintf_plus(stderr, "Decryption of OpenPGP data succeeded\n"); } /* Seek back to the beginning of the GPGME plaintext data buffer */ if(gpgme_data_seek(dh_plain, (off_t)0, SEEK_SET) == -1){ perror_plus("gpgme_data_seek"); plaintext_length = -1; goto decrypt_end; } *plaintext = NULL; while(true){ plaintext_capacity = incbuffer(plaintext, (size_t)plaintext_length, plaintext_capacity); if(plaintext_capacity == 0){ perror_plus("incbuffer"); plaintext_length = -1; goto decrypt_end; } ret = gpgme_data_read(dh_plain, *plaintext + plaintext_length, BUFFER_SIZE); /* Print the data, if any */ if(ret == 0){ /* EOF */ break; } if(ret < 0){ perror_plus("gpgme_data_read"); plaintext_length = -1; goto decrypt_end; } plaintext_length += ret; } if(debug){ fprintf_plus(stderr, "Decrypted password is: "); for(ssize_t i = 0; i < plaintext_length; i++){ fprintf(stderr, "%02hhX ", (*plaintext)[i]); } fprintf(stderr, "\n"); } decrypt_end: /* Delete the GPGME cryptotext data buffer */ gpgme_data_release(dh_crypto); /* Delete the GPGME plaintext data buffer */ gpgme_data_release(dh_plain); return plaintext_length; } static const char * safer_gnutls_strerror(int value){ const char *ret = gnutls_strerror(value); if(ret == NULL) ret = "(unknown)"; return ret; } /* GnuTLS log function callback */ static void debuggnutls(__attribute__((unused)) int level, const char* string){ fprintf_plus(stderr, "GnuTLS: %s", string); } static int init_gnutls_global(const char *pubkeyfilename, const char *seckeyfilename, mandos_context *mc){ int ret; if(debug){ fprintf_plus(stderr, "Initializing GnuTLS\n"); } ret = gnutls_global_init(); if(ret != GNUTLS_E_SUCCESS){ fprintf_plus(stderr, "GnuTLS global_init: %s\n", safer_gnutls_strerror(ret)); return -1; } if(debug){ /* "Use a log level over 10 to enable all debugging options." * - GnuTLS manual */ gnutls_global_set_log_level(11); gnutls_global_set_log_function(debuggnutls); } /* OpenPGP credentials */ ret = gnutls_certificate_allocate_credentials(&mc->cred); if(ret != GNUTLS_E_SUCCESS){ fprintf_plus(stderr, "GnuTLS memory error: %s\n", safer_gnutls_strerror(ret)); gnutls_global_deinit(); return -1; } if(debug){ fprintf_plus(stderr, "Attempting to use OpenPGP public key %s and" " secret key %s as GnuTLS credentials\n", pubkeyfilename, seckeyfilename); } ret = gnutls_certificate_set_openpgp_key_file (mc->cred, pubkeyfilename, seckeyfilename, GNUTLS_OPENPGP_FMT_BASE64); if(ret != GNUTLS_E_SUCCESS){ fprintf_plus(stderr, "Error[%d] while reading the OpenPGP key pair ('%s'," " '%s')\n", ret, pubkeyfilename, seckeyfilename); fprintf_plus(stderr, "The GnuTLS error is: %s\n", safer_gnutls_strerror(ret)); goto globalfail; } /* GnuTLS server initialization */ ret = gnutls_dh_params_init(&mc->dh_params); if(ret != GNUTLS_E_SUCCESS){ fprintf_plus(stderr, "Error in GnuTLS DH parameter" " initialization: %s\n", safer_gnutls_strerror(ret)); goto globalfail; } ret = gnutls_dh_params_generate2(mc->dh_params, mc->dh_bits); if(ret != GNUTLS_E_SUCCESS){ fprintf_plus(stderr, "Error in GnuTLS prime generation: %s\n", safer_gnutls_strerror(ret)); goto globalfail; } gnutls_certificate_set_dh_params(mc->cred, mc->dh_params); return 0; globalfail: gnutls_certificate_free_credentials(mc->cred); gnutls_global_deinit(); gnutls_dh_params_deinit(mc->dh_params); return -1; } static int init_gnutls_session(gnutls_session_t *session, mandos_context *mc){ int ret; /* GnuTLS session creation */ do { ret = gnutls_init(session, GNUTLS_SERVER); if(quit_now){ return -1; } } while(ret == GNUTLS_E_INTERRUPTED or ret == GNUTLS_E_AGAIN); if(ret != GNUTLS_E_SUCCESS){ fprintf_plus(stderr, "Error in GnuTLS session initialization: %s\n", safer_gnutls_strerror(ret)); } { const char *err; do { ret = gnutls_priority_set_direct(*session, mc->priority, &err); if(quit_now){ gnutls_deinit(*session); return -1; } } while(ret == GNUTLS_E_INTERRUPTED or ret == GNUTLS_E_AGAIN); if(ret != GNUTLS_E_SUCCESS){ fprintf_plus(stderr, "Syntax error at: %s\n", err); fprintf_plus(stderr, "GnuTLS error: %s\n", safer_gnutls_strerror(ret)); gnutls_deinit(*session); return -1; } } do { ret = gnutls_credentials_set(*session, GNUTLS_CRD_CERTIFICATE, mc->cred); if(quit_now){ gnutls_deinit(*session); return -1; } } while(ret == GNUTLS_E_INTERRUPTED or ret == GNUTLS_E_AGAIN); if(ret != GNUTLS_E_SUCCESS){ fprintf_plus(stderr, "Error setting GnuTLS credentials: %s\n", safer_gnutls_strerror(ret)); gnutls_deinit(*session); return -1; } /* ignore client certificate if any. */ gnutls_certificate_server_set_request(*session, GNUTLS_CERT_IGNORE); gnutls_dh_set_prime_bits(*session, mc->dh_bits); return 0; } /* Avahi log function callback */ static void empty_log(__attribute__((unused)) AvahiLogLevel level, __attribute__((unused)) const char *txt){} /* Called when a Mandos server is found */ static int start_mandos_communication(const char *ip, in_port_t port, AvahiIfIndex if_index, int af, mandos_context *mc){ int ret, tcp_sd = -1; ssize_t sret; union { struct sockaddr_in in; struct sockaddr_in6 in6; } to; char *buffer = NULL; char *decrypted_buffer = NULL; size_t buffer_length = 0; size_t buffer_capacity = 0; size_t written; int retval = -1; gnutls_session_t session; int pf; /* Protocol family */ errno = 0; if(quit_now){ errno = EINTR; return -1; } switch(af){ case AF_INET6: pf = PF_INET6; break; case AF_INET: pf = PF_INET; break; default: fprintf_plus(stderr, "Bad address family: %d\n", af); errno = EINVAL; return -1; } /* If the interface is specified and we have a list of interfaces */ if(if_index != AVAHI_IF_UNSPEC and mc->interfaces != NULL){ /* Check if the interface is one of the interfaces we are using */ bool match = false; { char *interface = NULL; while((interface=argz_next(mc->interfaces, mc->interfaces_size, interface))){ if(if_nametoindex(interface) == (unsigned int)if_index){ match = true; break; } } } if(not match){ /* This interface does not match any in the list, so we don't connect to the server */ if(debug){ char interface[IF_NAMESIZE]; if(if_indextoname((unsigned int)if_index, interface) == NULL){ perror_plus("if_indextoname"); } else { fprintf_plus(stderr, "Skipping server on non-used interface" " \"%s\"\n", if_indextoname((unsigned int)if_index, interface)); } } return -1; } } ret = init_gnutls_session(&session, mc); if(ret != 0){ return -1; } if(debug){ fprintf_plus(stderr, "Setting up a TCP connection to %s, port %" PRIuMAX "\n", ip, (uintmax_t)port); } tcp_sd = socket(pf, SOCK_STREAM, 0); if(tcp_sd < 0){ int e = errno; perror_plus("socket"); errno = e; goto mandos_end; } if(quit_now){ errno = EINTR; goto mandos_end; } memset(&to, 0, sizeof(to)); if(af == AF_INET6){ to.in6.sin6_family = (sa_family_t)af; ret = inet_pton(af, ip, &to.in6.sin6_addr); } else { /* IPv4 */ to.in.sin_family = (sa_family_t)af; ret = inet_pton(af, ip, &to.in.sin_addr); } if(ret < 0 ){ int e = errno; perror_plus("inet_pton"); errno = e; goto mandos_end; } if(ret == 0){ int e = errno; fprintf_plus(stderr, "Bad address: %s\n", ip); errno = e; goto mandos_end; } if(af == AF_INET6){ to.in6.sin6_port = htons(port); if(IN6_IS_ADDR_LINKLOCAL /* Spurious warnings from */ (&to.in6.sin6_addr)){ /* -Wstrict-aliasing=2 or lower and -Wunreachable-code*/ if(if_index == AVAHI_IF_UNSPEC){ fprintf_plus(stderr, "An IPv6 link-local address is" " incomplete without a network interface\n"); errno = EINVAL; goto mandos_end; } /* Set the network interface number as scope */ to.in6.sin6_scope_id = (uint32_t)if_index; } } else { to.in.sin_port = htons(port); /* Spurious warnings from -Wconversion and -Wunreachable-code */ } if(quit_now){ errno = EINTR; goto mandos_end; } if(debug){ if(af == AF_INET6 and if_index != AVAHI_IF_UNSPEC){ char interface[IF_NAMESIZE]; if(if_indextoname((unsigned int)if_index, interface) == NULL){ perror_plus("if_indextoname"); } else { fprintf_plus(stderr, "Connection to: %s%%%s, port %" PRIuMAX "\n", ip, interface, (uintmax_t)port); } } else { fprintf_plus(stderr, "Connection to: %s, port %" PRIuMAX "\n", ip, (uintmax_t)port); } char addrstr[(INET_ADDRSTRLEN > INET6_ADDRSTRLEN) ? INET_ADDRSTRLEN : INET6_ADDRSTRLEN] = ""; const char *pcret; if(af == AF_INET6){ pcret = inet_ntop(af, &(to.in6.sin6_addr), addrstr, sizeof(addrstr)); } else { pcret = inet_ntop(af, &(to.in.sin_addr), addrstr, sizeof(addrstr)); } if(pcret == NULL){ perror_plus("inet_ntop"); } else { if(strcmp(addrstr, ip) != 0){ fprintf_plus(stderr, "Canonical address form: %s\n", addrstr); } } } if(quit_now){ errno = EINTR; goto mandos_end; } if(af == AF_INET6){ ret = connect(tcp_sd, &to.in6, sizeof(to)); } else { ret = connect(tcp_sd, &to.in, sizeof(to)); /* IPv4 */ } if(ret < 0){ if ((errno != ECONNREFUSED and errno != ENETUNREACH) or debug){ int e = errno; perror_plus("connect"); errno = e; } goto mandos_end; } if(quit_now){ errno = EINTR; goto mandos_end; } const char *out = mandos_protocol_version; written = 0; while(true){ size_t out_size = strlen(out); ret = (int)TEMP_FAILURE_RETRY(write(tcp_sd, out + written, out_size - written)); if(ret == -1){ int e = errno; perror_plus("write"); errno = e; goto mandos_end; } written += (size_t)ret; if(written < out_size){ continue; } else { if(out == mandos_protocol_version){ written = 0; out = "\r\n"; } else { break; } } if(quit_now){ errno = EINTR; goto mandos_end; } } if(debug){ fprintf_plus(stderr, "Establishing TLS session with %s\n", ip); } if(quit_now){ errno = EINTR; goto mandos_end; } /* This casting via intptr_t is to eliminate warning about casting an int to a pointer type. This is exactly how the GnuTLS Guile function "set-session-transport-fd!" does it. */ gnutls_transport_set_ptr(session, (gnutls_transport_ptr_t)(intptr_t)tcp_sd); if(quit_now){ errno = EINTR; goto mandos_end; } do { ret = gnutls_handshake(session); if(quit_now){ errno = EINTR; goto mandos_end; } } while(ret == GNUTLS_E_AGAIN or ret == GNUTLS_E_INTERRUPTED); if(ret != GNUTLS_E_SUCCESS){ if(debug){ fprintf_plus(stderr, "*** GnuTLS Handshake failed ***\n"); gnutls_perror(ret); } errno = EPROTO; goto mandos_end; } /* Read OpenPGP packet that contains the wanted password */ if(debug){ fprintf_plus(stderr, "Retrieving OpenPGP encrypted password from" " %s\n", ip); } while(true){ if(quit_now){ errno = EINTR; goto mandos_end; } buffer_capacity = incbuffer(&buffer, buffer_length, buffer_capacity); if(buffer_capacity == 0){ int e = errno; perror_plus("incbuffer"); errno = e; goto mandos_end; } if(quit_now){ errno = EINTR; goto mandos_end; } sret = gnutls_record_recv(session, buffer+buffer_length, BUFFER_SIZE); if(sret == 0){ break; } if(sret < 0){ switch(sret){ case GNUTLS_E_INTERRUPTED: case GNUTLS_E_AGAIN: break; case GNUTLS_E_REHANDSHAKE: do { ret = gnutls_handshake(session); if(quit_now){ errno = EINTR; goto mandos_end; } } while(ret == GNUTLS_E_AGAIN or ret == GNUTLS_E_INTERRUPTED); if(ret < 0){ fprintf_plus(stderr, "*** GnuTLS Re-handshake failed " "***\n"); gnutls_perror(ret); errno = EPROTO; goto mandos_end; } break; default: fprintf_plus(stderr, "Unknown error while reading data from" " encrypted session with Mandos server\n"); gnutls_bye(session, GNUTLS_SHUT_RDWR); errno = EIO; goto mandos_end; } } else { buffer_length += (size_t) sret; } } if(debug){ fprintf_plus(stderr, "Closing TLS session\n"); } if(quit_now){ errno = EINTR; goto mandos_end; } do { ret = gnutls_bye(session, GNUTLS_SHUT_RDWR); if(quit_now){ errno = EINTR; goto mandos_end; } } while(ret == GNUTLS_E_AGAIN or ret == GNUTLS_E_INTERRUPTED); if(buffer_length > 0){ ssize_t decrypted_buffer_size; decrypted_buffer_size = pgp_packet_decrypt(buffer, buffer_length, &decrypted_buffer, mc); if(decrypted_buffer_size >= 0){ written = 0; while(written < (size_t) decrypted_buffer_size){ if(quit_now){ errno = EINTR; goto mandos_end; } ret = (int)fwrite(decrypted_buffer + written, 1, (size_t)decrypted_buffer_size - written, stdout); if(ret == 0 and ferror(stdout)){ int e = errno; if(debug){ fprintf_plus(stderr, "Error writing encrypted data: %s\n", strerror(errno)); } errno = e; goto mandos_end; } written += (size_t)ret; } retval = 0; } } /* Shutdown procedure */ mandos_end: { int e = errno; free(decrypted_buffer); free(buffer); if(tcp_sd >= 0){ ret = (int)TEMP_FAILURE_RETRY(close(tcp_sd)); } if(ret == -1){ if(e == 0){ e = errno; } perror_plus("close"); } gnutls_deinit(session); errno = e; if(quit_now){ errno = EINTR; retval = -1; } } return retval; } static void resolve_callback(AvahiSServiceResolver *r, AvahiIfIndex interface, AvahiProtocol proto, AvahiResolverEvent event, const char *name, const char *type, const char *domain, const char *host_name, const AvahiAddress *address, uint16_t port, AVAHI_GCC_UNUSED AvahiStringList *txt, AVAHI_GCC_UNUSED AvahiLookupResultFlags flags, void* mc){ if(r == NULL){ return; } /* Called whenever a service has been resolved successfully or timed out */ if(quit_now){ return; } switch(event){ default: case AVAHI_RESOLVER_FAILURE: fprintf_plus(stderr, "(Avahi Resolver) Failed to resolve service " "'%s' of type '%s' in domain '%s': %s\n", name, type, domain, avahi_strerror(avahi_server_errno (((mandos_context*)mc)->server))); break; case AVAHI_RESOLVER_FOUND: { char ip[AVAHI_ADDRESS_STR_MAX]; avahi_address_snprint(ip, sizeof(ip), address); if(debug){ fprintf_plus(stderr, "Mandos server \"%s\" found on %s (%s, %" PRIdMAX ") on port %" PRIu16 "\n", name, host_name, ip, (intmax_t)interface, port); } int ret = start_mandos_communication(ip, (in_port_t)port, interface, avahi_proto_to_af(proto), mc); if(ret == 0){ avahi_simple_poll_quit(simple_poll); } else { if(not add_server(ip, (in_port_t)port, interface, avahi_proto_to_af(proto), &((mandos_context*)mc)->current_server)){ fprintf_plus(stderr, "Failed to add server \"%s\" to server" " list\n", name); } } } } avahi_s_service_resolver_free(r); } static void browse_callback(AvahiSServiceBrowser *b, AvahiIfIndex interface, AvahiProtocol protocol, AvahiBrowserEvent event, const char *name, const char *type, const char *domain, AVAHI_GCC_UNUSED AvahiLookupResultFlags flags, void* mc){ if(b == NULL){ return; } /* Called whenever a new services becomes available on the LAN or is removed from the LAN */ if(quit_now){ return; } switch(event){ default: case AVAHI_BROWSER_FAILURE: fprintf_plus(stderr, "(Avahi browser) %s\n", avahi_strerror(avahi_server_errno (((mandos_context*)mc)->server))); avahi_simple_poll_quit(simple_poll); return; case AVAHI_BROWSER_NEW: /* We ignore the returned Avahi resolver object. In the callback function we free it. If the Avahi server is terminated before the callback function is called the Avahi server will free the resolver for us. */ if(avahi_s_service_resolver_new(((mandos_context*)mc)->server, interface, protocol, name, type, domain, protocol, 0, resolve_callback, mc) == NULL) fprintf_plus(stderr, "Avahi: Failed to resolve service '%s':" " %s\n", name, avahi_strerror(avahi_server_errno (((mandos_context*)mc)->server))); break; case AVAHI_BROWSER_REMOVE: break; case AVAHI_BROWSER_ALL_FOR_NOW: case AVAHI_BROWSER_CACHE_EXHAUSTED: if(debug){ fprintf_plus(stderr, "No Mandos server found, still" " searching...\n"); } break; } } /* Signal handler that stops main loop after SIGTERM */ static void handle_sigterm(int sig){ if(quit_now){ return; } quit_now = 1; signal_received = sig; int old_errno = errno; /* set main loop to exit */ if(simple_poll != NULL){ avahi_simple_poll_quit(simple_poll); } errno = old_errno; } bool get_flags(const char *ifname, struct ifreq *ifr){ int ret; error_t ret_errno; int s = socket(PF_INET6, SOCK_DGRAM, IPPROTO_IP); if(s < 0){ ret_errno = errno; perror_plus("socket"); errno = ret_errno; return false; } strcpy(ifr->ifr_name, ifname); ret = ioctl(s, SIOCGIFFLAGS, ifr); if(ret == -1){ if(debug){ ret_errno = errno; perror_plus("ioctl SIOCGIFFLAGS"); errno = ret_errno; } return false; } return true; } bool good_flags(const char *ifname, const struct ifreq *ifr){ /* Reject the loopback device */ if(ifr->ifr_flags & IFF_LOOPBACK){ if(debug){ fprintf_plus(stderr, "Rejecting loopback interface \"%s\"\n", ifname); } return false; } /* Accept point-to-point devices only if connect_to is specified */ if(connect_to != NULL and (ifr->ifr_flags & IFF_POINTOPOINT)){ if(debug){ fprintf_plus(stderr, "Accepting point-to-point interface" " \"%s\"\n", ifname); } return true; } /* Otherwise, reject non-broadcast-capable devices */ if(not (ifr->ifr_flags & IFF_BROADCAST)){ if(debug){ fprintf_plus(stderr, "Rejecting non-broadcast interface" " \"%s\"\n", ifname); } return false; } /* Reject non-ARP interfaces (including dummy interfaces) */ if(ifr->ifr_flags & IFF_NOARP){ if(debug){ fprintf_plus(stderr, "Rejecting non-ARP interface \"%s\"\n", ifname); } return false; } /* Accept this device */ if(debug){ fprintf_plus(stderr, "Interface \"%s\" is good\n", ifname); } return true; } /* * This function determines if a directory entry in /sys/class/net * corresponds to an acceptable network device. * (This function is passed to scandir(3) as a filter function.) */ int good_interface(const struct dirent *if_entry){ if(if_entry->d_name[0] == '.'){ return 0; } struct ifreq ifr; if(not get_flags(if_entry->d_name, &ifr)){ if(debug){ fprintf_plus(stderr, "Failed to get flags for interface " "\"%s\"\n", if_entry->d_name); } return 0; } if(not good_flags(if_entry->d_name, &ifr)){ return 0; } return 1; } /* * This function determines if a network interface is up. */ bool interface_is_up(const char *interface){ struct ifreq ifr; if(not get_flags(interface, &ifr)){ if(debug){ fprintf_plus(stderr, "Failed to get flags for interface " "\"%s\"\n", interface); } return false; } return (bool)(ifr.ifr_flags & IFF_UP); } /* * This function determines if a network interface is running */ bool interface_is_running(const char *interface){ struct ifreq ifr; if(not get_flags(interface, &ifr)){ if(debug){ fprintf_plus(stderr, "Failed to get flags for interface " "\"%s\"\n", interface); } return false; } return (bool)(ifr.ifr_flags & IFF_RUNNING); } int notdotentries(const struct dirent *direntry){ /* Skip "." and ".." */ if(direntry->d_name[0] == '.' and (direntry->d_name[1] == '\0' or (direntry->d_name[1] == '.' and direntry->d_name[2] == '\0'))){ return 0; } return 1; } /* Is this directory entry a runnable program? */ int runnable_hook(const struct dirent *direntry){ int ret; size_t sret; struct stat st; if((direntry->d_name)[0] == '\0'){ /* Empty name? */ return 0; } sret = strspn(direntry->d_name, "ABCDEFGHIJKLMNOPQRSTUVWXYZ" "abcdefghijklmnopqrstuvwxyz" "0123456789" "_-"); if((direntry->d_name)[sret] != '\0'){ /* Contains non-allowed characters */ if(debug){ fprintf_plus(stderr, "Ignoring hook \"%s\" with bad name\n", direntry->d_name); } return 0; } char *fullname = NULL; ret = asprintf(&fullname, "%s/%s", hookdir, direntry->d_name); if(ret < 0){ perror_plus("asprintf"); return 0; } ret = stat(fullname, &st); if(ret == -1){ if(debug){ perror_plus("Could not stat hook"); } return 0; } if(not (S_ISREG(st.st_mode))){ /* Not a regular file */ if(debug){ fprintf_plus(stderr, "Ignoring hook \"%s\" - not a file\n", direntry->d_name); } return 0; } if(not (st.st_mode & (S_IXUSR | S_IXGRP | S_IXOTH))){ /* Not executable */ if(debug){ fprintf_plus(stderr, "Ignoring hook \"%s\" - not executable\n", direntry->d_name); } return 0; } if(debug){ fprintf_plus(stderr, "Hook \"%s\" is acceptable\n", direntry->d_name); } return 1; } int avahi_loop_with_timeout(AvahiSimplePoll *s, int retry_interval, mandos_context *mc){ int ret; struct timespec now; struct timespec waited_time; intmax_t block_time; while(true){ if(mc->current_server == NULL){ if (debug){ fprintf_plus(stderr, "Wait until first server is found." " No timeout!\n"); } ret = avahi_simple_poll_iterate(s, -1); } else { if (debug){ fprintf_plus(stderr, "Check current_server if we should run" " it, or wait\n"); } /* the current time */ ret = clock_gettime(CLOCK_MONOTONIC, &now); if(ret == -1){ perror_plus("clock_gettime"); return -1; } /* Calculating in ms how long time between now and server who we visted longest time ago. Now - last seen. */ waited_time.tv_sec = (now.tv_sec - mc->current_server->last_seen.tv_sec); waited_time.tv_nsec = (now.tv_nsec - mc->current_server->last_seen.tv_nsec); /* total time is 10s/10,000ms. Converting to s from ms by dividing by 1,000, and ns to ms by dividing by 1,000,000. */ block_time = ((retry_interval - ((intmax_t)waited_time.tv_sec * 1000)) - ((intmax_t)waited_time.tv_nsec / 1000000)); if (debug){ fprintf_plus(stderr, "Blocking for %" PRIdMAX " ms\n", block_time); } if(block_time <= 0){ ret = start_mandos_communication(mc->current_server->ip, mc->current_server->port, mc->current_server->if_index, mc->current_server->af, mc); if(ret == 0){ avahi_simple_poll_quit(s); return 0; } ret = clock_gettime(CLOCK_MONOTONIC, &mc->current_server->last_seen); if(ret == -1){ perror_plus("clock_gettime"); return -1; } mc->current_server = mc->current_server->next; block_time = 0; /* Call avahi to find new Mandos servers, but don't block */ } ret = avahi_simple_poll_iterate(s, (int)block_time); } if(ret != 0){ if (ret > 0 or errno != EINTR){ return (ret != 1) ? ret : 0; } } } } /* Set effective uid to 0, return errno */ error_t raise_privileges(void){ error_t old_errno = errno; error_t ret_errno = 0; if(seteuid(0) == -1){ ret_errno = errno; perror_plus("seteuid"); } errno = old_errno; return ret_errno; } /* Set effective and real user ID to 0. Return errno. */ error_t raise_privileges_permanently(void){ error_t old_errno = errno; error_t ret_errno = raise_privileges(); if(ret_errno != 0){ errno = old_errno; return ret_errno; } if(setuid(0) == -1){ ret_errno = errno; perror_plus("seteuid"); } errno = old_errno; return ret_errno; } /* Set effective user ID to unprivileged saved user ID */ error_t lower_privileges(void){ error_t old_errno = errno; error_t ret_errno = 0; if(seteuid(uid) == -1){ ret_errno = errno; perror_plus("seteuid"); } errno = old_errno; return ret_errno; } /* Lower privileges permanently */ error_t lower_privileges_permanently(void){ error_t old_errno = errno; error_t ret_errno = 0; if(setuid(uid) == -1){ ret_errno = errno; perror_plus("setuid"); } errno = old_errno; return ret_errno; } bool run_network_hooks(const char *mode, const char *interface, const float delay){ struct dirent **direntries; struct dirent *direntry; int ret; int numhooks = scandir(hookdir, &direntries, runnable_hook, alphasort); if(numhooks == -1){ if(errno == ENOENT){ if(debug){ fprintf_plus(stderr, "Network hook directory \"%s\" not" " found\n", hookdir); } } else { perror_plus("scandir"); } } else { int devnull = open("/dev/null", O_RDONLY); for(int i = 0; i < numhooks; i++){ direntry = direntries[i]; char *fullname = NULL; ret = asprintf(&fullname, "%s/%s", hookdir, direntry->d_name); if(ret < 0){ perror_plus("asprintf"); continue; } if(debug){ fprintf_plus(stderr, "Running network hook \"%s\"\n", direntry->d_name); } pid_t hook_pid = fork(); if(hook_pid == 0){ /* Child */ /* Raise privileges */ raise_privileges_permanently(); /* Set group */ errno = 0; ret = setgid(0); if(ret == -1){ perror_plus("setgid"); } /* Reset supplementary groups */ errno = 0; ret = setgroups(0, NULL); if(ret == -1){ perror_plus("setgroups"); } dup2(devnull, STDIN_FILENO); close(devnull); dup2(STDERR_FILENO, STDOUT_FILENO); ret = setenv("MANDOSNETHOOKDIR", hookdir, 1); if(ret == -1){ perror_plus("setenv"); _exit(EX_OSERR); } ret = setenv("DEVICE", interface, 1); if(ret == -1){ perror_plus("setenv"); _exit(EX_OSERR); } ret = setenv("VERBOSITY", debug ? "1" : "0", 1); if(ret == -1){ perror_plus("setenv"); _exit(EX_OSERR); } ret = setenv("MODE", mode, 1); if(ret == -1){ perror_plus("setenv"); _exit(EX_OSERR); } char *delaystring; ret = asprintf(&delaystring, "%f", delay); if(ret == -1){ perror_plus("asprintf"); _exit(EX_OSERR); } ret = setenv("DELAY", delaystring, 1); if(ret == -1){ free(delaystring); perror_plus("setenv"); _exit(EX_OSERR); } free(delaystring); if(connect_to != NULL){ ret = setenv("CONNECT", connect_to, 1); if(ret == -1){ perror_plus("setenv"); _exit(EX_OSERR); } } if(execl(fullname, direntry->d_name, mode, NULL) == -1){ perror_plus("execl"); _exit(EXIT_FAILURE); } } else { int status; if(TEMP_FAILURE_RETRY(waitpid(hook_pid, &status, 0)) == -1){ perror_plus("waitpid"); free(fullname); continue; } if(WIFEXITED(status)){ if(WEXITSTATUS(status) != 0){ fprintf_plus(stderr, "Warning: network hook \"%s\" exited" " with status %d\n", direntry->d_name, WEXITSTATUS(status)); free(fullname); continue; } } else if(WIFSIGNALED(status)){ fprintf_plus(stderr, "Warning: network hook \"%s\" died by" " signal %d\n", direntry->d_name, WTERMSIG(status)); free(fullname); continue; } else { fprintf_plus(stderr, "Warning: network hook \"%s\"" " crashed\n", direntry->d_name); free(fullname); continue; } } free(fullname); if(debug){ fprintf_plus(stderr, "Network hook \"%s\" ran successfully\n", direntry->d_name); } } close(devnull); } return true; } error_t bring_up_interface(const char *const interface, const float delay){ int sd = -1; error_t old_errno = errno; error_t ret_errno = 0; int ret, ret_setflags; struct ifreq network; unsigned int if_index = if_nametoindex(interface); if(if_index == 0){ fprintf_plus(stderr, "No such interface: \"%s\"\n", interface); errno = old_errno; return ENXIO; } if(quit_now){ errno = old_errno; return EINTR; } if(not interface_is_up(interface)){ if(not get_flags(interface, &network) and debug){ ret_errno = errno; fprintf_plus(stderr, "Failed to get flags for interface " "\"%s\"\n", interface); return ret_errno; } network.ifr_flags |= IFF_UP; sd = socket(PF_INET6, SOCK_DGRAM, IPPROTO_IP); if(sd < 0){ ret_errno = errno; perror_plus("socket"); errno = old_errno; return ret_errno; } if(quit_now){ close(sd); errno = old_errno; return EINTR; } if(debug){ fprintf_plus(stderr, "Bringing up interface \"%s\"\n", interface); } /* Raise priviliges */ raise_privileges(); #ifdef __linux__ /* Lower kernel loglevel to KERN_NOTICE to avoid KERN_INFO messages about the network interface to mess up the prompt */ int ret_linux = klogctl(8, NULL, 5); bool restore_loglevel = true; if(ret_linux == -1){ restore_loglevel = false; perror_plus("klogctl"); } #endif /* __linux__ */ ret_setflags = ioctl(sd, SIOCSIFFLAGS, &network); ret_errno = errno; #ifdef __linux__ if(restore_loglevel){ ret_linux = klogctl(7, NULL, 0); if(ret_linux == -1){ perror_plus("klogctl"); } } #endif /* __linux__ */ /* Lower privileges */ lower_privileges(); /* Close the socket */ ret = (int)TEMP_FAILURE_RETRY(close(sd)); if(ret == -1){ perror_plus("close"); } if(ret_setflags == -1){ errno = ret_errno; perror_plus("ioctl SIOCSIFFLAGS +IFF_UP"); errno = old_errno; return ret_errno; } } else if(debug){ fprintf_plus(stderr, "Interface \"%s\" is already up; good\n", interface); } /* Sleep checking until interface is running. Check every 0.25s, up to total time of delay */ for(int i=0; i < delay * 4; i++){ if(interface_is_running(interface)){ break; } struct timespec sleeptime = { .tv_nsec = 250000000 }; ret = nanosleep(&sleeptime, NULL); if(ret == -1 and errno != EINTR){ perror_plus("nanosleep"); } } errno = old_errno; return 0; } error_t take_down_interface(const char *const interface){ int sd = -1; error_t old_errno = errno; error_t ret_errno = 0; int ret, ret_setflags; struct ifreq network; unsigned int if_index = if_nametoindex(interface); if(if_index == 0){ fprintf_plus(stderr, "No such interface: \"%s\"\n", interface); errno = old_errno; return ENXIO; } if(interface_is_up(interface)){ if(not get_flags(interface, &network) and debug){ ret_errno = errno; fprintf_plus(stderr, "Failed to get flags for interface " "\"%s\"\n", interface); return ret_errno; } network.ifr_flags &= ~(short)IFF_UP; /* clear flag */ sd = socket(PF_INET6, SOCK_DGRAM, IPPROTO_IP); if(sd < 0){ ret_errno = errno; perror_plus("socket"); errno = old_errno; return ret_errno; } if(debug){ fprintf_plus(stderr, "Taking down interface \"%s\"\n", interface); } /* Raise priviliges */ raise_privileges(); ret_setflags = ioctl(sd, SIOCSIFFLAGS, &network); ret_errno = errno; /* Lower privileges */ lower_privileges(); /* Close the socket */ ret = (int)TEMP_FAILURE_RETRY(close(sd)); if(ret == -1){ perror_plus("close"); } if(ret_setflags == -1){ errno = ret_errno; perror_plus("ioctl SIOCSIFFLAGS -IFF_UP"); errno = old_errno; return ret_errno; } } else if(debug){ fprintf_plus(stderr, "Interface \"%s\" is already down; odd\n", interface); } errno = old_errno; return 0; } int main(int argc, char *argv[]){ mandos_context mc = { .server = NULL, .dh_bits = 1024, .priority = "SECURE256:!CTYPE-X.509:" "+CTYPE-OPENPGP", .current_server = NULL, .interfaces = NULL, .interfaces_size = 0 }; AvahiSServiceBrowser *sb = NULL; error_t ret_errno; int ret; intmax_t tmpmax; char *tmp; int exitcode = EXIT_SUCCESS; char *interfaces_to_take_down = NULL; size_t interfaces_to_take_down_size = 0; char tempdir[] = "/tmp/mandosXXXXXX"; bool tempdir_created = false; AvahiIfIndex if_index = AVAHI_IF_UNSPEC; const char *seckey = PATHDIR "/" SECKEY; const char *pubkey = PATHDIR "/" PUBKEY; char *interfaces_hooks = NULL; size_t interfaces_hooks_size = 0; bool gnutls_initialized = false; bool gpgme_initialized = false; float delay = 2.5f; double retry_interval = 10; /* 10s between trying a server and retrying the same server again */ struct sigaction old_sigterm_action = { .sa_handler = SIG_DFL }; struct sigaction sigterm_action = { .sa_handler = handle_sigterm }; uid = getuid(); gid = getgid(); /* Lower any group privileges we might have, just to be safe */ errno = 0; ret = setgid(gid); if(ret == -1){ perror_plus("setgid"); } /* Lower user privileges (temporarily) */ errno = 0; ret = seteuid(uid); if(ret == -1){ perror_plus("seteuid"); } if(quit_now){ goto end; } { struct argp_option options[] = { { .name = "debug", .key = 128, .doc = "Debug mode", .group = 3 }, { .name = "connect", .key = 'c', .arg = "ADDRESS:PORT", .doc = "Connect directly to a specific Mandos server", .group = 1 }, { .name = "interface", .key = 'i', .arg = "NAME", .doc = "Network interface that will be used to search for" " Mandos servers", .group = 1 }, { .name = "seckey", .key = 's', .arg = "FILE", .doc = "OpenPGP secret key file base name", .group = 1 }, { .name = "pubkey", .key = 'p', .arg = "FILE", .doc = "OpenPGP public key file base name", .group = 2 }, { .name = "dh-bits", .key = 129, .arg = "BITS", .doc = "Bit length of the prime number used in the" " Diffie-Hellman key exchange", .group = 2 }, { .name = "priority", .key = 130, .arg = "STRING", .doc = "GnuTLS priority string for the TLS handshake", .group = 1 }, { .name = "delay", .key = 131, .arg = "SECONDS", .doc = "Maximum delay to wait for interface startup", .group = 2 }, { .name = "retry", .key = 132, .arg = "SECONDS", .doc = "Retry interval used when denied by the Mandos server", .group = 2 }, { .name = "network-hook-dir", .key = 133, .arg = "DIR", .doc = "Directory where network hooks are located", .group = 2 }, /* * These reproduce what we would get without ARGP_NO_HELP */ { .name = "help", .key = '?', .doc = "Give this help list", .group = -1 }, { .name = "usage", .key = -3, .doc = "Give a short usage message", .group = -1 }, { .name = "version", .key = 'V', .doc = "Print program version", .group = -1 }, { .name = NULL } }; error_t parse_opt(int key, char *arg, struct argp_state *state){ errno = 0; switch(key){ case 128: /* --debug */ debug = true; break; case 'c': /* --connect */ connect_to = arg; break; case 'i': /* --interface */ ret_errno = argz_add_sep(&mc.interfaces, &mc.interfaces_size, arg, (int)','); if(ret_errno != 0){ argp_error(state, "%s", strerror(ret_errno)); } break; case 's': /* --seckey */ seckey = arg; break; case 'p': /* --pubkey */ pubkey = arg; break; case 129: /* --dh-bits */ errno = 0; tmpmax = strtoimax(arg, &tmp, 10); if(errno != 0 or tmp == arg or *tmp != '\0' or tmpmax != (typeof(mc.dh_bits))tmpmax){ argp_error(state, "Bad number of DH bits"); } mc.dh_bits = (typeof(mc.dh_bits))tmpmax; break; case 130: /* --priority */ mc.priority = arg; break; case 131: /* --delay */ errno = 0; delay = strtof(arg, &tmp); if(errno != 0 or tmp == arg or *tmp != '\0'){ argp_error(state, "Bad delay"); } case 132: /* --retry */ errno = 0; retry_interval = strtod(arg, &tmp); if(errno != 0 or tmp == arg or *tmp != '\0' or (retry_interval * 1000) > INT_MAX or retry_interval < 0){ argp_error(state, "Bad retry interval"); } break; case 133: /* --network-hook-dir */ hookdir = arg; break; /* * These reproduce what we would get without ARGP_NO_HELP */ case '?': /* --help */ argp_state_help(state, state->out_stream, (ARGP_HELP_STD_HELP | ARGP_HELP_EXIT_ERR) & ~(unsigned int)ARGP_HELP_EXIT_OK); case -3: /* --usage */ argp_state_help(state, state->out_stream, ARGP_HELP_USAGE | ARGP_HELP_EXIT_ERR); case 'V': /* --version */ fprintf_plus(state->out_stream, "%s\n", argp_program_version); exit(argp_err_exit_status); break; default: return ARGP_ERR_UNKNOWN; } return errno; } struct argp argp = { .options = options, .parser = parse_opt, .args_doc = "", .doc = "Mandos client -- Get and decrypt" " passwords from a Mandos server" }; ret = argp_parse(&argp, argc, argv, ARGP_IN_ORDER | ARGP_NO_HELP, 0, NULL); switch(ret){ case 0: break; case ENOMEM: default: errno = ret; perror_plus("argp_parse"); exitcode = EX_OSERR; goto end; case EINVAL: exitcode = EX_USAGE; goto end; } } { /* Work around Debian bug #633582: */ /* Re-raise priviliges */ if(raise_privileges() == 0){ struct stat st; if(strcmp(seckey, PATHDIR "/" SECKEY) == 0){ int seckey_fd = open(seckey, O_RDONLY); if(seckey_fd == -1){ perror_plus("open"); } else { ret = (int)TEMP_FAILURE_RETRY(fstat(seckey_fd, &st)); if(ret == -1){ perror_plus("fstat"); } else { if(S_ISREG(st.st_mode) and st.st_uid == 0 and st.st_gid == 0){ ret = fchown(seckey_fd, uid, gid); if(ret == -1){ perror_plus("fchown"); } } } TEMP_FAILURE_RETRY(close(seckey_fd)); } } if(strcmp(pubkey, PATHDIR "/" PUBKEY) == 0){ int pubkey_fd = open(pubkey, O_RDONLY); if(pubkey_fd == -1){ perror_plus("open"); } else { ret = (int)TEMP_FAILURE_RETRY(fstat(pubkey_fd, &st)); if(ret == -1){ perror_plus("fstat"); } else { if(S_ISREG(st.st_mode) and st.st_uid == 0 and st.st_gid == 0){ ret = fchown(pubkey_fd, uid, gid); if(ret == -1){ perror_plus("fchown"); } } } TEMP_FAILURE_RETRY(close(pubkey_fd)); } } /* Lower privileges */ lower_privileges(); } } /* Remove invalid interface names (except "none") */ { char *interface = NULL; while((interface = argz_next(mc.interfaces, mc.interfaces_size, interface))){ if(strcmp(interface, "none") != 0 and if_nametoindex(interface) == 0){ if(interface[0] != '\0'){ fprintf_plus(stderr, "Not using nonexisting interface" " \"%s\"\n", interface); } argz_delete(&mc.interfaces, &mc.interfaces_size, interface); interface = NULL; } } } /* Run network hooks */ { if(mc.interfaces != NULL){ interfaces_hooks = malloc(mc.interfaces_size); if(interfaces_hooks == NULL){ perror_plus("malloc"); goto end; } memcpy(interfaces_hooks, mc.interfaces, mc.interfaces_size); interfaces_hooks_size = mc.interfaces_size; argz_stringify(interfaces_hooks, interfaces_hooks_size, (int)','); } if(not run_network_hooks("start", interfaces_hooks != NULL ? interfaces_hooks : "", delay)){ goto end; } } if(not debug){ avahi_set_log_function(empty_log); } /* Initialize Avahi early so avahi_simple_poll_quit() can be called from the signal handler */ /* Initialize the pseudo-RNG for Avahi */ srand((unsigned int) time(NULL)); simple_poll = avahi_simple_poll_new(); if(simple_poll == NULL){ fprintf_plus(stderr, "Avahi: Failed to create simple poll object.\n"); exitcode = EX_UNAVAILABLE; goto end; } sigemptyset(&sigterm_action.sa_mask); ret = sigaddset(&sigterm_action.sa_mask, SIGINT); if(ret == -1){ perror_plus("sigaddset"); exitcode = EX_OSERR; goto end; } ret = sigaddset(&sigterm_action.sa_mask, SIGHUP); if(ret == -1){ perror_plus("sigaddset"); exitcode = EX_OSERR; goto end; } ret = sigaddset(&sigterm_action.sa_mask, SIGTERM); if(ret == -1){ perror_plus("sigaddset"); exitcode = EX_OSERR; goto end; } /* Need to check if the handler is SIG_IGN before handling: | [[info:libc:Initial Signal Actions]] | | [[info:libc:Basic Signal Handling]] | */ ret = sigaction(SIGINT, NULL, &old_sigterm_action); if(ret == -1){ perror_plus("sigaction"); return EX_OSERR; } if(old_sigterm_action.sa_handler != SIG_IGN){ ret = sigaction(SIGINT, &sigterm_action, NULL); if(ret == -1){ perror_plus("sigaction"); exitcode = EX_OSERR; goto end; } } ret = sigaction(SIGHUP, NULL, &old_sigterm_action); if(ret == -1){ perror_plus("sigaction"); return EX_OSERR; } if(old_sigterm_action.sa_handler != SIG_IGN){ ret = sigaction(SIGHUP, &sigterm_action, NULL); if(ret == -1){ perror_plus("sigaction"); exitcode = EX_OSERR; goto end; } } ret = sigaction(SIGTERM, NULL, &old_sigterm_action); if(ret == -1){ perror_plus("sigaction"); return EX_OSERR; } if(old_sigterm_action.sa_handler != SIG_IGN){ ret = sigaction(SIGTERM, &sigterm_action, NULL); if(ret == -1){ perror_plus("sigaction"); exitcode = EX_OSERR; goto end; } } /* If no interfaces were specified, make a list */ if(mc.interfaces == NULL){ struct dirent **direntries; /* Look for any good interfaces */ ret = scandir(sys_class_net, &direntries, good_interface, alphasort); if(ret >= 1){ /* Add all found interfaces to interfaces list */ for(int i = 0; i < ret; ++i){ ret_errno = argz_add(&mc.interfaces, &mc.interfaces_size, direntries[i]->d_name); if(ret_errno != 0){ perror_plus("argz_add"); continue; } if(debug){ fprintf_plus(stderr, "Will use interface \"%s\"\n", direntries[i]->d_name); } } free(direntries); } else { free(direntries); fprintf_plus(stderr, "Could not find a network interface\n"); exitcode = EXIT_FAILURE; goto end; } } /* Bring up interfaces which are down, and remove any "none"s */ { char *interface = NULL; while((interface = argz_next(mc.interfaces, mc.interfaces_size, interface))){ /* If interface name is "none", stop bringing up interfaces. Also remove all instances of "none" from the list */ if(strcmp(interface, "none") == 0){ argz_delete(&mc.interfaces, &mc.interfaces_size, interface); interface = NULL; while((interface = argz_next(mc.interfaces, mc.interfaces_size, interface))){ if(strcmp(interface, "none") == 0){ argz_delete(&mc.interfaces, &mc.interfaces_size, interface); interface = NULL; } } break; } bool interface_was_up = interface_is_up(interface); ret = bring_up_interface(interface, delay); if(not interface_was_up){ if(ret != 0){ errno = ret; perror_plus("Failed to bring up interface"); } else { ret_errno = argz_add(&interfaces_to_take_down, &interfaces_to_take_down_size, interface); } } } if(debug and (interfaces_to_take_down == NULL)){ fprintf_plus(stderr, "No interfaces were brought up\n"); } } /* If we only got one interface, explicitly use only that one */ if(argz_count(mc.interfaces, mc.interfaces_size) == 1){ if(debug){ fprintf_plus(stderr, "Using only interface \"%s\"\n", mc.interfaces); } if_index = (AvahiIfIndex)if_nametoindex(mc.interfaces); } if(quit_now){ goto end; } ret = init_gnutls_global(pubkey, seckey, &mc); if(ret == -1){ fprintf_plus(stderr, "init_gnutls_global failed\n"); exitcode = EX_UNAVAILABLE; goto end; } else { gnutls_initialized = true; } if(quit_now){ goto end; } if(mkdtemp(tempdir) == NULL){ perror_plus("mkdtemp"); goto end; } tempdir_created = true; if(quit_now){ goto end; } if(not init_gpgme(pubkey, seckey, tempdir, &mc)){ fprintf_plus(stderr, "init_gpgme failed\n"); exitcode = EX_UNAVAILABLE; goto end; } else { gpgme_initialized = true; } if(quit_now){ goto end; } if(connect_to != NULL){ /* Connect directly, do not use Zeroconf */ /* (Mainly meant for debugging) */ char *address = strrchr(connect_to, ':'); if(address == NULL){ fprintf_plus(stderr, "No colon in address\n"); exitcode = EX_USAGE; goto end; } if(quit_now){ goto end; } in_port_t port; errno = 0; tmpmax = strtoimax(address+1, &tmp, 10); if(errno != 0 or tmp == address+1 or *tmp != '\0' or tmpmax != (in_port_t)tmpmax){ fprintf_plus(stderr, "Bad port number\n"); exitcode = EX_USAGE; goto end; } if(quit_now){ goto end; } port = (in_port_t)tmpmax; *address = '\0'; /* Colon in address indicates IPv6 */ int af; if(strchr(connect_to, ':') != NULL){ af = AF_INET6; /* Accept [] around IPv6 address - see RFC 5952 */ if(connect_to[0] == '[' and address[-1] == ']') { connect_to++; address[-1] = '\0'; } } else { af = AF_INET; } address = connect_to; if(quit_now){ goto end; } while(not quit_now){ ret = start_mandos_communication(address, port, if_index, af, &mc); if(quit_now or ret == 0){ break; } if(debug){ fprintf_plus(stderr, "Retrying in %d seconds\n", (int)retry_interval); } sleep((int)retry_interval); } if (not quit_now){ exitcode = EXIT_SUCCESS; } goto end; } if(quit_now){ goto end; } { AvahiServerConfig config; /* Do not publish any local Zeroconf records */ avahi_server_config_init(&config); config.publish_hinfo = 0; config.publish_addresses = 0; config.publish_workstation = 0; config.publish_domain = 0; /* Allocate a new server */ mc.server = avahi_server_new(avahi_simple_poll_get(simple_poll), &config, NULL, NULL, &ret_errno); /* Free the Avahi configuration data */ avahi_server_config_free(&config); } /* Check if creating the Avahi server object succeeded */ if(mc.server == NULL){ fprintf_plus(stderr, "Failed to create Avahi server: %s\n", avahi_strerror(ret_errno)); exitcode = EX_UNAVAILABLE; goto end; } if(quit_now){ goto end; } /* Create the Avahi service browser */ sb = avahi_s_service_browser_new(mc.server, if_index, AVAHI_PROTO_UNSPEC, "_mandos._tcp", NULL, 0, browse_callback, (void *)&mc); if(sb == NULL){ fprintf_plus(stderr, "Failed to create service browser: %s\n", avahi_strerror(avahi_server_errno(mc.server))); exitcode = EX_UNAVAILABLE; goto end; } if(quit_now){ goto end; } /* Run the main loop */ if(debug){ fprintf_plus(stderr, "Starting Avahi loop search\n"); } ret = avahi_loop_with_timeout(simple_poll, (int)(retry_interval * 1000), &mc); if(debug){ fprintf_plus(stderr, "avahi_loop_with_timeout exited %s\n", (ret == 0) ? "successfully" : "with error"); } end: if(debug){ fprintf_plus(stderr, "%s exiting\n", argv[0]); } /* Cleanup things */ free(mc.interfaces); if(sb != NULL) avahi_s_service_browser_free(sb); if(mc.server != NULL) avahi_server_free(mc.server); if(simple_poll != NULL) avahi_simple_poll_free(simple_poll); if(gnutls_initialized){ gnutls_certificate_free_credentials(mc.cred); gnutls_global_deinit(); gnutls_dh_params_deinit(mc.dh_params); } if(gpgme_initialized){ gpgme_release(mc.ctx); } /* Cleans up the circular linked list of Mandos servers the client has seen */ if(mc.current_server != NULL){ mc.current_server->prev->next = NULL; while(mc.current_server != NULL){ server *next = mc.current_server->next; free(mc.current_server); mc.current_server = next; } } /* Re-raise priviliges */ { raise_privileges(); /* Run network hooks */ run_network_hooks("stop", interfaces_hooks != NULL ? interfaces_hooks : "", delay); /* Take down the network interfaces which were brought up */ { char *interface = NULL; while((interface=argz_next(interfaces_to_take_down, interfaces_to_take_down_size, interface))){ ret_errno = take_down_interface(interface); if(ret_errno != 0){ errno = ret_errno; perror_plus("Failed to take down interface"); } } if(debug and (interfaces_to_take_down == NULL)){ fprintf_plus(stderr, "No interfaces needed to be taken" " down\n"); } } lower_privileges_permanently(); } free(interfaces_to_take_down); free(interfaces_hooks); /* Removes the GPGME temp directory and all files inside */ if(tempdir_created){ struct dirent **direntries = NULL; struct dirent *direntry = NULL; int numentries = scandir(tempdir, &direntries, notdotentries, alphasort); if (numentries > 0){ for(int i = 0; i < numentries; i++){ direntry = direntries[i]; char *fullname = NULL; ret = asprintf(&fullname, "%s/%s", tempdir, direntry->d_name); if(ret < 0){ perror_plus("asprintf"); continue; } ret = remove(fullname); if(ret == -1){ fprintf_plus(stderr, "remove(\"%s\"): %s\n", fullname, strerror(errno)); } free(fullname); } } /* need to clean even if 0 because man page doesn't specify */ free(direntries); if (numentries == -1){ perror_plus("scandir"); } ret = rmdir(tempdir); if(ret == -1 and errno != ENOENT){ perror_plus("rmdir"); } } if(quit_now){ sigemptyset(&old_sigterm_action.sa_mask); old_sigterm_action.sa_handler = SIG_DFL; ret = (int)TEMP_FAILURE_RETRY(sigaction(signal_received, &old_sigterm_action, NULL)); if(ret == -1){ perror_plus("sigaction"); } do { ret = raise(signal_received); } while(ret != 0 and errno == EINTR); if(ret != 0){ perror_plus("raise"); abort(); } TEMP_FAILURE_RETRY(pause()); } return exitcode; } mandos-1.6.0/plugins.d/usplash.xml0000664000175000017500000002550711767455003017372 0ustar teddyteddy00000000000000 %common; ]> Mandos Manual Mandos &version; &TIMESTAMP; Björn Påhlsson
belorn@recompile.se
Teddy Hogeborn
teddy@recompile.se
2008 2009 2011 2012 Teddy Hogeborn Björn Påhlsson
&COMMANDNAME; 8mandos &COMMANDNAME; Mandos plugin to use usplash to get a password. &COMMANDNAME; DESCRIPTION This program prompts for a password using usplash8 and outputs any given password to standard output. If no usplash8 process can be found, this program will immediately exit with an exit code indicating failure. This program is not very useful on its own. This program is really meant to run as a plugin in the Mandos client-side system, where it is used as a fallback and alternative to retrieving passwords from a Mandos server. If this program is killed (presumably by plugin-runner 8mandos because some other plugin provided the password), it cannot tell usplash8 to abort requesting a password, because usplash 8 does not support this. Therefore, this program will then kill the running usplash 8 process and start a new one using the same command line arguments as the old one was using. OPTIONS This program takes no options. EXIT STATUS If exit status is 0, the output from the program is the password as it was read. Otherwise, if exit status is other than 0, the program was interrupted or encountered an error, and any output so far could be corrupt and/or truncated, and should therefore be ignored. ENVIRONMENT cryptsource crypttarget If set, these environment variables will be assumed to contain the source device name and the target device mapper name, respectively, and will be shown as part of the prompt. These variables will normally be inherited from plugin-runner 8mandos, which will normally have inherited them from /scripts/local-top/cryptroot in the initial RAM disk environment, which will have set them from parsing kernel arguments and /conf/conf.d/cryptroot (also in the initial RAM disk environment), which in turn will have been created when the initial RAM disk image was created by /usr/share/initramfs-tools/hooks/cryptroot, by extracting the information of the root file system from /etc/crypttab. This behavior is meant to exactly mirror the behavior of askpass, the default password prompter. FILES /dev/.initramfs/usplash_fifo This is the FIFO to where this program will write the commands for usplash8 . See fifo7 . /dev/.initramfs/usplash_outfifo This is the FIFO where this program will read the password from usplash8 . See fifo7 . /proc To find the running usplash8 , this directory will be searched for numeric entries which will be assumed to be directories. In all those directories, the exe and cmdline entries will be used to determine the name of the running binary, effective user and group ID, and the command line arguments. See proc5 . /sbin/usplash This is the name of the binary which will be searched for in the process list. See usplash8 . BUGS Killing usplash 8 and starting a new one is ugly, but necessary as long as it does not support aborting a password request. EXAMPLE Note that normally, this program will not be invoked directly, but instead started by the Mandos plugin-runner8mandos . This program takes no options. &COMMANDNAME; SECURITY If this program is killed by a signal, it will kill the process ID which at the start of this program was determined to run usplash8 as root (see also ). There is a very slight risk that, in the time between those events, that process ID was freed and then taken up by another process; the wrong process would then be killed. Now, this program can only be killed by the user who started it; see plugin-runner 8mandos. This program should therefore be started by a completely separate non-privileged user, and no other programs should be allowed to run as that special user. This means that it is not recommended to use the user "nobody" to start this program, as other possibly less trusted programs could be running as "nobody", and they would then be able to kill this program, triggering the killing of the process ID which may or may not be usplash 8. The only other thing that could be considered worthy of note is this: This program is meant to be run by plugin-runner8mandos, and will, when run standalone, outside, in a normal environment, immediately output on its standard output any presumably secret password it just received. Therefore, when running this program standalone (which should never normally be done), take care not to type in any real secret password by force of habit, since it would then immediately be shown as output. SEE ALSO intro 8mandos, crypttab 5, fifo 7, plugin-runner 8mandos, proc 5, usplash 8
mandos-1.6.0/plugins.d/usplash.c0000664000175000017500000004044711767455003017014 0ustar teddyteddy00000000000000/* -*- coding: utf-8 -*- */ /* * Usplash - Read a password from usplash and output it * * Copyright © 2008-2012 Teddy Hogeborn * Copyright © 2008-2012 Björn Påhlsson * * 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 of the * License, 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 * . * * Contact the authors at . */ #define _GNU_SOURCE /* asprintf(), TEMP_FAILURE_RETRY() */ #include /* sig_atomic_t, struct sigaction, sigemptyset(), sigaddset(), SIGINT, SIGHUP, SIGTERM, sigaction(), SIG_IGN, kill(), SIGKILL */ #include /* bool, false, true */ #include /* open(), O_WRONLY, O_RDONLY */ #include /* and, or, not*/ #include /* errno, EINTR */ #include #include /* size_t, ssize_t, pid_t, DIR, struct dirent */ #include /* NULL */ #include /* strlen(), memcmp(), strerror() */ #include /* asprintf(), vasprintf(), vprintf(), fprintf() */ #include /* close(), write(), readlink(), read(), STDOUT_FILENO, sleep(), fork(), setuid(), geteuid(), setsid(), chdir(), dup2(), STDERR_FILENO, execv() */ #include /* free(), EXIT_FAILURE, realloc(), EXIT_SUCCESS, malloc(), _exit(), getenv() */ #include /* opendir(), readdir(), closedir() */ #include /* intmax_t, strtoimax() */ #include /* struct stat, lstat(), S_ISLNK */ #include /* EX_OSERR, EX_UNAVAILABLE */ #include /* argz_count(), argz_extract() */ #include /* va_list, va_start(), ... */ sig_atomic_t interrupted_by_signal = 0; int signal_received; const char usplash_name[] = "/sbin/usplash"; /* Function to use when printing errors */ __attribute__((format (gnu_printf, 3, 4))) void error_plus(int status, int errnum, const char *formatstring, ...){ va_list ap; char *text; int ret; va_start(ap, formatstring); ret = vasprintf(&text, formatstring, ap); if (ret == -1){ fprintf(stderr, "Mandos plugin %s: ", program_invocation_short_name); vfprintf(stderr, formatstring, ap); fprintf(stderr, ": "); fprintf(stderr, "%s\n", strerror(errnum)); error(status, errno, "vasprintf while printing error"); return; } fprintf(stderr, "Mandos plugin "); error(status, errnum, "%s", text); free(text); } static void termination_handler(int signum){ if(interrupted_by_signal){ return; } interrupted_by_signal = 1; signal_received = signum; } static bool usplash_write(int *fifo_fd_r, const char *cmd, const char *arg){ /* * usplash_write(&fd, "TIMEOUT", "15") will write "TIMEOUT 15\0" * usplash_write(&fd, "PULSATE", NULL) will write "PULSATE\0" * SEE ALSO * usplash_write(8) */ int ret; if(*fifo_fd_r == -1){ ret = open("/dev/.initramfs/usplash_fifo", O_WRONLY); if(ret == -1){ return false; } *fifo_fd_r = ret; } const char *cmd_line; size_t cmd_line_len; char *cmd_line_alloc = NULL; if(arg == NULL){ cmd_line = cmd; cmd_line_len = strlen(cmd) + 1; } else { do { ret = asprintf(&cmd_line_alloc, "%s %s", cmd, arg); if(ret == -1){ int e = errno; TEMP_FAILURE_RETRY(close(*fifo_fd_r)); errno = e; return false; } } while(ret == -1); cmd_line = cmd_line_alloc; cmd_line_len = (size_t)ret + 1; } size_t written = 0; ssize_t sret = 0; while(written < cmd_line_len){ sret = write(*fifo_fd_r, cmd_line + written, cmd_line_len - written); if(sret == -1){ int e = errno; TEMP_FAILURE_RETRY(close(*fifo_fd_r)); free(cmd_line_alloc); errno = e; return false; } written += (size_t)sret; } free(cmd_line_alloc); return true; } /* Create prompt string */ char *makeprompt(void){ int ret = 0; char *prompt; const char *const cryptsource = getenv("cryptsource"); const char *const crypttarget = getenv("crypttarget"); const char prompt_start[] = "Enter passphrase to unlock the disk"; if(cryptsource == NULL){ if(crypttarget == NULL){ ret = asprintf(&prompt, "%s: ", prompt_start); } else { ret = asprintf(&prompt, "%s (%s): ", prompt_start, crypttarget); } } else { if(crypttarget == NULL){ ret = asprintf(&prompt, "%s %s: ", prompt_start, cryptsource); } else { ret = asprintf(&prompt, "%s %s (%s): ", prompt_start, cryptsource, crypttarget); } } if(ret == -1){ return NULL; } return prompt; } pid_t find_usplash(char **cmdline_r, size_t *cmdline_len_r){ int ret = 0; ssize_t sret = 0; char *cmdline = NULL; size_t cmdline_len = 0; DIR *proc_dir = opendir("/proc"); if(proc_dir == NULL){ error_plus(0, errno, "opendir"); return -1; } errno = 0; for(struct dirent *proc_ent = readdir(proc_dir); proc_ent != NULL; proc_ent = readdir(proc_dir)){ pid_t pid; { intmax_t tmpmax; char *tmp; tmpmax = strtoimax(proc_ent->d_name, &tmp, 10); if(errno != 0 or tmp == proc_ent->d_name or *tmp != '\0' or tmpmax != (pid_t)tmpmax){ /* Not a process */ errno = 0; continue; } pid = (pid_t)tmpmax; } /* Find the executable name by doing readlink() on the /proc//exe link */ char exe_target[sizeof(usplash_name)]; { /* create file name string */ char *exe_link; ret = asprintf(&exe_link, "/proc/%s/exe", proc_ent->d_name); if(ret == -1){ error_plus(0, errno, "asprintf"); goto fail_find_usplash; } /* Check that it refers to a symlink owned by root:root */ struct stat exe_stat; ret = lstat(exe_link, &exe_stat); if(ret == -1){ if(errno == ENOENT){ free(exe_link); continue; } error_plus(0, errno, "lstat"); free(exe_link); goto fail_find_usplash; } if(not S_ISLNK(exe_stat.st_mode) or exe_stat.st_uid != 0 or exe_stat.st_gid != 0){ free(exe_link); continue; } sret = readlink(exe_link, exe_target, sizeof(exe_target)); free(exe_link); } /* Compare executable name */ if((sret != ((ssize_t)sizeof(exe_target)-1)) or (memcmp(usplash_name, exe_target, sizeof(exe_target)-1) != 0)){ /* Not it */ continue; } /* Found usplash */ /* Read and save the command line of usplash in "cmdline" */ { /* Open /proc//cmdline */ int cl_fd; { char *cmdline_filename; ret = asprintf(&cmdline_filename, "/proc/%s/cmdline", proc_ent->d_name); if(ret == -1){ error_plus(0, errno, "asprintf"); goto fail_find_usplash; } cl_fd = open(cmdline_filename, O_RDONLY); free(cmdline_filename); if(cl_fd == -1){ error_plus(0, errno, "open"); goto fail_find_usplash; } } size_t cmdline_allocated = 0; char *tmp; const size_t blocksize = 1024; do { /* Allocate more space? */ if(cmdline_len + blocksize > cmdline_allocated){ tmp = realloc(cmdline, cmdline_allocated + blocksize); if(tmp == NULL){ error_plus(0, errno, "realloc"); close(cl_fd); goto fail_find_usplash; } cmdline = tmp; cmdline_allocated += blocksize; } /* Read data */ sret = read(cl_fd, cmdline + cmdline_len, cmdline_allocated - cmdline_len); if(sret == -1){ error_plus(0, errno, "read"); close(cl_fd); goto fail_find_usplash; } cmdline_len += (size_t)sret; } while(sret != 0); ret = close(cl_fd); if(ret == -1){ error_plus(0, errno, "close"); goto fail_find_usplash; } } /* Close directory */ ret = closedir(proc_dir); if(ret == -1){ error_plus(0, errno, "closedir"); goto fail_find_usplash; } /* Success */ *cmdline_r = cmdline; *cmdline_len_r = cmdline_len; return pid; } fail_find_usplash: free(cmdline); if(proc_dir != NULL){ int e = errno; closedir(proc_dir); errno = e; } return 0; } int main(__attribute__((unused))int argc, __attribute__((unused))char **argv){ int ret = 0; ssize_t sret; int fifo_fd = -1; int outfifo_fd = -1; char *buf = NULL; size_t buf_len = 0; pid_t usplash_pid = -1; bool usplash_accessed = false; int status = EXIT_FAILURE; /* Default failure exit status */ char *prompt = makeprompt(); if(prompt == NULL){ status = EX_OSERR; goto failure; } /* Find usplash process */ char *cmdline = NULL; size_t cmdline_len = 0; usplash_pid = find_usplash(&cmdline, &cmdline_len); if(usplash_pid == 0){ status = EX_UNAVAILABLE; goto failure; } /* Set up the signal handler */ { struct sigaction old_action, new_action = { .sa_handler = termination_handler, .sa_flags = 0 }; sigemptyset(&new_action.sa_mask); ret = sigaddset(&new_action.sa_mask, SIGINT); if(ret == -1){ error_plus(0, errno, "sigaddset"); status = EX_OSERR; goto failure; } ret = sigaddset(&new_action.sa_mask, SIGHUP); if(ret == -1){ error_plus(0, errno, "sigaddset"); status = EX_OSERR; goto failure; } ret = sigaddset(&new_action.sa_mask, SIGTERM); if(ret == -1){ error_plus(0, errno, "sigaddset"); status = EX_OSERR; goto failure; } ret = sigaction(SIGINT, NULL, &old_action); if(ret == -1){ if(errno != EINTR){ error_plus(0, errno, "sigaction"); status = EX_OSERR; } goto failure; } if(old_action.sa_handler != SIG_IGN){ ret = sigaction(SIGINT, &new_action, NULL); if(ret == -1){ if(errno != EINTR){ error_plus(0, errno, "sigaction"); status = EX_OSERR; } goto failure; } } ret = sigaction(SIGHUP, NULL, &old_action); if(ret == -1){ if(errno != EINTR){ error_plus(0, errno, "sigaction"); status = EX_OSERR; } goto failure; } if(old_action.sa_handler != SIG_IGN){ ret = sigaction(SIGHUP, &new_action, NULL); if(ret == -1){ if(errno != EINTR){ error_plus(0, errno, "sigaction"); status = EX_OSERR; } goto failure; } } ret = sigaction(SIGTERM, NULL, &old_action); if(ret == -1){ if(errno != EINTR){ error_plus(0, errno, "sigaction"); status = EX_OSERR; } goto failure; } if(old_action.sa_handler != SIG_IGN){ ret = sigaction(SIGTERM, &new_action, NULL); if(ret == -1){ if(errno != EINTR){ error_plus(0, errno, "sigaction"); status = EX_OSERR; } goto failure; } } } usplash_accessed = true; /* Write command to FIFO */ if(not usplash_write(&fifo_fd, "TIMEOUT", "0")){ if(errno != EINTR){ error_plus(0, errno, "usplash_write"); status = EX_OSERR; } goto failure; } if(interrupted_by_signal){ goto failure; } if(not usplash_write(&fifo_fd, "INPUTQUIET", prompt)){ if(errno != EINTR){ error_plus(0, errno, "usplash_write"); status = EX_OSERR; } goto failure; } if(interrupted_by_signal){ goto failure; } free(prompt); prompt = NULL; /* Read reply from usplash */ /* Open FIFO */ outfifo_fd = open("/dev/.initramfs/usplash_outfifo", O_RDONLY); if(outfifo_fd == -1){ if(errno != EINTR){ error_plus(0, errno, "open"); status = EX_OSERR; } goto failure; } if(interrupted_by_signal){ goto failure; } /* Read from FIFO */ size_t buf_allocated = 0; const size_t blocksize = 1024; do { /* Allocate more space */ if(buf_len + blocksize > buf_allocated){ char *tmp = realloc(buf, buf_allocated + blocksize); if(tmp == NULL){ if(errno != EINTR){ error_plus(0, errno, "realloc"); status = EX_OSERR; } goto failure; } buf = tmp; buf_allocated += blocksize; } sret = read(outfifo_fd, buf + buf_len, buf_allocated - buf_len); if(sret == -1){ if(errno != EINTR){ error_plus(0, errno, "read"); status = EX_OSERR; } TEMP_FAILURE_RETRY(close(outfifo_fd)); goto failure; } if(interrupted_by_signal){ break; } buf_len += (size_t)sret; } while(sret != 0); ret = close(outfifo_fd); if(ret == -1){ if(errno != EINTR){ error_plus(0, errno, "close"); status = EX_OSERR; } goto failure; } outfifo_fd = -1; if(interrupted_by_signal){ goto failure; } if(not usplash_write(&fifo_fd, "TIMEOUT", "15")){ if(errno != EINTR){ error_plus(0, errno, "usplash_write"); status = EX_OSERR; } goto failure; } if(interrupted_by_signal){ goto failure; } ret = close(fifo_fd); if(ret == -1){ if(errno != EINTR){ error_plus(0, errno, "close"); status = EX_OSERR; } goto failure; } fifo_fd = -1; /* Print password to stdout */ size_t written = 0; while(written < buf_len){ do { sret = write(STDOUT_FILENO, buf + written, buf_len - written); if(sret == -1){ if(errno != EINTR){ error_plus(0, errno, "write"); status = EX_OSERR; } goto failure; } } while(sret == -1); if(interrupted_by_signal){ goto failure; } written += (size_t)sret; } free(buf); buf = NULL; if(interrupted_by_signal){ goto failure; } free(cmdline); return EXIT_SUCCESS; failure: free(buf); free(prompt); /* If usplash was never accessed, we can stop now */ if(not usplash_accessed){ return status; } /* Close FIFO */ if(fifo_fd != -1){ ret = (int)TEMP_FAILURE_RETRY(close(fifo_fd)); if(ret == -1 and errno != EINTR){ error_plus(0, errno, "close"); } fifo_fd = -1; } /* Close output FIFO */ if(outfifo_fd != -1){ ret = (int)TEMP_FAILURE_RETRY(close(outfifo_fd)); if(ret == -1){ error_plus(0, errno, "close"); } } /* Create argv for new usplash*/ char **cmdline_argv = malloc((argz_count(cmdline, cmdline_len) + 1) * sizeof(char *)); /* Count args */ if(cmdline_argv == NULL){ error_plus(0, errno, "malloc"); return status; } argz_extract(cmdline, cmdline_len, cmdline_argv); /* Create argv */ /* Kill old usplash */ kill(usplash_pid, SIGTERM); sleep(2); while(kill(usplash_pid, 0) == 0){ kill(usplash_pid, SIGKILL); sleep(1); } pid_t new_usplash_pid = fork(); if(new_usplash_pid == 0){ /* Child; will become new usplash process */ /* Make the effective user ID (root) the only user ID instead of the real user ID (_mandos) */ ret = setuid(geteuid()); if(ret == -1){ error_plus(0, errno, "setuid"); } setsid(); ret = chdir("/"); if(ret == -1){ error_plus(0, errno, "chdir"); _exit(EX_OSERR); } /* if(fork() != 0){ */ /* _exit(EXIT_SUCCESS); */ /* } */ ret = dup2(STDERR_FILENO, STDOUT_FILENO); /* replace our stdout */ if(ret == -1){ error_plus(0, errno, "dup2"); _exit(EX_OSERR); } execv(usplash_name, cmdline_argv); if(not interrupted_by_signal){ error_plus(0, errno, "execv"); } free(cmdline); free(cmdline_argv); _exit(EX_OSERR); } free(cmdline); free(cmdline_argv); sleep(2); if(not usplash_write(&fifo_fd, "PULSATE", NULL)){ if(errno != EINTR){ error_plus(0, errno, "usplash_write"); } } /* Close FIFO (again) */ if(fifo_fd != -1){ ret = (int)TEMP_FAILURE_RETRY(close(fifo_fd)); if(ret == -1 and errno != EINTR){ error_plus(0, errno, "close"); } fifo_fd = -1; } if(interrupted_by_signal){ struct sigaction signal_action = { .sa_handler = SIG_DFL }; sigemptyset(&signal_action.sa_mask); ret = (int)TEMP_FAILURE_RETRY(sigaction(signal_received, &signal_action, NULL)); if(ret == -1){ error_plus(0, errno, "sigaction"); } do { ret = raise(signal_received); } while(ret != 0 and errno == EINTR); if(ret != 0){ error_plus(0, errno, "raise"); abort(); } TEMP_FAILURE_RETRY(pause()); } return status; } mandos-1.6.0/plugins.d/plymouth.c0000664000175000017500000003104511767455003017210 0ustar teddyteddy00000000000000/* -*- coding: utf-8 -*- */ /* * Plymouth - Read a password from Plymouth and output it * * Copyright © 2010-2012 Teddy Hogeborn * Copyright © 2010-2012 Björn Påhlsson * * 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 of the * License, 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 * . * * Contact the authors at . */ #define _GNU_SOURCE /* asprintf(), TEMP_FAILURE_RETRY() */ #include /* sig_atomic_t, struct sigaction, sigemptyset(), sigaddset(), SIGINT, SIGHUP, SIGTERM, sigaction(), kill(), SIG_IGN */ #include /* bool, false, true */ #include /* open(), O_RDONLY */ #include /* and, or, not*/ #include /* size_t, ssize_t, pid_t, struct dirent, waitpid() */ #include /* waitpid() */ #include /* NULL */ #include /* strchr(), memcmp() */ #include /* asprintf(), perror(), fopen(), fscanf(), vasprintf(), fprintf(), vfprintf() */ #include /* close(), readlink(), read(), fork(), setsid(), chdir(), dup2(), STDERR_FILENO, execv(), access() */ #include /* free(), EXIT_FAILURE, realloc(), EXIT_SUCCESS, malloc(), _exit(), getenv() */ #include /* scandir(), alphasort() */ #include /* intmax_t, strtoumax(), SCNuMAX */ #include /* struct stat, lstat() */ #include /* EX_OSERR, EX_UNAVAILABLE */ #include /* error() */ #include /* TEMP_FAILURE_RETRY */ #include /* argz_count(), argz_extract() */ #include /* va_list, va_start(), ... */ sig_atomic_t interrupted_by_signal = 0; /* Used by Ubuntu 11.04 (Natty Narwahl) */ const char plymouth_old_pid[] = "/dev/.initramfs/plymouth.pid"; /* Used by Ubuntu 11.10 (Oneiric Ocelot) */ const char plymouth_pid[] = "/run/initramfs/plymouth.pid"; const char plymouth_path[] = "/bin/plymouth"; const char plymouthd_path[] = "/sbin/plymouthd"; const char *plymouthd_default_argv[] = {"/sbin/plymouthd", "--mode=boot", "--attach-to-session", NULL }; static void termination_handler(__attribute__((unused))int signum){ if(interrupted_by_signal){ return; } interrupted_by_signal = 1; } /* Function to use when printing errors */ __attribute__((format (gnu_printf, 3, 4))) void error_plus(int status, int errnum, const char *formatstring, ...){ va_list ap; char *text; int ret; va_start(ap, formatstring); ret = vasprintf(&text, formatstring, ap); if (ret == -1){ fprintf(stderr, "Mandos plugin %s: ", program_invocation_short_name); vfprintf(stderr, formatstring, ap); fprintf(stderr, ": "); fprintf(stderr, "%s\n", strerror(errnum)); error(status, errno, "vasprintf while printing error"); return; } fprintf(stderr, "Mandos plugin "); error(status, errnum, "%s", text); free(text); } /* Create prompt string */ char *makeprompt(void){ int ret = 0; char *prompt; const char *const cryptsource = getenv("cryptsource"); const char *const crypttarget = getenv("crypttarget"); const char prompt_start[] = "Unlocking the disk"; const char prompt_end[] = "Enter passphrase"; if(cryptsource == NULL){ if(crypttarget == NULL){ ret = asprintf(&prompt, "%s\n%s", prompt_start, prompt_end); } else { ret = asprintf(&prompt, "%s (%s)\n%s", prompt_start, crypttarget, prompt_end); } } else { if(crypttarget == NULL){ ret = asprintf(&prompt, "%s %s\n%s", prompt_start, cryptsource, prompt_end); } else { ret = asprintf(&prompt, "%s %s (%s)\n%s", prompt_start, cryptsource, crypttarget, prompt_end); } } if(ret == -1){ return NULL; } return prompt; } void kill_and_wait(pid_t pid){ TEMP_FAILURE_RETRY(kill(pid, SIGTERM)); TEMP_FAILURE_RETRY(waitpid(pid, NULL, 0)); } bool become_a_daemon(void){ int ret = setuid(geteuid()); if(ret == -1){ error_plus(0, errno, "setuid"); } setsid(); ret = chdir("/"); if(ret == -1){ error_plus(0, errno, "chdir"); return false; } ret = dup2(STDERR_FILENO, STDOUT_FILENO); /* replace our stdout */ if(ret == -1){ error_plus(0, errno, "dup2"); return false; } return true; } __attribute__((nonnull (2, 3))) bool exec_and_wait(pid_t *pid_return, const char *path, const char **argv, bool interruptable, bool daemonize){ int status; int ret; pid_t pid; pid = fork(); if(pid == -1){ error_plus(0, errno, "fork"); return false; } if(pid == 0){ /* Child */ if(daemonize){ if(not become_a_daemon()){ _exit(EX_OSERR); } } char **new_argv = NULL; char **tmp; int i = 0; for (; argv[i]!=NULL; i++){ tmp = realloc(new_argv, sizeof(const char *) * ((size_t)i + 1)); if (tmp == NULL){ error_plus(0, errno, "realloc"); free(new_argv); _exit(EX_OSERR); } new_argv = tmp; new_argv[i] = strdup(argv[i]); } new_argv[i] = NULL; execv(path, (char *const *)new_argv); error_plus(0, errno, "execv"); _exit(EXIT_FAILURE); } if(pid_return != NULL){ *pid_return = pid; } do { ret = waitpid(pid, &status, 0); } while(ret == -1 and errno == EINTR and ((not interrupted_by_signal) or (not interruptable))); if(interrupted_by_signal and interruptable){ return false; } if(ret == -1){ error_plus(0, errno, "waitpid"); return false; } if(WIFEXITED(status) and (WEXITSTATUS(status) == 0)){ return true; } return false; } __attribute__((nonnull)) int is_plymouth(const struct dirent *proc_entry){ int ret; { uintmax_t proc_id; char *tmp; errno = 0; proc_id = strtoumax(proc_entry->d_name, &tmp, 10); if(errno != 0 or *tmp != '\0' or proc_id != (uintmax_t)((pid_t)proc_id)){ return 0; } } char exe_target[sizeof(plymouthd_path)]; char *exe_link; ret = asprintf(&exe_link, "/proc/%s/exe", proc_entry->d_name); if(ret == -1){ error_plus(0, errno, "asprintf"); return 0; } struct stat exe_stat; ret = lstat(exe_link, &exe_stat); if(ret == -1){ free(exe_link); if(errno != ENOENT){ error_plus(0, errno, "lstat"); } return 0; } if(not S_ISLNK(exe_stat.st_mode) or exe_stat.st_uid != 0 or exe_stat.st_gid != 0){ free(exe_link); return 0; } ssize_t sret = readlink(exe_link, exe_target, sizeof(exe_target)); free(exe_link); if((sret != (ssize_t)sizeof(plymouthd_path)-1) or (memcmp(plymouthd_path, exe_target, sizeof(plymouthd_path)-1) != 0)){ return 0; } return 1; } pid_t get_pid(void){ int ret; uintmax_t proc_id = 0; FILE *pidfile = fopen(plymouth_pid, "r"); /* Try the new pid file location */ if(pidfile != NULL){ ret = fscanf(pidfile, "%" SCNuMAX, &proc_id); if(ret != 1){ proc_id = 0; } fclose(pidfile); } /* Try the old pid file location */ if(proc_id == 0){ pidfile = fopen(plymouth_pid, "r"); if(pidfile != NULL){ ret = fscanf(pidfile, "%" SCNuMAX, &proc_id); if(ret != 1){ proc_id = 0; } fclose(pidfile); } } /* Look for a plymouth process */ if(proc_id == 0){ struct dirent **direntries = NULL; ret = scandir("/proc", &direntries, is_plymouth, alphasort); if (ret == -1){ error_plus(0, errno, "scandir"); } if (ret > 0){ ret = sscanf(direntries[0]->d_name, "%" SCNuMAX, &proc_id); if (ret < 0){ error_plus(0, errno, "sscanf"); } } /* scandir might preallocate for this variable (man page unclear). even if ret == 0, therefore we need to free it. */ free(direntries); } pid_t pid; pid = (pid_t)proc_id; if((uintmax_t)pid == proc_id){ return pid; } return 0; } const char **getargv(pid_t pid){ int cl_fd; char *cmdline_filename; ssize_t sret; int ret; ret = asprintf(&cmdline_filename, "/proc/%" PRIuMAX "/cmdline", (uintmax_t)pid); if(ret == -1){ error_plus(0, errno, "asprintf"); return NULL; } /* Open /proc//cmdline */ cl_fd = open(cmdline_filename, O_RDONLY); free(cmdline_filename); if(cl_fd == -1){ error_plus(0, errno, "open"); return NULL; } size_t cmdline_allocated = 0; size_t cmdline_len = 0; char *cmdline = NULL; char *tmp; const size_t blocksize = 1024; do { /* Allocate more space? */ if(cmdline_len + blocksize > cmdline_allocated){ tmp = realloc(cmdline, cmdline_allocated + blocksize); if(tmp == NULL){ error_plus(0, errno, "realloc"); free(cmdline); close(cl_fd); return NULL; } cmdline = tmp; cmdline_allocated += blocksize; } /* Read data */ sret = read(cl_fd, cmdline + cmdline_len, cmdline_allocated - cmdline_len); if(sret == -1){ error_plus(0, errno, "read"); free(cmdline); close(cl_fd); return NULL; } cmdline_len += (size_t)sret; } while(sret != 0); ret = close(cl_fd); if(ret == -1){ error_plus(0, errno, "close"); free(cmdline); return NULL; } /* we got cmdline and cmdline_len, ignore rest... */ char **argv = malloc((argz_count(cmdline, cmdline_len) + 1) * sizeof(char *)); /* Get number of args */ if(argv == NULL){ error_plus(0, errno, "argv = malloc()"); free(cmdline); return NULL; } argz_extract(cmdline, cmdline_len, argv); /* Create argv */ return (const char **)argv; } int main(__attribute__((unused))int argc, __attribute__((unused))char **argv){ char *prompt; char *prompt_arg; pid_t plymouth_command_pid; int ret; bool bret; /* test -x /bin/plymouth */ ret = access(plymouth_path, X_OK); if(ret == -1){ /* Plymouth is probably not installed. Don't print an error message, just exit. */ exit(EX_UNAVAILABLE); } { /* Add signal handlers */ struct sigaction old_action, new_action = { .sa_handler = termination_handler, .sa_flags = 0 }; sigemptyset(&new_action.sa_mask); for(int *sig = (int[]){ SIGINT, SIGHUP, SIGTERM, 0 }; *sig != 0; sig++){ ret = sigaddset(&new_action.sa_mask, *sig); if(ret == -1){ error_plus(EX_OSERR, errno, "sigaddset"); } ret = sigaction(*sig, NULL, &old_action); if(ret == -1){ error_plus(EX_OSERR, errno, "sigaction"); } if(old_action.sa_handler != SIG_IGN){ ret = sigaction(*sig, &new_action, NULL); if(ret == -1){ error_plus(EX_OSERR, errno, "sigaction"); } } } } /* plymouth --ping */ bret = exec_and_wait(&plymouth_command_pid, plymouth_path, (const char *[]) { plymouth_path, "--ping", NULL }, true, false); if(not bret){ if(interrupted_by_signal){ kill_and_wait(plymouth_command_pid); exit(EXIT_FAILURE); } /* Plymouth is probably not running. Don't print an error message, just exit. */ exit(EX_UNAVAILABLE); } prompt = makeprompt(); ret = asprintf(&prompt_arg, "--prompt=%s", prompt); free(prompt); if(ret == -1){ error_plus(EX_OSERR, errno, "asprintf"); } /* plymouth ask-for-password --prompt="$prompt" */ bret = exec_and_wait(&plymouth_command_pid, plymouth_path, (const char *[]) { plymouth_path, "ask-for-password", prompt_arg, NULL }, true, false); free(prompt_arg); if(bret){ exit(EXIT_SUCCESS); } if(not interrupted_by_signal){ /* exec_and_wait failed for some other reason */ exit(EXIT_FAILURE); } kill_and_wait(plymouth_command_pid); const char **plymouthd_argv; pid_t pid = get_pid(); if(pid == 0){ error_plus(0, 0, "plymouthd pid not found"); plymouthd_argv = plymouthd_default_argv; } else { plymouthd_argv = getargv(pid); } bret = exec_and_wait(NULL, plymouth_path, (const char *[]) { plymouth_path, "quit", NULL }, false, false); if(not bret){ exit(EXIT_FAILURE); } bret = exec_and_wait(NULL, plymouthd_path, plymouthd_argv, false, true); if(not bret){ exit(EXIT_FAILURE); } exec_and_wait(NULL, plymouth_path, (const char *[]) { plymouth_path, "show-splash", NULL }, false, false); exit(EXIT_FAILURE); } mandos-1.6.0/plugins.d/askpass-fifo.c0000664000175000017500000001165011767455003017715 0ustar teddyteddy00000000000000/* -*- coding: utf-8 -*- */ /* * Askpass-FIFO - Read a password from a FIFO and output it * * Copyright © 2008-2012 Teddy Hogeborn * Copyright © 2008-2012 Björn Påhlsson * * 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 of the * License, 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 * . * * Contact the authors at . */ #define _GNU_SOURCE /* TEMP_FAILURE_RETRY() */ #include /* ssize_t */ #include /* mkfifo(), S_IRUSR, S_IWUSR */ #include /* and */ #include /* errno, EACCES, ENOTDIR, ELOOP, ENAMETOOLONG, ENOSPC, EROFS, ENOENT, EEXIST, EFAULT, EMFILE, ENFILE, ENOMEM, EBADF, EINVAL, EIO, EISDIR, EFBIG */ #include /* error() */ #include /* fprintf(), vfprintf(), vasprintf() */ #include /* EXIT_FAILURE, NULL, size_t, free(), realloc(), EXIT_SUCCESS */ #include /* open(), O_RDONLY */ #include /* read(), close(), write(), STDOUT_FILENO */ #include /* EX_OSERR, EX_OSFILE, EX_UNAVAILABLE, EX_IOERR */ #include /* strerror() */ #include /* va_list, va_start(), ... */ /* Function to use when printing errors */ __attribute__((format (gnu_printf, 3, 4))) void error_plus(int status, int errnum, const char *formatstring, ...){ va_list ap; char *text; int ret; va_start(ap, formatstring); ret = vasprintf(&text, formatstring, ap); if (ret == -1){ fprintf(stderr, "Mandos plugin %s: ", program_invocation_short_name); vfprintf(stderr, formatstring, ap); fprintf(stderr, ": "); fprintf(stderr, "%s\n", strerror(errnum)); error(status, errno, "vasprintf while printing error"); return; } fprintf(stderr, "Mandos plugin "); error(status, errnum, "%s", text); free(text); } int main(__attribute__((unused))int argc, __attribute__((unused))char **argv){ int ret = 0; ssize_t sret; /* Create FIFO */ const char passfifo[] = "/lib/cryptsetup/passfifo"; ret = mkfifo(passfifo, S_IRUSR | S_IWUSR); if(ret == -1){ int e = errno; switch(e){ case EACCES: case ENOTDIR: case ELOOP: error_plus(EX_OSFILE, errno, "mkfifo"); case ENAMETOOLONG: case ENOSPC: case EROFS: default: error_plus(EX_OSERR, errno, "mkfifo"); case ENOENT: /* no "/lib/cryptsetup"? */ error_plus(EX_UNAVAILABLE, errno, "mkfifo"); case EEXIST: break; /* not an error */ } } /* Open FIFO */ int fifo_fd = open(passfifo, O_RDONLY); if(fifo_fd == -1){ int e = errno; error_plus(0, errno, "open"); switch(e){ case EACCES: case ENOENT: case EFAULT: return EX_UNAVAILABLE; case ENAMETOOLONG: case EMFILE: case ENFILE: case ENOMEM: default: return EX_OSERR; case ENOTDIR: case ELOOP: return EX_OSFILE; } } /* Read from FIFO */ char *buf = NULL; size_t buf_len = 0; { size_t buf_allocated = 0; const size_t blocksize = 1024; do { if(buf_len + blocksize > buf_allocated){ char *tmp = realloc(buf, buf_allocated + blocksize); if(tmp == NULL){ error_plus(0, errno, "realloc"); free(buf); return EX_OSERR; } buf = tmp; buf_allocated += blocksize; } sret = read(fifo_fd, buf + buf_len, buf_allocated - buf_len); if(sret == -1){ int e = errno; free(buf); errno = e; error_plus(0, errno, "read"); switch(e){ case EBADF: case EFAULT: case EINVAL: default: return EX_OSERR; case EIO: return EX_IOERR; case EISDIR: return EX_UNAVAILABLE; } } buf_len += (size_t)sret; } while(sret != 0); } /* Close FIFO */ close(fifo_fd); /* Print password to stdout */ size_t written = 0; while(written < buf_len){ sret = write(STDOUT_FILENO, buf + written, buf_len - written); if(sret == -1){ int e = errno; free(buf); errno = e; error_plus(0, errno, "write"); switch(e){ case EBADF: case EFAULT: case EINVAL: return EX_OSFILE; case EFBIG: case EIO: case ENOSPC: default: return EX_IOERR; } } written += (size_t)sret; } free(buf); ret = close(STDOUT_FILENO); if(ret == -1){ int e = errno; error_plus(0, errno, "close"); switch(e){ case EBADF: return EX_OSFILE; case EIO: default: return EX_IOERR; } } return EXIT_SUCCESS; } mandos-1.6.0/plugins.d/splashy.xml0000664000175000017500000002442111767455003017370 0ustar teddyteddy00000000000000 %common; ]> Mandos Manual Mandos &version; &TIMESTAMP; Björn Påhlsson
belorn@recompile.se
Teddy Hogeborn
teddy@recompile.se
2008 2009 2012 Teddy Hogeborn Björn Påhlsson
&COMMANDNAME; 8mandos &COMMANDNAME; Mandos plugin to use splashy to get a password. &COMMANDNAME; DESCRIPTION This program prompts for a password using splashy_update 8 and outputs any given password to standard output. If no splashy8 process can be found, this program will immediately exit with an exit code indicating failure. This program is not very useful on its own. This program is really meant to run as a plugin in the Mandos client-side system, where it is used as a fallback and alternative to retrieving passwords from a Mandos server. If this program is killed (presumably by plugin-runner 8mandos because some other plugin provided the password), it cannot tell splashy8 to abort requesting a password, because splashy 8 does not support this. Therefore, this program will then kill the running splashy 8 process and start a new one, using boot as the only argument. OPTIONS This program takes no options. EXIT STATUS If exit status is 0, the output from the program is the password as it was read. Otherwise, if exit status is other than 0, the program was interrupted or encountered an error, and any output so far could be corrupt and/or truncated, and should therefore be ignored. ENVIRONMENT cryptsource crypttarget If set, these environment variables will be assumed to contain the source device name and the target device mapper name, respectively, and will be shown as part of the prompt. These variables will normally be inherited from plugin-runner 8mandos, which will normally have inherited them from /scripts/local-top/cryptroot in the initial RAM disk environment, which will have set them from parsing kernel arguments and /conf/conf.d/cryptroot (also in the initial RAM disk environment), which in turn will have been created when the initial RAM disk image was created by /usr/share/initramfs-tools/hooks/cryptroot, by extracting the information of the root file system from /etc/crypttab. This behavior is meant to exactly mirror the behavior of askpass, the default password prompter. FILES /sbin/splashy_update This is the command run to retrieve a password from splashy 8. See splashy_update8 . /proc To find the running splashy8 , this directory will be searched for numeric entries which will be assumed to be directories. In all those directories, the exe entry will be used to determine the name of the running binary and the effective user and group ID of the process. See proc5. /sbin/splashy This is the name of the binary which will be searched for in the process list. See splashy8 . BUGS Killing splashy 8 and starting a new one is ugly, but necessary as long as it does not support aborting a password request. EXAMPLE Note that normally, this program will not be invoked directly, but instead started by the Mandos plugin-runner8mandos . This program takes no options. &COMMANDNAME; SECURITY If this program is killed by a signal, it will kill the process ID which at the start of this program was determined to run splashy8 as root (see also ). There is a very slight risk that, in the time between those events, that process ID was freed and then taken up by another process; the wrong process would then be killed. Now, this program can only be killed by the user who started it; see plugin-runner 8mandos. This program should therefore be started by a completely separate non-privileged user, and no other programs should be allowed to run as that special user. This means that it is not recommended to use the user "nobody" to start this program, as other possibly less trusted programs could be running as "nobody", and they would then be able to kill this program, triggering the killing of the process ID which may or may not be splashy 8. The only other thing that could be considered worthy of note is this: This program is meant to be run by plugin-runner8mandos, and will, when run standalone, outside, in a normal environment, immediately output on its standard output any presumably secret password it just received. Therefore, when running this program standalone (which should never normally be done), take care not to type in any real secret password by force of habit, since it would then immediately be shown as output. SEE ALSO intro 8mandos, crypttab 5, plugin-runner 8mandos, proc 5, splashy 8, splashy_update 8
mandos-1.6.0/plugins.d/plymouth.xml0000664000175000017500000002416411767455003017572 0ustar teddyteddy00000000000000 %common; ]> Mandos Manual Mandos &version; &TIMESTAMP; Björn Påhlsson
belorn@recompile.se
Teddy Hogeborn
teddy@recompile.se
2010 2011 2012 Teddy Hogeborn Björn Påhlsson
&COMMANDNAME; 8mandos &COMMANDNAME; Mandos plugin to use plymouth to get a password. &COMMANDNAME; DESCRIPTION This program prompts for a password using plymouth8 and outputs any given password to standard output. If no plymouth8 process can be found, this program will immediately exit with an exit code indicating failure. This program is not very useful on its own. This program is really meant to run as a plugin in the Mandos client-side system, where it is used as a fallback and alternative to retrieving passwords from a Mandos server. If this program is killed (presumably by plugin-runner 8mandos because some other plugin provided the password), it cannot tell plymouth8 to abort requesting a password, because plymouth 8 does not support this. Therefore, this program will then kill the running plymouth 8 process and start a new one using the same command line arguments as the old one was using. OPTIONS This program takes no options. EXIT STATUS If exit status is 0, the output from the program is the password as it was read. Otherwise, if exit status is other than 0, the program was interrupted or encountered an error, and any output so far could be corrupt and/or truncated, and should therefore be ignored. ENVIRONMENT cryptsource crypttarget If set, these environment variables will be assumed to contain the source device name and the target device mapper name, respectively, and will be shown as part of the prompt. These variables will normally be inherited from plugin-runner 8mandos, which will normally have inherited them from /scripts/local-top/cryptroot in the initial RAM disk environment, which will have set them from parsing kernel arguments and /conf/conf.d/cryptroot (also in the initial RAM disk environment), which in turn will have been created when the initial RAM disk image was created by /usr/share/initramfs-tools/hooks/cryptroot, by extracting the information of the root file system from /etc/crypttab. This behavior is meant to exactly mirror the behavior of askpass, the default password prompter. FILES /bin/plymouth This is the command run to retrieve a password from plymouth 8. /proc To find the running plymouth8 , this directory will be searched for numeric entries which will be assumed to be directories. In all those directories, the exe and cmdline entries will be used to determine the name of the running binary, effective user and group ID, and the command line arguments. See proc5 . /sbin/plymouthd This is the name of the binary which will be searched for in the process list. See plymouth8 . BUGS Killing the plymouth8 daemon and starting a new one is ugly, but necessary as long as it does not support aborting a password request. EXAMPLE Note that normally, this program will not be invoked directly, but instead started by the Mandos plugin-runner8mandos . This program takes no options. &COMMANDNAME; SECURITY If this program is killed by a signal, it will kill the process ID which at the start of this program was determined to run plymouth8 as root (see also ). There is a very slight risk that, in the time between those events, that process ID was freed and then taken up by another process; the wrong process would then be killed. Now, this program can only be killed by the user who started it; see plugin-runner 8mandos. This program should therefore be started by a completely separate non-privileged user, and no other programs should be allowed to run as that special user. This means that it is not recommended to use the user "nobody" to start this program, as other possibly less trusted programs could be running as "nobody", and they would then be able to kill this program, triggering the killing of the process ID which may or may not be plymouth 8. The only other thing that could be considered worthy of note is this: This program is meant to be run by plugin-runner8mandos, and will, when run standalone, outside, in a normal environment, immediately output on its standard output any presumably secret password it just received. Therefore, when running this program standalone (which should never normally be done), take care not to type in any real secret password by force of habit, since it would then immediately be shown as output. SEE ALSO intro 8mandos, crypttab 5, plugin-runner 8mandos, proc 5, plymouth 8
mandos-1.6.0/plugins.d/password-prompt.c0000664000175000017500000003434311767455003020514 0ustar teddyteddy00000000000000/* -*- coding: utf-8; mode: c; mode: orgtbl -*- */ /* * Password-prompt - Read a password from the terminal and print it * * Copyright © 2008-2012 Teddy Hogeborn * Copyright © 2008-2012 Björn Påhlsson * * 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 of the * License, 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 * . * * Contact the authors at . */ #define _GNU_SOURCE /* getline(), asprintf() */ #include /* struct termios, tcsetattr(), TCSAFLUSH, tcgetattr(), ECHO */ #include /* struct termios, tcsetattr(), STDIN_FILENO, TCSAFLUSH, tcgetattr(), ECHO, readlink() */ #include /* sig_atomic_t, raise(), struct sigaction, sigemptyset(), sigaction(), sigaddset(), SIGINT, SIGQUIT, SIGHUP, SIGTERM, raise() */ #include /* NULL, size_t, ssize_t */ #include /* ssize_t, struct dirent, pid_t, ssize_t, open() */ #include /* EXIT_SUCCESS, EXIT_FAILURE, getenv(), free() */ #include /* scandir(), alphasort() */ #include /* fprintf(), stderr, getline(), stdin, feof(), fputc(), vfprintf(), vasprintf() */ #include /* errno, EBADF, ENOTTY, EINVAL, EFAULT, EFBIG, EIO, ENOSPC, EINTR */ #include /* error() */ #include /* or, not */ #include /* bool, false, true */ #include /* strtoumax() */ #include /* struct stat, lstat(), open() */ #include /* strlen, rindex, memcmp, strerror() */ #include /* struct argp_option, struct argp_state, struct argp, argp_parse(), error_t, ARGP_KEY_ARG, ARGP_KEY_END, ARGP_ERR_UNKNOWN */ #include /* EX_SOFTWARE, EX_OSERR, EX_UNAVAILABLE, EX_IOERR, EX_OK */ #include /* open() */ #include /* va_list, va_start(), ... */ volatile sig_atomic_t quit_now = 0; int signal_received; bool debug = false; const char *argp_program_version = "password-prompt " VERSION; const char *argp_program_bug_address = ""; /* Needed for conflict resolution */ const char plymouth_name[] = "plymouthd"; __attribute__((format (gnu_printf, 2, 3), nonnull(1))) int fprintf_plus(FILE *stream, const char *format, ...){ va_list ap; va_start (ap, format); TEMP_FAILURE_RETRY(fprintf(stream, "Mandos plugin %s: ", program_invocation_short_name)); return TEMP_FAILURE_RETRY(vfprintf(stream, format, ap)); } /* Function to use when printing errors */ __attribute__((format (gnu_printf, 3, 4))) void error_plus(int status, int errnum, const char *formatstring, ...){ va_list ap; char *text; int ret; va_start(ap, formatstring); ret = vasprintf(&text, formatstring, ap); if (ret == -1){ fprintf(stderr, "Mandos plugin %s: ", program_invocation_short_name); vfprintf(stderr, formatstring, ap); fprintf(stderr, ": %s\n", strerror(errnum)); error(status, errno, "vasprintf while printing error"); return; } fprintf(stderr, "Mandos plugin "); error(status, errnum, "%s", text); free(text); } static void termination_handler(int signum){ if(quit_now){ return; } quit_now = 1; signal_received = signum; } bool conflict_detection(void){ /* plymouth conflicts with password-prompt since both want to read from the terminal. Password-prompt will exit if it detects plymouth since plymouth performs the same functionality. */ __attribute__((nonnull)) int is_plymouth(const struct dirent *proc_entry){ int ret; int cl_fd; { uintmax_t proc_id; char *tmp; errno = 0; proc_id = strtoumax(proc_entry->d_name, &tmp, 10); if(errno != 0 or *tmp != '\0' or proc_id != (uintmax_t)((pid_t)proc_id)){ return 0; } } char *cmdline_filename; ret = asprintf(&cmdline_filename, "/proc/%s/cmdline", proc_entry->d_name); if(ret == -1){ error_plus(0, errno, "asprintf"); return 0; } /* Open /proc//cmdline */ cl_fd = open(cmdline_filename, O_RDONLY); free(cmdline_filename); if(cl_fd == -1){ if(errno != ENOENT){ error_plus(0, errno, "open"); } return 0; } char *cmdline = NULL; { size_t cmdline_len = 0; size_t cmdline_allocated = 0; char *tmp; const size_t blocksize = 1024; ssize_t sret; do { /* Allocate more space? */ if(cmdline_len + blocksize + 1 > cmdline_allocated){ tmp = realloc(cmdline, cmdline_allocated + blocksize + 1); if(tmp == NULL){ error_plus(0, errno, "realloc"); free(cmdline); close(cl_fd); return 0; } cmdline = tmp; cmdline_allocated += blocksize; } /* Read data */ sret = read(cl_fd, cmdline + cmdline_len, cmdline_allocated - cmdline_len); if(sret == -1){ error_plus(0, errno, "read"); free(cmdline); close(cl_fd); return 0; } cmdline_len += (size_t)sret; } while(sret != 0); ret = close(cl_fd); if(ret == -1){ error_plus(0, errno, "close"); free(cmdline); return 0; } cmdline[cmdline_len] = '\0'; /* Make sure it is terminated */ } /* we now have cmdline */ /* get basename */ char *cmdline_base = strrchr(cmdline, '/'); if(cmdline_base != NULL){ cmdline_base += 1; /* skip the slash */ } else { cmdline_base = cmdline; } if(strcmp(cmdline_base, plymouth_name) != 0){ if(debug){ fprintf(stderr, "\"%s\" is not \"%s\"\n", cmdline_base, plymouth_name); } free(cmdline); return 0; } if(debug){ fprintf(stderr, "\"%s\" equals \"%s\"\n", cmdline_base, plymouth_name); } free(cmdline); return 1; } struct dirent **direntries = NULL; int ret; ret = scandir("/proc", &direntries, is_plymouth, alphasort); if (ret == -1){ error_plus(1, errno, "scandir"); } free(direntries); return ret > 0; } int main(int argc, char **argv){ ssize_t sret; int ret; size_t n; struct termios t_new, t_old; char *buffer = NULL; char *prefix = NULL; int status = EXIT_SUCCESS; struct sigaction old_action, new_action = { .sa_handler = termination_handler, .sa_flags = 0 }; { struct argp_option options[] = { { .name = "prefix", .key = 'p', .arg = "PREFIX", .flags = 0, .doc = "Prefix shown before the prompt", .group = 2 }, { .name = "debug", .key = 128, .doc = "Debug mode", .group = 3 }, /* * These reproduce what we would get without ARGP_NO_HELP */ { .name = "help", .key = '?', .doc = "Give this help list", .group = -1 }, { .name = "usage", .key = -3, .doc = "Give a short usage message", .group = -1 }, { .name = "version", .key = 'V', .doc = "Print program version", .group = -1 }, { .name = NULL } }; __attribute__((nonnull(3))) error_t parse_opt (int key, char *arg, struct argp_state *state){ errno = 0; switch (key){ case 'p': prefix = arg; break; case 128: debug = true; break; /* * These reproduce what we would get without ARGP_NO_HELP */ case '?': /* --help */ argp_state_help(state, state->out_stream, (ARGP_HELP_STD_HELP | ARGP_HELP_EXIT_ERR) & ~(unsigned int)ARGP_HELP_EXIT_OK); case -3: /* --usage */ argp_state_help(state, state->out_stream, ARGP_HELP_USAGE | ARGP_HELP_EXIT_ERR); case 'V': /* --version */ fprintf(state->out_stream, "%s\n", argp_program_version); exit(argp_err_exit_status); break; default: return ARGP_ERR_UNKNOWN; } return errno; } struct argp argp = { .options = options, .parser = parse_opt, .args_doc = "", .doc = "Mandos password-prompt -- Read and" " output a password" }; ret = argp_parse(&argp, argc, argv, ARGP_IN_ORDER | ARGP_NO_HELP, NULL, NULL); switch(ret){ case 0: break; case ENOMEM: default: errno = ret; error_plus(0, errno, "argp_parse"); return EX_OSERR; case EINVAL: return EX_USAGE; } } if(debug){ fprintf(stderr, "Starting %s\n", argv[0]); } if (conflict_detection()){ if(debug){ fprintf(stderr, "Stopping %s because of conflict\n", argv[0]); } return EXIT_FAILURE; } if(debug){ fprintf(stderr, "Storing current terminal attributes\n"); } if(tcgetattr(STDIN_FILENO, &t_old) != 0){ int e = errno; error_plus(0, errno, "tcgetattr"); switch(e){ case EBADF: case ENOTTY: return EX_UNAVAILABLE; default: return EX_OSERR; } } sigemptyset(&new_action.sa_mask); ret = sigaddset(&new_action.sa_mask, SIGINT); if(ret == -1){ error_plus(0, errno, "sigaddset"); return EX_OSERR; } ret = sigaddset(&new_action.sa_mask, SIGHUP); if(ret == -1){ error_plus(0, errno, "sigaddset"); return EX_OSERR; } ret = sigaddset(&new_action.sa_mask, SIGTERM); if(ret == -1){ error_plus(0, errno, "sigaddset"); return EX_OSERR; } /* Need to check if the handler is SIG_IGN before handling: | [[info:libc:Initial Signal Actions]] | | [[info:libc:Basic Signal Handling]] | */ ret = sigaction(SIGINT, NULL, &old_action); if(ret == -1){ error_plus(0, errno, "sigaction"); return EX_OSERR; } if(old_action.sa_handler != SIG_IGN){ ret = sigaction(SIGINT, &new_action, NULL); if(ret == -1){ error_plus(0, errno, "sigaction"); return EX_OSERR; } } ret = sigaction(SIGHUP, NULL, &old_action); if(ret == -1){ error_plus(0, errno, "sigaction"); return EX_OSERR; } if(old_action.sa_handler != SIG_IGN){ ret = sigaction(SIGHUP, &new_action, NULL); if(ret == -1){ error_plus(0, errno, "sigaction"); return EX_OSERR; } } ret = sigaction(SIGTERM, NULL, &old_action); if(ret == -1){ error_plus(0, errno, "sigaction"); return EX_OSERR; } if(old_action.sa_handler != SIG_IGN){ ret = sigaction(SIGTERM, &new_action, NULL); if(ret == -1){ error_plus(0, errno, "sigaction"); return EX_OSERR; } } if(debug){ fprintf(stderr, "Removing echo flag from terminal attributes\n"); } t_new = t_old; t_new.c_lflag &= ~(tcflag_t)ECHO; if(tcsetattr(STDIN_FILENO, TCSAFLUSH, &t_new) != 0){ int e = errno; error_plus(0, errno, "tcsetattr-echo"); switch(e){ case EBADF: case ENOTTY: return EX_UNAVAILABLE; case EINVAL: default: return EX_OSERR; } } if(debug){ fprintf(stderr, "Waiting for input from stdin \n"); } while(true){ if(quit_now){ if(debug){ fprintf(stderr, "Interrupted by signal, exiting.\n"); } status = EXIT_FAILURE; break; } if(prefix){ fprintf(stderr, "%s ", prefix); } { const char *cryptsource = getenv("CRYPTTAB_SOURCE"); const char *crypttarget = getenv("CRYPTTAB_NAME"); /* Before cryptsetup 1.1.0~rc2 */ if(cryptsource == NULL){ cryptsource = getenv("cryptsource"); } if(crypttarget == NULL){ crypttarget = getenv("crypttarget"); } const char *const prompt1 = "Unlocking the disk"; const char *const prompt2 = "Enter passphrase"; if(cryptsource == NULL){ if(crypttarget == NULL){ fprintf(stderr, "%s to unlock the disk: ", prompt2); } else { fprintf(stderr, "%s (%s)\n%s: ", prompt1, crypttarget, prompt2); } } else { if(crypttarget == NULL){ fprintf(stderr, "%s %s\n%s: ", prompt1, cryptsource, prompt2); } else { fprintf(stderr, "%s %s (%s)\n%s: ", prompt1, cryptsource, crypttarget, prompt2); } } } sret = getline(&buffer, &n, stdin); if(sret > 0){ status = EXIT_SUCCESS; /* Make n = data size instead of allocated buffer size */ n = (size_t)sret; /* Strip final newline */ if(n > 0 and buffer[n-1] == '\n'){ buffer[n-1] = '\0'; /* not strictly necessary */ n--; } size_t written = 0; while(written < n){ sret = write(STDOUT_FILENO, buffer + written, n - written); if(sret < 0){ int e = errno; error_plus(0, errno, "write"); switch(e){ case EBADF: case EFAULT: case EINVAL: case EFBIG: case EIO: case ENOSPC: default: status = EX_IOERR; break; case EINTR: status = EXIT_FAILURE; break; } break; } written += (size_t)sret; } sret = close(STDOUT_FILENO); if(sret == -1){ int e = errno; error_plus(0, errno, "close"); switch(e){ case EBADF: status = EX_OSFILE; break; case EIO: default: status = EX_IOERR; break; } } break; } if(sret < 0){ int e = errno; if(errno != EINTR and not feof(stdin)){ error_plus(0, errno, "getline"); switch(e){ case EBADF: status = EX_UNAVAILABLE; break; case EIO: case EINVAL: default: status = EX_IOERR; break; } break; } } /* if(sret == 0), then the only sensible thing to do is to retry to read from stdin */ fputc('\n', stderr); if(debug and not quit_now){ /* If quit_now is nonzero, we were interrupted by a signal, and will print that later, so no need to show this too. */ fprintf(stderr, "getline() returned 0, retrying.\n"); } } free(buffer); if(debug){ fprintf(stderr, "Restoring terminal attributes\n"); } if(tcsetattr(STDIN_FILENO, TCSAFLUSH, &t_old) != 0){ error_plus(0, errno, "tcsetattr+echo"); } if(quit_now){ sigemptyset(&old_action.sa_mask); old_action.sa_handler = SIG_DFL; ret = sigaction(signal_received, &old_action, NULL); if(ret == -1){ error_plus(0, errno, "sigaction"); } raise(signal_received); } if(debug){ fprintf(stderr, "%s is exiting with status %d\n", argv[0], status); } if(status == EXIT_SUCCESS or status == EX_OK){ fputc('\n', stderr); } return status; } mandos-1.6.0/plugins.d/askpass-fifo.xml0000664000175000017500000001177611767455003020304 0ustar teddyteddy00000000000000 %common; ]> Mandos Manual Mandos &version; &TIMESTAMP; Björn Påhlsson
belorn@recompile.se
Teddy Hogeborn
teddy@recompile.se
2008 2009 2011 2012 Teddy Hogeborn Björn Påhlsson
&COMMANDNAME; 8mandos &COMMANDNAME; Mandos plugin to get a password from a FIFO. &COMMANDNAME; DESCRIPTION This program reads a password from a FIFO and outputs it to standard output. This program is not very useful on its own. This program is really meant to run as a plugin in the Mandos client-side system, where it is used as a fallback and alternative to retrieving passwords from a Mandos server. This program is meant to be imitate a feature of the askpass program, so that programs written to interface with it can keep working under the Mandos system. OPTIONS This program takes no options. EXIT STATUS If exit status is 0, the output from the program is the password as it was read. Otherwise, if exit status is other than 0, the program was interrupted or encountered an error, and any output so far could be corrupt and/or truncated, and should therefore be ignored. FILES /lib/cryptsetup/passfifo This is the FIFO where this program will read the password. If it does not exist, it will be created. EXAMPLE Note that normally, this program will not be invoked directly, but instead started by the Mandos plugin-runner8mandos . This program takes no options. &COMMANDNAME; SECURITY The only thing that could be considered worthy of note is this: This program is meant to be run by plugin-runner8mandos, and will, when run standalone, outside, in a normal environment, immediately output on its standard output any presumably secret password it just received. Therefore, when running this program standalone (which should never normally be done), take care not to type in any real secret password by force of habit, since it would then immediately be shown as output. SEE ALSO intro 8mandos, fifo 7, plugin-runner 8mandos
mandos-1.6.0/plugins.d/password-prompt.xml0000664000175000017500000002312111767455003021062 0ustar teddyteddy00000000000000 %common; ]> Mandos Manual Mandos &version; &TIMESTAMP; Björn Påhlsson
belorn@recompile.se
Teddy Hogeborn
teddy@recompile.se
2008 2009 2011 2012 Teddy Hogeborn Björn Påhlsson
&COMMANDNAME; 8mandos &COMMANDNAME; Prompt for a password and output it. &COMMANDNAME; PREFIX &COMMANDNAME; &COMMANDNAME; &COMMANDNAME; DESCRIPTION All &COMMANDNAME; does is prompt for a password and output any given password to standard output. This program is not very useful on its own. This program is really meant to run as a plugin in the Mandos client-side system, where it is used as a fallback and alternative to retrieving passwords from a Mandos server. This program is little more than a getpass3 wrapper, although actual use of that function is not guaranteed or implied. OPTIONS This program is commonly not invoked from the command line; it is normally started by the Mandos plugin runner, see plugin-runner8mandos . Any command line options this program accepts are therefore normally provided by the plugin runner, and not directly. Prefix string shown before the password prompt. Enable debug mode. This will enable a lot of output to standard error about what the program is doing. The program will still perform all other functions normally. Gives a help message about options and their meanings. Gives a short usage message. Prints the program version. EXIT STATUS If exit status is 0, the output from the program is the password as it was read. Otherwise, if exit status is other than 0, the program has encountered an error, and any output so far could be corrupt and/or truncated, and should therefore be ignored. ENVIRONMENT CRYPTTAB_SOURCE CRYPTTAB_NAME If set, these environment variables will be assumed to contain the source device name and the target device mapper name, respectively, and will be shown as part of the prompt. These variables will normally be inherited from plugin-runner 8mandos, which will normally have inherited them from /scripts/local-top/cryptroot in the initial RAM disk environment, which will have set them from parsing kernel arguments and /conf/conf.d/cryptroot (also in the initial RAM disk environment), which in turn will have been created when the initial RAM disk image was created by /usr/share/initramfs-tools/hooks/cryptroot, by extracting the information of the root file system from /etc/crypttab. This behavior is meant to exactly mirror the behavior of askpass, the default password prompter. BUGS None are known at this time. EXAMPLE Note that normally, command line options will not be given directly, but via options for the Mandos plugin-runner 8mandos. Normal invocation needs no options: &COMMANDNAME; Show a prefix before the prompt; in this case, a host name. It might be useful to be reminded of which host needs a password, in case of KVM switches, etc. &COMMANDNAME; --prefix=host.example.org: Run in debug mode. &COMMANDNAME; --debug SECURITY On its own, this program is very simple, and does not exactly present any security risks. The one thing that could be considered worthy of note is this: This program is meant to be run by plugin-runner8mandos , and will, when run standalone, outside, in a normal environment, immediately output on its standard output any presumably secret password it just received. Therefore, when running this program standalone (which should never normally be done), take care not to type in any real secret password by force of habit, since it would then immediately be shown as output. To further alleviate any risk of being locked out of a system, the plugin-runner 8mandos has a fallback mode which does the same thing as this program, only with less features. SEE ALSO intro 8mandos crypttab 5 mandos-client 8mandos plugin-runner 8mandos,
mandos-1.6.0/plugins.d/splashy.c0000664000175000017500000002720211767455003017012 0ustar teddyteddy00000000000000/* -*- coding: utf-8 -*- */ /* * Splashy - Read a password from splashy and output it * * Copyright © 2008-2012 Teddy Hogeborn * Copyright © 2008-2012 Björn Påhlsson * * 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 of the * License, 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 * . * * Contact the authors at . */ #define _GNU_SOURCE /* TEMP_FAILURE_RETRY(), asprintf() */ #include /* sig_atomic_t, struct sigaction, sigemptyset(), sigaddset(), SIGINT, SIGHUP, SIGTERM, sigaction, SIG_IGN, kill(), SIGKILL */ #include /* NULL */ #include /* getenv() */ #include /* asprintf(), vasprintf(), vprintf(), fprintf() */ #include /* EXIT_FAILURE, free(), EXIT_SUCCESS */ #include /* pid_t, DIR, struct dirent, ssize_t */ #include /* opendir(), readdir(), closedir() */ #include /* intmax_t, strtoimax() */ #include /* struct stat, lstat(), S_ISLNK */ #include /* not, or, and */ #include /* readlink(), fork(), execl(), sleep(), dup2() STDERR_FILENO, STDOUT_FILENO, _exit(), pause() */ #include /* memcmp(), strerror() */ #include /* errno, EACCES, ENOTDIR, ELOOP, ENOENT, ENAMETOOLONG, EMFILE, ENFILE, ENOMEM, ENOEXEC, EINVAL, E2BIG, EFAULT, EIO, ETXTBSY, EISDIR, ELIBBAD, EPERM, EINTR, ECHILD */ #include /* error() */ #include /* waitpid(), WIFEXITED(), WEXITSTATUS() */ #include /* EX_OSERR, EX_OSFILE, EX_UNAVAILABLE */ #include /* va_list, va_start(), ... */ sig_atomic_t interrupted_by_signal = 0; int signal_received; /* Function to use when printing errors */ __attribute__((format (gnu_printf, 3, 4))) void error_plus(int status, int errnum, const char *formatstring, ...){ va_list ap; char *text; int ret; va_start(ap, formatstring); ret = vasprintf(&text, formatstring, ap); if (ret == -1){ fprintf(stderr, "Mandos plugin %s: ", program_invocation_short_name); vfprintf(stderr, formatstring, ap); fprintf(stderr, ": "); fprintf(stderr, "%s\n", strerror(errnum)); error(status, errno, "vasprintf while printing error"); return; } fprintf(stderr, "Mandos plugin "); error(status, errnum, "%s", text); free(text); } static void termination_handler(int signum){ if(interrupted_by_signal){ return; } interrupted_by_signal = 1; signal_received = signum; } int main(__attribute__((unused))int argc, __attribute__((unused))char **argv){ int ret = 0; char *prompt = NULL; DIR *proc_dir = NULL; pid_t splashy_pid = 0; pid_t splashy_command_pid = 0; int exitstatus = EXIT_FAILURE; /* Create prompt string */ { const char *const cryptsource = getenv("cryptsource"); const char *const crypttarget = getenv("crypttarget"); const char *const prompt_start = "getpass " "Enter passphrase to unlock the disk"; if(cryptsource == NULL){ if(crypttarget == NULL){ ret = asprintf(&prompt, "%s: ", prompt_start); } else { ret = asprintf(&prompt, "%s (%s): ", prompt_start, crypttarget); } } else { if(crypttarget == NULL){ ret = asprintf(&prompt, "%s %s: ", prompt_start, cryptsource); } else { ret = asprintf(&prompt, "%s %s (%s): ", prompt_start, cryptsource, crypttarget); } } if(ret == -1){ prompt = NULL; exitstatus = EX_OSERR; goto failure; } } /* Find splashy process */ { const char splashy_name[] = "/sbin/splashy"; proc_dir = opendir("/proc"); if(proc_dir == NULL){ int e = errno; error_plus(0, errno, "opendir"); switch(e){ case EACCES: case ENOTDIR: case ELOOP: case ENOENT: default: exitstatus = EX_OSFILE; break; case ENAMETOOLONG: case EMFILE: case ENFILE: case ENOMEM: exitstatus = EX_OSERR; break; } goto failure; } for(struct dirent *proc_ent = readdir(proc_dir); proc_ent != NULL; proc_ent = readdir(proc_dir)){ pid_t pid; { intmax_t tmpmax; char *tmp; errno = 0; tmpmax = strtoimax(proc_ent->d_name, &tmp, 10); if(errno != 0 or tmp == proc_ent->d_name or *tmp != '\0' or tmpmax != (pid_t)tmpmax){ /* Not a process */ continue; } pid = (pid_t)tmpmax; } /* Find the executable name by doing readlink() on the /proc//exe link */ char exe_target[sizeof(splashy_name)]; ssize_t sret; { char *exe_link; ret = asprintf(&exe_link, "/proc/%s/exe", proc_ent->d_name); if(ret == -1){ error_plus(0, errno, "asprintf"); exitstatus = EX_OSERR; goto failure; } /* Check that it refers to a symlink owned by root:root */ struct stat exe_stat; ret = lstat(exe_link, &exe_stat); if(ret == -1){ if(errno == ENOENT){ free(exe_link); continue; } int e = errno; error_plus(0, errno, "lstat"); free(exe_link); switch(e){ case EACCES: case ENOTDIR: case ELOOP: default: exitstatus = EX_OSFILE; break; case ENAMETOOLONG: exitstatus = EX_OSERR; break; } goto failure; } if(not S_ISLNK(exe_stat.st_mode) or exe_stat.st_uid != 0 or exe_stat.st_gid != 0){ free(exe_link); continue; } sret = readlink(exe_link, exe_target, sizeof(exe_target)); free(exe_link); } if((sret == ((ssize_t)sizeof(exe_target)-1)) and (memcmp(splashy_name, exe_target, sizeof(exe_target)-1) == 0)){ splashy_pid = pid; break; } } closedir(proc_dir); proc_dir = NULL; } if(splashy_pid == 0){ exitstatus = EX_UNAVAILABLE; goto failure; } /* Set up the signal handler */ { struct sigaction old_action, new_action = { .sa_handler = termination_handler, .sa_flags = 0 }; sigemptyset(&new_action.sa_mask); ret = sigaddset(&new_action.sa_mask, SIGINT); if(ret == -1){ error_plus(0, errno, "sigaddset"); exitstatus = EX_OSERR; goto failure; } ret = sigaddset(&new_action.sa_mask, SIGHUP); if(ret == -1){ error_plus(0, errno, "sigaddset"); exitstatus = EX_OSERR; goto failure; } ret = sigaddset(&new_action.sa_mask, SIGTERM); if(ret == -1){ error_plus(0, errno, "sigaddset"); exitstatus = EX_OSERR; goto failure; } ret = sigaction(SIGINT, NULL, &old_action); if(ret == -1){ error_plus(0, errno, "sigaction"); exitstatus = EX_OSERR; goto failure; } if(old_action.sa_handler != SIG_IGN){ ret = sigaction(SIGINT, &new_action, NULL); if(ret == -1){ error_plus(0, errno, "sigaction"); exitstatus = EX_OSERR; goto failure; } } ret = sigaction(SIGHUP, NULL, &old_action); if(ret == -1){ error_plus(0, errno, "sigaction"); exitstatus = EX_OSERR; goto failure; } if(old_action.sa_handler != SIG_IGN){ ret = sigaction(SIGHUP, &new_action, NULL); if(ret == -1){ error_plus(0, errno, "sigaction"); exitstatus = EX_OSERR; goto failure; } } ret = sigaction(SIGTERM, NULL, &old_action); if(ret == -1){ error_plus(0, errno, "sigaction"); exitstatus = EX_OSERR; goto failure; } if(old_action.sa_handler != SIG_IGN){ ret = sigaction(SIGTERM, &new_action, NULL); if(ret == -1){ error_plus(0, errno, "sigaction"); exitstatus = EX_OSERR; goto failure; } } } if(interrupted_by_signal){ goto failure; } /* Fork off the splashy command to prompt for password */ splashy_command_pid = fork(); if(splashy_command_pid != 0 and interrupted_by_signal){ goto failure; } if(splashy_command_pid == -1){ error_plus(0, errno, "fork"); exitstatus = EX_OSERR; goto failure; } /* Child */ if(splashy_command_pid == 0){ if(not interrupted_by_signal){ const char splashy_command[] = "/sbin/splashy_update"; execl(splashy_command, splashy_command, prompt, (char *)NULL); int e = errno; error_plus(0, errno, "execl"); switch(e){ case EACCES: case ENOENT: case ENOEXEC: case EINVAL: _exit(EX_UNAVAILABLE); case ENAMETOOLONG: case E2BIG: case ENOMEM: case EFAULT: case EIO: case EMFILE: case ENFILE: case ETXTBSY: default: _exit(EX_OSERR); case ENOTDIR: case ELOOP: case EISDIR: #ifdef ELIBBAD case ELIBBAD: /* Linux only */ #endif case EPERM: _exit(EX_OSFILE); } } free(prompt); _exit(EXIT_FAILURE); } /* Parent */ free(prompt); prompt = NULL; if(interrupted_by_signal){ goto failure; } /* Wait for command to complete */ { int status; do { ret = waitpid(splashy_command_pid, &status, 0); } while(ret == -1 and errno == EINTR and not interrupted_by_signal); if(interrupted_by_signal){ goto failure; } if(ret == -1){ error_plus(0, errno, "waitpid"); if(errno == ECHILD){ splashy_command_pid = 0; } } else { /* The child process has exited */ splashy_command_pid = 0; if(WIFEXITED(status) and WEXITSTATUS(status) == 0){ return EXIT_SUCCESS; } } } failure: free(prompt); if(proc_dir != NULL){ TEMP_FAILURE_RETRY(closedir(proc_dir)); } if(splashy_command_pid != 0){ TEMP_FAILURE_RETRY(kill(splashy_command_pid, SIGTERM)); TEMP_FAILURE_RETRY(kill(splashy_pid, SIGTERM)); sleep(2); while(TEMP_FAILURE_RETRY(kill(splashy_pid, 0)) == 0){ TEMP_FAILURE_RETRY(kill(splashy_pid, SIGKILL)); sleep(1); } pid_t new_splashy_pid = (pid_t)TEMP_FAILURE_RETRY(fork()); if(new_splashy_pid == 0){ /* Child; will become new splashy process */ /* Make the effective user ID (root) the only user ID instead of the real user ID (_mandos) */ ret = setuid(geteuid()); if(ret == -1){ error_plus(0, errno, "setuid"); } setsid(); ret = chdir("/"); if(ret == -1){ error_plus(0, errno, "chdir"); } /* if(fork() != 0){ */ /* _exit(EXIT_SUCCESS); */ /* } */ ret = dup2(STDERR_FILENO, STDOUT_FILENO); /* replace stdout */ if(ret == -1){ error_plus(0, errno, "dup2"); _exit(EX_OSERR); } execl("/sbin/splashy", "/sbin/splashy", "boot", (char *)NULL); { int e = errno; error_plus(0, errno, "execl"); switch(e){ case EACCES: case ENOENT: case ENOEXEC: default: _exit(EX_UNAVAILABLE); case ENAMETOOLONG: case E2BIG: case ENOMEM: _exit(EX_OSERR); case ENOTDIR: case ELOOP: _exit(EX_OSFILE); } } } } if(interrupted_by_signal){ struct sigaction signal_action; sigemptyset(&signal_action.sa_mask); signal_action.sa_handler = SIG_DFL; ret = (int)TEMP_FAILURE_RETRY(sigaction(signal_received, &signal_action, NULL)); if(ret == -1){ error_plus(0, errno, "sigaction"); } do { ret = raise(signal_received); } while(ret != 0 and errno == EINTR); if(ret != 0){ error_plus(0, errno, "raise"); abort(); } TEMP_FAILURE_RETRY(pause()); } return exitstatus; } mandos-1.6.0/plugins.d/mandos-client.xml0000664000175000017500000007170611767455003020452 0ustar teddyteddy00000000000000 %common; ]> Mandos Manual Mandos &version; &TIMESTAMP; Björn Påhlsson
belorn@recompile.se
Teddy Hogeborn
teddy@recompile.se
2008 2009 2012 Teddy Hogeborn Björn Påhlsson
&COMMANDNAME; 8mandos &COMMANDNAME; Client for Mandos &COMMANDNAME; &COMMANDNAME; &COMMANDNAME; &COMMANDNAME; DESCRIPTION &COMMANDNAME; is a client program that communicates with mandos8 to get a password. In slightly more detail, this client program brings up network interfaces, uses the interfaces’ IPv6 link-local addresses to get network connectivity, uses Zeroconf to find servers on the local network, and communicates with servers using TLS with an OpenPGP key to ensure authenticity and confidentiality. This client program keeps running, trying all servers on the network, until it receives a satisfactory reply or a TERM signal. After all servers have been tried, all servers are periodically retried. If no servers are found it will wait indefinitely for new servers to appear. The network interfaces are selected like this: If any interfaces are specified using the option, those interface are used. Otherwise, &COMMANDNAME; will use all interfaces that are not loopback interfaces, are not point-to-point interfaces, are capable of broadcasting and do not have the NOARP flag (see netdevice 7). (If the option is used, point-to-point interfaces and non-broadcast interfaces are accepted.) If any used interfaces are not up and running, they are first taken up (and later taken down again on program exit). Before network interfaces are selected, all network hooks are run; see . This program is not meant to be run directly; it is really meant to run as a plugin of the Mandos plugin-runner 8mandos, which runs in the initial RAM disk environment because it is specified as a keyscript in the crypttab5 file. PURPOSE The purpose of this is to enable remote and unattended rebooting of client host computer with an encrypted root file system. See for details. OPTIONS This program is commonly not invoked from the command line; it is normally started by the Mandos plugin runner, see plugin-runner8mandos . Any command line options this program accepts are therefore normally provided by the plugin runner, and not directly. Do not use Zeroconf to locate servers. Connect directly to only one specified Mandos server. Note that an IPv6 address has colon characters in it, so the last colon character is assumed to separate the address from the port number. This option is normally only useful for testing and debugging. Comma separated list of network interfaces that will be brought up and scanned for Mandos servers to connect to. The default is the empty string, which will automatically use all appropriate interfaces. If the option is used, and exactly one interface name is specified (except none), this specifies the interface to use to connect to the address given. Note that since this program will normally run in the initial RAM disk environment, the interface must be an interface which exists at that stage. Thus, the interface can normally not be a pseudo-interface such as br0 or tun0; such interfaces will not exist until much later in the boot process, and can not be used by this program, unless created by a network hook — see . NAME can be the string none; this will make &COMMANDNAME; not bring up any interfaces specified after this string. This is not recommended, and only meant for advanced users. OpenPGP public key file name. The default name is /conf/conf.d/mandos/pubkey.txt. OpenPGP secret key file name. The default name is /conf/conf.d/mandos/seckey.txt. Sets the number of bits to use for the prime number in the TLS Diffie-Hellman key exchange. Default is 1024. After bringing a network interface up, the program waits for the interface to arrive in a running state before proceeding. During this time, the kernel log level will be lowered to reduce clutter on the system console, alleviating any other plugins which might be using the system console. This option sets the upper limit of seconds to wait. The default is 2.5 seconds. All Mandos servers are tried repeatedly until a password is received. This value specifies, in seconds, how long between each successive try for the same server. The default is 10 seconds. Network hook directory. The default directory is /lib/mandos/network-hooks.d. Enable debug mode. This will enable a lot of output to standard error about what the program is doing. The program will still perform all other functions normally. It will also enable debug mode in the Avahi and GnuTLS libraries, making them print large amounts of debugging output. Gives a help message about options and their meanings. Gives a short usage message. Prints the program version. OVERVIEW This program is the client part. It is a plugin started by plugin-runner 8mandos which will run in an initial RAM disk environment. This program could, theoretically, be used as a keyscript in /etc/crypttab, but it would then be impossible to enter a password for the encrypted root disk at the console, since this program does not read from the console at all. This is why a separate plugin runner ( plugin-runner 8mandos) is used to run both this program and others in in parallel, one of which ( password-prompt 8mandos) will prompt for passwords on the system console. EXIT STATUS This program will exit with a successful (zero) exit status if a server could be found and the password received from it could be successfully decrypted and output on standard output. The program will exit with a non-zero exit status only if a critical error occurs. Otherwise, it will forever connect to any discovered Mandos servers, trying to get a decryptable password and print it. ENVIRONMENT This program does not use any environment variables, not even the ones provided by cryptsetup8 . NETWORK HOOKS If a network interface like a bridge or tunnel is required to find a Mandos server, this requires the interface to be up and running before &COMMANDNAME; starts looking for Mandos servers. This can be accomplished by creating a network hook program, and placing it in a special directory. Before the network is used (and again before program exit), any runnable programs found in the network hook directory are run with the argument start or stop. This should bring up or down, respectively, any network interface which &COMMANDNAME; should use. REQUIREMENTS A network hook must be an executable file, and its name must consist entirely of upper and lower case letters, digits, underscores, periods, and hyphens. A network hook will receive one argument, which can be one of the following: start This should make the network hook create (if necessary) and bring up a network interface. stop This should make the network hook take down a network interface, and delete it if it did not exist previously. files This should make the network hook print, one file per line, all the files needed for it to run. (These files will be copied into the initial RAM filesystem.) Typical use is for a network hook which is a shell script to print its needed binaries. It is not necessary to print any non-executable files already in the network hook directory, these will be copied implicitly if they otherwise satisfy the name requirement. modules This should make the network hook print, on separate lines, all the kernel modules needed for it to run. (These modules will be copied into the initial RAM filesystem.) For instance, a tunnel interface needs the tun module. The network hook will be provided with a number of environment variables: MANDOSNETHOOKDIR The network hook directory, specified to &COMMANDNAME; by the option. Note: this should always be used by the network hook to refer to itself or any files in the hook directory it may require. DEVICE The network interfaces, as specified to &COMMANDNAME; by the option, combined to one string and separated by commas. If this is set, and does not contain the interface a hook will bring up, there is no reason for a hook to continue. MODE This will be the same as the first argument; i.e. start, stop, files, or modules. VERBOSITY This will be the 1 if the option is passed to &COMMANDNAME;, otherwise 0. DELAY This will be the same as the option passed to &COMMANDNAME;. Is only set if MODE is start or stop. CONNECT This will be the same as the option passed to &COMMANDNAME;. Is only set if is passed and MODE is start or stop. A hook may not read from standard input, and should be restrictive in printing to standard output or standard error unless VERBOSITY is 1. FILES /conf/conf.d/mandos/pubkey.txt /conf/conf.d/mandos/seckey.txt OpenPGP public and private key files, in ASCII Armor format. These are the default file names, they can be changed with the and options. /lib/mandos/network-hooks.d Directory where network hooks are located. Change this with the option. See . EXAMPLE Note that normally, command line options will not be given directly, but via options for the Mandos plugin-runner 8mandos. Normal invocation needs no options, if the network interface can be automatically determined: &COMMANDNAME; Search for Mandos servers (and connect to them) using another interface: &COMMANDNAME; --interface eth1 Run in debug mode, and use a custom key: &COMMANDNAME; --debug --pubkey keydir/pubkey.txt --seckey keydir/seckey.txt Run in debug mode, with a custom key, and do not use Zeroconf to locate a server; connect directly to the IPv6 link-local address fe80::aede:48ff:fe71:f6f2, port 4711, using interface eth2: &COMMANDNAME; --debug --pubkey keydir/pubkey.txt --seckey keydir/seckey.txt --connect fe80::aede:48ff:fe71:f6f2:4711 --interface eth2 SECURITY This program is set-uid to root, but will switch back to the original (and presumably non-privileged) user and group after bringing up the network interface. To use this program for its intended purpose (see ), the password for the root file system will have to be given out to be stored in a server computer, after having been encrypted using an OpenPGP key. This encrypted data which will be stored in a server can only be decrypted by the OpenPGP key, and the data will only be given out to those clients who can prove they actually have that key. This key, however, is stored unencrypted on the client side in its initial RAM disk image file system. This is normally readable by all, but this is normally fixed during installation of this program; file permissions are set so that no-one is able to read that file. The only remaining weak point is that someone with physical access to the client hard drive might turn off the client computer, read the OpenPGP keys directly from the hard drive, and communicate with the server. To safeguard against this, the server is supposed to notice the client disappearing and stop giving out the encrypted data. Therefore, it is important to set the timeout and checker interval values tightly on the server. See mandos8. It will also help if the checker program on the server is configured to request something from the client which can not be spoofed by someone else on the network, unlike unencrypted ICMP echo (ping) replies. Note: This makes it completely insecure to have Mandos clients which dual-boot to another operating system which is not trusted to keep the initial RAM disk image confidential. SEE ALSO intro 8mandos, cryptsetup 8, crypttab 5, mandos 8, password-prompt 8mandos, plugin-runner 8mandos Zeroconf Zeroconf is the network protocol standard used for finding Mandos servers on the local network. Avahi Avahi is the library this program calls to find Zeroconf services. GnuTLS GnuTLS is the library this client uses to implement TLS for communicating securely with the server, and at the same time send the public OpenPGP key to the server. GPGME GPGME is the library used to decrypt the OpenPGP data sent by the server. RFC 4291: IP Version 6 Addressing Architecture Section 2.2: Text Representation of Addresses Section 2.5.5.2: IPv4-Mapped IPv6 Address Section 2.5.6, Link-Local IPv6 Unicast Addresses This client uses IPv6 link-local addresses, which are immediately usable since a link-local addresses is automatically assigned to a network interfaces when it is brought up. RFC 4346: The Transport Layer Security (TLS) Protocol Version 1.1 TLS 1.1 is the protocol implemented by GnuTLS. RFC 4880: OpenPGP Message Format The data received from the server is binary encrypted OpenPGP data. RFC 5081: Using OpenPGP Keys for Transport Layer Security This is implemented by GnuTLS and used by this program so that OpenPGP keys can be used.
mandos-1.6.0/mandos-options.xml0000664000175000017500000001067711767455003016764 0ustar teddyteddy00000000000000
<para id="interface"> If this is specified, the server will only announce the service and listen to requests on the specified network interface. Default is to use all available interfaces. <emphasis >Note:</emphasis> a failure to bind to the specified interface is not considered critical, and the server will not exit, but instead continue normally. </para> <para id="address"> If this option is used, the server will only listen to the specified IPv6 address. If a link-local address is specified, an interface should be set, since a link-local address is only valid on a single interface. By default, the server will listen to all available addresses. If set, this must normally be an IPv6 address; an IPv4 address can only be specified using IPv4-mapped IPv6 address syntax: <quote><systemitem class="ipaddress" >::FFFF:192.0.2.3</systemitem ></quote>. (Only if IPv6 usage is <emphasis>disabled</emphasis> (see below) must this be an IPv4 address.) </para> <para id="port"> If this option is used, the server will bind to that port. By default, the server will listen to an arbitrary port given by the operating system. </para> <para id="debug"> If the server is run in debug mode, it will run in the foreground and print a lot of debugging information. The default is to <emphasis>not</emphasis> run in debug mode. </para> <para id="priority"> GnuTLS priority string for the <acronym>TLS</acronym> handshake. The default is <quote><literal >SECURE256:!CTYPE-X.509:+CTYPE-OPENPGP</literal></quote>. See <citerefentry><refentrytitle>gnutls_priority_init</refentrytitle> <manvolnum>3</manvolnum></citerefentry> for the syntax. <emphasis>Warning</emphasis>: changing this may make the <acronym>TLS</acronym> handshake fail, making server-client communication impossible. </para> <para id="servicename"> Zeroconf service name. The default is <quote><literal>Mandos</literal></quote>. This only needs to be changed if for some reason is would be necessary to run more than one server on the same <emphasis>host</emphasis>. This would not normally be useful. If there are name collisions on the same <emphasis>network</emphasis>, the newer server will automatically rename itself to <quote><literal>Mandos #2</literal></quote>, and so on; therefore, this option is not needed in that case. </para> <para id="dbus"> This option controls whether the server will provide a D-Bus system bus interface. The default is to provide such an interface. </para> <para id="ipv6"> This option controls whether the server will use IPv6 sockets and addresses. The default is to use IPv6. This option should <emphasis>never</emphasis> normally be turned off, <emphasis>even in IPv4-only environments</emphasis>. This is because <citerefentry> <refentrytitle>mandos-client</refentrytitle> <manvolnum>8mandos</manvolnum></citerefentry> will normally use IPv6 link-local addresses, and will not be able to find or connect to the server if this option is turned off. <emphasis>Only advanced users should consider changing this option</emphasis>. </para> <para id="restore"> This option controls whether the server will restore its state from the last time it ran. Default is to restore last state. </para> <para id="statedir"> Directory to save (and restore) state in. Default is <quote><filename class="directory">/var/lib/mandos</filename></quote>. </para> <para id="socket"> If this option is used, the server will not create a new network socket, but will instead use the supplied file descriptor. By default, the server will create a new network socket. </para> <para id="foreground"> This option will make the server run in the foreground and not write a PID file. The default is to <emphasis>not</emphasis> run in the foreground, except in <option>debug</option> mode, which implies this option. </para> </section> �����������������������������������������������������������������mandos-1.6.0/dbus-mandos.conf�����������������������������������������������������������������������0000664�0001750�0001750�00000001464�11767455003�016345� 0����������������������������������������������������������������������������������������������������ustar �teddy���������������������������teddy���������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������<!DOCTYPE busconfig PUBLIC "-//freedesktop//DTD D-BUS Bus Configuration 1.0//EN" "http://www.freedesktop.org/standards/dbus/1.0/busconfig.dtd"> <busconfig> <policy group="_mandos"> <allow own="se.bsnet.fukt.Mandos"/> <allow send_destination="se.bsnet.fukt.Mandos"/> <allow receive_sender="se.bsnet.fukt.Mandos"/> <allow own="se.recompile.Mandos"/> <allow send_destination="se.recompile.Mandos"/> <allow receive_sender="se.recompile.Mandos"/> </policy> <policy user="root"> <allow own="se.bsnet.fukt.Mandos"/> <allow send_destination="se.bsnet.fukt.Mandos"/> <allow receive_sender="se.bsnet.fukt.Mandos"/> <allow own="se.recompile.Mandos"/> <allow send_destination="se.recompile.Mandos"/> <allow receive_sender="se.recompile.Mandos"/> </policy> </busconfig> ������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������mandos-1.6.0/mandos���������������������������������������������������������������������������������0000775�0001750�0001750�00000335456�11767455003�014504� 0����������������������������������������������������������������������������������������������������ustar �teddy���������������������������teddy���������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������#!/usr/bin/python # -*- mode: python; coding: utf-8 -*- # # Mandos server - give out binary blobs to connecting clients. # # This program is partly derived from an example program for an Avahi # service publisher, downloaded from # <http://avahi.org/wiki/PythonPublishExample>. This includes the # methods "add", "remove", "server_state_changed", # "entry_group_state_changed", "cleanup", and "activate" in the # "AvahiService" class, and some lines in "main". # # Everything else is # Copyright © 2008-2012 Teddy Hogeborn # Copyright © 2008-2012 Björn Påhlsson # # 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 of the License, 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/>. # # Contact the authors at <mandos@recompile.se>. # from __future__ import (division, absolute_import, print_function, unicode_literals) from future_builtins import * import SocketServer as socketserver import socket import argparse import datetime import errno import gnutls.crypto import gnutls.connection import gnutls.errors import gnutls.library.functions import gnutls.library.constants import gnutls.library.types import ConfigParser as configparser import sys import re import os import signal import subprocess import atexit import stat import logging import logging.handlers import pwd import contextlib import struct import fcntl import functools import cPickle as pickle import multiprocessing import types import binascii import tempfile import itertools import dbus import dbus.service import gobject import avahi from dbus.mainloop.glib import DBusGMainLoop import ctypes import ctypes.util import xml.dom.minidom import inspect import GnuPGInterface try: SO_BINDTODEVICE = socket.SO_BINDTODEVICE except AttributeError: try: from IN import SO_BINDTODEVICE except ImportError: SO_BINDTODEVICE = None version = "1.6.0" stored_state_file = "clients.pickle" logger = logging.getLogger() syslogger = (logging.handlers.SysLogHandler (facility = logging.handlers.SysLogHandler.LOG_DAEMON, address = str("/dev/log"))) try: if_nametoindex = (ctypes.cdll.LoadLibrary (ctypes.util.find_library("c")) .if_nametoindex) except (OSError, AttributeError): def if_nametoindex(interface): "Get an interface index the hard way, i.e. using fcntl()" SIOCGIFINDEX = 0x8933 # From /usr/include/linux/sockios.h with contextlib.closing(socket.socket()) as s: ifreq = fcntl.ioctl(s, SIOCGIFINDEX, struct.pack(str("16s16x"), interface)) interface_index = struct.unpack(str("I"), ifreq[16:20])[0] return interface_index def initlogger(debug, level=logging.WARNING): """init logger and add loglevel""" syslogger.setFormatter(logging.Formatter ('Mandos [%(process)d]: %(levelname)s:' ' %(message)s')) logger.addHandler(syslogger) if debug: console = logging.StreamHandler() console.setFormatter(logging.Formatter('%(asctime)s %(name)s' ' [%(process)d]:' ' %(levelname)s:' ' %(message)s')) logger.addHandler(console) logger.setLevel(level) class PGPError(Exception): """Exception if encryption/decryption fails""" pass class PGPEngine(object): """A simple class for OpenPGP symmetric encryption & decryption""" def __init__(self): self.gnupg = GnuPGInterface.GnuPG() self.tempdir = tempfile.mkdtemp(prefix="mandos-") self.gnupg = GnuPGInterface.GnuPG() self.gnupg.options.meta_interactive = False self.gnupg.options.homedir = self.tempdir self.gnupg.options.extra_args.extend(['--force-mdc', '--quiet', '--no-use-agent']) def __enter__(self): return self def __exit__(self, exc_type, exc_value, traceback): self._cleanup() return False def __del__(self): self._cleanup() def _cleanup(self): if self.tempdir is not None: # Delete contents of tempdir for root, dirs, files in os.walk(self.tempdir, topdown = False): for filename in files: os.remove(os.path.join(root, filename)) for dirname in dirs: os.rmdir(os.path.join(root, dirname)) # Remove tempdir os.rmdir(self.tempdir) self.tempdir = None def password_encode(self, password): # Passphrase can not be empty and can not contain newlines or # NUL bytes. So we prefix it and hex encode it. return b"mandos" + binascii.hexlify(password) def encrypt(self, data, password): self.gnupg.passphrase = self.password_encode(password) with open(os.devnull, "w") as devnull: try: proc = self.gnupg.run(['--symmetric'], create_fhs=['stdin', 'stdout'], attach_fhs={'stderr': devnull}) with contextlib.closing(proc.handles['stdin']) as f: f.write(data) with contextlib.closing(proc.handles['stdout']) as f: ciphertext = f.read() proc.wait() except IOError as e: raise PGPError(e) self.gnupg.passphrase = None return ciphertext def decrypt(self, data, password): self.gnupg.passphrase = self.password_encode(password) with open(os.devnull, "w") as devnull: try: proc = self.gnupg.run(['--decrypt'], create_fhs=['stdin', 'stdout'], attach_fhs={'stderr': devnull}) with contextlib.closing(proc.handles['stdin']) as f: f.write(data) with contextlib.closing(proc.handles['stdout']) as f: decrypted_plaintext = f.read() proc.wait() except IOError as e: raise PGPError(e) self.gnupg.passphrase = None return decrypted_plaintext class AvahiError(Exception): def __init__(self, value, *args, **kwargs): self.value = value super(AvahiError, self).__init__(value, *args, **kwargs) def __unicode__(self): return unicode(repr(self.value)) class AvahiServiceError(AvahiError): pass class AvahiGroupError(AvahiError): pass class AvahiService(object): """An Avahi (Zeroconf) service. Attributes: interface: integer; avahi.IF_UNSPEC or an interface index. Used to optionally bind to the specified interface. name: string; Example: 'Mandos' type: string; Example: '_mandos._tcp'. See <http://www.dns-sd.org/ServiceTypes.html> port: integer; what port to announce TXT: list of strings; TXT record for the service domain: string; Domain to publish on, default to .local if empty. host: string; Host to publish records for, default is localhost max_renames: integer; maximum number of renames rename_count: integer; counter so we only rename after collisions a sensible number of times group: D-Bus Entry Group server: D-Bus Server bus: dbus.SystemBus() """ def __init__(self, interface = avahi.IF_UNSPEC, name = None, servicetype = None, port = None, TXT = None, domain = "", host = "", max_renames = 32768, protocol = avahi.PROTO_UNSPEC, bus = None): self.interface = interface self.name = name self.type = servicetype self.port = port self.TXT = TXT if TXT is not None else [] self.domain = domain self.host = host self.rename_count = 0 self.max_renames = max_renames self.protocol = protocol self.group = None # our entry group self.server = None self.bus = bus self.entry_group_state_changed_match = None def rename(self): """Derived from the Avahi example code""" if self.rename_count >= self.max_renames: logger.critical("No suitable Zeroconf service name found" " after %i retries, exiting.", self.rename_count) raise AvahiServiceError("Too many renames") self.name = unicode(self.server .GetAlternativeServiceName(self.name)) logger.info("Changing Zeroconf service name to %r ...", self.name) self.remove() try: self.add() except dbus.exceptions.DBusException as error: logger.critical("D-Bus Exception", exc_info=error) self.cleanup() os._exit(1) self.rename_count += 1 def remove(self): """Derived from the Avahi example code""" if self.entry_group_state_changed_match is not None: self.entry_group_state_changed_match.remove() self.entry_group_state_changed_match = None if self.group is not None: self.group.Reset() def add(self): """Derived from the Avahi example code""" self.remove() if self.group is None: self.group = dbus.Interface( self.bus.get_object(avahi.DBUS_NAME, self.server.EntryGroupNew()), avahi.DBUS_INTERFACE_ENTRY_GROUP) self.entry_group_state_changed_match = ( self.group.connect_to_signal( 'StateChanged', self.entry_group_state_changed)) logger.debug("Adding Zeroconf service '%s' of type '%s' ...", self.name, self.type) self.group.AddService( self.interface, self.protocol, dbus.UInt32(0), # flags self.name, self.type, self.domain, self.host, dbus.UInt16(self.port), avahi.string_array_to_txt_array(self.TXT)) self.group.Commit() def entry_group_state_changed(self, state, error): """Derived from the Avahi example code""" logger.debug("Avahi entry group state change: %i", state) if state == avahi.ENTRY_GROUP_ESTABLISHED: logger.debug("Zeroconf service established.") elif state == avahi.ENTRY_GROUP_COLLISION: logger.info("Zeroconf service name collision.") self.rename() elif state == avahi.ENTRY_GROUP_FAILURE: logger.critical("Avahi: Error in group state changed %s", unicode(error)) raise AvahiGroupError("State changed: {0!s}" .format(error)) def cleanup(self): """Derived from the Avahi example code""" if self.group is not None: try: self.group.Free() except (dbus.exceptions.UnknownMethodException, dbus.exceptions.DBusException): pass self.group = None self.remove() def server_state_changed(self, state, error=None): """Derived from the Avahi example code""" logger.debug("Avahi server state change: %i", state) bad_states = { avahi.SERVER_INVALID: "Zeroconf server invalid", avahi.SERVER_REGISTERING: None, avahi.SERVER_COLLISION: "Zeroconf server name collision", avahi.SERVER_FAILURE: "Zeroconf server failure" } if state in bad_states: if bad_states[state] is not None: if error is None: logger.error(bad_states[state]) else: logger.error(bad_states[state] + ": %r", error) self.cleanup() elif state == avahi.SERVER_RUNNING: self.add() else: if error is None: logger.debug("Unknown state: %r", state) else: logger.debug("Unknown state: %r: %r", state, error) def activate(self): """Derived from the Avahi example code""" if self.server is None: self.server = dbus.Interface( self.bus.get_object(avahi.DBUS_NAME, avahi.DBUS_PATH_SERVER, follow_name_owner_changes=True), avahi.DBUS_INTERFACE_SERVER) self.server.connect_to_signal("StateChanged", self.server_state_changed) self.server_state_changed(self.server.GetState()) class AvahiServiceToSyslog(AvahiService): def rename(self): """Add the new name to the syslog messages""" ret = AvahiService.rename(self) syslogger.setFormatter(logging.Formatter ('Mandos ({0}) [%(process)d]:' ' %(levelname)s: %(message)s' .format(self.name))) return ret def timedelta_to_milliseconds(td): "Convert a datetime.timedelta() to milliseconds" return ((td.days * 24 * 60 * 60 * 1000) + (td.seconds * 1000) + (td.microseconds // 1000)) class Client(object): """A representation of a client host served by this server. Attributes: approved: bool(); 'None' if not yet approved/disapproved approval_delay: datetime.timedelta(); Time to wait for approval approval_duration: datetime.timedelta(); Duration of one approval checker: subprocess.Popen(); a running checker process used to see if the client lives. 'None' if no process is running. checker_callback_tag: a gobject event source tag, or None checker_command: string; External command which is run to check if client lives. %() expansions are done at runtime with vars(self) as dict, so that for instance %(name)s can be used in the command. checker_initiator_tag: a gobject event source tag, or None created: datetime.datetime(); (UTC) object creation client_structure: Object describing what attributes a client has and is used for storing the client at exit current_checker_command: string; current running checker_command disable_initiator_tag: a gobject event source tag, or None enabled: bool() fingerprint: string (40 or 32 hexadecimal digits); used to uniquely identify the client host: string; available for use by the checker command interval: datetime.timedelta(); How often to start a new checker last_approval_request: datetime.datetime(); (UTC) or None last_checked_ok: datetime.datetime(); (UTC) or None last_checker_status: integer between 0 and 255 reflecting exit status of last checker. -1 reflects crashed checker, -2 means no checker completed yet. last_enabled: datetime.datetime(); (UTC) or None name: string; from the config file, used in log messages and D-Bus identifiers secret: bytestring; sent verbatim (over TLS) to client timeout: datetime.timedelta(); How long from last_checked_ok until this client is disabled extended_timeout: extra long timeout when secret has been sent runtime_expansions: Allowed attributes for runtime expansion. expires: datetime.datetime(); time (UTC) when a client will be disabled, or None """ runtime_expansions = ("approval_delay", "approval_duration", "created", "enabled", "expires", "fingerprint", "host", "interval", "last_approval_request", "last_checked_ok", "last_enabled", "name", "timeout") client_defaults = { "timeout": "5m", "extended_timeout": "15m", "interval": "2m", "checker": "fping -q -- %%(host)s", "host": "", "approval_delay": "0s", "approval_duration": "1s", "approved_by_default": "True", "enabled": "True", } def timeout_milliseconds(self): "Return the 'timeout' attribute in milliseconds" return timedelta_to_milliseconds(self.timeout) def extended_timeout_milliseconds(self): "Return the 'extended_timeout' attribute in milliseconds" return timedelta_to_milliseconds(self.extended_timeout) def interval_milliseconds(self): "Return the 'interval' attribute in milliseconds" return timedelta_to_milliseconds(self.interval) def approval_delay_milliseconds(self): return timedelta_to_milliseconds(self.approval_delay) @staticmethod def config_parser(config): """Construct a new dict of client settings of this form: { client_name: {setting_name: value, ...}, ...} with exceptions for any special settings as defined above. NOTE: Must be a pure function. Must return the same result value given the same arguments. """ settings = {} for client_name in config.sections(): section = dict(config.items(client_name)) client = settings[client_name] = {} client["host"] = section["host"] # Reformat values from string types to Python types client["approved_by_default"] = config.getboolean( client_name, "approved_by_default") client["enabled"] = config.getboolean(client_name, "enabled") client["fingerprint"] = (section["fingerprint"].upper() .replace(" ", "")) if "secret" in section: client["secret"] = section["secret"].decode("base64") elif "secfile" in section: with open(os.path.expanduser(os.path.expandvars (section["secfile"])), "rb") as secfile: client["secret"] = secfile.read() else: raise TypeError("No secret or secfile for section {0}" .format(section)) client["timeout"] = string_to_delta(section["timeout"]) client["extended_timeout"] = string_to_delta( section["extended_timeout"]) client["interval"] = string_to_delta(section["interval"]) client["approval_delay"] = string_to_delta( section["approval_delay"]) client["approval_duration"] = string_to_delta( section["approval_duration"]) client["checker_command"] = section["checker"] client["last_approval_request"] = None client["last_checked_ok"] = None client["last_checker_status"] = -2 return settings def __init__(self, settings, name = None): self.name = name # adding all client settings for setting, value in settings.iteritems(): setattr(self, setting, value) if self.enabled: if not hasattr(self, "last_enabled"): self.last_enabled = datetime.datetime.utcnow() if not hasattr(self, "expires"): self.expires = (datetime.datetime.utcnow() + self.timeout) else: self.last_enabled = None self.expires = None logger.debug("Creating client %r", self.name) # Uppercase and remove spaces from fingerprint for later # comparison purposes with return value from the fingerprint() # function logger.debug(" Fingerprint: %s", self.fingerprint) self.created = settings.get("created", datetime.datetime.utcnow()) # attributes specific for this server instance self.checker = None self.checker_initiator_tag = None self.disable_initiator_tag = None self.checker_callback_tag = None self.current_checker_command = None self.approved = None self.approvals_pending = 0 self.changedstate = (multiprocessing_manager .Condition(multiprocessing_manager .Lock())) self.client_structure = [attr for attr in self.__dict__.iterkeys() if not attr.startswith("_")] self.client_structure.append("client_structure") for name, t in inspect.getmembers(type(self), lambda obj: isinstance(obj, property)): if not name.startswith("_"): self.client_structure.append(name) # Send notice to process children that client state has changed def send_changedstate(self): with self.changedstate: self.changedstate.notify_all() def enable(self): """Start this client's checker and timeout hooks""" if getattr(self, "enabled", False): # Already enabled return self.expires = datetime.datetime.utcnow() + self.timeout self.enabled = True self.last_enabled = datetime.datetime.utcnow() self.init_checker() self.send_changedstate() def disable(self, quiet=True): """Disable this client.""" if not getattr(self, "enabled", False): return False if not quiet: logger.info("Disabling client %s", self.name) if getattr(self, "disable_initiator_tag", None) is not None: gobject.source_remove(self.disable_initiator_tag) self.disable_initiator_tag = None self.expires = None if getattr(self, "checker_initiator_tag", None) is not None: gobject.source_remove(self.checker_initiator_tag) self.checker_initiator_tag = None self.stop_checker() self.enabled = False if not quiet: self.send_changedstate() # Do not run this again if called by a gobject.timeout_add return False def __del__(self): self.disable() def init_checker(self): # Schedule a new checker to be started an 'interval' from now, # and every interval from then on. if self.checker_initiator_tag is not None: gobject.source_remove(self.checker_initiator_tag) self.checker_initiator_tag = (gobject.timeout_add (self.interval_milliseconds(), self.start_checker)) # Schedule a disable() when 'timeout' has passed if self.disable_initiator_tag is not None: gobject.source_remove(self.disable_initiator_tag) self.disable_initiator_tag = (gobject.timeout_add (self.timeout_milliseconds(), self.disable)) # Also start a new checker *right now*. self.start_checker() def checker_callback(self, pid, condition, command): """The checker has completed, so take appropriate actions.""" self.checker_callback_tag = None self.checker = None if os.WIFEXITED(condition): self.last_checker_status = os.WEXITSTATUS(condition) if self.last_checker_status == 0: logger.info("Checker for %(name)s succeeded", vars(self)) self.checked_ok() else: logger.info("Checker for %(name)s failed", vars(self)) else: self.last_checker_status = -1 logger.warning("Checker for %(name)s crashed?", vars(self)) def checked_ok(self): """Assert that the client has been seen, alive and well.""" self.last_checked_ok = datetime.datetime.utcnow() self.last_checker_status = 0 self.bump_timeout() def bump_timeout(self, timeout=None): """Bump up the timeout for this client.""" if timeout is None: timeout = self.timeout if self.disable_initiator_tag is not None: gobject.source_remove(self.disable_initiator_tag) self.disable_initiator_tag = None if getattr(self, "enabled", False): self.disable_initiator_tag = (gobject.timeout_add (timedelta_to_milliseconds (timeout), self.disable)) self.expires = datetime.datetime.utcnow() + timeout def need_approval(self): self.last_approval_request = datetime.datetime.utcnow() def start_checker(self): """Start a new checker subprocess if one is not running. If a checker already exists, leave it running and do nothing.""" # The reason for not killing a running checker is that if we # did that, and if a checker (for some reason) started running # slowly and taking more than 'interval' time, then the client # would inevitably timeout, since no checker would get a # chance to run to completion. If we instead leave running # checkers alone, the checker would have to take more time # than 'timeout' for the client to be disabled, which is as it # should be. # If a checker exists, make sure it is not a zombie try: pid, status = os.waitpid(self.checker.pid, os.WNOHANG) except (AttributeError, OSError) as error: if (isinstance(error, OSError) and error.errno != errno.ECHILD): raise error else: if pid: logger.warning("Checker was a zombie") gobject.source_remove(self.checker_callback_tag) self.checker_callback(pid, status, self.current_checker_command) # Start a new checker if needed if self.checker is None: # Escape attributes for the shell escaped_attrs = dict( (attr, re.escape(unicode(getattr(self, attr)))) for attr in self.runtime_expansions) try: command = self.checker_command % escaped_attrs except TypeError as error: logger.error('Could not format string "%s"', self.checker_command, exc_info=error) return True # Try again later self.current_checker_command = command try: logger.info("Starting checker %r for %s", command, self.name) # We don't need to redirect stdout and stderr, since # in normal mode, that is already done by daemon(), # and in debug mode we don't want to. (Stdin is # always replaced by /dev/null.) self.checker = subprocess.Popen(command, close_fds=True, shell=True, cwd="/") except OSError as error: logger.error("Failed to start subprocess", exc_info=error) self.checker_callback_tag = (gobject.child_watch_add (self.checker.pid, self.checker_callback, data=command)) # The checker may have completed before the gobject # watch was added. Check for this. pid, status = os.waitpid(self.checker.pid, os.WNOHANG) if pid: gobject.source_remove(self.checker_callback_tag) self.checker_callback(pid, status, command) # Re-run this periodically if run by gobject.timeout_add return True def stop_checker(self): """Force the checker process, if any, to stop.""" if self.checker_callback_tag: gobject.source_remove(self.checker_callback_tag) self.checker_callback_tag = None if getattr(self, "checker", None) is None: return logger.debug("Stopping checker for %(name)s", vars(self)) try: self.checker.terminate() #time.sleep(0.5) #if self.checker.poll() is None: # self.checker.kill() except OSError as error: if error.errno != errno.ESRCH: # No such process raise self.checker = None def dbus_service_property(dbus_interface, signature="v", access="readwrite", byte_arrays=False): """Decorators for marking methods of a DBusObjectWithProperties to become properties on the D-Bus. The decorated method will be called with no arguments by "Get" and with one argument by "Set". The parameters, where they are supported, are the same as dbus.service.method, except there is only "signature", since the type from Get() and the type sent to Set() is the same. """ # Encoding deeply encoded byte arrays is not supported yet by the # "Set" method, so we fail early here: if byte_arrays and signature != "ay": raise ValueError("Byte arrays not supported for non-'ay'" " signature {0!r}".format(signature)) def decorator(func): func._dbus_is_property = True func._dbus_interface = dbus_interface func._dbus_signature = signature func._dbus_access = access func._dbus_name = func.__name__ if func._dbus_name.endswith("_dbus_property"): func._dbus_name = func._dbus_name[:-14] func._dbus_get_args_options = {'byte_arrays': byte_arrays } return func return decorator def dbus_interface_annotations(dbus_interface): """Decorator for marking functions returning interface annotations Usage: @dbus_interface_annotations("org.example.Interface") def _foo(self): # Function name does not matter return {"org.freedesktop.DBus.Deprecated": "true", "org.freedesktop.DBus.Property.EmitsChangedSignal": "false"} """ def decorator(func): func._dbus_is_interface = True func._dbus_interface = dbus_interface func._dbus_name = dbus_interface return func return decorator def dbus_annotations(annotations): """Decorator to annotate D-Bus methods, signals or properties Usage: @dbus_service_property("org.example.Interface", signature="b", access="r") @dbus_annotations({{"org.freedesktop.DBus.Deprecated": "true", "org.freedesktop.DBus.Property." "EmitsChangedSignal": "false"}) def Property_dbus_property(self): return dbus.Boolean(False) """ def decorator(func): func._dbus_annotations = annotations return func return decorator class DBusPropertyException(dbus.exceptions.DBusException): """A base class for D-Bus property-related exceptions """ def __unicode__(self): return unicode(str(self)) class DBusPropertyAccessException(DBusPropertyException): """A property's access permissions disallows an operation. """ pass class DBusPropertyNotFound(DBusPropertyException): """An attempt was made to access a non-existing property. """ pass class DBusObjectWithProperties(dbus.service.Object): """A D-Bus object with properties. Classes inheriting from this can use the dbus_service_property decorator to expose methods as D-Bus properties. It exposes the standard Get(), Set(), and GetAll() methods on the D-Bus. """ @staticmethod def _is_dbus_thing(thing): """Returns a function testing if an attribute is a D-Bus thing If called like _is_dbus_thing("method") it returns a function suitable for use as predicate to inspect.getmembers(). """ return lambda obj: getattr(obj, "_dbus_is_{0}".format(thing), False) def _get_all_dbus_things(self, thing): """Returns a generator of (name, attribute) pairs """ return ((getattr(athing.__get__(self), "_dbus_name", name), athing.__get__(self)) for cls in self.__class__.__mro__ for name, athing in inspect.getmembers(cls, self._is_dbus_thing(thing))) def _get_dbus_property(self, interface_name, property_name): """Returns a bound method if one exists which is a D-Bus property with the specified name and interface. """ for cls in self.__class__.__mro__: for name, value in (inspect.getmembers (cls, self._is_dbus_thing("property"))): if (value._dbus_name == property_name and value._dbus_interface == interface_name): return value.__get__(self) # No such property raise DBusPropertyNotFound(self.dbus_object_path + ":" + interface_name + "." + property_name) @dbus.service.method(dbus.PROPERTIES_IFACE, in_signature="ss", out_signature="v") def Get(self, interface_name, property_name): """Standard D-Bus property Get() method, see D-Bus standard. """ prop = self._get_dbus_property(interface_name, property_name) if prop._dbus_access == "write": raise DBusPropertyAccessException(property_name) value = prop() if not hasattr(value, "variant_level"): return value return type(value)(value, variant_level=value.variant_level+1) @dbus.service.method(dbus.PROPERTIES_IFACE, in_signature="ssv") def Set(self, interface_name, property_name, value): """Standard D-Bus property Set() method, see D-Bus standard. """ prop = self._get_dbus_property(interface_name, property_name) if prop._dbus_access == "read": raise DBusPropertyAccessException(property_name) if prop._dbus_get_args_options["byte_arrays"]: # The byte_arrays option is not supported yet on # signatures other than "ay". if prop._dbus_signature != "ay": raise ValueError value = dbus.ByteArray(b''.join(chr(byte) for byte in value)) prop(value) @dbus.service.method(dbus.PROPERTIES_IFACE, in_signature="s", out_signature="a{sv}") def GetAll(self, interface_name): """Standard D-Bus property GetAll() method, see D-Bus standard. Note: Will not include properties with access="write". """ properties = {} for name, prop in self._get_all_dbus_things("property"): if (interface_name and interface_name != prop._dbus_interface): # Interface non-empty but did not match continue # Ignore write-only properties if prop._dbus_access == "write": continue value = prop() if not hasattr(value, "variant_level"): properties[name] = value continue properties[name] = type(value)(value, variant_level= value.variant_level+1) return dbus.Dictionary(properties, signature="sv") @dbus.service.method(dbus.INTROSPECTABLE_IFACE, out_signature="s", path_keyword='object_path', connection_keyword='connection') def Introspect(self, object_path, connection): """Overloading of standard D-Bus method. Inserts property tags and interface annotation tags. """ xmlstring = dbus.service.Object.Introspect(self, object_path, connection) try: document = xml.dom.minidom.parseString(xmlstring) def make_tag(document, name, prop): e = document.createElement("property") e.setAttribute("name", name) e.setAttribute("type", prop._dbus_signature) e.setAttribute("access", prop._dbus_access) return e for if_tag in document.getElementsByTagName("interface"): # Add property tags for tag in (make_tag(document, name, prop) for name, prop in self._get_all_dbus_things("property") if prop._dbus_interface == if_tag.getAttribute("name")): if_tag.appendChild(tag) # Add annotation tags for typ in ("method", "signal", "property"): for tag in if_tag.getElementsByTagName(typ): annots = dict() for name, prop in (self. _get_all_dbus_things(typ)): if (name == tag.getAttribute("name") and prop._dbus_interface == if_tag.getAttribute("name")): annots.update(getattr (prop, "_dbus_annotations", {})) for name, value in annots.iteritems(): ann_tag = document.createElement( "annotation") ann_tag.setAttribute("name", name) ann_tag.setAttribute("value", value) tag.appendChild(ann_tag) # Add interface annotation tags for annotation, value in dict( itertools.chain.from_iterable( annotations().iteritems() for name, annotations in self._get_all_dbus_things("interface") if name == if_tag.getAttribute("name") )).iteritems(): ann_tag = document.createElement("annotation") ann_tag.setAttribute("name", annotation) ann_tag.setAttribute("value", value) if_tag.appendChild(ann_tag) # Add the names to the return values for the # "org.freedesktop.DBus.Properties" methods if (if_tag.getAttribute("name") == "org.freedesktop.DBus.Properties"): for cn in if_tag.getElementsByTagName("method"): if cn.getAttribute("name") == "Get": for arg in cn.getElementsByTagName("arg"): if (arg.getAttribute("direction") == "out"): arg.setAttribute("name", "value") elif cn.getAttribute("name") == "GetAll": for arg in cn.getElementsByTagName("arg"): if (arg.getAttribute("direction") == "out"): arg.setAttribute("name", "props") xmlstring = document.toxml("utf-8") document.unlink() except (AttributeError, xml.dom.DOMException, xml.parsers.expat.ExpatError) as error: logger.error("Failed to override Introspection method", exc_info=error) return xmlstring def datetime_to_dbus(dt, variant_level=0): """Convert a UTC datetime.datetime() to a D-Bus type.""" if dt is None: return dbus.String("", variant_level = variant_level) return dbus.String(dt.isoformat(), variant_level=variant_level) def alternate_dbus_interfaces(alt_interface_names, deprecate=True): """A class decorator; applied to a subclass of dbus.service.Object, it will add alternate D-Bus attributes with interface names according to the "alt_interface_names" mapping. Usage: @alternate_dbus_interfaces({"org.example.Interface": "net.example.AlternateInterface"}) class SampleDBusObject(dbus.service.Object): @dbus.service.method("org.example.Interface") def SampleDBusMethod(): pass The above "SampleDBusMethod" on "SampleDBusObject" will be reachable via two interfaces: "org.example.Interface" and "net.example.AlternateInterface", the latter of which will have its D-Bus annotation "org.freedesktop.DBus.Deprecated" set to "true", unless "deprecate" is passed with a False value. This works for methods and signals, and also for D-Bus properties (from DBusObjectWithProperties) and interfaces (from the dbus_interface_annotations decorator). """ def wrapper(cls): for orig_interface_name, alt_interface_name in ( alt_interface_names.iteritems()): attr = {} interface_names = set() # Go though all attributes of the class for attrname, attribute in inspect.getmembers(cls): # Ignore non-D-Bus attributes, and D-Bus attributes # with the wrong interface name if (not hasattr(attribute, "_dbus_interface") or not attribute._dbus_interface .startswith(orig_interface_name)): continue # Create an alternate D-Bus interface name based on # the current name alt_interface = (attribute._dbus_interface .replace(orig_interface_name, alt_interface_name)) interface_names.add(alt_interface) # Is this a D-Bus signal? if getattr(attribute, "_dbus_is_signal", False): # Extract the original non-method function by # black magic nonmethod_func = (dict( zip(attribute.func_code.co_freevars, attribute.__closure__))["func"] .cell_contents) # Create a new, but exactly alike, function # object, and decorate it to be a new D-Bus signal # with the alternate D-Bus interface name new_function = (dbus.service.signal (alt_interface, attribute._dbus_signature) (types.FunctionType( nonmethod_func.func_code, nonmethod_func.func_globals, nonmethod_func.func_name, nonmethod_func.func_defaults, nonmethod_func.func_closure))) # Copy annotations, if any try: new_function._dbus_annotations = ( dict(attribute._dbus_annotations)) except AttributeError: pass # Define a creator of a function to call both the # original and alternate functions, so both the # original and alternate signals gets sent when # the function is called def fixscope(func1, func2): """This function is a scope container to pass func1 and func2 to the "call_both" function outside of its arguments""" def call_both(*args, **kwargs): """This function will emit two D-Bus signals by calling func1 and func2""" func1(*args, **kwargs) func2(*args, **kwargs) return call_both # Create the "call_both" function and add it to # the class attr[attrname] = fixscope(attribute, new_function) # Is this a D-Bus method? elif getattr(attribute, "_dbus_is_method", False): # Create a new, but exactly alike, function # object. Decorate it to be a new D-Bus method # with the alternate D-Bus interface name. Add it # to the class. attr[attrname] = (dbus.service.method (alt_interface, attribute._dbus_in_signature, attribute._dbus_out_signature) (types.FunctionType (attribute.func_code, attribute.func_globals, attribute.func_name, attribute.func_defaults, attribute.func_closure))) # Copy annotations, if any try: attr[attrname]._dbus_annotations = ( dict(attribute._dbus_annotations)) except AttributeError: pass # Is this a D-Bus property? elif getattr(attribute, "_dbus_is_property", False): # Create a new, but exactly alike, function # object, and decorate it to be a new D-Bus # property with the alternate D-Bus interface # name. Add it to the class. attr[attrname] = (dbus_service_property (alt_interface, attribute._dbus_signature, attribute._dbus_access, attribute ._dbus_get_args_options ["byte_arrays"]) (types.FunctionType (attribute.func_code, attribute.func_globals, attribute.func_name, attribute.func_defaults, attribute.func_closure))) # Copy annotations, if any try: attr[attrname]._dbus_annotations = ( dict(attribute._dbus_annotations)) except AttributeError: pass # Is this a D-Bus interface? elif getattr(attribute, "_dbus_is_interface", False): # Create a new, but exactly alike, function # object. Decorate it to be a new D-Bus interface # with the alternate D-Bus interface name. Add it # to the class. attr[attrname] = (dbus_interface_annotations (alt_interface) (types.FunctionType (attribute.func_code, attribute.func_globals, attribute.func_name, attribute.func_defaults, attribute.func_closure))) if deprecate: # Deprecate all alternate interfaces iname="_AlternateDBusNames_interface_annotation{0}" for interface_name in interface_names: @dbus_interface_annotations(interface_name) def func(self): return { "org.freedesktop.DBus.Deprecated": "true" } # Find an unused name for aname in (iname.format(i) for i in itertools.count()): if aname not in attr: attr[aname] = func break if interface_names: # Replace the class with a new subclass of it with # methods, signals, etc. as created above. cls = type(b"{0}Alternate".format(cls.__name__), (cls,), attr) return cls return wrapper @alternate_dbus_interfaces({"se.recompile.Mandos": "se.bsnet.fukt.Mandos"}) class ClientDBus(Client, DBusObjectWithProperties): """A Client class using D-Bus Attributes: dbus_object_path: dbus.ObjectPath bus: dbus.SystemBus() """ runtime_expansions = (Client.runtime_expansions + ("dbus_object_path",)) # dbus.service.Object doesn't use super(), so we can't either. def __init__(self, bus = None, *args, **kwargs): self.bus = bus Client.__init__(self, *args, **kwargs) # Only now, when this client is initialized, can it show up on # the D-Bus client_object_name = unicode(self.name).translate( {ord("."): ord("_"), ord("-"): ord("_")}) self.dbus_object_path = (dbus.ObjectPath ("/clients/" + client_object_name)) DBusObjectWithProperties.__init__(self, self.bus, self.dbus_object_path) def notifychangeproperty(transform_func, dbus_name, type_func=lambda x: x, variant_level=1): """ Modify a variable so that it's a property which announces its changes to DBus. transform_fun: Function that takes a value and a variant_level and transforms it to a D-Bus type. dbus_name: D-Bus name of the variable type_func: Function that transform the value before sending it to the D-Bus. Default: no transform variant_level: D-Bus variant level. Default: 1 """ attrname = "_{0}".format(dbus_name) def setter(self, value): if hasattr(self, "dbus_object_path"): if (not hasattr(self, attrname) or type_func(getattr(self, attrname, None)) != type_func(value)): dbus_value = transform_func(type_func(value), variant_level =variant_level) self.PropertyChanged(dbus.String(dbus_name), dbus_value) setattr(self, attrname, value) return property(lambda self: getattr(self, attrname), setter) expires = notifychangeproperty(datetime_to_dbus, "Expires") approvals_pending = notifychangeproperty(dbus.Boolean, "ApprovalPending", type_func = bool) enabled = notifychangeproperty(dbus.Boolean, "Enabled") last_enabled = notifychangeproperty(datetime_to_dbus, "LastEnabled") checker = notifychangeproperty(dbus.Boolean, "CheckerRunning", type_func = lambda checker: checker is not None) last_checked_ok = notifychangeproperty(datetime_to_dbus, "LastCheckedOK") last_checker_status = notifychangeproperty(dbus.Int16, "LastCheckerStatus") last_approval_request = notifychangeproperty( datetime_to_dbus, "LastApprovalRequest") approved_by_default = notifychangeproperty(dbus.Boolean, "ApprovedByDefault") approval_delay = notifychangeproperty(dbus.UInt64, "ApprovalDelay", type_func = timedelta_to_milliseconds) approval_duration = notifychangeproperty( dbus.UInt64, "ApprovalDuration", type_func = timedelta_to_milliseconds) host = notifychangeproperty(dbus.String, "Host") timeout = notifychangeproperty(dbus.UInt64, "Timeout", type_func = timedelta_to_milliseconds) extended_timeout = notifychangeproperty( dbus.UInt64, "ExtendedTimeout", type_func = timedelta_to_milliseconds) interval = notifychangeproperty(dbus.UInt64, "Interval", type_func = timedelta_to_milliseconds) checker_command = notifychangeproperty(dbus.String, "Checker") del notifychangeproperty def __del__(self, *args, **kwargs): try: self.remove_from_connection() except LookupError: pass if hasattr(DBusObjectWithProperties, "__del__"): DBusObjectWithProperties.__del__(self, *args, **kwargs) Client.__del__(self, *args, **kwargs) def checker_callback(self, pid, condition, command, *args, **kwargs): self.checker_callback_tag = None self.checker = None if os.WIFEXITED(condition): exitstatus = os.WEXITSTATUS(condition) # Emit D-Bus signal self.CheckerCompleted(dbus.Int16(exitstatus), dbus.Int64(condition), dbus.String(command)) else: # Emit D-Bus signal self.CheckerCompleted(dbus.Int16(-1), dbus.Int64(condition), dbus.String(command)) return Client.checker_callback(self, pid, condition, command, *args, **kwargs) def start_checker(self, *args, **kwargs): old_checker = self.checker if self.checker is not None: old_checker_pid = self.checker.pid else: old_checker_pid = None r = Client.start_checker(self, *args, **kwargs) # Only if new checker process was started if (self.checker is not None and old_checker_pid != self.checker.pid): # Emit D-Bus signal self.CheckerStarted(self.current_checker_command) return r def _reset_approved(self): self.approved = None return False def approve(self, value=True): self.approved = value gobject.timeout_add(timedelta_to_milliseconds (self.approval_duration), self._reset_approved) self.send_changedstate() ## D-Bus methods, signals & properties _interface = "se.recompile.Mandos.Client" ## Interfaces @dbus_interface_annotations(_interface) def _foo(self): return { "org.freedesktop.DBus.Property.EmitsChangedSignal": "false"} ## Signals # CheckerCompleted - signal @dbus.service.signal(_interface, signature="nxs") def CheckerCompleted(self, exitcode, waitstatus, command): "D-Bus signal" pass # CheckerStarted - signal @dbus.service.signal(_interface, signature="s") def CheckerStarted(self, command): "D-Bus signal" pass # PropertyChanged - signal @dbus.service.signal(_interface, signature="sv") def PropertyChanged(self, property, value): "D-Bus signal" pass # GotSecret - signal @dbus.service.signal(_interface) def GotSecret(self): """D-Bus signal Is sent after a successful transfer of secret from the Mandos server to mandos-client """ pass # Rejected - signal @dbus.service.signal(_interface, signature="s") def Rejected(self, reason): "D-Bus signal" pass # NeedApproval - signal @dbus.service.signal(_interface, signature="tb") def NeedApproval(self, timeout, default): "D-Bus signal" return self.need_approval() ## Methods # Approve - method @dbus.service.method(_interface, in_signature="b") def Approve(self, value): self.approve(value) # CheckedOK - method @dbus.service.method(_interface) def CheckedOK(self): self.checked_ok() # Enable - method @dbus.service.method(_interface) def Enable(self): "D-Bus method" self.enable() # StartChecker - method @dbus.service.method(_interface) def StartChecker(self): "D-Bus method" self.start_checker() # Disable - method @dbus.service.method(_interface) def Disable(self): "D-Bus method" self.disable() # StopChecker - method @dbus.service.method(_interface) def StopChecker(self): self.stop_checker() ## Properties # ApprovalPending - property @dbus_service_property(_interface, signature="b", access="read") def ApprovalPending_dbus_property(self): return dbus.Boolean(bool(self.approvals_pending)) # ApprovedByDefault - property @dbus_service_property(_interface, signature="b", access="readwrite") def ApprovedByDefault_dbus_property(self, value=None): if value is None: # get return dbus.Boolean(self.approved_by_default) self.approved_by_default = bool(value) # ApprovalDelay - property @dbus_service_property(_interface, signature="t", access="readwrite") def ApprovalDelay_dbus_property(self, value=None): if value is None: # get return dbus.UInt64(self.approval_delay_milliseconds()) self.approval_delay = datetime.timedelta(0, 0, 0, value) # ApprovalDuration - property @dbus_service_property(_interface, signature="t", access="readwrite") def ApprovalDuration_dbus_property(self, value=None): if value is None: # get return dbus.UInt64(timedelta_to_milliseconds( self.approval_duration)) self.approval_duration = datetime.timedelta(0, 0, 0, value) # Name - property @dbus_service_property(_interface, signature="s", access="read") def Name_dbus_property(self): return dbus.String(self.name) # Fingerprint - property @dbus_service_property(_interface, signature="s", access="read") def Fingerprint_dbus_property(self): return dbus.String(self.fingerprint) # Host - property @dbus_service_property(_interface, signature="s", access="readwrite") def Host_dbus_property(self, value=None): if value is None: # get return dbus.String(self.host) self.host = unicode(value) # Created - property @dbus_service_property(_interface, signature="s", access="read") def Created_dbus_property(self): return datetime_to_dbus(self.created) # LastEnabled - property @dbus_service_property(_interface, signature="s", access="read") def LastEnabled_dbus_property(self): return datetime_to_dbus(self.last_enabled) # Enabled - property @dbus_service_property(_interface, signature="b", access="readwrite") def Enabled_dbus_property(self, value=None): if value is None: # get return dbus.Boolean(self.enabled) if value: self.enable() else: self.disable() # LastCheckedOK - property @dbus_service_property(_interface, signature="s", access="readwrite") def LastCheckedOK_dbus_property(self, value=None): if value is not None: self.checked_ok() return return datetime_to_dbus(self.last_checked_ok) # LastCheckerStatus - property @dbus_service_property(_interface, signature="n", access="read") def LastCheckerStatus_dbus_property(self): return dbus.Int16(self.last_checker_status) # Expires - property @dbus_service_property(_interface, signature="s", access="read") def Expires_dbus_property(self): return datetime_to_dbus(self.expires) # LastApprovalRequest - property @dbus_service_property(_interface, signature="s", access="read") def LastApprovalRequest_dbus_property(self): return datetime_to_dbus(self.last_approval_request) # Timeout - property @dbus_service_property(_interface, signature="t", access="readwrite") def Timeout_dbus_property(self, value=None): if value is None: # get return dbus.UInt64(self.timeout_milliseconds()) old_timeout = self.timeout self.timeout = datetime.timedelta(0, 0, 0, value) # Reschedule disabling if self.enabled: now = datetime.datetime.utcnow() self.expires += self.timeout - old_timeout if self.expires <= now: # The timeout has passed self.disable() else: if (getattr(self, "disable_initiator_tag", None) is None): return gobject.source_remove(self.disable_initiator_tag) self.disable_initiator_tag = ( gobject.timeout_add( timedelta_to_milliseconds(self.expires - now), self.disable)) # ExtendedTimeout - property @dbus_service_property(_interface, signature="t", access="readwrite") def ExtendedTimeout_dbus_property(self, value=None): if value is None: # get return dbus.UInt64(self.extended_timeout_milliseconds()) self.extended_timeout = datetime.timedelta(0, 0, 0, value) # Interval - property @dbus_service_property(_interface, signature="t", access="readwrite") def Interval_dbus_property(self, value=None): if value is None: # get return dbus.UInt64(self.interval_milliseconds()) self.interval = datetime.timedelta(0, 0, 0, value) if getattr(self, "checker_initiator_tag", None) is None: return if self.enabled: # Reschedule checker run gobject.source_remove(self.checker_initiator_tag) self.checker_initiator_tag = (gobject.timeout_add (value, self.start_checker)) self.start_checker() # Start one now, too # Checker - property @dbus_service_property(_interface, signature="s", access="readwrite") def Checker_dbus_property(self, value=None): if value is None: # get return dbus.String(self.checker_command) self.checker_command = unicode(value) # CheckerRunning - property @dbus_service_property(_interface, signature="b", access="readwrite") def CheckerRunning_dbus_property(self, value=None): if value is None: # get return dbus.Boolean(self.checker is not None) if value: self.start_checker() else: self.stop_checker() # ObjectPath - property @dbus_service_property(_interface, signature="o", access="read") def ObjectPath_dbus_property(self): return self.dbus_object_path # is already a dbus.ObjectPath # Secret = property @dbus_service_property(_interface, signature="ay", access="write", byte_arrays=True) def Secret_dbus_property(self, value): self.secret = str(value) del _interface class ProxyClient(object): def __init__(self, child_pipe, fpr, address): self._pipe = child_pipe self._pipe.send(('init', fpr, address)) if not self._pipe.recv(): raise KeyError() def __getattribute__(self, name): if name == '_pipe': return super(ProxyClient, self).__getattribute__(name) self._pipe.send(('getattr', name)) data = self._pipe.recv() if data[0] == 'data': return data[1] if data[0] == 'function': def func(*args, **kwargs): self._pipe.send(('funcall', name, args, kwargs)) return self._pipe.recv()[1] return func def __setattr__(self, name, value): if name == '_pipe': return super(ProxyClient, self).__setattr__(name, value) self._pipe.send(('setattr', name, value)) class ClientHandler(socketserver.BaseRequestHandler, object): """A class to handle client connections. Instantiated once for each connection to handle it. Note: This will run in its own forked process.""" def handle(self): with contextlib.closing(self.server.child_pipe) as child_pipe: logger.info("TCP connection from: %s", unicode(self.client_address)) logger.debug("Pipe FD: %d", self.server.child_pipe.fileno()) session = (gnutls.connection .ClientSession(self.request, gnutls.connection .X509Credentials())) # Note: gnutls.connection.X509Credentials is really a # generic GnuTLS certificate credentials object so long as # no X.509 keys are added to it. Therefore, we can use it # here despite using OpenPGP certificates. #priority = ':'.join(("NONE", "+VERS-TLS1.1", # "+AES-256-CBC", "+SHA1", # "+COMP-NULL", "+CTYPE-OPENPGP", # "+DHE-DSS")) # Use a fallback default, since this MUST be set. priority = self.server.gnutls_priority if priority is None: priority = "NORMAL" (gnutls.library.functions .gnutls_priority_set_direct(session._c_object, priority, None)) # Start communication using the Mandos protocol # Get protocol number line = self.request.makefile().readline() logger.debug("Protocol version: %r", line) try: if int(line.strip().split()[0]) > 1: raise RuntimeError except (ValueError, IndexError, RuntimeError) as error: logger.error("Unknown protocol version: %s", error) return # Start GnuTLS connection try: session.handshake() except gnutls.errors.GNUTLSError as error: logger.warning("Handshake failed: %s", error) # Do not run session.bye() here: the session is not # established. Just abandon the request. return logger.debug("Handshake succeeded") approval_required = False try: try: fpr = self.fingerprint(self.peer_certificate (session)) except (TypeError, gnutls.errors.GNUTLSError) as error: logger.warning("Bad certificate: %s", error) return logger.debug("Fingerprint: %s", fpr) try: client = ProxyClient(child_pipe, fpr, self.client_address) except KeyError: return if client.approval_delay: delay = client.approval_delay client.approvals_pending += 1 approval_required = True while True: if not client.enabled: logger.info("Client %s is disabled", client.name) if self.server.use_dbus: # Emit D-Bus signal client.Rejected("Disabled") return if client.approved or not client.approval_delay: #We are approved or approval is disabled break elif client.approved is None: logger.info("Client %s needs approval", client.name) if self.server.use_dbus: # Emit D-Bus signal client.NeedApproval( client.approval_delay_milliseconds(), client.approved_by_default) else: logger.warning("Client %s was not approved", client.name) if self.server.use_dbus: # Emit D-Bus signal client.Rejected("Denied") return #wait until timeout or approved time = datetime.datetime.now() client.changedstate.acquire() client.changedstate.wait( float(timedelta_to_milliseconds(delay) / 1000)) client.changedstate.release() time2 = datetime.datetime.now() if (time2 - time) >= delay: if not client.approved_by_default: logger.warning("Client %s timed out while" " waiting for approval", client.name) if self.server.use_dbus: # Emit D-Bus signal client.Rejected("Approval timed out") return else: break else: delay -= time2 - time sent_size = 0 while sent_size < len(client.secret): try: sent = session.send(client.secret[sent_size:]) except gnutls.errors.GNUTLSError as error: logger.warning("gnutls send failed", exc_info=error) return logger.debug("Sent: %d, remaining: %d", sent, len(client.secret) - (sent_size + sent)) sent_size += sent logger.info("Sending secret to %s", client.name) # bump the timeout using extended_timeout client.bump_timeout(client.extended_timeout) if self.server.use_dbus: # Emit D-Bus signal client.GotSecret() finally: if approval_required: client.approvals_pending -= 1 try: session.bye() except gnutls.errors.GNUTLSError as error: logger.warning("GnuTLS bye failed", exc_info=error) @staticmethod def peer_certificate(session): "Return the peer's OpenPGP certificate as a bytestring" # If not an OpenPGP certificate... if (gnutls.library.functions .gnutls_certificate_type_get(session._c_object) != gnutls.library.constants.GNUTLS_CRT_OPENPGP): # ...do the normal thing return session.peer_certificate list_size = ctypes.c_uint(1) cert_list = (gnutls.library.functions .gnutls_certificate_get_peers (session._c_object, ctypes.byref(list_size))) if not bool(cert_list) and list_size.value != 0: raise gnutls.errors.GNUTLSError("error getting peer" " certificate") if list_size.value == 0: return None cert = cert_list[0] return ctypes.string_at(cert.data, cert.size) @staticmethod def fingerprint(openpgp): "Convert an OpenPGP bytestring to a hexdigit fingerprint" # New GnuTLS "datum" with the OpenPGP public key datum = (gnutls.library.types .gnutls_datum_t(ctypes.cast(ctypes.c_char_p(openpgp), ctypes.POINTER (ctypes.c_ubyte)), ctypes.c_uint(len(openpgp)))) # New empty GnuTLS certificate crt = gnutls.library.types.gnutls_openpgp_crt_t() (gnutls.library.functions .gnutls_openpgp_crt_init(ctypes.byref(crt))) # Import the OpenPGP public key into the certificate (gnutls.library.functions .gnutls_openpgp_crt_import(crt, ctypes.byref(datum), gnutls.library.constants .GNUTLS_OPENPGP_FMT_RAW)) # Verify the self signature in the key crtverify = ctypes.c_uint() (gnutls.library.functions .gnutls_openpgp_crt_verify_self(crt, 0, ctypes.byref(crtverify))) if crtverify.value != 0: gnutls.library.functions.gnutls_openpgp_crt_deinit(crt) raise (gnutls.errors.CertificateSecurityError ("Verify failed")) # New buffer for the fingerprint buf = ctypes.create_string_buffer(20) buf_len = ctypes.c_size_t() # Get the fingerprint from the certificate into the buffer (gnutls.library.functions .gnutls_openpgp_crt_get_fingerprint(crt, ctypes.byref(buf), ctypes.byref(buf_len))) # Deinit the certificate gnutls.library.functions.gnutls_openpgp_crt_deinit(crt) # Convert the buffer to a Python bytestring fpr = ctypes.string_at(buf, buf_len.value) # Convert the bytestring to hexadecimal notation hex_fpr = binascii.hexlify(fpr).upper() return hex_fpr class MultiprocessingMixIn(object): """Like socketserver.ThreadingMixIn, but with multiprocessing""" def sub_process_main(self, request, address): try: self.finish_request(request, address) except Exception: self.handle_error(request, address) self.close_request(request) def process_request(self, request, address): """Start a new process to process the request.""" proc = multiprocessing.Process(target = self.sub_process_main, args = (request, address)) proc.start() return proc class MultiprocessingMixInWithPipe(MultiprocessingMixIn, object): """ adds a pipe to the MixIn """ def process_request(self, request, client_address): """Overrides and wraps the original process_request(). This function creates a new pipe in self.pipe """ parent_pipe, self.child_pipe = multiprocessing.Pipe() proc = MultiprocessingMixIn.process_request(self, request, client_address) self.child_pipe.close() self.add_pipe(parent_pipe, proc) def add_pipe(self, parent_pipe, proc): """Dummy function; override as necessary""" raise NotImplementedError class IPv6_TCPServer(MultiprocessingMixInWithPipe, socketserver.TCPServer, object): """IPv6-capable TCP server. Accepts 'None' as address and/or port Attributes: enabled: Boolean; whether this server is activated yet interface: None or a network interface name (string) use_ipv6: Boolean; to use IPv6 or not """ def __init__(self, server_address, RequestHandlerClass, interface=None, use_ipv6=True, socketfd=None): """If socketfd is set, use that file descriptor instead of creating a new one with socket.socket(). """ self.interface = interface if use_ipv6: self.address_family = socket.AF_INET6 if socketfd is not None: # Save the file descriptor self.socketfd = socketfd # Save the original socket.socket() function self.socket_socket = socket.socket # To implement --socket, we monkey patch socket.socket. # # (When socketserver.TCPServer is a new-style class, we # could make self.socket into a property instead of monkey # patching socket.socket.) # # Create a one-time-only replacement for socket.socket() @functools.wraps(socket.socket) def socket_wrapper(*args, **kwargs): # Restore original function so subsequent calls are # not affected. socket.socket = self.socket_socket del self.socket_socket # This time only, return a new socket object from the # saved file descriptor. return socket.fromfd(self.socketfd, *args, **kwargs) # Replace socket.socket() function with wrapper socket.socket = socket_wrapper # The socketserver.TCPServer.__init__ will call # socket.socket(), which might be our replacement, # socket_wrapper(), if socketfd was set. socketserver.TCPServer.__init__(self, server_address, RequestHandlerClass) def server_bind(self): """This overrides the normal server_bind() function to bind to an interface if one was specified, and also NOT to bind to an address or port if they were not specified.""" if self.interface is not None: if SO_BINDTODEVICE is None: logger.error("SO_BINDTODEVICE does not exist;" " cannot bind to interface %s", self.interface) else: try: self.socket.setsockopt(socket.SOL_SOCKET, SO_BINDTODEVICE, str(self.interface + '\0')) except socket.error as error: if error.errno == errno.EPERM: logger.error("No permission to bind to" " interface %s", self.interface) elif error.errno == errno.ENOPROTOOPT: logger.error("SO_BINDTODEVICE not available;" " cannot bind to interface %s", self.interface) elif error.errno == errno.ENODEV: logger.error("Interface %s does not exist," " cannot bind", self.interface) else: raise # Only bind(2) the socket if we really need to. if self.server_address[0] or self.server_address[1]: if not self.server_address[0]: if self.address_family == socket.AF_INET6: any_address = "::" # in6addr_any else: any_address = socket.INADDR_ANY self.server_address = (any_address, self.server_address[1]) elif not self.server_address[1]: self.server_address = (self.server_address[0], 0) # if self.interface: # self.server_address = (self.server_address[0], # 0, # port # 0, # flowinfo # if_nametoindex # (self.interface)) return socketserver.TCPServer.server_bind(self) class MandosServer(IPv6_TCPServer): """Mandos server. Attributes: clients: set of Client objects gnutls_priority GnuTLS priority string use_dbus: Boolean; to emit D-Bus signals or not Assumes a gobject.MainLoop event loop. """ def __init__(self, server_address, RequestHandlerClass, interface=None, use_ipv6=True, clients=None, gnutls_priority=None, use_dbus=True, socketfd=None): self.enabled = False self.clients = clients if self.clients is None: self.clients = {} self.use_dbus = use_dbus self.gnutls_priority = gnutls_priority IPv6_TCPServer.__init__(self, server_address, RequestHandlerClass, interface = interface, use_ipv6 = use_ipv6, socketfd = socketfd) def server_activate(self): if self.enabled: return socketserver.TCPServer.server_activate(self) def enable(self): self.enabled = True def add_pipe(self, parent_pipe, proc): # Call "handle_ipc" for both data and EOF events gobject.io_add_watch(parent_pipe.fileno(), gobject.IO_IN | gobject.IO_HUP, functools.partial(self.handle_ipc, parent_pipe = parent_pipe, proc = proc)) def handle_ipc(self, source, condition, parent_pipe=None, proc = None, client_object=None): # error, or the other end of multiprocessing.Pipe has closed if condition & (gobject.IO_ERR | gobject.IO_HUP): # Wait for other process to exit proc.join() return False # Read a request from the child request = parent_pipe.recv() command = request[0] if command == 'init': fpr = request[1] address = request[2] for c in self.clients.itervalues(): if c.fingerprint == fpr: client = c break else: logger.info("Client not found for fingerprint: %s, ad" "dress: %s", fpr, address) if self.use_dbus: # Emit D-Bus signal mandos_dbus_service.ClientNotFound(fpr, address[0]) parent_pipe.send(False) return False gobject.io_add_watch(parent_pipe.fileno(), gobject.IO_IN | gobject.IO_HUP, functools.partial(self.handle_ipc, parent_pipe = parent_pipe, proc = proc, client_object = client)) parent_pipe.send(True) # remove the old hook in favor of the new above hook on # same fileno return False if command == 'funcall': funcname = request[1] args = request[2] kwargs = request[3] parent_pipe.send(('data', getattr(client_object, funcname)(*args, **kwargs))) if command == 'getattr': attrname = request[1] if callable(client_object.__getattribute__(attrname)): parent_pipe.send(('function',)) else: parent_pipe.send(('data', client_object .__getattribute__(attrname))) if command == 'setattr': attrname = request[1] value = request[2] setattr(client_object, attrname, value) return True def string_to_delta(interval): """Parse a string and return a datetime.timedelta >>> string_to_delta('7d') datetime.timedelta(7) >>> string_to_delta('60s') datetime.timedelta(0, 60) >>> string_to_delta('60m') datetime.timedelta(0, 3600) >>> string_to_delta('24h') datetime.timedelta(1) >>> string_to_delta('1w') datetime.timedelta(7) >>> string_to_delta('5m 30s') datetime.timedelta(0, 330) """ timevalue = datetime.timedelta(0) for s in interval.split(): try: suffix = unicode(s[-1]) value = int(s[:-1]) if suffix == "d": delta = datetime.timedelta(value) elif suffix == "s": delta = datetime.timedelta(0, value) elif suffix == "m": delta = datetime.timedelta(0, 0, 0, 0, value) elif suffix == "h": delta = datetime.timedelta(0, 0, 0, 0, 0, value) elif suffix == "w": delta = datetime.timedelta(0, 0, 0, 0, 0, 0, value) else: raise ValueError("Unknown suffix {0!r}" .format(suffix)) except (ValueError, IndexError) as e: raise ValueError(*(e.args)) timevalue += delta return timevalue def daemon(nochdir = False, noclose = False): """See daemon(3). Standard BSD Unix function. This should really exist as os.daemon, but it doesn't (yet).""" if os.fork(): sys.exit() os.setsid() if not nochdir: os.chdir("/") if os.fork(): sys.exit() if not noclose: # Close all standard open file descriptors null = os.open(os.devnull, os.O_NOCTTY | os.O_RDWR) if not stat.S_ISCHR(os.fstat(null).st_mode): raise OSError(errno.ENODEV, "{0} not a character device" .format(os.devnull)) os.dup2(null, sys.stdin.fileno()) os.dup2(null, sys.stdout.fileno()) os.dup2(null, sys.stderr.fileno()) if null > 2: os.close(null) def main(): ################################################################## # Parsing of options, both command line and config file parser = argparse.ArgumentParser() parser.add_argument("-v", "--version", action="version", version = "%(prog)s {0}".format(version), help="show version number and exit") parser.add_argument("-i", "--interface", metavar="IF", help="Bind to interface IF") parser.add_argument("-a", "--address", help="Address to listen for requests on") parser.add_argument("-p", "--port", type=int, help="Port number to receive requests on") parser.add_argument("--check", action="store_true", help="Run self-test") parser.add_argument("--debug", action="store_true", help="Debug mode; run in foreground and log" " to terminal") parser.add_argument("--debuglevel", metavar="LEVEL", help="Debug level for stdout output") parser.add_argument("--priority", help="GnuTLS" " priority string (see GnuTLS documentation)") parser.add_argument("--servicename", metavar="NAME", help="Zeroconf service name") parser.add_argument("--configdir", default="/etc/mandos", metavar="DIR", help="Directory to search for configuration" " files") parser.add_argument("--no-dbus", action="store_false", dest="use_dbus", help="Do not provide D-Bus" " system bus interface") parser.add_argument("--no-ipv6", action="store_false", dest="use_ipv6", help="Do not use IPv6") parser.add_argument("--no-restore", action="store_false", dest="restore", help="Do not restore stored" " state") parser.add_argument("--socket", type=int, help="Specify a file descriptor to a network" " socket to use instead of creating one") parser.add_argument("--statedir", metavar="DIR", help="Directory to save/restore state in") parser.add_argument("--foreground", action="store_true", help="Run in foreground") options = parser.parse_args() if options.check: import doctest doctest.testmod() sys.exit() # Default values for config file for server-global settings server_defaults = { "interface": "", "address": "", "port": "", "debug": "False", "priority": "SECURE256:!CTYPE-X.509:+CTYPE-OPENPGP", "servicename": "Mandos", "use_dbus": "True", "use_ipv6": "True", "debuglevel": "", "restore": "True", "socket": "", "statedir": "/var/lib/mandos", "foreground": "False", } # Parse config file for server-global settings server_config = configparser.SafeConfigParser(server_defaults) del server_defaults server_config.read(os.path.join(options.configdir, "mandos.conf")) # Convert the SafeConfigParser object to a dict server_settings = server_config.defaults() # Use the appropriate methods on the non-string config options for option in ("debug", "use_dbus", "use_ipv6", "foreground"): server_settings[option] = server_config.getboolean("DEFAULT", option) if server_settings["port"]: server_settings["port"] = server_config.getint("DEFAULT", "port") if server_settings["socket"]: server_settings["socket"] = server_config.getint("DEFAULT", "socket") # Later, stdin will, and stdout and stderr might, be dup'ed # over with an opened os.devnull. But we don't want this to # happen with a supplied network socket. if 0 <= server_settings["socket"] <= 2: server_settings["socket"] = os.dup(server_settings ["socket"]) del server_config # Override the settings from the config file with command line # options, if set. for option in ("interface", "address", "port", "debug", "priority", "servicename", "configdir", "use_dbus", "use_ipv6", "debuglevel", "restore", "statedir", "socket", "foreground"): value = getattr(options, option) if value is not None: server_settings[option] = value del options # Force all strings to be unicode for option in server_settings.keys(): if type(server_settings[option]) is str: server_settings[option] = unicode(server_settings[option]) # Debug implies foreground if server_settings["debug"]: server_settings["foreground"] = True # Now we have our good server settings in "server_settings" ################################################################## # For convenience debug = server_settings["debug"] debuglevel = server_settings["debuglevel"] use_dbus = server_settings["use_dbus"] use_ipv6 = server_settings["use_ipv6"] stored_state_path = os.path.join(server_settings["statedir"], stored_state_file) foreground = server_settings["foreground"] if debug: initlogger(debug, logging.DEBUG) else: if not debuglevel: initlogger(debug) else: level = getattr(logging, debuglevel.upper()) initlogger(debug, level) if server_settings["servicename"] != "Mandos": syslogger.setFormatter(logging.Formatter ('Mandos ({0}) [%(process)d]:' ' %(levelname)s: %(message)s' .format(server_settings ["servicename"]))) # Parse config file with clients client_config = configparser.SafeConfigParser(Client .client_defaults) client_config.read(os.path.join(server_settings["configdir"], "clients.conf")) global mandos_dbus_service mandos_dbus_service = None tcp_server = MandosServer((server_settings["address"], server_settings["port"]), ClientHandler, interface=(server_settings["interface"] or None), use_ipv6=use_ipv6, gnutls_priority= server_settings["priority"], use_dbus=use_dbus, socketfd=(server_settings["socket"] or None)) if not foreground: pidfilename = "/var/run/mandos.pid" pidfile = None try: pidfile = open(pidfilename, "w") except IOError as e: logger.error("Could not open file %r", pidfilename, exc_info=e) for name in ("_mandos", "mandos", "nobody"): try: uid = pwd.getpwnam(name).pw_uid gid = pwd.getpwnam(name).pw_gid break except KeyError: continue else: uid = 65534 gid = 65534 try: os.setgid(gid) os.setuid(uid) except OSError as error: if error.errno != errno.EPERM: raise error if debug: # Enable all possible GnuTLS debugging # "Use a log level over 10 to enable all debugging options." # - GnuTLS manual gnutls.library.functions.gnutls_global_set_log_level(11) @gnutls.library.types.gnutls_log_func def debug_gnutls(level, string): logger.debug("GnuTLS: %s", string[:-1]) (gnutls.library.functions .gnutls_global_set_log_function(debug_gnutls)) # Redirect stdin so all checkers get /dev/null null = os.open(os.devnull, os.O_NOCTTY | os.O_RDWR) os.dup2(null, sys.stdin.fileno()) if null > 2: os.close(null) # Need to fork before connecting to D-Bus if not foreground: # Close all input and output, do double fork, etc. daemon() # multiprocessing will use threads, so before we use gobject we # need to inform gobject that threads will be used. gobject.threads_init() global main_loop # From the Avahi example code DBusGMainLoop(set_as_default=True) main_loop = gobject.MainLoop() bus = dbus.SystemBus() # End of Avahi example code if use_dbus: try: bus_name = dbus.service.BusName("se.recompile.Mandos", bus, do_not_queue=True) old_bus_name = (dbus.service.BusName ("se.bsnet.fukt.Mandos", bus, do_not_queue=True)) except dbus.exceptions.NameExistsException as e: logger.error("Disabling D-Bus:", exc_info=e) use_dbus = False server_settings["use_dbus"] = False tcp_server.use_dbus = False protocol = avahi.PROTO_INET6 if use_ipv6 else avahi.PROTO_INET service = AvahiServiceToSyslog(name = server_settings["servicename"], servicetype = "_mandos._tcp", protocol = protocol, bus = bus) if server_settings["interface"]: service.interface = (if_nametoindex (str(server_settings["interface"]))) global multiprocessing_manager multiprocessing_manager = multiprocessing.Manager() client_class = Client if use_dbus: client_class = functools.partial(ClientDBus, bus = bus) client_settings = Client.config_parser(client_config) old_client_settings = {} clients_data = {} # Get client data and settings from last running state. if server_settings["restore"]: try: with open(stored_state_path, "rb") as stored_state: clients_data, old_client_settings = (pickle.load (stored_state)) os.remove(stored_state_path) except IOError as e: if e.errno == errno.ENOENT: logger.warning("Could not load persistent state: {0}" .format(os.strerror(e.errno))) else: logger.critical("Could not load persistent state:", exc_info=e) raise except EOFError as e: logger.warning("Could not load persistent state: " "EOFError:", exc_info=e) with PGPEngine() as pgp: for client_name, client in clients_data.iteritems(): # Decide which value to use after restoring saved state. # We have three different values: Old config file, # new config file, and saved state. # New config value takes precedence if it differs from old # config value, otherwise use saved state. for name, value in client_settings[client_name].items(): try: # For each value in new config, check if it # differs from the old config value (Except for # the "secret" attribute) if (name != "secret" and value != old_client_settings[client_name] [name]): client[name] = value except KeyError: pass # Clients who has passed its expire date can still be # enabled if its last checker was successful. Clients # whose checker succeeded before we stored its state is # assumed to have successfully run all checkers during # downtime. if client["enabled"]: if datetime.datetime.utcnow() >= client["expires"]: if not client["last_checked_ok"]: logger.warning( "disabling client {0} - Client never " "performed a successful checker" .format(client_name)) client["enabled"] = False elif client["last_checker_status"] != 0: logger.warning( "disabling client {0} - Client " "last checker failed with error code {1}" .format(client_name, client["last_checker_status"])) client["enabled"] = False else: client["expires"] = (datetime.datetime .utcnow() + client["timeout"]) logger.debug("Last checker succeeded," " keeping {0} enabled" .format(client_name)) try: client["secret"] = ( pgp.decrypt(client["encrypted_secret"], client_settings[client_name] ["secret"])) except PGPError: # If decryption fails, we use secret from new settings logger.debug("Failed to decrypt {0} old secret" .format(client_name)) client["secret"] = ( client_settings[client_name]["secret"]) # Add/remove clients based on new changes made to config for client_name in (set(old_client_settings) - set(client_settings)): del clients_data[client_name] for client_name in (set(client_settings) - set(old_client_settings)): clients_data[client_name] = client_settings[client_name] # Create all client objects for client_name, client in clients_data.iteritems(): tcp_server.clients[client_name] = client_class( name = client_name, settings = client) if not tcp_server.clients: logger.warning("No clients defined") if not foreground: if pidfile is not None: try: with pidfile: pid = os.getpid() pidfile.write(str(pid) + "\n".encode("utf-8")) except IOError: logger.error("Could not write to file %r with PID %d", pidfilename, pid) del pidfile del pidfilename signal.signal(signal.SIGHUP, lambda signum, frame: sys.exit()) signal.signal(signal.SIGTERM, lambda signum, frame: sys.exit()) if use_dbus: @alternate_dbus_interfaces({"se.recompile.Mandos": "se.bsnet.fukt.Mandos"}) class MandosDBusService(DBusObjectWithProperties): """A D-Bus proxy object""" def __init__(self): dbus.service.Object.__init__(self, bus, "/") _interface = "se.recompile.Mandos" @dbus_interface_annotations(_interface) def _foo(self): return { "org.freedesktop.DBus.Property" ".EmitsChangedSignal": "false"} @dbus.service.signal(_interface, signature="o") def ClientAdded(self, objpath): "D-Bus signal" pass @dbus.service.signal(_interface, signature="ss") def ClientNotFound(self, fingerprint, address): "D-Bus signal" pass @dbus.service.signal(_interface, signature="os") def ClientRemoved(self, objpath, name): "D-Bus signal" pass @dbus.service.method(_interface, out_signature="ao") def GetAllClients(self): "D-Bus method" return dbus.Array(c.dbus_object_path for c in tcp_server.clients.itervalues()) @dbus.service.method(_interface, out_signature="a{oa{sv}}") def GetAllClientsWithProperties(self): "D-Bus method" return dbus.Dictionary( ((c.dbus_object_path, c.GetAll("")) for c in tcp_server.clients.itervalues()), signature="oa{sv}") @dbus.service.method(_interface, in_signature="o") def RemoveClient(self, object_path): "D-Bus method" for c in tcp_server.clients.itervalues(): if c.dbus_object_path == object_path: del tcp_server.clients[c.name] c.remove_from_connection() # Don't signal anything except ClientRemoved c.disable(quiet=True) # Emit D-Bus signal self.ClientRemoved(object_path, c.name) return raise KeyError(object_path) del _interface mandos_dbus_service = MandosDBusService() def cleanup(): "Cleanup function; run on exit" service.cleanup() multiprocessing.active_children() if not (tcp_server.clients or client_settings): return # Store client before exiting. Secrets are encrypted with key # based on what config file has. If config file is # removed/edited, old secret will thus be unrecovable. clients = {} with PGPEngine() as pgp: for client in tcp_server.clients.itervalues(): key = client_settings[client.name]["secret"] client.encrypted_secret = pgp.encrypt(client.secret, key) client_dict = {} # A list of attributes that can not be pickled # + secret. exclude = set(("bus", "changedstate", "secret", "checker")) for name, typ in (inspect.getmembers (dbus.service.Object)): exclude.add(name) client_dict["encrypted_secret"] = (client .encrypted_secret) for attr in client.client_structure: if attr not in exclude: client_dict[attr] = getattr(client, attr) clients[client.name] = client_dict del client_settings[client.name]["secret"] try: with (tempfile.NamedTemporaryFile (mode='wb', suffix=".pickle", prefix='clients-', dir=os.path.dirname(stored_state_path), delete=False)) as stored_state: pickle.dump((clients, client_settings), stored_state) tempname=stored_state.name os.rename(tempname, stored_state_path) except (IOError, OSError) as e: if not debug: try: os.remove(tempname) except NameError: pass if e.errno in (errno.ENOENT, errno.EACCES, errno.EEXIST): logger.warning("Could not save persistent state: {0}" .format(os.strerror(e.errno))) else: logger.warning("Could not save persistent state:", exc_info=e) raise e # Delete all clients, and settings from config while tcp_server.clients: name, client = tcp_server.clients.popitem() if use_dbus: client.remove_from_connection() # Don't signal anything except ClientRemoved client.disable(quiet=True) if use_dbus: # Emit D-Bus signal mandos_dbus_service.ClientRemoved(client .dbus_object_path, client.name) client_settings.clear() atexit.register(cleanup) for client in tcp_server.clients.itervalues(): if use_dbus: # Emit D-Bus signal mandos_dbus_service.ClientAdded(client.dbus_object_path) # Need to initiate checking of clients if client.enabled: client.init_checker() tcp_server.enable() tcp_server.server_activate() # Find out what port we got service.port = tcp_server.socket.getsockname()[1] if use_ipv6: logger.info("Now listening on address %r, port %d," " flowinfo %d, scope_id %d", *tcp_server.socket.getsockname()) else: # IPv4 logger.info("Now listening on address %r, port %d", *tcp_server.socket.getsockname()) #service.interface = tcp_server.socket.getsockname()[3] try: # From the Avahi example code try: service.activate() except dbus.exceptions.DBusException as error: logger.critical("D-Bus Exception", exc_info=error) cleanup() sys.exit(1) # End of Avahi example code gobject.io_add_watch(tcp_server.fileno(), gobject.IO_IN, lambda *args, **kwargs: (tcp_server.handle_request (*args[2:], **kwargs) or True)) logger.debug("Starting main loop") main_loop.run() except AvahiError as error: logger.critical("Avahi Error", exc_info=error) cleanup() sys.exit(1) except KeyboardInterrupt: if debug: print("", file=sys.stderr) logger.debug("Server received KeyboardInterrupt") logger.debug("Server exiting") # Must run before the D-Bus bus name gets deregistered cleanup() if __name__ == '__main__': main() ������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������mandos-1.6.0/network-hooks.d/�����������������������������������������������������������������������0000775�0001750�0001750�00000000000�11767455003�016311� 5����������������������������������������������������������������������������������������������������ustar �teddy���������������������������teddy���������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������mandos-1.6.0/network-hooks.d/openvpn.conf�����������������������������������������������������������0000664�0001750�0001750�00000000443�11767455003�020646� 0����������������������������������������������������������������������������������������������������ustar �teddy���������������������������teddy���������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������# Sample OpenVPN configuration file # Uncomment and change - see openvpn(8) # Network device. #dev tun # Our remote peer #remote 192.0.2.3 #float 192.0.2.3 #port 1194 # VPN endpoints #ifconfig 10.1.0.1 10.1.0.2 # A pre-shared static key #secret openvpn.key # Cipher #cipher AES-128-CBC �����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������mandos-1.6.0/network-hooks.d/wireless���������������������������������������������������������������0000775�0001750�0001750�00000010041�11767455003�020070� 0����������������������������������������������������������������������������������������������������ustar �teddy���������������������������teddy���������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������#!/bin/sh # # This is an example of a Mandos client network hook. This hook # brings up a wireless interface as specified in a separate # configuration file. To be used, this file and any needed # configuration file(s) should be copied into the # /etc/mandos/network-hooks.d directory. # # Copyright © 2012 Teddy Hogeborn # Copyright © 2012 Björn Påhlsson # # Copying and distribution of this file, with or without modification, # are permitted in any medium without royalty provided the copyright # notice and this notice are preserved. This file is offered as-is, # without any warranty. set -e RUNDIR="/run" CTRL="$RUNDIR/wpa_supplicant-global" CTRLDIR="$RUNDIR/wpa_supplicant" PIDFILE="$RUNDIR/wpa_supplicant-mandos.pid" CONFIG="$MANDOSNETHOOKDIR/wireless.conf" addrtoif(){ grep -liFe "$1" /sys/class/net/*/address \ | sed -e 's,.*/\([^/]*\)/[^/]*,\1,' } # Read config file if [ -e "$CONFIG" ]; then . "$CONFIG" else exit fi ifkeys=`sed -n -e 's/^ADDRESS_\([^=]*\)=.*/\1/p' "$CONFIG" | sort -u` # Exit if DEVICE is set and is not any of the wireless interfaces if [ -n "$DEVICE" ]; then while :; do for KEY in $ifkeys; do ADDRESS=`eval 'echo "$ADDRESS_'"$KEY"\"` INTERFACE=`addrtoif "$ADDRESS"` case "$DEVICE" in *,"$INTERFACE"|*,"$INTERFACE",*|"$INTERFACE",*|"$INTERFACE") break 2;; esac done exit done fi wpa_supplicant=/sbin/wpa_supplicant wpa_cli=/sbin/wpa_cli ip=/bin/ip # Used by the wpa_interface_* functions in the wireless.conf file wpa_cli_set(){ case "$1" in ssid|psk) arg="\"$2\"" ;; *) arg="$2" ;; esac "$wpa_cli" -p "$CTRLDIR" -i "$INTERFACE" set_network "$NETWORK" \ "$1" "$arg" 2>&1 | sed -e '/^OK$/d' } if [ $VERBOSITY -gt 0 ]; then WPAS_OPTIONS="-d $WPAS_OPTIONS" fi if [ -n "$PIDFILE" ]; then WPAS_OPTIONS="-P$PIDFILE $WPAS_OPTIONS" fi do_start(){ mkdir -m u=rwx,go= -p "$CTRLDIR" "$wpa_supplicant" -B -g "$CTRL" -p "$CTRLDIR" $WPAS_OPTIONS for KEY in $ifkeys; do ADDRESS=`eval 'echo "$ADDRESS_'"$KEY"\"` INTERFACE=`addrtoif "$ADDRESS"` DRIVER=`eval 'echo "$WPA_DRIVER_'"$KEY"\"` IFDELAY=`eval 'echo "$DELAY_'"$KEY"\"` "$wpa_cli" -g "$CTRL" interface_add "$INTERFACE" "" \ "${DRIVER:-wext}" "$CTRLDIR" > /dev/null \ | sed -e '/^OK$/d' NETWORK=`"$wpa_cli" -p "$CTRLDIR" -i "$INTERFACE" add_network` eval wpa_interface_"$KEY" "$wpa_cli" -p "$CTRLDIR" -i "$INTERFACE" enable_network \ "$NETWORK" | sed -e '/^OK$/d' sleep "${IFDELAY:-$DELAY}" & sleep=$! while :; do kill -0 $sleep 2>/dev/null || break STATE=`"$wpa_cli" -p "$CTRLDIR" -i "$INTERFACE" status \ | sed -n -e 's/^wpa_state=//p'` if [ "$STATE" = COMPLETED ]; then while :; do kill -0 $sleep 2>/dev/null || break 2 UP=`cat /sys/class/net/"$INTERFACE"/operstate` if [ "$UP" = up ]; then kill $sleep 2>/dev/null break 2 fi sleep 1 done fi sleep 1 done & wait $sleep || : IPADDRS=`eval 'echo "$IPADDRS_'"$KEY"\"` if [ -n "$IPADDRS" ]; then if [ "$IPADDRS" = dhcp ]; then ipconfig -c dhcp -d "$INTERFACE" || : #dhclient "$INTERFACE" else for ipaddr in $IPADDRS; do "$ip" addr add "$ipaddr" dev "$INTERFACE" done fi fi ROUTES=`eval 'echo "$ROUTES_'"$KEY"\"` if [ -n "$ROUTES" ]; then for route in $ROUTES; do "$ip" route add "$route" dev "$INTERFACE" done fi done } do_stop(){ "$wpa_cli" -g "$CTRL" terminate 2>&1 | sed -e '/^OK$/d' for KEY in $ifkeys; do ADDRESS=`eval 'echo "$ADDRESS_'"$KEY"\"` INTERFACE=`addrtoif "$ADDRESS"` "$ip" addr show scope global permanent dev "$INTERFACE" \ | while read type addr rest; do case "$type" in inet|inet6) "$ip" addr del "$addr" dev "$INTERFACE" ;; esac done "$ip" link set dev "$INTERFACE" down done } case "${MODE:-$1}" in start|stop) do_"${MODE:-$1}" ;; files) echo "$wpa_supplicant" echo "$wpa_cli" echo "$ip" ;; modules) if [ "$IPADDRS" = dhcp ]; then echo af_packet fi sed -n -e 's/#.*$//' -e 's/[ ]*$//' \ -e 's/^MODULE_[^=]\+=//p' "$CONFIG" ;; esac �����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������mandos-1.6.0/network-hooks.d/bridge.conf������������������������������������������������������������0000664�0001750�0001750�00000000275�11767455003�020420� 0����������������������������������������������������������������������������������������������������ustar �teddy���������������������������teddy���������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������## Required #BRIDGE=br0 #PORT_ADDRESSES="00:11:22:33:44:55 11:22:33:44:55:66" ## Optional #IPADDRS="192.0.2.3/24 2001:DB8::aede:48ff:fe71:f6f2/32" #ROUTES="192.0.2.0/24 2001:DB8::/32" �����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������mandos-1.6.0/network-hooks.d/bridge�����������������������������������������������������������������0000775�0001750�0001750�00000004044�11767455003�017475� 0����������������������������������������������������������������������������������������������������ustar �teddy���������������������������teddy���������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������#!/bin/sh # # This is an example of a Mandos client network hook. This hook # brings up a bridge interface as specified in a separate # configuration file. To be used, this file and any needed # configuration file(s) should be copied into the # /etc/mandos/network-hooks.d directory. # # Copyright © 2012 Teddy Hogeborn # Copyright © 2012 Björn Påhlsson # # Copying and distribution of this file, with or without modification, # are permitted in any medium without royalty provided the copyright # notice and this notice are preserved. This file is offered as-is, # without any warranty. set -e CONFIG="$MANDOSNETHOOKDIR/bridge.conf" addrtoif(){ grep -liFe "$1" /sys/class/net/*/address \ | sed -e 's,.*/\([^/]*\)/[^/]*,\1,' -e "/^${BRIDGE}\$/d" } # Read config file, which must set "BRIDGE", "PORT_ADDRESSES", and # optionally "IPADDRS" and "ROUTES". if [ -e "$CONFIG" ]; then . "$CONFIG" fi if [ -z "$BRIDGE" -o -z "$PORT_ADDRESSES" ]; then exit fi if [ -n "$DEVICE" ]; then case "$DEVICE" in *,"$BRIDGE"|*,"$BRIDGE",*|"$BRIDGE",*|"$BRIDGE") :;; *) exit;; esac fi brctl="/sbin/brctl" for b in "$brctl" /usr/sbin/brctl; do if [ -e "$b" ]; then brctl="$b" break fi done do_start(){ "$brctl" addbr "$BRIDGE" for address in $PORT_ADDRESSES; do interface=`addrtoif "$address"` "$brctl" addif "$BRIDGE" "$interface" ip link set dev "$interface" up done ip link set dev "$BRIDGE" up sleep "${DELAY%%.*}" if [ -n "$IPADDRS" ]; then for ipaddr in $IPADDRS; do ip addr add "$ipaddr" dev "$BRIDGE" done fi if [ -n "$ROUTES" ]; then for route in $ROUTES; do ip route add "$route" dev "$BRIDGE" done fi } do_stop(){ ip link set dev "$BRIDGE" down for address in $PORT_ADDRESSES; do interface=`addrtoif "$address"` ip link set dev "$interface" down "$brctl" delif "$BRIDGE" "$interface" done "$brctl" delbr "$BRIDGE" } case "${MODE:-$1}" in start|stop) do_"${MODE:-$1}" ;; files) echo /bin/ip echo "$brctl" ;; modules) echo bridge ;; esac ��������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������mandos-1.6.0/network-hooks.d/openvpn����������������������������������������������������������������0000775�0001750�0001750�00000002702�11767455003�017725� 0����������������������������������������������������������������������������������������������������ustar �teddy���������������������������teddy���������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������#!/bin/sh # # This is an example of a Mandos client network hook. This hook # brings up an OpenVPN interface as specified in a separate # configuration file. To be used, this file and any needed # configuration file(s) should be copied into the # /etc/mandos/network-hooks.d directory. # # Copyright © 2012 Teddy Hogeborn # Copyright © 2012 Björn Påhlsson # # Copying and distribution of this file, with or without modification, # are permitted in any medium without royalty provided the copyright # notice and this notice are preserved. This file is offered as-is, # without any warranty. set -e CONFIG="openvpn.conf" # Extract the "dev" setting from the config file VPNDEVICE=`sed -n -e 's/[[:space:]]#.*//' \ -e 's/^[[:space:]]*dev[[:space:]]\+//p' \ "$MANDOSNETHOOKDIR/$CONFIG"` PIDFILE=/run/openvpn-mandos.pid # Exit if no device set in config if [ -z "$VPNDEVICE" ]; then exit fi # Exit if DEVICE is set and it doesn't match the VPN interface if [ -n "$DEVICE" ]; then case "$DEVICE" in *,"$VPNDEVICE"*|"$VPNDEVICE"*) :;; *) exit;; esac fi openvpn=/usr/sbin/openvpn do_start(){ "$openvpn" --cd "$MANDOSNETHOOKDIR" --daemon 'openvpn(Mandos)' \ --writepid "$PIDFILE" --config "$CONFIG" sleep "$DELAY" } do_stop(){ PID="`cat \"$PIDFILE\"`" if [ "$PID" -gt 0 ]; then kill "$PID" fi } case "${MODE:-$1}" in start|stop) do_"${MODE:-$1}" ;; files) echo "$openvpn" ;; modules) echo tun ;; esac ��������������������������������������������������������������mandos-1.6.0/network-hooks.d/wireless.conf����������������������������������������������������������0000664�0001750�0001750�00000001143�11767455003�021014� 0����������������������������������������������������������������������������������������������������ustar �teddy���������������������������teddy���������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������# Extra options for wpa_supplicant, if any #WPAS_OPTIONS="" # wlan0 ADDRESS_0=00:11:22:33:44:55 MODULE_0=ath9k #WPA_DRIVER_0=wext wpa_interface_0(){ # Use this format to set simple things: wpa_cli_set ssid home wpa_cli_set psk "secret passphrase" # Use this format to do more complex things with wpa_cli: #"$wpa_cli" -p "$CTRLDIR" -i "$INTERFACE" bssid "$NETWORK" 00:11:22:33:44:55 #"$wpa_cli" -g "$CTRL" ping } #DELAY_0=10 IPADDRS_0=dhcp #IPADDRS_0="192.0.2.3/24 2001:DB8::aede:48ff:fe71:f6f2/32" #ROUTES_0="192.0.2.0/24 2001:DB8::/32" #ADDRESS_1=11:22:33:44:55:66 #MODULE_1=... #... �����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������mandos-1.6.0/mandos-monitor.xml���������������������������������������������������������������������0000664�0001750�0001750�00000014122�11767455003�016745� 0����������������������������������������������������������������������������������������������������ustar �teddy���������������������������teddy���������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������<?xml version="1.0" encoding="UTF-8"?> <!DOCTYPE refentry PUBLIC "-//OASIS//DTD DocBook XML V4.5//EN" "http://www.oasis-open.org/docbook/xml/4.5/docbookx.dtd" [ <!ENTITY COMMANDNAME "mandos-monitor"> <!ENTITY TIMESTAMP "2012-01-01"> <!ENTITY % common SYSTEM "common.ent"> %common; ]> <refentry xmlns:xi="http://www.w3.org/2001/XInclude"> <refentryinfo> <title>Mandos Manual Mandos &version; &TIMESTAMP; Björn Påhlsson
belorn@recompile.se
Teddy Hogeborn
teddy@recompile.se
2010 2011 2012 Teddy Hogeborn Björn Påhlsson &COMMANDNAME; 8 &COMMANDNAME; Text-based GUI to control the Mandos server. &COMMANDNAME; DESCRIPTION &COMMANDNAME; is an interactive program to monitor and control the operations of the Mandos server (see mandos8). PURPOSE The purpose of this is to enable remote and unattended rebooting of client host computer with an encrypted root file system. See for details. OVERVIEW This program is used to monitor and control the Mandos server. In particular, it can be used to approve Mandos clients which have been configured to require approval. It also shows all significant events reported by the Mandos server. KEYS This program is used to monitor and control the Mandos server. In particular, it can be used to approve Mandos clients which have been configured to require approval. It also shows all significant events reported by the Mandos server. Global Keys Keys Function q, Q Quit Ctrl-L Redraw screen ?, F1 Show help l, D Toggle log window TAB Switch window w, i Toggle log window line wrap Up, Ctrl-P, k Move up a line Down, Ctrl-N, j Move down a line PageUp, Meta-V, b Move up a page PageDown, Ctrl-V, SPACE, f Move down a page
Client List Keys Keys Function + Enable client - Disable client a Approve client d Deny client R, _, Ctrl-K Remove client s Start checker for client S Stop checker for client C Force a successful check for this client.
BUGS This program can currently only be used to monitor and control a Mandos server with the default D-Bus service name of Mandos. EXAMPLE This program takes no options: &COMMANDNAME; SECURITY This program must be permitted to access the Mandos server via the D-Bus interface. This normally requires the root user, but could be configured otherwise by reconfiguring the D-Bus server. SEE ALSO intro 8mandos, mandos 8, mandos-ctl 8 mandos-1.6.0/plugin-runner.xml0000664000175000017500000005113311767455003016607 0ustar teddyteddy00000000000000 %common; ]> Mandos Manual Mandos &version; &TIMESTAMP; Björn Påhlsson
belorn@recompile.se
Teddy Hogeborn
teddy@recompile.se
2008 2009 2012 Teddy Hogeborn Björn Påhlsson
&COMMANDNAME; 8mandos &COMMANDNAME; Run Mandos plugins, pass data from first to succeed. &COMMANDNAME; &COMMANDNAME; &COMMANDNAME; &COMMANDNAME; DESCRIPTION &COMMANDNAME; is a program which is meant to be specified as a keyscript for the root disk in crypttab 5. The aim of this program is therefore to output a password, which then cryptsetup 8 will use to unlock the root disk. This program is not meant to be invoked directly, but can be in order to test it. Note that any password obtained will simply be output on standard output. PURPOSE The purpose of this is to enable remote and unattended rebooting of client host computer with an encrypted root file system. See for details. OPTIONS This option will add an environment variable setting to all plugins. This will override any inherited environment variable. This option will add an environment variable setting to the PLUGIN plugin. This will override any inherited environment variables or environment variables specified using . Pass some options to all plugins. OPTIONS is a comma separated list of options. This is not a very useful option, except for specifying the option to all plugins. Pass some options to a specific plugin. PLUGIN is the name (file basename) of a plugin, and OPTIONS is a comma separated list of options. Note that since options are not split on whitespace, the way to pass, to the plugin foo, the option with the option argument baz is either --options-for=foo:--bar=baz or --options-for=foo:--bar,baz. Using --options-for="foo:--bar baz". will not work. Disable the plugin named PLUGIN. The plugin will not be started. Re-enable the plugin named PLUGIN. This is only useful to undo a previous option, maybe from the configuration file. Change to group ID ID on startup. The default is 65534. All plugins will be started using this group ID. Note: This must be a number, not a name. Change to user ID ID on startup. The default is 65534. All plugins will be started using this user ID. Note: This must be a number, not a name. Specify a different plugin directory. The default is /lib/mandos/plugins.d, which will exist in the initial RAM disk environment. Specify a different file to read additional options from. See . Other command line options will override options specified in the file. Enable debug mode. This will enable a lot of output to standard error about what the program is doing. The program will still perform all other functions normally. The default is to not run in debug mode. The plugins will not be affected by this option. Use if complete debugging eruption is desired. Gives a help message about options and their meanings. Gives a short usage message. Prints the program version. OVERVIEW This program will run on the client side in the initial RAM disk environment, and is responsible for getting a password. It does this by running plugins, one of which will normally be the actual client program communicating with the server. PLUGINS This program will get a password by running a number of plugins, which are simply executable programs in a directory in the initial RAM disk environment. The default directory is /lib/mandos/plugins.d, but this can be changed with the option. The plugins are started in parallel, and the first plugin to output a password and exit with a successful exit code will make this plugin-runner output the password from that plugin, stop any other plugins, and exit. WRITING PLUGINS A plugin is simply a program which prints a password to its standard output and then exits with a successful (zero) exit status. If the exit status is not zero, any output on standard output will be ignored by the plugin runner. Any output on its standard error channel will simply be passed to the standard error of the plugin runner, usually the system console. If the password is a single-line, manually entered passprase, a final trailing newline character should not be printed. The plugin will run in the initial RAM disk environment, so care must be taken not to depend on any files or running services not available there. The plugin must exit cleanly and free all allocated resources upon getting the TERM signal, since this is what the plugin runner uses to stop all other plugins when one plugin has output a password and exited cleanly. The plugin must not use resources, like for instance reading from the standard input, without knowing that no other plugin is also using it. It is useful, but not required, for the plugin to take the option. FALLBACK If no plugins succeed, this program will, as a fallback, ask for a password on the console using getpass3, and output it. This is not meant to be the normal mode of operation, as there is a separate plugin for getting a password from the console. EXIT STATUS Exit status of this program is zero if no errors were encountered, and otherwise not. The fallback (see ) may or may not have succeeded in either case. ENVIRONMENT This program does not use any environment variables itself, it only passes on its environment to all the plugins. The environment passed to plugins can be modified using the and options. FILES /conf/conf.d/mandos/plugin-runner.conf Since this program will be run as a keyscript, there is little to no opportunity to pass command line arguments to it. Therefore, it will also read this file and use its contents as whitespace-separated command line options. Also, everything from a # character to the end of a line is ignored. This program is meant to run in the initial RAM disk environment, so that is where this file is assumed to exist. The file does not need to exist in the normal file system. This file will be processed before the normal command line options, so the latter can override the former, if need be. This file name is the default; the file to read for arguments can be changed using the option. BUGS The option is ignored when specified from within a configuration file. EXAMPLE Normal invocation needs no options: &COMMANDNAME; Run the program, but not the plugins, in debug mode: &COMMANDNAME; --debug Run all plugins, but run the foo plugin in debug mode: &COMMANDNAME; --options-for=foo:--debug Run all plugins, but not the program, in debug mode: &COMMANDNAME; --global-options=--debug Run plugins from a different directory, read a different configuration file, and add two options to the mandos-client 8mandos plugin: cd /etc/keys/mandos; &COMMANDNAME; --config-file=/etc/mandos/plugin-runner.conf --plugin-dir /usr/lib/mandos/plugins.d --options-for=mandos-client:--pubkey=pubkey.txt,--seckey=seckey.txt SECURITY This program will, when starting, try to switch to another user. If it is started as root, it will succeed, and will by default switch to user and group 65534, which are assumed to be non-privileged. This user and group is then what all plugins will be started as. Therefore, the only way to run a plugin as a privileged user is to have the set-user-ID or set-group-ID bit set on the plugin executable file (see execve2 ). If this program is used as a keyscript in crypttab5 , there is a slight risk that if this program fails to work, there might be no way to boot the system except for booting from another media and editing the initial RAM disk image to not run this program. This is, however, unlikely, since the password-prompt8mandos plugin will read a password from the console in case of failure of the other plugins, and this plugin runner will also, in case of catastrophic failure, itself fall back to asking and outputting a password on the console (see ). SEE ALSO intro 8mandos, cryptsetup 8, crypttab 5, execve 2, mandos 8, password-prompt 8mandos, mandos-client 8mandos
mandos-1.6.0/mandos.conf0000664000175000017500000000272611767455003015414 0ustar teddyteddy00000000000000# This file must have exactly one section named "DEFAULT". [DEFAULT] # These are the default values for the server, uncomment and change # them if needed. # If "interface" is set, the server will only listen to a specific # network interface. ;interface = # If "address" is set, the server will only listen to a specific # address. This must currently be an IPv6 address; an IPv4 address # can be specified using the "::FFFF:192.0.2.3" syntax. Also, if this # is a link-local address, an interface should be set above. ;address = # If "port" is set, the server to bind to that port. By default, the # server will listen to an arbitrary port. ;port = # If "debug" is true, the server will run in the foreground and print # a lot of debugging information. ;debug = False # GnuTLS priority for the TLS handshake. See gnutls_priority_init(3). ;priority = SECURE256:!CTYPE-X.509:+CTYPE-OPENPGP # Zeroconf service name. You need to change this if you for some # reason want to run more than one server on the same *host*. # If there are name collisions on the same *network*, the server will # rename itself to "Mandos #2", etc. ;servicename = Mandos # Whether to provide a D-Bus system bus interface or not ;use_dbus = True # Whether to use IPv6. (Changing this is NOT recommended.) ;use_ipv6 = True # Whether to restore saved state on startup ;restore = True # The directory where state is saved ;statedir = /var/lib/mandos # Whether to run in the foreground ;foreground = False mandos-1.6.0/initramfs-tools-hook-conf0000664000175000017500000000062711767455003020220 0ustar teddyteddy00000000000000# -*- shell-script -*- # if mkinitramfs is started by mkinitramfs-kpkg, mkinitramfs-kpkg has # already touched the initrd file with umask 022 before we had a # chance to affect it. We cannot allow a readable initrd file, # therefore we must fix this now. if [ -e "${outfile}" ] \ && [ `stat --format=%s "${outfile}"` -eq 0 ]; then rm "${outfile}" (umask 027; touch "${outfile}") fi UMASK=027 mandos-1.6.0/mandos-keygen.xml0000664000175000017500000003467711767455003016561 0ustar teddyteddy00000000000000 %common; ]> Mandos Manual Mandos &version; &TIMESTAMP; Björn Påhlsson
belorn@recompile.se
Teddy Hogeborn
teddy@recompile.se
2008 2009 2011 2012 Teddy Hogeborn Björn Påhlsson
&COMMANDNAME; 8 &COMMANDNAME; Generate key and password for Mandos client and server. &COMMANDNAME; &COMMANDNAME; FILE &COMMANDNAME; &COMMANDNAME; DESCRIPTION &COMMANDNAME; is a program to generate the OpenPGP key used by mandos-client 8mandos. The key is normally written to /etc/mandos for later installation into the initrd image, but this, and most other things, can be changed with command line options. This program can also be used with the or options to generate a ready-made section for clients.conf (see mandos-clients.conf 5). PURPOSE The purpose of this is to enable remote and unattended rebooting of client host computer with an encrypted root file system. See for details. OPTIONS Show a help message and exit Target directory for key files. Default is /etc/mandos. Key type. Default is DSA. Key length in bits. Default is 2048. Subkey type. Default is ELG-E (Elgamal encryption-only). Subkey length in bits. Default is 2048. Email address of key. Default is empty. Comment field for key. The default value is Mandos client key. Key expire time. Default is no expiration. See gpg 1 for syntax. Force overwriting old key. Prompt for a password and encrypt it with the key already present in either /etc/mandos or the directory specified with the option. Outputs, on standard output, a section suitable for inclusion in mandos-clients.conf8. The host name or the name specified with the option is used for the section header. All other options are ignored, and no key is created. The same as , but read from FILE, not the terminal. OVERVIEW This program is a small utility to generate new OpenPGP keys for new Mandos clients, and to generate sections for inclusion in clients.conf on the server. EXIT STATUS The exit status will be 0 if a new key (or password, if the option was used) was successfully created, otherwise not. ENVIRONMENT TMPDIR If set, temporary files will be created here. See mktemp 1. FILES Use the option to change where &COMMANDNAME; will write the key files. The default file names are shown here. /etc/mandos/seckey.txt OpenPGP secret key file which will be created or overwritten. /etc/mandos/pubkey.txt OpenPGP public key file which will be created or overwritten. /tmp Temporary files will be written here if TMPDIR is not set. EXAMPLE Normal invocation needs no options: &COMMANDNAME; Create key in another directory and of another type. Force overwriting old key files: &COMMANDNAME; --dir ~/keydir --type RSA --force Prompt for a password, encrypt it with the key in /etc/mandos and output a section suitable for clients.conf. &COMMANDNAME; --password Prompt for a password, encrypt it with the key in the client-key directory and output a section suitable for clients.conf. &COMMANDNAME; --password --dir client-key SECURITY The , , , and options can be used to create keys of low security. If in doubt, leave them to the default values. The key expire time is not guaranteed to be honored by mandos 8. SEE ALSO intro 8mandos, gpg 1, mandos-clients.conf 5, mandos 8, mandos-client 8mandos
mandos-1.6.0/default-mandos0000664000175000017500000000025611767455003016106 0ustar teddyteddy00000000000000# Directory where configuration files are located. Default is # "/etc/mandos". # #CONFIGDIR=/etc/mandos # Additional options that are passed to the Daemon. DAEMON_ARGS="" mandos-1.6.0/clients.conf0000664000175000017500000000615511767455003015574 0ustar teddyteddy00000000000000# Default settings for all clients. These values are the default # values, so uncomment and change them if you want different ones. [DEFAULT] # How long until a client is disabled and not be allowed to get the # data this server holds. ;timeout = 5m # How often to run the checker to confirm that a client is still up. # Note: a new checker will not be started if an old one is still # running. The server will wait for a checker to complete until the # above "timeout" occurs, at which time the client will be disabled, # and any running checker killed. ;interval = 2m # Extended timeout is an added timeout that is given once after a # password has been sent sucessfully to a client. This allows for # additional delays caused by file system checks and quota checks. ;extended_timeout = 15m # What command to run as "the checker". ;checker = fping -q -- %%(host)s # Whether to approve a client by default after the approval delay. ;approved_by_default = True # How long to wait for approval. ;approval_delay = 0s # How long one approval will last. ;approval_duration = 1s # Whether this client is enabled by default ;enabled = True ;#### ;# Example client ;[foo] ; ;# OpenPGP key fingerprint ;fingerprint = 7788 2722 5BA7 DE53 9C5A 7CFA 59CF F7CD BD9A 5920 ; ;# This is base64-encoded binary data. It will be decoded and sent to ;# the client matching the above fingerprint. This should, of course, ;# be OpenPGP encrypted data, decryptable only by the client. ;secret = ; hQIOA6QdEjBs2L/HEAf/TCyrDe5Xnm9esa+Pb/vWF9CUqfn4srzVgSu234 ; REJMVv7lBSrPE2132Lmd2gqF1HeLKDJRSVxJpt6xoWOChGHg+TMyXDxK+N ; Xl89vGvdU1XfhKkVm9MDLOgT5ECDPysDGHFPDhqHOSu3Kaw2DWMV/iH9vz ; 3Z20erVNbdcvyBnuojcoWO/6yfB5EQO0BXp7kcyy00USA3CjD5FGZdoQGI ; Tb8A/ar0tVA5crSQmaSotm6KmNLhrFnZ5BxX+TiE+eTUTqSloWRY6VAvqW ; QHC7OASxK5E6RXPBuFH5IohUA2Qbk5AHt99pYvsIPX88j2rWauOokoiKZo ; t/9leJ8VxO5l3wf/U64IH8bkPIoWmWZfd/nqh4uwGNbCgKMyT+AnvH7kMJ ; 3i7DivfWl2mKLV0PyPHUNva0VQxX6yYjcOhj1R6fCr/at8/NSLe2OhLchz ; dC+Ls9h+kvJXgF8Sisv+Wk/1RadPLFmraRlqvJwt6Ww21LpiXqXHV2mIgq ; WnR98YgSvUi3TJHrUQiNc9YyBzuRo0AjgG2C9qiE3FM+Y28+iQ/sR3+bFs ; zYuZKVTObqiIslwXu7imO0cvvFRgJF/6u3HNFQ4LUTGhiM3FQmC6NNlF3/ ; vJM2hwRDMcJqDd54Twx90Wh+tYz0z7QMsK4ANXWHHWHR0JchnLWmenzbtW ; 5MHdW9AYsNJZAQSOpirE4Xi31CSlWAi9KV+cUCmWF5zOFy1x23P6PjdaRm ; 4T2zw4dxS5NswXWU0sVEXxjs6PYxuIiCTL7vdpx8QjBkrPWDrAbcMyBr2O ; QlnHIvPzEArRQLo= ; ;# Host name; used only by the checker, not used by the server itself. ;host = foo.example.org ;#### ;#### ;# Another example client, named "bar". ;[bar] ;# The fingerprint is not space or case sensitive ;fingerprint = 3e393aeaefb84c7e89e2f547b3a107558fca3a27 ; ;# If "secret" is not specified, a file can be read for the data. ;secfile = /etc/mandos/bar-secret.bin ; ;# An IP address for host is also fine, if the checker accepts it. ;host = 192.0.2.3 ; ;# Parameters from the [DEFAULT] section can be overridden per client. ;interval = 1m ; ;# This client requires manual approval before it receives its secret. ;approved_by_default = False ;# Require approval within 30 seconds. ;approval_delay = 30s ;#### mandos-1.6.0/initramfs-tools-script0000775000175000017500000000731311767455003017643 0ustar teddyteddy00000000000000#!/bin/sh -e # # This script will run in the initrd environment at boot and edit # /conf/conf.d/cryptroot to set /lib/mandos/plugin-runner as keyscript # when no other keyscript is set, before cryptsetup. # # This script should be installed as # "/usr/share/initramfs-tools/scripts/init-premount/mandos" which will # eventually be "/scripts/init-premount/mandos" in the initrd.img # file. PREREQ="udev" prereqs() { echo "$PREREQ" } case $1 in prereqs) prereqs exit 0 ;; esac . /scripts/functions for param in `cat /proc/cmdline`; do case "$param" in ip=*) IPOPTS="${param#ip=}" ;; mandos=*) # Split option line on commas old_ifs="$IFS" IFS="$IFS," for mpar in ${param#mandos=}; do IFS="$old_ifs" case "$mpar" in off) exit 0 ;; connect) connect="" ;; connect:*) connect="${mpar#connect:}" ;; *) log_warning_msg "$0: Bad option ${mpar}" ;; esac done unset mpar IFS="$old_ifs" unset old_ifs ;; esac done unset param chmod a=rwxt /tmp test -r /conf/conf.d/cryptroot test -w /conf/conf.d # Get DEVICE from /conf/initramfs.conf and other files . /conf/initramfs.conf for conf in /conf/conf.d/*; do [ -f ${conf} ] && . ${conf} done if [ -e /conf/param.conf ]; then . /conf/param.conf fi # Override DEVICE from sixth field of ip= kernel option, if passed case "$IPOPTS" in *:*:*:*:*:*) # At least six fields # Remove the first five fields device="${IPOPTS#*:*:*:*:*:}" # Remove all fields except the first one DEVICE="${device%%:*}" ;; esac # Add device setting (if any) to plugin-runner.conf if [ "${DEVICE+set}" = set ]; then # Did we get the device from an ip= option? if [ "${device+set}" = set ]; then # Let ip= option override local config; append: cat <<-EOF >>/conf/conf.d/mandos/plugin-runner.conf --options-for=mandos-client:--interface=${DEVICE} EOF else # Prepend device setting so any later options would override: sed -i -e \ '1i--options-for=mandos-client:--interface='"${DEVICE}" \ /conf/conf.d/mandos/plugin-runner.conf fi fi unset device # If we are connecting directly, run "configure_networking" (from # /scripts/functions); it needs IPOPTS and DEVICE if [ "${connect+set}" = set ]; then configure_networking if [ -n "$connect" ]; then cat <<-EOF >>/conf/conf.d/mandos/plugin-runner.conf --options-for=mandos-client:--connect=${connect} EOF fi fi # Do not replace cryptroot file unless we need to. replace_cryptroot=no # Our keyscript mandos=/lib/mandos/plugin-runner test -x "$mandos" # parse /conf/conf.d/cryptroot. Format: # target=sda2_crypt,source=/dev/sda2,key=none,keyscript=/foo/bar/baz exec 3>/conf/conf.d/cryptroot.mandos while read options; do newopts="" # Split option line on commas old_ifs="$IFS" IFS="$IFS," for opt in $options; do # Find the keyscript option, if any case "$opt" in keyscript=*) keyscript="${opt#keyscript=}" newopts="$newopts,$opt" ;; "") : ;; *) newopts="$newopts,$opt" ;; esac done IFS="$old_ifs" unset old_ifs # If there was no keyscript option, add one. if [ -z "$keyscript" ]; then replace_cryptroot=yes newopts="$newopts,keyscript=$mandos" fi newopts="${newopts#,}" echo "$newopts" >&3 done < /conf/conf.d/cryptroot exec 3>&- # If we need to, replace the old cryptroot file with the new file. if [ "$replace_cryptroot" = yes ]; then mv /conf/conf.d/cryptroot /conf/conf.d/cryptroot.mandos-old mv /conf/conf.d/cryptroot.mandos /conf/conf.d/cryptroot else rm /conf/conf.d/cryptroot.mandos fi mandos-1.6.0/TODO0000664000175000017500000001072411767455003013751 0ustar teddyteddy00000000000000-*- org -*- * [[http://www.undeadly.org/cgi?action=article&sid=20110530221728][OpenBSD]] * mandos-applet * mandos-client ** TODO [#B] Use capabilities instead of seteuid(). ** TODO [#B] Use struct sockaddr_storage instead of a union ** TODO [#B] Use getaddrinfo(hints=AI_NUMERICHOST) instead of inet_pton() ** TODO [#B] Use getnameinfo(serv=NULL, NI_NUMERICHOST) instead of inet_ntop() ** TODO [#B] Prefer /run/tmp over /tmp, if it exists ** TODO [#C] Make start_mandos_communication() take "struct server". * splashy ** TODO [#B] use scandir(3) instead of readdir(3) * usplash (Deprecated) ** TODO [#A] Make it work again ** TODO [#B] use scandir(3) instead of readdir(3) * askpass-fifo ** TODO [#B] Drop privileges after opening FIFO. * password-prompt ** TODO [#B] lock stdin (with flock()?) * plymouth * TODO [#B] passdev * plugin-runner ** TODO handle printing for errors for plugins *** Hook up stderr of plugins, buffer them, and prepend "Mandos Plugin [plugin name]" ** TODO [#B] use scandir(3) instead of readdir(3) ** TODO [#C] use same file name rules as run-parts(8) ** kernel command line option for debug info ** TODO [#B] Use openat() * mandos (server) ** TODO [#B] Log level :BUGS: *** TODO /etc/mandos/clients.d/*.conf Watch this directory and add/remove/update clients? ** TODO [#C] config for TXT record ** TODO Log level dbus option SetLogLevel D-Bus call ** TODO [#C] DBusServiceObjectUsingSuper ** TODO [#B] Global enable/disable flag ** TODO [#B] By-client countdown on number of secrets given ** TODO [#B] Support RFC 3339 time duration syntax ** D-Bus Client method NeedsPassword(50) - Timeout, default disapprove + SetPass(u"gazonk", True) -> Approval, persistent + Approve(False) -> Close client connection immediately ** TODO [#C] python-parsedatetime ** TODO [#C] systemd/launchd http://0pointer.de/blog/projects/systemd.html http://wiki.debian.org/systemd ** TODO Separate logging logic to own object ** TODO [#A] Limit approval_delay to max gnutls/tls timeout value ** TODO [#B] break the wait on approval_delay if connection dies ** TODO Generate Client.runtime_expansions from client options + extra ** TODO Allow %%(checker)s as a runtime expansion ** TODO Use python-tlslite? ** TODO D-Bus AddClient() method on server object ** TODO Use org.freedesktop.DBus.Method.NoReply annotation on async methods. ** TODO Emit [[http://dbus.freedesktop.org/doc/dbus-specification.html#standard-interfaces-properties][org.freedesktop.DBus.Properties.PropertiesChanged]] signal TODO Deprecate se.recompile.Mandos.Client.PropertyChanged - annotate! TODO Can use "invalidates" annotation to also emit on changed secret. ** TODO Support [[http://dbus.freedesktop.org/doc/dbus-specification.html#standard-interfaces-objectmanager][org.freedesktop.DBus.ObjectManager]] interface on server object Deprecate methods GetAllClients(), GetAllClientsWithProperties() and signals ClientAdded and ClientRemoved. ** TODO Save state periodically to recover better from hard shutdowns ** TODO CheckerCompleted method, deprecate CheckedOK ** TODO Secret Service API? http://standards.freedesktop.org/secret-service/ * mandos.xml ** Add mandos contact info in manual pages * mandos-ctl *** Handle "no D-Bus server" and/or "no Mandos server found" better *** [#B] --dump option ** TODO Support RFC 3339 time duration syntax * TODO mandos-dispatch Listens for specified D-Bus signals and spawns shell commands with arguments. * mandos-monitor ** TODO help should be toggleable ** Urwid client data displayer Better view of client data in the listing *** Properties popup ** Print a nice "We are sorry" message, save stack trace to log. ** Show timeout countdown for approval * mandos-keygen ** TODO "--secfile" option Using the "secfile" option instead of "secret" ** TODO [#B] "--test" option For testing decryption before rebooting. * Makefile ** TODO [#C] Implement DEB_BUILD_OPTIONS http://www.debian.org/doc/debian-policy/ch-source.html#s-debianrules-options * Package ** /usr/share/initramfs-tools/hooks/mandos *** TODO [#C] use same file name rules as run-parts(8) *** TODO [#C] Do not install in initrd.img if configured not to. Use "/etc/initramfs-tools/hooksconf.d/mandos"? ** TODO [#C] /etc/bash_completion.d/mandos From XML sources directly? * Side Stuff ** TODO Locate which package moves the other bin/sh when busybox is deactivated ** TODO contact owner of package, and ask them to have that shell static in position regardless of busybox #+STARTUP: showall mandos-1.6.0/mandos.lsm0000664000175000017500000000160311767455003015253 0ustar teddyteddy00000000000000Begin4 Title: Mandos Version: 1.6.0 Entered-date: 2012-06-18 Description: The Mandos system allows computers to have encrypted root file systems and at the same time be capable of remote and/or unattended reboots. Keywords: boot, encryption, luks, cryptsetup, network, openpgp, tls, dm-crypt Author: teddy@recompile.se (Teddy Hogeborn), belorn@recompile.se (Björn Påhlsson) Maintained-by: teddy@recompile.se (Teddy Hogeborn), belorn@recompile.se (Björn Påhlsson) Primary-site: http://www.recompile.se/mandos 150K mandos_1.6.0.orig.tar.gz Alternate-site: ftp://ftp.recompile.se/pub/mandos 150K mandos_1.6.0.orig.tar.gz Platforms: Requires GCC, GNU libC, Avahi, GnuPG, Python 2.6, and various other libraries. While made for Debian GNU/Linux, it is probably portable to other distributions, but not other Unixes. Copying-policy: GNU General Public License version 3.0 or later End mandos-1.6.0/legalnotice.xml0000664000175000017500000000201211767455003016260 0ustar teddyteddy00000000000000 This manual page 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 of the License, or (at your option) any later version. This manual page 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/. mandos-1.6.0/mandos-ctl.xml0000664000175000017500000004011211767455003016036 0ustar teddyteddy00000000000000 %common; ]> Mandos Manual Mandos &version; &TIMESTAMP; Björn Påhlsson
belorn@recompile.se
Teddy Hogeborn
teddy@recompile.se
2010 2011 2012 Teddy Hogeborn Björn Påhlsson
&COMMANDNAME; 8 &COMMANDNAME; Control the operation of the Mandos server &COMMANDNAME; CLIENT &COMMANDNAME; CLIENT &COMMANDNAME; CLIENT &COMMANDNAME; &COMMANDNAME; DESCRIPTION &COMMANDNAME; is a program to control the operation of the Mandos server mandos8. This program can be used to change client settings, approve or deny client requests, and to remove clients from the server. PURPOSE The purpose of this is to enable remote and unattended rebooting of client host computer with an encrypted root file system. See for details. OPTIONS Show a help message and exit Enable client(s). An enabled client will be eligble to receive its secret. Disable client(s). A disabled client will not be eligble to receive its secret, and no checkers will be started for it. Bump the timeout of the specified client(s), just as if a checker had completed successfully for it/them. Start a new checker now for the specified client(s). Stop any running checker for the specified client(s). Remove the specified client(s) from the server. Set the checker option of the specified client(s); see mandos-clients.conf5. Set the timeout option of the specified client(s); see mandos-clients.conf5. Set the extended_timeout option of the specified client(s); see mandos-clients.conf5. Set the interval option of the specified client(s); see mandos-clients.conf5. Set the approved_by_default option of the specified client(s) to True or False, respectively; see mandos-clients.conf5. Set the approval_delay option of the specified client(s); see mandos-clients.conf5. Set the approval_duration option of the specified client(s); see mandos-clients.conf5. Set the host option of the specified client(s); see mandos-clients.conf5. Set the secfile option of the specified client(s); see mandos-clients.conf5. Approve client(s) if currently waiting for approval. Deny client(s) if currently waiting for approval. Make the client-modifying options modify all clients. Show all client settings, not just a subset. Check if a single client is enabled or not, and exit with a successful exit status only if the client is enabled. OVERVIEW This program is a small utility to generate new OpenPGP keys for new Mandos clients, and to generate sections for inclusion in clients.conf on the server. EXIT STATUS If the option is used, the exit status will be 0 only if the specified client is enabled. EXAMPLE To list all clients: &COMMANDNAME; To list all settings for the clients named foo1.example.org and foo2.example.org: &COMMANDNAME; --verbose foo1.example.org foo2.example.org To enable all clients: &COMMANDNAME; --enable --all To change timeout and interval value for the clients named foo1.example.org and foo2.example.org: &COMMANDNAME; --timeout="5m" --interval="1m" foo1.example.org foo2.example.org To approve all clients currently waiting for it: &COMMANDNAME; --approve --all SECURITY This program must be permitted to access the Mandos server via the D-Bus interface. This normally requires the root user, but could be configured otherwise by reconfiguring the D-Bus server. SEE ALSO intro 8mandos, mandos 8, mandos-clients.conf 5, mandos-monitor 8
mandos-1.6.0/init.d-mandos0000664000175000017500000001035111767455003015644 0ustar teddyteddy00000000000000#! /bin/sh ### BEGIN INIT INFO # Provides: mandos # Required-Start: $remote_fs $syslog avahi # Required-Stop: $remote_fs $syslog avahi # Default-Start: 2 3 4 5 # Default-Stop: 0 1 6 # Short-Description: Mandos server # Description: Gives encrypted passwords to Mandos clients ### END INIT INFO # Author: Teddy Hogeborn # Author: Björn Påhlsson # # Please remove the "Author" lines above and replace them # with your own name if you copy and modify this script. # Do NOT "set -e" # PATH should only include /usr/* if it runs after the mountnfs.sh script PATH=/sbin:/usr/sbin:/bin:/usr/bin DESC="Mandos root file system password server" NAME=mandos DAEMON=/usr/sbin/$NAME DAEMON_ARGS="" PIDFILE=/var/run/$NAME.pid SCRIPTNAME=/etc/init.d/$NAME # Exit if the package is not installed [ -x "$DAEMON" ] || exit 0 # Read configuration variable file if it is present [ -r /etc/default/$NAME ] && . /etc/default/$NAME if [ -n "$CONFIGDIR" ]; then DAEMON_ARGS="$DAEMON_ARGS --configdir $CONFIGDIR" fi # Load the VERBOSE setting and other rcS variables . /lib/init/vars.sh # Define LSB log_* functions. # Depend on lsb-base (>= 3.0-6) to ensure that this file is present. . /lib/lsb/init-functions # # Function that starts the daemon/service # do_start() { # Return # 0 if daemon has been started # 1 if daemon was already running # 2 if daemon could not be started start-stop-daemon --start --quiet --pidfile $PIDFILE --exec $DAEMON --test > /dev/null \ || return 1 start-stop-daemon --start --quiet --pidfile $PIDFILE --exec $DAEMON -- \ $DAEMON_ARGS \ || return 2 # Add code here, if necessary, that waits for the process to be ready # to handle requests from services started subsequently which depend # on this one. As a last resort, sleep for some time. } # # Function that stops the daemon/service # do_stop() { # Return # 0 if daemon has been stopped # 1 if daemon was already stopped # 2 if daemon could not be stopped # other if a failure occurred start-stop-daemon --stop --quiet --retry=TERM/30/KILL/5 --pidfile $PIDFILE --name $NAME RETVAL="$?" [ "$RETVAL" = 2 ] && return 2 # Wait for children to finish too if this is a daemon that forks # and if the daemon is only ever run from this initscript. # If the above conditions are not satisfied then add some other code # that waits for the process to drop all resources that could be # needed by services started subsequently. A last resort is to # sleep for some time. start-stop-daemon --stop --quiet --oknodo --retry=0/30/KILL/5 --exec $DAEMON [ "$?" = 2 ] && return 2 # Many daemons don't delete their pidfiles when they exit. rm -f $PIDFILE return "$RETVAL" } # # Function that sends a SIGHUP to the daemon/service # do_reload() { # # If the daemon can reload its configuration without # restarting (for example, when it is sent a SIGHUP), # then implement that here. # start-stop-daemon --stop --signal 1 --quiet --pidfile $PIDFILE --name $NAME return 0 } case "$1" in start) [ "$VERBOSE" != no ] && log_daemon_msg "Starting $DESC" "$NAME" do_start case "$?" in 0|1) [ "$VERBOSE" != no ] && log_end_msg 0 ;; 2) [ "$VERBOSE" != no ] && log_end_msg 1 ;; esac ;; stop) [ "$VERBOSE" != no ] && log_daemon_msg "Stopping $DESC" "$NAME" do_stop case "$?" in 0|1) [ "$VERBOSE" != no ] && log_end_msg 0 ;; 2) [ "$VERBOSE" != no ] && log_end_msg 1 ;; esac ;; #reload|force-reload) # # If do_reload() is not implemented then leave this commented out # and leave 'force-reload' as an alias for 'restart'. # #log_daemon_msg "Reloading $DESC" "$NAME" #do_reload #log_end_msg $? #;; restart|force-reload) # # If the "reload" option is implemented then remove the # 'force-reload' alias # log_daemon_msg "Restarting $DESC" "$NAME" do_stop case "$?" in 0|1) do_start case "$?" in 0) log_end_msg 0 ;; 1) log_end_msg 1 ;; # Old process is still running *) log_end_msg 1 ;; # Failed to start esac ;; *) # Failed to stop log_end_msg 1 ;; esac ;; status) status_of_proc "$DAEMON" "$NAME" -p "$PIDFILE" ;; *) #echo "Usage: $SCRIPTNAME {start|stop|restart|reload|force-reload}" >&2 echo "Usage: $SCRIPTNAME {start|stop|restart|force-reload}" >&2 exit 3 ;; esac : mandos-1.6.0/plugin-runner.c0000664000175000017500000010301611767455003016227 0ustar teddyteddy00000000000000/* -*- coding: utf-8; mode: c; mode: orgtbl -*- */ /* * Mandos plugin runner - Run Mandos plugins * * Copyright © 2008-2012 Teddy Hogeborn * Copyright © 2008-2012 Björn Påhlsson * * 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 of the * License, 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 * . * * Contact the authors at . */ #define _GNU_SOURCE /* TEMP_FAILURE_RETRY(), getline(), asprintf(), O_CLOEXEC */ #include /* size_t, NULL */ #include /* malloc(), exit(), EXIT_SUCCESS, realloc() */ #include /* bool, true, false */ #include /* fileno(), fprintf(), stderr, STDOUT_FILENO */ #include /* DIR, fdopendir(), stat(), struct stat, waitpid(), WIFEXITED(), WEXITSTATUS(), wait(), pid_t, uid_t, gid_t, getuid(), getgid(), dirfd() */ #include /* fd_set, select(), FD_ZERO(), FD_SET(), FD_ISSET(), FD_CLR */ #include /* wait(), waitpid(), WIFEXITED(), WEXITSTATUS(), WTERMSIG(), WCOREDUMP() */ #include /* struct stat, stat(), S_ISREG() */ #include /* and, or, not */ #include /* DIR, struct dirent, fdopendir(), readdir(), closedir(), dirfd() */ #include /* struct stat, stat(), S_ISREG(), fcntl(), setuid(), setgid(), F_GETFD, F_SETFD, FD_CLOEXEC, access(), pipe(), fork(), close() dup2(), STDOUT_FILENO, _exit(), execv(), write(), read(), close() */ #include /* fcntl(), F_GETFD, F_SETFD, FD_CLOEXEC */ #include /* strsep, strlen(), asprintf(), strsignal(), strcmp(), strncmp() */ #include /* errno */ #include /* struct argp_option, struct argp_state, struct argp, argp_parse(), ARGP_ERR_UNKNOWN, ARGP_KEY_END, ARGP_KEY_ARG, error_t */ #include /* struct sigaction, sigemptyset(), sigaddset(), sigaction(), sigprocmask(), SIG_BLOCK, SIGCHLD, SIG_UNBLOCK, kill(), sig_atomic_t */ #include /* errno, EBADF */ #include /* intmax_t, PRIdMAX, strtoimax() */ #include /* EX_OSERR, EX_USAGE, EX_IOERR, EX_CONFIG, EX_UNAVAILABLE, EX_OK */ #include /* errno */ #include /* error() */ #define BUFFER_SIZE 256 #define PDIR "/lib/mandos/plugins.d" #define AFILE "/conf/conf.d/mandos/plugin-runner.conf" const char *argp_program_version = "plugin-runner " VERSION; const char *argp_program_bug_address = ""; typedef struct plugin{ char *name; /* can be NULL or any plugin name */ char **argv; int argc; char **environ; int envc; bool disabled; /* Variables used for running processes*/ pid_t pid; int fd; char *buffer; size_t buffer_size; size_t buffer_length; bool eof; volatile sig_atomic_t completed; int status; struct plugin *next; } plugin; static plugin *plugin_list = NULL; /* Gets an existing plugin based on name, or if none is found, creates a new one */ static plugin *getplugin(char *name){ /* Check for existing plugin with that name */ for(plugin *p = plugin_list; p != NULL; p = p->next){ if((p->name == name) or (p->name and name and (strcmp(p->name, name) == 0))){ return p; } } /* Create a new plugin */ plugin *new_plugin = NULL; do { new_plugin = malloc(sizeof(plugin)); } while(new_plugin == NULL and errno == EINTR); if(new_plugin == NULL){ return NULL; } char *copy_name = NULL; if(name != NULL){ do { copy_name = strdup(name); } while(copy_name == NULL and errno == EINTR); if(copy_name == NULL){ int e = errno; free(new_plugin); errno = e; return NULL; } } *new_plugin = (plugin){ .name = copy_name, .argc = 1, .disabled = false, .next = plugin_list }; do { new_plugin->argv = malloc(sizeof(char *) * 2); } while(new_plugin->argv == NULL and errno == EINTR); if(new_plugin->argv == NULL){ int e = errno; free(copy_name); free(new_plugin); errno = e; return NULL; } new_plugin->argv[0] = copy_name; new_plugin->argv[1] = NULL; do { new_plugin->environ = malloc(sizeof(char *)); } while(new_plugin->environ == NULL and errno == EINTR); if(new_plugin->environ == NULL){ int e = errno; free(copy_name); free(new_plugin->argv); free(new_plugin); errno = e; return NULL; } new_plugin->environ[0] = NULL; /* Append the new plugin to the list */ plugin_list = new_plugin; return new_plugin; } /* Helper function for add_argument and add_environment */ __attribute__((nonnull)) static bool add_to_char_array(const char *new, char ***array, int *len){ /* Resize the pointed-to array to hold one more pointer */ do { *array = realloc(*array, sizeof(char *) * (size_t) ((*len) + 2)); } while(*array == NULL and errno == EINTR); /* Malloc check */ if(*array == NULL){ return false; } /* Make a copy of the new string */ char *copy; do { copy = strdup(new); } while(copy == NULL and errno == EINTR); if(copy == NULL){ return false; } /* Insert the copy */ (*array)[*len] = copy; (*len)++; /* Add a new terminating NULL pointer to the last element */ (*array)[*len] = NULL; return true; } /* Add to a plugin's argument vector */ __attribute__((nonnull(2))) static bool add_argument(plugin *p, const char *arg){ if(p == NULL){ return false; } return add_to_char_array(arg, &(p->argv), &(p->argc)); } /* Add to a plugin's environment */ __attribute__((nonnull(2))) static bool add_environment(plugin *p, const char *def, bool replace){ if(p == NULL){ return false; } /* namelen = length of name of environment variable */ size_t namelen = (size_t)(strchrnul(def, '=') - def); /* Search for this environment variable */ for(char **e = p->environ; *e != NULL; e++){ if(strncmp(*e, def, namelen + 1) == 0){ /* It already exists */ if(replace){ char *new; do { new = realloc(*e, strlen(def) + 1); } while(new == NULL and errno == EINTR); if(new == NULL){ return false; } *e = new; strcpy(*e, def); } return true; } } return add_to_char_array(def, &(p->environ), &(p->envc)); } /* * Based on the example in the GNU LibC manual chapter 13.13 "File * Descriptor Flags". | [[info:libc:Descriptor%20Flags][File Descriptor Flags]] | */ static int set_cloexec_flag(int fd){ int ret = (int)TEMP_FAILURE_RETRY(fcntl(fd, F_GETFD, 0)); /* If reading the flags failed, return error indication now. */ if(ret < 0){ return ret; } /* Store modified flag word in the descriptor. */ return (int)TEMP_FAILURE_RETRY(fcntl(fd, F_SETFD, ret | FD_CLOEXEC)); } /* Mark processes as completed when they exit, and save their exit status. */ static void handle_sigchld(__attribute__((unused)) int sig){ int old_errno = errno; while(true){ plugin *proc = plugin_list; int status; pid_t pid = waitpid(-1, &status, WNOHANG); if(pid == 0){ /* Only still running child processes */ break; } if(pid == -1){ if(errno == ECHILD){ /* No child processes */ break; } error(0, errno, "waitpid"); } /* A child exited, find it in process_list */ while(proc != NULL and proc->pid != pid){ proc = proc->next; } if(proc == NULL){ /* Process not found in process list */ continue; } proc->status = status; proc->completed = 1; } errno = old_errno; } /* Prints out a password to stdout */ __attribute__((nonnull)) static bool print_out_password(const char *buffer, size_t length){ ssize_t ret; for(size_t written = 0; written < length; written += (size_t)ret){ ret = TEMP_FAILURE_RETRY(write(STDOUT_FILENO, buffer + written, length - written)); if(ret < 0){ return false; } } return true; } /* Removes and free a plugin from the plugin list */ __attribute__((nonnull)) static void free_plugin(plugin *plugin_node){ for(char **arg = plugin_node->argv; *arg != NULL; arg++){ free(*arg); } free(plugin_node->argv); for(char **env = plugin_node->environ; *env != NULL; env++){ free(*env); } free(plugin_node->environ); free(plugin_node->buffer); /* Removes the plugin from the singly-linked list */ if(plugin_node == plugin_list){ /* First one - simple */ plugin_list = plugin_list->next; } else { /* Second one or later */ for(plugin *p = plugin_list; p != NULL; p = p->next){ if(p->next == plugin_node){ p->next = plugin_node->next; break; } } } free(plugin_node); } static void free_plugin_list(void){ while(plugin_list != NULL){ free_plugin(plugin_list); } } int main(int argc, char *argv[]){ char *plugindir = NULL; char *argfile = NULL; FILE *conffp; size_t d_name_len; DIR *dir = NULL; struct dirent *dirst; struct stat st; fd_set rfds_all; int ret, maxfd = 0; ssize_t sret; uid_t uid = 65534; gid_t gid = 65534; bool debug = false; int exitstatus = EXIT_SUCCESS; struct sigaction old_sigchld_action; struct sigaction sigchld_action = { .sa_handler = handle_sigchld, .sa_flags = SA_NOCLDSTOP }; char **custom_argv = NULL; int custom_argc = 0; /* Establish a signal handler */ sigemptyset(&sigchld_action.sa_mask); ret = sigaddset(&sigchld_action.sa_mask, SIGCHLD); if(ret == -1){ error(0, errno, "sigaddset"); exitstatus = EX_OSERR; goto fallback; } ret = sigaction(SIGCHLD, &sigchld_action, &old_sigchld_action); if(ret == -1){ error(0, errno, "sigaction"); exitstatus = EX_OSERR; goto fallback; } /* The options we understand. */ struct argp_option options[] = { { .name = "global-options", .key = 'g', .arg = "OPTION[,OPTION[,...]]", .doc = "Options passed to all plugins" }, { .name = "global-env", .key = 'G', .arg = "VAR=value", .doc = "Environment variable passed to all plugins" }, { .name = "options-for", .key = 'o', .arg = "PLUGIN:OPTION[,OPTION[,...]]", .doc = "Options passed only to specified plugin" }, { .name = "env-for", .key = 'E', .arg = "PLUGIN:ENV=value", .doc = "Environment variable passed to specified plugin" }, { .name = "disable", .key = 'd', .arg = "PLUGIN", .doc = "Disable a specific plugin", .group = 1 }, { .name = "enable", .key = 'e', .arg = "PLUGIN", .doc = "Enable a specific plugin", .group = 1 }, { .name = "plugin-dir", .key = 128, .arg = "DIRECTORY", .doc = "Specify a different plugin directory", .group = 2 }, { .name = "config-file", .key = 129, .arg = "FILE", .doc = "Specify a different configuration file", .group = 2 }, { .name = "userid", .key = 130, .arg = "ID", .flags = 0, .doc = "User ID the plugins will run as", .group = 3 }, { .name = "groupid", .key = 131, .arg = "ID", .flags = 0, .doc = "Group ID the plugins will run as", .group = 3 }, { .name = "debug", .key = 132, .doc = "Debug mode", .group = 4 }, /* * These reproduce what we would get without ARGP_NO_HELP */ { .name = "help", .key = '?', .doc = "Give this help list", .group = -1 }, { .name = "usage", .key = -3, .doc = "Give a short usage message", .group = -1 }, { .name = "version", .key = 'V', .doc = "Print program version", .group = -1 }, { .name = NULL } }; __attribute__((nonnull(3))) error_t parse_opt(int key, char *arg, struct argp_state *state){ errno = 0; switch(key){ char *tmp; intmax_t tmp_id; case 'g': /* --global-options */ { char *plugin_option; while((plugin_option = strsep(&arg, ",")) != NULL){ if(not add_argument(getplugin(NULL), plugin_option)){ break; } } } break; case 'G': /* --global-env */ add_environment(getplugin(NULL), arg, true); break; case 'o': /* --options-for */ { char *option_list = strchr(arg, ':'); if(option_list == NULL){ argp_error(state, "No colon in \"%s\"", arg); errno = EINVAL; break; } *option_list = '\0'; option_list++; if(arg[0] == '\0'){ argp_error(state, "Empty plugin name"); errno = EINVAL; break; } char *option; while((option = strsep(&option_list, ",")) != NULL){ if(not add_argument(getplugin(arg), option)){ break; } } } break; case 'E': /* --env-for */ { char *envdef = strchr(arg, ':'); if(envdef == NULL){ argp_error(state, "No colon in \"%s\"", arg); errno = EINVAL; break; } *envdef = '\0'; envdef++; if(arg[0] == '\0'){ argp_error(state, "Empty plugin name"); errno = EINVAL; break; } add_environment(getplugin(arg), envdef, true); } break; case 'd': /* --disable */ { plugin *p = getplugin(arg); if(p != NULL){ p->disabled = true; } } break; case 'e': /* --enable */ { plugin *p = getplugin(arg); if(p != NULL){ p->disabled = false; } } break; case 128: /* --plugin-dir */ free(plugindir); plugindir = strdup(arg); break; case 129: /* --config-file */ /* This is already done by parse_opt_config_file() */ break; case 130: /* --userid */ tmp_id = strtoimax(arg, &tmp, 10); if(errno != 0 or tmp == arg or *tmp != '\0' or tmp_id != (uid_t)tmp_id){ argp_error(state, "Bad user ID number: \"%s\", using %" PRIdMAX, arg, (intmax_t)uid); break; } uid = (uid_t)tmp_id; break; case 131: /* --groupid */ tmp_id = strtoimax(arg, &tmp, 10); if(errno != 0 or tmp == arg or *tmp != '\0' or tmp_id != (gid_t)tmp_id){ argp_error(state, "Bad group ID number: \"%s\", using %" PRIdMAX, arg, (intmax_t)gid); break; } gid = (gid_t)tmp_id; break; case 132: /* --debug */ debug = true; break; /* * These reproduce what we would get without ARGP_NO_HELP */ case '?': /* --help */ state->flags &= ~(unsigned int)ARGP_NO_EXIT; /* force exit */ argp_state_help(state, state->out_stream, ARGP_HELP_STD_HELP); case -3: /* --usage */ state->flags &= ~(unsigned int)ARGP_NO_EXIT; /* force exit */ argp_state_help(state, state->out_stream, ARGP_HELP_USAGE | ARGP_HELP_EXIT_OK); case 'V': /* --version */ fprintf(state->out_stream, "%s\n", argp_program_version); exit(EXIT_SUCCESS); break; /* * When adding more options before this line, remember to also add a * "case" to the "parse_opt_config_file" function below. */ case ARGP_KEY_ARG: /* Cryptsetup always passes an argument, which is an empty string if "none" was specified in /etc/crypttab. So if argument was empty, we ignore it silently. */ if(arg[0] == '\0'){ break; } default: return ARGP_ERR_UNKNOWN; } return errno; /* Set to 0 at start */ } /* This option parser is the same as parse_opt() above, except it ignores everything but the --config-file option. */ error_t parse_opt_config_file(int key, char *arg, __attribute__((unused)) struct argp_state *state){ errno = 0; switch(key){ case 'g': /* --global-options */ case 'G': /* --global-env */ case 'o': /* --options-for */ case 'E': /* --env-for */ case 'd': /* --disable */ case 'e': /* --enable */ case 128: /* --plugin-dir */ break; case 129: /* --config-file */ free(argfile); argfile = strdup(arg); break; case 130: /* --userid */ case 131: /* --groupid */ case 132: /* --debug */ case '?': /* --help */ case -3: /* --usage */ case 'V': /* --version */ case ARGP_KEY_ARG: break; default: return ARGP_ERR_UNKNOWN; } return errno; } struct argp argp = { .options = options, .parser = parse_opt_config_file, .args_doc = "", .doc = "Mandos plugin runner -- Run plugins" }; /* Parse using parse_opt_config_file() in order to get the custom config file location, if any. */ ret = argp_parse(&argp, argc, argv, ARGP_IN_ORDER | ARGP_NO_EXIT | ARGP_NO_HELP, NULL, NULL); switch(ret){ case 0: break; case ENOMEM: default: errno = ret; error(0, errno, "argp_parse"); exitstatus = EX_OSERR; goto fallback; case EINVAL: exitstatus = EX_USAGE; goto fallback; } /* Reset to the normal argument parser */ argp.parser = parse_opt; /* Open the configfile if available */ if(argfile == NULL){ conffp = fopen(AFILE, "r"); } else { conffp = fopen(argfile, "r"); } if(conffp != NULL){ char *org_line = NULL; char *p, *arg, *new_arg, *line; size_t size = 0; const char whitespace_delims[] = " \r\t\f\v\n"; const char comment_delim[] = "#"; custom_argc = 1; custom_argv = malloc(sizeof(char*) * 2); if(custom_argv == NULL){ error(0, errno, "malloc"); exitstatus = EX_OSERR; goto fallback; } custom_argv[0] = argv[0]; custom_argv[1] = NULL; /* for each line in the config file, strip whitespace and ignore commented text */ while(true){ sret = getline(&org_line, &size, conffp); if(sret == -1){ break; } line = org_line; arg = strsep(&line, comment_delim); while((p = strsep(&arg, whitespace_delims)) != NULL){ if(p[0] == '\0'){ continue; } new_arg = strdup(p); if(new_arg == NULL){ error(0, errno, "strdup"); exitstatus = EX_OSERR; free(org_line); goto fallback; } custom_argc += 1; custom_argv = realloc(custom_argv, sizeof(char *) * ((unsigned int) custom_argc + 1)); if(custom_argv == NULL){ error(0, errno, "realloc"); exitstatus = EX_OSERR; free(org_line); goto fallback; } custom_argv[custom_argc-1] = new_arg; custom_argv[custom_argc] = NULL; } } do { ret = fclose(conffp); } while(ret == EOF and errno == EINTR); if(ret == EOF){ error(0, errno, "fclose"); exitstatus = EX_IOERR; goto fallback; } free(org_line); } else { /* Check for harmful errors and go to fallback. Other errors might not affect opening plugins */ if(errno == EMFILE or errno == ENFILE or errno == ENOMEM){ error(0, errno, "fopen"); exitstatus = EX_OSERR; goto fallback; } } /* If there were any arguments from the configuration file, pass them to parser as command line arguments */ if(custom_argv != NULL){ ret = argp_parse(&argp, custom_argc, custom_argv, ARGP_IN_ORDER | ARGP_NO_EXIT | ARGP_NO_HELP, NULL, NULL); switch(ret){ case 0: break; case ENOMEM: default: errno = ret; error(0, errno, "argp_parse"); exitstatus = EX_OSERR; goto fallback; case EINVAL: exitstatus = EX_CONFIG; goto fallback; } } /* Parse actual command line arguments, to let them override the config file */ ret = argp_parse(&argp, argc, argv, ARGP_IN_ORDER | ARGP_NO_EXIT | ARGP_NO_HELP, NULL, NULL); switch(ret){ case 0: break; case ENOMEM: default: errno = ret; error(0, errno, "argp_parse"); exitstatus = EX_OSERR; goto fallback; case EINVAL: exitstatus = EX_USAGE; goto fallback; } if(debug){ for(plugin *p = plugin_list; p != NULL; p=p->next){ fprintf(stderr, "Plugin: %s has %d arguments\n", p->name ? p->name : "Global", p->argc - 1); for(char **a = p->argv; *a != NULL; a++){ fprintf(stderr, "\tArg: %s\n", *a); } fprintf(stderr, "...and %d environment variables\n", p->envc); for(char **a = p->environ; *a != NULL; a++){ fprintf(stderr, "\t%s\n", *a); } } } if(getuid() == 0){ /* Work around Debian bug #633582: */ int plugindir_fd = open(/* plugindir or */ PDIR, O_RDONLY); if(plugindir_fd == -1){ error(0, errno, "open"); } else { ret = (int)TEMP_FAILURE_RETRY(fstat(plugindir_fd, &st)); if(ret == -1){ error(0, errno, "fstat"); } else { if(S_ISDIR(st.st_mode) and st.st_uid == 0 and st.st_gid == 0){ ret = fchown(plugindir_fd, uid, gid); if(ret == -1){ error(0, errno, "fchown"); } } } TEMP_FAILURE_RETRY(close(plugindir_fd)); } } /* Lower permissions */ setgid(gid); if(ret == -1){ error(0, errno, "setgid"); } ret = setuid(uid); if(ret == -1){ error(0, errno, "setuid"); } /* Open plugin directory with close_on_exec flag */ { int dir_fd = -1; if(plugindir == NULL){ dir_fd = open(PDIR, O_RDONLY | #ifdef O_CLOEXEC O_CLOEXEC #else /* not O_CLOEXEC */ 0 #endif /* not O_CLOEXEC */ ); } else { dir_fd = open(plugindir, O_RDONLY | #ifdef O_CLOEXEC O_CLOEXEC #else /* not O_CLOEXEC */ 0 #endif /* not O_CLOEXEC */ ); } if(dir_fd == -1){ error(0, errno, "Could not open plugin dir"); exitstatus = EX_UNAVAILABLE; goto fallback; } #ifndef O_CLOEXEC /* Set the FD_CLOEXEC flag on the directory */ ret = set_cloexec_flag(dir_fd); if(ret < 0){ error(0, errno, "set_cloexec_flag"); TEMP_FAILURE_RETRY(close(dir_fd)); exitstatus = EX_OSERR; goto fallback; } #endif /* O_CLOEXEC */ dir = fdopendir(dir_fd); if(dir == NULL){ error(0, errno, "Could not open plugin dir"); TEMP_FAILURE_RETRY(close(dir_fd)); exitstatus = EX_OSERR; goto fallback; } } FD_ZERO(&rfds_all); /* Read and execute any executable in the plugin directory*/ while(true){ do { dirst = readdir(dir); } while(dirst == NULL and errno == EINTR); /* All directory entries have been processed */ if(dirst == NULL){ if(errno == EBADF){ error(0, errno, "readdir"); exitstatus = EX_IOERR; goto fallback; } break; } d_name_len = strlen(dirst->d_name); /* Ignore dotfiles, backup files and other junk */ { bool bad_name = false; const char const *bad_prefixes[] = { ".", "#", NULL }; const char const *bad_suffixes[] = { "~", "#", ".dpkg-new", ".dpkg-old", ".dpkg-bak", ".dpkg-divert", NULL }; for(const char **pre = bad_prefixes; *pre != NULL; pre++){ size_t pre_len = strlen(*pre); if((d_name_len >= pre_len) and strncmp((dirst->d_name), *pre, pre_len) == 0){ if(debug){ fprintf(stderr, "Ignoring plugin dir entry \"%s\"" " with bad prefix %s\n", dirst->d_name, *pre); } bad_name = true; break; } } if(bad_name){ continue; } for(const char **suf = bad_suffixes; *suf != NULL; suf++){ size_t suf_len = strlen(*suf); if((d_name_len >= suf_len) and (strcmp((dirst->d_name) + d_name_len-suf_len, *suf) == 0)){ if(debug){ fprintf(stderr, "Ignoring plugin dir entry \"%s\"" " with bad suffix %s\n", dirst->d_name, *suf); } bad_name = true; break; } } if(bad_name){ continue; } } char *filename; if(plugindir == NULL){ ret = (int)TEMP_FAILURE_RETRY(asprintf(&filename, PDIR "/%s", dirst->d_name)); } else { ret = (int)TEMP_FAILURE_RETRY(asprintf(&filename, "%s/%s", plugindir, dirst->d_name)); } if(ret < 0){ error(0, errno, "asprintf"); continue; } ret = (int)TEMP_FAILURE_RETRY(stat(filename, &st)); if(ret == -1){ error(0, errno, "stat"); free(filename); continue; } /* Ignore non-executable files */ if(not S_ISREG(st.st_mode) or (TEMP_FAILURE_RETRY(access(filename, X_OK)) != 0)){ if(debug){ fprintf(stderr, "Ignoring plugin dir entry \"%s\"" " with bad type or mode\n", filename); } free(filename); continue; } plugin *p = getplugin(dirst->d_name); if(p == NULL){ error(0, errno, "getplugin"); free(filename); continue; } if(p->disabled){ if(debug){ fprintf(stderr, "Ignoring disabled plugin \"%s\"\n", dirst->d_name); } free(filename); continue; } { /* Add global arguments to argument list for this plugin */ plugin *g = getplugin(NULL); if(g != NULL){ for(char **a = g->argv + 1; *a != NULL; a++){ if(not add_argument(p, *a)){ error(0, errno, "add_argument"); } } /* Add global environment variables */ for(char **e = g->environ; *e != NULL; e++){ if(not add_environment(p, *e, false)){ error(0, errno, "add_environment"); } } } } /* If this plugin has any environment variables, we will call using execve and need to duplicate the environment from this process, too. */ if(p->environ[0] != NULL){ for(char **e = environ; *e != NULL; e++){ if(not add_environment(p, *e, false)){ error(0, errno, "add_environment"); } } } int pipefd[2]; ret = (int)TEMP_FAILURE_RETRY(pipe(pipefd)); if(ret == -1){ error(0, errno, "pipe"); exitstatus = EX_OSERR; goto fallback; } /* Ask OS to automatic close the pipe on exec */ ret = set_cloexec_flag(pipefd[0]); if(ret < 0){ error(0, errno, "set_cloexec_flag"); exitstatus = EX_OSERR; goto fallback; } ret = set_cloexec_flag(pipefd[1]); if(ret < 0){ error(0, errno, "set_cloexec_flag"); exitstatus = EX_OSERR; goto fallback; } /* Block SIGCHLD until process is safely in process list */ ret = (int)TEMP_FAILURE_RETRY(sigprocmask(SIG_BLOCK, &sigchld_action.sa_mask, NULL)); if(ret < 0){ error(0, errno, "sigprocmask"); exitstatus = EX_OSERR; goto fallback; } /* Starting a new process to be watched */ pid_t pid; do { pid = fork(); } while(pid == -1 and errno == EINTR); if(pid == -1){ error(0, errno, "fork"); exitstatus = EX_OSERR; goto fallback; } if(pid == 0){ /* this is the child process */ ret = sigaction(SIGCHLD, &old_sigchld_action, NULL); if(ret < 0){ error(0, errno, "sigaction"); _exit(EX_OSERR); } ret = sigprocmask(SIG_UNBLOCK, &sigchld_action.sa_mask, NULL); if(ret < 0){ error(0, errno, "sigprocmask"); _exit(EX_OSERR); } ret = dup2(pipefd[1], STDOUT_FILENO); /* replace our stdout */ if(ret == -1){ error(0, errno, "dup2"); _exit(EX_OSERR); } if(dirfd(dir) < 0){ /* If dir has no file descriptor, we could not set FD_CLOEXEC above and must now close it manually here. */ closedir(dir); } if(p->environ[0] == NULL){ if(execv(filename, p->argv) < 0){ error(0, errno, "execv for %s", filename); _exit(EX_OSERR); } } else { if(execve(filename, p->argv, p->environ) < 0){ error(0, errno, "execve for %s", filename); _exit(EX_OSERR); } } /* no return */ } /* Parent process */ TEMP_FAILURE_RETRY(close(pipefd[1])); /* Close unused write end of pipe */ free(filename); plugin *new_plugin = getplugin(dirst->d_name); if(new_plugin == NULL){ error(0, errno, "getplugin"); ret = (int)(TEMP_FAILURE_RETRY (sigprocmask(SIG_UNBLOCK, &sigchld_action.sa_mask, NULL))); if(ret < 0){ error(0, errno, "sigprocmask"); } exitstatus = EX_OSERR; goto fallback; } new_plugin->pid = pid; new_plugin->fd = pipefd[0]; /* Unblock SIGCHLD so signal handler can be run if this process has already completed */ ret = (int)TEMP_FAILURE_RETRY(sigprocmask(SIG_UNBLOCK, &sigchld_action.sa_mask, NULL)); if(ret < 0){ error(0, errno, "sigprocmask"); exitstatus = EX_OSERR; goto fallback; } FD_SET(new_plugin->fd, &rfds_all); /* Spurious warning from -Wconversion */ if(maxfd < new_plugin->fd){ maxfd = new_plugin->fd; } } TEMP_FAILURE_RETRY(closedir(dir)); dir = NULL; free_plugin(getplugin(NULL)); for(plugin *p = plugin_list; p != NULL; p = p->next){ if(p->pid != 0){ break; } if(p->next == NULL){ fprintf(stderr, "No plugin processes started. Incorrect plugin" " directory?\n"); free_plugin_list(); } } /* Main loop while running plugins exist */ while(plugin_list){ fd_set rfds = rfds_all; int select_ret = select(maxfd+1, &rfds, NULL, NULL, NULL); if(select_ret == -1 and errno != EINTR){ error(0, errno, "select"); exitstatus = EX_OSERR; goto fallback; } /* OK, now either a process completed, or something can be read from one of them */ for(plugin *proc = plugin_list; proc != NULL;){ /* Is this process completely done? */ if(proc->completed and proc->eof){ /* Only accept the plugin output if it exited cleanly */ if(not WIFEXITED(proc->status) or WEXITSTATUS(proc->status) != 0){ /* Bad exit by plugin */ if(debug){ if(WIFEXITED(proc->status)){ fprintf(stderr, "Plugin %s [%" PRIdMAX "] exited with" " status %d\n", proc->name, (intmax_t) (proc->pid), WEXITSTATUS(proc->status)); } else if(WIFSIGNALED(proc->status)){ fprintf(stderr, "Plugin %s [%" PRIdMAX "] killed by" " signal %d: %s\n", proc->name, (intmax_t) (proc->pid), WTERMSIG(proc->status), strsignal(WTERMSIG(proc->status))); } else if(WCOREDUMP(proc->status)){ fprintf(stderr, "Plugin %s [%" PRIdMAX "] dumped" " core\n", proc->name, (intmax_t) (proc->pid)); } } /* Remove the plugin */ FD_CLR(proc->fd, &rfds_all); /* Spurious warning from -Wconversion */ /* Block signal while modifying process_list */ ret = (int)TEMP_FAILURE_RETRY(sigprocmask (SIG_BLOCK, &sigchld_action.sa_mask, NULL)); if(ret < 0){ error(0, errno, "sigprocmask"); exitstatus = EX_OSERR; goto fallback; } plugin *next_plugin = proc->next; free_plugin(proc); proc = next_plugin; /* We are done modifying process list, so unblock signal */ ret = (int)(TEMP_FAILURE_RETRY (sigprocmask(SIG_UNBLOCK, &sigchld_action.sa_mask, NULL))); if(ret < 0){ error(0, errno, "sigprocmask"); exitstatus = EX_OSERR; goto fallback; } if(plugin_list == NULL){ break; } continue; } /* This process exited nicely, so print its buffer */ bool bret = print_out_password(proc->buffer, proc->buffer_length); if(not bret){ error(0, errno, "print_out_password"); exitstatus = EX_IOERR; } goto fallback; } /* This process has not completed. Does it have any output? */ if(proc->eof or not FD_ISSET(proc->fd, &rfds)){ /* Spurious warning from -Wconversion */ /* This process had nothing to say at this time */ proc = proc->next; continue; } /* Before reading, make the process' data buffer large enough */ if(proc->buffer_length + BUFFER_SIZE > proc->buffer_size){ proc->buffer = realloc(proc->buffer, proc->buffer_size + (size_t) BUFFER_SIZE); if(proc->buffer == NULL){ error(0, errno, "malloc"); exitstatus = EX_OSERR; goto fallback; } proc->buffer_size += BUFFER_SIZE; } /* Read from the process */ sret = TEMP_FAILURE_RETRY(read(proc->fd, proc->buffer + proc->buffer_length, BUFFER_SIZE)); if(sret < 0){ /* Read error from this process; ignore the error */ proc = proc->next; continue; } if(sret == 0){ /* got EOF */ proc->eof = true; } else { proc->buffer_length += (size_t) sret; } } } fallback: if(plugin_list == NULL or (exitstatus != EXIT_SUCCESS and exitstatus != EX_OK)){ /* Fallback if all plugins failed, none are found or an error occured */ bool bret; fprintf(stderr, "Going to fallback mode using getpass(3)\n"); char *passwordbuffer = getpass("Password: "); size_t len = strlen(passwordbuffer); /* Strip trailing newline */ if(len > 0 and passwordbuffer[len-1] == '\n'){ passwordbuffer[len-1] = '\0'; /* not strictly necessary */ len--; } bret = print_out_password(passwordbuffer, len); if(not bret){ error(0, errno, "print_out_password"); exitstatus = EX_IOERR; } } /* Restore old signal handler */ ret = sigaction(SIGCHLD, &old_sigchld_action, NULL); if(ret == -1){ error(0, errno, "sigaction"); exitstatus = EX_OSERR; } if(custom_argv != NULL){ for(char **arg = custom_argv+1; *arg != NULL; arg++){ free(*arg); } free(custom_argv); } if(dir != NULL){ closedir(dir); } /* Kill the processes */ for(plugin *p = plugin_list; p != NULL; p = p->next){ if(p->pid != 0){ close(p->fd); ret = kill(p->pid, SIGTERM); if(ret == -1 and errno != ESRCH){ /* Set-uid proccesses might not get closed */ error(0, errno, "kill"); } } } /* Wait for any remaining child processes to terminate */ do { ret = wait(NULL); } while(ret >= 0); if(errno != ECHILD){ error(0, errno, "wait"); } free_plugin_list(); free(plugindir); free(argfile); return exitstatus; } mandos-1.6.0/COPYING0000664000175000017500000010437411767455003014321 0ustar teddyteddy00000000000000 GNU GENERAL PUBLIC LICENSE Version 3, 29 June 2007 Copyright (C) 2007 Free Software Foundation, Inc. Everyone is permitted to copy and distribute verbatim copies of this license document, but changing it is not allowed. Preamble The GNU General Public License is a free, copyleft license for software and other kinds of works. The licenses for most software and other practical works are designed to take away your freedom to share and change the works. By contrast, the GNU General Public License is intended to guarantee your freedom to share and change all versions of a program--to make sure it remains free software for all its users. We, the Free Software Foundation, use the GNU General Public License for most of our software; it applies also to any other work released this way by its authors. You can apply it to your programs, too. When we speak of free software, we are referring to freedom, not price. Our General Public Licenses are designed to make sure that you have the freedom to distribute copies of free software (and charge for them if you wish), that you receive source code or can get it if you want it, that you can change the software or use pieces of it in new free programs, and that you know you can do these things. To protect your rights, we need to prevent others from denying you these rights or asking you to surrender the rights. Therefore, you have certain responsibilities if you distribute copies of the software, or if you modify it: responsibilities to respect the freedom of others. For example, if you distribute copies of such a program, whether gratis or for a fee, you must pass on to the recipients the same freedoms that you received. You must make sure that they, too, receive or can get the source code. And you must show them these terms so they know their rights. Developers that use the GNU GPL protect your rights with two steps: (1) assert copyright on the software, and (2) offer you this License giving you legal permission to copy, distribute and/or modify it. For the developers' and authors' protection, the GPL clearly explains that there is no warranty for this free software. For both users' and authors' sake, the GPL requires that modified versions be marked as changed, so that their problems will not be attributed erroneously to authors of previous versions. Some devices are designed to deny users access to install or run modified versions of the software inside them, although the manufacturer can do so. This is fundamentally incompatible with the aim of protecting users' freedom to change the software. The systematic pattern of such abuse occurs in the area of products for individuals to use, which is precisely where it is most unacceptable. Therefore, we have designed this version of the GPL to prohibit the practice for those products. If such problems arise substantially in other domains, we stand ready to extend this provision to those domains in future versions of the GPL, as needed to protect the freedom of users. Finally, every program is threatened constantly by software patents. States should not allow patents to restrict development and use of software on general-purpose computers, but in those that do, we wish to avoid the special danger that patents applied to a free program could make it effectively proprietary. To prevent this, the GPL assures that patents cannot be used to render the program non-free. The precise terms and conditions for copying, distribution and modification follow. TERMS AND CONDITIONS 0. Definitions. "This License" refers to version 3 of the GNU General Public License. "Copyright" also means copyright-like laws that apply to other kinds of works, such as semiconductor masks. "The Program" refers to any copyrightable work licensed under this License. Each licensee is addressed as "you". "Licensees" and "recipients" may be individuals or organizations. To "modify" a work means to copy from or adapt all or part of the work in a fashion requiring copyright permission, other than the making of an exact copy. The resulting work is called a "modified version" of the earlier work or a work "based on" the earlier work. A "covered work" means either the unmodified Program or a work based on the Program. To "propagate" a work means to do anything with it that, without permission, would make you directly or secondarily liable for infringement under applicable copyright law, except executing it on a computer or modifying a private copy. Propagation includes copying, distribution (with or without modification), making available to the public, and in some countries other activities as well. To "convey" a work means any kind of propagation that enables other parties to make or receive copies. Mere interaction with a user through a computer network, with no transfer of a copy, is not conveying. An interactive user interface displays "Appropriate Legal Notices" to the extent that it includes a convenient and prominently visible feature that (1) displays an appropriate copyright notice, and (2) tells the user that there is no warranty for the work (except to the extent that warranties are provided), that licensees may convey the work under this License, and how to view a copy of this License. If the interface presents a list of user commands or options, such as a menu, a prominent item in the list meets this criterion. 1. Source Code. The "source code" for a work means the preferred form of the work for making modifications to it. "Object code" means any non-source form of a work. A "Standard Interface" means an interface that either is an official standard defined by a recognized standards body, or, in the case of interfaces specified for a particular programming language, one that is widely used among developers working in that language. The "System Libraries" of an executable work include anything, other than the work as a whole, that (a) is included in the normal form of packaging a Major Component, but which is not part of that Major Component, and (b) serves only to enable use of the work with that Major Component, or to implement a Standard Interface for which an implementation is available to the public in source code form. A "Major Component", in this context, means a major essential component (kernel, window system, and so on) of the specific operating system (if any) on which the executable work runs, or a compiler used to produce the work, or an object code interpreter used to run it. The "Corresponding Source" for a work in object code form means all the source code needed to generate, install, and (for an executable work) run the object code and to modify the work, including scripts to control those activities. However, it does not include the work's System Libraries, or general-purpose tools or generally available free programs which are used unmodified in performing those activities but which are not part of the work. For example, Corresponding Source includes interface definition files associated with source files for the work, and the source code for shared libraries and dynamically linked subprograms that the work is specifically designed to require, such as by intimate data communication or control flow between those subprograms and other parts of the work. The Corresponding Source need not include anything that users can regenerate automatically from other parts of the Corresponding Source. The Corresponding Source for a work in source code form is that same work. 2. Basic Permissions. All rights granted under this License are granted for the term of copyright on the Program, and are irrevocable provided the stated conditions are met. This License explicitly affirms your unlimited permission to run the unmodified Program. The output from running a covered work is covered by this License only if the output, given its content, constitutes a covered work. This License acknowledges your rights of fair use or other equivalent, as provided by copyright law. You may make, run and propagate covered works that you do not convey, without conditions so long as your license otherwise remains in force. You may convey covered works to others for the sole purpose of having them make modifications exclusively for you, or provide you with facilities for running those works, provided that you comply with the terms of this License in conveying all material for which you do not control copyright. Those thus making or running the covered works for you must do so exclusively on your behalf, under your direction and control, on terms that prohibit them from making any copies of your copyrighted material outside their relationship with you. Conveying under any other circumstances is permitted solely under the conditions stated below. Sublicensing is not allowed; section 10 makes it unnecessary. 3. Protecting Users' Legal Rights From Anti-Circumvention Law. No covered work shall be deemed part of an effective technological measure under any applicable law fulfilling obligations under article 11 of the WIPO copyright treaty adopted on 20 December 1996, or similar laws prohibiting or restricting circumvention of such measures. When you convey a covered work, you waive any legal power to forbid circumvention of technological measures to the extent such circumvention is effected by exercising rights under this License with respect to the covered work, and you disclaim any intention to limit operation or modification of the work as a means of enforcing, against the work's users, your or third parties' legal rights to forbid circumvention of technological measures. 4. Conveying Verbatim Copies. You may convey verbatim copies of the Program's source code as you receive it, in any medium, provided that you conspicuously and appropriately publish on each copy an appropriate copyright notice; keep intact all notices stating that this License and any non-permissive terms added in accord with section 7 apply to the code; keep intact all notices of the absence of any warranty; and give all recipients a copy of this License along with the Program. You may charge any price or no price for each copy that you convey, and you may offer support or warranty protection for a fee. 5. Conveying Modified Source Versions. You may convey a work based on the Program, or the modifications to produce it from the Program, in the form of source code under the terms of section 4, provided that you also meet all of these conditions: a) The work must carry prominent notices stating that you modified it, and giving a relevant date. b) The work must carry prominent notices stating that it is released under this License and any conditions added under section 7. This requirement modifies the requirement in section 4 to "keep intact all notices". c) You must license the entire work, as a whole, under this License to anyone who comes into possession of a copy. This License will therefore apply, along with any applicable section 7 additional terms, to the whole of the work, and all its parts, regardless of how they are packaged. This License gives no permission to license the work in any other way, but it does not invalidate such permission if you have separately received it. d) If the work has interactive user interfaces, each must display Appropriate Legal Notices; however, if the Program has interactive interfaces that do not display Appropriate Legal Notices, your work need not make them do so. A compilation of a covered work with other separate and independent works, which are not by their nature extensions of the covered work, and which are not combined with it such as to form a larger program, in or on a volume of a storage or distribution medium, is called an "aggregate" if the compilation and its resulting copyright are not used to limit the access or legal rights of the compilation's users beyond what the individual works permit. Inclusion of a covered work in an aggregate does not cause this License to apply to the other parts of the aggregate. 6. Conveying Non-Source Forms. You may convey a covered work in object code form under the terms of sections 4 and 5, provided that you also convey the machine-readable Corresponding Source under the terms of this License, in one of these ways: a) Convey the object code in, or embodied in, a physical product (including a physical distribution medium), accompanied by the Corresponding Source fixed on a durable physical medium customarily used for software interchange. b) Convey the object code in, or embodied in, a physical product (including a physical distribution medium), accompanied by a written offer, valid for at least three years and valid for as long as you offer spare parts or customer support for that product model, to give anyone who possesses the object code either (1) a copy of the Corresponding Source for all the software in the product that is covered by this License, on a durable physical medium customarily used for software interchange, for a price no more than your reasonable cost of physically performing this conveying of source, or (2) access to copy the Corresponding Source from a network server at no charge. c) Convey individual copies of the object code with a copy of the written offer to provide the Corresponding Source. This alternative is allowed only occasionally and noncommercially, and only if you received the object code with such an offer, in accord with subsection 6b. d) Convey the object code by offering access from a designated place (gratis or for a charge), and offer equivalent access to the Corresponding Source in the same way through the same place at no further charge. You need not require recipients to copy the Corresponding Source along with the object code. If the place to copy the object code is a network server, the Corresponding Source may be on a different server (operated by you or a third party) that supports equivalent copying facilities, provided you maintain clear directions next to the object code saying where to find the Corresponding Source. Regardless of what server hosts the Corresponding Source, you remain obligated to ensure that it is available for as long as needed to satisfy these requirements. e) Convey the object code using peer-to-peer transmission, provided you inform other peers where the object code and Corresponding Source of the work are being offered to the general public at no charge under subsection 6d. A separable portion of the object code, whose source code is excluded from the Corresponding Source as a System Library, need not be included in conveying the object code work. A "User Product" is either (1) a "consumer product", which means any tangible personal property which is normally used for personal, family, or household purposes, or (2) anything designed or sold for incorporation into a dwelling. In determining whether a product is a consumer product, doubtful cases shall be resolved in favor of coverage. For a particular product received by a particular user, "normally used" refers to a typical or common use of that class of product, regardless of the status of the particular user or of the way in which the particular user actually uses, or expects or is expected to use, the product. A product is a consumer product regardless of whether the product has substantial commercial, industrial or non-consumer uses, unless such uses represent the only significant mode of use of the product. "Installation Information" for a User Product means any methods, procedures, authorization keys, or other information required to install and execute modified versions of a covered work in that User Product from a modified version of its Corresponding Source. The information must suffice to ensure that the continued functioning of the modified object code is in no case prevented or interfered with solely because modification has been made. If you convey an object code work under this section in, or with, or specifically for use in, a User Product, and the conveying occurs as part of a transaction in which the right of possession and use of the User Product is transferred to the recipient in perpetuity or for a fixed term (regardless of how the transaction is characterized), the Corresponding Source conveyed under this section must be accompanied by the Installation Information. But this requirement does not apply if neither you nor any third party retains the ability to install modified object code on the User Product (for example, the work has been installed in ROM). The requirement to provide Installation Information does not include a requirement to continue to provide support service, warranty, or updates for a work that has been modified or installed by the recipient, or for the User Product in which it has been modified or installed. Access to a network may be denied when the modification itself materially and adversely affects the operation of the network or violates the rules and protocols for communication across the network. Corresponding Source conveyed, and Installation Information provided, in accord with this section must be in a format that is publicly documented (and with an implementation available to the public in source code form), and must require no special password or key for unpacking, reading or copying. 7. Additional Terms. "Additional permissions" are terms that supplement the terms of this License by making exceptions from one or more of its conditions. Additional permissions that are applicable to the entire Program shall be treated as though they were included in this License, to the extent that they are valid under applicable law. If additional permissions apply only to part of the Program, that part may be used separately under those permissions, but the entire Program remains governed by this License without regard to the additional permissions. When you convey a copy of a covered work, you may at your option remove any additional permissions from that copy, or from any part of it. (Additional permissions may be written to require their own removal in certain cases when you modify the work.) You may place additional permissions on material, added by you to a covered work, for which you have or can give appropriate copyright permission. Notwithstanding any other provision of this License, for material you add to a covered work, you may (if authorized by the copyright holders of that material) supplement the terms of this License with terms: a) Disclaiming warranty or limiting liability differently from the terms of sections 15 and 16 of this License; or b) Requiring preservation of specified reasonable legal notices or author attributions in that material or in the Appropriate Legal Notices displayed by works containing it; or c) Prohibiting misrepresentation of the origin of that material, or requiring that modified versions of such material be marked in reasonable ways as different from the original version; or d) Limiting the use for publicity purposes of names of licensors or authors of the material; or e) Declining to grant rights under trademark law for use of some trade names, trademarks, or service marks; or f) Requiring indemnification of licensors and authors of that material by anyone who conveys the material (or modified versions of it) with contractual assumptions of liability to the recipient, for any liability that these contractual assumptions directly impose on those licensors and authors. All other non-permissive additional terms are considered "further restrictions" within the meaning of section 10. If the Program as you received it, or any part of it, contains a notice stating that it is governed by this License along with a term that is a further restriction, you may remove that term. If a license document contains a further restriction but permits relicensing or conveying under this License, you may add to a covered work material governed by the terms of that license document, provided that the further restriction does not survive such relicensing or conveying. If you add terms to a covered work in accord with this section, you must place, in the relevant source files, a statement of the additional terms that apply to those files, or a notice indicating where to find the applicable terms. Additional terms, permissive or non-permissive, may be stated in the form of a separately written license, or stated as exceptions; the above requirements apply either way. 8. Termination. You may not propagate or modify a covered work except as expressly provided under this License. Any attempt otherwise to propagate or modify it is void, and will automatically terminate your rights under this License (including any patent licenses granted under the third paragraph of section 11). However, if you cease all violation of this License, then your license from a particular copyright holder is reinstated (a) provisionally, unless and until the copyright holder explicitly and finally terminates your license, and (b) permanently, if the copyright holder fails to notify you of the violation by some reasonable means prior to 60 days after the cessation. Moreover, your license from a particular copyright holder is reinstated permanently if the copyright holder notifies you of the violation by some reasonable means, this is the first time you have received notice of violation of this License (for any work) from that copyright holder, and you cure the violation prior to 30 days after your receipt of the notice. Termination of your rights under this section does not terminate the licenses of parties who have received copies or rights from you under this License. If your rights have been terminated and not permanently reinstated, you do not qualify to receive new licenses for the same material under section 10. 9. Acceptance Not Required for Having Copies. You are not required to accept this License in order to receive or run a copy of the Program. Ancillary propagation of a covered work occurring solely as a consequence of using peer-to-peer transmission to receive a copy likewise does not require acceptance. However, nothing other than this License grants you permission to propagate or modify any covered work. These actions infringe copyright if you do not accept this License. Therefore, by modifying or propagating a covered work, you indicate your acceptance of this License to do so. 10. Automatic Licensing of Downstream Recipients. Each time you convey a covered work, the recipient automatically receives a license from the original licensors, to run, modify and propagate that work, subject to this License. You are not responsible for enforcing compliance by third parties with this License. An "entity transaction" is a transaction transferring control of an organization, or substantially all assets of one, or subdividing an organization, or merging organizations. If propagation of a covered work results from an entity transaction, each party to that transaction who receives a copy of the work also receives whatever licenses to the work the party's predecessor in interest had or could give under the previous paragraph, plus a right to possession of the Corresponding Source of the work from the predecessor in interest, if the predecessor has it or can get it with reasonable efforts. You may not impose any further restrictions on the exercise of the rights granted or affirmed under this License. For example, you may not impose a license fee, royalty, or other charge for exercise of rights granted under this License, and you may not initiate litigation (including a cross-claim or counterclaim in a lawsuit) alleging that any patent claim is infringed by making, using, selling, offering for sale, or importing the Program or any portion of it. 11. Patents. A "contributor" is a copyright holder who authorizes use under this License of the Program or a work on which the Program is based. The work thus licensed is called the contributor's "contributor version". A contributor's "essential patent claims" are all patent claims owned or controlled by the contributor, whether already acquired or hereafter acquired, that would be infringed by some manner, permitted by this License, of making, using, or selling its contributor version, but do not include claims that would be infringed only as a consequence of further modification of the contributor version. For purposes of this definition, "control" includes the right to grant patent sublicenses in a manner consistent with the requirements of this License. Each contributor grants you a non-exclusive, worldwide, royalty-free patent license under the contributor's essential patent claims, to make, use, sell, offer for sale, import and otherwise run, modify and propagate the contents of its contributor version. In the following three paragraphs, a "patent license" is any express agreement or commitment, however denominated, not to enforce a patent (such as an express permission to practice a patent or covenant not to sue for patent infringement). To "grant" such a patent license to a party means to make such an agreement or commitment not to enforce a patent against the party. If you convey a covered work, knowingly relying on a patent license, and the Corresponding Source of the work is not available for anyone to copy, free of charge and under the terms of this License, through a publicly available network server or other readily accessible means, then you must either (1) cause the Corresponding Source to be so available, or (2) arrange to deprive yourself of the benefit of the patent license for this particular work, or (3) arrange, in a manner consistent with the requirements of this License, to extend the patent license to downstream recipients. "Knowingly relying" means you have actual knowledge that, but for the patent license, your conveying the covered work in a country, or your recipient's use of the covered work in a country, would infringe one or more identifiable patents in that country that you have reason to believe are valid. If, pursuant to or in connection with a single transaction or arrangement, you convey, or propagate by procuring conveyance of, a covered work, and grant a patent license to some of the parties receiving the covered work authorizing them to use, propagate, modify or convey a specific copy of the covered work, then the patent license you grant is automatically extended to all recipients of the covered work and works based on it. A patent license is "discriminatory" if it does not include within the scope of its coverage, prohibits the exercise of, or is conditioned on the non-exercise of one or more of the rights that are specifically granted under this License. You may not convey a covered work if you are a party to an arrangement with a third party that is in the business of distributing software, under which you make payment to the third party based on the extent of your activity of conveying the work, and under which the third party grants, to any of the parties who would receive the covered work from you, a discriminatory patent license (a) in connection with copies of the covered work conveyed by you (or copies made from those copies), or (b) primarily for and in connection with specific products or compilations that contain the covered work, unless you entered into that arrangement, or that patent license was granted, prior to 28 March 2007. Nothing in this License shall be construed as excluding or limiting any implied license or other defenses to infringement that may otherwise be available to you under applicable patent law. 12. No Surrender of Others' Freedom. If conditions are imposed on you (whether by court order, agreement or otherwise) that contradict the conditions of this License, they do not excuse you from the conditions of this License. If you cannot convey a covered work so as to satisfy simultaneously your obligations under this License and any other pertinent obligations, then as a consequence you may not convey it at all. For example, if you agree to terms that obligate you to collect a royalty for further conveying from those to whom you convey the Program, the only way you could satisfy both those terms and this License would be to refrain entirely from conveying the Program. 13. Use with the GNU Affero General Public License. Notwithstanding any other provision of this License, you have permission to link or combine any covered work with a work licensed under version 3 of the GNU Affero General Public License into a single combined work, and to convey the resulting work. The terms of this License will continue to apply to the part which is the covered work, but the special requirements of the GNU Affero General Public License, section 13, concerning interaction through a network will apply to the combination as such. 14. Revised Versions of this License. The Free Software Foundation may publish revised and/or new versions of the GNU General Public License from time to time. Such new versions will be similar in spirit to the present version, but may differ in detail to address new problems or concerns. Each version is given a distinguishing version number. If the Program specifies that a certain numbered version of the GNU General Public License "or any later version" applies to it, you have the option of following the terms and conditions either of that numbered version or of any later version published by the Free Software Foundation. If the Program does not specify a version number of the GNU General Public License, you may choose any version ever published by the Free Software Foundation. If the Program specifies that a proxy can decide which future versions of the GNU General Public License can be used, that proxy's public statement of acceptance of a version permanently authorizes you to choose that version for the Program. Later license versions may give you additional or different permissions. However, no additional obligations are imposed on any author or copyright holder as a result of your choosing to follow a later version. 15. Disclaimer of Warranty. THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, REPAIR OR CORRECTION. 16. Limitation of Liability. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGES. 17. Interpretation of Sections 15 and 16. If the disclaimer of warranty and limitation of liability provided above cannot be given local legal effect according to their terms, reviewing courts shall apply local law that most closely approximates an absolute waiver of all civil liability in connection with the Program, unless a warranty or assumption of liability accompanies a copy of the Program in return for a fee. END OF TERMS AND CONDITIONS How to Apply These Terms to Your New Programs If you develop a new program, and you want it to be of the greatest possible use to the public, the best way to achieve this is to make it free software which everyone can redistribute and change under these terms. To do so, attach the following notices to the program. It is safest to attach them to the start of each source file to most effectively state the exclusion of warranty; and each file should have at least the "copyright" line and a pointer to where the full notice is found. Copyright (C) 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 of the License, 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 . Also add information on how to contact you by electronic and paper mail. If the program does terminal interaction, make it output a short notice like this when it starts in an interactive mode: Copyright (C) This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'. This is free software, and you are welcome to redistribute it under certain conditions; type `show c' for details. The hypothetical commands `show w' and `show c' should show the appropriate parts of the General Public License. Of course, your program's commands might be different; for a GUI interface, you would use an "about box". You should also get your employer (if you work as a programmer) or school, if any, to sign a "copyright disclaimer" for the program, if necessary. For more information on this, and how to apply and follow the GNU GPL, see . The GNU General Public License does not permit incorporating your program into proprietary programs. If your program is a subroutine library, you may consider it more useful to permit linking proprietary applications with the library. If this is what you want to do, use the GNU Lesser General Public License instead of this License. But first, please read . mandos-1.6.0/mandos-clients.conf.xml0000664000175000017500000004427411767455003017656 0ustar teddyteddy00000000000000 /etc/mandos/clients.conf"> %common; ]> Mandos Manual Mandos &version; &TIMESTAMP; Björn Påhlsson
belorn@recompile.se
Teddy Hogeborn
teddy@recompile.se
2008 2009 2010 2011 2012 Teddy Hogeborn Björn Påhlsson
&CONFNAME; 5 &CONFNAME; Configuration file for the Mandos server &CONFPATH; DESCRIPTION The file &CONFPATH; is a configuration file for mandos 8, read by it at startup. The file needs to list all clients that should be able to use the service. The settings in this file can be overridden by runtime changes to the server, which it saves across restarts. (See the section called PERSISTENT STATE in mandos8.) However, any changes to this file (including adding and removing clients) will, at startup, override changes done during runtime. The format starts with a [section header] which is either [DEFAULT] or [client name]. The client name can be anything, and is not tied to a host name. Following the section header is any number of option=value entries, with continuations in the style of RFC 822. option: value is also accepted. Note that leading whitespace is removed from values. Values can contain format strings which refer to other values in the same section, or values in the DEFAULT section (see ). Lines beginning with # or ; are ignored and may be used to provide comments. OPTIONS Note: all option values are subject to start time expansion, see . Unknown options are ignored. The used options are as follows: This option is optional. How long to wait for external approval before resorting to use the value. The default is 0s, i.e. not to wait. The format of TIME is the same as for timeout below. This option is optional. How long an external approval lasts. The default is 1 second. The format of TIME is the same as for timeout below. Whether to approve a client by default after the . The default is True. This option is optional. This option overrides the default shell command that the server will use to check if the client is still up. Any output of the command will be ignored, only the exit code is checked: If the exit code of the command is zero, the client is considered up. The command will be run using /bin/sh , so PATH will be searched. The default value for the checker command is fping %%(host)s. In addition to normal start time expansion, this option will also be subject to runtime expansion; see . This option is optional. Extended timeout is an added timeout that is given once after a password has been sent successfully to a client. The timeout is by default longer than the normal timeout, and is used for handling the extra long downtime while a machine is booting up. Time to take into consideration when changing this value is file system checks and quota checks. The default value is 15 minutes. The format of TIME is the same as for timeout below. This option is required. This option sets the OpenPGP fingerprint that identifies the public key that clients authenticate themselves with through TLS. The string needs to be in hexidecimal form, but spaces or upper/lower case are not significant. This option is optional, but highly recommended unless the option is modified to a non-standard value without %%(host)s in it. Host name for this client. This is not used by the server directly, but can be, and is by default, used by the checker. See the option. This option is optional. How often to run the checker to confirm that a client is still up. Note: a new checker will not be started if an old one is still running. The server will wait for a checker to complete until the below timeout occurs, at which time the client will be disabled, and any running checker killed. The default interval is 2 minutes. The format of TIME is the same as for timeout below. This option is only used if is not specified, in which case this option is required. Similar to the , except the secret data is in an external file. The contents of the file should not be base64-encoded, but will be sent to clients verbatim. File names of the form ~user/foo/bar and $ENVVAR/foo/bar are supported. If this option is not specified, the option is required to be present. If present, this option must be set to a string of base64-encoded binary data. It will be decoded and sent to the client matching the above . This should, of course, be OpenPGP encrypted data, decryptable only by the client. The program mandos-keygen8 can, using its option, be used to generate this, if desired. Note: this value of this option will probably be very long. A useful feature to avoid having unreadably-long lines is that a line beginning with white space adds to the value of the previous line, RFC 822-style. This option is optional. The timeout is how long the server will wait, after a successful checker run, until a client is disabled and not allowed to get the data this server holds. By default Mandos will use 5 minutes. See also the option. The TIME is specified as a space-separated number of values, each of which is a number and a one-character suffix. The suffix must be one of d, s, m, h, and w for days, seconds, minutes, hours, and weeks, respectively. The values are added together to give the total time value, so all of 330s, 110s 110s 110s, and 5m 30s will give a value of five minutes and thirty seconds. Whether this client should be enabled by default. The default is true. EXPANSION There are two forms of expansion: Start time expansion and runtime expansion. START TIME EXPANSION Any string in an option value of the form %(foo)s will be replaced by the value of the option foo either in the same section, or, if it does not exist there, the [DEFAULT] section. This is done at start time, when the configuration file is read. Note that this means that, in order to include an actual percent character (%) in an option value, two percent characters in a row (%%) must be entered. RUNTIME EXPANSION This is currently only done for the checker option. Any string in an option value of the form %%(foo)s will be replaced by the value of the attribute foo of the internal Client object in the Mandos server. The currently allowed values for foo are: approval_delay, approval_duration, created, enabled, expires, fingerprint, host, interval, last_approval_request, last_checked_ok, last_enabled, name, timeout, and, if using D-Bus, dbus_object_path. See the source code for details. Currently, none of these attributes except host are guaranteed to be valid in future versions. Therefore, please let the authors know of any attributes that are useful so they may be preserved to any new versions of this software. Note that this means that, in order to include an actual percent character (%) in a checker option, four percent characters in a row (%%%%) must be entered. Also, a bad format here will lead to an immediate but silent run-time fatal exit; debug mode is needed to expose an error of this kind. FILES The file described here is &CONFPATH; BUGS The format for specifying times for timeout and interval is not very good. The difference between %%(foo)s and %(foo)s is obscure. EXAMPLE [DEFAULT] timeout = 5m interval = 2m checker = fping -q -- %%(host)s # Client "foo" [foo] fingerprint = 7788 2722 5BA7 DE53 9C5A 7CFA 59CF F7CD BD9A 5920 secret = hQIOA6QdEjBs2L/HEAf/TCyrDe5Xnm9esa+Pb/vWF9CUqfn4srzVgSu234 REJMVv7lBSrPE2132Lmd2gqF1HeLKDJRSVxJpt6xoWOChGHg+TMyXDxK+N Xl89vGvdU1XfhKkVm9MDLOgT5ECDPysDGHFPDhqHOSu3Kaw2DWMV/iH9vz 3Z20erVNbdcvyBnuojcoWO/6yfB5EQO0BXp7kcyy00USA3CjD5FGZdoQGI Tb8A/ar0tVA5crSQmaSotm6KmNLhrFnZ5BxX+TiE+eTUTqSloWRY6VAvqW QHC7OASxK5E6RXPBuFH5IohUA2Qbk5AHt99pYvsIPX88j2rWauOokoiKZo t/9leJ8VxO5l3wf/U64IH8bkPIoWmWZfd/nqh4uwGNbCgKMyT+AnvH7kMJ 3i7DivfWl2mKLV0PyPHUNva0VQxX6yYjcOhj1R6fCr/at8/NSLe2OhLchz dC+Ls9h+kvJXgF8Sisv+Wk/1RadPLFmraRlqvJwt6Ww21LpiXqXHV2mIgq WnR98YgSvUi3TJHrUQiNc9YyBzuRo0AjgG2C9qiE3FM+Y28+iQ/sR3+bFs zYuZKVTObqiIslwXu7imO0cvvFRgJF/6u3HNFQ4LUTGhiM3FQmC6NNlF3/ vJM2hwRDMcJqDd54Twx90Wh+tYz0z7QMsK4ANXWHHWHR0JchnLWmenzbtW 5MHdW9AYsNJZAQSOpirE4Xi31CSlWAi9KV+cUCmWF5zOFy1x23P6PjdaRm 4T2zw4dxS5NswXWU0sVEXxjs6PYxuIiCTL7vdpx8QjBkrPWDrAbcMyBr2O QlnHIvPzEArRQLo= host = foo.example.org interval = 1m # Client "bar" [bar] fingerprint = 3e393aeaefb84c7e89e2f547b3a107558fca3a27 secfile = /etc/mandos/bar-secret timeout = 15m approved_by_default = False approval_delay = 30s SEE ALSO intro 8mandos, mandos-keygen 8, mandos.conf 5, mandos 8, fping 8
mandos-1.6.0/mandos-keygen0000775000175000017500000002310611767455003015746 0ustar teddyteddy00000000000000#!/bin/sh -e # # Mandos key generator - create a new OpenPGP key for a Mandos client # # Copyright © 2008-2012 Teddy Hogeborn # Copyright © 2008-2012 Björn Påhlsson # # 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 of the License, 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 . # # Contact the authors at . # VERSION="1.6.0" KEYDIR="/etc/keys/mandos" KEYTYPE=DSA KEYLENGTH=2048 SUBKEYTYPE=ELG-E SUBKEYLENGTH=2048 KEYNAME="`hostname --fqdn 2>/dev/null || hostname`" KEYEMAIL="" KEYCOMMENT="Mandos client key" KEYEXPIRE=0 FORCE=no KEYCOMMENT_ORIG="$KEYCOMMENT" mode=keygen if [ ! -d "$KEYDIR" ]; then KEYDIR="/etc/mandos/keys" fi # Parse options TEMP=`getopt --options vhpF:d:t:l:s:L:n:e:c:x:f \ --longoptions version,help,password,passfile:,dir:,type:,length:,subtype:,sublength:,name:,email:,comment:,expire:,force \ --name "$0" -- "$@"` help(){ basename="`basename $0`" cat <&2; exit 1;; esac done if [ "$#" -gt 0 ]; then echo "Unknown arguments: '$@'" >&2 exit 1 fi SECKEYFILE="$KEYDIR/seckey.txt" PUBKEYFILE="$KEYDIR/pubkey.txt" # Check for some invalid values if [ ! -d "$KEYDIR" ]; then echo "$KEYDIR not a directory" >&2 exit 1 fi if [ ! -r "$KEYDIR" ]; then echo "Directory $KEYDIR not readable" >&2 exit 1 fi if [ "$mode" = keygen ]; then if [ ! -w "$KEYDIR" ]; then echo "Directory $KEYDIR not writeable" >&2 exit 1 fi if [ -z "$KEYTYPE" ]; then echo "Empty key type" >&2 exit 1 fi if [ -z "$KEYNAME" ]; then echo "Empty key name" >&2 exit 1 fi if [ -z "$KEYLENGTH" ] || [ "$KEYLENGTH" -lt 512 ]; then echo "Invalid key length" >&2 exit 1 fi if [ -z "$KEYEXPIRE" ]; then echo "Empty key expiration" >&2 exit 1 fi # Make FORCE be 0 or 1 case "$FORCE" in [Yy][Ee][Ss]|[Tt][Rr][Uu][Ee]) FORCE=1;; [Nn][Oo]|[Ff][Aa][Ll][Ss][Ee]|*) FORCE=0;; esac if [ \( -e "$SECKEYFILE" -o -e "$PUBKEYFILE" \) \ -a "$FORCE" -eq 0 ]; then echo "Refusing to overwrite old key files; use --force" >&2 exit 1 fi # Set lines for GnuPG batch file if [ -n "$KEYCOMMENT" ]; then KEYCOMMENTLINE="Name-Comment: $KEYCOMMENT" fi if [ -n "$KEYEMAIL" ]; then KEYEMAILLINE="Name-Email: $KEYEMAIL" fi # Create temporary gpg batch file BATCHFILE="`mktemp -t mandos-keygen-batch.XXXXXXXXXX`" fi if [ "$mode" = password ]; then # Create temporary encrypted password file SECFILE="`mktemp -t mandos-keygen-secfile.XXXXXXXXXX`" fi # Create temporary key ring directory RINGDIR="`mktemp -d -t mandos-keygen-keyrings.XXXXXXXXXX`" # Remove temporary files on exit trap " set +e; \ test -n \"$SECFILE\" && shred --remove \"$SECFILE\"; \ shred --remove \"$RINGDIR\"/sec*; test -n \"$BATCHFILE\" && rm --force \"$BATCHFILE\"; \ rm --recursive --force \"$RINGDIR\"; tty --quiet && stty echo; \ " EXIT set -e umask 077 if [ "$mode" = keygen ]; then # Create batch file for GnuPG cat >"$BATCHFILE" <<-EOF Key-Type: $KEYTYPE Key-Length: $KEYLENGTH #Key-Usage: encrypt,sign,auth Subkey-Type: $SUBKEYTYPE Subkey-Length: $SUBKEYLENGTH #Subkey-Usage: encrypt,sign,auth Name-Real: $KEYNAME $KEYCOMMENTLINE $KEYEMAILLINE Expire-Date: $KEYEXPIRE #Preferences: #Handle: #%pubring pubring.gpg #%secring secring.gpg %commit EOF if tty --quiet; then cat <<-EOF Note: Due to entropy requirements, key generation could take anything from a few minutes to SEVERAL HOURS. Please be patient and/or supply the system with more entropy if needed. EOF echo -n "Started: " date fi # Generate a new key in the key rings gpg --quiet --batch --no-tty --no-options --enable-dsa2 \ --homedir "$RINGDIR" --trust-model always \ --gen-key "$BATCHFILE" rm --force "$BATCHFILE" if tty --quiet; then echo -n "Finished: " date fi # Backup any old key files if cp --backup=numbered --force "$SECKEYFILE" "$SECKEYFILE" \ 2>/dev/null; then shred --remove "$SECKEYFILE" fi if cp --backup=numbered --force "$PUBKEYFILE" "$PUBKEYFILE" \ 2>/dev/null; then rm --force "$PUBKEYFILE" fi FILECOMMENT="Mandos client key for $KEYNAME" if [ "$KEYCOMMENT" != "$KEYCOMMENT_ORIG" ]; then FILECOMMENT="$FILECOMMENT ($KEYCOMMENT)" fi if [ -n "$KEYEMAIL" ]; then FILECOMMENT="$FILECOMMENT <$KEYEMAIL>" fi # Export key from key rings to key files gpg --quiet --batch --no-tty --no-options --enable-dsa2 \ --homedir "$RINGDIR" --armor --export-options export-minimal \ --comment "$FILECOMMENT" --output "$SECKEYFILE" \ --export-secret-keys gpg --quiet --batch --no-tty --no-options --enable-dsa2 \ --homedir "$RINGDIR" --armor --export-options export-minimal \ --comment "$FILECOMMENT" --output "$PUBKEYFILE" --export fi if [ "$mode" = password ]; then # Import key into temporary key rings gpg --quiet --batch --no-tty --no-options --enable-dsa2 \ --homedir "$RINGDIR" --trust-model always --armor \ --import "$SECKEYFILE" gpg --quiet --batch --no-tty --no-options --enable-dsa2 \ --homedir "$RINGDIR" --trust-model always --armor \ --import "$PUBKEYFILE" # Get fingerprint of key FINGERPRINT="`gpg --quiet --batch --no-tty --no-options \ --enable-dsa2 --homedir \"$RINGDIR\" --trust-model always \ --fingerprint --with-colons \ | sed --quiet \ --expression='/^fpr:/{s/^fpr:.*:\\([0-9A-Z]*\\):\$/\\1/p;q}'`" test -n "$FINGERPRINT" FILECOMMENT="Encrypted password for a Mandos client" while [ ! -s "$SECFILE" ]; do if [ -n "$PASSFILE" ]; then cat "$PASSFILE" else tty --quiet && stty -echo read -p "Enter passphrase: " first tty --quiet && echo >&2 read -p "Repeat passphrase: " second if tty --quiet; then echo >&2 stty echo fi if [ "$first" != "$second" ]; then echo "Passphrase mismatch" >&2 touch "$RINGDIR"/mismatch else echo -n "$first" fi fi | gpg --quiet --batch --no-tty --no-options --enable-dsa2 \ --homedir "$RINGDIR" --trust-model always --armor \ --encrypt --sign --recipient "$FINGERPRINT" --comment \ "$FILECOMMENT" > "$SECFILE" if [ -e "$RINGDIR"/mismatch ]; then rm --force "$RINGDIR"/mismatch if tty --quiet; then > "$SECFILE" else exit 1 fi fi done cat <<-EOF [$KEYNAME] host = $KEYNAME fingerprint = $FINGERPRINT secret = EOF sed --quiet --expression=' /^-----BEGIN PGP MESSAGE-----$/,/^-----END PGP MESSAGE-----$/{ /^$/,${ # Remove 24-bit Radix-64 checksum s/=....$// # Indent four spaces /^[^-]/s/^/ /p } }' < "$SECFILE" fi trap - EXIT set +e # Remove the password file, if any if [ -n "$SECFILE" ]; then shred --remove "$SECFILE" fi # Remove the key rings shred --remove "$RINGDIR"/sec* rm --recursive --force "$RINGDIR" mandos-1.6.0/mandos-ctl0000775000175000017500000003410111767455003015243 0ustar teddyteddy00000000000000#!/usr/bin/python # -*- mode: python; coding: utf-8 -*- # # Mandos Monitor - Control and monitor the Mandos server # # Copyright © 2008-2012 Teddy Hogeborn # Copyright © 2008-2012 Björn Påhlsson # # 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 of the License, 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 # . # # Contact the authors at . # from __future__ import (division, absolute_import, print_function, unicode_literals) from future_builtins import * import sys import dbus import argparse import locale import datetime import re import os locale.setlocale(locale.LC_ALL, "") tablewords = { "Name": "Name", "Enabled": "Enabled", "Timeout": "Timeout", "LastCheckedOK": "Last Successful Check", "LastApprovalRequest": "Last Approval Request", "Created": "Created", "Interval": "Interval", "Host": "Host", "Fingerprint": "Fingerprint", "CheckerRunning": "Check Is Running", "LastEnabled": "Last Enabled", "ApprovalPending": "Approval Is Pending", "ApprovedByDefault": "Approved By Default", "ApprovalDelay": "Approval Delay", "ApprovalDuration": "Approval Duration", "Checker": "Checker", "ExtendedTimeout" : "Extended Timeout" } defaultkeywords = ("Name", "Enabled", "Timeout", "LastCheckedOK") domain = "se.recompile" busname = domain + ".Mandos" server_path = "/" server_interface = domain + ".Mandos" client_interface = domain + ".Mandos.Client" version = "1.6.0" def timedelta_to_milliseconds(td): """Convert a datetime.timedelta object to milliseconds""" return ((td.days * 24 * 60 * 60 * 1000) + (td.seconds * 1000) + (td.microseconds // 1000)) def milliseconds_to_string(ms): td = datetime.timedelta(0, 0, 0, ms) return ("{days}{hours:02}:{minutes:02}:{seconds:02}" .format(days = "{0}T".format(td.days) if td.days else "", hours = td.seconds // 3600, minutes = (td.seconds % 3600) // 60, seconds = td.seconds % 60, )) def string_to_delta(interval): """Parse a string and return a datetime.timedelta >>> string_to_delta("7d") datetime.timedelta(7) >>> string_to_delta("60s") datetime.timedelta(0, 60) >>> string_to_delta("60m") datetime.timedelta(0, 3600) >>> string_to_delta("24h") datetime.timedelta(1) >>> string_to_delta("1w") datetime.timedelta(7) >>> string_to_delta("5m 30s") datetime.timedelta(0, 330) """ value = datetime.timedelta(0) regexp = re.compile("(\d+)([dsmhw]?)") for num, suffix in regexp.findall(interval): if suffix == "d": value += datetime.timedelta(int(num)) elif suffix == "s": value += datetime.timedelta(0, int(num)) elif suffix == "m": value += datetime.timedelta(0, 0, 0, 0, int(num)) elif suffix == "h": value += datetime.timedelta(0, 0, 0, 0, 0, int(num)) elif suffix == "w": value += datetime.timedelta(0, 0, 0, 0, 0, 0, int(num)) elif suffix == "": value += datetime.timedelta(0, 0, 0, int(num)) return value def print_clients(clients, keywords): def valuetostring(value, keyword): if type(value) is dbus.Boolean: return "Yes" if value else "No" if keyword in ("Timeout", "Interval", "ApprovalDelay", "ApprovalDuration", "ExtendedTimeout"): return milliseconds_to_string(value) return unicode(value) # Create format string to print table rows format_string = " ".join("{{{key}:{width}}}".format( width = max(len(tablewords[key]), max(len(valuetostring(client[key], key)) for client in clients)), key = key) for key in keywords) # Print header line print(format_string.format(**tablewords)) for client in clients: print(format_string.format(**dict((key, valuetostring(client[key], key)) for key in keywords))) def has_actions(options): return any((options.enable, options.disable, options.bump_timeout, options.start_checker, options.stop_checker, options.is_enabled, options.remove, options.checker is not None, options.timeout is not None, options.extended_timeout is not None, options.interval is not None, options.approved_by_default is not None, options.approval_delay is not None, options.approval_duration is not None, options.host is not None, options.secret is not None, options.approve, options.deny)) def main(): parser = argparse.ArgumentParser() parser.add_argument("--version", action="version", version = "%(prog)s {0}".format(version), help="show version number and exit") parser.add_argument("-a", "--all", action="store_true", help="Select all clients") parser.add_argument("-v", "--verbose", action="store_true", help="Print all fields") parser.add_argument("-e", "--enable", action="store_true", help="Enable client") parser.add_argument("-d", "--disable", action="store_true", help="disable client") parser.add_argument("-b", "--bump-timeout", action="store_true", help="Bump timeout for client") parser.add_argument("--start-checker", action="store_true", help="Start checker for client") parser.add_argument("--stop-checker", action="store_true", help="Stop checker for client") parser.add_argument("-V", "--is-enabled", action="store_true", help="Check if client is enabled") parser.add_argument("-r", "--remove", action="store_true", help="Remove client") parser.add_argument("-c", "--checker", help="Set checker command for client") parser.add_argument("-t", "--timeout", help="Set timeout for client") parser.add_argument("--extended-timeout", help="Set extended timeout for client") parser.add_argument("-i", "--interval", help="Set checker interval for client") parser.add_argument("--approve-by-default", action="store_true", default=None, dest="approved_by_default", help="Set client to be approved by default") parser.add_argument("--deny-by-default", action="store_false", dest="approved_by_default", help="Set client to be denied by default") parser.add_argument("--approval-delay", help="Set delay before client approve/deny") parser.add_argument("--approval-duration", help="Set duration of one client approval") parser.add_argument("-H", "--host", help="Set host for client") parser.add_argument("-s", "--secret", type=file, help="Set password blob (file) for client") parser.add_argument("-A", "--approve", action="store_true", help="Approve any current client request") parser.add_argument("-D", "--deny", action="store_true", help="Deny any current client request") parser.add_argument("client", nargs="*", help="Client name") options = parser.parse_args() if has_actions(options) and not (options.client or options.all): parser.error("Options require clients names or --all.") if options.verbose and has_actions(options): parser.error("--verbose can only be used alone or with" " --all.") if options.all and not has_actions(options): parser.error("--all requires an action.") try: bus = dbus.SystemBus() mandos_dbus_objc = bus.get_object(busname, server_path) except dbus.exceptions.DBusException: print("Could not connect to Mandos server", file=sys.stderr) sys.exit(1) mandos_serv = dbus.Interface(mandos_dbus_objc, dbus_interface = server_interface) #block stderr since dbus library prints to stderr null = os.open(os.path.devnull, os.O_RDWR) stderrcopy = os.dup(sys.stderr.fileno()) os.dup2(null, sys.stderr.fileno()) os.close(null) try: try: mandos_clients = mandos_serv.GetAllClientsWithProperties() finally: #restore stderr os.dup2(stderrcopy, sys.stderr.fileno()) os.close(stderrcopy) except dbus.exceptions.DBusException: print("Access denied: Accessing mandos server through dbus.", file=sys.stderr) sys.exit(1) # Compile dict of (clients: properties) to process clients={} if options.all or not options.client: clients = dict((bus.get_object(busname, path), properties) for path, properties in mandos_clients.iteritems()) else: for name in options.client: for path, client in mandos_clients.iteritems(): if client["Name"] == name: client_objc = bus.get_object(busname, path) clients[client_objc] = client break else: print("Client not found on server: {0!r}" .format(name), file=sys.stderr) sys.exit(1) if not has_actions(options) and clients: if options.verbose: keywords = ("Name", "Enabled", "Timeout", "LastCheckedOK", "Created", "Interval", "Host", "Fingerprint", "CheckerRunning", "LastEnabled", "ApprovalPending", "ApprovedByDefault", "LastApprovalRequest", "ApprovalDelay", "ApprovalDuration", "Checker", "ExtendedTimeout") else: keywords = defaultkeywords print_clients(clients.values(), keywords) else: # Process each client in the list by all selected options for client in clients: def set_client_prop(prop, value): """Set a Client D-Bus property""" client.Set(client_interface, prop, value, dbus_interface=dbus.PROPERTIES_IFACE) def set_client_prop_ms(prop, value): """Set a Client D-Bus property, converted from a string to milliseconds.""" set_client_prop(prop, timedelta_to_milliseconds (string_to_delta(value))) if options.remove: mandos_serv.RemoveClient(client.__dbus_object_path__) if options.enable: set_client_prop("Enabled", dbus.Boolean(True)) if options.disable: set_client_prop("Enabled", dbus.Boolean(False)) if options.bump_timeout: set_client_prop("LastCheckedOK", "") if options.start_checker: set_client_prop("CheckerRunning", dbus.Boolean(True)) if options.stop_checker: set_client_prop("CheckerRunning", dbus.Boolean(False)) if options.is_enabled: sys.exit(0 if client.Get(client_interface, "Enabled", dbus_interface= dbus.PROPERTIES_IFACE) else 1) if options.checker is not None: set_client_prop("Checker", options.checker) if options.host is not None: set_client_prop("Host", options.host) if options.interval is not None: set_client_prop_ms("Interval", options.interval) if options.approval_delay is not None: set_client_prop_ms("ApprovalDelay", options.approval_delay) if options.approval_duration is not None: set_client_prop_ms("ApprovalDuration", options.approval_duration) if options.timeout is not None: set_client_prop_ms("Timeout", options.timeout) if options.extended_timeout is not None: set_client_prop_ms("ExtendedTimeout", options.extended_timeout) if options.secret is not None: set_client_prop("Secret", dbus.ByteArray(options.secret.read())) if options.approved_by_default is not None: set_client_prop("ApprovedByDefault", dbus.Boolean(options .approved_by_default)) if options.approve: client.Approve(dbus.Boolean(True), dbus_interface=client_interface) elif options.deny: client.Approve(dbus.Boolean(False), dbus_interface=client_interface) if __name__ == "__main__": main() mandos-1.6.0/DBUS-API0000664000175000017500000001545711767455003014360 0ustar teddyteddy00000000000000 -*- mode: org; coding: utf-8 -*- Mandos Server D-Bus Interface This file documents the D-Bus interface to the Mandos server. * Bus: System bus Bus name: "se.recompile.Mandos" * Object Paths: | Path | Object | |-----------------------+-------------------| | "/" | The Mandos Server | | "/clients/CLIENTNAME" | Mandos Client | * Mandos Server Interface: Interface name: "se.recompile.Mandos" ** Methods: *** GetAllClients() → (ao: Clients) Returns an array of all client D-Bus object paths *** GetAllClientsWithProperties() → (a{oa{sv}}: ClientProperties) Returns an array of all clients and all their properties *** RemoveClient(o: ObjectPath) → nothing Removes a client ** Signals: *** ClientAdded(o: ObjectPath) A new client was added. *** ClientNotFound(s: Fingerprint, s: Address) A client connected from Address using Fingerprint, but was rejected because it was not found in the server. The fingerprint is represented as a string of hexadecimal digits. The address is an IPv4 or IPv6 address in its normal string format. *** ClientRemoved(o: ObjectPath, s: Name) A client named Name on ObjectPath was removed. * Mandos Client Interface: Interface name: "se.recompile.Mandos.Client" ** Methods *** Approve(b: Approve) → nothing Approve or deny a connected client waiting for approval. If denied, a client will not be sent its secret. *** CheckedOK() → nothing Assert that this client has been checked and found to be alive. This will restart the timeout before disabling this client. See also the "LastCheckedOK" property. *** Disable() → nothing Disable this client. See also the "Enabled" property. *** Enable() → nothing Enable this client. See also the "Enabled" property. *** StartChecker() → nothing Start a new checker for this client, if none is currently running. See also the "CheckerRunning" property. *** StopChecker() → nothing Abort a running checker process for this client, if any. See also the "CheckerRunning" property. ** Properties Note: Many of these properties directly correspond to a setting in "clients.conf", in which case they are fully documented in mandos-clients.conf(5). | Name | Type | Access | clients.conf | |-------------------------+------+------------+---------------------| | ApprovedByDefault | b | Read/Write | approved_by_default | | ApprovalDelay (a) | t | Read/Write | approval_delay | | ApprovalDuration (a) | t | Read/Write | approval_duration | | ApprovalPending (b) | b | Read | N/A | | Checker | s | Read/Write | checker | | CheckerRunning (c) | b | Read/Write | N/A | | Created (d) | s | Read | N/A | | Enabled (e) | b | Read/Write | N/A | | Expires (f) | s | Read | N/A | | ExtendedTimeout (a) | t | Read/Write | extended_timeout | | Fingerprint | s | Read | fingerprint | | Host | s | Read/Write | host | | Interval (a) | t | Read/Write | interval | | LastApprovalRequest (g) | s | Read | N/A | | LastCheckedOK (h) | s | Read/Write | N/A | | LastCheckerStatus (i) | n | Read | N/A | | LastEnabled (j) | s | Read | N/A | | Name | s | Read | (Section name) | | ObjectPath | o | Read | N/A | | Secret (k) | ay | Write | secret (or secfile) | | Timeout (a) | t | Read/Write | timeout | a) Represented as milliseconds. b) An approval is currently pending. c) Setting this property is equivalent to calling StartChecker() or StopChecker(). d) The creation time of this client object, as an RFC 3339 string. e) Setting this property is equivalent to calling Enable() or Disable(). f) The date and time this client will be disabled, as an RFC 3339 string, or an empty string if this is not scheduled. g) The date and time of the last approval request, as an RFC 3339 string, or an empty string if this has not happened. h) The date and time a checker was last successful, as an RFC 3339 string, or an empty string if this has not happened. Setting this property is equivalent to calling CheckedOK(), i.e. the current time is set, regardless of the string sent. Please always use an empty string when setting this property, to allow for possible future expansion. i) The exit status of the last checker, -1 if it did not exit cleanly, -2 if a checker has not yet returned. j) The date and time this client was last enabled, as an RFC 3339 string, or an empty string if this has not happened. k) A raw byte array, not hexadecimal digits. ** Signals *** CheckerCompleted(n: Exitcode, x: Waitstatus, s: Command) A checker (Command) has completed. Exitcode is either the exit code or -1 for abnormal exit. In any case, the full Waitstatus (as from wait(2)) is also available. *** CheckerStarted(s: Command) A checker command (Command) has just been started. *** GotSecret() This client has been sent its secret. *** NeedApproval(t: Timeout, b: ApprovedByDefault) This client will be approved or denied in exactly Timeout milliseconds, depending on ApprovedByDefault. Approve() can now usefully be called on this client object. *** PropertyChanged(s: Property, v: Value) The Property on this client has changed to Value. *** Rejected(s: Reason) This client was not given its secret for a specified Reason. * Copyright Copyright © 2010-2012 Teddy Hogeborn Copyright © 2010-2012 Björn Påhlsson ** License: 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 of the License, 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 . #+STARTUP: showall mandos-1.6.0/overview.xml0000664000175000017500000000163611767455003015653 0ustar teddyteddy00000000000000 This is part of the Mandos system for allowing computers to have encrypted root file systems and at the same time be capable of remote and/or unattended reboots. The computers run a small client program in the initial RAM disk environment which will communicate with a server over a network. All network communication is encrypted using TLS. The clients are identified by the server using an OpenPGP key; each client has one unique to it. The server sends the clients an encrypted password. The encrypted password is decrypted by the clients using the same OpenPGP key, and the password is then used to unlock the root file system, whereupon the computers can continue booting normally. mandos-1.6.0/mandos-monitor0000775000175000017500000007251311767455003016161 0ustar teddyteddy00000000000000#!/usr/bin/python # -*- mode: python; coding: utf-8 -*- # # Mandos Monitor - Control and monitor the Mandos server # # Copyright © 2009-2012 Teddy Hogeborn # Copyright © 2009-2012 Björn Påhlsson # # 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 of the License, 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 # . # # Contact the authors at . # from __future__ import (division, absolute_import, print_function, unicode_literals) from future_builtins import * import sys import os import signal import datetime import urwid.curses_display import urwid from dbus.mainloop.glib import DBusGMainLoop import gobject import dbus import UserList import locale locale.setlocale(locale.LC_ALL, '') import logging logging.getLogger('dbus.proxies').setLevel(logging.CRITICAL) # Some useful constants domain = 'se.recompile' server_interface = domain + '.Mandos' client_interface = domain + '.Mandos.Client' version = "1.6.0" # Always run in monochrome mode urwid.curses_display.curses.has_colors = lambda : False # Urwid doesn't support blinking, but we want it. Since we have no # use for underline on its own, we make underline also always blink. urwid.curses_display.curses.A_UNDERLINE |= ( urwid.curses_display.curses.A_BLINK) def isoformat_to_datetime(iso): "Parse an ISO 8601 date string to a datetime.datetime()" if not iso: return None d, t = iso.split("T", 1) year, month, day = d.split("-", 2) hour, minute, second = t.split(":", 2) second, fraction = divmod(float(second), 1) return datetime.datetime(int(year), int(month), int(day), int(hour), int(minute), int(second), # Whole seconds int(fraction*1000000)) # Microseconds class MandosClientPropertyCache(object): """This wraps a Mandos Client D-Bus proxy object, caches the properties and calls a hook function when any of them are changed. """ def __init__(self, proxy_object=None, properties=None, **kwargs): self.proxy = proxy_object # Mandos Client proxy object self.properties = dict() if properties is None else properties self.property_changed_match = ( self.proxy.connect_to_signal("PropertyChanged", self._property_changed, client_interface, byte_arrays=True)) if properties is None: self.properties.update( self.proxy.GetAll(client_interface, dbus_interface = dbus.PROPERTIES_IFACE)) super(MandosClientPropertyCache, self).__init__(**kwargs) def _property_changed(self, property, value): """Helper which takes positional arguments""" return self.property_changed(property=property, value=value) def property_changed(self, property=None, value=None): """This is called whenever we get a PropertyChanged signal It updates the changed property in the "properties" dict. """ # Update properties dict with new value self.properties[property] = value def delete(self): self.property_changed_match.remove() class MandosClientWidget(urwid.FlowWidget, MandosClientPropertyCache): """A Mandos Client which is visible on the screen. """ def __init__(self, server_proxy_object=None, update_hook=None, delete_hook=None, logger=None, **kwargs): # Called on update self.update_hook = update_hook # Called on delete self.delete_hook = delete_hook # Mandos Server proxy object self.server_proxy_object = server_proxy_object # Logger self.logger = logger self._update_timer_callback_tag = None # The widget shown normally self._text_widget = urwid.Text("") # The widget shown when we have focus self._focus_text_widget = urwid.Text("") super(MandosClientWidget, self).__init__(**kwargs) self.update() self.opened = False self.match_objects = ( self.proxy.connect_to_signal("CheckerCompleted", self.checker_completed, client_interface, byte_arrays=True), self.proxy.connect_to_signal("CheckerStarted", self.checker_started, client_interface, byte_arrays=True), self.proxy.connect_to_signal("GotSecret", self.got_secret, client_interface, byte_arrays=True), self.proxy.connect_to_signal("NeedApproval", self.need_approval, client_interface, byte_arrays=True), self.proxy.connect_to_signal("Rejected", self.rejected, client_interface, byte_arrays=True)) #self.logger('Created client {0}' # .format(self.properties["Name"])) def using_timer(self, flag): """Call this method with True or False when timer should be activated or deactivated. """ if flag and self._update_timer_callback_tag is None: # Will update the shown timer value every second self._update_timer_callback_tag = (gobject.timeout_add (1000, self.update_timer)) elif not (flag or self._update_timer_callback_tag is None): gobject.source_remove(self._update_timer_callback_tag) self._update_timer_callback_tag = None def checker_completed(self, exitstatus, condition, command): if exitstatus == 0: self.update() return # Checker failed if os.WIFEXITED(condition): self.logger('Checker for client {0} (command "{1}")' ' failed with exit code {2}' .format(self.properties["Name"], command, os.WEXITSTATUS(condition))) elif os.WIFSIGNALED(condition): self.logger('Checker for client {0} (command "{1}") was' ' killed by signal {2}' .format(self.properties["Name"], command, os.WTERMSIG(condition))) elif os.WCOREDUMP(condition): self.logger('Checker for client {0} (command "{1}")' ' dumped core' .format(self.properties["Name"], command)) else: self.logger('Checker for client {0} completed' ' mysteriously' .format(self.properties["Name"])) self.update() def checker_started(self, command): """Server signals that a checker started. This could be useful to log in the future. """ #self.logger('Client {0} started checker "{1}"' # .format(self.properties["Name"], # unicode(command))) pass def got_secret(self): self.logger('Client {0} received its secret' .format(self.properties["Name"])) def need_approval(self, timeout, default): if not default: message = 'Client {0} needs approval within {1} seconds' else: message = 'Client {0} will get its secret in {1} seconds' self.logger(message.format(self.properties["Name"], timeout/1000)) def rejected(self, reason): self.logger('Client {0} was rejected; reason: {1}' .format(self.properties["Name"], reason)) def selectable(self): """Make this a "selectable" widget. This overrides the method from urwid.FlowWidget.""" return True def rows(self, maxcolrow, focus=False): """How many rows this widget will occupy might depend on whether we have focus or not. This overrides the method from urwid.FlowWidget""" return self.current_widget(focus).rows(maxcolrow, focus=focus) def current_widget(self, focus=False): if focus or self.opened: return self._focus_widget return self._widget def update(self): "Called when what is visible on the screen should be updated." # How to add standout mode to a style with_standout = { "normal": "standout", "bold": "bold-standout", "underline-blink": "underline-blink-standout", "bold-underline-blink": "bold-underline-blink-standout", } # Rebuild focus and non-focus widgets using current properties # Base part of a client. Name! base = '{name}: '.format(name=self.properties["Name"]) if not self.properties["Enabled"]: message = "DISABLED" self.using_timer(False) elif self.properties["ApprovalPending"]: timeout = datetime.timedelta(milliseconds = self.properties ["ApprovalDelay"]) last_approval_request = isoformat_to_datetime( self.properties["LastApprovalRequest"]) if last_approval_request is not None: timer = max(timeout - (datetime.datetime.utcnow() - last_approval_request), datetime.timedelta()) else: timer = datetime.timedelta() if self.properties["ApprovedByDefault"]: message = "Approval in {0}. (d)eny?" else: message = "Denial in {0}. (a)pprove?" message = message.format(unicode(timer).rsplit(".", 1)[0]) self.using_timer(True) elif self.properties["LastCheckerStatus"] != 0: # When checker has failed, show timer until client expires expires = self.properties["Expires"] if expires == "": timer = datetime.timedelta(0) else: expires = (datetime.datetime.strptime (expires, '%Y-%m-%dT%H:%M:%S.%f')) timer = max(expires - datetime.datetime.utcnow(), datetime.timedelta()) message = ('A checker has failed! Time until client' ' gets disabled: {0}' .format(unicode(timer).rsplit(".", 1)[0])) self.using_timer(True) else: message = "enabled" self.using_timer(False) self._text = "{0}{1}".format(base, message) if not urwid.supports_unicode(): self._text = self._text.encode("ascii", "replace") textlist = [("normal", self._text)] self._text_widget.set_text(textlist) self._focus_text_widget.set_text([(with_standout[text[0]], text[1]) if isinstance(text, tuple) else text for text in textlist]) self._widget = self._text_widget self._focus_widget = urwid.AttrWrap(self._focus_text_widget, "standout") # Run update hook, if any if self.update_hook is not None: self.update_hook() def update_timer(self): """called by gobject. Will indefinitely loop until gobject.source_remove() on tag is called""" self.update() return True # Keep calling this def delete(self, **kwargs): if self._update_timer_callback_tag is not None: gobject.source_remove(self._update_timer_callback_tag) self._update_timer_callback_tag = None for match in self.match_objects: match.remove() self.match_objects = () if self.delete_hook is not None: self.delete_hook(self) return super(MandosClientWidget, self).delete(**kwargs) def render(self, maxcolrow, focus=False): """Render differently if we have focus. This overrides the method from urwid.FlowWidget""" return self.current_widget(focus).render(maxcolrow, focus=focus) def keypress(self, maxcolrow, key): """Handle keys. This overrides the method from urwid.FlowWidget""" if key == "+": self.proxy.Enable(dbus_interface = client_interface, ignore_reply=True) elif key == "-": self.proxy.Disable(dbus_interface = client_interface, ignore_reply=True) elif key == "a": self.proxy.Approve(dbus.Boolean(True, variant_level=1), dbus_interface = client_interface, ignore_reply=True) elif key == "d": self.proxy.Approve(dbus.Boolean(False, variant_level=1), dbus_interface = client_interface, ignore_reply=True) elif key == "R" or key == "_" or key == "ctrl k": self.server_proxy_object.RemoveClient(self.proxy .object_path, ignore_reply=True) elif key == "s": self.proxy.StartChecker(dbus_interface = client_interface, ignore_reply=True) elif key == "S": self.proxy.StopChecker(dbus_interface = client_interface, ignore_reply=True) elif key == "C": self.proxy.CheckedOK(dbus_interface = client_interface, ignore_reply=True) # xxx # elif key == "p" or key == "=": # self.proxy.pause() # elif key == "u" or key == ":": # self.proxy.unpause() # elif key == "RET": # self.open() else: return key def property_changed(self, property=None, **kwargs): """Call self.update() if old value is not new value. This overrides the method from MandosClientPropertyCache""" property_name = unicode(property) old_value = self.properties.get(property_name) super(MandosClientWidget, self).property_changed( property=property, **kwargs) if self.properties.get(property_name) != old_value: self.update() class ConstrainedListBox(urwid.ListBox): """Like a normal urwid.ListBox, but will consume all "up" or "down" key presses, thus not allowing any containing widgets to use them as an excuse to shift focus away from this widget. """ def keypress(self, *args, **kwargs): ret = super(ConstrainedListBox, self).keypress(*args, **kwargs) if ret in ("up", "down"): return return ret class UserInterface(object): """This is the entire user interface - the whole screen with boxes, lists of client widgets, etc. """ def __init__(self, max_log_length=1000): DBusGMainLoop(set_as_default=True) self.screen = urwid.curses_display.Screen() self.screen.register_palette(( ("normal", "default", "default", None), ("bold", "default", "default", "bold"), ("underline-blink", "default", "default", "underline"), ("standout", "default", "default", "standout"), ("bold-underline-blink", "default", "default", ("bold", "underline")), ("bold-standout", "default", "default", ("bold", "standout")), ("underline-blink-standout", "default", "default", ("underline", "standout")), ("bold-underline-blink-standout", "default", "default", ("bold", "underline", "standout")), )) if urwid.supports_unicode(): self.divider = "─" # \u2500 #self.divider = "━" # \u2501 else: #self.divider = "-" # \u002d self.divider = "_" # \u005f self.screen.start() self.size = self.screen.get_cols_rows() self.clients = urwid.SimpleListWalker([]) self.clients_dict = {} # We will add Text widgets to this list self.log = [] self.max_log_length = max_log_length # We keep a reference to the log widget so we can remove it # from the ListWalker without it getting destroyed self.logbox = ConstrainedListBox(self.log) # This keeps track of whether self.uilist currently has # self.logbox in it or not self.log_visible = True self.log_wrap = "any" self.rebuild() self.log_message_raw(("bold", "Mandos Monitor version " + version)) self.log_message_raw(("bold", "q: Quit ?: Help")) self.busname = domain + '.Mandos' self.main_loop = gobject.MainLoop() def client_not_found(self, fingerprint, address): self.log_message("Client with address {0} and fingerprint" " {1} could not be found" .format(address, fingerprint)) def rebuild(self): """This rebuilds the User Interface. Call this when the widget layout needs to change""" self.uilist = [] #self.uilist.append(urwid.ListBox(self.clients)) self.uilist.append(urwid.Frame(ConstrainedListBox(self. clients), #header=urwid.Divider(), header=None, footer= urwid.Divider(div_char= self.divider))) if self.log_visible: self.uilist.append(self.logbox) self.topwidget = urwid.Pile(self.uilist) def log_message(self, message): timestamp = datetime.datetime.now().isoformat() self.log_message_raw(timestamp + ": " + message) def log_message_raw(self, markup): """Add a log message to the log buffer.""" self.log.append(urwid.Text(markup, wrap=self.log_wrap)) if (self.max_log_length and len(self.log) > self.max_log_length): del self.log[0:len(self.log)-self.max_log_length-1] self.logbox.set_focus(len(self.logbox.body.contents), coming_from="above") self.refresh() def toggle_log_display(self): """Toggle visibility of the log buffer.""" self.log_visible = not self.log_visible self.rebuild() #self.log_message("Log visibility changed to: " # + unicode(self.log_visible)) def change_log_display(self): """Change type of log display. Currently, this toggles wrapping of text lines.""" if self.log_wrap == "clip": self.log_wrap = "any" else: self.log_wrap = "clip" for textwidget in self.log: textwidget.set_wrap_mode(self.log_wrap) #self.log_message("Wrap mode: " + self.log_wrap) def find_and_remove_client(self, path, name): """Find a client by its object path and remove it. This is connected to the ClientRemoved signal from the Mandos server object.""" try: client = self.clients_dict[path] except KeyError: # not found? self.log_message("Unknown client {0!r} ({1!r}) removed" .format(name, path)) return client.delete() def add_new_client(self, path): client_proxy_object = self.bus.get_object(self.busname, path) self.add_client(MandosClientWidget(server_proxy_object =self.mandos_serv, proxy_object =client_proxy_object, update_hook =self.refresh, delete_hook =self.remove_client, logger =self.log_message), path=path) def add_client(self, client, path=None): self.clients.append(client) if path is None: path = client.proxy.object_path self.clients_dict[path] = client self.clients.sort(None, lambda c: c.properties["Name"]) self.refresh() def remove_client(self, client, path=None): self.clients.remove(client) if path is None: path = client.proxy.object_path del self.clients_dict[path] if not self.clients_dict: # Work around bug in Urwid 0.9.8.3 - if a SimpleListWalker # is completely emptied, we need to recreate it. self.clients = urwid.SimpleListWalker([]) self.rebuild() self.refresh() def refresh(self): """Redraw the screen""" canvas = self.topwidget.render(self.size, focus=True) self.screen.draw_screen(self.size, canvas) def run(self): """Start the main loop and exit when it's done.""" self.bus = dbus.SystemBus() mandos_dbus_objc = self.bus.get_object( self.busname, "/", follow_name_owner_changes=True) self.mandos_serv = dbus.Interface(mandos_dbus_objc, dbus_interface = server_interface) try: mandos_clients = (self.mandos_serv .GetAllClientsWithProperties()) except dbus.exceptions.DBusException: mandos_clients = dbus.Dictionary() (self.mandos_serv .connect_to_signal("ClientRemoved", self.find_and_remove_client, dbus_interface=server_interface, byte_arrays=True)) (self.mandos_serv .connect_to_signal("ClientAdded", self.add_new_client, dbus_interface=server_interface, byte_arrays=True)) (self.mandos_serv .connect_to_signal("ClientNotFound", self.client_not_found, dbus_interface=server_interface, byte_arrays=True)) for path, client in mandos_clients.iteritems(): client_proxy_object = self.bus.get_object(self.busname, path) self.add_client(MandosClientWidget(server_proxy_object =self.mandos_serv, proxy_object =client_proxy_object, properties=client, update_hook =self.refresh, delete_hook =self.remove_client, logger =self.log_message), path=path) self.refresh() self._input_callback_tag = (gobject.io_add_watch (sys.stdin.fileno(), gobject.IO_IN, self.process_input)) self.main_loop.run() # Main loop has finished, we should close everything now gobject.source_remove(self._input_callback_tag) self.screen.stop() def stop(self): self.main_loop.quit() def process_input(self, source, condition): keys = self.screen.get_input() translations = { "ctrl n": "down", # Emacs "ctrl p": "up", # Emacs "ctrl v": "page down", # Emacs "meta v": "page up", # Emacs " ": "page down", # less "f": "page down", # less "b": "page up", # less "j": "down", # vi "k": "up", # vi } for key in keys: try: key = translations[key] except KeyError: # :-) pass if key == "q" or key == "Q": self.stop() break elif key == "window resize": self.size = self.screen.get_cols_rows() self.refresh() elif key == "\f": # Ctrl-L self.refresh() elif key == "l" or key == "D": self.toggle_log_display() self.refresh() elif key == "w" or key == "i": self.change_log_display() self.refresh() elif key == "?" or key == "f1" or key == "esc": if not self.log_visible: self.log_visible = True self.rebuild() self.log_message_raw(("bold", " ". join(("q: Quit", "?: Help", "l: Log window toggle", "TAB: Switch window", "w: Wrap (log)")))) self.log_message_raw(("bold", " " .join(("Clients:", "+: Enable", "-: Disable", "R: Remove", "s: Start new checker", "S: Stop checker", "C: Checker OK", "a: Approve", "d: Deny")))) self.refresh() elif key == "tab": if self.topwidget.get_focus() is self.logbox: self.topwidget.set_focus(0) else: self.topwidget.set_focus(self.logbox) self.refresh() #elif (key == "end" or key == "meta >" or key == "G" # or key == ">"): # pass # xxx end-of-buffer #elif (key == "home" or key == "meta <" or key == "g" # or key == "<"): # pass # xxx beginning-of-buffer #elif key == "ctrl e" or key == "$": # pass # xxx move-end-of-line #elif key == "ctrl a" or key == "^": # pass # xxx move-beginning-of-line #elif key == "ctrl b" or key == "meta (" or key == "h": # pass # xxx left #elif key == "ctrl f" or key == "meta )" or key == "l": # pass # xxx right #elif key == "a": # pass # scroll up log #elif key == "z": # pass # scroll down log elif self.topwidget.selectable(): self.topwidget.keypress(self.size, key) self.refresh() return True ui = UserInterface() try: ui.run() except KeyboardInterrupt: ui.screen.stop() except Exception, e: ui.log_message(unicode(e)) ui.screen.stop() raise mandos-1.6.0/README0000664000175000017500000000063111767455003014135 0ustar teddyteddy00000000000000Please see: http://www.recompile.se/mandos/man/intro.8mandos This information previously in this file has been moved to the intro(8mandos) manual page. Go to the above URL, or install the Mandos server and run this command: man 8mandos intro In short, this is the Mandos system; it allows computers to have encrypted root file systems and at the same time be capable of remote and/or unattended reboots. mandos-1.6.0/intro.xml0000664000175000017500000003655211767455003015145 0ustar teddyteddy00000000000000 %common; ]> Mandos Manual Mandos &version; &TIMESTAMP; Björn Påhlsson
belorn@recompile.se
Teddy Hogeborn
teddy@recompile.se
2011 2012 Teddy Hogeborn Björn Påhlsson
intro 8mandos intro Introduction to the Mandos system DESCRIPTION This is the the Mandos system, which allows computers to have encrypted root file systems and at the same time be capable of remote and/or unattended reboots. The computers run a small client program in the initial RAM disk environment which will communicate with a server over a network. All network communication is encrypted using TLS. The clients are identified by the server using an OpenPGP key; each client has one unique to it. The server sends the clients an encrypted password. The encrypted password is decrypted by the clients using the same OpenPGP key, and the password is then used to unlock the root file system, whereupon the computers can continue booting normally. INTRODUCTION You know how it is. You’ve heard of it happening. The Man comes and takes away your servers, your friends’ servers, the servers of everybody in the same hosting facility. The servers of their neighbors, and their neighbors’ friends. The servers of people who owe them money. And like that, they’re gone. And you doubt you’ll ever see them again. That is why your servers have encrypted root file systems. However, there’s a downside. There’s no going around it: rebooting is a pain. Dragging out that rarely-used keyboard and screen and unraveling cables behind your servers to plug them in to type in that password is messy, especially if you have many servers. There are some people who do clever things like using serial line consoles and daisy-chain it to the next server, and keep all the servers connected in a ring with serial cables, which will work, if your servers are physically close enough. There are also other out-of-band management solutions, but with all these, you still have to be on hand and manually type in the password at boot time. Otherwise the server just sits there, waiting for a password. Wouldn’t it be great if you could have the security of encrypted root file systems and still have servers that could boot up automatically if there was a short power outage while you were asleep? That you could reboot at will, without having someone run over to the server to type in the password? Well, with Mandos, you (almost) can! The gain in convenience will only be offset by a small loss in security. The setup is as follows: The server will still have its encrypted root file system. The password to this file system will be stored on another computer (henceforth known as the Mandos server) on the same local network. The password will not be stored in plaintext, but encrypted with OpenPGP. To decrypt this password, a key is needed. This key (the Mandos client key) will not be stored there, but back on the original server (henceforth known as the Mandos client) in the initial RAM disk image. Oh, and all network Mandos client/server communications will be encrypted, using TLS (SSL). So, at boot time, the Mandos client will ask for its encrypted data over the network, decrypt it to get the password, use it to decrypt the root file, and continue booting. Now, of course the initial RAM disk image is not on the encrypted root file system, so anyone who had physical access could take the Mandos client computer offline and read the disk with their own tools to get the authentication keys used by a client. But, by then the Mandos server should notice that the original server has been offline for too long, and will no longer give out the encrypted key. The timing here is the only real weak point, and the method, frequency and timeout of the server’s checking can be adjusted to any desired level of paranoia (The encrypted keys on the Mandos server is on its normal file system, so those are safe, provided the root file system of that server is encrypted.) FREQUENTLY ASKED QUESTIONS Couldn’t the security be defeated by… Grabbing the Mandos client key from the initrd <emphasis>really quickly</emphasis>? This, as mentioned above, is the only real weak point. But if you set the timing values tight enough, this will be really difficult to do. An attacker would have to physically disassemble the client computer, extract the key from the initial RAM disk image, and then connect to a still online Mandos server to get the encrypted key, and do all this before the Mandos server timeout kicks in and the Mandos server refuses to give out the key to anyone. Now, as the typical procedure seems to be to barge in and turn off and grab all computers, to maybe look at them months later, this is not likely. If someone does that, the whole system will lock itself up completely, since Mandos servers are no longer running. For sophisticated attackers who could do the clever thing, and had physical access to the server for enough time, it would be simpler to get a key for an encrypted file system by using hardware memory scanners and reading it right off the memory bus. Replay attacks? Nope, the network stuff is all done over TLS, which provides protection against that. Man-in-the-middle? No. The server only gives out the passwords to clients which have in the TLS handshake proven that they do indeed hold the OpenPGP private key corresponding to that client. Physically grabbing the Mandos server computer? You could protect that computer the old-fashioned way, with a must-type-in-the-password-at-boot method. Or you could have two computers be the Mandos server for each other. Multiple Mandos servers can coexist on a network without any trouble. They do not clash, and clients will try all available servers. This means that if just one reboots then the other can bring it back up, but if both reboot at the same time they will stay down until someone types in the password on one of them. Faking ping replies? The default for the server is to use fping, the replies to which could be faked to eliminate the timeout. But this could easily be changed to any shell command, with any security measures you like. It could, for instance, be changed to an SSH command with strict keychecking, which could not be faked. Or IPsec could be used for the ping packets, making them secure. SECURITY So, in summary: The only weakness in the Mandos system is from people who have: The power to come in and physically take your servers, and The cunning and patience to do it carefully, one at a time, and quickly, faking Mandos client/server responses for each one before the timeout. While there are some who may be threatened by people who have both these attributes, they do not, probably, constitute the majority. If you do face such opponents, you must figure that they could just as well open your servers and read the file system keys right off the memory by running wires to the memory bus. What Mandos is designed to protect against is not such determined, focused, and competent attacks, but against the early morning knock on your door and the sudden absence of all the servers in your server room. Which it does nicely. PLUGINS In the early designs, the mandos-client8mandos program (which retrieves a password from the Mandos server) also prompted for a password on the terminal, in case a Mandos server could not be found. Other ways of retrieving a password could easily be envisoned, but this multiplicity of purpose was seen to be too complex to be a viable way to continue. Instead, the original program was separated into mandos-client8mandos and password-prompt8mandos, and a plugin-runner8mandos exist to run them both in parallel, allowing the first successful plugin to provide the password. This opened up for any number of additional plugins to run, all competing to be the first to find a password and provide it to the plugin runner. Four additional plugins are provided: plymouth 8mandos This prompts for a password when using plymouth8. usplash 8mandos This prompts for a password when using usplash8. splashy 8mandos This prompts for a password when using splashy8. askpass-fifo 8mandos To provide compatibility with the "askpass" program from cryptsetup, this plugin listens to the same FIFO as askpass would do. More plugins can easily be written and added by the system administrator; see the section called "WRITING PLUGINS" in plugin-runner 8mandos to learn the plugin requirements. SEE ALSO mandos 8, mandos.conf 5, mandos-clients.conf 5, mandos-ctl 8, mandos-monitor 8, plugin-runner 8mandos, mandos-client 8mandos, password-prompt 8mandos, plymouth 8mandos, usplash 8mandos, splashy 8mandos, askpass-fifo 8mandos, mandos-keygen 8 Mandos The Mandos home page.
mandos-1.6.0/mandos.xml0000664000175000017500000005651111767455003015270 0ustar teddyteddy00000000000000 %common; ]> Mandos Manual Mandos &version; &TIMESTAMP; Björn Påhlsson
belorn@recompile.se
Teddy Hogeborn
teddy@recompile.se
2008 2009 2010 2011 2012 Teddy Hogeborn Björn Påhlsson
&COMMANDNAME; 8 &COMMANDNAME; Gives encrypted passwords to authenticated Mandos clients &COMMANDNAME; &COMMANDNAME; &COMMANDNAME; &COMMANDNAME; DESCRIPTION &COMMANDNAME; is a server daemon which handles incoming request for passwords for a pre-defined list of client host computers. For an introduction, see intro 8mandos. The Mandos server uses Zeroconf to announce itself on the local network, and uses TLS to communicate securely with and to authenticate the clients. The Mandos server uses IPv6 to allow Mandos clients to use IPv6 link-local addresses, since the clients will probably not have any other addresses configured (see ). Any authenticated client is then given the stored pre-encrypted password for that specific client. PURPOSE The purpose of this is to enable remote and unattended rebooting of client host computer with an encrypted root file system. See for details. OPTIONS Show a help message and exit NAME NAME Run the server’s self-tests. This includes any unit tests, etc. Set the debugging log level. LEVEL is a string, one of CRITICAL, ERROR, WARNING, INFO, or DEBUG, in order of increasing verbosity. The default level is WARNING. Directory to search for configuration files. Default is /etc/mandos. See mandos.conf 5 and mandos-clients.conf 5. Prints the program version and exit. See also . See also . OVERVIEW This program is the server part. It is a normal server program and will run in a normal system environment, not in an initial RAM disk environment. NETWORK PROTOCOL The Mandos server announces itself as a Zeroconf service of type _mandos._tcp. The Mandos client connects to the announced address and port, and sends a line of text where the first whitespace-separated field is the protocol version, which currently is 1. The client and server then start a TLS protocol handshake with a slight quirk: the Mandos server program acts as a TLS client while the connecting Mandos client acts as a TLS server. The Mandos client must supply an OpenPGP certificate, and the fingerprint of this certificate is used by the Mandos server to look up (in a list read from clients.conf at start time) which binary blob to give the client. No other authentication or authorization is done by the server. Mandos Protocol (Version 1) Mandos Client Direction Mandos Server Connect -> 1\r\n -> TLS handshake as TLS server <-> TLS handshake as TLS client OpenPGP public key (part of TLS handshake) -> <- Binary blob (client will assume OpenPGP data) <- Close
CHECKING The server will, by default, continually check that the clients are still up. If a client has not been confirmed as being up for some time, the client is assumed to be compromised and is no longer eligible to receive the encrypted password. (Manual intervention is required to re-enable a client.) The timeout, extended timeout, checker program, and interval between checks can be configured both globally and per client; see mandos-clients.conf 5. APPROVAL The server can be configured to require manual approval for a client before it is sent its secret. The delay to wait for such approval and the default action (approve or deny) can be configured both globally and per client; see mandos-clients.conf 5. By default all clients will be approved immediately without delay. This can be used to deny a client its secret if not manually approved within a specified time. It can also be used to make the server delay before giving a client its secret, allowing optional manual denying of this specific client. LOGGING The server will send log message with various severity levels to /dev/log. With the option, it will log even more messages, and also show them on the console. PERSISTENT STATE Client settings, initially read from clients.conf, are persistent across restarts, and run-time changes will override settings in clients.conf. However, if a setting is changed (or a client added, or removed) in clients.conf, this will take precedence. D-BUS INTERFACE The server will by default provide a D-Bus system bus interface. This interface will only be accessible by the root user or a Mandos-specific user, if such a user exists. For documentation of the D-Bus API, see the file DBUS-API. EXIT STATUS The server will exit with a non-zero exit status only when a critical error is encountered. ENVIRONMENT PATH To start the configured checker (see ), the server uses /bin/sh, which in turn uses PATH to search for matching commands if an absolute path is not given. See sh1 . FILES Use the option to change where &COMMANDNAME; looks for its configurations files. The default file names are listed here. /etc/mandos/mandos.conf Server-global settings. See mandos.conf 5 for details. /etc/mandos/clients.conf List of clients and client-specific settings. See mandos-clients.conf 5 for details. /var/run/mandos.pid The file containing the process id of the &COMMANDNAME; process started last. /dev/log /var/lib/mandos Directory where persistent state will be saved. Change this with the option. See also the option. /dev/log The Unix domain socket to where local syslog messages are sent. /bin/sh This is used to start the configured checker command for each client. See mandos-clients.conf 5 for details. BUGS This server might, on especially fatal errors, emit a Python backtrace. This could be considered a feature. There is no fine-grained control over logging and debug output. This server does not check the expire time of clients’ OpenPGP keys. EXAMPLE Normal invocation needs no options: &COMMANDNAME; Run the server in debug mode, read configuration files from the ~/mandos directory, and use the Zeroconf service name Test to not collide with any other official Mandos server on this host: &COMMANDNAME; --debug --configdir ~/mandos --servicename Test Run the server normally, but only listen to one interface and only on the link-local address on that interface: &COMMANDNAME; --interface eth7 --address fe80::aede:48ff:fe71:f6f2 SECURITY SERVER Running this &COMMANDNAME; server program should not in itself present any security risk to the host computer running it. The program switches to a non-root user soon after startup. CLIENTS The server only gives out its stored data to clients which does have the OpenPGP key of the stored fingerprint. This is guaranteed by the fact that the client sends its OpenPGP public key in the TLS handshake; this ensures it to be genuine. The server computes the fingerprint of the key itself and looks up the fingerprint in its list of clients. The clients.conf file (see mandos-clients.conf 5) must be made non-readable by anyone except the user starting the server (usually root). As detailed in , the status of all client computers will continually be checked and be assumed compromised if they are gone for too long. For more details on client-side security, see mandos-client 8mandos. SEE ALSO intro 8mandos, mandos-clients.conf 5, mandos.conf 5, mandos-client 8mandos, sh 1 Zeroconf Zeroconf is the network protocol standard used by clients for finding this Mandos server on the local network. Avahi Avahi is the library this server calls to implement Zeroconf service announcements. GnuTLS GnuTLS is the library this server uses to implement TLS for communicating securely with the client, and at the same time confidently get the client’s public OpenPGP key. RFC 4291: IP Version 6 Addressing Architecture Section 2.2: Text Representation of Addresses Section 2.5.5.2: IPv4-Mapped IPv6 Address Section 2.5.6, Link-Local IPv6 Unicast Addresses The clients use IPv6 link-local addresses, which are immediately usable since a link-local addresses is automatically assigned to a network interfaces when it is brought up. RFC 4346: The Transport Layer Security (TLS) Protocol Version 1.1 TLS 1.1 is the protocol implemented by GnuTLS. RFC 4880: OpenPGP Message Format The data sent to clients is binary encrypted OpenPGP data. RFC 5081: Using OpenPGP Keys for Transport Layer Security This is implemented by GnuTLS and used by this server so that OpenPGP keys can be used.
mandos-1.6.0/Makefile0000664000175000017500000003645211767455003014727 0ustar teddyteddy00000000000000WARN=-O -Wall -Wformat=2 -Winit-self -Wmissing-include-dirs \ -Wswitch-default -Wswitch-enum -Wunused-parameter \ -Wstrict-aliasing=1 -Wextra -Wfloat-equal -Wundef -Wshadow \ -Wunsafe-loop-optimizations -Wpointer-arith \ -Wbad-function-cast -Wcast-qual -Wcast-align -Wwrite-strings \ -Wconversion -Wstrict-prototypes -Wold-style-definition \ -Wpacked -Wnested-externs -Winline -Wvolatile-register-var # -Wunreachable-code #DEBUG=-ggdb3 # For info about _FORTIFY_SOURCE, see # # and . FORTIFY=-D_FORTIFY_SOURCE=2 -fstack-protector-all -fPIC LINK_FORTIFY_LD=-z relro -z now LINK_FORTIFY= # If BROKEN_PIE is set, do not build with -pie ifndef BROKEN_PIE FORTIFY += -fPIE LINK_FORTIFY += -pie endif #COVERAGE=--coverage OPTIMIZE=-Os LANGUAGE=-std=gnu99 htmldir=man version=1.6.0 SED=sed USER=$(firstword $(subst :, ,$(shell getent passwd _mandos || getent passwd nobody || echo 65534))) GROUP=$(firstword $(subst :, ,$(shell getent group _mandos || getent group nobody || echo 65534))) ## Use these settings for a traditional /usr/local install # PREFIX=$(DESTDIR)/usr/local # CONFDIR=$(DESTDIR)/etc/mandos # KEYDIR=$(DESTDIR)/etc/mandos/keys # MANDIR=$(PREFIX)/man # INITRAMFSTOOLS=$(DESTDIR)/etc/initramfs-tools # STATEDIR=$(DESTDIR)/var/lib/mandos ## ## These settings are for a package-type install PREFIX=$(DESTDIR)/usr CONFDIR=$(DESTDIR)/etc/mandos KEYDIR=$(DESTDIR)/etc/keys/mandos MANDIR=$(PREFIX)/share/man INITRAMFSTOOLS=$(DESTDIR)/usr/share/initramfs-tools STATEDIR=$(DESTDIR)/var/lib/mandos ## GNUTLS_CFLAGS=$(shell pkg-config --cflags-only-I gnutls) GNUTLS_LIBS=$(shell pkg-config --libs gnutls) AVAHI_CFLAGS=$(shell pkg-config --cflags-only-I avahi-core) AVAHI_LIBS=$(shell pkg-config --libs avahi-core) GPGME_CFLAGS=$(shell gpgme-config --cflags; getconf LFS_CFLAGS) GPGME_LIBS=$(shell gpgme-config --libs; getconf LFS_LIBS; \ getconf LFS_LDFLAGS) # Do not change these two CFLAGS=$(WARN) $(DEBUG) $(FORTIFY) $(COVERAGE) $(OPTIMIZE) \ $(LANGUAGE) $(GNUTLS_CFLAGS) $(AVAHI_CFLAGS) $(GPGME_CFLAGS) \ -DVERSION='"$(version)"' LDFLAGS=-Xlinker --as-needed $(COVERAGE) $(LINK_FORTIFY) $(foreach flag,$(LINK_FORTIFY_LD),-Xlinker $(flag)) # Commands to format a DocBook document into a manual page DOCBOOKTOMAN=$(strip cd $(dir $<); xsltproc --nonet --xinclude \ --param man.charmap.use.subset 0 \ --param make.year.ranges 1 \ --param make.single.year.ranges 1 \ --param man.output.quietly 1 \ --param man.authors.section.enabled 0 \ /usr/share/xml/docbook/stylesheet/nwalsh/manpages/docbook.xsl \ $(notdir $<); \ $(MANPOST) $(notdir $@);\ if locale --all 2>/dev/null | grep --regexp='^en_US\.utf8$$' \ && type man 2>/dev/null; then LANG=en_US.UTF-8 MANWIDTH=80 \ man --warnings --encoding=UTF-8 --local-file $(notdir $@); \ fi >/dev/null) # DocBook-to-man post-processing to fix a '\n' escape bug MANPOST=$(SED) --in-place --expression='s,\\\\en,\\en,g;s,\\n,\\en,g' DOCBOOKTOHTML=$(strip xsltproc --nonet --xinclude \ --param make.year.ranges 1 \ --param make.single.year.ranges 1 \ --param man.output.quietly 1 \ --param man.authors.section.enabled 0 \ --param citerefentry.link 1 \ --output $@ \ /usr/share/xml/docbook/stylesheet/nwalsh/xhtml/docbook.xsl \ $<; $(HTMLPOST) $@) # Fix citerefentry links HTMLPOST=$(SED) --in-place \ --expression='s/\(\)\([^<]*\)\(<\/span>(\)\([^)]*\)\()<\/span><\/a>\)/\1\3.\5\2\3\4\5\6/g' PLUGINS=plugins.d/password-prompt plugins.d/mandos-client \ plugins.d/usplash plugins.d/splashy plugins.d/askpass-fifo \ plugins.d/plymouth CPROGS=plugin-runner $(PLUGINS) PROGS=mandos mandos-keygen mandos-ctl mandos-monitor $(CPROGS) DOCS=mandos.8 mandos-keygen.8 mandos-monitor.8 mandos-ctl.8 \ mandos.conf.5 mandos-clients.conf.5 plugin-runner.8mandos \ plugins.d/mandos-client.8mandos \ plugins.d/password-prompt.8mandos plugins.d/usplash.8mandos \ plugins.d/splashy.8mandos plugins.d/askpass-fifo.8mandos \ plugins.d/plymouth.8mandos intro.8mandos htmldocs=$(addsuffix .xhtml,$(DOCS)) objects=$(addsuffix .o,$(CPROGS)) all: $(PROGS) mandos.lsm doc: $(DOCS) html: $(htmldocs) %.5: %.xml common.ent legalnotice.xml $(DOCBOOKTOMAN) %.5.xhtml: %.xml common.ent legalnotice.xml $(DOCBOOKTOHTML) %.8: %.xml common.ent legalnotice.xml $(DOCBOOKTOMAN) %.8.xhtml: %.xml common.ent legalnotice.xml $(DOCBOOKTOHTML) %.8mandos: %.xml common.ent legalnotice.xml $(DOCBOOKTOMAN) %.8mandos.xhtml: %.xml common.ent legalnotice.xml $(DOCBOOKTOHTML) intro.8mandos: intro.xml common.ent legalnotice.xml $(DOCBOOKTOMAN) intro.8mandos.xhtml: intro.xml common.ent legalnotice.xml $(DOCBOOKTOHTML) mandos.8: mandos.xml common.ent mandos-options.xml overview.xml \ legalnotice.xml $(DOCBOOKTOMAN) mandos.8.xhtml: mandos.xml common.ent mandos-options.xml \ overview.xml legalnotice.xml $(DOCBOOKTOHTML) mandos-keygen.8: mandos-keygen.xml common.ent overview.xml \ legalnotice.xml $(DOCBOOKTOMAN) mandos-keygen.8.xhtml: mandos-keygen.xml common.ent overview.xml \ legalnotice.xml $(DOCBOOKTOHTML) mandos-monitor.8: mandos-monitor.xml common.ent overview.xml \ legalnotice.xml $(DOCBOOKTOMAN) mandos-monitor.8.xhtml: mandos-monitor.xml common.ent overview.xml \ legalnotice.xml $(DOCBOOKTOHTML) mandos-ctl.8: mandos-ctl.xml common.ent overview.xml \ legalnotice.xml $(DOCBOOKTOMAN) mandos-ctl.8.xhtml: mandos-ctl.xml common.ent overview.xml \ legalnotice.xml $(DOCBOOKTOHTML) mandos.conf.5: mandos.conf.xml common.ent mandos-options.xml \ legalnotice.xml $(DOCBOOKTOMAN) mandos.conf.5.xhtml: mandos.conf.xml common.ent mandos-options.xml \ legalnotice.xml $(DOCBOOKTOHTML) plugin-runner.8mandos: plugin-runner.xml common.ent overview.xml \ legalnotice.xml $(DOCBOOKTOMAN) plugin-runner.8mandos.xhtml: plugin-runner.xml common.ent \ overview.xml legalnotice.xml $(DOCBOOKTOHTML) plugins.d/mandos-client.8mandos: plugins.d/mandos-client.xml \ common.ent \ mandos-options.xml \ overview.xml legalnotice.xml $(DOCBOOKTOMAN) plugins.d/mandos-client.8mandos.xhtml: plugins.d/mandos-client.xml \ common.ent \ mandos-options.xml \ overview.xml legalnotice.xml $(DOCBOOKTOHTML) # Update all these files with version number $(version) common.ent: Makefile $(strip $(SED) --in-place \ --expression='s/^\($$/\1$(version)">/' \ $@) mandos: Makefile $(strip $(SED) --in-place \ --expression='s/^\(version = "\)[^"]*"$$/\1$(version)"/' \ $@) mandos-keygen: Makefile $(strip $(SED) --in-place \ --expression='s/^\(VERSION="\)[^"]*"$$/\1$(version)"/' \ $@) mandos-ctl: Makefile $(strip $(SED) --in-place \ --expression='s/^\(version = "\)[^"]*"$$/\1$(version)"/' \ $@) mandos-monitor: Makefile $(strip $(SED) --in-place \ --expression='s/^\(version = "\)[^"]*"$$/\1$(version)"/' \ $@) mandos.lsm: Makefile $(strip $(SED) --in-place \ --expression='s/^\(Version:\).*/\1\t$(version)/' \ $@) $(strip $(SED) --in-place \ --expression='s/^\(Entered-date:\).*/\1\t$(shell date --rfc-3339=date --reference=Makefile)/' \ $@) $(strip $(SED) --in-place \ --expression='s/\(mandos_\)[0-9.]\+\(\.orig\.tar\.gz\)/\1$(version)\2/' \ $@) plugins.d/mandos-client: plugins.d/mandos-client.c $(LINK.c) $^ -lrt $(GNUTLS_LIBS) $(AVAHI_LIBS) $(strip\ ) $(GPGME_LIBS) $(LOADLIBES) $(LDLIBS) -o $@ .PHONY : all doc html clean distclean run-client run-server install \ install-server install-client uninstall uninstall-server \ uninstall-client purge purge-server purge-client clean: -rm --force $(CPROGS) $(objects) $(htmldocs) $(DOCS) core distclean: clean mostlyclean: clean maintainer-clean: clean -rm --force --recursive keydir confdir statedir check: all ./mandos --check # Run the client with a local config and key run-client: all keydir/seckey.txt keydir/pubkey.txt @echo "###################################################################" @echo "# The following error messages are harmless and can be safely #" @echo "# ignored. The messages are caused by not running as root, but #" @echo "# you should NOT run \"make run-client\" as root unless you also #" @echo "# unpacked and compiled Mandos as root, which is NOT recommended. #" @echo "# From plugin-runner: setuid: Operation not permitted #" @echo "# From askpass-fifo: mkfifo: Permission denied #" @echo "# From mandos-client: setuid: Operation not permitted #" @echo "# seteuid: Operation not permitted #" @echo "# klogctl: Operation not permitted #" @echo "###################################################################" ./plugin-runner --plugin-dir=plugins.d \ --config-file=plugin-runner.conf \ --options-for=mandos-client:--seckey=keydir/seckey.txt,--pubkey=keydir/pubkey.txt,--network-hook-dir=network-hooks.d \ $(CLIENTARGS) # Used by run-client keydir/seckey.txt keydir/pubkey.txt: mandos-keygen install --directory keydir ./mandos-keygen --dir keydir --force # Run the server with a local config run-server: confdir/mandos.conf confdir/clients.conf statedir ./mandos --debug --no-dbus --configdir=confdir \ --statedir=statedir $(SERVERARGS) # Used by run-server confdir/mandos.conf: mandos.conf install --directory confdir install --mode=u=rw,go=r $^ $@ confdir/clients.conf: clients.conf keydir/seckey.txt install --directory confdir install --mode=u=rw $< $@ # Add a client password ./mandos-keygen --dir keydir --password >> $@ statedir: install --directory statedir install: install-server install-client-nokey install-html: html install --directory $(htmldir) install --mode=u=rw,go=r --target-directory=$(htmldir) \ $(htmldocs) install-server: doc install --directory $(CONFDIR) install --directory --mode=u=rwx --owner=$(USER) \ --group=$(GROUP) $(STATEDIR) install --mode=u=rwx,go=rx mandos $(PREFIX)/sbin/mandos install --mode=u=rwx,go=rx --target-directory=$(PREFIX)/sbin \ mandos-ctl install --mode=u=rwx,go=rx --target-directory=$(PREFIX)/sbin \ mandos-monitor install --mode=u=rw,go=r --target-directory=$(CONFDIR) \ mandos.conf install --mode=u=rw --target-directory=$(CONFDIR) \ clients.conf install --mode=u=rw,go=r dbus-mandos.conf \ $(DESTDIR)/etc/dbus-1/system.d/mandos.conf install --mode=u=rwx,go=rx init.d-mandos \ $(DESTDIR)/etc/init.d/mandos install --mode=u=rw,go=r default-mandos \ $(DESTDIR)/etc/default/mandos if [ -z $(DESTDIR) ]; then \ update-rc.d mandos defaults 25 15;\ fi gzip --best --to-stdout mandos.8 \ > $(MANDIR)/man8/mandos.8.gz gzip --best --to-stdout mandos-monitor.8 \ > $(MANDIR)/man8/mandos-monitor.8.gz gzip --best --to-stdout mandos-ctl.8 \ > $(MANDIR)/man8/mandos-ctl.8.gz gzip --best --to-stdout mandos.conf.5 \ > $(MANDIR)/man5/mandos.conf.5.gz gzip --best --to-stdout mandos-clients.conf.5 \ > $(MANDIR)/man5/mandos-clients.conf.5.gz gzip --best --to-stdout intro.8mandos \ > $(MANDIR)/man8/intro.8mandos.gz install-client-nokey: all doc install --directory $(PREFIX)/lib/mandos $(CONFDIR) install --directory --mode=u=rwx $(KEYDIR) \ $(PREFIX)/lib/mandos/plugins.d if [ "$(CONFDIR)" != "$(PREFIX)/lib/mandos" ]; then \ install --mode=u=rwx \ --directory "$(CONFDIR)/plugins.d"; \ fi install --mode=u=rwx,go=rx --directory \ "$(CONFDIR)/network-hooks.d" install --mode=u=rwx,go=rx \ --target-directory=$(PREFIX)/lib/mandos plugin-runner install --mode=u=rwx,go=rx --target-directory=$(PREFIX)/sbin \ mandos-keygen install --mode=u=rwx,go=rx \ --target-directory=$(PREFIX)/lib/mandos/plugins.d \ plugins.d/password-prompt install --mode=u=rwxs,go=rx \ --target-directory=$(PREFIX)/lib/mandos/plugins.d \ plugins.d/mandos-client install --mode=u=rwxs,go=rx \ --target-directory=$(PREFIX)/lib/mandos/plugins.d \ plugins.d/usplash install --mode=u=rwxs,go=rx \ --target-directory=$(PREFIX)/lib/mandos/plugins.d \ plugins.d/splashy install --mode=u=rwxs,go=rx \ --target-directory=$(PREFIX)/lib/mandos/plugins.d \ plugins.d/askpass-fifo install --mode=u=rwxs,go=rx \ --target-directory=$(PREFIX)/lib/mandos/plugins.d \ plugins.d/plymouth install initramfs-tools-hook \ $(INITRAMFSTOOLS)/hooks/mandos install --mode=u=rw,go=r initramfs-tools-hook-conf \ $(INITRAMFSTOOLS)/conf-hooks.d/mandos install initramfs-tools-script \ $(INITRAMFSTOOLS)/scripts/init-premount/mandos install --mode=u=rw,go=r plugin-runner.conf $(CONFDIR) gzip --best --to-stdout mandos-keygen.8 \ > $(MANDIR)/man8/mandos-keygen.8.gz gzip --best --to-stdout plugin-runner.8mandos \ > $(MANDIR)/man8/plugin-runner.8mandos.gz gzip --best --to-stdout plugins.d/mandos-client.8mandos \ > $(MANDIR)/man8/mandos-client.8mandos.gz gzip --best --to-stdout plugins.d/password-prompt.8mandos \ > $(MANDIR)/man8/password-prompt.8mandos.gz gzip --best --to-stdout plugins.d/usplash.8mandos \ > $(MANDIR)/man8/usplash.8mandos.gz gzip --best --to-stdout plugins.d/splashy.8mandos \ > $(MANDIR)/man8/splashy.8mandos.gz gzip --best --to-stdout plugins.d/askpass-fifo.8mandos \ > $(MANDIR)/man8/askpass-fifo.8mandos.gz gzip --best --to-stdout plugins.d/plymouth.8mandos \ > $(MANDIR)/man8/plymouth.8mandos.gz install-client: install-client-nokey # Post-installation stuff -$(PREFIX)/sbin/mandos-keygen --dir "$(KEYDIR)" update-initramfs -k all -u echo "Now run mandos-keygen --password --dir $(KEYDIR)" uninstall: uninstall-server uninstall-client uninstall-server: -rm --force $(PREFIX)/sbin/mandos \ $(PREFIX)/sbin/mandos-ctl \ $(PREFIX)/sbin/mandos-monitor \ $(MANDIR)/man8/mandos.8.gz \ $(MANDIR)/man8/mandos-monitor.8.gz \ $(MANDIR)/man8/mandos-ctl.8.gz \ $(MANDIR)/man5/mandos.conf.5.gz \ $(MANDIR)/man5/mandos-clients.conf.5.gz update-rc.d -f mandos remove -rmdir $(CONFDIR) uninstall-client: # Refuse to uninstall client if /etc/crypttab is explicitly configured # to use it. ! grep --regexp='^ *[^ #].*keyscript=[^,=]*/mandos/' \ $(DESTDIR)/etc/crypttab -rm --force $(PREFIX)/sbin/mandos-keygen \ $(PREFIX)/lib/mandos/plugin-runner \ $(PREFIX)/lib/mandos/plugins.d/password-prompt \ $(PREFIX)/lib/mandos/plugins.d/mandos-client \ $(PREFIX)/lib/mandos/plugins.d/usplash \ $(PREFIX)/lib/mandos/plugins.d/splashy \ $(PREFIX)/lib/mandos/plugins.d/askpass-fifo \ $(PREFIX)/lib/mandos/plugins.d/plymouth \ $(INITRAMFSTOOLS)/hooks/mandos \ $(INITRAMFSTOOLS)/conf-hooks.d/mandos \ $(INITRAMFSTOOLS)/scripts/init-premount/mandos \ $(MANDIR)/man8/mandos-keygen.8.gz \ $(MANDIR)/man8/plugin-runner.8mandos.gz \ $(MANDIR)/man8/mandos-client.8mandos.gz $(MANDIR)/man8/password-prompt.8mandos.gz \ $(MANDIR)/man8/usplash.8mandos.gz \ $(MANDIR)/man8/splashy.8mandos.gz \ $(MANDIR)/man8/askpass-fifo.8mandos.gz \ $(MANDIR)/man8/plymouth.8mandos.gz \ -rmdir $(PREFIX)/lib/mandos/plugins.d $(CONFDIR)/plugins.d \ $(PREFIX)/lib/mandos $(CONFDIR) $(KEYDIR) update-initramfs -k all -u purge: purge-server purge-client purge-server: uninstall-server -rm --force $(CONFDIR)/mandos.conf $(CONFDIR)/clients.conf \ $(DESTDIR)/etc/dbus-1/system.d/mandos.conf $(DESTDIR)/etc/default/mandos \ $(DESTDIR)/etc/init.d/mandos \ $(DESTDIR)/var/run/mandos.pid -rmdir $(CONFDIR) purge-client: uninstall-client -shred --remove $(KEYDIR)/seckey.txt -rm --force $(CONFDIR)/plugin-runner.conf \ $(KEYDIR)/pubkey.txt $(KEYDIR)/seckey.txt -rmdir $(KEYDIR) $(CONFDIR)/plugins.d $(CONFDIR) mandos-1.6.0/INSTALL0000664000175000017500000001207711767455003014315 0ustar teddyteddy00000000000000-*- org -*- * Prerequisites ** Operating System Debian 6.0 "squeeze" or Ubuntu 10.10 "Maverick Meerkat". This is mostly for the support scripts which make sure that the client is installed and started in the initial RAM disk environment and that the initrd.img file is automatically made unreadable. The server and client programs themselves *could* be run in other distributions, but they *are* specific to GNU/Linux systems, and are not written with portabillity to other Unixes in mind. ** Libraries The following libraries and packages are needed. (It is possible that it might work with older versions of some of these, but these versions are confirmed to work. Newer versions are almost certainly OK.) *** Documentation These are required to build the manual pages for both the server and client: + DocBook 4.5 http://www.docbook.org/ Note: DocBook 5.0 is not compatible. + DocBook XSL stylesheets 1.71.0 http://wiki.docbook.org/topic/DocBookXslStylesheets Package names: docbook docbook-xsl To build just the documentation, run the command "make doc". Then the manual page "mandos.8", for example, can be read by running "man -l mandos.8". *** Mandos Server + GnuTLS 2.4 http://www.gnu.org/software/gnutls/ + Avahi 0.6.16 http://www.avahi.org/ + Python 2.6 http://www.python.org/ + Python-GnuTLS 1.1.5 http://pypi.python.org/pypi/python-gnutls/ + dbus-python 0.82.4 http://dbus.freedesktop.org/doc/dbus-python/ + PyGObject 2.14.2 http://library.gnome.org/devel/pygobject/ + Urwid 0.9.8.3 http://excess.org/urwid/ Strongly recommended: + fping 2.4b2-to-ipv6 http://www.fping.com/ Package names: python-gnutls avahi-daemon python python-avahi python-dbus python-gobject python-urwid *** Mandos Client + initramfs-tools 0.85i http://packages.qa.debian.org/i/initramfs-tools.html + GnuTLS 2.4 http://www.gnu.org/software/gnutls/ + Avahi 0.6.16 http://www.avahi.org/ + GnuPG 1.4.9 http://www.gnupg.org/ + GPGME 1.1.6 http://www.gnupg.org/related_software/gpgme/ Package names: initramfs-tools libgnutls-dev libavahi-core-dev gnupg libgpgme11-dev * Installing the Mandos server 1. Do "make doc". 2. On the computer to run as a Mandos server, run the following command: For Debian: su -c 'make install-server' For Ubuntu: sudo make install-server (This creates a configuration without any clients configured; you need an actually configured client to do that; see below.) * Installing the Mandos client. 1. Do "make all doc". 2. On the computer to run as a Mandos client, run the following command: For Debian: su -c 'make install-client' For Ubuntu: sudo make install-client This will also create an OpenPGP key, which will take some time and entropy, so be patient. 3. Run the following command: For Debian: su -c 'mandos-keygen --password' For Ubuntu: sudo mandos-keygen --password When prompted, enter the password/passphrase for the encrypted root file system on this client computer. The command will output a section of text, starting with a [section header]. Copy and append this to the file "/etc/mandos/clients.conf" *on the server computer*. 4. Configure the client to use the correct network interface. The interface to use is automatically chosen at boot, and if this needs to be adjusted, it will be necessary to edit /etc/initramfs-tools/initramfs.conf to change the DEVICE setting there. Alternatively, the file /etc/mandos/plugin-runner.conf can be edited to add a "--device" parameter for the mandos-client(8) plugin. Please note: If any of those files are changed, the initrd.img file must be updated, possibly using the following command: # update-initramfs -k all -u 5. On the server computer, start the server by running the command For Debian: su -c 'invoke-rc.d mandos start' For Ubuntu: sudo service mandos start At this point, it is possible to verify that the correct password will be received by the client by running the command: # /usr/lib/mandos/plugins.d/mandos-client \ --pubkey=/etc/keys/mandos/pubkey.txt \ --seckey=/etc/keys/mandos/seckey.txt; echo This command should retrieve the password from the server, decrypt it, and output it to standard output. After this, the client computer should be able to reboot without needing a password entered on the console, as long as it does not take more than an hour to reboot. * Further customizations You may want to tighten or loosen the timeouts in the server configuration files; see mandos.conf(5) and mandos-clients.conf(5). If IPsec is not used, it is suggested that a more cryptographically secure checker program is used and configured, since without IPsec ping packets can be faked. mandos-1.6.0/initramfs-tools-hook0000775000175000017500000001341311767455003017275 0ustar teddyteddy00000000000000#!/bin/sh # This script will be run by 'mkinitramfs' when it creates the image. # Its job is to decide which files to install, then install them into # the staging area, where the initramfs is being created. This # happens when a new 'linux-image' package is installed, or when the # administrator runs 'update-initramfs' by hand to update an initramfs # image. # The environment contains at least: # # DESTDIR -- The staging directory where the image is being built. # No initramfs pre-requirements PREREQ="cryptroot" prereqs() { echo "$PREREQ" } case $1 in # get pre-requisites prereqs) prereqs exit 0 ;; esac . /usr/share/initramfs-tools/hook-functions for d in /usr /usr/local; do if [ -d "$d"/lib/mandos ]; then prefix="$d" break fi done if [ -z "$prefix" ]; then # Mandos not found exit 1 fi for d in /etc/keys/mandos /etc/mandos/keys; do if [ -d "$d" ]; then keydir="$d" break fi done if [ -z "$keydir" ]; then # Mandos key directory not found exit 1 fi set `{ getent passwd _mandos \ || getent passwd nobody \ || echo ::65534:65534:::; } \ | cut --delimiter=: --fields=3,4 --only-delimited \ --output-delimiter=" "` mandos_user="$1" mandos_group="$2" # The Mandos network client uses the network auto_add_modules net # The Mandos network client uses IPv6 force_load ipv6 # These are directories inside the initrd CONFDIR="/conf/conf.d/mandos" MANDOSDIR="/lib/mandos" PLUGINDIR="${MANDOSDIR}/plugins.d" HOOKDIR="${MANDOSDIR}/network-hooks.d" # Make directories install --directory --mode=u=rwx,go=rx "${DESTDIR}${CONFDIR}" \ "${DESTDIR}${MANDOSDIR}" "${DESTDIR}${HOOKDIR}" install --owner=${mandos_user} --group=${mandos_group} --directory \ --mode=u=rwx "${DESTDIR}${PLUGINDIR}" # Copy the Mandos plugin runner copy_exec "$prefix"/lib/mandos/plugin-runner "${MANDOSDIR}" # Copy the plugins # Copy the packaged plugins for file in "$prefix"/lib/mandos/plugins.d/*; do base="`basename \"$file\"`" # Is this plugin overridden? if [ -e "/etc/mandos/plugins.d/$base" ]; then continue fi case "$base" in *~|.*|\#*\#|*.dpkg-old|*.dpkg-bak|*.dpkg-new|*.dpkg-divert) : ;; "*") echo "W: Mandos client plugin directory is empty." >&2 ;; *) copy_exec "$file" "${PLUGINDIR}" ;; esac done # Copy any user-supplied plugins for file in /etc/mandos/plugins.d/*; do base="`basename \"$file\"`" case "$base" in *~|.*|\#*\#|*.dpkg-old|*.dpkg-bak|*.dpkg-new|*.dpkg-divert) : ;; "*") : ;; *) copy_exec "$file" "${PLUGINDIR}" ;; esac done # Get DEVICE from initramfs.conf and other files . /etc/initramfs-tools/initramfs.conf for conf in /etc/initramfs-tools/conf.d/*; do if [ -n `basename \"$conf\" | grep '^[[:alnum:]][[:alnum:]\._-]*$' \ | grep -v '\.dpkg-.*$'` ]; then [ -f ${conf} ] && . ${conf} fi done export DEVICE # Copy network hooks for hook in /etc/mandos/network-hooks.d/*; do case "`basename \"$hook\"`" in "*") continue ;; *[!A-Za-z0-9_.-]*) continue ;; *) test -d "$hook" || copy_exec "$hook" "${HOOKDIR}" ;; esac if [ -x "$hook" ]; then # Copy any files needed by the network hook MANDOSNETHOOKDIR=/etc/mandos/network-hooks.d MODE=files \ VERBOSITY=0 "$hook" files | while read file target; do if [ ! -e "${file}" ]; then echo "WARNING: file ${file} not found, requested by Mandos network hook '${hook##*/}'" >&2 fi if [ -z "${target}" ]; then copy_exec "$file" else copy_exec "$file" "$target" fi done # Copy and load any modules needed by the network hook MANDOSNETHOOKDIR=/etc/mandos/network-hooks.d MODE=modules \ VERBOSITY=0 "$hook" modules | while read module; do if [ -z "${target}" ]; then force_load "$module" fi done fi done # GPGME needs /usr/bin/gpg if [ ! -e "${DESTDIR}/usr/bin/gpg" \ -a -n "`ls \"${DESTDIR}\"/usr/lib/libgpgme.so* \ 2>/dev/null`" ]; then copy_exec /usr/bin/gpg fi # Config files for file in /etc/mandos/plugin-runner.conf; do if [ -d "$file" ]; then continue fi cp --archive --sparse=always "$file" "${DESTDIR}${CONFDIR}" done if [ ${mandos_user} != 65534 ]; then sed --in-place --expression="1i--userid=${mandos_user}" \ "${DESTDIR}${CONFDIR}/plugin-runner.conf" fi if [ ${mandos_group} != 65534 ]; then sed --in-place --expression="1i--groupid=${mandos_group}" \ "${DESTDIR}${CONFDIR}/plugin-runner.conf" fi # Key files for file in "$keydir"/*; do if [ -d "$file" ]; then continue fi cp --archive --sparse=always "$file" "${DESTDIR}${CONFDIR}" chown ${mandos_user}:${mandos_group} \ "${DESTDIR}${CONFDIR}/`basename \"$file\"`" done # /lib/mandos/plugin-runner will drop priviliges, but needs access to # its plugin directory and its config file. However, since almost all # files in initrd have been created with umask 027, this opening of # permissions is needed. # # (The umask is not really intended to affect the files inside the # initrd; it is intended to affect the initrd.img file itself, since # it now contains secret key files. There is, however, no other way # to set the permission of the initrd.img file without a race # condition. This umask is set by "initramfs-tools-hook-conf", # installed as "/usr/share/initramfs-tools/conf-hooks.d/mandos".) # for full in "${MANDOSDIR}" "${CONFDIR}"; do while [ "$full" != "/" ]; do chmod a+rX "${DESTDIR}$full" full="`dirname \"$full\"`" done done # Reset some other things to sane permissions which we have # inadvertently affected with our umask setting. for dir in / /bin /etc /keyscripts /sbin /scripts /usr /usr/bin; do if [ -d "${DESTDIR}$dir" ]; then chmod a+rX "${DESTDIR}$dir" fi done for dir in "${DESTDIR}"/lib* "${DESTDIR}"/usr/lib*; do if [ -d "$dir" ]; then find "$dir" \! -perm -u+rw,g+r -prune -or -print0 \ | xargs --null --no-run-if-empty chmod a+rX fi done mandos-1.6.0/common.ent0000664000175000017500000000013511767455003015254 0ustar teddyteddy00000000000000 mandos-1.6.0/NEWS0000664000175000017500000002330511767455003013757 0ustar teddyteddy00000000000000This NEWS file records noteworthy changes, very tersely. See the manual for detailed information. Version 1.6.0 (2012-06-18) * Server ** Takes new --foreground option ** Init script supports new "status" action. * Client ** Now uses all interfaces by default; the --interface option can still be used to restrict it, and the argument to --interface (as well as the $DEVICE environment variable for the network hooks) is now a comma-separated list of interfaces to use. Version 1.5.5 (2012-06-01) * Server ** Server takes new --socket option Version 1.5.4 (2012-05-20) * Server ** Bug fix: Regression fix: Make non-zero approval timeout values work. ** Bug fix: Regression fix: Allow changing the Timeout D-Bus property. ** Fall back to not bind to an interface if an invalid interface name is given. ** Removed support for undocumented feature of using plain "%%s" in "checker" client option. ** Old D-Bus interface are now marked as deprecated. ** mandos-monitor: Bug fix: show approval timers correctly. ** mandos-ctl: Show "Extended Timeout" correctly, not as milliseconds. Version 1.5.3 (2012-01-15) * Server ** Add D-Bus property se.recompile.Client.LastCheckerStatus and use it in mandos-monitor. * Client ** Fix bugs in the example "bridge" network hook. Version 1.5.2 (2012-01-08) * Server ** Removed D-Bus signal se.recompile.Mandos.NewRequest() added in 1.5.0. It was buggy and was of questionable utility. Version 1.5.1 (2012-01-01) * Server ** Include intro(8mandos) manual page, missing since migration from README file in version 1.4.0. Version 1.5.0 (2012-01-01) * Client ** Network hooks. The Mandos client can now run custom scripts to take up a network interface before the client is run. Three example scripts are provided: "wireless", "openvpn", and "bridge". To facilitate this, the client now prefers network interfaces which are up (if any) over all other interfaces. * Server ** Persistent state. Client state is now saved between server restarts. ** clients.conf file can now contain "enabled" setting for clients. ** Bug fix: Fix rare crash bug. ** Bug fix: Send corrent D-Bus type in PropertyChanged for "ApprovalDelay", "ApprovalDuration", "Timeout", and "ExtendedTimeout". ** mandos-ctl: Bare numbers as arguments are taken to be milliseconds. ** Bug fix: mandos-ctl --secret option now works. ** New D-Bus signal: se.recompile.Mandos.NewRequest(s). Version 1.4.1 (2011-10-15) * Server ** Make D-Bus properties settable again, and handle checkers for disabled clients correctly. * Miscellaneous fixes to "pedantic" Lintian warnings Version 1.4.0 (2011-10-09) * README file migrated to manual page intro(8mandos). * Client: ** Fixed warning about "rmdir: Directory not empty". * Server: ** Default values changed: timeout 5 minutes, interval 2 minutes. ** Clients gets an expiration extension when receiving a password, controlled by new "extended_timeout" setting. ** New domain name: "fukt.bsnet.se" changes to "recompile.se". This also affects the D-Bus bus and interface names (old names still work). Users should start using the new names immediately. ** New D-Bus Client object properties "Expires" and "ExtendedTimeout"; see DBUS-API for details. Version 1.3.1 (2011-07-27) * Client: ** Client now retries all Mandos servers periodically. ** Work around Debian bug #633582 - fixes "Permission denied" problem. Version 1.3.0 (2011-03-08) * Server: ** Updated for Python 2.6. * Client: ** Bug fix: Make the password-prompt plugin not conflict with Plymouth. ** Bug fix: Bug fix: update initramfs also when purging package. Version 1.2.3 (2010-10-11) * Server: ** Bug fix: Expose D-Bus API also in non-debug mode. Version 1.2.2 (2010-10-07) * Client: ** splashy: Minor fix to compile with non-Linux kernels. Version 1.2.1 (2010-10-02) * Server: ** mandos-monitor(8): Documentation bug fix: Key for removing client is "R", not "r". Version 1.2 (2010-09-28) * Client: ** New "plymouth" plugin to ask for a password using the Plymouth graphical boot system. ** The Mandos client now automatically chooses a network interface if the DEVICE setting in /etc/initramfs-tools/initramfs.conf is set to the empty string. This is also the new default instead of "eth0". ** The Mandos client --connect option now loops indefinitely until a password is received from the specified server. ** Bug fix: Quote directory correctly in mandos-keygen with --password ** Bug fix: don't use "echo -e" in mandos-keygen; unsupported by dash. * Server: ** Terminology change: clients are now "ENABLED" or "DISABLED", not "valid" or "invalid". ** New D-Bus API; see the file "DBUS-API". ** New control utilities using the new D-Bus API: + mandos-ctl A command-line based utility + mandos-monitor A text-based GUI interface ** New feature: manual interactive approval or denying of clients on a case-by-case basis. ** New --debuglevel option to control logging ** Will not write PID file if --debug is passed ** Bug fix: Avoid race conditions with short "interval" values or fast checkers. ** Bug fix: Don't try to bind to a network interface when none is specified Version 1.0.14 (2009-10-25) Enable building without -pie and -fPIE if BROKEN_PIE is set. Version 1.0.13 (2009-10-22) * Client ** Security bug fix: If Mandos server is also installed, do not copy its config files (with encrypted passwords) into the initrd.img-* files. Version 1.0.12 (2009-09-17) * Client ** Bug fix: Allow network interface renaming by "udev" by taking down the network interface after using it. ** Bug fix: User-supplied plugins are now installed correctly. ** Bug fix: If usplash was used but the password was instead provided by the Mandos server, the usplash daemon used to ignore the first command passed to it. This has been fixed. ** Bug fix: Make the "--userid" and "--groupid" options in "plugin-runner.conf" work. * Server ** Bug fix: Fix the LSB header in the init.d script to make dependency based booting work. ** A client receiving its password now also counts as if a checker was run successfully (i.e. the timeout timer is reset). Version 1.0.11 (2009-05-23) * Client ** Bug fix: Use "pkg-config" instead of old "libgnutls-config". Version 1.0.10 (2009-05-17) * Client ** Security bug fix: Fix permissions on initrd.img-*.bak files when upgrading from older versions. Version 1.0.9 (2009-05-17) * Client ** Security bug fix: Fix permissions on initrd.img file when installing new linux-image-* packages calling mkinitramfs-kpkg (all version lower than 2.6.28-1-* does this). Version 1.0.8 (2009-02-25) * Client ** Bug fix: Fix missing quote characters in initramfs-tools-hook. Version 1.0.7 (2009-02-24) * Client ** Bug fix: Do not depend on GNU awk. Version 1.0.6 (2009-02-13) * Server ** Fix bug where server would stop responding, with a zombie checker ** Support for disabling IPv6 (only for advanced users) ** Fix bug which made server not change group ID * Client ** Bug fix: Fix permission for /lib64 (on relevant architechtures). ** Add support for IPv4 addresses. ** Add support in mandos-client for not bringing up a network interface by specifying an empty string to "--interface". ** Make password prompt on boot not be mangled by kernel log messages about network interface. ** Get network interface from initramfs.conf and/or from kernel command line. ** If set by "ip=" kernel command line, configure network on boot. ** Support connecting directly using "mandos=connect" kernel command. line option, provided network is configured using "ip=". ** Fix bug which made plugin-runner and mandos-client not change group ID. ** Fix bug where the "--options-for" option of plugin-runner would truncate the value at the first colon character. ** Fix bug where plugin-runner would not go to fallback if all plugins failed. ** Fix bug where mandos-client would not clean temporary directory on a signal or on certain file systems. ** Bug fix: remove bashism in /bin/sh script "mandos-keygen". Version 1.0.5 (2009-01-17) * Client ** Fix small memory leak in plugin-runner. Version 1.0.4 (2009-01-15) * Server ** Only find matched user/group pairs when searching for suitable nonprivileged user/group to switch to. * Client ** New kernel parameter "mandos=off" makes client not run at boot. ** Fix linking errors and compilation warnings on AMD64. ** Parse numbers in command line options better. ** The splashy and usplash plugins are more robust while traversing /proc, and will not abort if a process suddenly disappears. Version 1.0.3 (2009-01-06) * Server ** Now tries to change to user and group "_mandos" before falling back to trying the old values "mandos", "nobody:nogroup", and "65534". ** Now does not abort on startup even if no clients are defined in clients.conf. * Client ** Plugins named "*.dpkg-bak" are now ignored. ** Hopefully fixed compilation failure on some architectures where the C compiler does not recognize the "-z" option as a linker option. Version 1.0.2 (2008-10-17) * mandos-keygen now signs the encrypted key blobs. This signature is not currently verified by mandos-client, but this may change in the future. Version 1.0.1 (2008-10-07) * Server ** Expand environment variables and ~user in clients.conf's "secfile" The "secfile" option in /etc/mandos/clients.conf now expands "~user/foo" and "$ENVVAR" strings. * Client (plugin-runner, plugins, etc.) ** Manual pages for the usplash, splashy, and askpass-fifo plugins. All plugins now have man pages. ** More secure compilation and linking flags. All programs are now compiled with "-fstack-protector-all -fPIE -pie", and linked using "-z relro -pie" for additional security. * There is now a "NEWS" file (this one), giving a history of noteworthy changes. mandos-1.6.0/mandos.conf.xml0000664000175000017500000002132711767455003016211 0ustar teddyteddy00000000000000 /etc/mandos/mandos.conf"> %common; ]> Mandos Manual Mandos &version; &TIMESTAMP; Björn Påhlsson
belorn@recompile.se
Teddy Hogeborn
teddy@recompile.se
2008 2009 2011 2012 Teddy Hogeborn Björn Påhlsson
&CONFNAME; 5 &CONFNAME; Configuration file for the Mandos server &CONFPATH; DESCRIPTION The file &CONFPATH; is a simple configuration file for mandos 8, and is read by it at startup. The configuration file starts with [DEFAULT] on a line by itself, followed by any number of option=value entries, with continuations in the style of RFC 822. option: value is also accepted. Note that leading whitespace is removed from values. Lines beginning with # or ; are ignored and may be used to provide comments. OPTIONS FILES The file described here is &CONFPATH; BUGS The [DEFAULT] is necessary because the Python built-in module ConfigParser requires it. EXAMPLE No options are actually required: [DEFAULT] An example using all the options: [DEFAULT] # A configuration example interface = eth0 address = fe80::aede:48ff:fe71:f6f2 port = 1025 debug = true priority = SECURE256:!CTYPE-X.509:+CTYPE-OPENPGP servicename = Daena use_dbus = False use_ipv6 = True restore = True statedir = /var/lib/mandos SEE ALSO intro 8mandos, gnutls_priority_init3, mandos 8, mandos-clients.conf 5 RFC 4291: IP Version 6 Addressing Architecture Section 2.2: Text Representation of Addresses Section 2.5.5.2: IPv4-Mapped IPv6 Address Section 2.5.6, Link-Local IPv6 Unicast Addresses The clients use IPv6 link-local addresses, which are immediately usable since a link-local addresses is automatically assigned to a network interface when it is brought up. Zeroconf Zeroconf is the network protocol standard used by clients for finding the Mandos server on the local network.
mandos-1.6.0/plugin-runner.conf0000664000175000017500000000057411767455003016737 0ustar teddyteddy00000000000000## This is the configuration file for plugin-runner(8mandos). This ## file should be installed as "/etc/mandos/plugin-runner.conf", and ## will be copied to "/conf/conf.d/mandos/plugin-runner.conf" in the ## initrd.img file. ## ## After editing this file, the initrd image file must be updated for ## the changes to take effect! ## Example: #--options-for=mandos-client:--debug