From 922f702da31bcdf7c6a0c7a5883ef00b2e0f453f Mon Sep 17 00:00:00 2001 From: Herman Slatman Date: Thu, 4 May 2023 15:33:06 +0200 Subject: [PATCH 1/5] Add logging for SSH certificate issuance --- api/api.go | 29 ++++++++++++++++++++++++++++- api/ssh.go | 2 +- 2 files changed, 29 insertions(+), 2 deletions(-) diff --git a/api/api.go b/api/api.go index 0ac73317..7a80dc44 100644 --- a/api/api.go +++ b/api/api.go @@ -1,6 +1,7 @@ package api import ( + "bytes" "context" "crypto" "crypto/dsa" //nolint:staticcheck // support legacy algorithms @@ -20,6 +21,8 @@ import ( "github.com/go-chi/chi" "github.com/pkg/errors" + "go.step.sm/crypto/sshutil" + "golang.org/x/crypto/ssh" "github.com/smallstep/certificates/api/log" "github.com/smallstep/certificates/api/render" @@ -469,7 +472,7 @@ func logOtt(w http.ResponseWriter, token string) { } } -// LogCertificate add certificate fields to the log message. +// LogCertificate adds certificate fields to the log message. func LogCertificate(w http.ResponseWriter, cert *x509.Certificate) { if rl, ok := w.(logging.ResponseLogger); ok { m := map[string]interface{}{ @@ -501,6 +504,30 @@ func LogCertificate(w http.ResponseWriter, cert *x509.Certificate) { } } +// LogSSHCertificate adds SSH certificate fields to the log message. +func LogSSHCertificate(w http.ResponseWriter, cert *ssh.Certificate) { + if rl, ok := w.(logging.ResponseLogger); ok { + mak := bytes.TrimSpace(ssh.MarshalAuthorizedKey(cert)) + certType := "user" + if cert.CertType == ssh.HostCert { + certType = "host" + } + m := map[string]interface{}{ + "serial": cert.Serial, + "principals": cert.ValidPrincipals, + "valid-from": time.Unix(int64(cert.ValidAfter), 0).Format(time.RFC3339), + "valid-to": time.Unix(int64(cert.ValidBefore), 0).Format(time.RFC3339), + "certificate": string(mak), + "certificate-type": certType, + } + fingerprint, err := sshutil.FormatFingerprint(mak, sshutil.DefaultFingerprint) + if err == nil { + m["public-key"] = fingerprint + } + rl.WithFields(m) + } +} + // ParseCursor parses the cursor and limit from the request query params. func ParseCursor(r *http.Request) (cursor string, limit int, err error) { q := r.URL.Query() diff --git a/api/ssh.go b/api/ssh.go index 4bd20495..273060d0 100644 --- a/api/ssh.go +++ b/api/ssh.go @@ -337,7 +337,7 @@ func SSHSign(w http.ResponseWriter, r *http.Request) { } identityCertificate = certChainToPEM(certChain) } - + LogSSHCertificate(w, cert) render.JSONStatus(w, &SSHSignResponse{ Certificate: SSHCertificate{cert}, AddUserCertificate: addUserCertificate, From 39e658b527771169d15dfe830a1ed2560b3ba026 Mon Sep 17 00:00:00 2001 From: Herman Slatman Date: Thu, 4 May 2023 15:52:49 +0200 Subject: [PATCH 2/5] Add test for `LogSSHCertificate` --- api/api_test.go | 35 ++++++++++++++++++++++++++++++++--- 1 file changed, 32 insertions(+), 3 deletions(-) diff --git a/api/api_test.go b/api/api_test.go index 24e77c75..4d850b54 100644 --- a/api/api_test.go +++ b/api/api_test.go @@ -29,13 +29,14 @@ import ( "github.com/go-chi/chi" "github.com/pkg/errors" sassert "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "go.step.sm/crypto/jose" + "go.step.sm/crypto/x509util" "golang.org/x/crypto/ssh" squarejose "gopkg.in/square/go-jose.v2" - "go.step.sm/crypto/jose" - "go.step.sm/crypto/x509util" - "github.com/smallstep/assert" + "github.com/smallstep/certificates/authority" "github.com/smallstep/certificates/authority/provisioner" "github.com/smallstep/certificates/errs" @@ -1657,3 +1658,31 @@ func TestProvisionersResponse_MarshalJSON(t *testing.T) { // MarshalJSON must not affect the struct properties itself sassert.Equal(t, expList, r.Provisioners) } + +const ( + fixtureECDSACertificate = `ecdsa-sha2-nistp256-cert-v01@openssh.com AAAAKGVjZHNhLXNoYTItbmlzdHAyNTYtY2VydC12MDFAb3BlbnNzaC5jb20AAAAgLnkvSk4odlo3b1R+RDw+LmorL3RkN354IilCIVFVen4AAAAIbmlzdHAyNTYAAABBBHjKHss8WM2ffMYlavisoLXR0I6UEIU+cidV1ogEH1U6+/SYaFPrlzQo0tGLM5CNkMbhInbyasQsrHzn8F1Rt7nHg5/tcSf9qwAAAAEAAAAGaGVybWFuAAAACgAAAAZoZXJtYW4AAAAAY8kvJwAAAABjyhBjAAAAAAAAAIIAAAAVcGVybWl0LVgxMS1mb3J3YXJkaW5nAAAAAAAAABdwZXJtaXQtYWdlbnQtZm9yd2FyZGluZwAAAAAAAAAWcGVybWl0LXBvcnQtZm9yd2FyZGluZwAAAAAAAAAKcGVybWl0LXB0eQAAAAAAAAAOcGVybWl0LXVzZXItcmMAAAAAAAAAAAAAAGgAAAATZWNkc2Etc2hhMi1uaXN0cDI1NgAAAAhuaXN0cDI1NgAAAEEE/ayqpPrZZF5uA1UlDt4FreTf15agztQIzpxnWq/XoxAHzagRSkFGkdgFpjgsfiRpP8URHH3BZScqc0ZDCTxhoQAAAGQAAAATZWNkc2Etc2hhMi1uaXN0cDI1NgAAAEkAAAAhAJuP1wCVwoyrKrEtHGfFXrVbRHySDjvXtS1tVTdHyqymAAAAIBa/CSSzfZb4D2NLP+eEmOOMJwSjYOiNM8fiOoAaqglI herman` +) + +func TestLogSSHCertificate(t *testing.T) { + + out, _, _, _, err := ssh.ParseAuthorizedKey([]byte(fixtureECDSACertificate)) + require.NoError(t, err) + + cert, ok := out.(*ssh.Certificate) + require.True(t, ok) + + w := httptest.NewRecorder() + rl := logging.NewResponseLogger(w) + LogSSHCertificate(rl, cert) + + sassert.Equal(t, 200, w.Result().StatusCode) + + fields := rl.Fields() + sassert.Equal(t, uint64(14376510277651266987), fields["serial"]) + sassert.Equal(t, []string{"herman"}, fields["principals"]) + sassert.Equal(t, "user", fields["certificate-type"]) + sassert.Equal(t, "2023-01-19T12:53:11+01:00", fields["valid-from"]) + sassert.Equal(t, "2023-01-20T04:54:11+01:00", fields["valid-to"]) + sassert.Equal(t, "ecdsa-sha2-nistp256-cert-v01@openssh.com AAAAKGVjZHNhLXNoYTItbmlzdHAyNTYtY2VydC12MDFAb3BlbnNzaC5jb20AAAAgLnkvSk4odlo3b1R+RDw+LmorL3RkN354IilCIVFVen4AAAAIbmlzdHAyNTYAAABBBHjKHss8WM2ffMYlavisoLXR0I6UEIU+cidV1ogEH1U6+/SYaFPrlzQo0tGLM5CNkMbhInbyasQsrHzn8F1Rt7nHg5/tcSf9qwAAAAEAAAAGaGVybWFuAAAACgAAAAZoZXJtYW4AAAAAY8kvJwAAAABjyhBjAAAAAAAAAIIAAAAVcGVybWl0LVgxMS1mb3J3YXJkaW5nAAAAAAAAABdwZXJtaXQtYWdlbnQtZm9yd2FyZGluZwAAAAAAAAAWcGVybWl0LXBvcnQtZm9yd2FyZGluZwAAAAAAAAAKcGVybWl0LXB0eQAAAAAAAAAOcGVybWl0LXVzZXItcmMAAAAAAAAAAAAAAGgAAAATZWNkc2Etc2hhMi1uaXN0cDI1NgAAAAhuaXN0cDI1NgAAAEEE/ayqpPrZZF5uA1UlDt4FreTf15agztQIzpxnWq/XoxAHzagRSkFGkdgFpjgsfiRpP8URHH3BZScqc0ZDCTxhoQAAAGQAAAATZWNkc2Etc2hhMi1uaXN0cDI1NgAAAEkAAAAhAJuP1wCVwoyrKrEtHGfFXrVbRHySDjvXtS1tVTdHyqymAAAAIBa/CSSzfZb4D2NLP+eEmOOMJwSjYOiNM8fiOoAaqglI", fields["certificate"]) + sassert.Equal(t, "256 SHA256:RvkDPGwl/G9d7LUFm1kmWhvOD9I/moPq4yxcb0STwr0 no comment (ECDSA-CERT)", fields["public-key"]) +} From 81140f859c8fe32e3718f08be99dca13dffe80fa Mon Sep 17 00:00:00 2001 From: Herman Slatman Date: Thu, 4 May 2023 16:15:03 +0200 Subject: [PATCH 3/5] Fix `valid-from` and `valid-to` times --- api/api_test.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/api/api_test.go b/api/api_test.go index 4d850b54..d1451623 100644 --- a/api/api_test.go +++ b/api/api_test.go @@ -1681,8 +1681,8 @@ func TestLogSSHCertificate(t *testing.T) { sassert.Equal(t, uint64(14376510277651266987), fields["serial"]) sassert.Equal(t, []string{"herman"}, fields["principals"]) sassert.Equal(t, "user", fields["certificate-type"]) - sassert.Equal(t, "2023-01-19T12:53:11+01:00", fields["valid-from"]) - sassert.Equal(t, "2023-01-20T04:54:11+01:00", fields["valid-to"]) + sassert.Equal(t, time.Unix(1674129191, 0).Format(time.RFC3339), fields["valid-from"]) + sassert.Equal(t, time.Unix(1674186851, 0).Format(time.RFC3339), fields["valid-to"]) sassert.Equal(t, "ecdsa-sha2-nistp256-cert-v01@openssh.com AAAAKGVjZHNhLXNoYTItbmlzdHAyNTYtY2VydC12MDFAb3BlbnNzaC5jb20AAAAgLnkvSk4odlo3b1R+RDw+LmorL3RkN354IilCIVFVen4AAAAIbmlzdHAyNTYAAABBBHjKHss8WM2ffMYlavisoLXR0I6UEIU+cidV1ogEH1U6+/SYaFPrlzQo0tGLM5CNkMbhInbyasQsrHzn8F1Rt7nHg5/tcSf9qwAAAAEAAAAGaGVybWFuAAAACgAAAAZoZXJtYW4AAAAAY8kvJwAAAABjyhBjAAAAAAAAAIIAAAAVcGVybWl0LVgxMS1mb3J3YXJkaW5nAAAAAAAAABdwZXJtaXQtYWdlbnQtZm9yd2FyZGluZwAAAAAAAAAWcGVybWl0LXBvcnQtZm9yd2FyZGluZwAAAAAAAAAKcGVybWl0LXB0eQAAAAAAAAAOcGVybWl0LXVzZXItcmMAAAAAAAAAAAAAAGgAAAATZWNkc2Etc2hhMi1uaXN0cDI1NgAAAAhuaXN0cDI1NgAAAEEE/ayqpPrZZF5uA1UlDt4FreTf15agztQIzpxnWq/XoxAHzagRSkFGkdgFpjgsfiRpP8URHH3BZScqc0ZDCTxhoQAAAGQAAAATZWNkc2Etc2hhMi1uaXN0cDI1NgAAAEkAAAAhAJuP1wCVwoyrKrEtHGfFXrVbRHySDjvXtS1tVTdHyqymAAAAIBa/CSSzfZb4D2NLP+eEmOOMJwSjYOiNM8fiOoAaqglI", fields["certificate"]) sassert.Equal(t, "256 SHA256:RvkDPGwl/G9d7LUFm1kmWhvOD9I/moPq4yxcb0STwr0 no comment (ECDSA-CERT)", fields["public-key"]) } From 4c56877d97d81ef8c70c4929e3827ccfd3e6e1e7 Mon Sep 17 00:00:00 2001 From: Herman Slatman Date: Fri, 5 May 2023 11:06:01 +0200 Subject: [PATCH 4/5] Add SSH certificate logging to renew and rekey too --- api/sign.go | 1 + api/ssh.go | 1 + api/sshRekey.go | 1 + api/sshRenew.go | 1 + 4 files changed, 4 insertions(+) diff --git a/api/sign.go b/api/sign.go index f7c3cc5a..c0c83ce2 100644 --- a/api/sign.go +++ b/api/sign.go @@ -88,6 +88,7 @@ func Sign(w http.ResponseWriter, r *http.Request) { if len(certChainPEM) > 1 { caPEM = certChainPEM[1] } + LogCertificate(w, certChain[0]) render.JSONStatus(w, &SignResponse{ ServerPEM: certChainPEM[0], diff --git a/api/ssh.go b/api/ssh.go index 273060d0..fbaa8c5a 100644 --- a/api/ssh.go +++ b/api/ssh.go @@ -337,6 +337,7 @@ func SSHSign(w http.ResponseWriter, r *http.Request) { } identityCertificate = certChainToPEM(certChain) } + LogSSHCertificate(w, cert) render.JSONStatus(w, &SSHSignResponse{ Certificate: SSHCertificate{cert}, diff --git a/api/sshRekey.go b/api/sshRekey.go index 6c0a5064..80fc6d87 100644 --- a/api/sshRekey.go +++ b/api/sshRekey.go @@ -89,6 +89,7 @@ func SSHRekey(w http.ResponseWriter, r *http.Request) { return } + LogSSHCertificate(w, newCert) render.JSONStatus(w, &SSHRekeyResponse{ Certificate: SSHCertificate{newCert}, IdentityCertificate: identity, diff --git a/api/sshRenew.go b/api/sshRenew.go index 4e4d0b04..cd6d9bde 100644 --- a/api/sshRenew.go +++ b/api/sshRenew.go @@ -81,6 +81,7 @@ func SSHRenew(w http.ResponseWriter, r *http.Request) { return } + LogSSHCertificate(w, newCert) render.JSONStatus(w, &SSHSignResponse{ Certificate: SSHCertificate{newCert}, IdentityCertificate: identity, From f17bfdf57dee05cabfdec08b0d9c6d3ce7d22a96 Mon Sep 17 00:00:00 2001 From: Herman Slatman Date: Mon, 8 May 2023 13:45:53 +0200 Subject: [PATCH 5/5] Reformat the SSH certificate logging output for read- and parsability --- api/api.go | 23 +++++++++++++++++------ api/api_test.go | 6 +++--- 2 files changed, 20 insertions(+), 9 deletions(-) diff --git a/api/api.go b/api/api.go index 7a80dc44..36c835cc 100644 --- a/api/api.go +++ b/api/api.go @@ -508,21 +508,32 @@ func LogCertificate(w http.ResponseWriter, cert *x509.Certificate) { func LogSSHCertificate(w http.ResponseWriter, cert *ssh.Certificate) { if rl, ok := w.(logging.ResponseLogger); ok { mak := bytes.TrimSpace(ssh.MarshalAuthorizedKey(cert)) - certType := "user" - if cert.CertType == ssh.HostCert { - certType = "host" + var certificate string + parts := strings.Split(string(mak), " ") + if len(parts) > 1 { + certificate = parts[1] } + var userOrHost string + if cert.CertType == ssh.HostCert { + userOrHost = "host" + } else { + userOrHost = "user" + } + certificateType := fmt.Sprintf("%s %s certificate", parts[0], userOrHost) // e.g. ecdsa-sha2-nistp256-cert-v01@openssh.com user certificate m := map[string]interface{}{ "serial": cert.Serial, "principals": cert.ValidPrincipals, "valid-from": time.Unix(int64(cert.ValidAfter), 0).Format(time.RFC3339), "valid-to": time.Unix(int64(cert.ValidBefore), 0).Format(time.RFC3339), - "certificate": string(mak), - "certificate-type": certType, + "certificate": certificate, + "certificate-type": certificateType, } fingerprint, err := sshutil.FormatFingerprint(mak, sshutil.DefaultFingerprint) if err == nil { - m["public-key"] = fingerprint + fpParts := strings.Split(fingerprint, " ") + if len(fpParts) > 3 { + m["public-key"] = fmt.Sprintf("%s %s", fpParts[1], fpParts[len(fpParts)-1]) + } } rl.WithFields(m) } diff --git a/api/api_test.go b/api/api_test.go index d1451623..1c90d91b 100644 --- a/api/api_test.go +++ b/api/api_test.go @@ -1680,9 +1680,9 @@ func TestLogSSHCertificate(t *testing.T) { fields := rl.Fields() sassert.Equal(t, uint64(14376510277651266987), fields["serial"]) sassert.Equal(t, []string{"herman"}, fields["principals"]) - sassert.Equal(t, "user", fields["certificate-type"]) + sassert.Equal(t, "ecdsa-sha2-nistp256-cert-v01@openssh.com user certificate", fields["certificate-type"]) sassert.Equal(t, time.Unix(1674129191, 0).Format(time.RFC3339), fields["valid-from"]) sassert.Equal(t, time.Unix(1674186851, 0).Format(time.RFC3339), fields["valid-to"]) - sassert.Equal(t, "ecdsa-sha2-nistp256-cert-v01@openssh.com AAAAKGVjZHNhLXNoYTItbmlzdHAyNTYtY2VydC12MDFAb3BlbnNzaC5jb20AAAAgLnkvSk4odlo3b1R+RDw+LmorL3RkN354IilCIVFVen4AAAAIbmlzdHAyNTYAAABBBHjKHss8WM2ffMYlavisoLXR0I6UEIU+cidV1ogEH1U6+/SYaFPrlzQo0tGLM5CNkMbhInbyasQsrHzn8F1Rt7nHg5/tcSf9qwAAAAEAAAAGaGVybWFuAAAACgAAAAZoZXJtYW4AAAAAY8kvJwAAAABjyhBjAAAAAAAAAIIAAAAVcGVybWl0LVgxMS1mb3J3YXJkaW5nAAAAAAAAABdwZXJtaXQtYWdlbnQtZm9yd2FyZGluZwAAAAAAAAAWcGVybWl0LXBvcnQtZm9yd2FyZGluZwAAAAAAAAAKcGVybWl0LXB0eQAAAAAAAAAOcGVybWl0LXVzZXItcmMAAAAAAAAAAAAAAGgAAAATZWNkc2Etc2hhMi1uaXN0cDI1NgAAAAhuaXN0cDI1NgAAAEEE/ayqpPrZZF5uA1UlDt4FreTf15agztQIzpxnWq/XoxAHzagRSkFGkdgFpjgsfiRpP8URHH3BZScqc0ZDCTxhoQAAAGQAAAATZWNkc2Etc2hhMi1uaXN0cDI1NgAAAEkAAAAhAJuP1wCVwoyrKrEtHGfFXrVbRHySDjvXtS1tVTdHyqymAAAAIBa/CSSzfZb4D2NLP+eEmOOMJwSjYOiNM8fiOoAaqglI", fields["certificate"]) - sassert.Equal(t, "256 SHA256:RvkDPGwl/G9d7LUFm1kmWhvOD9I/moPq4yxcb0STwr0 no comment (ECDSA-CERT)", fields["public-key"]) + sassert.Equal(t, "AAAAKGVjZHNhLXNoYTItbmlzdHAyNTYtY2VydC12MDFAb3BlbnNzaC5jb20AAAAgLnkvSk4odlo3b1R+RDw+LmorL3RkN354IilCIVFVen4AAAAIbmlzdHAyNTYAAABBBHjKHss8WM2ffMYlavisoLXR0I6UEIU+cidV1ogEH1U6+/SYaFPrlzQo0tGLM5CNkMbhInbyasQsrHzn8F1Rt7nHg5/tcSf9qwAAAAEAAAAGaGVybWFuAAAACgAAAAZoZXJtYW4AAAAAY8kvJwAAAABjyhBjAAAAAAAAAIIAAAAVcGVybWl0LVgxMS1mb3J3YXJkaW5nAAAAAAAAABdwZXJtaXQtYWdlbnQtZm9yd2FyZGluZwAAAAAAAAAWcGVybWl0LXBvcnQtZm9yd2FyZGluZwAAAAAAAAAKcGVybWl0LXB0eQAAAAAAAAAOcGVybWl0LXVzZXItcmMAAAAAAAAAAAAAAGgAAAATZWNkc2Etc2hhMi1uaXN0cDI1NgAAAAhuaXN0cDI1NgAAAEEE/ayqpPrZZF5uA1UlDt4FreTf15agztQIzpxnWq/XoxAHzagRSkFGkdgFpjgsfiRpP8URHH3BZScqc0ZDCTxhoQAAAGQAAAATZWNkc2Etc2hhMi1uaXN0cDI1NgAAAEkAAAAhAJuP1wCVwoyrKrEtHGfFXrVbRHySDjvXtS1tVTdHyqymAAAAIBa/CSSzfZb4D2NLP+eEmOOMJwSjYOiNM8fiOoAaqglI", fields["certificate"]) + sassert.Equal(t, "SHA256:RvkDPGwl/G9d7LUFm1kmWhvOD9I/moPq4yxcb0STwr0 (ECDSA-CERT)", fields["public-key"]) }