Compare commits
3 Commits
e8b5241a54
..
main
| Author | SHA1 | Date | |
|---|---|---|---|
| 27461a0fbe | |||
| c537ae5dd7 | |||
| ad6af575dc |
@@ -174,7 +174,7 @@ Generates Apple `.mobileconfig` profiles for distributing CA certificates and op
|
|||||||
|---|---|
|
|---|---|
|
||||||
| `--ca-cert` only | CA trust anchor |
|
| `--ca-cert` only | CA trust anchor |
|
||||||
| `--ca-cert` + `--client-cert` + `--client-key` | CA trust anchor + PKCS#12 client certificate |
|
| `--ca-cert` + `--client-cert` + `--client-key` | CA trust anchor + PKCS#12 client certificate |
|
||||||
| All of the above + `--remote-address` + `--match-domains` | CA + client cert + IKEv2 VPN |
|
| All of the above + `--remote-address` + `--dns` + `--match-domains` | CA + client cert + IKEv2 VPN |
|
||||||
|
|
||||||
### Usage
|
### Usage
|
||||||
|
|
||||||
@@ -182,10 +182,9 @@ Generates Apple `.mobileconfig` profiles for distributing CA certificates and op
|
|||||||
generate-mobileconfig.py --ca-cert CA.pem --output profile.mobileconfig \
|
generate-mobileconfig.py --ca-cert CA.pem --output profile.mobileconfig \
|
||||||
--identifier com.example.vpn \
|
--identifier com.example.vpn \
|
||||||
[--client-cert CLIENT.pem --client-key CLIENT_KEY.pem] \
|
[--client-cert CLIENT.pem --client-key CLIENT_KEY.pem] \
|
||||||
[--remote-address vpn.example.com --match-domains example.com] \
|
[--remote-address vpn.example.com --dns 10.0.0.1 --match-domains example.com] \
|
||||||
[--profile-name "My VPN"] [--ca-name "My CA"] \
|
[--profile-name "My VPN"] [--ca-name "My CA"] \
|
||||||
[--client-name "My Cert"] [--vpn-name "My VPN Connection"] \
|
[--client-name "My Cert"] [--vpn-name "My VPN Connection"]
|
||||||
[--openssl /usr/bin/openssl]
|
|
||||||
```
|
```
|
||||||
|
|
||||||
#### Required arguments
|
#### Required arguments
|
||||||
@@ -202,6 +201,7 @@ generate-mobileconfig.py --ca-cert CA.pem --output profile.mobileconfig \
|
|||||||
#### VPN (requires client certificate)
|
#### VPN (requires client certificate)
|
||||||
|
|
||||||
- `--remote-address FQDN` — VPN gateway hostname.
|
- `--remote-address FQDN` — VPN gateway hostname.
|
||||||
|
- `--dns IP [IP …]` — DNS server(s) for split DNS.
|
||||||
- `--match-domains DOMAIN [DOMAIN …]` — Split-DNS domains routed through the VPN.
|
- `--match-domains DOMAIN [DOMAIN …]` — Split-DNS domains routed through the VPN.
|
||||||
|
|
||||||
#### Display name overrides (all optional)
|
#### Display name overrides (all optional)
|
||||||
@@ -211,10 +211,6 @@ generate-mobileconfig.py --ca-cert CA.pem --output profile.mobileconfig \
|
|||||||
- `--client-name NAME` — Client cert payload display name (default: certificate CN).
|
- `--client-name NAME` — Client cert payload display name (default: certificate CN).
|
||||||
- `--vpn-name NAME` — VPN connection display name (default: profile name).
|
- `--vpn-name NAME` — VPN connection display name (default: profile name).
|
||||||
|
|
||||||
#### Other
|
|
||||||
|
|
||||||
- `--openssl PATH` — Path to the `openssl` binary (default: `/usr/bin/openssl`).
|
|
||||||
|
|
||||||
### Examples
|
### Examples
|
||||||
|
|
||||||
**CA trust profile only:**
|
**CA trust profile only:**
|
||||||
@@ -245,6 +241,7 @@ python3 generate-mobileconfig.py \
|
|||||||
--client-cert certs/alice_cert.pem \
|
--client-cert certs/alice_cert.pem \
|
||||||
--client-key certs/alice_key.pem \
|
--client-key certs/alice_key.pem \
|
||||||
--remote-address vpn.example.com \
|
--remote-address vpn.example.com \
|
||||||
|
--dns 10.0.0.1 \
|
||||||
--match-domains example.com internal.example.com \
|
--match-domains example.com internal.example.com \
|
||||||
--output alice-vpn.mobileconfig
|
--output alice-vpn.mobileconfig
|
||||||
```
|
```
|
||||||
|
|||||||
@@ -67,6 +67,7 @@ def main():
|
|||||||
|
|
||||||
g_vpn = parser.add_argument_group("VPN (optional, requires client certificate)")
|
g_vpn = parser.add_argument_group("VPN (optional, requires client certificate)")
|
||||||
g_vpn.add_argument("--remote-address", metavar="FQDN", help="VPN gateway FQDN")
|
g_vpn.add_argument("--remote-address", metavar="FQDN", help="VPN gateway FQDN")
|
||||||
|
g_vpn.add_argument("--dns", metavar="IP", nargs="+", help="DNS server(s) for split DNS")
|
||||||
g_vpn.add_argument("--match-domains", metavar="DOMAIN", nargs="+", help="Split DNS domains")
|
g_vpn.add_argument("--match-domains", metavar="DOMAIN", nargs="+", help="Split DNS domains")
|
||||||
|
|
||||||
g_meta = parser.add_argument_group("Profile metadata")
|
g_meta = parser.add_argument_group("Profile metadata")
|
||||||
@@ -91,7 +92,7 @@ def main():
|
|||||||
if args.client_key and not args.client_cert:
|
if args.client_key and not args.client_cert:
|
||||||
parser.error("--client-cert is required when --client-key is specified")
|
parser.error("--client-cert is required when --client-key is specified")
|
||||||
|
|
||||||
vpn_args = [args.remote_address, args.match_domains]
|
vpn_args = [args.remote_address, args.dns, args.match_domains]
|
||||||
if any(vpn_args) and not all(vpn_args):
|
if any(vpn_args) and not all(vpn_args):
|
||||||
parser.error("--remote-address and --match-domains must be specified together")
|
parser.error("--remote-address and --match-domains must be specified together")
|
||||||
if args.remote_address and not args.client_cert:
|
if args.remote_address and not args.client_cert:
|
||||||
@@ -164,9 +165,13 @@ def main():
|
|||||||
"AuthenticationMethod": "None",
|
"AuthenticationMethod": "None",
|
||||||
"ExtendedAuthEnabled": 1,
|
"ExtendedAuthEnabled": 1,
|
||||||
"PayloadCertificateUUID": uuid_cert,
|
"PayloadCertificateUUID": uuid_cert,
|
||||||
"SupplementalMatchDomains": args.match_domains,
|
|
||||||
"OnDemandEnabled": 0,
|
"OnDemandEnabled": 0,
|
||||||
},
|
},
|
||||||
|
"DNS": {
|
||||||
|
"ServerAddresses": args.dns,
|
||||||
|
"SupplementalMatchDomains": args.match_domains,
|
||||||
|
"SupplementalMatchDomainsNoSearch": 1,
|
||||||
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
profile = {
|
profile = {
|
||||||
|
|||||||
+44
-8
@@ -41,6 +41,13 @@ reset_dirs() {
|
|||||||
SIMPLE_CA_DIR=""
|
SIMPLE_CA_DIR=""
|
||||||
}
|
}
|
||||||
|
|
||||||
|
assert_file() {
|
||||||
|
if [[ ! -f "$1" ]]; then
|
||||||
|
echo "ERROR: expected file not found: $1" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
verify_cert() {
|
verify_cert() {
|
||||||
local CERT_PATH="$1"
|
local CERT_PATH="$1"
|
||||||
if ! openssl verify -CAfile "$CA_DIR/ca_bundle.pem" "$CERT_PATH" 2>/dev/null; then
|
if ! openssl verify -CAfile "$CA_DIR/ca_bundle.pem" "$CERT_PATH" 2>/dev/null; then
|
||||||
@@ -50,6 +57,18 @@ verify_cert() {
|
|||||||
echo "Verified: $CERT_PATH"
|
echo "Verified: $CERT_PATH"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
assert_eku() {
|
||||||
|
local CERT_PATH="$1"
|
||||||
|
local EKU="$2"
|
||||||
|
local TEXT
|
||||||
|
TEXT="$(openssl x509 -in "$CERT_PATH" -noout -text 2>/dev/null)"
|
||||||
|
if ! echo "$TEXT" | grep -q "$EKU"; then
|
||||||
|
echo "ERROR: EKU '$EKU' not found in $CERT_PATH" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
echo "EKU OK: $EKU"
|
||||||
|
}
|
||||||
|
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
# Standalone CA — certs issued by root CA go into CA_DIR
|
# Standalone CA — certs issued by root CA go into CA_DIR
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
@@ -58,12 +77,12 @@ echo
|
|||||||
echo "--- [shell] Standalone CA ---"
|
echo "--- [shell] Standalone CA ---"
|
||||||
reset_dirs
|
reset_dirs
|
||||||
make_ca --ca-dir "$CA_DIR" "Test CA" 2>/dev/null
|
make_ca --ca-dir "$CA_DIR" "Test CA" 2>/dev/null
|
||||||
[[ -f "$CA_DIR/ca_cert.pem" ]] || { echo "ERROR: ca_cert.pem not created" >&2; exit 1; }
|
assert_file "$CA_DIR/ca_cert.pem"
|
||||||
[[ -f "$CA_DIR/ca_bundle.pem" ]] || { echo "ERROR: ca_bundle.pem not created" >&2; exit 1; }
|
assert_file "$CA_DIR/ca_bundle.pem"
|
||||||
verify_cert "$CA_DIR/ca_cert.pem"
|
verify_cert "$CA_DIR/ca_cert.pem"
|
||||||
|
|
||||||
make_cert "test" "test.example.com" "127.0.0.1" 2>/dev/null
|
make_cert "test" "test.example.com" "127.0.0.1" 2>/dev/null
|
||||||
[[ -f "$CA_DIR/test_cert.pem" ]] || { echo "ERROR: test_cert.pem not created in CA_DIR" >&2; exit 1; }
|
assert_file "$CA_DIR/test_cert.pem"
|
||||||
verify_cert "$CA_DIR/test_cert.pem"
|
verify_cert "$CA_DIR/test_cert.pem"
|
||||||
|
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
@@ -77,18 +96,35 @@ make_ca --ca-dir "$CA_DIR" "Test Root CA" 2>/dev/null
|
|||||||
verify_cert "$CA_DIR/ca_cert.pem"
|
verify_cert "$CA_DIR/ca_cert.pem"
|
||||||
|
|
||||||
make_ca --issuing-ca "issuing_ca" "Issuing CA" 2>/dev/null
|
make_ca --issuing-ca "issuing_ca" "Issuing CA" 2>/dev/null
|
||||||
[[ -f "$CA_DIR/issuing_ca/ca_cert.pem" ]] || { echo "ERROR: issuing_ca/ca_cert.pem not created" >&2; exit 1; }
|
assert_file "$CA_DIR/issuing_ca/ca_cert.pem"
|
||||||
verify_cert "$CA_DIR/issuing_ca/ca_cert.pem"
|
verify_cert "$CA_DIR/issuing_ca/ca_cert.pem"
|
||||||
|
|
||||||
make_cert --issuing-ca "issuing_ca" "test" "test.example.com" "127.0.0.1" 2>/dev/null
|
make_cert --issuing-ca "issuing_ca" "test" "test.example.com" "127.0.0.1" 2>/dev/null
|
||||||
[[ -f "$CA_DIR/issuing_ca/test_cert.pem" ]] || { echo "ERROR: issuing_ca/test_cert.pem not created" >&2; exit 1; }
|
assert_file "$CA_DIR/issuing_ca/test_cert.pem"
|
||||||
verify_cert "$CA_DIR/issuing_ca/test_cert.pem"
|
verify_cert "$CA_DIR/issuing_ca/test_cert.pem"
|
||||||
|
assert_eku "$CA_DIR/issuing_ca/test_cert.pem" "TLS Web Server Authentication"
|
||||||
|
assert_eku "$CA_DIR/issuing_ca/test_cert.pem" "TLS Web Client Authentication"
|
||||||
|
|
||||||
make_pfx --issuing-ca "issuing_ca" --password "s3cr3t" "$CA_DIR/issuing_ca/test_cert.pem" 2>/dev/null
|
make_pfx --issuing-ca "issuing_ca" --password "s3cr3t" "$CA_DIR/issuing_ca/test_cert.pem" 2>/dev/null
|
||||||
[[ -f "$CA_DIR/issuing_ca/test.pfx" ]] || { echo "ERROR: issuing_ca/test.pfx not created" >&2; exit 1; }
|
assert_file "$CA_DIR/issuing_ca/test.pfx"
|
||||||
openssl pkcs12 -in "$CA_DIR/issuing_ca/test.pfx" -noout -info -password pass:"s3cr3t" 2>/dev/null \
|
if ! openssl pkcs12 -in "$CA_DIR/issuing_ca/test.pfx" -noout -info -password pass:"s3cr3t" 2>/dev/null; then
|
||||||
|| { echo "ERROR: PFX verification failed" >&2; exit 1; }
|
echo "ERROR: PFX verification failed" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
echo "PFX: OK"
|
echo "PFX: OK"
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# User certificate
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
echo
|
||||||
|
echo "--- [shell] User certificate ---"
|
||||||
|
make_cert --issuing-ca "issuing_ca" --type user "Alice Example" "alice@example.com" 2>/dev/null
|
||||||
|
assert_file "$CA_DIR/issuing_ca/Alice Example_cert.pem"
|
||||||
|
verify_cert "$CA_DIR/issuing_ca/Alice Example_cert.pem"
|
||||||
|
assert_eku "$CA_DIR/issuing_ca/Alice Example_cert.pem" "TLS Web Client Authentication"
|
||||||
|
assert_eku "$CA_DIR/issuing_ca/Alice Example_cert.pem" "E-mail Protection"
|
||||||
|
assert_eku "$CA_DIR/issuing_ca/Alice Example_cert.pem" "Code Signing"
|
||||||
|
|
||||||
echo
|
echo
|
||||||
echo "All shell tests passed."
|
echo "All shell tests passed."
|
||||||
|
|||||||
+64
-23
@@ -59,6 +59,7 @@ _require_ca_dir() {
|
|||||||
|
|
||||||
_is_ip() { [[ "$1" =~ ^[0-9]{1,3}(\.[0-9]{1,3}){3}$ ]]; }
|
_is_ip() { [[ "$1" =~ ^[0-9]{1,3}(\.[0-9]{1,3}){3}$ ]]; }
|
||||||
_is_dns() { [[ "$1" =~ ^[a-z0-9-]+(\.[a-z0-9-]+)*$ ]]; }
|
_is_dns() { [[ "$1" =~ ^[a-z0-9-]+(\.[a-z0-9-]+)*$ ]]; }
|
||||||
|
_is_email() { [[ "$1" =~ ^[^@]+@[^@]+\.[^@]+$ ]]; }
|
||||||
|
|
||||||
make_ca() {
|
make_ca() {
|
||||||
local CA_DAYS=3650
|
local CA_DAYS=3650
|
||||||
@@ -187,6 +188,7 @@ make_cert() {
|
|||||||
local ISSUING_CA=""
|
local ISSUING_CA=""
|
||||||
local CERT_DAYS=365
|
local CERT_DAYS=365
|
||||||
local CERT_DIR=""
|
local CERT_DIR=""
|
||||||
|
local CERT_TYPE="server"
|
||||||
|
|
||||||
while [[ $# -gt 0 ]]; do
|
while [[ $# -gt 0 ]]; do
|
||||||
case "$1" in
|
case "$1" in
|
||||||
@@ -218,6 +220,16 @@ make_cert() {
|
|||||||
return 1
|
return 1
|
||||||
fi
|
fi
|
||||||
CERT_DAYS="$2"; shift 2 ;;
|
CERT_DAYS="$2"; shift 2 ;;
|
||||||
|
--type)
|
||||||
|
if [[ -z "$2" ]]; then
|
||||||
|
echo "ERROR: --type requires a value." >&2
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
if [[ "$2" != "server" && "$2" != "user" ]]; then
|
||||||
|
echo "ERROR: --type must be 'server' or 'user'." >&2
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
CERT_TYPE="$2"; shift 2 ;;
|
||||||
*) break ;;
|
*) break ;;
|
||||||
esac
|
esac
|
||||||
done
|
done
|
||||||
@@ -233,7 +245,7 @@ make_cert() {
|
|||||||
echo "ERROR: Subject name is required." >&2
|
echo "ERROR: Subject name is required." >&2
|
||||||
return 1
|
return 1
|
||||||
fi
|
fi
|
||||||
if ! _is_dns "$CERT_SUBJECT_NAME"; then
|
if [[ "$CERT_TYPE" == "server" ]] && ! _is_dns "$CERT_SUBJECT_NAME"; then
|
||||||
echo "ERROR: Invalid subject name '$CERT_SUBJECT_NAME'. Must be a valid DNS name." >&2
|
echo "ERROR: Invalid subject name '$CERT_SUBJECT_NAME'. Must be a valid DNS name." >&2
|
||||||
return 1
|
return 1
|
||||||
fi
|
fi
|
||||||
@@ -256,11 +268,17 @@ make_cert() {
|
|||||||
fi
|
fi
|
||||||
|
|
||||||
local CERT_NAME="${CERT_SUBJECT_NAME%%.*}"
|
local CERT_NAME="${CERT_SUBJECT_NAME%%.*}"
|
||||||
local SANS=("DNS:${CERT_SUBJECT_NAME}")
|
local SANS=()
|
||||||
|
|
||||||
|
if [[ "$CERT_TYPE" == "server" ]]; then
|
||||||
|
SANS=("DNS:${CERT_SUBJECT_NAME}")
|
||||||
|
fi
|
||||||
|
|
||||||
while [[ $# -gt 0 ]]; do
|
while [[ $# -gt 0 ]]; do
|
||||||
if _is_ip "$1"; then
|
if _is_ip "$1"; then
|
||||||
SANS+=("IP:$1")
|
SANS+=("IP:$1")
|
||||||
|
elif _is_email "$1"; then
|
||||||
|
SANS+=("email:$1")
|
||||||
elif _is_dns "$1"; then
|
elif _is_dns "$1"; then
|
||||||
SANS+=("DNS:$1")
|
SANS+=("DNS:$1")
|
||||||
else
|
else
|
||||||
@@ -270,7 +288,11 @@ make_cert() {
|
|||||||
shift
|
shift
|
||||||
done
|
done
|
||||||
|
|
||||||
|
if [[ "$CERT_TYPE" == "server" ]]; then
|
||||||
echo "Generating server certificate for '$CERT_SUBJECT_NAME' with SANs:"
|
echo "Generating server certificate for '$CERT_SUBJECT_NAME' with SANs:"
|
||||||
|
else
|
||||||
|
echo "Generating user certificate for '$CERT_SUBJECT_NAME':"
|
||||||
|
fi
|
||||||
for san in "${SANS[@]}"; do echo " - $san"; done
|
for san in "${SANS[@]}"; do echo " - $san"; done
|
||||||
|
|
||||||
if [[ -f "$CERT_DIR/${CERT_NAME}_cert.pem" && -f "$CERT_DIR/${CERT_NAME}_key.pem" ]]; then
|
if [[ -f "$CERT_DIR/${CERT_NAME}_cert.pem" && -f "$CERT_DIR/${CERT_NAME}_key.pem" ]]; then
|
||||||
@@ -278,28 +300,47 @@ make_cert() {
|
|||||||
return 0
|
return 0
|
||||||
fi
|
fi
|
||||||
|
|
||||||
local SANS_EXT="subjectAltName=$(IFS=,; echo "${SANS[*]}")"
|
local REQ_ARGS=(
|
||||||
|
-newkey rsa:4096
|
||||||
|
-keyout "$CERT_DIR/${CERT_NAME}_key.pem"
|
||||||
|
-noenc
|
||||||
|
-subj "/CN=${CERT_SUBJECT_NAME}"
|
||||||
|
-addext "basicConstraints=critical,CA:FALSE"
|
||||||
|
)
|
||||||
|
|
||||||
echo "Generating server certificate and key..."
|
local X509_ARGS=(
|
||||||
if ! openssl req \
|
-req
|
||||||
-newkey rsa:4096 \
|
-CA "$SIGNING_CERT"
|
||||||
-keyout "$CERT_DIR/${CERT_NAME}_key.pem" \
|
-CAkey "$SIGNING_KEY"
|
||||||
-noenc \
|
-copy_extensions copyall
|
||||||
-subj "/CN=${CERT_SUBJECT_NAME}" \
|
-days "$CERT_DAYS"
|
||||||
-addext "basicConstraints=critical,CA:FALSE" \
|
-text
|
||||||
-addext "keyUsage=critical,digitalSignature,keyEncipherment" \
|
-out "$CERT_DIR/${CERT_NAME}_cert.pem"
|
||||||
-addext "extendedKeyUsage=serverAuth,clientAuth" \
|
)
|
||||||
-addext "$SANS_EXT" \
|
|
||||||
${AIA_URL:+-addext "authorityInfoAccess=caIssuers;URI:${AIA_URL}"} \
|
if [[ "$CERT_TYPE" == "server" ]]; then
|
||||||
| openssl x509 \
|
REQ_ARGS+=(
|
||||||
-req \
|
-addext "keyUsage=critical,digitalSignature,keyEncipherment"
|
||||||
-CA "$SIGNING_CERT" \
|
-addext "extendedKeyUsage=serverAuth,clientAuth"
|
||||||
-CAkey "$SIGNING_KEY" \
|
-addext "subjectAltName=$(IFS=,; echo "${SANS[*]}")"
|
||||||
-copy_extensions copyall \
|
)
|
||||||
-days "$CERT_DAYS" \
|
else
|
||||||
-text \
|
REQ_ARGS+=(
|
||||||
-out "$CERT_DIR/${CERT_NAME}_cert.pem"; then
|
-addext "keyUsage=critical,digitalSignature,nonRepudiation"
|
||||||
echo "ERROR: Failed to generate server certificate and key." >&2
|
-addext "extendedKeyUsage=clientAuth,emailProtection,codeSigning"
|
||||||
|
)
|
||||||
|
if [[ ${#SANS[@]} -gt 0 ]]; then
|
||||||
|
REQ_ARGS+=(-addext "subjectAltName=$(IFS=,; echo "${SANS[*]}")")
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [[ -n "$AIA_URL" ]]; then
|
||||||
|
REQ_ARGS+=(-addext "authorityInfoAccess=caIssuers;URI:${AIA_URL}")
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "Generating certificate and key..."
|
||||||
|
if ! openssl req "${REQ_ARGS[@]}" | openssl x509 "${X509_ARGS[@]}"; then
|
||||||
|
echo "ERROR: Failed to generate certificate and key." >&2
|
||||||
return 1
|
return 1
|
||||||
fi
|
fi
|
||||||
}
|
}
|
||||||
|
|||||||
+45
-8
@@ -288,19 +288,23 @@ func makeCA(caDir, caName string, days int, issuingCA, aiaBaseURL string) error
|
|||||||
var (
|
var (
|
||||||
ipRE = regexp.MustCompile(`^[0-9]{1,3}(\.[0-9]{1,3}){3}$`)
|
ipRE = regexp.MustCompile(`^[0-9]{1,3}(\.[0-9]{1,3}){3}$`)
|
||||||
dnsRE = regexp.MustCompile(`^[a-z0-9-]+(\.[a-z0-9-]+)*$`)
|
dnsRE = regexp.MustCompile(`^[a-z0-9-]+(\.[a-z0-9-]+)*$`)
|
||||||
|
emailRE = regexp.MustCompile(`^[^@]+@[^@]+\.[^@]+$`)
|
||||||
)
|
)
|
||||||
|
|
||||||
func makeCert(subjectName string, sans []string, caDir, certDir, issuingCA string, days int) error {
|
func makeCert(subjectName string, sans []string, caDir, certDir, issuingCA, certType string, days int) error {
|
||||||
if issuingCA == "ca" {
|
if issuingCA == "ca" {
|
||||||
return errors.New("--issuing-ca cannot be 'ca'")
|
return errors.New("--issuing-ca cannot be 'ca'")
|
||||||
}
|
}
|
||||||
|
if certType != "server" && certType != "user" {
|
||||||
|
return fmt.Errorf("--type must be 'server' or 'user', got '%s'", certType)
|
||||||
|
}
|
||||||
if !dirExists(caDir) {
|
if !dirExists(caDir) {
|
||||||
return fmt.Errorf("CA directory %s does not exist", caDir)
|
return fmt.Errorf("CA directory %s does not exist", caDir)
|
||||||
}
|
}
|
||||||
if subjectName == "" {
|
if subjectName == "" {
|
||||||
return errors.New("subject name is required")
|
return errors.New("subject name is required")
|
||||||
}
|
}
|
||||||
if !dnsRE.MatchString(subjectName) {
|
if certType == "server" && !dnsRE.MatchString(subjectName) {
|
||||||
return fmt.Errorf("invalid subject name '%s'. Must be a valid DNS name", subjectName)
|
return fmt.Errorf("invalid subject name '%s'. Must be a valid DNS name", subjectName)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -337,12 +341,20 @@ func makeCert(subjectName string, sans []string, caDir, certDir, issuingCA strin
|
|||||||
certName = certName[:i]
|
certName = certName[:i]
|
||||||
}
|
}
|
||||||
|
|
||||||
dnsNames := []string{subjectName}
|
var dnsNames []string
|
||||||
|
var emails []string
|
||||||
var ips []net.IP
|
var ips []net.IP
|
||||||
|
|
||||||
|
if certType == "server" {
|
||||||
|
dnsNames = []string{subjectName}
|
||||||
|
}
|
||||||
|
|
||||||
for _, entry := range sans {
|
for _, entry := range sans {
|
||||||
switch {
|
switch {
|
||||||
case ipRE.MatchString(entry):
|
case ipRE.MatchString(entry):
|
||||||
ips = append(ips, net.ParseIP(entry))
|
ips = append(ips, net.ParseIP(entry))
|
||||||
|
case emailRE.MatchString(entry):
|
||||||
|
emails = append(emails, entry)
|
||||||
case dnsRE.MatchString(entry):
|
case dnsRE.MatchString(entry):
|
||||||
dnsNames = append(dnsNames, entry)
|
dnsNames = append(dnsNames, entry)
|
||||||
default:
|
default:
|
||||||
@@ -350,10 +362,17 @@ func makeCert(subjectName string, sans []string, caDir, certDir, issuingCA strin
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if certType == "server" {
|
||||||
fmt.Printf("Generating server certificate for '%s' with SANs:\n", subjectName)
|
fmt.Printf("Generating server certificate for '%s' with SANs:\n", subjectName)
|
||||||
|
} else {
|
||||||
|
fmt.Printf("Generating user certificate for '%s':\n", subjectName)
|
||||||
|
}
|
||||||
for _, dns := range dnsNames {
|
for _, dns := range dnsNames {
|
||||||
fmt.Printf(" - DNS:%s\n", dns)
|
fmt.Printf(" - DNS:%s\n", dns)
|
||||||
}
|
}
|
||||||
|
for _, email := range emails {
|
||||||
|
fmt.Printf(" - email:%s\n", email)
|
||||||
|
}
|
||||||
for _, ip := range ips {
|
for _, ip := range ips {
|
||||||
fmt.Printf(" - IP:%s\n", ip)
|
fmt.Printf(" - IP:%s\n", ip)
|
||||||
}
|
}
|
||||||
@@ -365,7 +384,7 @@ func makeCert(subjectName string, sans []string, caDir, certDir, issuingCA strin
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
fmt.Println("Generating server certificate and key...")
|
fmt.Println("Generating certificate and key...")
|
||||||
caCert, err := loadCert(caCertPath)
|
caCert, err := loadCert(caCertPath)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
@@ -383,18 +402,35 @@ func makeCert(subjectName string, sans []string, caDir, certDir, issuingCA strin
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
now := time.Now().UTC()
|
now := time.Now().UTC()
|
||||||
tmpl := &x509.Certificate{
|
|
||||||
|
var tmpl *x509.Certificate
|
||||||
|
if certType == "server" {
|
||||||
|
tmpl = &x509.Certificate{
|
||||||
SerialNumber: serial,
|
SerialNumber: serial,
|
||||||
Subject: pkix.Name{CommonName: subjectName},
|
Subject: pkix.Name{CommonName: subjectName},
|
||||||
NotBefore: now,
|
NotBefore: now,
|
||||||
NotAfter: now.AddDate(0, 0, days),
|
NotAfter: now.AddDate(0, 0, days),
|
||||||
IsCA: false,
|
|
||||||
BasicConstraintsValid: true,
|
BasicConstraintsValid: true,
|
||||||
KeyUsage: x509.KeyUsageDigitalSignature | x509.KeyUsageKeyEncipherment,
|
KeyUsage: x509.KeyUsageDigitalSignature | x509.KeyUsageKeyEncipherment,
|
||||||
ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth, x509.ExtKeyUsageClientAuth},
|
ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth, x509.ExtKeyUsageClientAuth},
|
||||||
DNSNames: dnsNames,
|
DNSNames: dnsNames,
|
||||||
IPAddresses: ips,
|
IPAddresses: ips,
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
|
tmpl = &x509.Certificate{
|
||||||
|
SerialNumber: serial,
|
||||||
|
Subject: pkix.Name{CommonName: subjectName},
|
||||||
|
NotBefore: now,
|
||||||
|
NotAfter: now.AddDate(0, 0, days),
|
||||||
|
BasicConstraintsValid: true,
|
||||||
|
KeyUsage: x509.KeyUsageDigitalSignature | x509.KeyUsageContentCommitment,
|
||||||
|
ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageClientAuth, x509.ExtKeyUsageEmailProtection, x509.ExtKeyUsageCodeSigning},
|
||||||
|
EmailAddresses: emails,
|
||||||
|
DNSNames: dnsNames,
|
||||||
|
IPAddresses: ips,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if aiaURL != "" {
|
if aiaURL != "" {
|
||||||
tmpl.IssuingCertificateURL = []string{aiaURL}
|
tmpl.IssuingCertificateURL = []string{aiaURL}
|
||||||
}
|
}
|
||||||
@@ -575,19 +611,20 @@ func newMakeCACmd() *cobra.Command {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func newMakeCertCmd() *cobra.Command {
|
func newMakeCertCmd() *cobra.Command {
|
||||||
var certDir, caDir, issuingCA string
|
var certDir, caDir, issuingCA, certType string
|
||||||
var days int
|
var days int
|
||||||
cmd := &cobra.Command{
|
cmd := &cobra.Command{
|
||||||
Use: "make-cert SUBJECT [SAN...]",
|
Use: "make-cert SUBJECT [SAN...]",
|
||||||
Short: "Create a server/client certificate signed by the CA.",
|
Short: "Create a server/client certificate signed by the CA.",
|
||||||
Args: cobra.MinimumNArgs(1),
|
Args: cobra.MinimumNArgs(1),
|
||||||
RunE: func(_ *cobra.Command, args []string) error {
|
RunE: func(_ *cobra.Command, args []string) error {
|
||||||
return makeCert(args[0], args[1:], resolveCADir(caDir), certDir, issuingCA, days)
|
return makeCert(args[0], args[1:], resolveCADir(caDir), certDir, issuingCA, certType, days)
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
cmd.Flags().StringVar(&certDir, "cert-dir", "", "output directory (default: signing CA directory)")
|
cmd.Flags().StringVar(&certDir, "cert-dir", "", "output directory (default: signing CA directory)")
|
||||||
cmd.Flags().StringVar(&caDir, "ca-dir", "", "CA root directory")
|
cmd.Flags().StringVar(&caDir, "ca-dir", "", "CA root directory")
|
||||||
cmd.Flags().StringVar(&issuingCA, "issuing-ca", "", "issuing CA directory name")
|
cmd.Flags().StringVar(&issuingCA, "issuing-ca", "", "issuing CA directory name")
|
||||||
|
cmd.Flags().StringVar(&certType, "type", "server", "certificate type: server or user")
|
||||||
cmd.Flags().IntVar(&days, "days", 365, "validity period in days")
|
cmd.Flags().IntVar(&days, "days", 365, "validity period in days")
|
||||||
return cmd
|
return cmd
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import (
|
|||||||
"os"
|
"os"
|
||||||
"os/exec"
|
"os/exec"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -15,6 +16,17 @@ func verifyCert(t *testing.T, bundle, cert string) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func assertEKU(t *testing.T, cert, eku string) {
|
||||||
|
t.Helper()
|
||||||
|
out, err := exec.Command("openssl", "x509", "-in", cert, "-noout", "-text").CombinedOutput()
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("openssl x509 failed: %v", err)
|
||||||
|
}
|
||||||
|
if !strings.Contains(string(out), eku) {
|
||||||
|
t.Fatalf("EKU %q not found in %s", eku, cert)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func TestStandaloneCA(t *testing.T) {
|
func TestStandaloneCA(t *testing.T) {
|
||||||
caDir := t.TempDir()
|
caDir := t.TempDir()
|
||||||
|
|
||||||
@@ -29,7 +41,7 @@ func TestStandaloneCA(t *testing.T) {
|
|||||||
}
|
}
|
||||||
verifyCert(t, filepath.Join(caDir, "ca_bundle.pem"), filepath.Join(caDir, "ca_cert.pem"))
|
verifyCert(t, filepath.Join(caDir, "ca_bundle.pem"), filepath.Join(caDir, "ca_cert.pem"))
|
||||||
|
|
||||||
if err := makeCert("test", []string{"test.example.com", "127.0.0.1"}, caDir, "", "", 365); err != nil {
|
if err := makeCert("test", []string{"test.example.com", "127.0.0.1"}, caDir, "", "", "server", 365); err != nil {
|
||||||
t.Fatalf("makeCert: %v", err)
|
t.Fatalf("makeCert: %v", err)
|
||||||
}
|
}
|
||||||
certPath := filepath.Join(caDir, "test_cert.pem")
|
certPath := filepath.Join(caDir, "test_cert.pem")
|
||||||
@@ -56,7 +68,7 @@ func TestTwoLevelCA(t *testing.T) {
|
|||||||
}
|
}
|
||||||
verifyCert(t, filepath.Join(caDir, "ca_bundle.pem"), issuingCert)
|
verifyCert(t, filepath.Join(caDir, "ca_bundle.pem"), issuingCert)
|
||||||
|
|
||||||
if err := makeCert("test", []string{"test.example.com", "127.0.0.1"}, caDir, "", "issuing_ca", 365); err != nil {
|
if err := makeCert("test", []string{"test.example.com", "127.0.0.1"}, caDir, "", "issuing_ca", "server", 365); err != nil {
|
||||||
t.Fatalf("makeCert: %v", err)
|
t.Fatalf("makeCert: %v", err)
|
||||||
}
|
}
|
||||||
certPath := filepath.Join(caDir, "issuing_ca", "test_cert.pem")
|
certPath := filepath.Join(caDir, "issuing_ca", "test_cert.pem")
|
||||||
@@ -64,6 +76,8 @@ func TestTwoLevelCA(t *testing.T) {
|
|||||||
t.Fatal("issuing_ca/test_cert.pem not created")
|
t.Fatal("issuing_ca/test_cert.pem not created")
|
||||||
}
|
}
|
||||||
verifyCert(t, filepath.Join(caDir, "ca_bundle.pem"), certPath)
|
verifyCert(t, filepath.Join(caDir, "ca_bundle.pem"), certPath)
|
||||||
|
assertEKU(t, certPath, "TLS Web Server Authentication")
|
||||||
|
assertEKU(t, certPath, "TLS Web Client Authentication")
|
||||||
|
|
||||||
if err := makePFX(certPath, caDir, "issuing_ca", "s3cr3t", false); err != nil {
|
if err := makePFX(certPath, caDir, "issuing_ca", "s3cr3t", false); err != nil {
|
||||||
t.Fatalf("makePFX: %v", err)
|
t.Fatalf("makePFX: %v", err)
|
||||||
@@ -79,6 +93,29 @@ func TestTwoLevelCA(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestUserCert(t *testing.T) {
|
||||||
|
caDir := t.TempDir()
|
||||||
|
|
||||||
|
if err := makeCA(caDir, "Test CA", 3650, "", ""); err != nil {
|
||||||
|
t.Fatalf("makeCA: %v", err)
|
||||||
|
}
|
||||||
|
if err := makeCA(caDir, "Issuing CA", 3650, "issuing_ca", ""); err != nil {
|
||||||
|
t.Fatalf("makeCA issuing: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := makeCert("Alice Example", []string{"alice@example.com"}, caDir, "", "issuing_ca", "user", 365); err != nil {
|
||||||
|
t.Fatalf("makeCert user: %v", err)
|
||||||
|
}
|
||||||
|
certPath := filepath.Join(caDir, "issuing_ca", "Alice Example_cert.pem")
|
||||||
|
if !fileExists(certPath) {
|
||||||
|
t.Fatal("Alice Example_cert.pem not created")
|
||||||
|
}
|
||||||
|
verifyCert(t, filepath.Join(caDir, "ca_bundle.pem"), certPath)
|
||||||
|
assertEKU(t, certPath, "TLS Web Client Authentication")
|
||||||
|
assertEKU(t, certPath, "E-mail Protection")
|
||||||
|
assertEKU(t, certPath, "Code Signing")
|
||||||
|
}
|
||||||
|
|
||||||
func TestCertDirOverride(t *testing.T) {
|
func TestCertDirOverride(t *testing.T) {
|
||||||
caDir := t.TempDir()
|
caDir := t.TempDir()
|
||||||
certDir := t.TempDir()
|
certDir := t.TempDir()
|
||||||
@@ -86,7 +123,7 @@ func TestCertDirOverride(t *testing.T) {
|
|||||||
if err := makeCA(caDir, "Test CA", 3650, "", ""); err != nil {
|
if err := makeCA(caDir, "Test CA", 3650, "", ""); err != nil {
|
||||||
t.Fatalf("makeCA: %v", err)
|
t.Fatalf("makeCA: %v", err)
|
||||||
}
|
}
|
||||||
if err := makeCert("test", []string{"test.example.com"}, caDir, certDir, "", 365); err != nil {
|
if err := makeCert("test", []string{"test.example.com"}, caDir, certDir, "", "server", 365); err != nil {
|
||||||
t.Fatalf("makeCert: %v", err)
|
t.Fatalf("makeCert: %v", err)
|
||||||
}
|
}
|
||||||
certPath := filepath.Join(certDir, "test_cert.pem")
|
certPath := filepath.Join(certDir, "test_cert.pem")
|
||||||
@@ -109,7 +146,7 @@ func TestAppleOpenSSL(t *testing.T) {
|
|||||||
if err := makeCA(caDir, "Issuing CA", 3650, "issuing_ca", ""); err != nil {
|
if err := makeCA(caDir, "Issuing CA", 3650, "issuing_ca", ""); err != nil {
|
||||||
t.Fatalf("makeCA issuing: %v", err)
|
t.Fatalf("makeCA issuing: %v", err)
|
||||||
}
|
}
|
||||||
if err := makeCert("test", []string{"test.example.com"}, caDir, "", "issuing_ca", 365); err != nil {
|
if err := makeCert("test", []string{"test.example.com"}, caDir, "", "issuing_ca", "server", 365); err != nil {
|
||||||
t.Fatalf("makeCert: %v", err)
|
t.Fatalf("makeCert: %v", err)
|
||||||
}
|
}
|
||||||
certPath := filepath.Join(caDir, "issuing_ca", "test_cert.pem")
|
certPath := filepath.Join(caDir, "issuing_ca", "test_cert.pem")
|
||||||
|
|||||||
Reference in New Issue
Block a user