openssl: Improve cert generation and generate vpn certs

* use easyrsa for cert generation/management
* generate vpn requirements (ca, crt, dh)
* remove `node` requirement for generating jwt kid
* fix root ca cert generation
* fix haproxy certificate generation

Signed-off-by: Will Boyce <will@resin.io>
This commit is contained in:
Will Boyce 2018-09-23 20:08:45 +01:00 committed by Will Boyce
parent 3a0bed7f43
commit 971bc55bb7
No known key found for this signature in database
GPG Key ID: 4E27760A85903481
12 changed files with 236 additions and 209 deletions

View File

@ -134,6 +134,10 @@ services:
SENTRY_DSN:
VPN_HAPROXY_USEPROXYPROTOCOL: 'true'
VPN_SERVICE_API_KEY: ${OPENBALENA_VPN_SERVICE_API_KEY}
VPN_OPENVPN_CA_CRT: ${OPENBALENA_VPN_CA}
VPN_OPENVPN_SERVER_CRT: ${OPENBALENA_VPN_SERVER_CRT}
VPN_OPENVPN_SERVER_KEY: ${OPENBALENA_VPN_SERVER_KEY}
VPN_OPENVPN_SERVER_DH: ${OPENBALENA_VPN_SERVER_DH}
# FIXME: remove all of the following
API_SERVICE_API_KEY: __unused__
PROXY_SERVICE_API_KEY: __unused__5
@ -176,6 +180,8 @@ services:
- img.${OPENBALENA_HOST_NAME}
environment:
BALENA_ROOT_CA: ${OPENBALENA_ROOT_CA}
BALENA_HAPROXY_CRT: ${OPENBALENA_ROOT_CRT}
BALENA_HAPROXY_KEY: ${OPENBALENA_ROOT_KEY}
HAPROXY_HOSTNAME: ${OPENBALENA_HOST_NAME}
# FIXME: remove the following

View File

@ -1,9 +1,10 @@
#!/bin/sh
#!/bin/bash -eu
CA_B64="$BALENA_ROOT_CA"
CA_FILE=/etc/ssl/private/root.chain.pem
mkdir -p $(dirname "$CA_FILE")
echo "$CA_B64" | base64 -d >"$CA_FILE"
exec haproxy -f /usr/local/etc/haproxy/haproxy.cfg
HAPROXY_CHAIN=/etc/ssl/private/open-balena.pem
mkdir -p "$(dirname "${HAPROXY_CHAIN}")"
(
echo "${BALENA_HAPROXY_CRT}" | base64 -d
echo "${BALENA_HAPROXY_KEY}" | base64 -d
echo "${BALENA_ROOT_CA}" | base64 -d
) > "${HAPROXY_CHAIN}"
exec haproxy -f /usr/local/etc/haproxy/haproxy.cfg

View File

@ -47,7 +47,7 @@ backend redirect-to-https-in
frontend https-in
mode http
option forwardfor
bind 127.0.0.1:444 ssl crt /etc/ssl/private/root.chain.pem accept-proxy
bind 127.0.0.1:444 ssl crt /etc/ssl/private/open-balena.pem accept-proxy
reqadd X-Forwarded-Proto:\ https
acl host_api hdr_dom(host) -i "api.${HAPROXY_HOSTNAME}"

View File

