Skip to content
This repository was archived by the owner on Jan 6, 2025. It is now read-only.

Commit cf545cf

Browse files
author
Janos Bonic
committed
Fixes ContainerSSH/ContainerSSH#331: Add SSH certificate information to webhook.
1 parent c925d96 commit cf545cf

28 files changed

+209
-76
lines changed

auditlog/message/auth.go

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -36,8 +36,9 @@ func (p PayloadAuthPasswordBackendError) Equals(other Payload) bool {
3636

3737
// PayloadAuthPubKey is a payload for a public key based authentication.
3838
type PayloadAuthPubKey struct {
39-
Username string `json:"username" yaml:"username"`
40-
Key string `json:"key" yaml:"key"`
39+
Username string `json:"username" yaml:"username"`
40+
Key string `json:"key" yaml:"key"`
41+
CACert *CACertificate `json:"caCertificate" yaml:"caCertificate"`
4142
}
4243

4344
// Equals compares two PayloadAuthPubKey payloads.
@@ -46,6 +47,12 @@ func (p PayloadAuthPubKey) Equals(other Payload) bool {
4647
if !ok {
4748
return false
4849
}
50+
if p.CACert == nil && p2.CACert != nil || p.CACert != nil && p.CACert == nil {
51+
return false
52+
}
53+
if p.CACert != nil && !p.CACert.Equals(p2.CACert) {
54+
return false
55+
}
4956
return p.Username == p2.Username && p.Key == p2.Key
5057
}
5158

auditlog/message/cacertificate.go

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
package message
2+
3+
import "time"
4+
5+
// CACertificate is an SSH certificate presented by a client to verify their key against a CA.
6+
type CACertificate struct {
7+
// PublicKey contains the public key of the CA signing the public key presented in the OpenSSH authorized key
8+
// format.
9+
PublicKey string `json:"key"`
10+
// KeyID contains an identifier for the key.
11+
KeyID string `json:"keyID"`
12+
// ValidPrincipals contains a list of principals for which this CA certificate is valid.
13+
ValidPrincipals []string `json:"validPrincipals"`
14+
// ValidAfter contains the time after which this certificate is valid. This may be empty.
15+
ValidAfter time.Time `json:"validAfter"`
16+
// ValidBefore contains the time when this certificate expires. This may be empty.
17+
ValidBefore time.Time `json:"validBefore"`
18+
}
19+
20+
// Equals compares the CACertificate record.
21+
func (c *CACertificate) Equals(cert *CACertificate) bool {
22+
if c == nil {
23+
return cert == nil
24+
}
25+
if cert == nil {
26+
return false
27+
}
28+
if c.PublicKey != cert.PublicKey {
29+
return false
30+
}
31+
if c.KeyID != cert.KeyID {
32+
return false
33+
}
34+
if len(c.ValidPrincipals) != len(cert.ValidPrincipals) {
35+
return false
36+
}
37+
for _, validPrincipal := range c.ValidPrincipals {
38+
found := false
39+
for _, otherPrincipal := range cert.ValidPrincipals {
40+
if otherPrincipal == validPrincipal {
41+
found = true
42+
break
43+
}
44+
}
45+
if !found {
46+
return false
47+
}
48+
}
49+
return c.ValidAfter == cert.ValidAfter && c.ValidBefore == cert.ValidBefore
50+
}

auth/protocol.go

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
package auth
22

3+
import "time"
4+
35
// PasswordAuthRequest is an authentication request for password authentication.
46
//
57
// swagger:model PasswordAuthRequest
@@ -58,6 +60,13 @@ type PublicKeyAuthRequest struct {
5860
//
5961
// required: true
6062
PublicKey string `json:"publicKey"`
63+
64+
// CACertificate contains information about the SSH certificate presented by a connecting client. This certificate
65+
// is not an SSL/TLS/x509 certificate and has a much simpler structure. However, this can be used to verify if the
66+
// connecting client belongs to an organization.
67+
//
68+
// required: false
69+
CACertificate *CACertificate `json:"caCertificate,omitempty"`
6170
}
6271

6372
// ResponseBody is a response to authentication requests.
@@ -85,3 +94,22 @@ type Response struct {
8594
// in: body
8695
ResponseBody
8796
}
97+
98+
// CACertificate contains information about the SSH certificate presented by a connecting client. This certificate
99+
// is not an SSL/TLS/x509 certificate and has a much simpler structure. However, this can be used to verify if the
100+
// connecting client belongs to an organization.
101+
//
102+
// swagger:model CACertificate
103+
type CACertificate struct {
104+
// PublicKey contains the public key of the CA signing the public key presented in the OpenSSH authorized key
105+
// format.
106+
PublicKey string `json:"key"`
107+
// KeyID contains an identifier for the key.
108+
KeyID string `json:"keyID"`
109+
// ValidPrincipals contains a list of principals for which this CA certificate is valid.
110+
ValidPrincipals []string `json:"validPrincipals"`
111+
// ValidAfter contains the time after which this certificate is valid.
112+
ValidAfter time.Time `json:"validAfter"`
113+
// ValidBefore contains the time when this certificate expires.
114+
ValidBefore time.Time `json:"validBefore"`
115+
}

auth/webhook/client.go

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ package webhook
33
import (
44
"net"
55

6+
protocol "github.com/containerssh/libcontainerssh/auth"
67
"github.com/containerssh/libcontainerssh/config"
78
"github.com/containerssh/libcontainerssh/internal/auth"
89
"github.com/containerssh/libcontainerssh/internal/geoip/dummy"
@@ -22,11 +23,22 @@ type Client interface {
2223

2324
// PubKey authenticates with a public key from the client. It returns a bool if the authentication as successful
2425
// or not. If an error happened while contacting the authentication server it will return an error.
26+
//
27+
// The parameters are as follows:
28+
//
29+
// - username is the username provided by the connecting client.
30+
// - pubKey is the public key offered by the connecting client. The client may offer multiple keys which will be
31+
// presented by calling this function multiple times.
32+
// - connectionID is an opaque random string representing this SSH connection across multiple webhooks and logs.
33+
// - remoteAddr is the IP address of the connecting client.
34+
// - caPubKey is the verified public key of the SSH CA certificate offered by the client. If no CA certificate
35+
// was offered this value is nil.
2536
PubKey(
2637
username string,
2738
pubKey string,
2839
connectionID string,
2940
remoteAddr net.IP,
41+
caPubKey *protocol.CACertificate,
3042
) AuthenticationContext
3143
}
3244

@@ -79,6 +91,7 @@ func (a authClientWrapper) PubKey(
7991
pubKey string,
8092
connectionID string,
8193
remoteAddr net.IP,
94+
caPubKey *protocol.CACertificate,
8295
) AuthenticationContext {
83-
return a.c.PubKey(username, pubKey, connectionID, remoteAddr)
96+
return a.c.PubKey(username, pubKey, connectionID, remoteAddr, caPubKey)
8497
}

internal/auditlog/logger.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,7 @@ type Connection interface {
3434
OnAuthPasswordBackendError(username string, password []byte, reason string)
3535

3636
// OnAuthPubKey creates an audit log message for an authentication attempt with public key.
37-
OnAuthPubKey(username string, pubKey string)
37+
OnAuthPubKey(username string, pubKey string, caKey *message.CACertificate)
3838
// OnAuthPubKeySuccess creates an audit log message for a successful public key authentication.
3939
OnAuthPubKeySuccess(username string, pubKey string)
4040
// OnAuthPubKeyFailed creates an audit log message for a failed public key authentication.

internal/auditlog/logger_empty.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -73,7 +73,7 @@ func (e *empty) OnAuthPasswordFailed(_ string, _ []byte) {}
7373

7474
func (e *empty) OnAuthPasswordBackendError(_ string, _ []byte, _ string) {}
7575

76-
func (e *empty) OnAuthPubKey(_ string, _ string) {}
76+
func (e *empty) OnAuthPubKey(_ string, _ string, _ *message.CACertificate) {}
7777

7878
func (e *empty) OnAuthPubKeySuccess(_ string, _ string) {}
7979

internal/auditlog/logger_impl.go

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -156,14 +156,15 @@ func (l *loggerConnection) OnAuthPasswordBackendError(username string, password
156156
})
157157
}
158158

159-
func (l *loggerConnection) OnAuthPubKey(username string, pubKey string) {
159+
func (l *loggerConnection) OnAuthPubKey(username string, pubKey string, caCert *message.CACertificate) {
160160
l.log(message.Message{
161161
ConnectionID: l.connectionID,
162162
Timestamp: time.Now().UnixNano(),
163163
MessageType: message.TypeAuthPubKey,
164164
Payload: message.PayloadAuthPubKey{
165165
Username: username,
166166
Key: pubKey,
167+
CACert: caCert,
167168
},
168169
ChannelID: nil,
169170
})

internal/auditlog/logger_test.go

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -254,11 +254,11 @@ func TestAuth(t *testing.T) {
254254
connection.OnAuthPasswordFailed("foo", []byte("bar"))
255255
connection.OnAuthPassword("foo", []byte("baz"))
256256
connection.OnAuthPasswordSuccess("foo", []byte("baz"))
257-
connection.OnAuthPubKey("foo", "ssh-rsa ASDF")
257+
connection.OnAuthPubKey("foo", "ssh-rsa ASDF", "")
258258
connection.OnAuthPubKeyBackendError("foo", "ssh-rsa ASDF", "no particular reason")
259-
connection.OnAuthPubKey("foo", "ssh-rsa ASDF")
259+
connection.OnAuthPubKey("foo", "ssh-rsa ASDF", "")
260260
connection.OnAuthPubKeyFailed("foo", "ssh-rsa ASDF")
261-
connection.OnAuthPubKey("foo", "ssh-rsa ABCDEF")
261+
connection.OnAuthPubKey("foo", "ssh-rsa ABCDEF", "")
262262
connection.OnAuthPubKeySuccess("foo", "ssh-rsa ABCDEF")
263263
connection.OnHandshakeSuccessful("foo")
264264
connection.OnDisconnect()

internal/auditlogintegration/handler_networkconnection.go

Lines changed: 16 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -83,17 +83,22 @@ func (n *networkConnectionHandler) OnAuthPassword(
8383
return response, metadata, reason
8484
}
8585

86-
func (n *networkConnectionHandler) OnAuthPubKey(
87-
username string,
88-
pubKey string,
89-
clientVersion string,
90-
) (
91-
response sshserver.AuthResponse,
92-
metadata map[string]string,
93-
reason error,
94-
) {
95-
n.audit.OnAuthPubKey(username, pubKey)
96-
response, metadata, reason = n.backend.OnAuthPubKey(username, pubKey, clientVersion)
86+
func convertCACertificate(caCert *sshserver.CACertificate) *message.CACertificate {
87+
if caCert == nil {
88+
return nil
89+
}
90+
return &message.CACertificate{
91+
PublicKey: caCert.PublicKey,
92+
KeyID: caCert.KeyID,
93+
ValidPrincipals: caCert.ValidPrincipals,
94+
ValidAfter: caCert.ValidAfter,
95+
ValidBefore: caCert.ValidBefore,
96+
}
97+
}
98+
99+
func (n *networkConnectionHandler) OnAuthPubKey(username string, pubKey string, clientVersion string, caCert *sshserver.CACertificate) (response sshserver.AuthResponse, metadata map[string]string, reason error) {
100+
n.audit.OnAuthPubKey(username, pubKey, convertCACertificate(caCert))
101+
response, metadata, reason = n.backend.OnAuthPubKey(username, pubKey, clientVersion, caCert)
97102
switch response {
98103
case sshserver.AuthResponseSuccess:
99104
n.audit.OnAuthPubKeySuccess(username, pubKey)

internal/auditlogintegration/integration_test.go

Lines changed: 1 addition & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -316,11 +316,7 @@ func (b *backendHandler) OnAuthPassword(username string, _ []byte, _ string) (
316316
return sshserver.AuthResponseFailure, nil, nil
317317
}
318318

319-
func (b *backendHandler) OnAuthPubKey(_ string, _ string, _ string) (
320-
response sshserver.AuthResponse,
321-
metadata map[string]string,
322-
reason error,
323-
) {
319+
func (b *backendHandler) OnAuthPubKey(username string, pubKey string, clientVersion string, caKey string) (response sshserver.AuthResponse, metadata map[string]string, reason error) {
324320
return sshserver.AuthResponseFailure, nil, nil
325321
}
326322

0 commit comments

Comments
 (0)