Merge pull request #8996 from dokku/8992-add-a-fingerprint-field-to-certs-report
Add fingerprint and serial to certs:report
This commit is contained in:
@@ -85,21 +85,25 @@ dokku certs:report
|
||||
```
|
||||
|
||||
```
|
||||
=====> node-js-app
|
||||
Ssl dir: /home/dokku/node-js-app/tls
|
||||
Ssl enabled: true
|
||||
Ssl hostnames: *.node-js-app.org node-js-app.org
|
||||
Ssl expires at: Oct 5 23:59:59 2019 GMT
|
||||
Ssl issuer: C=GB, ST=Greater Manchester, L=Salford, O=COMODO CA Limited, CN=COMODO RSA Domain Validation Secure Server CA
|
||||
Ssl starts at: Oct 5 00:00:00 2016 GMT
|
||||
Ssl subject: OU=Domain Control Validated; OU=PositiveSSL Wildcard; CN=*.node-js-app.org
|
||||
Ssl verified: self signed.
|
||||
=====> python-app
|
||||
Ssl dir: /home/dokku/python-app/tls
|
||||
Ssl enabled: false
|
||||
Ssl hostnames:
|
||||
=====> node-js-app ssl information
|
||||
Ssl dir: /home/dokku/node-js-app/tls
|
||||
Ssl enabled: true
|
||||
Ssl expires at: Oct 5 23:59:59 2019 GMT
|
||||
Ssl fingerprint: 9E:2F:0B:74:C3:5A:81:D6:4F:AA:12:38:E7:6B:90:5C:D1:47:2E:8A:B3:65:F9:0D:28:C4:71:AE:53:80:6F:12
|
||||
Ssl hostnames: *.node-js-app.org node-js-app.org
|
||||
Ssl issuer: C=GB, ST=Greater Manchester, L=Salford, O=COMODO CA Limited, CN=COMODO RSA Domain Validation Secure Server CA
|
||||
Ssl serial: 2F4B8C1D6E9A0357B8C2D4E6F8091A2B
|
||||
Ssl starts at: Oct 5 00:00:00 2016 GMT
|
||||
Ssl subject: OU=Domain Control Validated; OU=PositiveSSL Wildcard; CN=*.node-js-app.org
|
||||
Ssl verified: self signed
|
||||
=====> python-app ssl information
|
||||
Ssl dir: /home/dokku/python-app/tls
|
||||
Ssl enabled: false
|
||||
Ssl expires at:
|
||||
Ssl fingerprint:
|
||||
Ssl hostnames:
|
||||
Ssl issuer:
|
||||
Ssl serial:
|
||||
Ssl starts at:
|
||||
Ssl subject:
|
||||
Ssl verified:
|
||||
@@ -113,14 +117,16 @@ dokku certs:report node-js-app
|
||||
|
||||
```
|
||||
=====> node-js-app ssl information
|
||||
Ssl dir: /home/dokku/node-js-app/tls
|
||||
Ssl enabled: true
|
||||
Ssl hostnames: *.dokku.org dokku.org
|
||||
Ssl expires at: Oct 5 23:59:59 2019 GMT
|
||||
Ssl issuer: C=GB, ST=Greater Manchester, L=Salford, O=COMODO CA Limited, CN=COMODO RSA Domain Validation Secure Server CA
|
||||
Ssl starts at: Oct 5 00:00:00 2016 GMT
|
||||
Ssl subject: OU=Domain Control Validated; OU=PositiveSSL Wildcard; CN=*.dokku.org
|
||||
Ssl verified: self signed.
|
||||
Ssl dir: /home/dokku/node-js-app/tls
|
||||
Ssl enabled: true
|
||||
Ssl expires at: Oct 5 23:59:59 2019 GMT
|
||||
Ssl fingerprint: 9E:2F:0B:74:C3:5A:81:D6:4F:AA:12:38:E7:6B:90:5C:D1:47:2E:8A:B3:65:F9:0D:28:C4:71:AE:53:80:6F:12
|
||||
Ssl hostnames: *.dokku.org dokku.org
|
||||
Ssl issuer: C=GB, ST=Greater Manchester, L=Salford, O=COMODO CA Limited, CN=COMODO RSA Domain Validation Secure Server CA
|
||||
Ssl serial: 2F4B8C1D6E9A0357B8C2D4E6F8091A2B
|
||||
Ssl starts at: Oct 5 00:00:00 2016 GMT
|
||||
Ssl subject: OU=Domain Control Validated; OU=PositiveSSL Wildcard; CN=*.dokku.org
|
||||
Ssl verified: self signed
|
||||
```
|
||||
|
||||
You can pass flags which will output only the value of the specific information you want. For example:
|
||||
@@ -164,6 +170,8 @@ The following flags surface in `certs:report` but are not managed by `certs:set`
|
||||
| `--ssl-hostnames` | Hostnames the certificate covers (CN plus Subject Alternative Names) |
|
||||
| `--ssl-issuer` | Certificate issuer DN |
|
||||
| `--ssl-subject` | Certificate subject DN |
|
||||
| `--ssl-verified` | `true` if the certificate chain verifies against the system CA bundle |
|
||||
| `--ssl-fingerprint` | SHA-256 digest of the leaf certificate's DER encoding, the value `openssl x509 -noout -fingerprint -sha256` prints |
|
||||
| `--ssl-serial` | Serial number of the leaf certificate |
|
||||
| `--ssl-verified` | `verified by a certificate authority` when the certificate chain verifies against the system CA bundle, `self signed` otherwise |
|
||||
| `--ssl-expires-at` | Certificate `notAfter` timestamp |
|
||||
| `--ssl-starts-at` | Certificate `notBefore` timestamp |
|
||||
|
||||
@@ -20,14 +20,16 @@ func ReportSingleApp(appName string, format string, infoFlag string) error {
|
||||
return err
|
||||
}
|
||||
flags = map[string]common.ReportFunc{
|
||||
"--ssl-dir": reportSSLDir,
|
||||
"--ssl-enabled": reportSSLEnabled,
|
||||
"--ssl-hostnames": reportSSLHostnames,
|
||||
"--ssl-expires-at": reportSSLExpiresAt,
|
||||
"--ssl-issuer": reportSSLIssuer,
|
||||
"--ssl-starts-at": reportSSLStartsAt,
|
||||
"--ssl-subject": reportSSLSubject,
|
||||
"--ssl-verified": reportSSLVerified,
|
||||
"--ssl-dir": reportSSLDir,
|
||||
"--ssl-enabled": reportSSLEnabled,
|
||||
"--ssl-hostnames": reportSSLHostnames,
|
||||
"--ssl-expires-at": reportSSLExpiresAt,
|
||||
"--ssl-fingerprint": reportSSLFingerprint,
|
||||
"--ssl-issuer": reportSSLIssuer,
|
||||
"--ssl-serial": reportSSLSerial,
|
||||
"--ssl-starts-at": reportSSLStartsAt,
|
||||
"--ssl-subject": reportSSLSubject,
|
||||
"--ssl-verified": reportSSLVerified,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -78,6 +80,21 @@ func opensslCertText(appName string) string {
|
||||
return result.Stdout
|
||||
}
|
||||
|
||||
// opensslCertOption runs "openssl x509 -in <server.crt> -noout" with the given
|
||||
// output options and returns the trimmed stdout, or an empty string when openssl
|
||||
// cannot read the certificate.
|
||||
func opensslCertOption(appName string, args ...string) string {
|
||||
result, err := common.CallExecCommand(common.ExecCommandInput{
|
||||
Command: "openssl",
|
||||
Args: append([]string{"x509", "-in", filepath.Join(certTLSPath(appName), "server.crt"), "-noout"}, args...),
|
||||
})
|
||||
if err != nil {
|
||||
return ""
|
||||
}
|
||||
|
||||
return result.StdoutContents()
|
||||
}
|
||||
|
||||
func reportSSLDir(appName string) string {
|
||||
return certTLSPath(appName)
|
||||
}
|
||||
@@ -124,6 +141,37 @@ func reportSSLStartsAt(appName string) string {
|
||||
return ""
|
||||
}
|
||||
|
||||
func reportSSLFingerprint(appName string) string {
|
||||
if !isSSLEnabled(appName) {
|
||||
return ""
|
||||
}
|
||||
|
||||
return formatSSLHexField(opensslCertOption(appName, "-fingerprint", "-sha256"))
|
||||
}
|
||||
|
||||
func reportSSLSerial(appName string) string {
|
||||
if !isSSLEnabled(appName) {
|
||||
return ""
|
||||
}
|
||||
|
||||
return formatSSLHexField(opensslCertOption(appName, "-serial"))
|
||||
}
|
||||
|
||||
// formatSSLHexField extracts the value from an openssl "key=HEX" output line, as
|
||||
// printed by "-fingerprint" and "-serial". The key is discarded rather than
|
||||
// matched against a literal: OpenSSL 3.x labels the digest "sha256" where
|
||||
// OpenSSL 1.x and LibreSSL label it "SHA256". Neither a fingerprint nor a serial
|
||||
// can contain an "=", so cutting at the first one is unambiguous. Only hex
|
||||
// values may be passed through here, as uppercasing would corrupt a subject.
|
||||
func formatSSLHexField(out string) string {
|
||||
_, value, found := strings.Cut(strings.TrimSpace(out), "=")
|
||||
if !found {
|
||||
return ""
|
||||
}
|
||||
|
||||
return strings.ToUpper(strings.TrimSpace(value))
|
||||
}
|
||||
|
||||
func reportSSLIssuer(appName string) string {
|
||||
if !isSSLEnabled(appName) {
|
||||
return ""
|
||||
@@ -144,15 +192,7 @@ func reportSSLSubject(appName string) string {
|
||||
return ""
|
||||
}
|
||||
|
||||
result, err := common.CallExecCommand(common.ExecCommandInput{
|
||||
Command: "openssl",
|
||||
Args: []string{"x509", "-in", filepath.Join(certTLSPath(appName), "server.crt"), "-noout", "-subject", "-nameopt", "compat"},
|
||||
})
|
||||
if err != nil {
|
||||
return ""
|
||||
}
|
||||
|
||||
return formatSSLSubject(result.StdoutContents())
|
||||
return formatSSLSubject(opensslCertOption(appName, "-subject", "-nameopt", "compat"))
|
||||
}
|
||||
|
||||
// formatSSLSubject normalizes an openssl "-subject -nameopt compat" line into a
|
||||
@@ -203,14 +243,7 @@ func reportSSLHostnames(appName string) string {
|
||||
return ""
|
||||
}
|
||||
|
||||
subject := ""
|
||||
subjectResult, err := common.CallExecCommand(common.ExecCommandInput{
|
||||
Command: "openssl",
|
||||
Args: []string{"x509", "-in", filepath.Join(certTLSPath(appName), "server.crt"), "-noout", "-subject", "-nameopt", "RFC2253"},
|
||||
})
|
||||
if err == nil {
|
||||
subject = subjectResult.StdoutContents()
|
||||
}
|
||||
subject := opensslCertOption(appName, "-subject", "-nameopt", "RFC2253")
|
||||
|
||||
return strings.Join(sslHostnames(subject, opensslCertText(appName)), " ")
|
||||
}
|
||||
|
||||
@@ -112,3 +112,30 @@ func TestFormatSSLSubject(t *testing.T) {
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestFormatSSLHexField(t *testing.T) {
|
||||
cases := []struct {
|
||||
name string
|
||||
in string
|
||||
want string
|
||||
}{
|
||||
{"fingerprint openssl 1.x label", "SHA256 Fingerprint=B7:DF:D5:84:C6:2E:27:BF", "B7:DF:D5:84:C6:2E:27:BF"},
|
||||
{"fingerprint openssl 3.x label", "sha256 Fingerprint=B7:DF:D5:84:C6:2E:27:BF", "B7:DF:D5:84:C6:2E:27:BF"},
|
||||
{"fingerprint lowercase digest", "sha256 Fingerprint=b7:df:d5:84:c6:2e:27:bf", "B7:DF:D5:84:C6:2E:27:BF"},
|
||||
{"serial", "serial=322844AD8CD6D4FF76B05C50833AB91DEEDA2AD1", "322844AD8CD6D4FF76B05C50833AB91DEEDA2AD1"},
|
||||
{"serial shorter", "serial=C46823E5D7C09FC9", "C46823E5D7C09FC9"},
|
||||
{"serial lowercase", "serial=c46823e5d7c09fc9", "C46823E5D7C09FC9"},
|
||||
{"serial negative", "serial=-1F2E", "-1F2E"},
|
||||
{"surrounding whitespace", " serial=C46823E5D7C09FC9 \n", "C46823E5D7C09FC9"},
|
||||
{"no separator", "unable to load certificate", ""},
|
||||
{"empty", "", ""},
|
||||
}
|
||||
|
||||
for _, tc := range cases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
if got := formatSSLHexField(tc.in); got != tc.want {
|
||||
t.Errorf("formatSSLHexField(%q) = %q, want %q", tc.in, got, tc.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
2
tests.mk
2
tests.mk
@@ -191,7 +191,7 @@ go-tests:
|
||||
@$(MAKE) go-test-plugin PLUGIN_NAME=traefik-vhosts
|
||||
|
||||
go-test-plugin:
|
||||
cd plugins/$(PLUGIN_NAME) && go get github.com/onsi/gomega && DOKKU_ROOT=/home/dokku DOKKU_LIB_ROOT=/var/lib/dokku go test -v -p 1 -race -mod=readonly || exit $$?
|
||||
cd plugins/$(PLUGIN_NAME) && DOKKU_ROOT=/home/dokku DOKKU_LIB_ROOT=/var/lib/dokku go test -v -p 1 -race -mod=readonly || exit $$?
|
||||
|
||||
go-test-plugin-in-docker:
|
||||
@echo running go unit tests...
|
||||
|
||||
@@ -53,6 +53,18 @@ teardown() {
|
||||
assert_success
|
||||
assert_output_contains "/$TEST_APP/tls"
|
||||
|
||||
run /bin/bash -c "dokku certs:report $TEST_APP --ssl-fingerprint"
|
||||
echo "output: $output"
|
||||
echo "status: $status"
|
||||
assert_success
|
||||
assert_output ""
|
||||
|
||||
run /bin/bash -c "dokku certs:report $TEST_APP --ssl-serial"
|
||||
echo "output: $output"
|
||||
echo "status: $status"
|
||||
assert_success
|
||||
assert_output ""
|
||||
|
||||
run /bin/bash -c "dokku certs:report $TEST_APP --ssl-invalid-flag"
|
||||
echo "output: $output"
|
||||
echo "status: $status"
|
||||
@@ -118,6 +130,75 @@ teardown() {
|
||||
assert_output "node-js-app.dokku.me"
|
||||
}
|
||||
|
||||
@test "(certs:report) reports the certificate fingerprint and serial" {
|
||||
local EXPECTED_FINGERPRINT EXPECTED_SERIAL
|
||||
EXPECTED_FINGERPRINT="$(openssl x509 -in "$BATS_TMPDIR/tls/server.crt" -noout -fingerprint -sha256 | cut -d= -f2-)"
|
||||
EXPECTED_SERIAL="$(openssl x509 -in "$BATS_TMPDIR/tls/server.crt" -noout -serial | cut -d= -f2-)"
|
||||
|
||||
run /bin/bash -c "dokku certs:add $TEST_APP $BATS_TMPDIR/tls/server.crt $BATS_TMPDIR/tls/server.key"
|
||||
echo "output: $output"
|
||||
echo "status: $status"
|
||||
assert_success
|
||||
|
||||
run /bin/bash -c "dokku certs:report $TEST_APP --ssl-fingerprint"
|
||||
echo "output: $output"
|
||||
echo "status: $status"
|
||||
assert_success
|
||||
assert_output "$EXPECTED_FINGERPRINT"
|
||||
|
||||
run /bin/bash -c "dokku certs:report $TEST_APP --ssl-serial"
|
||||
echo "output: $output"
|
||||
echo "status: $status"
|
||||
assert_success
|
||||
assert_output "$EXPECTED_SERIAL"
|
||||
|
||||
run /bin/bash -c "dokku certs:report $TEST_APP --format json | jq -r '.fingerprint'"
|
||||
echo "output: $output"
|
||||
echo "status: $status"
|
||||
assert_success
|
||||
assert_output "$EXPECTED_FINGERPRINT"
|
||||
|
||||
run /bin/bash -c "dokku certs:report $TEST_APP --format json | jq -r '.serial'"
|
||||
echo "output: $output"
|
||||
echo "status: $status"
|
||||
assert_success
|
||||
assert_output "$EXPECTED_SERIAL"
|
||||
}
|
||||
|
||||
@test "(certs:report) fingerprint and serial follow a replaced certificate" {
|
||||
local EXPECTED_FINGERPRINT EXPECTED_SERIAL
|
||||
EXPECTED_FINGERPRINT="$(openssl x509 -in "$BATS_TMPDIR/tls/domain.com.crt" -noout -fingerprint -sha256 | cut -d= -f2-)"
|
||||
EXPECTED_SERIAL="$(openssl x509 -in "$BATS_TMPDIR/tls/domain.com.crt" -noout -serial | cut -d= -f2-)"
|
||||
|
||||
run /bin/bash -c "dokku certs:add $TEST_APP $BATS_TMPDIR/tls/server.crt $BATS_TMPDIR/tls/server.key"
|
||||
echo "output: $output"
|
||||
echo "status: $status"
|
||||
assert_success
|
||||
|
||||
run /bin/bash -c "dokku certs:report $TEST_APP --ssl-fingerprint"
|
||||
echo "output: $output"
|
||||
echo "status: $status"
|
||||
assert_success
|
||||
assert_not_output "$EXPECTED_FINGERPRINT"
|
||||
|
||||
run /bin/bash -c "dokku certs:update $TEST_APP $BATS_TMPDIR/tls/domain.com.crt $BATS_TMPDIR/tls/domain.com.key"
|
||||
echo "output: $output"
|
||||
echo "status: $status"
|
||||
assert_success
|
||||
|
||||
run /bin/bash -c "dokku certs:report $TEST_APP --ssl-fingerprint"
|
||||
echo "output: $output"
|
||||
echo "status: $status"
|
||||
assert_success
|
||||
assert_output "$EXPECTED_FINGERPRINT"
|
||||
|
||||
run /bin/bash -c "dokku certs:report $TEST_APP --ssl-serial"
|
||||
echo "output: $output"
|
||||
echo "status: $status"
|
||||
assert_success
|
||||
assert_output "$EXPECTED_SERIAL"
|
||||
}
|
||||
|
||||
@test "(certs:report) reports the CN and all SANs as hostnames" {
|
||||
local SANS_TLS="$BATS_TMPDIR/tls-sans"
|
||||
mkdir -p "$SANS_TLS"
|
||||
|
||||
Reference in New Issue
Block a user