@ -1,79 +0,0 @@
'use strict';
var crypto = require('crypto');
var fs = require('fs');
var base32 = (function() {
// Extracted from https://github.com/chrisumbel/thirty-two
// to avoid having to install packages for this script.
var charTable = "ABCDEFGHIJKLMNOPQRSTUVWXYZ234567";
var byteTable = [
0xff, 0xff, 0x1a, 0x1b, 0x1c, 0x1d, 0x1e, 0x1f,
0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
0xff, 0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06,
0x07, 0x08, 0x09, 0x0a, 0x0b, 0x0c, 0x0d, 0x0e,
0x0f, 0x10, 0x11, 0x12, 0x13, 0x14, 0x15, 0x16,
0x17, 0x18, 0x19, 0xff, 0xff, 0xff, 0xff, 0xff,
0xff, 0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06,
0x07, 0x08, 0x09, 0x0a, 0x0b, 0x0c, 0x0d, 0x0e,
0x0f, 0x10, 0x11, 0x12, 0x13, 0x14, 0x15, 0x16,
0x17, 0x18, 0x19, 0xff, 0xff, 0xff, 0xff, 0xff
];
function quintetCount(buff) {
var quintets = Math.floor(buff.length / 5);
return buff.length % 5 == 0 ? quintets: quintets + 1;
}
return function(plain) {
if (!Buffer.isBuffer(plain)) {
plain = new Buffer(plain);
}
var i = 0;
var j = 0;
var shiftIndex = 0;
var digit = 0;
var encoded = new Buffer(quintetCount(plain) * 8);
/* byte by byte isn't as pretty as quintet by quintet but tests a bit
faster. will have to revisit. */
while(i < plain.length) {
var current = plain[i];
if(shiftIndex > 3) {
digit = current & (0xff >> shiftIndex);
shiftIndex = (shiftIndex + 5) % 8;
digit = (digit << shiftIndex) | ((i + 1 < plain.length) ?
plain[i + 1] : 0) >> (8 - shiftIndex);
i++;
} else {
digit = (current >> (8 - (shiftIndex + 5))) & 0x1f;
shiftIndex = (shiftIndex + 5) % 8;
if(shiftIndex == 0) i++;
}
encoded[j] = charTable.charCodeAt(digit);
j++;
}
for (i = j; i < encoded.length; i++) {
encoded[i] = 0x3d; //'='.charCodeAt(0)
}
return encoded;
}
})();
function joseKeyId(der) {
var hasher = crypto.createHash('sha256');
hasher.update(der);
var b32 = base32(hasher.digest().slice(0, 30)).toString('ascii');
var chunks = [];
for (var i = 0; i < b32.length; i += 4) {
chunks.push(b32.substr(i, 4));
}
return chunks.join(':');
}
var derFilePath = process.argv[2];
var der = fs.readFileSync(derFilePath);
process.stdout.write(joseKeyId(der));

32
scripts/gen-root-ca Executable file
View File

@ -0,0 +1,32 @@
#!/bin/bash -eu
usage() {
echo "usage: $0 COMMON_NAME [OUT]"
echo
echo " COMMON_NAME the domain name the certificate is valid for, eg. example.com"
echo " OUT path to output directory generated files will be placed in"
echo
}
if [ -z "$1" ]; then
usage
exit 1
fi
CMD="$(realpath "$0")"
DIR="$(dirname "${CMD}")"
CN="$1"
OUT="$(realpath "${2:-.}")"
source "${DIR}/ssl-common.sh"
# Create a secret key and CA file for the self-signed CA
"$easyrsa_bin" --pki-dir="${ROOT_PKI}" init-pki 2>/dev/null
"$easyrsa_bin" --pki-dir="${ROOT_PKI}" --days="${CA_EXPIRY_DAYS}" --req-cn="ca.${CN}" build-ca nopass 2>/dev/null
ROOT_CA="${ROOT_PKI}/ca.crt"
echo "ROOT_CA=${ROOT_CA//$OUT/\$OUT}"
# update indexes and generate CRLs
"$easyrsa_bin" --pki-dir="${ROOT_PKI}" update-db 2>/dev/null
"$easyrsa_bin" --pki-dir="${ROOT_PKI}" gen-crl 2>/dev/null

View File

