diff --git a/irc/accounts.go b/irc/accounts.go index 7a408658..fb35c1de 100644 --- a/irc/accounts.go +++ b/irc/accounts.go @@ -5,6 +5,7 @@ package irc import ( "bytes" + "crypto/x509" "encoding/json" "fmt" "sort" @@ -1466,7 +1467,7 @@ func (am *AccountManager) ChannelsForAccount(account string) (channels []string) return unmarshalRegisteredChannels(channelStr) } -func (am *AccountManager) AuthenticateByCertFP(client *Client, certfp, authzid string) (err error) { +func (am *AccountManager) AuthenticateByCertificate(client *Client, certfp string, peerCerts []*x509.Certificate, authzid string) (err error) { if certfp == "" { return errAccountInvalidCredentials } @@ -1495,7 +1496,7 @@ func (am *AccountManager) AuthenticateByCertFP(client *Client, certfp, authzid s if config.Accounts.AuthScript.Enabled { var output AuthScriptOutput output, err = CheckAuthScript(am.server.semaphores.AuthScript, config.Accounts.AuthScript.ScriptConfig, - AuthScriptInput{Certfp: certfp, IP: client.IP().String()}) + AuthScriptInput{Certfp: certfp, IP: client.IP().String(), peerCerts: peerCerts}) if err != nil { am.server.logger.Error("internal", "failed shell auth invocation", err.Error()) } else if output.Success && output.AccountName != "" { diff --git a/irc/authscript.go b/irc/authscript.go index 38083a6e..80e7e304 100644 --- a/irc/authscript.go +++ b/irc/authscript.go @@ -4,7 +4,9 @@ package irc import ( + "crypto/x509" "encoding/json" + "encoding/pem" "fmt" "net" @@ -13,9 +15,11 @@ import ( // JSON-serializable input and output types for the script type AuthScriptInput struct { - AccountName string `json:"accountName,omitempty"` - Passphrase string `json:"passphrase,omitempty"` - Certfp string `json:"certfp,omitempty"` + AccountName string `json:"accountName,omitempty"` + Passphrase string `json:"passphrase,omitempty"` + Certfp string `json:"certfp,omitempty"` + PeerCerts []string `json:"peerCerts,omitempty"` + peerCerts []*x509.Certificate IP string `json:"ip,omitempty"` } @@ -31,6 +35,14 @@ func CheckAuthScript(sem utils.Semaphore, config ScriptConfig, input AuthScriptI defer sem.Release() } + // PEM-encode the peer certificates before applying JSON + if len(input.peerCerts) != 0 { + input.PeerCerts = make([]string, len(input.peerCerts)) + for i, cert := range input.peerCerts { + input.PeerCerts[i] = string(pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: cert.Raw})) + } + } + inputBytes, err := json.Marshal(input) if err != nil { return diff --git a/irc/client.go b/irc/client.go index 70e6628d..da4829e5 100644 --- a/irc/client.go +++ b/irc/client.go @@ -6,6 +6,7 @@ package irc import ( + "crypto/x509" "fmt" "net" "runtime/debug" @@ -163,6 +164,7 @@ type Session struct { destroyed uint32 certfp string + peerCerts []*x509.Certificate sasl saslStatus passStatus serverPassStatus @@ -384,7 +386,7 @@ func (server *Server) RunClient(conn IRCConn) { if wConn.Config.TLSConfig != nil { // error is not useful to us here anyways so we can ignore it - session.certfp, _ = utils.GetCertFP(wConn.Conn, RegisterTimeout) + session.certfp, session.peerCerts, _ = utils.GetCertFP(wConn.Conn, RegisterTimeout) } if session.isTor { diff --git a/irc/gateways.go b/irc/gateways.go index 0acbe5e7..841adf60 100644 --- a/irc/gateways.go +++ b/irc/gateways.go @@ -99,6 +99,7 @@ func (client *Client) ApplyProxiedIP(session *Session, proxiedIP net.IP, tls boo // nickmask will be updated when the client completes registration // set tls info session.certfp = "" + session.peerCerts = nil client.SetMode(modes.TLS, tls) return nil, "" diff --git a/irc/handlers.go b/irc/handlers.go index 585c2387..bd81da88 100644 --- a/irc/handlers.go +++ b/irc/handlers.go @@ -303,7 +303,7 @@ func authExternalHandler(server *Server, client *Client, mechanism string, value rb.session.deviceID = deviceID } } - err = server.accounts.AuthenticateByCertFP(client, rb.session.certfp, authzid) + err = server.accounts.AuthenticateByCertificate(client, rb.session.certfp, rb.session.peerCerts, authzid) } if err != nil { diff --git a/irc/nickserv.go b/irc/nickserv.go index e7d49a15..6e97c702 100644 --- a/irc/nickserv.go +++ b/irc/nickserv.go @@ -721,7 +721,7 @@ func nsIdentifyHandler(server *Server, client *Client, command string, params [] // try certfp if !loginSuccessful && rb.session.certfp != "" { - err = server.accounts.AuthenticateByCertFP(client, rb.session.certfp, "") + err = server.accounts.AuthenticateByCertificate(client, rb.session.certfp, rb.session.peerCerts, "") loginSuccessful = (err == nil) } diff --git a/irc/utils/crypto.go b/irc/utils/crypto.go index 624d1488..fe4a389c 100644 --- a/irc/utils/crypto.go +++ b/irc/utils/crypto.go @@ -8,6 +8,7 @@ import ( "crypto/sha256" "crypto/subtle" "crypto/tls" + "crypto/x509" "encoding/base32" "encoding/base64" "encoding/hex" @@ -92,10 +93,10 @@ func NormalizeCertfp(certfp string) (result string, err error) { return } -func GetCertFP(conn net.Conn, handshakeTimeout time.Duration) (result string, err error) { +func GetCertFP(conn net.Conn, handshakeTimeout time.Duration) (fingerprint string, peerCerts []*x509.Certificate, err error) { tlsConn, isTLS := conn.(*tls.Conn) if !isTLS { - return "", ErrNotTLS + return "", nil, ErrNotTLS } // ensure handshake is performed @@ -104,16 +105,16 @@ func GetCertFP(conn net.Conn, handshakeTimeout time.Duration) (result string, er tlsConn.SetDeadline(time.Time{}) if err != nil { - return "", err + return "", nil, err } - peerCerts := tlsConn.ConnectionState().PeerCertificates + peerCerts = tlsConn.ConnectionState().PeerCertificates if len(peerCerts) < 1 { - return "", ErrNoPeerCerts + return "", nil, ErrNoPeerCerts } rawCert := sha256.Sum256(peerCerts[0].Raw) - fingerprint := hex.EncodeToString(rawCert[:]) + fingerprint = hex.EncodeToString(rawCert[:]) - return fingerprint, nil + return fingerprint, peerCerts, nil }