diff options
36 files changed, 1901 insertions, 0 deletions
diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..2ffdf1a --- /dev/null +++ b/.gitignore @@ -0,0 +1,14 @@ +/pkg +/bin/ +/nshd.service +/nshd.socket +.var.* +.tmp.* +/LICENSE.*.txt +*.o + +*~ +output/ +cache/ +parabola-keyring-*.tar.gz + diff --git a/.gitmodules b/.gitmodules new file mode 100644 index 0000000..5e18a55 --- /dev/null +++ b/.gitmodules @@ -0,0 +1,12 @@ +[submodule "src/gopkg.in/yaml.v2"] + path = src/gopkg.in/yaml.v2 + url = https://gopkg.in/yaml.v2/ +[submodule "src/lukeshu.com/git/go/libgnulinux.git"] + path = src/lukeshu.com/git/go/libgnulinux.git + url = https://lukeshu.com/git/go/libgnulinux.git/ +[submodule "src/lukeshu.com/git/go/libnslcd.git"] + path = src/lukeshu.com/git/go/libnslcd.git + url = https://lukeshu.com/git/go/libnslcd.git/ +[submodule "src/lukeshu.com/git/go/libsystemd.git"] + path = src/lukeshu.com/git/go/libsystemd.git + url = https://lukeshu.com/git/go/libsystemd.git/ diff --git a/LICENSE.txt b/LICENSE.txt new file mode 100644 index 0000000..6031cd7 --- /dev/null +++ b/LICENSE.txt @@ -0,0 +1,24 @@ +The software in this repository is under a number of licenses: + + scripts/ GPL v2+ + src/nshd/ GPL v2+ + test/ LGPL v2.1+ + + src/gopkg.in/yaml.v2 LGPL v3, with linking exception + src/lukeshu.com/git/go/libgnulinux.git/dl GPL v2+, with documentation output clarification + src/lukeshu.com/git/go/libgnulinux.git/inotify LGPL v2.1+ + src/lukeshu.com/git/go/libgnulinux.git/getgr LGPL v2.1+ + src/lukeshu.com/git/go/libnslcd.git/ LGPL v2.1+ + src/lukeshu.com/git/go/libsystemd.git/ Apache v2.0 + +The general notion is that the core application is GPL (v2+), while +supporting libraries that might be useful outside of this specific +application are LGPL (v2.1+), with the exceptions: + - The systemd package is Apache licensed because of code taken from + CoreOS and Docker. + - The dl package is GPL v2+ licensed because of wording in comments + taken from the Linux Programmer's Manual. + - The YAML library uses v3 of the LGPL because it's a 3rd-party + library, and that's how the authors licensed it. + +For more details, see each individual file. diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..7f98070 --- /dev/null +++ b/Makefile @@ -0,0 +1,92 @@ +# Copyright 2015 Luke Shumaker <lukeshu@sbcglobal.net>. +# +# This 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 2 of +# the License, or (at your option) any later version. +# +# The GNU General Public License's references to "object code" and +# "executables" are to be interpreted to also include the output of +# any document formatting or typesetting system, including +# intermediate and printed output. +# +# This software 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 manual; if not, see +# <http://www.gnu.org/licenses/>. + +MAKEFLAGS += --no-builtin-rules + +prefix = /usr/local +bindir = $(prefix)/bin +libdir = $(prefix)/lib +systemddir = $(libdir)/systemd + +Q ?= @ +#NET ?= FORCE + +user = nshd +group = nshd + +CFLAGS = -std=c99 -Wall -Wextra -Werror -pedantic +CGO_CFLAGS = $(CFLAGS) -Wno-unused-parameter +CGO_ENABLED = 1 + +deps += gopkg.in/yaml.v2 +deps += lukeshu.com/git/go/libgnulinux.git +deps += lukeshu.com/git/go/libnslcd.git +deps += lukeshu.com/git/go/libsystemd.git + +srcdir := $(abspath $(patsubst %/,%,$(dir $(lastword $(MAKEFILE_LIST))))) +topdir := $(srcdir) + +subdirs = src/lukeshu.com/git/go/libnslcd.git/proto + +generate += $(addprefix $(topdir)/src/,$(deps)) +generate += $(topdir)/LICENSE.lgpl-2.1.txt $(topdir)/LICENSE.gpl-2.txt $(topdir)/LICENSE.apache-2.0.txt +generate_secondary += $(topdir)/src/*.*/ +build += $(topdir)/bin/nshd $(topdir)/nshd.service $(topdir)/nshd.socket $(topdir)/test/runner +build_secondary += $(topdir)/bin $(topdir)/pkg $(topdir)/test/*.o .var.* +install += $(addprefix $(DESTDIR),$(bindir)/nshd $(systemddir)/system/nshd.socket $(systemddir)/system/nshd.service) + +ifeq (1,$(words $(MAKEFILE_LIST))) + include $(topdir)/common.mk +endif +src/lukeshu.com/git/go/libnslcd.git/proto/Makefile: $(topdir)/src/lukeshu.com/git/go/libnslcd.git + +$(topdir)/LICENSE.lgpl-2.1.txt: $(NET) + curl https://www.gnu.org/licenses/old-licenses/lgpl-2.1.txt > $@ +$(topdir)/LICENSE.gpl-2.txt: $(NET) + curl https://www.gnu.org/licenses/old-licenses/gpl-2.0.txt > $@ +$(topdir)/LICENSE.apache-2.0.txt: $(NET) + curl https://www.apache.org/licenses/LICENSE-2.0 > $@ +$(topdir)/LICENSE.wtfpl-2.txt: $(NET) + curl http://www.wtfpl.net/txt/copying/ > $@ + +include $(topdir)/golang.mk + +$(call goget,$(topdir),$(deps)) + +$(topdir)/bin/nshd: $(generate) $(configure) $(call gosrc,$(topdir)) + $(call goinstall,$(topdir),nshd) + +%.o: %.c .var.CC .var.CPPFLAGS .var.CFLAGS + $(CC) $(CPPFLAGS) $(CFLAGS) -c -o $@ $(filter-out .var.%,$^) +%: %.o .var.CC .var.LDFLAGS .var.LOADLIBES .var.LDLIBS + $(CC) $(LDFLAGS) -o $@ $(filter-out .var.%,$^) $(LOADLIBES) $(LDLIBS) + +%: %.in + < $< sed $(foreach v,$(patsubst .var.%,%,$(filter .var.%,$^)), -e 's|@$v@|$($v)|g' ) > $@ +$(topdir)/nshd.service: .var.bindir .var.user .var.group +$(topdir)/nshd.socket: .var.user .var.group + +$(DESTDIR)$(bindir)/%: bin/% + install -TDm755 $< $@ +$(DESTDIR)$(systemddir)/system/%.socket: %.socket + install -TDm644 $< $@ +$(DESTDIR)$(systemddir)/system/%.service: %.service + install -TDm644 $< $@ diff --git a/README.md b/README.md new file mode 100644 index 0000000..824e74d --- /dev/null +++ b/README.md @@ -0,0 +1,76 @@ +This repository contains tools for working with hackers.git +information. + +The most important 4 programs are: + + - `meta-check`: sanity-check hackers.git data + - `ssh-list-authorized-keys`: configure sshd to use this for + AuthorizedKeysCommand to have it get SSH keys directly from + hackers.git + - `postfix-generate-virtual-map`: generate a virtual map + for Postfix that provides email aliases for users in hackers.git + - `pacman-make-keyring` generate a tarball with the pacman-keyring + files for the users in hackers.git + +The others are: + + - `meta-normalize-stdio`: used by `meta-check` + - `meta-cat`: used by `nshd` + - `pgp-list-keyids`: used by `pacman-make-keyring` + - `uid-map`: used by `pacman-make-keyring` + +Each of the programs looks for `parabola-hackers.yml` in he current +directory (except for `meta-normalize-stdio`, which has no +configuration). + +# Configuration + +The main two things programs at are `yamldir` which tells them where +to find `hackers.git/users`, and `groupgroups` which augments the +`groups` array for each user. + +## pacman-make-keyring + +`pacman-make-keyring` also looks at `keyring_cachedir` to see where to +store files that can be cached between versions of the keyring. + +## ssh-list-authorized-keys + +`ssh-list-authorized-keys` also looks at `ssh_pseudo_users`. +System users (`/etc/passwd`) mentioned in this variable may be SSH'ed +into by hackers.git users who are in a group of the same name. + +## nshd (TODO) + +`nshd` also looks at `pam_password_prohibit_message` to decide what to +say when prohibiting a user from being changed via PAM. + +# Usage + +## meta-check + +Just run it, it will report any problems with hackers.git data. + +## ssh-list-authorized-keys + +Configure `sshd_config:AuthorizedKeysCommand` to be this program. +`sshd` will run it as `ssh-list-authorized-keys ${USERNAME}` + +## postfix-generate-virtual-map + + postfix-show-virtual-map > /etc/postfix/virtual-parabola.nu + postmap hash:/etc/postfix/virtual-parabola.nu + +## pacman-make-keyring + + pacman-make-keyring V=$(date -u +%Y%m%d) + scp parabola-keyring-$(date -u +%Y%m%d).tar.gz repo.parabola.nu:/srv/repo/main/other/parabola-keyring/ + +or + + cd $(. "$(librelib conf)" && load_files makepkg && echo "$SRCDEST") + pacman-make-keyring V=$(date -u +%Y%m%d) + +In the latter case, it would get uploaded automagically by +`librerelease` when you release a parabola-keyring with the matching +version. diff --git a/common.mk b/common.mk new file mode 100644 index 0000000..41f5c79 --- /dev/null +++ b/common.mk @@ -0,0 +1,55 @@ +# Copyright © 2015 Luke Shumaker +# This work is free. You can redistribute it and/or modify it under the +# terms of the Do What The Fuck You Want To Public License, Version 2, +# as published by Sam Hocevar. See http://www.wtfpl.net/ for more details. + +rel = $(patsubst $(abspath .)/%,./%,$(abspath $1)) + +all: build +.PHONY: all + +include $(addsuffix /Makefile,$(subdirs)) + +generate: $(generate) +.PHONY: generate + +configure: generate $(configure) +.PHONY: configure + +build: configure $(build) +.PHONY: build + +install: build $(install) +.PHONY: install + +# un-build +clean: + rm -rf -- $(build) $(build_secondary) +.PHONY: clean + +# un-configure +distclean: clean + rm -rf -- $(configure) $(configure_secondary) +.PHONY: distclean + +# un-generate +maintainer-clean: distclean + rm -rf -- $(generate) $(generate_secondary) +.PHONY: maintainer-clean + +# un-install +uninstall: + rm -f -- $(install) + rmdir -p -- $(sort $(dir $(install))) 2>/dev/null || true +.PHONY: uninstall + + +# Now, this is magic. It stores the values of environment variables, +# so that if you change them in a way that would cause something to be +# rebuilt, then Make knows. +.var.%: FORCE + $(Q)printf '%s' '$($*)' > .tmp$@ && { cmp -s .tmp$@ $@ && rm -f -- .tmp$@ || mv -Tf .tmp$@ $@; } || { rm -f -- .tmp$@; false; } + +.DELETE_ON_ERROR: +.SECONDARY: +.PHONY: FORCE diff --git a/golang.mk b/golang.mk new file mode 100644 index 0000000..222bbdf --- /dev/null +++ b/golang.mk @@ -0,0 +1,26 @@ +# Copyright 2015 Luke Shumaker + +_golang_cgo_variables = CGO_ENABLED CGO_CFLAGS CGO_CPPFLAGS CGO_CXXFLAGS CGO_LDFLAGS CC CXX +export $(_golang_cgo_variables) +_golang_src_cmd = find -L $1/src -name '.*' -prune -o \( -type f \( -false $(foreach e,go c s S cc cpp cxx h hh hpp hxx,-o -name '*.$e') \) -o -type d \) -print + +# Iterate over external dependencies, and create a rule to download it +goget = $(foreach d,$2,$(eval $1/src/$d: $(NET); GOPATH='$(abspath $1)' go get -d -u $d)) + +#|| { rm -rf -- $$@; false; })) + +gosrc = $(shell $(_golang_src_cmd)) $(addprefix .var.,$(_golang_cgo_variables)) +define goinstall + $(Q)for target in $(addprefix $1/bin/,$(notdir $2)); do \ + if test -e $$target; then \ + for dep in $(filter .var.%,$^); do \ + if test $$dep -nt $$target; then \ + rm -rf -- $1/bin $1/pkg || exit $$?; \ + exit 0; \ + fi \ + done \ + fi \ + done + GOPATH='$(abspath $1)' go install $2 + $(Q)true $(foreach e,$(notdir $2), && test -f $1/bin/$e -a -x $1/bin/$e && touch $1/bin/$e) +endef diff --git a/nshd.service.in b/nshd.service.in new file mode 100644 index 0000000..caf5508 --- /dev/null +++ b/nshd.service.in @@ -0,0 +1,15 @@ +[Unit] +Description=Parabola hackers.git authentication +Requires=nshd.socket +After=syslog.target nshd.socket + +[Service] +Type=notify +Sockets=nshd.socket +ExecStart=@bindir@/nshd + +User=@user@ +Group=@group@ + +[Install] +WantedBy=multi-user.target diff --git a/nshd.socket.in b/nshd.socket.in new file mode 100644 index 0000000..a514391 --- /dev/null +++ b/nshd.socket.in @@ -0,0 +1,13 @@ +[Unit] +Description=Parabola hackers.git authentication + +[Socket] +ListenStream=/var/run/nslcd/socket +PassCredentials=yes +PassSecurity=yes + +SocketUser=@user@ +SocketGroup=@group@ + +[Install] +WantedBy=sockets.target diff --git a/parabola-hackers.yml b/parabola-hackers.yml new file mode 100644 index 0000000..c09f21b --- /dev/null +++ b/parabola-hackers.yml @@ -0,0 +1,40 @@ +--- +# Where to look for "${uid}.yml" files +#yamldir: "/var/lib/hackers-git/users" +yamldir: "users" + +# Which groups imply membership in other groups (since UNIX groups +# can't be nested). Only one level of nesting is supported ATM. +# +# That is, if you are in the 'hackers' group, you are also in the +# 'repo' and 'git' groups, even if they aren't listed. +groupgroups: + hackers: + - repo + - git + - ssh + - email + - keyring-trusted + fellows: + - email + trustedusers: + - keyring-secondary + bots: + - keyring-trusted + +# Groups that are system users that can be ssh'ed into. +# +# So, if 'lukeshu' is in the 'repo' group, he can ssh to +# 'repo'@hostname. +ssh_pseudo_users: +- repo +- git + +# The message, if any, that is presented to the user when password +# modification through PAM is prohibited. +pam_password_prohibit_message: '' + +# Where to keep files that can be cached between versions when making +# the pacman keyring. +#keyring_cachedir: "/var/cache/parabola-hackers" +keyring_cachedir: "cache" diff --git a/scripts/common.rb b/scripts/common.rb new file mode 100644 index 0000000..91e14be --- /dev/null +++ b/scripts/common.rb @@ -0,0 +1,22 @@ +require 'yaml' + +def cfg + @cfg ||= YAML::load(open("parabola-hackers.yml")) +end + +def load_user_yaml(filename) + user = YAML::load(open(filename)) + groups = user["groups"] || [] + user["groups"] = groups.concat((groups & cfg["groupgroups"].keys).map{|g|cfg["groupgroups"][g]}.flatten) + return user +end + +def load_all_users + users = {} + Dir.glob("#{cfg["yamldir"]}/*.yml").map{|filename| + uid = File.basename(filename).sub(/^([0-9]*)\.yml$/, "\\1").to_i + user = load_user_yaml(filename) + users[uid] = user + } + return users +end diff --git a/scripts/meta-cat b/scripts/meta-cat new file mode 100755 index 0000000..e6b9edd --- /dev/null +++ b/scripts/meta-cat @@ -0,0 +1,6 @@ +#!/usr/bin/env ruby +# Usage: meta-cat + +load "#{File.dirname(__FILE__)}/common.rb" + +print load_all_users.to_yaml diff --git a/scripts/meta-check b/scripts/meta-check new file mode 100755 index 0000000..4a2981e --- /dev/null +++ b/scripts/meta-check @@ -0,0 +1,44 @@ +#!/bin/bash + +. libremessages + +mydir="$(dirname "$0")" +PATH="$mydir:$PATH" + +check-yaml() { + file=$1 + msg 'Inspecting %q' "$file" + norm=$(mktemp --tmpdir) + trap "rm -f -- $(printf '%q' "$norm")" RETURN + meta-normalize-stdio < "$file" > "$norm" || return $? + colordiff -u "$file" "$norm" || return $? +} + +main() { + declare -i ret=0 + + yamldir="$(ruby -e "load '$mydir/common.rb'; print cfg['yamldir']")" + + # Check the user YAML files + for file in "$yamldir"/*.yml; do + check-yaml "$file" || ret=$? + done + + msg 'Checking for duplicate usernames' + dups=($(sed -n 's/^username: //p' -- "$yamldir"/*.yml| sort | uniq -d)) + if (( ${#dups[@]} )); then + error 'Duplicate usernames:' + plain '%s' "${dups[@]}" + ret=1 + fi + + msg 'Checking PGP keys' + if pgp-list-keyids | grep -Ev '^(trusted|secondary|revoked)/[a-z][a-z0-9-]* [0-9A-F]{40}$'; then + error 'Bad pgp keys ^^^' + ret=1 + fi + + return $ret +} + +main "$@" diff --git a/scripts/meta-normalize-stdio b/scripts/meta-normalize-stdio new file mode 100755 index 0000000..5611ae6 --- /dev/null +++ b/scripts/meta-normalize-stdio @@ -0,0 +1,171 @@ +#!/usr/bin/env ruby + +# First we define a bunch of code-generators, then at the end is a +# very neat and readable definition of the format of the YAML files. + +require 'yaml' + +def error(msg) + $stderr.puts "ERROR: #{msg}" + @err = 1 +end + +def warning(msg) + $stderr.puts "WARNING: #{msg}" +end + + +# Generic validators/formatters + +def semiordered_list(cnt, validator) + lambda {|name,ary| + if ary.class != Array + error "`#{name}' must be a list" + else + ary.each_index{|i| ary[i] = validator.call("#{name}[#{i}]", ary[i])} + ary = ary.first(cnt).concat(ary.last(ary.count-cnt).sort) + end + ary + } +end + +def unordered_list(validator) + semiordered_list(0, validator) +end + +def _unknown(map_name, key) + error "Unknown item: #{map_name}[#{key.inspect}]" + 0 +end +def unordered_map1(validator) + lambda {|name,hash| + if hash.class != Hash + error "`#{name}' must be a map" + else + order = Hash[[*validator.keys.map.with_index]] + hash = Hash[hash.sort_by{|k,v| order[k] || _unknown(name,k) }] + hash.keys.each{|k| + if validator[k] + hash[k] = validator[k].call("#{name}[#{k.inspect}]", hash[k]) + end + } + end + hash + } +end + +def unordered_map2(key_validator, val_validator) + lambda {|name,hash| + if hash.class != Hash + error "`#{name}' must be a map" + else + hash = Hash[hash.sort_by{|k,v| k}] + hash.keys.each{|k| + key_validator.call("#{name} key #{k.inspect}", k) + hash[k] = val_validator.call("#{name}[#{k.inspect}]", hash[k]) + } + end + hash + } +end + +string = lambda {|name,str| + if str.class != String + error "`#{name}' must be a string" + else + str + end +} + +# Regular Expression String +def restring(re) + lambda {|name,str| + if str.class != String + error "`#{name}' must be a string" + else + unless re =~ str + error "`#{name}' does not match #{re.inspect}: #{str}" + end + str + end + } +end + + +# Specific validators/formatters + +year = lambda {|name, num| + if num.class != Fixnum + error "`#{name}' must be a year" + else + if (num < 1900 || num > 3000) + error "`#{name}' is a number, but doesn't look like a year" + end + num + end +} + +# This regex is taken from http://www.w3.org/TR/html5/forms.html#valid-e-mail-address +_email_regex = /^[a-zA-Z0-9.!\#$%&'*+\/=?^_`{|}~-]+@[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(?:\.[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)*$/ +email_list = lambda {|name, ary| + if ary.class != Array + error "`#{name}' must be a list" + elsif not ary.empty? + preserve = 1 + if ary.first.end_with?("@parabola.nu") and ary.count >= 2 + preserve = 2 + end + ary = semiordered_list(preserve, restring(_email_regex)).call(name, ary) + end + ary +} + +shell = lambda {|name, sh| + if sh.class != String + error "`#{name}' must be a string" + else + @valid_shells ||= open("/etc/shells").read.split("\n") + .find_all{|line| /^[^\#]/ =~ line} + unless @valid_shells.include?(sh) + warning "shell not listed in /etc/shells: #{sh}" + end + end + sh +} + + +# The format of the YAML files + +format = unordered_map1( + { + "username" => restring(/^[a-z][a-z0-9-]*$/), + "fullname" => string, + "email" => email_list, + "groups" => semiordered_list(1, string), + "pgp_keyid" => restring(/^[0-9A-F]{40}$/), + "pgp_revoked_keyids" => unordered_list(restring(/^[0-9A-F]{40}$/)), + "ssh_keys" => unordered_map2(string, string), + "shell" => shell, + "extra" => unordered_map1( + { + "alias" => string, + "other_contact" => string, + "roles" => string, + "website" => string, + "occupation" => string, + "yob" => year, + "location" => string, + "languages" => string, + "interests" => string, + "favorite_distros" => string, + }) + }) + + + +@err = 0 +user = format.call("user", YAML::load(STDIN)) +if @err != 0 + exit @err +end +print user.to_yaml diff --git a/scripts/pacman-make-keyring b/scripts/pacman-make-keyring new file mode 100755 index 0000000..589984d --- /dev/null +++ b/scripts/pacman-make-keyring @@ -0,0 +1,150 @@ +#!/usr/bin/make -rRf +# Usage: pacman-make-keyring V=$(date -u +%Y%m%d) +ifeq ($(origin V),undefined) +$(info Usage: pacman-make-keyring V=$$(date -u +%Y%m%d)) +$(error You must set V= on the command line) +endif + +bin := $(patsubst %/,%,$(dir $(lastword $(MAKEFILE_LIST)))) +yamldir := $(shell ruby -e "load '$(bin)/common.rb'; print cfg['yamldir']") +cachedir := $(shell ruby -e "load '$(bin)/common.rb'; print cfg['keyring_cachedir']") + +outputdir = $(cachedir)/$(KEYRING_NAME)-keyring-$(V) +KEYRING_NAME = parabola + +all: $(KEYRING_NAME)-keyring-$(V).tar.gz +.PHONY: all + +export SHELL = /bin/bash -o pipefail +.PHONY: FORCE +.SECONDARY: +.DELETE_ON_ERROR: + +dirs = \ + $(outputdir) \ + $(cachedir) \ + $(cachedir)/gpghome \ + $(cachedir)/keys/trusted \ + $(cachedir)/keys/secondary \ + $(cachedir)/keys/revoked + +$(dirs): + mkdir -p $@ + +$(cachedir)/var.%: FORCE | $(cachedir) + @$(file >$(@D)/tmp.$(@F),$($*)) + @sed -i 's|^|#|' $(@D)/tmp.$(@F) + @if cmp -s $(@D)/tmp.$(@F) $@; then \ + rm -f $(@D)/tmp.$(@F) || :; \ + else \ + mv -f $(@D)/tmp.$(@F) $@; \ + fi +-include $(wildcard $(cachedir)/var.*) +$(cachedir)/txt.%: $(cachedir)/var.% + sed 's|^#||' < $< > $@ +var=$(cachedir)/var. + +keyring-files = \ + $(outputdir)/Makefile \ + $(outputdir)/${KEYRING_NAME}.gpg \ + $(outputdir)/${KEYRING_NAME}-trusted \ + $(outputdir)/${KEYRING_NAME}-revoked + +$(KEYRING_NAME)-keyring-$(V).tar.gz: %.tar.gz: $(keyring-files) + bsdtar --format=ustar -cf - -C $(cachedir) $(addprefix $*/,$(notdir $^)) | gzip -9 > $@ + +define Makefile.in +V=@V@ + +prefix = /usr/local +PREFIX = $$(prefix) + +install: + install -dm755 $$(DESTDIR)$$(PREFIX)/share/pacman/keyrings/ + install -m0644 @KEYRING_NAME@{.gpg,-trusted,-revoked} $$(DESTDIR)$$(PREFIX)/share/pacman/keyrings/ + +uninstall: + rm -f $$(DESTDIR)$$(PREFIX)/share/pacman/keyrings/@KEYRING_NAME@{.gpg,-trusted,-revoked} + rmdir -p --ignore-fail-on-non-empty $$(DESTDIR)$$(PREFIX)/share/pacman/keyrings/ + +.PHONY: install uninstall +endef + +$(outputdir)/Makefile: $(cachedir)/txt.Makefile.in $(var)V $(var)KEYRING_NAME | $(outputdir) + sed $(foreach v,$(patsubst $(var)%,%,$(filter $(var)%,$^)), -e 's|@$v@|$($v)|' ) < $< > $@ + + +users := $(sort $(shell find $(yamldir))) $(var)users + +# Assemble the list of .asc files needed to generate the keyring +$(cachedir)/deps.mk: ${users} $(var)outputdir $(var)cachedir $(var)KEYRING_NAME| $(cachedir) + { \ + echo $(outputdir)/${KEYRING_NAME}.gpg: $$($(bin)/pgp-list-keyids | sed -r 's|(\S+) .*|$$(cachedir)/keys/\1.asc|') && \ + echo $(cachedir)/stamp.ownertrust: $$($(bin)/pgp-list-keyids | sed -rn 's|^(trusted/\S+) .*|$$(cachedir)/keys/\1.asc|p') && \ + $(bin)/pgp-list-keyids | sed -rn 's|^trusted/(\S+) (.*)|keyid.\1 = \2|p' && \ + $(bin)/uid-map | sed 's|.*|trusted:&\nsecondary:&\nrevoked:&|' | sed -r 's|(.*):(.*):(.*)|$$(cachedir)/keys/\1/\3.asc: $$(yamldir)/\2.yml|' && \ + :; }> $@ +-include $(cachedir)/deps.mk + +# The remainder of file is mostly just a translation of the shell +# script `update-keys`. +# +# https://git.archlinux.org/archlinux-keyring.git/tree/update-keys + +export LANG=C + +KEYSERVER = hkp://pool.sks-keyservers.net + +GPG = gpg --quiet --batch --no-tty --no-permission-warning --keyserver ${KEYSERVER} --homedir $(cachedir)/gpghome + +define gpg-init +%echo Generating Parabola Keyring keychain master key... +Key-Type: RSA +Key-Length: 1024 +Key-Usage: sign +Name-Real: Parabola Keyring Keychain Master Key +Name-Email: parabola-keyring@localhost +Expire-Date: 0 +%no-protection +%commit +%echo Done +endef +$(cachedir)/stamp.gpg-init: $(cachedir)/txt.gpg-init $(var)GPG | $(cachedir)/gpghome + ${GPG} --gen-key < $< + touch $@ + +# The appropriate ${uid}.yml file is added as a dependency to +# ${username}.yml by deps.mk +keyid=$(keyid.$(patsubst %.asc,%,$(notdir $@))) + +# In 'update-keys', this is the 'master-keyids' loop +$(outputdir)/${KEYRING_NAME}-trusted: ${users} | $(outputdir) + $(bin)/pgp-list-keyids | sed -rn 's|^trusted/\S+ (\S+)|\1:4:|p' > $@ +$(cachedir)/keys/trusted/%.asc : $(cachedir)/stamp.gpg-init | $(cachedir)/keys/trusted + ${GPG} --recv-keys ${keyid} &>/dev/null + printf 'minimize\nquit\ny\n' | ${GPG} --command-fd 0 --edit-key ${keyid} + printf 'y\ny\n' | ${GPG} --command-fd 0 --lsign-key ${keyid} &>/dev/null + ${GPG} --armor --no-emit-version --export ${keyid} > $@ + +$(cachedir)/stamp.ownertrust: $(outputdir)/${KEYRING_NAME}-trusted $(cachedir)/deps.mk + ${GPG} --import-ownertrust < $< 2>/dev/null + touch $@ + +# In 'update-keys', this is the 'packager-keyids' loop +$(cachedir)/keys/secondary/%.asc: $(cachedir)/stamp.ownertrust | $(cachedir)/keys/secondary + ${GPG} --recv-keys ${keyid} &>/dev/null + printf 'clean\nquit\ny\n' | ${GPG} --command-fd 0 --edit-key ${keyid} + ${GPG} --list-keys --with-colons ${keyid} 2>/dev/null | grep -q '^pub:f:' # make sure it is trusted + ${GPG} --armor --no-emit-version --export ${keyid} > $@ + +# In 'update-keys', this is the 'packager-revoked-keyids' loop +$(outputdir)/${KEYRING_NAME}-revoked: ${users} | $(outputdir) + $(bin)/pgp-list-keyids | sed -rn 's|^revoked/\S+ ||p' > $@ +$(cachedir)/keys/revoked/%.asc : $(cachedir)/stamp.ownertrust | $(cachedir)/keys/revoked + ${GPG} --recv-keys ${keyid} &>/dev/null + printf 'clean\nquit\ny\n' | ${GPG} --command-fd 0 --edit-key ${keyid} + ! ${GPG} --list-keys --with-colons ${keyid} 2>/dev/null | grep -q '^pub:f:' # make sure it isn't trusted + ${GPG} --armor --no-emit-version --export ${keyid} > $@ + +$(outputdir)/${KEYRING_NAME}.gpg: $(cachedir)/deps.mk | $(outputdir) + cat $(filter %.asc,$^) > $@ diff --git a/scripts/pgp-list-keyids b/scripts/pgp-list-keyids new file mode 100755 index 0000000..9682b1a --- /dev/null +++ b/scripts/pgp-list-keyids @@ -0,0 +1,21 @@ +#!/usr/bin/env ruby +# Usage: pgp-list-keyids + +load "#{File.dirname(__FILE__)}/common.rb" + +load_all_users.each do |uid,user| + if user["groups"] + if user["groups"].include?("keyring-trusted") + puts "trusted/#{user["username"]} #{user["pgp_keyid"]}" + elsif user["groups"].include?("keyring-secondary") + puts "secondary/#{user["username"]} #{user["pgp_keyid"]}" + elsif user["pgp_keyid"] + #puts "revoked/#{user["username"]} #{user["pgp_keyid"]}" + end + end + if user["pgp_revoked_keyids"] + user["pgp_revoked_keyids"].each do |keyid| + puts "revoked/#{user["username"]} #{keyid}" + end + end +end diff --git a/scripts/postfix-generate-virtual-map b/scripts/postfix-generate-virtual-map new file mode 100755 index 0000000..d5c2d21 --- /dev/null +++ b/scripts/postfix-generate-virtual-map @@ -0,0 +1,18 @@ +#!/usr/bin/env ruby +# Usage: postfix-show-virtual-map > ${file} && postmap hash:${file} + +load "#{File.dirname(__FILE__)}/common.rb" + +users = load_all_users.values.find_all{|u|u["groups"].include?("email")} + +users.each do |user| + if user["email"] and user["email"].length > 0 + if user["email"][0] =~ /.*@parabola.nu$/ + if user["email"].length > 1 + puts "#{user["username"]}@parabola.nu #{user["email"][1]}" + end + else + puts "#{user["username"]}@parabola.nu #{user["email"][0]}" + end + end +end diff --git a/scripts/ssh-list-authorized-keys b/scripts/ssh-list-authorized-keys new file mode 100755 index 0000000..5fb1ea1 --- /dev/null +++ b/scripts/ssh-list-authorized-keys @@ -0,0 +1,25 @@ +#!/usr/bin/env ruby +# Usage: ssh-list-authorized-keys [username] + +load "#{File.dirname(__FILE__)}/common.rb" + +all_users = load_all_users.values + +groupnames = ARGV & cfg["ssh_pseudo_users"] +usernames = ARGV & all_users.map{|u|u["username"]} + +users = all_users.find_all{|u| + # [ username was listed ] or [ the user is in a listed group ] + usernames.include?(u["username"]) or not (u["groups"] & groupnames).empty? +} + +# Buffer the output to avoid EPIPE when the reader hangs up early +output="" +users.each do |user| + if user["ssh_keys"] + user["ssh_keys"].each do |addr,key| + output+="#{key} #{user["fullname"]} (#{user["username"]}) <#{addr}>\n" + end + end +end +print output diff --git a/scripts/uid-map b/scripts/uid-map new file mode 100755 index 0000000..10c3fac --- /dev/null +++ b/scripts/uid-map @@ -0,0 +1,8 @@ +#!/usr/bin/env ruby +# Usage: uid-map + +load "#{File.dirname(__FILE__)}/common.rb" + +load_all_users.each do |uid,user| + puts "#{uid}:#{user["username"]}" +end diff --git a/src/gopkg.in/yaml.v2 b/src/gopkg.in/yaml.v2 new file mode 160000 +Subproject f7716cbe52baa25d2e9b0d0da546fcf909fc16b diff --git a/src/lukeshu.com/git/go/libgnulinux.git b/src/lukeshu.com/git/go/libgnulinux.git new file mode 160000 +Subproject 58bfc6259f5ced680b5b200687240d2449313b2 diff --git a/src/lukeshu.com/git/go/libnslcd.git b/src/lukeshu.com/git/go/libnslcd.git new file mode 160000 +Subproject dad3de8e9835a11b1df9e718eef3faa50459ccb diff --git a/src/lukeshu.com/git/go/libsystemd.git b/src/lukeshu.com/git/go/libsystemd.git new file mode 160000 +Subproject 89efdfbee5f9a22f9dd1083f7a383daba54d4f1 diff --git a/src/nshd/hackers_git/check_password.go b/src/nshd/hackers_git/check_password.go new file mode 100644 index 0000000..81ad932 --- /dev/null +++ b/src/nshd/hackers_git/check_password.go @@ -0,0 +1,50 @@ +// Copyright 2015 Luke Shumaker <lukeshu@sbcglobal.net>. +// +// This 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 2 of +// the License, or (at your option) any later version. +// +// The GNU General Public License's references to "object code" and +// "executables" are to be interpreted to also include the output of +// any document formatting or typesetting system, including +// intermediate and printed output. +// +// This software 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 manual; if not, see +// <http://www.gnu.org/licenses/>. + +package hackers_git + +import "unsafe" + +/* +#cgo LDFLAGS: -lcrypt +#define _GNU_SOURCE // for crypt_r(3) in crypt.h +#include <stdlib.h> // for free(3) +#include <crypt.h> // for crypt_r(3) +#include <string.h> // for strcmp(3) and memset(3) +int check_password(const char *password, const char *hash) +{ + int ret; + struct crypt_data data; + data.initialized = 0; + ret = (strcmp(crypt_r(password, hash, &data), hash) == 0); + memset(&data, 0, sizeof(data)); + return ret; +} +*/ +import "C" + +func check_password(password string, hash string) bool { + cpassword := C.CString(password) + defer C.free(unsafe.Pointer(cpassword)) + chash := C.CString(hash) + defer C.free(unsafe.Pointer(chash)) + return C.check_password(cpassword, chash) != 0 +} diff --git a/src/nshd/hackers_git/db_config.go b/src/nshd/hackers_git/db_config.go new file mode 100644 index 0000000..dc3b99e --- /dev/null +++ b/src/nshd/hackers_git/db_config.go @@ -0,0 +1,44 @@ +// Copyright 2015 Luke Shumaker <lukeshu@sbcglobal.net>. +// +// This 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 2 of +// the License, or (at your option) any later version. +// +// The GNU General Public License's references to "object code" and +// "executables" are to be interpreted to also include the output of +// any document formatting or typesetting system, including +// intermediate and printed output. +// +// This software 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 manual; if not, see +// <http://www.gnu.org/licenses/>. + +package hackers_git + +import ( + p "lukeshu.com/git/go/libnslcd.git/proto" + s "syscall" +) + +func (o *Hackers) Config_Get(cred s.Ucred, req p.Request_Config_Get) <-chan p.Config { + o.lock.RLock() + ret := make(chan p.Config) + go func() { + defer o.lock.RUnlock() + defer close(ret) + + switch req.Key { + case p.NSLCD_CONFIG_PAM_PASSWORD_PROHIBIT_MESSAGE: + if o.Cfg.Pam_password_prohibit_message != "" { + ret <- p.Config{Value: o.Cfg.Pam_password_prohibit_message} + } + } + }() + return ret +} diff --git a/src/nshd/hackers_git/db_group.go b/src/nshd/hackers_git/db_group.go new file mode 100644 index 0000000..7e97c05 --- /dev/null +++ b/src/nshd/hackers_git/db_group.go @@ -0,0 +1,144 @@ +// Copyright 2015 Luke Shumaker <lukeshu@sbcglobal.net>. +// +// This 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 2 of +// the License, or (at your option) any later version. +// +// The GNU General Public License's references to "object code" and +// "executables" are to be interpreted to also include the output of +// any document formatting or typesetting system, including +// intermediate and printed output. +// +// This software 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 manual; if not, see +// <http://www.gnu.org/licenses/>. + +package hackers_git + +import ( + p "lukeshu.com/git/go/libnslcd.git/proto" + s "syscall" +) + +func (o *Hackers) groupByName(name string, users bool) p.Group { + members_set, found := o.groups[name] + if !found { + return p.Group{ID: -1} + } + gid := name2gid(name) + if gid < 0 { + return p.Group{ID: -1} + } + var members_list []string + if users { + members_list = set2list(members_set) + } else { + members_list = make([]string, 0) + } + return p.Group{ + Name: name, + PwHash: "x", + ID: gid, + Members: members_list, + } +} + +func (o *Hackers) groupByGid(gid int32, users bool) p.Group { + name, found := gid2name(gid) + if !found { + return p.Group{ID: -1} + } + members_set, found := o.groups[name] + if !found { + return p.Group{ID: -1} + } + var members_list []string + if users { + members_list = set2list(members_set) + } else { + members_list = make([]string, 0) + } + return p.Group{ + Name: name, + PwHash: "x", + ID: gid, + Members: members_list, + } +} + +func (o *Hackers) Group_ByName(cred s.Ucred, req p.Request_Group_ByName) <-chan p.Group { + o.lock.RLock() + ret := make(chan p.Group) + go func() { + defer o.lock.RUnlock() + defer close(ret) + + group := o.groupByName(req.Name, true) + if group.ID < 0 { + return + } + ret <- group + }() + return ret +} + +func (o *Hackers) Group_ByGid(cred s.Ucred, req p.Request_Group_ByGid) <-chan p.Group { + o.lock.RLock() + ret := make(chan p.Group) + go func() { + defer o.lock.RUnlock() + defer close(ret) + + group := o.groupByGid(req.Gid, true) + if group.ID < 0 { + return + } + ret <- group + }() + return ret +} + +// note that the BYMEMBER call returns an empty members list +func (o *Hackers) Group_ByMember(cred s.Ucred, req p.Request_Group_ByMember) <-chan p.Group { + o.lock.RLock() + ret := make(chan p.Group) + go func() { + defer o.lock.RUnlock() + defer close(ret) + + uid := o.name2uid(req.Member) + if uid < 0 { + return + } + for _, name := range o.users[uid].groups { + group := o.groupByName(name, false) + if group.ID >= 0 { + ret <- group + } + } + }() + return ret +} + +func (o *Hackers) Group_All(cred s.Ucred, req p.Request_Group_All) <-chan p.Group { + o.lock.RLock() + ret := make(chan p.Group) + go func() { + defer o.lock.RUnlock() + defer close(ret) + + for name, _ := range o.groups { + group := o.groupByName(name, true) + if group.ID >= 0 { + ret <- group + } + } + }() + return ret +} diff --git a/src/nshd/hackers_git/db_pam.go b/src/nshd/hackers_git/db_pam.go new file mode 100644 index 0000000..2ef8d9a --- /dev/null +++ b/src/nshd/hackers_git/db_pam.go @@ -0,0 +1,105 @@ +// Copyright 2015 Luke Shumaker <lukeshu@sbcglobal.net>. +// +// This 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 2 of +// the License, or (at your option) any later version. +// +// The GNU General Public License's references to "object code" and +// "executables" are to be interpreted to also include the output of +// any document formatting or typesetting system, including +// intermediate and printed output. +// +// This software 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 manual; if not, see +// <http://www.gnu.org/licenses/>. + +package hackers_git + +import ( + "crypto/rand" + p "lukeshu.com/git/go/libnslcd.git/proto" + "math/big" + s "syscall" +) + +func (o *Hackers) PAM_Authentication(cred s.Ucred, req p.Request_PAM_Authentication) <-chan p.PAM_Authentication { + o.lock.RLock() + ret := make(chan p.PAM_Authentication) + go func() { + defer o.lock.RUnlock() + defer close(ret) + + uid := o.name2uid(req.UserName) + if uid < 0 { + return + } + + user := o.users[uid] + obj := p.PAM_Authentication{ + AuthenticationResult: p.NSLCD_PAM_AUTH_ERR, + UserName: "", + AuthorizationResult: p.NSLCD_PAM_AUTH_ERR, + AuthorizationError: "", + } + if check_password(req.Password, user.passwd.PwHash) { + obj.AuthenticationResult = p.NSLCD_PAM_SUCCESS + obj.AuthorizationResult = obj.AuthenticationResult + obj.UserName = user.passwd.Name + } + ret <- obj + }() + return ret +} + +func (o *Hackers) PAM_Authorization(cred s.Ucred, req p.Request_PAM_Authorization) <-chan p.PAM_Authorization { + o.lock.RLock() + ret := make(chan p.PAM_Authorization) + go func() { + defer o.lock.RUnlock() + defer close(ret) + + uid := o.name2uid(req.UserName) + if uid < 0 { + return + } + ret <- p.PAM_Authorization{ + Result: p.NSLCD_PAM_SUCCESS, + Error: "", + } + }() + return ret +} + +const alphabet = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789" + +var alphabet_len = big.NewInt(int64(len(alphabet))) + +func (o *Hackers) PAM_SessionOpen(cred s.Ucred, req p.Request_PAM_SessionOpen) <-chan p.PAM_SessionOpen { + ret := make(chan p.PAM_SessionOpen) + go func() { + defer close(ret) + + var sessionid [24]byte + for i := 0; i < len(sessionid); i++ { + bigint, err := rand.Int(rand.Reader, alphabet_len) + if err != nil { + return + } + sessionid[i] = alphabet[bigint.Int64()] + } + ret <- p.PAM_SessionOpen{SessionID: string(sessionid[:])} + }() + return ret +} + +func (o *Hackers) PAM_SessionClose(cred s.Ucred, req p.Request_PAM_SessionClose) <-chan p.PAM_SessionClose { + ret := make(chan p.PAM_SessionClose) + go close(ret) + return ret +} diff --git a/src/nshd/hackers_git/db_passwd.go b/src/nshd/hackers_git/db_passwd.go new file mode 100644 index 0000000..0be3910 --- /dev/null +++ b/src/nshd/hackers_git/db_passwd.go @@ -0,0 +1,86 @@ +// Copyright 2015 Luke Shumaker <lukeshu@sbcglobal.net>. +// +// This 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 2 of +// the License, or (at your option) any later version. +// +// The GNU General Public License's references to "object code" and +// "executables" are to be interpreted to also include the output of +// any document formatting or typesetting system, including +// intermediate and printed output. +// +// This software 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 manual; if not, see +// <http://www.gnu.org/licenses/>. + +package hackers_git + +import ( + p "lukeshu.com/git/go/libnslcd.git/proto" + s "syscall" +) + +/* Note that the output password hash value should be one of: + <empty> - no password set, allow login without password + ! - used to prevent logins + x - "valid" encrypted password that does not match any valid password + often used to indicate that the password is defined elsewhere + other - encrypted password, in crypt(3) format */ + +func (o *Hackers) Passwd_ByName(cred s.Ucred, req p.Request_Passwd_ByName) <-chan p.Passwd { + o.lock.RLock() + ret := make(chan p.Passwd) + go func() { + defer o.lock.RUnlock() + defer close(ret) + + uid := o.name2uid(req.Name) + if uid < 0 { + return + } + passwd := o.users[uid].passwd + passwd.PwHash = "x" // only put actual hashes in the Shadow DB + ret <- passwd + }() + return ret +} + +func (o *Hackers) Passwd_ByUID(cred s.Ucred, req p.Request_Passwd_ByUID) <-chan p.Passwd { + o.lock.RLock() + ret := make(chan p.Passwd) + go func() { + defer o.lock.RUnlock() + defer close(ret) + + user, found := o.users[req.UID] + if !found { + return + } + passwd := user.passwd + passwd.PwHash = "x" // only put actual hashes in the Shadow DB + ret <- passwd + }() + return ret +} + +func (o *Hackers) Passwd_All(cred s.Ucred, req p.Request_Passwd_All) <-chan p.Passwd { + o.lock.RLock() + ret := make(chan p.Passwd) + go func() { + defer o.lock.RUnlock() + defer close(ret) + + for _, user := range o.users { + passwd := user.passwd + passwd.PwHash = "x" // only put actual hashes in the Shadow DB + ret <- passwd + } + }() + return ret +} diff --git a/src/nshd/hackers_git/db_shadow.go b/src/nshd/hackers_git/db_shadow.go new file mode 100644 index 0000000..c2719b3 --- /dev/null +++ b/src/nshd/hackers_git/db_shadow.go @@ -0,0 +1,82 @@ +// Copyright 2015 Luke Shumaker <lukeshu@sbcglobal.net>. +// +// This 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 2 of +// the License, or (at your option) any later version. +// +// The GNU General Public License's references to "object code" and +// "executables" are to be interpreted to also include the output of +// any document formatting or typesetting system, including +// intermediate and printed output. +// +// This software 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 manual; if not, see +// <http://www.gnu.org/licenses/>. + +package hackers_git + +import ( + p "lukeshu.com/git/go/libnslcd.git/proto" + s "syscall" +) + +func (o *Hackers) Shadow_ByName(cred s.Ucred, req p.Request_Shadow_ByName) <-chan p.Shadow { + o.lock.RLock() + ret := make(chan p.Shadow) + go func() { + defer o.lock.RUnlock() + defer close(ret) + + if cred.Uid != 0 { + return + } + uid := o.name2uid(req.Name) + user := o.users[uid] + ret <- p.Shadow{ + Name: user.passwd.Name, + PwHash: user.passwd.PwHash, + LastChangeDate: -1, + MinDays: -1, + MaxDays: -1, + WarnDays: -1, + InactDays: -1, + ExpireDate: -1, + Flag: -1, + } + }() + return ret +} + +func (o *Hackers) Shadow_All(cred s.Ucred, req p.Request_Shadow_All) <-chan p.Shadow { + o.lock.RLock() + ret := make(chan p.Shadow) + go func() { + defer o.lock.RUnlock() + defer close(ret) + + if cred.Uid != 0 { + return + } + + for _, user := range o.users { + ret <- p.Shadow{ + Name: user.passwd.Name, + PwHash: user.passwd.PwHash, + LastChangeDate: -1, + MinDays: -1, + MaxDays: -1, + WarnDays: -1, + InactDays: -1, + ExpireDate: -1, + Flag: -1, + } + } + }() + return ret +} diff --git a/src/nshd/hackers_git/gid.go b/src/nshd/hackers_git/gid.go new file mode 100644 index 0000000..f7bbac7 --- /dev/null +++ b/src/nshd/hackers_git/gid.go @@ -0,0 +1,42 @@ +// Copyright 2015 Luke Shumaker <lukeshu@sbcglobal.net>. +// +// This 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 2 of +// the License, or (at your option) any later version. +// +// The GNU General Public License's references to "object code" and +// "executables" are to be interpreted to also include the output of +// any document formatting or typesetting system, including +// intermediate and printed output. +// +// This software 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 manual; if not, see +// <http://www.gnu.org/licenses/>. + +package hackers_git + +import "lukeshu.com/git/go/libgnulinux.git/getgr" + +func name2gid(name string) int32 { + gr, err := getgr.ByName(name) + if gr == nil || err != nil { + return -1 + } else { + return int32(gr.Gid) + } +} + +func gid2name(gid int32) (string, bool) { + gr, err := getgr.ByGid(gid) + if gr == nil || err != nil { + return "", false + } else { + return gr.Name, true + } +} diff --git a/src/nshd/hackers_git/hackers.go b/src/nshd/hackers_git/hackers.go new file mode 100644 index 0000000..b72698f --- /dev/null +++ b/src/nshd/hackers_git/hackers.go @@ -0,0 +1,121 @@ +// Copyright 2015 Luke Shumaker <lukeshu@sbcglobal.net>. +// +// This 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 2 of +// the License, or (at your option) any later version. +// +// The GNU General Public License's references to "object code" and +// "executables" are to be interpreted to also include the output of +// any document formatting or typesetting system, including +// intermediate and printed output. +// +// This software 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 manual; if not, see +// <http://www.gnu.org/licenses/>. + +// Package hackers_git is an nslcd_server Backend that speaks to +// hackers.git. +package hackers_git + +import ( + "lukeshu.com/git/go/libnslcd.git/proto" + "lukeshu.com/git/go/libnslcd.git/proto/server" + "lukeshu.com/git/go/libnslcd.git/systemd" + "lukeshu.com/git/go/libsystemd.git/sd_daemon/logger" + "path/filepath" + "sync" +) + +type user struct { + passwd nslcd_proto.Passwd + groups []string +} + +type Config struct { + Pam_password_prohibit_message string + Yamldir string +} + +type Hackers struct { + nslcd_server.NilBackend + Cfg Config + lock sync.RWMutex + + users map[int32]user + groups map[string]map[string]bool +} + +var _ nslcd_systemd.Backend = &Hackers{} +var _ nslcd_server.Backend = &Hackers{} + +func (o *Hackers) Init() error { + err := o.Reload() + if err != nil { + logger.Err("hackers.git: Could not initialize: %v", err) + return err + } + return nil +} + +func (o *Hackers) Close() { + logger.Info("hackers.git: Closing session") + o.lock.Lock() + defer o.lock.Unlock() + + o.users = make(map[int32]user, 0) + o.groups = make(map[string]map[string]bool) +} + +func (o *Hackers) Reload() error { + logger.Info("hackers.git: Loading session") + o.lock.Lock() + defer o.lock.Unlock() + + filenames, err := filepath.Glob(o.Cfg.Yamldir + "/*.yml") + if err != nil { + return err + } + o.users = make(map[int32]user, len(filenames)) + o.groups = make(map[string]map[string]bool) + for _, filename := range filenames { + logger.Debug("hackers.git: Loading YAML file: %s", filename) + + user, err := parse_user_yaml(filename) + if err != nil { + logger.Warning("hackers.git: -> File ignored: %v", err) + continue + } + for _, groupname := range user.groups { + o.add_user_to_group(user.passwd.Name, groupname) + } + user.passwd.PwHash = parse_user_password(user.passwd.HomeDir + "/.password") + o.users[user.passwd.UID] = user + logger.Debug("hackers.git: -> User %d(%s) added", user.passwd.UID, user.passwd.Name) + } + + return nil +} + +func (o *Hackers) name2uid(name string) int32 { + for uid, data := range o.users { + if data.passwd.Name == name { + return uid + } + } + return -1 +} + +func (o *Hackers) add_user_to_group(username string, groupname string) { + group, found := o.groups[groupname] + if !found { + group = make(map[string]bool) + o.groups[groupname] = group + } + group[username] = true +} diff --git a/src/nshd/hackers_git/hackers_parse.go b/src/nshd/hackers_git/hackers_parse.go new file mode 100644 index 0000000..d5370eb --- /dev/null +++ b/src/nshd/hackers_git/hackers_parse.go @@ -0,0 +1,157 @@ +// Copyright 2015 Luke Shumaker <lukeshu@sbcglobal.net>. +// +// This 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 2 of +// the License, or (at your option) any later version. +// +// The GNU General Public License's references to "object code" and +// "executables" are to be interpreted to also include the output of +// any document formatting or typesetting system, including +// intermediate and printed output. +// +// This software 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 manual; if not, see +// <http://www.gnu.org/licenses/>. + +package hackers_git + +import ( + "fmt" + yaml "gopkg.in/yaml.v2" + "io/ioutil" + "lukeshu.com/git/go/libsystemd.git/sd_daemon/logger" + "os" + "path" + "strconv" + "strings" +) + +func filename2uid(filename string) int32 { + basename := path.Base(filename) + parts := strings.SplitN(basename, ".", 2) + if len(parts) != 2 || parts[1] != "yml" { + return -1 + } + uid, err := strconv.ParseInt(parts[0], 10, 32) + if err != nil { + return -1 + } + return int32(uid) +} + +var usersGid = name2gid("users") + +func parse_user_yaml(filename string) (ret user, err error) { + ret.passwd.UID = filename2uid(filename) + + if ret.passwd.UID < 0 { + err = fmt.Errorf("Invalid user filename: %q", filename) + return + } + file, err := os.Open(filename) + if err != nil { + return + } + contents, err := ioutil.ReadAll(file) + if err != nil { + return + } + var _data interface{} + err = yaml.Unmarshal(contents, &_data) + if err != nil { + return + } + data, isMap := _data.(map[interface{}]interface{}) + errs := []string{} + if !isMap { + errs = append(errs, "root node is not a map") + } else { + if iface, isSet := data["username"]; !isSet { + errs = append(errs, "\"username\" is not set") + } else if str, isTyp := iface.(string); !isTyp { + errs = append(errs, "\"username\" is not a string") + } else { + ret.passwd.Name = str + ret.passwd.HomeDir = "/home/" + str + } + + if iface, isSet := data["fullname"]; !isSet { + errs = append(errs, "\"fullname\" is not set") + } else if str, isTyp := iface.(string); !isTyp { + errs = append(errs, "\"fullname\" is not a string") + } else { + ret.passwd.GECOS = str + } + + if iface, isSet := data["shell"]; !isSet { + errs = append(errs, "\"shell\" is not set") + } else if str, isTyp := iface.(string); !isTyp { + errs = append(errs, "\"shell\" is not a string") + } else { + ret.passwd.Shell = str + } + + if iface, isSet := data["groups"]; !isSet { + ret.groups = make([]string, 0) + } else if ary, isTyp := iface.([]interface{}); !isTyp { + errs = append(errs, "\"groups\" is not an array") + } else { + groups := make(map[string]bool, len(ary)) + e := false + for _, iface := range ary { + if str, isTyp := iface.(string); !isTyp { + errs = append(errs, "\"group\" item is not an array") + e = true + break + } else { + groups[str] = true + } + } + if !e { + ret.groups = set2list(groups) + } + } + } + if len(errs) > 0 { + err = &yaml.TypeError{Errors: errs} + } + + ret.passwd.PwHash = "!" + ret.passwd.GID = usersGid + + return +} + +func parse_user_password(filename string) (hash string) { + hash = "!" + file, err := os.Open(filename) + if err != nil { + logger.Debug("hackers.git: %v", err) + return + } + contents, err := ioutil.ReadAll(file) + if err != nil { + logger.Debug("hackers.git: Error while reading: %q: %v", filename, err) + return + } + lines := strings.Split(string(contents), "\n") + switch len(lines) { + case 1: + hash = lines[0] + case 2: + if lines[1] == "" { + hash = lines[0] + } else { + logger.Debug("hackers.git: Invalid password format in file: %q", filename) + } + default: + logger.Debug("hackers.git: Invalid password format in file: %q", filename) + } + return +} diff --git a/src/nshd/hackers_git/set.go b/src/nshd/hackers_git/set.go new file mode 100644 index 0000000..dc1d443 --- /dev/null +++ b/src/nshd/hackers_git/set.go @@ -0,0 +1,32 @@ +// Copyright 2015 Luke Shumaker <lukeshu@sbcglobal.net>. +// +// This 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 2 of +// the License, or (at your option) any later version. +// +// The GNU General Public License's references to "object code" and +// "executables" are to be interpreted to also include the output of +// any document formatting or typesetting system, including +// intermediate and printed output. +// +// This software 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 manual; if not, see +// <http://www.gnu.org/licenses/>. + +package hackers_git + +func set2list(set map[string]bool) []string { + list := make([]string, len(set)) + i := uint(0) + for item, _ := range set { + list[i] = item + i++ + } + return list +} diff --git a/src/nshd/main.go b/src/nshd/main.go new file mode 100644 index 0000000..6871518 --- /dev/null +++ b/src/nshd/main.go @@ -0,0 +1,37 @@ +// Copyright 2015 Luke Shumaker <lukeshu@sbcglobal.net>. +// +// This 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 2 of +// the License, or (at your option) any later version. +// +// The GNU General Public License's references to "object code" and +// "executables" are to be interpreted to also include the output of +// any document formatting or typesetting system, including +// intermediate and printed output. +// +// This software 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 manual; if not, see +// <http://www.gnu.org/licenses/>. + +// Command nshd is an implementation of nslcd that talks to hackers.git instead of LDAP. +package main + +import ( + "lukeshu.com/git/go/libnslcd.git/systemd" + "nshd/hackers_git" + "os" +) + +func main() { + backend := &hackers_git.Hackers{Cfg: hackers_git.Config{ + Pam_password_prohibit_message: "", + Yamldir: "/var/cache/parabola-hackers/users", + }} + os.Exit(int(nslcd_systemd.Main(backend))) +} diff --git a/test/.gitignore b/test/.gitignore new file mode 100644 index 0000000..09230a9 --- /dev/null +++ b/test/.gitignore @@ -0,0 +1 @@ +/runner diff --git a/test/runner.c b/test/runner.c new file mode 100644 index 0000000..110819d --- /dev/null +++ b/test/runner.c @@ -0,0 +1,168 @@ +/* Copyright (C) 2015 Luke Shumaker <lukeshu@sbcglobal.net> + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 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 + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA + * 02110-1301 USA + */ +#define _GNU_SOURCE +#include <stdio.h> +#include <stdlib.h> +#include <sys/socket.h> +#include <sys/un.h> +#include <error.h> +#include <errno.h> +#include <stdarg.h> +#include <signal.h> +#include <unistd.h> +#include <sys/stat.h> +#include <sys/wait.h> + +#define _(s) s + +const char *xgetenv(const char *name, const char *unset) { + const char *val = getenv(name); + if (!val) + val = unset; + return val; +} + +char *xasprintf(const char *format, ...) { + va_list arg; + int len; + char *str = NULL; + + va_start(arg, format); + len = vasprintf(&str, format, arg); + va_end(arg); + + if (len < 0) + error(EXIT_FAILURE, errno, _("Could not allocate memory in vasprintf")); + + return str; +} + +#define xasprintfa(...) (__extension__ ({ char *heap = xasprintf(__VA_ARGS__); char *stack = strdupa(heap); free(heap); stack; })) + +int pid = -1; +void +sigchld_handler(int sig __attribute__((__unused__))) { + int status; + pid = waitpid(pid, &status, WNOHANG); + int exited = WEXITSTATUS(status); + error(exited, 0, "%ld exited with status %d", (long)pid, exited); + exit(0); +} + +union addr { + struct sockaddr gen; + struct sockaddr_un un; +}; + +int new_unix_sock(const char *filename, int type) { + union addr addr; + addr.un.sun_family = AF_UNIX; + strcpy(addr.un.sun_path, filename); + + int sock = socket(AF_UNIX, type, 0); + if (sock < 0) + error(EXIT_FAILURE, errno, "socket(%d, %d)", AF_UNIX, type); + unlink(filename); + if (bind(sock, &addr.gen, sizeof(addr))) + error(EXIT_FAILURE, errno, "bind(%d, sockaddr(\"%s\"))", sock, filename); + switch (type) { + case SOCK_STREAM: + case SOCK_SEQPACKET: + if (listen(sock, 5)) + error(EXIT_FAILURE, errno, "listen(%d /* \"%s\" */, %d)", sock, filename, 5); + break; + case SOCK_DGRAM: + break; + default: + error(EXIT_FAILURE, errno, "new_unix_sock: Unrecognized type: %d", type); + } + return sock; +} + +char *cmdname = "nshd_runner"; +const char *notify_sockname = "notify.sock"; +const char *nslcd_sockname = "nslcd.sock"; +void cleanup(void) { + if (nslcd_sockname) + unlink(nslcd_sockname); + if (notify_sockname) + unlink(notify_sockname); + fprintf(stderr, "%s: Exiting\n", cmdname); +} + +int main(int argc, char *argv[]) { + cmdname = argv[0]; + if (argc != 2) { + error(2, 0, _("Usage: %s NSHD_PROGRAM"), argv[0]); + } + + atexit(&cleanup); + int nslcd_sock = new_unix_sock(nslcd_sockname , SOCK_STREAM); + int notify_sock = new_unix_sock(notify_sockname, SOCK_DGRAM ); + + struct sigaction act; + sigemptyset(&act.sa_mask); + act.sa_flags = SA_RESTART; + act.sa_handler = sigchld_handler; + if (sigaction(SIGCHLD, &act, 0)) + error(EXIT_FAILURE, errno, "sigaction"); + + + pid = fork(); + switch (pid) { + case -1: + error(EXIT_FAILURE, errno, "fork"); + case 0: + close(notify_sock); + dup2(nslcd_sock, 3); + if (nslcd_sock != 3) + close(nslcd_sock); + pid = getpid(); + setenv("LISTEN_PID", xasprintfa("%ld", (long)pid), 1); + setenv("LISTEN_FDS", "1", 1); + setenv("NOTIFY_SOCKET", notify_sockname, 1); + execl(argv[1], argv[1], NULL); + error(EXIT_FAILURE, errno, "execl"); + } + + while (1) { + union addr client_addr; + socklen_t client_size; + char buf[4097]; + ssize_t bytes_read = recvfrom(notify_sock, buf, sizeof(buf)-1, 0, &client_addr.gen, &client_size); + if (bytes_read < 1) + error(EXIT_FAILURE, errno, "recvfrom"); + if (buf[bytes_read-1] != '\n') { + buf[bytes_read] = '\n'; + bytes_read++; + } + ssize_t bytes_written = 0; + while (bytes_written < bytes_read) { + ssize_t n = write(2, &(buf[bytes_written]), bytes_read-bytes_written); + if (n < 0) { + bytes_written = -1; + break; + } + bytes_written += n; + } + if (bytes_written < 0) + error(EXIT_FAILURE, errno, "write"); + } + error(EXIT_FAILURE, 0, "not reached"); + return EXIT_FAILURE; +} |