Skip to content

Commit

Permalink
Merge pull request #5 from sonatype-nexus-community/feat/support-jks-…
Browse files Browse the repository at this point in the history
…trust-store

feat: support custom Java Keystore (JKS format) as a trust store
  • Loading branch information
madpah authored Oct 18, 2024
2 parents 9e8e70a + acac9a3 commit 428492d
Show file tree
Hide file tree
Showing 7 changed files with 225 additions and 24 deletions.
98 changes: 98 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,14 +17,112 @@ Unpack it, and make sure the file is executable.

## Running

See all the options:

```
./ssl-inspector --help
Usage: ssl-inspector [OPTOINS]
-X Enable debug logging
-endpoint string
Endpoint to inspect SSL on. Can be https://domain (assuming port 443) or a domain and port after a colon (:)
-trustStore string
Path to an optional JKS trust store
-trustStorePassphrase string
Passphrase for optional JKS trust store
```

Example for a known expired certificate at the endpoint:

```
./ssl-inspector -endpoint expired.badssl.com:443
____ ____ _ _ _ ____ ____ _____ ____ _____ ____ ____
/ ___\/ ___\/ \ / \/ \ /|/ ___\/ __\/ __// _\/__ __\/ _ \/ __\
| \| \| | | || |\ ||| \| \/|| \ | / / \ | / \|| \/|
\___ |\___ || |_/\ | || | \||\___ || __/| /_ | \__ | | | \_/|| /
\____/\____/\____/ \_/\_/ \|\____/\_/ \____\\____/ \_/ \____/\_/\_\
Running on: darwin/arm64
Version: development
Validated - conducting test against: expired.badssl.com:443
❌ Connection to expired.badssl.com:443 will not work.
There are 1 certificate errors connecting to expired.badssl.com:443. They are:
[1] - Certifcate for CN=*.badssl.com,OU=Domain Control Validated+OU=PositiveSSL Wildcard is invalid because: Certificate expired
```

Example for a known certificate that is signed by a CA that is not trusted:

```
./ssl-inspector -endpoint untrusted-root.badssl.com:443
____ ____ _ _ _ ____ ____ _____ ____ _____ ____ ____
/ ___\/ ___\/ \ / \/ \ /|/ ___\/ __\/ __// _\/__ __\/ _ \/ __\
| \| \| | | || |\ ||| \| \/|| \ | / / \ | / \|| \/|
\___ |\___ || |_/\ | || | \||\___ || __/| /_ | \__ | | | \_/|| /
\____/\____/\____/ \_/\_/ \|\____/\_/ \____\\____/ \_/ \____/\_/\_\
Running on: darwin/arm64
Version: development
Validated - conducting test against: untrusted-root.badssl.com:443
❌ Connection to untrusted-root.badssl.com:443 will not work.
There are 1 certificate errors connecting to untrusted-root.badssl.com:443. They are:
[1] - Certifcate for CN=*.badssl.com,O=BadSSL,L=San Francisco,ST=California,C=US is not trusted. This could be because:
1. It is self-signed
2. It is signed by an unknown authority
3. The CA that signed this certificate is not a invalid Certificate Authority
It was signed by: BadSSL Untrusted Root Certificate Authority
```

Example for a known certificate that is signed by a CA that is included in the supplied trust store:

```
./ssl-inspector -endpoint untrusted-root.badssl.com:443 -trustStore ./test-data/test-keystore.jks -trustStorePassphrase changeit
____ ____ _ _ _ ____ ____ _____ ____ _____ ____ ____
/ ___\/ ___\/ \ / \/ \ /|/ ___\/ __\/ __// _\/__ __\/ _ \/ __\
| \| \| | | || |\ ||| \| \/|| \ | / / \ | / \|| \/|
\___ |\___ || |_/\ | || | \||\___ || __/| /_ | \__ | | | \_/|| /
\____/\____/\____/ \_/\_/ \|\____/\_/ \____\\____/ \_/ \____/\_/\_\
Running on: darwin/arm64
Version: development
Validated - conducting test against: untrusted-root.badssl.com:443
- Loaded Custom CA: BadSSL Untrusted Root Certificate Authority
✅ All checked passed connecting to untrusted-root.badssl.com:443
```