@ -1,55 +0,0 @@
#!/bin/sh
set -e
CN=$1
OUT=${2:-.}
CA="${CN}-ca"
CA_FILE="${OUT}/ca"
CN_FILE="${OUT}/root"
CN_EXPIRY_DAYS=730
CA_EXPIRY_DAYS=3650
usage() {
echo "usage: $0 HOST_NAME [OUT]"
echo
echo " HOST_NAME the domain name the certificate is valid for, eg. example.com"
echo " OUT path to output directory generated files will be placed in"
echo
}
if [ -z "$CN" ]; then
usage
exit 1
fi
cat > "${CN_FILE}.v3.ext" <<STR
authorityKeyIdentifier = keyid,issuer
basicConstraints = CA:FALSE
keyUsage = digitalSignature, nonRepudiation, keyEncipherment, dataEncipherment
subjectAltName = @alt_names
[alt_names]
DNS.1 = *.${CN}
STR
# Create a secret key and CA file for the self-signed CA
openssl genrsa -out "${CA_FILE}.key" 2048 2>/dev/null
openssl req -x509 -new -nodes -sha256 -days $CA_EXPIRY_DAYS -key "${CA_FILE}.key" -subj "/CN=${CA}" -out "${CA_FILE}.pem" 2>/dev/null
# Create a secret key and Certificate Signing Request (CSR) for the domain
openssl genrsa -out "${CN_FILE}.key" 2048 2>/dev/null
openssl req -new -key "${CN_FILE}.key" -subj "/CN=${CN}" -out "${CN_FILE}.csr" 2>/dev/null
# Sign the request with the self-signed CA and extension file
openssl x509 -req -sha256 -days $CN_EXPIRY_DAYS -in "${CN_FILE}.csr" -CA "${CA_FILE}.pem" -CAkey "${CA_FILE}.key" -CAcreateserial -out "${CN_FILE}.pem" -extfile "${CN_FILE}.v3.ext" 2>/dev/null
# Create the custom certificate chain file
cat "${CN_FILE}.pem" "${CA_FILE}.pem" "${CN_FILE}.key" >"${CN_FILE}.chain.pem"
# Cleanup
rm "${CA_FILE}.key" "${CA_FILE}.pem" "${CN_FILE}.key" "${CN_FILE}.pem" "${CN_FILE}.csr" "${CN_FILE}.v3.ext"
echo "ROOT_CA=${CN_FILE}.chain.pem"

33
scripts/gen-root-cert Executable file
View File

@ -0,0 +1,33 @@
#!/bin/bash -eu
usage() {
echo "usage: $0 COMMON_NAME [OUT]"
echo
echo " COMMON_NAME the domain name the certificate is valid for, eg. example.com"
echo " OUT path to output directory generated files will be placed in"
echo
}
if [ -z "$1" ]; then
usage
exit 1
fi
CMD="$(realpath "$0")"
DIR="$(dirname "${CMD}")"
CN="$1"
OUT="$(realpath "${2:-.}")"
source "${DIR}/ssl-common.sh"
# generate default CSR and sign (root + wildcard)
"$easyrsa_bin" --pki-dir="${ROOT_PKI}" --days="${CRT_EXPIRY_DAYS}" --subject-alt-name="DNS:${CN}" build-server-full "*.${CN}" nopass 2>/dev/null
ROOT_CRT="${ROOT_PKI}"'/issued/*.'"${CN}"'.crt'
ROOT_KEY="${ROOT_PKI}"'/private/*.'"${CN}"'.key'
echo "ROOT_CRT=${ROOT_CRT//$OUT/\$OUT}"
echo "ROOT_KEY=${ROOT_KEY//$OUT/\$OUT}"
# update indexes and generate CRLs
"$easyrsa_bin" --pki-dir="${ROOT_PKI}" update-db 2>/dev/null
"$easyrsa_bin" --pki-dir="${ROOT_PKI}" gen-crl 2>/dev/null

View File

