Skip to content

Commit

Permalink
Expose raw cert info through tlsinfo (#479)
Browse files Browse the repository at this point in the history
* Expose raw cert info through tlsinfo

In some types of debugging, it's useful to get a more detailed view of a certificate than what `tlsinfo get-cert` currently provides. The existing fields that we parse out are the most commonly useful ones so they're good to show by default, but I'd like it to be possible to display more. My solution is to pass back the full raw certificate and allow printing it if a flag is set.

We transfer the cert over the wire in binary ASN.1 DER format because it's more compact and it's easy to get from the libraries we're using, and we print it in PEM to avoid issues with unprintable characters and to keep it in a format that many CLI tools understand.

I confirmed that generated PEMs were readable with `keytool -printcert -file` and `openssl x509 -in`.


Example commands:

```
$ go run ./cmd/sanssh -targets localhost tlsinfo get-certs www.example.com:443 
---Server Certificate--- 0
Issuer: CN=DigiCert Global G2 TLS RSA SHA256 2020 CA1,O=DigiCert Inc,C=US
Subject: CN=www.example.org,O=Internet Corporation for Assigned Names and Numbers,L=Los Angeles,ST=California,C=US
NotBefore: 2024-01-29 16:00:00 -0800 PST
NotAfter: 2025-03-01 15:59:59 -0800 PST
DNS Names: [www.example.org example.net example.edu example.com example.org www.example.com www.example.edu www.example.net]
IP Addresses: []

---Server Certificate--- 1
Issuer: CN=DigiCert Global Root G2,OU=www.digicert.com,O=DigiCert Inc,C=US
Subject: CN=DigiCert Global G2 TLS RSA SHA256 2020 CA1,O=DigiCert Inc,C=US
NotBefore: 2021-03-29 17:00:00 -0700 PDT
NotAfter: 2031-03-29 16:59:59 -0700 PDT
DNS Names: []
IP Addresses: []
$ go run ./cmd/sanssh -targets localhost tlsinfo get-certs -pem www.example.com:443 
-----BEGIN CERTIFICATE-----
MIIHbjCCBlagAwIBAgIQB1vO8waJyK3fE+Ua9K/hhzANBgkqhkiG9w0BAQsFADBZ
MQswCQYDVQQGEwJVUzEVMBMGA1UEChMMRGlnaUNlcnQgSW5jMTMwMQYDVQQDEypE
aWdpQ2VydCBHbG9iYWwgRzIgVExTIFJTQSBTSEEyNTYgMjAyMCBDQTEwHhcNMjQw
MTMwMDAwMDAwWhcNMjUwMzAxMjM1OTU5WjCBljELMAkGA1UEBhMCVVMxEzARBgNV
BAgTCkNhbGlmb3JuaWExFDASBgNVBAcTC0xvcyBBbmdlbGVzMUIwQAYDVQQKDDlJ
bnRlcm5ldMKgQ29ycG9yYXRpb27CoGZvcsKgQXNzaWduZWTCoE5hbWVzwqBhbmTC
oE51bWJlcnMxGDAWBgNVBAMTD3d3dy5leGFtcGxlLm9yZzCCASIwDQYJKoZIhvcN
AQEBBQADggEPADCCAQoCggEBAIaFD7sO+cpf2fXgCjIsM9mqDgcpqC8IrXi9wga/
9y0rpqcnPVOmTMNLsid3INbBVEm4CNr5cKlh9rJJnWlX2vttJDRyLkfwBD+dsVvi
vGYxWTLmqX6/1LDUZPVrynv/cltemtg/1Aay88jcj2ZaRoRmqBgVeacIzgU8+zmJ
7236TnFSe7fkoKSclsBhPaQKcE3Djs1uszJs8sdECQTdoFX9I6UgeLKFXtg7rRf/
hcW5dI0zubhXbrW8aWXbCzySVZn0c7RkJMpnTCiZzNxnPXnHFpwr5quqqjVyN/aB
KkjoP04Zmr+eRqoyk/+lslq0sS8eaYSSHbC5ja/yMWyVhvMCAwEAAaOCA/IwggPu
MB8GA1UdIwQYMBaAFHSFgMBmx9833s+9KTeqAx2+7c0XMB0GA1UdDgQWBBRM/tAS
TS4hz2v68vK4TEkCHTGRijCBgQYDVR0RBHoweIIPd3d3LmV4YW1wbGUub3Jnggtl
eGFtcGxlLm5ldIILZXhhbXBsZS5lZHWCC2V4YW1wbGUuY29tggtleGFtcGxlLm9y
Z4IPd3d3LmV4YW1wbGUuY29tgg93d3cuZXhhbXBsZS5lZHWCD3d3dy5leGFtcGxl
Lm5ldDA+BgNVHSAENzA1MDMGBmeBDAECAjApMCcGCCsGAQUFBwIBFhtodHRwOi8v
d3d3LmRpZ2ljZXJ0LmNvbS9DUFMwDgYDVR0PAQH/BAQDAgWgMB0GA1UdJQQWMBQG
CCsGAQUFBwMBBggrBgEFBQcDAjCBnwYDVR0fBIGXMIGUMEigRqBEhkJodHRwOi8v
Y3JsMy5kaWdpY2VydC5jb20vRGlnaUNlcnRHbG9iYWxHMlRMU1JTQVNIQTI1NjIw
MjBDQTEtMS5jcmwwSKBGoESGQmh0dHA6Ly9jcmw0LmRpZ2ljZXJ0LmNvbS9EaWdp
Q2VydEdsb2JhbEcyVExTUlNBU0hBMjU2MjAyMENBMS0xLmNybDCBhwYIKwYBBQUH
AQEEezB5MCQGCCsGAQUFBzABhhhodHRwOi8vb2NzcC5kaWdpY2VydC5jb20wUQYI
KwYBBQUHMAKGRWh0dHA6Ly9jYWNlcnRzLmRpZ2ljZXJ0LmNvbS9EaWdpQ2VydEds
b2JhbEcyVExTUlNBU0hBMjU2MjAyMENBMS0xLmNydDAMBgNVHRMBAf8EAjAAMIIB
fQYKKwYBBAHWeQIEAgSCAW0EggFpAWcAdABOdaMnXJoQwzhbbNTfP1LrHfDgjhuN
acCx+mSxYpo53wAAAY1b0vxkAAAEAwBFMEMCH0BRCgxPbBBVxhcWZ26a8JCe83P1
JZ6wmv56GsVcyMACIDgpMbEo5HJITTRPnoyT4mG8cLrWjEvhchUdEcWUuk1TAHYA
fVkeEuF4KnscYWd8Xv340IdcFKBOlZ65Ay/ZDowuebgAAAGNW9L8MAAABAMARzBF
AiBdv5Z3pZFbfgoM3tGpCTM3ZxBMQsxBRSdTS6d8d2NAcwIhALLoCT9mTMN9OyFz
IBV5MkXVLyuTf2OAzAOa7d8x2H6XAHcA5tIxY0B3jMEQQQbXcbnOwdJA9paEhvu6
hzId/R43jlAAAAGNW9L8XwAABAMASDBGAiEA4Koh/VizdQU1tjZ2E2VGgWSXXkwn
QmiYhmAeKcVLHeACIQD7JIGFsdGol7kss2pe4lYrCgPVc+iGZkuqnj26hqhr0TAN
BgkqhkiG9w0BAQsFAAOCAQEABOFuAj4N4yNG9OOWNQWTNSICC4Rd4nOG1HRP/Bsn
rz7KrcPORtb6D+Jx+Q0amhO31QhIvVBYs14gY4Ypyj7MzHgm4VmPXcqLvEkxb2G9
Qv9hYuEiNSQmm1fr5QAN/0AzbEbCM3cImLJ69kP5bUjfv/76KB57is8tYf9sh5ik
LGKauxCM/zRIcGa3bXLDafk5S2g5Vr2hs230d/NGW1wZrE+zdGuMxfGJzJP+DAFv
iBfcQnFg4+1zMEKcqS87oniOyG+60RMM0MdejBD7AS43m9us96Gsun/4kufLQUTI
FfnzxLutUV++3seshgefQOy5C/ayi8y1VTNmujPCxPCi6Q==
-----END CERTIFICATE-----
-----BEGIN CERTIFICATE-----
MIIEyDCCA7CgAwIBAgIQDPW9BitWAvR6uFAsI8zwZjANBgkqhkiG9w0BAQsFADBh
MQswCQYDVQQGEwJVUzEVMBMGA1UEChMMRGlnaUNlcnQgSW5jMRkwFwYDVQQLExB3
d3cuZGlnaWNlcnQuY29tMSAwHgYDVQQDExdEaWdpQ2VydCBHbG9iYWwgUm9vdCBH
MjAeFw0yMTAzMzAwMDAwMDBaFw0zMTAzMjkyMzU5NTlaMFkxCzAJBgNVBAYTAlVT
MRUwEwYDVQQKEwxEaWdpQ2VydCBJbmMxMzAxBgNVBAMTKkRpZ2lDZXJ0IEdsb2Jh
bCBHMiBUTFMgUlNBIFNIQTI1NiAyMDIwIENBMTCCASIwDQYJKoZIhvcNAQEBBQAD
ggEPADCCAQoCggEBAMz3EGJPprtjb+2QUlbFbSd7ehJWivH0+dbn4Y+9lavyYEEV
cNsSAPonCrVXOFt9slGTcZUOakGUWzUb+nv6u8W+JDD+Vu/E832X4xT1FE3LpxDy
FuqrIvAxIhFhaZAmunjZlx/jfWardUSVc8is/+9dCopZQ+GssjoP80j812s3wWPc
3kbW20X+fSP9kOhRBx5Ro1/tSUZUfyyIxfQTnJcVPAPooTncaQwywa8WV0yUR0J8
osicfebUTVSvQpmowQTCd5zWSOTOEeAqgJnwQ3DPP3Zr0UxJqyRewg2C/Uaoq2yT
zGJSQnWS+Jr6Xl6ysGHlHx+5fwmY6D36g39HaaECAwEAAaOCAYIwggF+MBIGA1Ud
EwEB/wQIMAYBAf8CAQAwHQYDVR0OBBYEFHSFgMBmx9833s+9KTeqAx2+7c0XMB8G
A1UdIwQYMBaAFE4iVCAYlebjbuYP+vq5Eu0GF485MA4GA1UdDwEB/wQEAwIBhjAd
BgNVHSUEFjAUBggrBgEFBQcDAQYIKwYBBQUHAwIwdgYIKwYBBQUHAQEEajBoMCQG
CCsGAQUFBzABhhhodHRwOi8vb2NzcC5kaWdpY2VydC5jb20wQAYIKwYBBQUHMAKG
NGh0dHA6Ly9jYWNlcnRzLmRpZ2ljZXJ0LmNvbS9EaWdpQ2VydEdsb2JhbFJvb3RH
Mi5jcnQwQgYDVR0fBDswOTA3oDWgM4YxaHR0cDovL2NybDMuZGlnaWNlcnQuY29t
L0RpZ2lDZXJ0R2xvYmFsUm9vdEcyLmNybDA9BgNVHSAENjA0MAsGCWCGSAGG/WwC
ATAHBgVngQwBATAIBgZngQwBAgEwCAYGZ4EMAQICMAgGBmeBDAECAzANBgkqhkiG
9w0BAQsFAAOCAQEAkPFwyyiXaZd8dP3A+iZ7U6utzWX9upwGnIrXWkOH7U1MVl+t
wcW1BSAuWdH/SvWgKtiwla3JLko716f2b4gp/DA/JIS7w7d7kwcsr4drdjPtAFVS
slme5LnQ89/nD/7d+MS5EHKBCQRfz5eeLjJ1js+aWNJXMX43AYGyZm0pGrFmCW3R
bpD0ufovARTFXFZkAdl9h6g4U5+LXUZtXMYnhIHUfoyMo5tS58aI7Dd8KvvwVVo4
chDYABPPTHPbqjc1qCmBaZx2vN4Ye5DUys/vZwP9BFohFrH/6j/f3IL16/RZkiMN
JCqVJUzKoZHm1Lesh3Sz8W2jmdv51b2EQJ8HmA==
-----END CERTIFICATE-----
```

* Update styling and add explicit error on blank cert
  • Loading branch information
stvnrhodes authored Aug 28, 2024
1 parent e7ef4fa commit 87ace58
Show file tree
Hide file tree
Showing 7 changed files with 81 additions and 39 deletions.
4 changes: 4 additions & 0 deletions cmd/sansshell-server/default-policy.rego
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,10 @@ allow {
input.method = "/SysInfo.SysInfo/Dmesg"
}

allow {
input.method = "/TLSInfo.TLSInfo/GetTLSCertificate"
}

# Allow anything from a proxy
allow {
input.peer.principal.id = "proxy"
Expand Down
40 changes: 31 additions & 9 deletions services/tlsinfo/client/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ package client

import (
"context"
"encoding/pem"
"flag"
"fmt"
"os"
Expand All @@ -27,6 +28,7 @@ import (
"github.com/Snowflake-Labs/sansshell/client"
pb "github.com/Snowflake-Labs/sansshell/services/tlsinfo"
"github.com/Snowflake-Labs/sansshell/services/util"
cliUtils "github.com/Snowflake-Labs/sansshell/services/util/cli"
"github.com/google/subcommands"
)

Expand Down Expand Up @@ -62,6 +64,7 @@ func (c *tlsInfoCmd) Execute(ctx context.Context, f *flag.FlagSet, args ...inter
type getCertsCmd struct {
serverName string
insecureSkipVerify bool
printPEM bool
}

func (*getCertsCmd) Name() string { return "get-certs" }
Expand All @@ -79,6 +82,7 @@ func (c *getCertsCmd) Usage() string {
}
func (c *getCertsCmd) SetFlags(f *flag.FlagSet) {
f.BoolVar(&c.insecureSkipVerify, "insecure-skip-verify", false, "If true, will skip verification of server's certificate chain and host name")
f.BoolVar(&c.printPEM, "pem", false, "Print certificates in PEM format")
f.StringVar(&c.serverName, "server-name", "", "server-name is used to specify the Server Name Indication (SNI) during the TLS handshake. It allows client to indicate which hostname it's trying to connect to.")
}

Expand All @@ -103,20 +107,38 @@ func (c *getCertsCmd) Execute(ctx context.Context, f *flag.FlagSet, args ...inte

foundErr := false
for r := range respChan {
fmt.Fprintf(state.Out[r.Index], "Target %s result:\n", r.Target)
targetLogger := cliUtils.NewStyledCliLogger(state.Out[r.Index], state.Err[r.Index], &cliUtils.CliLoggerOptions{
ApplyStylingForErr: util.IsStreamToTerminal(state.Err[r.Index]),
ApplyStylingForOut: util.IsStreamToTerminal(state.Out[r.Index]),
})

if r.Error != nil {
fmt.Fprintf(state.Err[r.Index], "Get TLS certificates failure: %v\n", r.Error)
targetLogger.Errorf("Get TLS certificates failure: %v\n", r.Error)
foundErr = true
continue
}
for i, cert := range r.Resp.Certificates {
fmt.Fprintf(state.Out[r.Index], "---Server Certificate--- %d\n", i)
fmt.Fprintf(state.Out[r.Index], "Issuer: %v\n", cert.Issuer)
fmt.Fprintf(state.Out[r.Index], "Subject: %v\n", cert.Subject)
fmt.Fprintf(state.Out[r.Index], "NotBefore: %s\n", time.Unix(cert.NotBefore, 0))
fmt.Fprintf(state.Out[r.Index], "NotAfter: %s\n", time.Unix(cert.NotAfter, 0))
fmt.Fprintf(state.Out[r.Index], "DNS Names: %v\n", cert.DnsNames)
fmt.Fprintf(state.Out[r.Index], "IP Addresses: %v\n\n", cert.IpAddresses)
if c.printPEM {
if len(cert.Raw) == 0 {
targetLogger.Errorf("no raw cert available for %v, sansshell-server may be too old to return cert info\n", cert.Subject)
} else {
err := pem.Encode(state.Out[r.Index], &pem.Block{
Type: "CERTIFICATE",
Bytes: cert.Raw,
})
if err != nil {
targetLogger.Errorf("unable to encode cert as pem: %v\n", cert.Raw)
}
}
} else {
targetLogger.Infof("---Server Certificate--- %d\n", i)
targetLogger.Infof("Issuer: %v\n", cliUtils.Colorize(cliUtils.GreenText, cert.Issuer))
targetLogger.Infof("Subject: %v\n", cliUtils.Colorize(cliUtils.GreenText, cert.Subject))
targetLogger.Infof("NotBefore: %s\n", cliUtils.Colorize(cliUtils.GreenText, time.Unix(cert.NotBefore, 0)))
targetLogger.Infof("NotAfter: %s\n", cliUtils.Colorize(cliUtils.GreenText, time.Unix(cert.NotAfter, 0)))
targetLogger.Infof("DNS Names: %v\n", cliUtils.Colorize(cliUtils.GreenText, cert.DnsNames))
targetLogger.Infof("IP Addresses: %v\n\n", cliUtils.Colorize(cliUtils.GreenText, cert.IpAddresses))
}
}
}

Expand Down
1 change: 1 addition & 0 deletions services/tlsinfo/server/server.go
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,7 @@ func (s *server) GetTLSCertificate(ctx context.Context, req *pb.TLSCertificateRe
NotBefore: cert.NotBefore.Unix(),
NotAfter: cert.NotAfter.Unix(),
DnsNames: cert.DNSNames,
Raw: cert.Raw,
}
for _, ipAddr := range cert.IPAddresses {
protoCert.IpAddresses = append(protoCert.IpAddresses, ipAddr.String())
Expand Down
55 changes: 32 additions & 23 deletions services/tlsinfo/tlsinfo.pb.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions services/tlsinfo/tlsinfo.proto
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ message TLSCertificate {
int64 not_after = 4;
repeated string dns_names = 5;
repeated string ip_addresses = 6;
bytes raw = 7; // Complete ASN.1 DER content (certificate, signature algorithm and signature)
}

message TLSCertificateChain {
Expand Down
15 changes: 10 additions & 5 deletions services/tlsinfo/tlsinfo_grpc.pb.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 2 additions & 2 deletions services/util/cli/styled-cli-logger.go
Original file line number Diff line number Diff line change
Expand Up @@ -145,9 +145,9 @@ type StyledText interface {
String() string
}

func Colorize(color ColorCode, text string) StyledText {
func Colorize(color ColorCode, a any) StyledText {
return &styledText{
text: text,
text: fmt.Sprint(a),
colorCode: color,
}
}
Expand Down

0 comments on commit 87ace58

Please sign in to comment.