-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathopenvpn-u2f-verify
executable file
·176 lines (162 loc) · 7.29 KB
/
openvpn-u2f-verify
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
#!/bin/sh -u
# openvpn-u2f-setup/openvpn-u2f-verify -- verify U2F keys on the OpenVPN server
# Copyright (C) 2021, Walter Doekes, OSSO B.V.
#
# This file is part of openvpn-u2f-setup. It 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, version 3
# or any later version.
#
# For documentation, sources and the full license, go to:
# https://github.com/ossobv/openvpn-u2f-setup
#
# This script needs to be installed as 'auth-user-pass-verify' script on
# the openvpn server.
# This must be the same on both sides for each new challenge. It's prefixed
# with "pam://" because the u2f-server is picky and requires either that or
# "http(s)://".
ORIGIN=pam://openvpn-server
# This must be the same for the lifetime of the key handle
# (registration). If the appId is different, the u2host application
# refuses to sign (-6, authenticator error).
APPID=openvpn
# Here we must choose key handles
if ! echo -n "$X509_0_CN" | grep -q '[A-Za-z0-9_][A-Za-z0-9_.-]*$'; then
echo "Certificate name contains unsafe characters: $X509_0_CN" >&2
exit 1
fi
handles_path=/etc/openvpn/u2f/$X509_0_CN
# In handles_path on the server, we expect two files:
# - /etc/openvpn/u2f/your-common-name/keyhandle.dat
# - /etc/openvpn/u2f/your-common-name/userkey.dat
# They are generated using:
# - u2f-server -a register -o $ORIGIN -i $APPID -k keyhandle.dat -p userkey.dat
# - (take stdout "challenge", feed to u2f-host)
# - u2f-host -a register -o $ORIGIN
# - (take stdout "registrationData", feed back to u2f-server)
# The u2f-host(1) command needs to be run on the user machine, where the
# YubiKey is inserted. The user will need the contents of 'keyhandle.dat' for
# their auth-user handling. (See: systemd-openvpn-u2f-auth)
# Allow everyone who does not have a path/configuration?
if ! test -d "$handles_path"; then
if true; then
echo "INFO: No U2F config in $handles_path -- disallowing.." >&2
exit 1
else
echo "WARNING: No U2F config in $handles_path -- allowing for now.." >&2
exit 0 # NO CONFIG => SINGLE FACTOR ALLOWED
fi
fi
if test -f "$handles_path/allow-without-u2f"; then
echo "INFO: Allowing because $handles_path/allow-without-u2f" >&2
exit 0
fi
# Read username and password:
# auth-user-pass ... via-file will pass user/pass through a file on $1.
exec 3<"$1" || exit 1
read username <&3 # $key_handle
read password <&3 # $timestamp/$response
# -- BEGIN OPTIONAL YUBICO OTP -----------------------------------------
# Two times Yubico as fallback? For those that to not have U2F support?
# Place a "yubico.id" file in the handles path with the handle in it:
# # sed -e 's/.\{32\}$//' >"$handles_path/yubico.id" # PRESS + ^D
# This *does* require access to the internet (and a working curl), so
# this is not the recommended method.
if test "${#username}" = "${#password}" -a -f "$handles_path/yubico.id"; then
# The yubico identifier is the first 2..16 chars. The rest should be 32
# chars (in modhex).
yubico_id=$(cat "$handles_path/yubico.id")
otp1=${username#$yubico_id}
otp2=${password#$yubico_id}
if echo "$otp1$otp2" | grep -qE '^[a-z]{64}$'; then
# Logic now dicates that we have two 32 byte sanitized otps, that we
# can safely send for validation.
_yubico_verify() {
set -u
local yubico_client_id=1 # needed?
# api{,2,3,4,5}.yubico.com
local yubico_uri="https://$1/wsapi/2.0/verify"
local otp="$2"
local nonce=$(dd if=/dev/urandom bs=20 count=1 2>/dev/null |
sha1sum | cut -f1 -d' ')
# We skip adding a signature for yubico_client_id 1. The server
# does not mandate it at the moment.
local url="$yubico_uri?id=$yubico_client_id&otp=$otp&nonce=$nonce"
echo "DEBUG: yubico curl: $url" >&2
resp=$(timeout 3s curl --fail -sS "$url") || exit 1
resp=$(echo "$resp" | tr -d '\r') # no CR in EOL please
if echo "$resp" | grep -q "^otp=$otp$" &&
echo "$resp" | grep -q "^nonce=$nonce$" &&
echo "$resp" | grep -q "^status=OK$"; then
echo "DEBUG:" $resp >&2
return
fi
echo "REJECTED:" $resp >&2
false
}
yubico_verify() {
ok=0
for host in api3.yubico.com api5.yubico.com; do
_yubico_verify "$host" "$1" && ok=1 && break
done
test $ok -eq 1
}
if yubico_verify "$yubico_id$otp1" &&
yubico_verify "$yubico_id$otp2"; then
echo "DEBUG: 2x PASS" >&2
exit 0
fi
exit 1
fi
fi
# -- END OPTIONAL YUBICO OTP -------------------------------------------
# Get values:
key_handle=$(cat "$handles_path/keyhandle.dat") || exit 1
timestamp=${password%%/*} # extract timestamp from password
response=${password#*/} # extract response from password
# Check that the timestamp is exactly 10 digits, calculate a delta and
# create the same challenge as the client created. We're using a 32
# bytes challenge, encoded in urlsafe-base64. The u2f-server(1) is picky
# about this.
echo "$timestamp" | grep -qE '^[0-9]{10}$' || exit 1 # 10-digit unixtime
ltimestamp=$(date +'%s')
latency=$(( ltimestamp - timestamp )) # how old is the challenge?
b64challenge=$( # <timestamp>Z<timestamp>Z<timestamp> = 32 octets
echo -n "${timestamp}Z${timestamp}Z${timestamp}" |
base64 -w0 | tr '+' '-' | tr '/' '_' | tr -d '=') || exit 1
# Validate the handle:
# TODO: we could allow multiple key_handles for a single user
test "$username" != "$key_handle" && echo BAD_USER_HANDLE && exit 1
# Validate the timestamp (=challenge) to avoid replay attacks. We allow 20
# seconds of room due to network latency and the optional administrator
# password the the user _also_ has to enter.
test "$latency" -lt -2 &&
echo "FUTURE_TIMESTAMP [local=$ltimestamp remote=$timestamp]" && exit 1
test "$latency" -gt 20 &&
echo "OLD_TIMESTAMP [local=$ltimestamp remote=$timestamp]" && exit 1
echo "(Handle path $handles_path, latency $latency)" >&2
# NOTE: we did not include the JSON verbatim, because the user/password
# fields sent by openvpn are limited in size. That means that creating
# the client_data is slightly more fragile.
#
# NOTE: This is the hashed bit. This must be **exactly** what u2f-host(1) feeds
# to the YubiKey, in this order, including whitespace around the braces.a
#
origin=$(echo "$ORIGIN" | sed -e 's#/#\\/#g')
client_data="{ \"challenge\": \"$b64challenge\", \"origin\":\
\"$origin\", \"typ\": \"navigator.id.getAssertion\" }"
client_data=$(echo -n "$client_data" | base64 -w0)
# Build validation request for u2f-server(1), including the challenge
# and response:
request="{\"signatureData\": \"$response\", \"clientData\": \"$client_data\",\
\"keyHandle\": \"$key_handle\"}"
# The u2f-server(1) luckily allows us to pre-specify the challenge (add
# '-d' to u2f-server for debug info):
echo "$request" |
u2f-server -a authenticate -o "$ORIGIN" -i "$APPID" \
-k "$handles_path/keyhandle.dat" -p "$handles_path/userkey.dat" \
-c "$b64challenge"
u2f_auth_ok=$?
test $u2f_auth_ok -ne 0 && echo "BAD_U2F" && exit 1
# Last line. Only exit with success if auth was good:
test $u2f_auth_ok -eq 0