## Testing with a custom Trust Store

A custom Java Keystore (JKS) can be used to provide additional trusted certificate authorities that may not be in the Operating System trust store.

1. Obtain the Certifcate for the CA you wish to trust (in PEM format) - try `https://untrusted-root.badssl.com` as a good example!
2. Load into a JKS:
```
keytool -importcert -storetype jks -file my-ca.pem -alias BadSSLCA -storepass changeit -keystore test-keystore.jks
```

## The Fine Print

Remember:
Expand Down
1 change: 1 addition & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ require (
)

require (
github.com/pavlo-v-chernykh/keystore-go/v4 v4.5.0
github.com/stretchr/testify v1.9.0
golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8 // indirect
)
2 changes: 2 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/pavlo-v-chernykh/keystore-go/v4 v4.5.0 h1:2nosf3P75OZv2/ZO/9Px5ZgZ5gbKrzA3joN1QMfOGMQ=
github.com/pavlo-v-chernykh/keystore-go/v4 v4.5.0/go.mod h1:lAVhWwbNaveeJmxrxuSTxMgKpF6DjnuVpn6T8WiBwYQ=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ=
Expand Down
90 changes: 74 additions & 16 deletions ssl-inspector.go
Original file line number Diff line number Diff line change
Expand Up @@ -27,19 +27,24 @@ import (
"runtime"
"strings"

"github.com/pavlo-v-chernykh/keystore-go/v4"
log "github.com/sirupsen/logrus"
)

var (
currentRuntime string = runtime.GOOS
debugLogging bool = false
testEndpoint string
version string = "development"
currentRuntime string = runtime.GOOS
debugLogging bool = false
testEndpoint string
trustStorePassphrase string
trustStorePath string
version string = "development"
)

func init() {
flag.BoolVar(&debugLogging, "X", false, "Enable debug logging")
flag.StringVar(&testEndpoint, "endpoint", "", "Endpoint to inspect SSL on. Can be https://domain (assuming port 443) or a domain and port after a colon (:)")
flag.StringVar(&trustStorePath, "trustStore", "", "Path to an optional JKS trust store")
flag.StringVar(&trustStorePassphrase, "trustStorePassphrase", "", "Passphrase for optional JKS trust store")
}