@ -1,42 +1,42 @@
#!/bin/sh
set -e
CMD=$0
DIR=$(dirname "$CMD")
CN=$1
OUT=${2:-.}
CERT_FILE="${OUT}/token-auth"
EXPIRY_DAYS=730
#!/bin/bash -eu
usage() {
echo "usage: $0 HOST_NAME [OUT]"
echo "usage: $0 COMMON_NAME [OUT]"
echo
echo " HOST_NAME the domain name the certificate is valid for, eg. example.com"
echo " OUT path to output directory generated files will be placed in"
echo " COMMON_NAME the domain name the certificate is valid for, eg. example.com"
echo " OUT path to output directory generated files will be placed in"
echo
}
keyid() {
# FIXME: do this in bash or python, not node
nodejs "${DIR}/_keyid.js" "$1"
}
if [ -z "$CN" ]; then
if [ -z "$1" ]; then
usage
exit 1
fi
openssl ecparam -name prime256v1 -genkey -noout -out "${CERT_FILE}.pem" 2>/dev/null
openssl req -x509 -new -nodes -days "${EXPIRY_DAYS}" -key "${CERT_FILE}.pem" -subj "/CN=api.${CN}" -out "${CERT_FILE}.crt" 2>/dev/null
openssl ec -in "${CERT_FILE}.pem" -pubout -outform DER -out "${CERT_FILE}".der 2>/dev/null
keyid "${CERT_FILE}".der >"${CERT_FILE}".kid
CMD="$(realpath "$0")"
DIR="$(dirname "${CMD}")"
# Cleanup
rm "${CERT_FILE}.der"
CN="$1"
OUT="$(realpath "${2:-.}")"
echo "PUB=${CERT_FILE}.crt"
echo "KEY=${CERT_FILE}.pem"
echo "KID=${CERT_FILE}.kid"
source "${DIR}/ssl-common.sh"
keyid() {
local der="$(openssl ec -in "$1" -pubout -outform DER 2>/dev/null)"
python -c "import sys as S; from base64 import b32encode as B; import hashlib as H; h = H.sha256(); h.update(S.argv[1].encode('ascii')); s = B(h.digest()[:30]).decode('ascii'); S.stdout.write(':'.join([s[i:i+4] for i in range(0, len(s), 4)]))" "${der}"
}
# generate api CSR and sign
"$easyrsa_bin" --pki-dir="${ROOT_PKI}" --days=730 --use-algo=ec --curve=prime256v1 build-server-full "api.${CN}" nopass 2>/dev/null
JWT_CRT="${ROOT_PKI}/issued/api.${CN}.crt"
JWT_KEY="${ROOT_PKI}/private/api.${CN}.key"
echo "JWT_CRT=${JWT_CRT//$OUT/\$OUT}"
echo "JWT_KEY=${JWT_KEY//$OUT/\$OUT}"
# update indexes and generate CRLs
"$easyrsa_bin" --pki-dir="${ROOT_PKI}" update-db 2>/dev/null
"$easyrsa_bin" --pki-dir="${ROOT_PKI}" gen-crl 2>/dev/null
# generate key ID
JWT_KID="$(keyid "${JWT_CRT}")"
echo "JWT_KID=${JWT_KID//$OUT/\$OUT}"

52
scripts/gen-vpn-certs Executable file
View File

@ -0,0 +1,52 @@
#!/bin/bash -eu
usage() {
echo "usage: $0 COMMON_NAME [OUT]"
echo
echo " COMMON_NAME the domain name the certificate is valid for, eg. example.com"
echo " OUT path to output directory generated files will be placed in"
echo
}
if [ -z "$1" ]; then
usage
exit 1
fi
CMD="$(realpath "$0")"
DIR="$(dirname "${CMD}")"
CN="$1"
OUT="$(realpath "${2:-.}")"
source "${DIR}/ssl-common.sh"
VPN_PKI="$(realpath "${OUT}/vpn")"
# generate VPN sub-CA
"$easyrsa_bin" --pki-dir="${VPN_PKI}" init-pki 2>/dev/null
"$easyrsa_bin" --pki-dir="${VPN_PKI}" --days="${CA_EXPIRY_DAYS}" --req-cn="vpn-ca.${CN}" build-ca nopass subca 2>/dev/null
# import sub-CA CSR into root PKI, sign, and copy back to vpn PKI
"$easyrsa_bin" --pki-dir="${ROOT_PKI}" import-req "${VPN_PKI}/reqs/ca.req" "vpn-ca" 2>/dev/null
"$easyrsa_bin" --pki-dir="${ROOT_PKI}" sign-req ca "vpn-ca" 2>/dev/null
cp "${ROOT_PKI}/issued/vpn-ca.crt" "${VPN_PKI}/ca.crt"
VPN_CA="${VPN_PKI}/ca.crt"
echo "VPN_CA=${VPN_CA//$OUT/\$OUT}"
# generate and sign vpn server certificate
"$easyrsa_bin" --pki-dir="${VPN_PKI}" --days="${CRT_EXPIRY_DAYS}" build-server-full "vpn.${CN}" nopass 2>/dev/null
VPN_CRT="${VPN_PKI}/issued/vpn.${CN}.crt"
VPN_KEY="${VPN_PKI}/private/vpn.${CN}.key"
echo "VPN_CRT=${VPN_CRT//$OUT/\$OUT}"
echo "VPN_KEY=${VPN_KEY//$OUT/\$OUT}"
# generate vpn dhparams (keysize of 2048 will do, 4096 can wind up taking hours to generate)
"$easyrsa_bin" --pki-dir="${VPN_PKI}" --keysize=2048 gen-dh 2>/dev/null
VPN_DH="${VPN_PKI}/dh.pem"
echo "VPN_DH=${VPN_DH//$OUT/\$OUT}"
# update indexes and generate CRLs
"$easyrsa_bin" --pki-dir="${ROOT_PKI}" update-db 2>/dev/null
"$easyrsa_bin" --pki-dir="${VPN_PKI}" update-db 2>/dev/null
"$easyrsa_bin" --pki-dir="${ROOT_PKI}" gen-crl 2>/dev/null
"$easyrsa_bin" --pki-dir="${VPN_PKI}" gen-crl 2>/dev/null

