diff --git a/pkg/cli/cert.go b/pkg/cli/cert.go index 233065459eef..9c0efff63558 100644 --- a/pkg/cli/cert.go +++ b/pkg/cli/cert.go @@ -19,6 +19,7 @@ package cli import ( "fmt" "os" + "strings" "time" "github.com/cockroachdb/cockroach/pkg/security" @@ -170,10 +171,10 @@ func runListCerts(cmd *cobra.Command, args []string) error { fmt.Fprintf(os.Stdout, "Certificate directory: %s\n", baseCfg.SSLCertsDir) - certTableHeaders := []string{"Usage", "Certificate File", "Key File", "Notes", "Expires", "Error"} + certTableHeaders := []string{"Usage", "Certificate File", "Key File", "Expires", "Notes", "Error"} var rows [][]string - addRow := func(ci *security.CertInfo, name string) { + addRow := func(ci *security.CertInfo, notes string) { var errString string if ci.Error != nil { errString = ci.Error.Error() @@ -182,22 +183,39 @@ func runListCerts(cmd *cobra.Command, args []string) error { ci.FileUsage.String(), ci.Filename, ci.KeyFilename, - name, ci.ExpirationTime.Format("2006/01/02"), + notes, errString, }) } - if ca := cm.CACert(); ca != nil { - addRow(ca, "") + if cert := cm.CACert(); cert != nil { + addRow(cert, "") } - if node := cm.NodeCert(); node != nil { - addRow(node, "") + if cert := cm.NodeCert(); cert != nil { + var addresses []string + if cert.Error == nil && len(cert.ParsedCertificates) > 0 { + addresses = cert.ParsedCertificates[0].DNSNames + for _, ip := range cert.ParsedCertificates[0].IPAddresses { + addresses = append(addresses, ip.String()) + } + } else { + addresses = append(addresses, "") + } + + addRow(cert, fmt.Sprintf("addresses: %s", strings.Join(addresses, ","))) } - for name, cert := range cm.ClientCerts() { - addRow(cert, fmt.Sprintf("user=%s", name)) + for _, cert := range cm.ClientCerts() { + var user string + if cert.Error == nil && len(cert.ParsedCertificates) > 0 { + user = cert.ParsedCertificates[0].Subject.CommonName + } else { + user = "" + } + + addRow(cert, fmt.Sprintf("user: %s", user)) } return printQueryOutput(os.Stdout, certTableHeaders, newRowSliceIter(rows), "", cliCtx.tableDisplayFormat) diff --git a/pkg/security/certs.go b/pkg/security/certs.go index 4d78ea466f21..19fd6e573cd9 100644 --- a/pkg/security/certs.go +++ b/pkg/security/certs.go @@ -288,3 +288,24 @@ func CreateClientPair( return nil } + +// PEMContentsToX509 takes raw pem-encoded contents and attempts to parse into +// x509.Certificate objects. +func PEMContentsToX509(contents []byte) ([]*x509.Certificate, error) { + derCerts, err := PEMToCertificates(contents) + if err != nil { + return nil, err + } + + certs := make([]*x509.Certificate, len(derCerts)) + for i, c := range derCerts { + x509Cert, err := x509.ParseCertificate(c.Bytes) + if err != nil { + return nil, err + } + + certs[i] = x509Cert + } + + return certs, nil +} diff --git a/pkg/security/utils.go b/pkg/security/utils.go new file mode 100644 index 000000000000..4ae42774545c --- /dev/null +++ b/pkg/security/utils.go @@ -0,0 +1,88 @@ +// Copyright 2017 The Cockroach Authors. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or +// implied. See the License for the specific language governing +// permissions and limitations under the License. +// +// Author: Marc Berhault (marc@cockroachlabs.com) + +package security + +import "crypto/x509" + +// KeyUsageToString returns the list of key usages described by the bitmask. +// This list may not up-to-date with https://golang.org/pkg/crypto/x509/#KeyUsage +func KeyUsageToString(ku x509.KeyUsage) []string { + ret := make([]string, 0) + if ku&x509.KeyUsageDigitalSignature != 0 { + ret = append(ret, "DigitalSignature") + } + if ku&x509.KeyUsageContentCommitment != 0 { + ret = append(ret, "ContentCommitment") + } + if ku&x509.KeyUsageKeyEncipherment != 0 { + ret = append(ret, "KeyEncipherment") + } + if ku&x509.KeyUsageDataEncipherment != 0 { + ret = append(ret, "DataEncirpherment") + } + if ku&x509.KeyUsageKeyAgreement != 0 { + ret = append(ret, "KeyAgreement") + } + if ku&x509.KeyUsageCertSign != 0 { + ret = append(ret, "CertSign") + } + if ku&x509.KeyUsageCRLSign != 0 { + ret = append(ret, "CRLSign") + } + if ku&x509.KeyUsageEncipherOnly != 0 { + ret = append(ret, "EncipherOnly") + } + if ku&x509.KeyUsageDecipherOnly != 0 { + ret = append(ret, "DecipherOnly") + } + + return ret +} + +// ExtKeyUsageToString converts a x509.ExtKeyUsage to a string, returning "unknown" if +// the list is not up-to-date. +func ExtKeyUsageToString(eku x509.ExtKeyUsage) string { + switch eku { + + case x509.ExtKeyUsageAny: + return "Any" + case x509.ExtKeyUsageServerAuth: + return "ServerAuth" + case x509.ExtKeyUsageClientAuth: + return "ClientAuth" + case x509.ExtKeyUsageCodeSigning: + return "CodeSigning" + case x509.ExtKeyUsageEmailProtection: + return "EmailProtection" + case x509.ExtKeyUsageIPSECEndSystem: + return "IPSECEndSystem" + case x509.ExtKeyUsageIPSECTunnel: + return "IPSECTunnel" + case x509.ExtKeyUsageIPSECUser: + return "IPSECUser" + case x509.ExtKeyUsageTimeStamping: + return "TimeStamping" + case x509.ExtKeyUsageOCSPSigning: + return "OCSPSigning" + case x509.ExtKeyUsageMicrosoftServerGatedCrypto: + return "MicrosoftServerGatedCrypto" + case x509.ExtKeyUsageNetscapeServerGatedCrypto: + return "NetscapeServerGatedCrypto" + default: + return "unknown" + } +} diff --git a/pkg/server/debug.go b/pkg/server/debug.go index 2aebdf66a337..d87409555d9d 100644 --- a/pkg/server/debug.go +++ b/pkg/server/debug.go @@ -155,6 +155,10 @@ func init() { raft raft + + security + certificates + pprof diff --git a/pkg/server/debug_certificates.go b/pkg/server/debug_certificates.go new file mode 100644 index 000000000000..4bbe4e4bdd46 --- /dev/null +++ b/pkg/server/debug_certificates.go @@ -0,0 +1,247 @@ +// Copyright 2017 The Cockroach Authors. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or +// implied. See the License for the specific language governing +// permissions and limitations under the License. +// +// Author: Marc Berhault (marc@cockroachlabs.com) + +package server + +import ( + "crypto/ecdsa" + "crypto/rsa" + "crypto/x509" + "crypto/x509/pkix" + "fmt" + "html/template" + "net/http" + "strings" + + "github.com/cockroachdb/cockroach/pkg/roachpb" + "github.com/cockroachdb/cockroach/pkg/security" + "github.com/cockroachdb/cockroach/pkg/server/serverpb" +) + +// Returns an HTML page displaying information about the certificates currently +// loaded on the requested node. +func (s *statusServer) handleDebugCertificates(w http.ResponseWriter, r *http.Request) { + ctx := s.AnnotateCtx(r.Context()) + w.Header().Add("Content-type", "text/html") + nodeIDString := r.URL.Query().Get("node_id") + + nodeID, _, err := s.parseNodeID(nodeIDString) + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + + req := &serverpb.CertificatesRequest{NodeId: nodeIDString} + certificatesResponse, err := s.Certificates(ctx, req) + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + + webData := prepareCertificateWebData(nodeID, certificatesResponse) + + t, err := template.New("webpage").Parse(debugCertificatesTemplate) + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + if err := t.Execute(w, webData); err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + } +} + +type certificatesWebData struct { + NodeID roachpb.NodeID + Certificates []perCertificateWebData +} + +type perCertificateWebData struct { + CertificateType string + certs []*x509.Certificate + CertFields [][]stringPair + ErrorMessage string +} + +type stringPair struct { + Key string + Value string +} + +func prepareCertificateWebData( + nodeID roachpb.NodeID, response *serverpb.CertificatesResponse, +) *certificatesWebData { + ret := &certificatesWebData{ + NodeID: nodeID, + Certificates: make([]perCertificateWebData, len(response.Certificates)), + } + + for i, c := range response.Certificates { + d := perCertificateWebData{} + + switch c.Type { + case serverpb.CertificateDetails_CA: + d.CertificateType = "Certificate Authority" + case serverpb.CertificateDetails_NODE: + d.CertificateType = "Node" + default: + d.CertificateType = fmt.Sprintf("unknown type: %s", c.Type.String()) + } + + d.ErrorMessage = c.ErrorMessage + if c.ErrorMessage == "" { + certs, err := security.PEMContentsToX509(c.Data) + if err != nil { + d.ErrorMessage = err.Error() + } else { + d.certs = certs + extractUsefulCertificateFields(&d) + } + } + ret.Certificates[i] = d + } + + return ret +} + +func extractUsefulCertificateFields(data *perCertificateWebData) { + data.CertFields = make([][]stringPair, len(data.certs)) + for i, c := range data.certs { + addresses := c.DNSNames + for _, ip := range c.IPAddresses { + addresses = append(addresses, ip.String()) + } + + formatNames := func(p pkix.Name) string { + return fmt.Sprintf("CommonName=%s, Organization=%s", p.CommonName, strings.Join(p.Organization, ",")) + } + + extKeyUsage := make([]string, len(c.ExtKeyUsage)) + for i, eku := range c.ExtKeyUsage { + extKeyUsage[i] = security.ExtKeyUsageToString(eku) + } + + var pubKeyInfo string + if rsaPub, ok := c.PublicKey.(*rsa.PublicKey); ok { + pubKeyInfo = fmt.Sprintf("%d bit RSA", rsaPub.N.BitLen()) + } else if ecdsaPub, ok := c.PublicKey.(*ecdsa.PublicKey); ok { + pubKeyInfo = fmt.Sprintf("%d bit ECDSA", ecdsaPub.Params().BitSize) + } else { + // go's x509 library does not support other types (so far). + pubKeyInfo = fmt.Sprintf("unknown key type %T", c.PublicKey) + } + + data.CertFields[i] = []stringPair{ + {"Issuer", formatNames(c.Issuer)}, + {"Subject", formatNames(c.Subject)}, + {"Valid From", c.NotBefore.Format("2006-01-02 15:04:05 MST")}, + {"Valid Until", c.NotAfter.Format("2006-01-02 15:04:05 MST")}, + {"Addresses", strings.Join(addresses, ", ")}, + {"Signature Algorithm", c.SignatureAlgorithm.String()}, + {"Public Key", pubKeyInfo}, + {"Key Usage", strings.Join(security.KeyUsageToString(c.KeyUsage), ", ")}, + {"Extended Key Usage", strings.Join(extKeyUsage, ", ")}, + } + } +} + +const debugCertificatesTemplate = ` + + + + + Node {{.NodeID}} certificates + + + +
+

Node {{.NodeID}} certificates

+ {{- range .Certificates}} +
+
+
Type
+
{{.CertificateType}}
+
+ {{- if not (eq .ErrorMessage "")}} +
+
Error
+
{{.ErrorMessage}}
+
+ {{- else}} + {{- range $i, $cert := .CertFields}} +
+
Cert ID
+
{{$i}}
+
+ {{- range .}} +
+
{{.Key}}
+
{{.Value}}
+
+ {{- end}} + {{- end}} + {{- end}} +
+ {{- end}} +
+ + +` diff --git a/pkg/server/server.go b/pkg/server/server.go index bd8092acc0fe..a5ebcec4282a 100644 --- a/pkg/server/server.go +++ b/pkg/server/server.go @@ -843,6 +843,7 @@ func (s *Server) Start(ctx context.Context) error { s.mux.Handle(statusVars, http.HandlerFunc(s.status.handleVars)) s.mux.Handle(rangeDebugEndpoint, http.HandlerFunc(s.status.handleDebugRange)) s.mux.Handle(problemRangesDebugEndpoint, http.HandlerFunc(s.status.handleProblemRanges)) + s.mux.Handle(certificatesDebugEndpoint, http.HandlerFunc(s.status.handleDebugCertificates)) log.Event(ctx, "added http endpoints") // Before serving SQL requests, we have to make sure the database is diff --git a/pkg/server/status.go b/pkg/server/status.go index 2ce7ab754aa9..166f89056192 100644 --- a/pkg/server/status.go +++ b/pkg/server/status.go @@ -72,6 +72,9 @@ const ( // that are experiencing problems. problemRangesDebugEndpoint = "/debug/problemranges" + // certificatesDebugEndpoint lists the certificates on a node. + certificatesDebugEndpoint = "/debug/certificates" + // raftStateDormant is used when there is no known raft state. raftStateDormant = "StateDormant" )