func usage() {
Expand All @@ -58,9 +63,16 @@ func main() {
flag.Usage = usage
flag.Parse()

println("")
println(fmt.Sprintf(" Running on: %s/%s", currentRuntime, runtime.GOARCH))
println(fmt.Sprintf(" Scanner Version: %s", version))
println(`
____ ____ _ _ _ ____ ____ _____ ____ _____ ____ ____
/ ___\/ ___\/ \ / \/ \ /|/ ___\/ __\/ __// _\/__ __\/ _ \/ __\
| \| \| | | || |\ ||| \| \/|| \ | / / \ | / \|| \/|
\___ |\___ || |_/\ | || | \||\___ || __/| /_ | \__ | | | \_/|| /
\____/\____/\____/ \_/\_/ \|\____/\_/ \____\\____/ \_/ \____/\_/\_\
`)
println(fmt.Sprintf("Running on: %s/%s", currentRuntime, runtime.GOARCH))
println(fmt.Sprintf("Version: %s", version))
println("")

validatedEndpoint, err := validateEndpoint(testEndpoint)
Expand All @@ -70,23 +82,46 @@ func main() {
} else {
println("Validated - conducting test against: " + *validatedEndpoint)
}
println("")

// Load System Root CAs
// rootCAs, _ := x509.SystemCertPool()
// if rootCAs == nil {
// rootCAs = x509.NewCertPool()
// }
rootCAs, _ := x509.SystemCertPool()
if rootCAs == nil {
rootCAs = x509.NewCertPool()
}

// Load any custom Trust Store too
if trustStorePath != "" {
keystore := readKeyStore(trustStorePath, []byte(trustStorePassphrase))

for _, a := range keystore.Aliases() {
tce, err := keystore.GetTrustedCertificateEntry(a)
if err != nil {
log.Fatal(err)
}

cert, err := x509.ParseCertificates(tce.Certificate.Content)
if err != nil {
log.Fatal(err)
}

rootCAs.AddCert(cert[0])
println("- Loaded Custom CA: " + cert[0].Subject.CommonName)
}
println("")
println("")
}

valid, messages, err := checkSSL(*validatedEndpoint)
valid, messages, err := checkSSL(*validatedEndpoint, rootCAs)
if err != nil {
println(fmt.Sprintf("Error performing checks: %v", err))
os.Exit(1)
}

if valid {
println(fmt.Sprintf("All checked passed connecting to %s", *validatedEndpoint))
println(fmt.Sprintf("All checked passed connecting to %s", *validatedEndpoint))
} else {
println(fmt.Sprintf("!!! Connection to %s will not work. !!!", *validatedEndpoint))
println(fmt.Sprintf(" Connection to %s will not work", *validatedEndpoint))
println("")
println(fmt.Sprintf("There are %d certificate errors connecting to %s. They are:", len(messages), *validatedEndpoint))
println("")
Expand All @@ -102,11 +137,11 @@ func main() {
*
* Returns true/false, list of messages and optionally an error.
*/
func checkSSL(endpoint string) (bool, []string, error) {
func checkSSL(endpoint string, rootCAs *x509.CertPool) (bool, []string, error) {
messages := make([]string, 0)
config := &tls.Config{
// InsecureSkipVerify: *insecure,
// RootCAs: rootCAs,
RootCAs: rootCAs,
}

_, err := tls.Dial("tcp", endpoint, config)
Expand Down Expand Up @@ -151,6 +186,29 @@ func checkSSL(endpoint string) (bool, []string, error) {
return true, messages, nil
}

/**
* Read JKS from path
*/
func readKeyStore(filename string, password []byte) keystore.KeyStore {
f, err := os.Open(filename)
if err != nil {
log.Fatal(err)
}

defer func() {
if err := f.Close(); err != nil {
log.Fatal(err)
}
}()

ks := keystore.New()
if err := ks.Load(f, password); err != nil {
log.Fatal(err) //nolint: gocritic
}

return ks
}

/**
* Validates the supplied endpoint returning the domain:port or error.
*/
Expand Down
21 changes: 13 additions & 8 deletions ssl-inspector_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,54 +17,57 @@
package main

import (
"crypto/x509"
"runtime"
"testing"

"github.com/stretchr/testify/assert"
)

func TestCheckSSL(t *testing.T) {
rootCAs, _ := x509.SystemCertPool()

t.Run("validAndTrustedByOS", func(t *testing.T) {
valid, messages, err := checkSSL("badssl.com:443")
valid, messages, err := checkSSL("badssl.com:443", rootCAs)
assert.NoError(t, err)
assert.Equal(t, 0, len(messages))
assert.True(t, valid)
})

t.Run("expiredAndTrustedByOS", func(t *testing.T) {
valid, messages, err := checkSSL("expired.badssl.com:443")
valid, messages, err := checkSSL("expired.badssl.com:443", rootCAs)
assert.NoError(t, err)
assert.Equal(t, 1, len(messages))
assert.Equal(t, "Certifcate for CN=*.badssl.com,OU=Domain Control Validated+OU=PositiveSSL Wildcard is invalid because: Certificate expired", messages[0])
assert.False(t, valid)
})

t.Run("expiredAndTrustedByOSWithPort", func(t *testing.T) {
valid, messages, err := checkSSL("expired.badssl.com:443")
valid, messages, err := checkSSL("expired.badssl.com:443", rootCAs)
assert.NoError(t, err)
assert.Equal(t, 1, len(messages))
assert.Equal(t, "Certifcate for CN=*.badssl.com,OU=Domain Control Validated+OU=PositiveSSL Wildcard is invalid because: Certificate expired", messages[0])
assert.False(t, valid)
})

t.Run("wrongHostAndTrustedByOS", func(t *testing.T) {
valid, messages, err := checkSSL("wrong.host.badssl.com:443")
valid, messages, err := checkSSL("wrong.host.badssl.com:443", rootCAs)
assert.NoError(t, err)
assert.Equal(t, 1, len(messages))
assert.Equal(t, "Certifcate for CN=*.badssl.com is not valid for wrong.host.badssl.com:443", messages[0])
assert.False(t, valid)
})

t.Run("unknownAuthorityAndTrustedByOS", func(t *testing.T) {
valid, messages, err := checkSSL("self-signed.badssl.com:443")
valid, messages, err := checkSSL("self-signed.badssl.com:443", rootCAs)
assert.NoError(t, err)
assert.Equal(t, 1, len(messages))
assert.Equal(t, "Certifcate for CN=*.badssl.com,O=BadSSL,L=San Francisco,ST=California,C=US is not trusted. This could be because:\n\t1. It is self-signed\n\t2. It is signed by an unknown authority\n\t3. The CA that signed this certificate is not a invalid Certificate Authority\n\t\n\tIt was signed by: *.badssl.com", messages[0])
assert.False(t, valid)
})

t.Run("untrustedRootAndTrustedByOS", func(t *testing.T) {
valid, messages, err := checkSSL("untrusted-root.badssl.com:443")
valid, messages, err := checkSSL("untrusted-root.badssl.com:443", rootCAs)
assert.NoError(t, err)
assert.Equal(t, 1, len(messages))
assert.False(t, valid)
Expand All @@ -73,7 +76,7 @@ func TestCheckSSL(t *testing.T) {
if runtime.GOOS == "darwin" {
// Looks like Windows and Linux do not do realtime CRL checks
t.Run("revokedAndTrustedByOS", func(t *testing.T) {
valid, messages, err := checkSSL("revoked.badssl.com:443")
valid, messages, err := checkSSL("revoked.badssl.com:443", rootCAs)
assert.NoError(t, err)
assert.Equal(t, 1, len(messages))
assert.Equal(t, "Certifcate for revoked.badssl.com:443 is invalid because: tls: failed to verify certificate: x509: “revoked.badssl.com” certificate is revoked", messages[0])
Expand All @@ -83,7 +86,9 @@ func TestCheckSSL(t *testing.T) {
}

func TestSingleCert(t *testing.T) {
valid, messages, err := checkSSL("expired.badssl.com:443")
rootCAs, _ := x509.SystemCertPool()

valid, messages, err := checkSSL("expired.badssl.com:443", rootCAs)
assert.NoError(t, err)
assert.Equal(t, 1, len(messages))
for _, m := range messages {
Expand Down
37 changes: 37 additions & 0 deletions test-data/BadSSL Untrusted Root Certificate Authority.pem
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
-----BEGIN CERTIFICATE-----
MIIGfjCCBGagAwIBAgIJAJeg/PrX5Sj9MA0GCSqGSIb3DQEBCwUAMIGBMQswCQYD
VQQGEwJVUzETMBEGA1UECAwKQ2FsaWZvcm5pYTEWMBQGA1UEBwwNU2FuIEZyYW5j
aXNjbzEPMA0GA1UECgwGQmFkU1NMMTQwMgYDVQQDDCtCYWRTU0wgVW50cnVzdGVk
IFJvb3QgQ2VydGlmaWNhdGUgQXV0aG9yaXR5MB4XDTE2MDcwNzA2MzEzNVoXDTM2
MDcwMjA2MzEzNVowgYExCzAJBgNVBAYTAlVTMRMwEQYDVQQIDApDYWxpZm9ybmlh
MRYwFAYDVQQHDA1TYW4gRnJhbmNpc2NvMQ8wDQYDVQQKDAZCYWRTU0wxNDAyBgNV
BAMMK0JhZFNTTCBVbnRydXN0ZWQgUm9vdCBDZXJ0aWZpY2F0ZSBBdXRob3JpdHkw
ggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQDKQtPMhEH073gis/HISWAi
bOEpCtOsatA3JmeVbaWal8O/5ZO5GAn9dFVsGn0CXAHR6eUKYDAFJLa/3AhjBvWa
tnQLoXaYlCvBjodjLEaFi8ckcJHrAYG9qZqioRQ16Yr8wUTkbgZf+er/Z55zi1yn
CnhWth7kekvrwVDGP1rApeLqbhYCSLeZf5W/zsjLlvJni9OrU7U3a9msvz8mcCOX
fJX9e3VbkD/uonIbK2SvmAGMaOj/1k0dASkZtMws0Bk7m1pTQL+qXDM/h3BQZJa5
DwTcATaa/Qnk6YHbj/MaS5nzCSmR0Xmvs/3CulQYiZJ3kypns1KdqlGuwkfiCCgD
yWJy7NE9qdj6xxLdqzne2DCyuPrjFPS0mmYimpykgbPnirEPBF1LW3GJc9yfhVXE
Cc8OY8lWzxazDNNbeSRDpAGbBeGSQXGjAbliFJxwLyGzZ+cG+G8lc+zSvWjQu4Xp
GJ+dOREhQhl+9U8oyPX34gfKo63muSgo539hGylqgQyzj+SX8OgK1FXXb2LS1gxt
VIR5Qc4MmiEG2LKwPwfU8Yi+t5TYjGh8gaFv6NnksoX4hU42gP5KvjYggDpR+NSN
CGQSWHfZASAYDpxjrOo+rk4xnO+sbuuMk7gORsrl+jgRT8F2VqoR9Z3CEdQxcCjR
5FsfTymZCk3GfIbWKkaeLQIDAQABo4H2MIHzMB0GA1UdDgQWBBRvx4NzSbWnY/91
3m1u/u37l6MsADCBtgYDVR0jBIGuMIGrgBRvx4NzSbWnY/913m1u/u37l6MsAKGB
h6SBhDCBgTELMAkGA1UEBhMCVVMxEzARBgNVBAgMCkNhbGlmb3JuaWExFjAUBgNV
BAcMDVNhbiBGcmFuY2lzY28xDzANBgNVBAoMBkJhZFNTTDE0MDIGA1UEAwwrQmFk
U1NMIFVudHJ1c3RlZCBSb290IENlcnRpZmljYXRlIEF1dGhvcml0eYIJAJeg/PrX
5Sj9MAwGA1UdEwQFMAMBAf8wCwYDVR0PBAQDAgEGMA0GCSqGSIb3DQEBCwUAA4IC
AQBQU9U8+jTRT6H9AIFm6y50tXTg/ySxRNmeP1Ey9Zf4jUE6yr3Q8xBv9gTFLiY1
qW2qfkDSmXVdBkl/OU3+xb5QOG5hW7wVolWQyKREV5EvUZXZxoH7LVEMdkCsRJDK
wYEKnEErFls5WPXY3bOglBOQqAIiuLQ0f77a2HXULDdQTn5SueW/vrA4RJEKuWxU
iD9XPnVZ9tPtky2Du7wcL9qhgTddpS/NgAuLO4PXh2TQ0EMCll5reZ5AEr0NSLDF
c/koDv/EZqB7VYhcPzr1bhQgbv1dl9NZU0dWKIMkRE/T7vZ97I3aPZqIapC2ulrf
KrlqjXidwrGFg8xbiGYQHPx3tHPZxoM5WG2voI6G3s1/iD+B4V6lUEvivd3f6tq7
d1V/3q1sL5DNv7TvaKGsq8g5un0TAkqaewJQ5fXLigF/yYu5a24/GUD783MdAPFv
gWz8F81evOyRfpf9CAqIswMF+T6Dwv3aw5L9hSniMrblkg+ai0K22JfoBcGOzMtB
Ke/Ps2Za56dTRoY/a4r62hrcGxufXd0mTdPaJLw3sJeHYjLxVAYWQq4QKJQWDgTS
dAEWyN2WXaBFPx5c8KIW95Eu8ShWE00VVC3oA4emoZ2nrzBXLrUScifY6VaYYkkR
2O2tSqU8Ri3XRdgpNPDWp8ZL49KhYGYo3R/k98gnMHiY5g==
-----END CERTIFICATE-----
Binary file added test-data/test-keystore.jks
Binary file not shown.

0 comments on commit 428492d

Please sign in to comment.