View File

@ -1,30 +1,42 @@
#!/bin/sh
set -e
HOST_NAME="$1"
ROOT_CA="$2"
TOKEN_AUTH_PUB="$3"
TOKEN_AUTH_KEY="$4"
TOKEN_AUTH_KID="$5"
#!/bin/bash -eu
usage() {
echo "usage: $0 HOST_NAME ROOT_CA TOKEN_AUTH_PUB TOKEN_AUTH_KEY TOKEN_AUTH_KID"
echo "usage: $0"
echo
echo "Required Variables:"
echo
echo " HOST_NAME"
echo " ROOT_CA Path to root CA certificate"
echo " ROOT_CRT Path to root/wildcard certificate"
echo " ROOT_KEY Path to root/wildcard private key"
echo " JWT_CRT Path to Token Auth certificate"
echo " JWT_KEY Path to Token Auth private key"
echo " JWT_KID The KeyID for the Token Auth certificate"
echo " VPN_CA Path to the VPN sub-CA certificate"
echo " VPN_CRT Path to the VPN server certificate"
echo " VPN_KEY Path to the VPN server private key"
echo " VPN_DH Path to the VPN server Diffie Hellman parameters"
echo
}
for var in HOST_NAME ROOT_CA ROOT_CRT ROOT_KEY JWT_CRT JWT_KEY JWT_KID VPN_CA VPN_CRT VPN_KEY VPN_DH; do
if [ -z "${!var-}" ]; then
usage
exit 1
fi
done
randstr() {
LC_CTYPE=C tr -dc A-Za-z0-9 < /dev/urandom | fold -w ${1:-32} | head -n 1
LC_CTYPE=C tr -dc A-Za-z0-9 < /dev/urandom | fold -w "${1:-32}" | head -n 1
}
b64encode() {
cat "$1" | base64 --wrap=0 2>/dev/null || cat "$1" | base64 --break=0
cat "$@" | base64 --wrap=0 2>/dev/null || cat "$@" | base64 --break=0
}
if [ -z "$HOST_NAME" ] || [ -z "$ROOT_CA" ] || [ -z "$TOKEN_AUTH_PUB" ] || [ -z "$TOKEN_AUTH_KEY" ] || [ -z "$TOKEN_AUTH_KID" ]; then
usage
exit 1
fi
b64encode_str() {
echo -n "$@" | base64 --wrap=0 - 2>/dev/null || echo -n "$@" | base64 --break=0 -
}
cat <<STR
export OPENBALENA_DEBUG=true
@ -33,8 +45,14 @@ export OPENBALENA_HOST_NAME=$HOST_NAME
export OPENBALENA_JWT_SECRET=$(randstr 32)
export OPENBALENA_RESINOS_REGISTRY_CODE=$(randstr 32)
export OPENBALENA_ROOT_CA=$(b64encode "$ROOT_CA")
export OPENBALENA_TOKEN_AUTH_PUB=$(b64encode "$TOKEN_AUTH_PUB")
export OPENBALENA_TOKEN_AUTH_KEY=$(b64encode "$TOKEN_AUTH_KEY")
export OPENBALENA_TOKEN_AUTH_KID=$(b64encode "$TOKEN_AUTH_KID")
export OPENBALENA_ROOT_CRT=$(b64encode "${ROOT_CRT}")
export OPENBALENA_ROOT_KEY=$(b64encode "${ROOT_KEY}")
export OPENBALENA_TOKEN_AUTH_PUB=$(b64encode "$JWT_CRT")
export OPENBALENA_TOKEN_AUTH_KEY=$(b64encode "$JWT_KEY")
export OPENBALENA_TOKEN_AUTH_KID=$(b64encode_str "$JWT_KID")
export OPENBALENA_VPN_CA=$(b64encode "$VPN_CA")
export OPENBALENA_VPN_SERVER_CRT=$(b64encode "$VPN_CRT")
export OPENBALENA_VPN_SERVER_KEY=$(b64encode "$VPN_KEY")
export OPENBALENA_VPN_SERVER_DH=$(b64encode "$VPN_DH")
export OPENBALENA_VPN_SERVICE_API_KEY=$(randstr 32)
STR

