diff options
-rw-r--r-- | .gitignore | 8 | ||||
-rw-r--r-- | README.md | 76 | ||||
-rw-r--r-- | bin/common.rb | 22 | ||||
-rwxr-xr-x | bin/meta-cat | 6 | ||||
-rwxr-xr-x | bin/meta-check | 44 | ||||
-rwxr-xr-x | bin/meta-normalize-stdio | 171 | ||||
-rwxr-xr-x | bin/pacman-make-keyring | 150 | ||||
-rwxr-xr-x | bin/pgp-list-keyids | 21 | ||||
-rwxr-xr-x | bin/postfix-generate-virtual-map | 18 | ||||
-rwxr-xr-x | bin/ssh-list-authorized-keys | 25 | ||||
-rwxr-xr-x | bin/uid-map | 8 | ||||
-rw-r--r-- | parabola-hackers.yml | 40 |
12 files changed, 588 insertions, 1 deletions
@@ -1,5 +1,5 @@ /pkg -/bin +/bin/nshd /src/*.*/ /nshd.service /nshd.socket @@ -7,3 +7,9 @@ .tmp.* /LICENSE.*.txt *.o + +*~ +output/ +cache/ +parabola-keyring-*.tar.gz + 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/bin/common.rb b/bin/common.rb new file mode 100644 index 0000000..91e14be --- /dev/null +++ b/bin/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/bin/meta-cat b/bin/meta-cat new file mode 100755 index 0000000..e6b9edd --- /dev/null +++ b/bin/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/bin/meta-check b/bin/meta-check new file mode 100755 index 0000000..4a2981e --- /dev/null +++ b/bin/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/bin/meta-normalize-stdio b/bin/meta-normalize-stdio new file mode 100755 index 0000000..5611ae6 --- /dev/null +++ b/bin/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/bin/pacman-make-keyring b/bin/pacman-make-keyring new file mode 100755 index 0000000..589984d --- /dev/null +++ b/bin/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/bin/pgp-list-keyids b/bin/pgp-list-keyids new file mode 100755 index 0000000..9682b1a --- /dev/null +++ b/bin/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/bin/postfix-generate-virtual-map b/bin/postfix-generate-virtual-map new file mode 100755 index 0000000..d5c2d21 --- /dev/null +++ b/bin/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/bin/ssh-list-authorized-keys b/bin/ssh-list-authorized-keys new file mode 100755 index 0000000..5fb1ea1 --- /dev/null +++ b/bin/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/bin/uid-map b/bin/uid-map new file mode 100755 index 0000000..10c3fac --- /dev/null +++ b/bin/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/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" |