diff --git a/api/api.go b/api/api.go index 0ac73317..36c835cc 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,41 @@ 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)) + 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": certificate, + "certificate-type": certificateType, + } + fingerprint, err := sshutil.FormatFingerprint(mak, sshutil.DefaultFingerprint) + if err == nil { + fpParts := strings.Split(fingerprint, " ") + if len(fpParts) > 3 { + m["public-key"] = fmt.Sprintf("%s %s", fpParts[1], fpParts[len(fpParts)-1]) + } + } + 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/api_test.go b/api/api_test.go index 24e77c75..1c90d91b 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, "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, "AAAAKGVjZHNhLXNoYTItbmlzdHAyNTYtY2VydC12MDFAb3BlbnNzaC5jb20AAAAgLnkvSk4odlo3b1R+RDw+LmorL3RkN354IilCIVFVen4AAAAIbmlzdHAyNTYAAABBBHjKHss8WM2ffMYlavisoLXR0I6UEIU+cidV1ogEH1U6+/SYaFPrlzQo0tGLM5CNkMbhInbyasQsrHzn8F1Rt7nHg5/tcSf9qwAAAAEAAAAGaGVybWFuAAAACgAAAAZoZXJtYW4AAAAAY8kvJwAAAABjyhBjAAAAAAAAAIIAAAAVcGVybWl0LVgxMS1mb3J3YXJkaW5nAAAAAAAAABdwZXJtaXQtYWdlbnQtZm9yd2FyZGluZwAAAAAAAAAWcGVybWl0LXBvcnQtZm9yd2FyZGluZwAAAAAAAAAKcGVybWl0LXB0eQAAAAAAAAAOcGVybWl0LXVzZXItcmMAAAAAAAAAAAAAAGgAAAATZWNkc2Etc2hhMi1uaXN0cDI1NgAAAAhuaXN0cDI1NgAAAEEE/ayqpPrZZF5uA1UlDt4FreTf15agztQIzpxnWq/XoxAHzagRSkFGkdgFpjgsfiRpP8URHH3BZScqc0ZDCTxhoQAAAGQAAAATZWNkc2Etc2hhMi1uaXN0cDI1NgAAAEkAAAAhAJuP1wCVwoyrKrEtHGfFXrVbRHySDjvXtS1tVTdHyqymAAAAIBa/CSSzfZb4D2NLP+eEmOOMJwSjYOiNM8fiOoAaqglI", fields["certificate"]) + sassert.Equal(t, "SHA256:RvkDPGwl/G9d7LUFm1kmWhvOD9I/moPq4yxcb0STwr0 (ECDSA-CERT)", fields["public-key"]) +} 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 4bd20495..fbaa8c5a 100644 --- a/api/ssh.go +++ b/api/ssh.go @@ -338,6 +338,7 @@ func SSHSign(w http.ResponseWriter, r *http.Request) { identityCertificate = certChainToPEM(certChain) } + LogSSHCertificate(w, cert) render.JSONStatus(w, &SSHSignResponse{ Certificate: SSHCertificate{cert}, AddUserCertificate: addUserCertificate, 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,