22
scripts/ssl-common.sh Normal file
View File

@ -0,0 +1,22 @@
#!/bin/bash -eu
# ensure we have `easyrsa` available
if [ -z "${easyrsa_bin-}" ] || [ ! -x "${easyrsa_bin}" ]; then
easyrsa_bin="$(command easyrsa 2>/dev/null || true)"
if [ -z "${easyrsa_bin}" ]; then
easyrsa_dir="$(mktemp -dt easyrsa.XXXXXXXX)"
easyrsa_url="https://github.com/OpenVPN/easy-rsa/releases/download/v3.0.5/EasyRSA-nix-3.0.5.tgz"
(cd "${easyrsa_dir}"; curl -sL "${easyrsa_url}" | tar xz --strip-components=1)
easyrsa_bin="${easyrsa_dir}/easyrsa"
trap "rm -rf \"${easyrsa_dir}\"" EXIT
fi
export EASYRSA_BATCH=1
export EASYRSA_KEY_SIZE=4096
fi
# setup ROOT_PKI path
ROOT_PKI="$(realpath "${OUT}/root")"
# global expiry settings
CA_EXPIRY_DAYS=3650
CRT_EXPIRY_DAYS=730

View File

@ -1,6 +1,4 @@
#!/bin/sh
set -e
#!/bin/bash -eu
CMD=$0
DIR=$(dirname "$CMD")
@ -21,7 +19,7 @@ usage() {
}
echo_bold() {
printf "\033[1m${@}\033[0m\n"
printf "\033[1m%s\033[0m\n" "${@}"
}
if [ -d "$PROJECT_DIR" ]; then
@ -32,21 +30,20 @@ fi
echo_bold "==> Creating new project at: $PROJECT_DIR"
mkdir -p "$PROJECT_DIR" "$CERTS_DIR"
echo_bold "==> Creating root CA cert..."
"${DIR}/gen-root-ca-cert" $HOST_NAME "$CERTS_DIR"
echo_bold "==> Generating root CA cert..."
source "${DIR}/gen-root-ca" "${HOST_NAME}" "${CERTS_DIR}"
echo_bold "==> Creating token auth cert..."
"${DIR}/gen-token-auth-cert" $HOST_NAME "$CERTS_DIR"
echo_bold "==> Generating root cert chain for haproxy..."
source "${DIR}/gen-root-cert" "${HOST_NAME}" "${CERTS_DIR}"
echo_bold "==> Generating token auth cert..."
source "${DIR}/gen-token-auth-cert" "${HOST_NAME}" "${CERTS_DIR}"
echo_bold "==> Generating VPN CA, cert and dhparam (this may take a while)..."
source "${DIR}/gen-vpn-certs" "${HOST_NAME}" "${CERTS_DIR}"
echo_bold "==> Setting up environment..."
cat >"${PROJECT_DIR}/activate" <<STR
$("${DIR}/make-env" \
$HOST_NAME \
"${CERTS_DIR}/root.chain.pem" \
"${CERTS_DIR}/token-auth.crt" \
"${CERTS_DIR}/token-auth.pem" \
"${CERTS_DIR}/token-auth.kid")
STR
cat >"${PROJECT_DIR}/activate" <(source "${DIR}/make-env")
echo_bold "==> Adding default compose file..."
cp "${BASE_DIR}/compose/template.yml" "${PROJECT_DIR}/docker-compose.yml"
@ -56,4 +53,4 @@ echo_bold "==> Patching /etc/hosts..."
"${DIR}/patch-hosts" $HOST_NAME
echo_bold "==> Activating project..."
"${DIR}/select-project" "$PROJECT_DIR"
"${DIR}/select-project" "${PROJECT_DIR}"