diff --git a/auditlog/message/auth.go b/auditlog/message/auth.go index d7cd0a55..9b0bd731 100644 --- a/auditlog/message/auth.go +++ b/auditlog/message/auth.go @@ -36,8 +36,9 @@ func (p PayloadAuthPasswordBackendError) Equals(other Payload) bool { // PayloadAuthPubKey is a payload for a public key based authentication. type PayloadAuthPubKey struct { - Username string `json:"username" yaml:"username"` - Key string `json:"key" yaml:"key"` + Username string `json:"username" yaml:"username"` + Key string `json:"key" yaml:"key"` + CACert *CACertificate `json:"caCertificate" yaml:"caCertificate"` } // Equals compares two PayloadAuthPubKey payloads. @@ -46,6 +47,12 @@ func (p PayloadAuthPubKey) Equals(other Payload) bool { if !ok { return false } + if p.CACert == nil && p2.CACert != nil || p.CACert != nil && p.CACert == nil { + return false + } + if p.CACert != nil && !p.CACert.Equals(p2.CACert) { + return false + } return p.Username == p2.Username && p.Key == p2.Key } diff --git a/auditlog/message/cacertificate.go b/auditlog/message/cacertificate.go new file mode 100644 index 00000000..a7395531 --- /dev/null +++ b/auditlog/message/cacertificate.go @@ -0,0 +1,50 @@ +package message + +import "time" + +// CACertificate is an SSH certificate presented by a client to verify their key against a CA. +type CACertificate struct { + // PublicKey contains the public key of the CA signing the public key presented in the OpenSSH authorized key + // format. + PublicKey string `json:"key"` + // KeyID contains an identifier for the key. + KeyID string `json:"keyID"` + // ValidPrincipals contains a list of principals for which this CA certificate is valid. + ValidPrincipals []string `json:"validPrincipals"` + // ValidAfter contains the time after which this certificate is valid. This may be empty. + ValidAfter time.Time `json:"validAfter"` + // ValidBefore contains the time when this certificate expires. This may be empty. + ValidBefore time.Time `json:"validBefore"` +} + +// Equals compares the CACertificate record. +func (c *CACertificate) Equals(cert *CACertificate) bool { + if c == nil { + return cert == nil + } + if cert == nil { + return false + } + if c.PublicKey != cert.PublicKey { + return false + } + if c.KeyID != cert.KeyID { + return false + } + if len(c.ValidPrincipals) != len(cert.ValidPrincipals) { + return false + } + for _, validPrincipal := range c.ValidPrincipals { + found := false + for _, otherPrincipal := range cert.ValidPrincipals { + if otherPrincipal == validPrincipal { + found = true + break + } + } + if !found { + return false + } + } + return c.ValidAfter == cert.ValidAfter && c.ValidBefore == cert.ValidBefore +} diff --git a/auth/protocol.go b/auth/protocol.go index e92b0094..535635b9 100644 --- a/auth/protocol.go +++ b/auth/protocol.go @@ -1,5 +1,7 @@ package auth +import "time" + // PasswordAuthRequest is an authentication request for password authentication. // // swagger:model PasswordAuthRequest @@ -58,6 +60,13 @@ type PublicKeyAuthRequest struct { // // required: true PublicKey string `json:"publicKey"` + + // CACertificate contains information about the SSH certificate presented by a connecting client. This certificate + // is not an SSL/TLS/x509 certificate and has a much simpler structure. However, this can be used to verify if the + // connecting client belongs to an organization. + // + // required: false + CACertificate *CACertificate `json:"caCertificate,omitempty"` } // ResponseBody is a response to authentication requests. @@ -85,3 +94,22 @@ type Response struct { // in: body ResponseBody } + +// CACertificate contains information about the SSH certificate presented by a connecting client. This certificate +// is not an SSL/TLS/x509 certificate and has a much simpler structure. However, this can be used to verify if the +// connecting client belongs to an organization. +// +// swagger:model CACertificate +type CACertificate struct { + // PublicKey contains the public key of the CA signing the public key presented in the OpenSSH authorized key + // format. + PublicKey string `json:"key"` + // KeyID contains an identifier for the key. + KeyID string `json:"keyID"` + // ValidPrincipals contains a list of principals for which this CA certificate is valid. + ValidPrincipals []string `json:"validPrincipals"` + // ValidAfter contains the time after which this certificate is valid. + ValidAfter time.Time `json:"validAfter"` + // ValidBefore contains the time when this certificate expires. + ValidBefore time.Time `json:"validBefore"` +} diff --git a/auth/webhook/client.go b/auth/webhook/client.go index c73371df..a2446bec 100644 --- a/auth/webhook/client.go +++ b/auth/webhook/client.go @@ -3,6 +3,7 @@ package webhook import ( "net" + protocol "github.com/containerssh/libcontainerssh/auth" "github.com/containerssh/libcontainerssh/config" "github.com/containerssh/libcontainerssh/internal/auth" "github.com/containerssh/libcontainerssh/internal/geoip/dummy" @@ -22,11 +23,22 @@ type Client interface { // PubKey authenticates with a public key from the client. It returns a bool if the authentication as successful // or not. If an error happened while contacting the authentication server it will return an error. + // + // The parameters are as follows: + // + // - username is the username provided by the connecting client. + // - pubKey is the public key offered by the connecting client. The client may offer multiple keys which will be + // presented by calling this function multiple times. + // - connectionID is an opaque random string representing this SSH connection across multiple webhooks and logs. + // - remoteAddr is the IP address of the connecting client. + // - caPubKey is the verified public key of the SSH CA certificate offered by the client. If no CA certificate + // was offered this value is nil. PubKey( username string, pubKey string, connectionID string, remoteAddr net.IP, + caPubKey *protocol.CACertificate, ) AuthenticationContext } @@ -79,6 +91,7 @@ func (a authClientWrapper) PubKey( pubKey string, connectionID string, remoteAddr net.IP, + caPubKey *protocol.CACertificate, ) AuthenticationContext { - return a.c.PubKey(username, pubKey, connectionID, remoteAddr) + return a.c.PubKey(username, pubKey, connectionID, remoteAddr, caPubKey) } diff --git a/internal/auditlog/logger.go b/internal/auditlog/logger.go index 78ea8256..2e89843e 100644 --- a/internal/auditlog/logger.go +++ b/internal/auditlog/logger.go @@ -34,7 +34,7 @@ type Connection interface { OnAuthPasswordBackendError(username string, password []byte, reason string) // OnAuthPubKey creates an audit log message for an authentication attempt with public key. - OnAuthPubKey(username string, pubKey string) + OnAuthPubKey(username string, pubKey string, caKey *message.CACertificate) // OnAuthPubKeySuccess creates an audit log message for a successful public key authentication. OnAuthPubKeySuccess(username string, pubKey string) // OnAuthPubKeyFailed creates an audit log message for a failed public key authentication. diff --git a/internal/auditlog/logger_empty.go b/internal/auditlog/logger_empty.go index 275b6eb5..f03d0346 100644 --- a/internal/auditlog/logger_empty.go +++ b/internal/auditlog/logger_empty.go @@ -73,7 +73,7 @@ func (e *empty) OnAuthPasswordFailed(_ string, _ []byte) {} func (e *empty) OnAuthPasswordBackendError(_ string, _ []byte, _ string) {} -func (e *empty) OnAuthPubKey(_ string, _ string) {} +func (e *empty) OnAuthPubKey(_ string, _ string, _ *message.CACertificate) {} func (e *empty) OnAuthPubKeySuccess(_ string, _ string) {} diff --git a/internal/auditlog/logger_impl.go b/internal/auditlog/logger_impl.go index 2b3ce7e2..a9c7c265 100644 --- a/internal/auditlog/logger_impl.go +++ b/internal/auditlog/logger_impl.go @@ -156,7 +156,7 @@ func (l *loggerConnection) OnAuthPasswordBackendError(username string, password }) } -func (l *loggerConnection) OnAuthPubKey(username string, pubKey string) { +func (l *loggerConnection) OnAuthPubKey(username string, pubKey string, caCert *message.CACertificate) { l.log(message.Message{ ConnectionID: l.connectionID, Timestamp: time.Now().UnixNano(), @@ -164,6 +164,7 @@ func (l *loggerConnection) OnAuthPubKey(username string, pubKey string) { Payload: message.PayloadAuthPubKey{ Username: username, Key: pubKey, + CACert: caCert, }, ChannelID: nil, }) diff --git a/internal/auditlog/logger_test.go b/internal/auditlog/logger_test.go index c0ceece4..a1bbd872 100644 --- a/internal/auditlog/logger_test.go +++ b/internal/auditlog/logger_test.go @@ -254,11 +254,11 @@ func TestAuth(t *testing.T) { connection.OnAuthPasswordFailed("foo", []byte("bar")) connection.OnAuthPassword("foo", []byte("baz")) connection.OnAuthPasswordSuccess("foo", []byte("baz")) - connection.OnAuthPubKey("foo", "ssh-rsa ASDF") + connection.OnAuthPubKey("foo", "ssh-rsa ASDF", "") connection.OnAuthPubKeyBackendError("foo", "ssh-rsa ASDF", "no particular reason") - connection.OnAuthPubKey("foo", "ssh-rsa ASDF") + connection.OnAuthPubKey("foo", "ssh-rsa ASDF", "") connection.OnAuthPubKeyFailed("foo", "ssh-rsa ASDF") - connection.OnAuthPubKey("foo", "ssh-rsa ABCDEF") + connection.OnAuthPubKey("foo", "ssh-rsa ABCDEF", "") connection.OnAuthPubKeySuccess("foo", "ssh-rsa ABCDEF") connection.OnHandshakeSuccessful("foo") connection.OnDisconnect() diff --git a/internal/auditlogintegration/handler_networkconnection.go b/internal/auditlogintegration/handler_networkconnection.go index f6ff7010..a4436698 100644 --- a/internal/auditlogintegration/handler_networkconnection.go +++ b/internal/auditlogintegration/handler_networkconnection.go @@ -83,17 +83,22 @@ func (n *networkConnectionHandler) OnAuthPassword( return response, metadata, reason } -func (n *networkConnectionHandler) OnAuthPubKey( - username string, - pubKey string, - clientVersion string, -) ( - response sshserver.AuthResponse, - metadata map[string]string, - reason error, -) { - n.audit.OnAuthPubKey(username, pubKey) - response, metadata, reason = n.backend.OnAuthPubKey(username, pubKey, clientVersion) +func convertCACertificate(caCert *sshserver.CACertificate) *message.CACertificate { + if caCert == nil { + return nil + } + return &message.CACertificate{ + PublicKey: caCert.PublicKey, + KeyID: caCert.KeyID, + ValidPrincipals: caCert.ValidPrincipals, + ValidAfter: caCert.ValidAfter, + ValidBefore: caCert.ValidBefore, + } +} + +func (n *networkConnectionHandler) OnAuthPubKey(username string, pubKey string, clientVersion string, caCert *sshserver.CACertificate) (response sshserver.AuthResponse, metadata map[string]string, reason error) { + n.audit.OnAuthPubKey(username, pubKey, convertCACertificate(caCert)) + response, metadata, reason = n.backend.OnAuthPubKey(username, pubKey, clientVersion, caCert) switch response { case sshserver.AuthResponseSuccess: n.audit.OnAuthPubKeySuccess(username, pubKey) diff --git a/internal/auditlogintegration/integration_test.go b/internal/auditlogintegration/integration_test.go index 0e1c80c3..9f0c42f7 100644 --- a/internal/auditlogintegration/integration_test.go +++ b/internal/auditlogintegration/integration_test.go @@ -316,11 +316,7 @@ func (b *backendHandler) OnAuthPassword(username string, _ []byte, _ string) ( return sshserver.AuthResponseFailure, nil, nil } -func (b *backendHandler) OnAuthPubKey(_ string, _ string, _ string) ( - response sshserver.AuthResponse, - metadata map[string]string, - reason error, -) { +func (b *backendHandler) OnAuthPubKey(username string, pubKey string, clientVersion string, caKey string) (response sshserver.AuthResponse, metadata map[string]string, reason error) { return sshserver.AuthResponseFailure, nil, nil } diff --git a/internal/auth/client.go b/internal/auth/client.go index 34913cdf..1e35197d 100644 --- a/internal/auth/client.go +++ b/internal/auth/client.go @@ -2,6 +2,8 @@ package auth import ( "net" + + "github.com/containerssh/libcontainerssh/auth" ) // AuthenticationContext holds the results of an authentication. @@ -38,6 +40,7 @@ type Client interface { pubKey string, connectionID string, remoteAddr net.IP, + caPubKey *auth.CACertificate, ) AuthenticationContext // KeyboardInteractive is a method to post a series of questions to the user and receive answers. @@ -69,4 +72,4 @@ type KeyboardInteractiveQuestion struct { type KeyboardInteractiveAnswers struct { // KeyboardInteractiveQuestion is the original question that was answered. Answers map[string]string -} \ No newline at end of file +} diff --git a/internal/auth/client_http.go b/internal/auth/client_http.go index a860c381..8718a4f3 100644 --- a/internal/auth/client_http.go +++ b/internal/auth/client_http.go @@ -95,12 +95,7 @@ func (client *httpAuthClient) Password( return client.processAuthWithRetry(username, method, authType, connectionID, url, authRequest, remoteAddr) } -func (client *httpAuthClient) PubKey( - username string, - pubKey string, - connectionID string, - remoteAddr net.IP, -) AuthenticationContext { +func (client *httpAuthClient) PubKey(username string, pubKey string, connectionID string, remoteAddr net.IP, caPubKey *auth.CACertificate) AuthenticationContext { if !client.enablePubKey { err := message.UserMessage( message.EAuthDisabled, @@ -117,6 +112,7 @@ func (client *httpAuthClient) PubKey( ConnectionID: connectionID, SessionID: connectionID, PublicKey: pubKey, + CACertificate: caPubKey, } method := "Public key" authType := "pubkey" diff --git a/internal/auth/client_oauth2.go b/internal/auth/client_oauth2.go index 9e4decfa..508d25df 100644 --- a/internal/auth/client_oauth2.go +++ b/internal/auth/client_oauth2.go @@ -6,6 +6,7 @@ import ( "net" "strings" + "github.com/containerssh/libcontainerssh/auth" "github.com/containerssh/libcontainerssh/log" "github.com/containerssh/libcontainerssh/message" ) @@ -48,7 +49,7 @@ func (o *oauth2Client) Password(_ string, _ []byte, _ string, _ net.IP) Authenti ), nil} } -func (o *oauth2Client) PubKey(_ string, _ string, _ string, _ net.IP) AuthenticationContext { +func (o *oauth2Client) PubKey(_ string, _ string, _ string, _ net.IP, _ *auth.CACertificate) AuthenticationContext { return &oauth2Context{false, nil, message.UserMessage( message.EAuthUnsupported, "Public key authentication is not available.", diff --git a/internal/auth/client_test.go b/internal/auth/client_test.go index 3d032fa1..c73d1349 100644 --- a/internal/auth/client_test.go +++ b/internal/auth/client_test.go @@ -88,6 +88,7 @@ func TestPubKeyDisabled(t *testing.T) { "ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABgQDP39LqSomHi4kicGADA3XVQoYxzNkvrBLOqN5AEEP01p0TZ39LXa6FdB4Pmvg8h51c+BNLoxpYrTk4UibMD87OPKYYXrNmLvq0GwjMPYpzoICevAJm+/2sDVlK9sXT93Fkin+tei+Evgf/hQK0xN+HXqP8dz8SWSXeWjBv588eHHCdrV+0FlZLXH+9D18tD4BNPHe9iJLpeeH6gsvQBvArXcIEQVvHIo1cCcsy28ymUFndG55LdOaTCA+pcfHLmRtL8HO2mI2Qc/0HBSc2d1gb3lHAnmdMT82K58OjRp9Tegc5hVuKVE+hkmNjfo3f1mVHsNu6JYLxRngnbJ20QdzuKcPb3pRMty+ggRgEQExvgl1pC3OVcgyc8YX1eXiyhYy0kXT/Jg++AcaIC1Xk/hDfB0T7WxCO0Wwd4KSjKr79tIxM/m4jP2K1Hk4yAnT7mZQ0GjdphLLuDk3yt8R809SPuzkPCXBM0sL6FrqT2GVDNihN2pBh1MyuUt7S8ZXpuW0=", "asdf", net.ParseIP("127.0.0.1"), + nil, ) if authContext.Success() { t.Fatal("Public key authentication method resulted in successful authentication.") diff --git a/internal/auth/integration_test.go b/internal/auth/integration_test.go index 7d74e409..e8503699 100644 --- a/internal/auth/integration_test.go +++ b/internal/auth/integration_test.go @@ -99,15 +99,15 @@ func TestAuth(t *testing.T) { assert.Equal(t, false, authenticationContext.Success()) assert.Equal(t, float64(1), metricsCollector.GetMetric(auth.MetricNameAuthBackendFailure)[0].Value) - authenticationContext = client.PubKey("foo", "ssh-rsa asdf", "0123456789ABCDEF", net.ParseIP("127.0.0.1")) + authenticationContext = client.PubKey("foo", "ssh-rsa asdf", "0123456789ABCDEF", net.ParseIP("127.0.0.1"), nil) assert.Equal(t, nil, authenticationContext.Error()) assert.Equal(t, true, authenticationContext.Success()) - authenticationContext = client.PubKey("foo", "ssh-rsa asdx", "0123456789ABCDEF", net.ParseIP("127.0.0.1")) + authenticationContext = client.PubKey("foo", "ssh-rsa asdx", "0123456789ABCDEF", net.ParseIP("127.0.0.1"), nil) assert.Equal(t, nil, authenticationContext.Error()) assert.Equal(t, false, authenticationContext.Success()) - authenticationContext = client.PubKey("crash", "ssh-rsa asdx", "0123456789ABCDEF", net.ParseIP("127.0.0.1")) + authenticationContext = client.PubKey("crash", "ssh-rsa asdx", "0123456789ABCDEF", net.ParseIP("127.0.0.1"), nil) assert.NotEqual(t, nil, authenticationContext.Error()) assert.Equal(t, false, authenticationContext.Success()) }) diff --git a/internal/authintegration/handler.go b/internal/authintegration/handler.go index 639ba7a0..5823de73 100644 --- a/internal/authintegration/handler.go +++ b/internal/authintegration/handler.go @@ -4,6 +4,7 @@ import ( "context" "net" + protocol "github.com/containerssh/libcontainerssh/auth" "github.com/containerssh/libcontainerssh/internal/sshserver" "github.com/containerssh/libcontainerssh/internal/auth" @@ -113,30 +114,49 @@ func (h *networkConnectionHandler) OnAuthPassword(username string, password []by } } -func (h *networkConnectionHandler) OnAuthPubKey(username string, pubKey string, clientVersion string) (response sshserver.AuthResponse, metadata map[string]string, reason error) { +func (h *networkConnectionHandler) OnAuthPubKey( + username string, + pubKey string, + clientVersion string, + caCert *sshserver.CACertificate, +) (response sshserver.AuthResponse, metadata map[string]string, reason error) { if h.authContext != nil { h.authContext.OnDisconnect() } - authContext := h.authClient.PubKey(username, pubKey, h.connectionID, h.ip) + convertedCACert := convertCACert(caCert) + authContext := h.authClient.PubKey(username, pubKey, h.connectionID, h.ip, convertedCACert) h.authContext = authContext if !authContext.Success() { if authContext.Error() != nil { if h.behavior == BehaviorPassthroughOnUnavailable { - return h.backend.OnAuthPubKey(username, pubKey, clientVersion) + return h.backend.OnAuthPubKey(username, pubKey, clientVersion, caCert) } return sshserver.AuthResponseUnavailable, authContext.Metadata(), authContext.Error() } if h.behavior == BehaviorPassthroughOnFailure { - return h.backend.OnAuthPubKey(username, pubKey, clientVersion) + return h.backend.OnAuthPubKey(username, pubKey, clientVersion, caCert) } return sshserver.AuthResponseFailure, authContext.Metadata(), authContext.Error() } if h.behavior == BehaviorPassthroughOnSuccess { - return h.backend.OnAuthPubKey(username, pubKey, clientVersion) + return h.backend.OnAuthPubKey(username, pubKey, clientVersion, caCert) } return sshserver.AuthResponseSuccess, authContext.Metadata(), authContext.Error() } +func convertCACert(cert *sshserver.CACertificate) *protocol.CACertificate { + if cert == nil { + return nil + } + return &protocol.CACertificate{ + PublicKey: cert.PublicKey, + KeyID: cert.KeyID, + ValidPrincipals: cert.ValidPrincipals, + ValidAfter: cert.ValidAfter, + ValidBefore: cert.ValidBefore, + } +} + func (h *networkConnectionHandler) OnAuthKeyboardInteractive( username string, challenge func( diff --git a/internal/backend/handler.go b/internal/backend/handler.go index b3ed36c1..e322f863 100644 --- a/internal/backend/handler.go +++ b/internal/backend/handler.go @@ -72,7 +72,7 @@ func (n *networkHandler) authResponse() (sshserver.AuthResponse, map[string]stri } } -func (n *networkHandler) OnAuthPubKey(_ string, _ string, _ string) (response sshserver.AuthResponse, metadata map[string]string, reason error) { +func (n *networkHandler) OnAuthPubKey(_ string, _ string, _ string, _ *sshserver.CACertificate) (response sshserver.AuthResponse, metadata map[string]string, reason error) { return n.authResponse() } diff --git a/internal/docker/handler_network.go b/internal/docker/handler_network.go index f68a5af1..056f5104 100644 --- a/internal/docker/handler_network.go +++ b/internal/docker/handler_network.go @@ -34,7 +34,7 @@ func (n *networkHandler) OnAuthPassword(_ string, _ []byte, _ string) (sshserver return sshserver.AuthResponseUnavailable, nil, fmt.Errorf("docker does not support authentication") } -func (n *networkHandler) OnAuthPubKey(_ string, _ string, _ string) (sshserver.AuthResponse, map[string]string, error) { +func (n *networkHandler) OnAuthPubKey(_ string, _ string, _ string, _ *sshserver.CACertificate) (sshserver.AuthResponse, map[string]string, error) { return sshserver.AuthResponseUnavailable, nil, fmt.Errorf("docker does not support authentication") } diff --git a/internal/kubernetes/networkHandler.go b/internal/kubernetes/networkHandler.go index 2b495758..ca9b56e7 100644 --- a/internal/kubernetes/networkHandler.go +++ b/internal/kubernetes/networkHandler.go @@ -33,7 +33,7 @@ func (n *networkHandler) OnAuthPassword(_ string, _ []byte, _ string) (response return sshserver.AuthResponseUnavailable, nil, fmt.Errorf("the backend handler does not support authentication") } -func (n *networkHandler) OnAuthPubKey(_ string, _ string, _ string) (response sshserver.AuthResponse, metadata map[string]string, reason error) { +func (n *networkHandler) OnAuthPubKey(username string, pubKey string, clientVersion string, caKey *sshserver.CACertificate) (response sshserver.AuthResponse, metadata map[string]string, reason error) { return sshserver.AuthResponseUnavailable, nil, fmt.Errorf("the backend handler does not support authentication") } diff --git a/internal/metricsintegration/handler.go b/internal/metricsintegration/handler.go index 4842132f..d5b5b277 100644 --- a/internal/metricsintegration/handler.go +++ b/internal/metricsintegration/handler.go @@ -64,12 +64,8 @@ func (m *metricsNetworkHandler) OnAuthPassword(username string, password []byte, return m.backend.OnAuthPassword(username, password, clientVersion) } -func (m *metricsNetworkHandler) OnAuthPubKey(username string, pubKey string, clientVersion string) ( - response sshserver.AuthResponse, - metadata map[string]string, - reason error, -) { - return m.backend.OnAuthPubKey(username, pubKey, clientVersion) +func (m *metricsNetworkHandler) OnAuthPubKey(username string, pubKey string, clientVersion string, caCert *sshserver.CACertificate) (response sshserver.AuthResponse, metadata map[string]string, reason error) { + return m.backend.OnAuthPubKey(username, pubKey, clientVersion, caCert) } func (m *metricsNetworkHandler) OnAuthKeyboardInteractive( diff --git a/internal/metricsintegration/integration_test.go b/internal/metricsintegration/integration_test.go index cef6e9df..7c6575b2 100644 --- a/internal/metricsintegration/integration_test.go +++ b/internal/metricsintegration/integration_test.go @@ -143,11 +143,7 @@ func (d *dummyBackendHandler) OnAuthPassword(_ string, _ []byte, _ string) ( return d.authResponse, nil, nil } -func (d *dummyBackendHandler) OnAuthPubKey(_ string, _ string, _ string) ( - response sshserver.AuthResponse, - metadata map[string]string, - reason error, -) { +func (d *dummyBackendHandler) OnAuthPubKey(_ string, _ string, _ string, _ *sshserver.CACertificate) (response sshserver.AuthResponse, metadata map[string]string, reason error) { return d.authResponse, nil, nil } diff --git a/internal/security/handler_network.go b/internal/security/handler_network.go index f93d8148..3ea8aa11 100644 --- a/internal/security/handler_network.go +++ b/internal/security/handler_network.go @@ -42,12 +42,8 @@ func (n *networkHandler) OnAuthPassword(username string, password []byte, client return n.backend.OnAuthPassword(username, password, clientVersion) } -func (n *networkHandler) OnAuthPubKey(username string, pubKey string, clientVersion string) ( - response sshserver.AuthResponse, - metadata map[string]string, - reason error, -) { - return n.backend.OnAuthPubKey(username, pubKey, clientVersion) +func (n *networkHandler) OnAuthPubKey(username string, pubKey string, clientVersion string, caCertificate *sshserver.CACertificate) (response sshserver.AuthResponse, metadata map[string]string, reason error) { + return n.backend.OnAuthPubKey(username, pubKey, clientVersion, caCertificate) } func (n *networkHandler) OnHandshakeFailed(reason error) { diff --git a/internal/sshproxy/networkConnectionHandler.go b/internal/sshproxy/networkConnectionHandler.go index a51b3e37..a2a770ad 100644 --- a/internal/sshproxy/networkConnectionHandler.go +++ b/internal/sshproxy/networkConnectionHandler.go @@ -41,11 +41,7 @@ func (s *networkConnectionHandler) OnAuthPassword(_ string, _ []byte, _ string) ) } -func (s *networkConnectionHandler) OnAuthPubKey(_ string, _ string, _ string) ( - sshserver.AuthResponse, - map[string]string, - error, -) { +func (s *networkConnectionHandler) OnAuthPubKey(username string, pubKey string, clientVersion string, caCert *sshserver.CACertificate) (sshserver.AuthResponse, map[string]string, error) { return sshserver.AuthResponseUnavailable, nil, fmt.Errorf( "ssh proxy does not support authentication", ) diff --git a/internal/sshserver/AbstractNetworkConnectionHandler.go b/internal/sshserver/AbstractNetworkConnectionHandler.go index c526cb35..a9e79690 100644 --- a/internal/sshserver/AbstractNetworkConnectionHandler.go +++ b/internal/sshserver/AbstractNetworkConnectionHandler.go @@ -15,10 +15,10 @@ func (a *AbstractNetworkConnectionHandler) OnAuthPassword(_ string, _ []byte, _ return AuthResponseUnavailable, nil, nil } -// OnAuthPassword is called when a user attempts a pubkey authentication. The implementation must always supply -// AuthResponse and may supply error as a reason description. The pubKey parameter is an SSH key in -// the form of "ssh-rsa KEY HERE". -func (a *AbstractNetworkConnectionHandler) OnAuthPubKey(_ string, _ string, _ string) (response AuthResponse, metadata map[string]string, reason error) { +// OnAuthPubKey is called when a user attempts a pubkey authentication. The implementation must always supply +// AuthResponse and may supply error as a reason description. The pubKey parameter is an SSH key in +// the form of "ssh-rsa KEY HERE". +func (a *AbstractNetworkConnectionHandler) OnAuthPubKey(_ string, _ string, _ string, _ *CACertificate) (response AuthResponse, metadata map[string]string, reason error) { return AuthResponseUnavailable, nil, nil } diff --git a/internal/sshserver/Server_test.go b/internal/sshserver/Server_test.go index dcda2c62..6322fbf7 100644 --- a/internal/sshserver/Server_test.go +++ b/internal/sshserver/Server_test.go @@ -521,7 +521,7 @@ func (f *fullNetworkConnectionHandler) OnAuthPassword( return sshserver.AuthResponseFailure, nil, fmt.Errorf("authentication failed") } -func (f *fullNetworkConnectionHandler) OnAuthPubKey(username string, pubKey string, _ string) (response sshserver.AuthResponse, metadata map[string]string, reason error) { +func (f *fullNetworkConnectionHandler) OnAuthPubKey(username string, pubKey string, _ string, _ *sshserver.CACertificate) (response sshserver.AuthResponse, metadata map[string]string, reason error) { if storedPubKey, ok := f.handler.pubKeys[username]; ok && storedPubKey == pubKey { return sshserver.AuthResponseSuccess, nil, nil } diff --git a/internal/sshserver/handler.go b/internal/sshserver/handler.go index 615c1e2e..e3c6d522 100644 --- a/internal/sshserver/handler.go +++ b/internal/sshserver/handler.go @@ -5,6 +5,7 @@ import ( "fmt" "io" "net" + "time" message2 "github.com/containerssh/libcontainerssh/message" "golang.org/x/crypto/ssh" @@ -91,6 +92,21 @@ func (k *KeyboardInteractiveAnswers) GetByQuestionText(question string) (string, return "", fmt.Errorf("no answer for question") } +// CACertificate is an SSH certificate presented by a client to verify their key against a CA. +type CACertificate struct { + // PublicKey contains the public key of the CA signing the public key presented in the OpenSSH authorized key + // format. + PublicKey string `json:"key"` + // KeyID contains an identifier for the key. + KeyID string `json:"keyID"` + // ValidPrincipals contains a list of principals for which this CA certificate is valid. + ValidPrincipals []string `json:"validPrincipals"` + // ValidAfter contains the time after which this certificate is valid. This may be empty. + ValidAfter time.Time `json:"validAfter"` + // ValidBefore contains the time when this certificate expires. This may be empty. + ValidBefore time.Time `json:"validBefore"` +} + // NetworkConnectionHandler is an object that is used to represent the underlying network connection and the SSH // handshake. type NetworkConnectionHandler interface { @@ -98,10 +114,10 @@ type NetworkConnectionHandler interface { // AuthResponse and may supply error as a reason description. OnAuthPassword(username string, password []byte, clientVersion string) (response AuthResponse, metadata map[string]string, reason error) - // OnAuthPassword is called when a user attempts a pubkey authentication. The implementation must always supply - // AuthResponse and may supply error as a reason description. The pubKey parameter is an SSH key in - // the form of "ssh-rsa KEY HERE". - OnAuthPubKey(username string, pubKey string, clientVersion string) (response AuthResponse, metadata map[string]string, reason error) + // OnAuthPubKey is called when a user attempts a pubkey authentication. The implementation must always supply + // AuthResponse and may supply error as a reason description. The pubKey parameter is an SSH key in + // the form of "ssh-rsa KEY HERE". + OnAuthPubKey(username string, pubKey string, clientVersion string, caKey *CACertificate) (response AuthResponse, metadata map[string]string, reason error) // OnAuthKeyboardInteractive is a callback for interactive authentication. The implementer will be passed a callback // function that can be used to issue challenges to the user. These challenges can, but do not have to contain diff --git a/internal/sshserver/serverImpl.go b/internal/sshserver/serverImpl.go index 491e2ec1..6731fdc3 100644 --- a/internal/sshserver/serverImpl.go +++ b/internal/sshserver/serverImpl.go @@ -6,6 +6,7 @@ import ( "net" "strings" "sync" + "time" "github.com/containerssh/libcontainerssh/config" ssh2 "github.com/containerssh/libcontainerssh/internal/ssh" @@ -223,10 +224,21 @@ func (s *serverImpl) createPubKeyAuthenticator( ) func(conn ssh.ConnMetadata, pubKey ssh.PublicKey) (*ssh.Permissions, map[string]string, error) { return func(conn ssh.ConnMetadata, pubKey ssh.PublicKey) (*ssh.Permissions, map[string]string, error) { authorizedKey := strings.TrimSpace(string(ssh.MarshalAuthorizedKey(pubKey))) + var caCert *CACertificate + if crt, ok := pubKey.(*ssh.Certificate); ok { + caCert = &CACertificate{ + PublicKey: string(ssh.MarshalAuthorizedKey(crt.SignatureKey)), + KeyID: crt.KeyId, + ValidPrincipals: crt.ValidPrincipals, + ValidAfter: time.Unix(int64(crt.ValidAfter), 0), + ValidBefore: time.Unix(int64(crt.ValidBefore), 0), + } + } authResponse, metadata, err := handlerNetworkConnection.OnAuthPubKey( conn.User(), authorizedKey, string(conn.ClientVersion()), + caCert, ) //goland:noinspection GoNilness switch authResponse { diff --git a/internal/sshserver/testAuthenticationNetworkHandler.go b/internal/sshserver/testAuthenticationNetworkHandler.go index c7dc3fa8..a5314c31 100644 --- a/internal/sshserver/testAuthenticationNetworkHandler.go +++ b/internal/sshserver/testAuthenticationNetworkHandler.go @@ -73,7 +73,7 @@ func (t *testAuthenticationNetworkHandler) OnAuthPassword(username string, passw return AuthResponseFailure, nil, ErrAuthenticationFailed } -func (t *testAuthenticationNetworkHandler) OnAuthPubKey(username string, pubKey string, clientVersion string) (response AuthResponse, metadata map[string]string, reason error) { +func (t *testAuthenticationNetworkHandler) OnAuthPubKey(username string, pubKey string, _ string, _ *CACertificate) (response AuthResponse, metadata map[string]string, reason error) { for _, user := range t.rootHandler.users { if user.username == username { for _, authorizedKey := range user.authorizedKeys {