diff --git a/irc/client.go b/irc/client.go index e3ef9034..9c95c3bf 100644 --- a/irc/client.go +++ b/irc/client.go @@ -1354,3 +1354,18 @@ func (client *Client) CheckInvited(casefoldedChannel string) (invited bool) { delete(client.invitedTo, casefoldedChannel) return } + +// Implements auto-oper by certfp (scans for an auto-eligible operator block that matches +// the client's cert, then applies it). +func (client *Client) attemptAutoOper(session *Session) { + if client.certfp == "" || client.HasMode(modes.Operator) { + return + } + for _, oper := range client.server.Config().operators { + if oper.Auto && oper.Pass == nil && utils.CertfpsMatch(oper.Fingerprint, client.certfp) { + rb := NewResponseBuffer(session) + applyOper(client, oper, rb) + rb.Send(true) + } + } +} diff --git a/irc/commands.go b/irc/commands.go index 24f8e5b8..31aa6c18 100644 --- a/irc/commands.go +++ b/irc/commands.go @@ -206,7 +206,7 @@ func init() { }, "OPER": { handler: operHandler, - minParams: 2, + minParams: 1, }, "PART": { handler: partHandler, diff --git a/irc/config.go b/irc/config.go index 464bcb00..58fa60da 100644 --- a/irc/config.go +++ b/irc/config.go @@ -207,11 +207,13 @@ type OperClassConfig struct { // OperConfig defines a specific operator's configuration. type OperConfig struct { - Class string - Vhost string - WhoisLine string `yaml:"whois-line"` - Password string - Modes string + Class string + Vhost string + WhoisLine string `yaml:"whois-line"` + Password string + Fingerprint string + Auto bool + Modes string } // LineLenConfig controls line lengths. @@ -454,12 +456,14 @@ func (conf *Config) OperatorClasses() (map[string]*OperClass, error) { // Oper represents a single assembled operator's config. type Oper struct { - Name string - Class *OperClass - WhoisLine string - Vhost string - Pass []byte - Modes []modes.ModeChange + Name string + Class *OperClass + WhoisLine string + Vhost string + Pass []byte + Fingerprint string + Auto bool + Modes []modes.ModeChange } // Operators returns a map of operator configs from the given OperClass and config. @@ -475,9 +479,17 @@ func (conf *Config) Operators(oc map[string]*OperClass) (map[string]*Oper, error } oper.Name = name - oper.Pass, err = decodeLegacyPasswordHash(opConf.Password) - if err != nil { - return nil, err + if opConf.Password != "" { + oper.Pass, err = decodeLegacyPasswordHash(opConf.Password) + if err != nil { + return nil, fmt.Errorf("Oper %s has an invalid password hash: %s", oper.Name, err.Error()) + } + } + oper.Fingerprint = opConf.Fingerprint + oper.Auto = opConf.Auto + + if oper.Pass == nil && oper.Fingerprint == "" { + return nil, fmt.Errorf("Oper %s has neither a password nor a fingerprint", name) } oper.Vhost = opConf.Vhost diff --git a/irc/handlers.go b/irc/handlers.go index dbcc5cb7..79df2fdb 100644 --- a/irc/handlers.go +++ b/irc/handlers.go @@ -2170,28 +2170,50 @@ func npcaHandler(server *Server, client *Client, msg ircmsg.IrcMessage, rb *Resp return false } -// OPER +// OPER [password] func operHandler(server *Server, client *Client, msg ircmsg.IrcMessage, rb *ResponseBuffer) bool { if client.HasMode(modes.Operator) { rb.Add(nil, server.name, ERR_UNKNOWNERROR, client.Nick(), "OPER", client.t("You're already opered-up!")) return false } - authorized := false + // must pass at least one check, and all enabled checks + var checkPassed, checkFailed bool oper := server.GetOperator(msg.Params[0]) if oper != nil { - password := []byte(msg.Params[1]) - authorized = (bcrypt.CompareHashAndPassword(oper.Pass, password) == nil) + if oper.Fingerprint != "" { + if utils.CertfpsMatch(oper.Fingerprint, client.certfp) { + checkPassed = true + } else { + checkFailed = true + } + } + if !checkFailed && oper.Pass != nil { + if len(msg.Params) == 1 || bcrypt.CompareHashAndPassword(oper.Pass, []byte(msg.Params[1])) != nil { + checkFailed = true + } else { + checkPassed = true + } + } } - if !authorized { + + if !checkPassed || checkFailed { rb.Add(nil, server.name, ERR_PASSWDMISMATCH, client.Nick(), client.t("Password incorrect")) client.Quit(client.t("Password incorrect"), rb.session) return true } - oldNickmask := client.NickMaskString() + applyOper(client, oper, rb) + return false +} + +// applies operator status to a client, who MUST NOT already be an operator +func applyOper(client *Client, oper *Oper, rb *ResponseBuffer) { + details := client.Details() + oldNickmask := details.nickMask client.SetOper(oper) - if client.NickMaskString() != oldNickmask { + newNickmask := client.NickMaskString() + if newNickmask != oldNickmask { client.sendChghost(oldNickmask, oper.Vhost) } @@ -2204,17 +2226,14 @@ func operHandler(server *Server, client *Client, msg ircmsg.IrcMessage, rb *Resp copy(modeChanges[1:], oper.Modes) applied := ApplyUserModeChanges(client, modeChanges, true) - rb.Add(nil, server.name, RPL_YOUREOPER, client.nick, client.t("You are now an IRC operator")) - rb.Add(nil, server.name, "MODE", client.nick, applied.String()) + client.server.snomasks.Send(sno.LocalOpers, fmt.Sprintf(ircfmt.Unescape("Client opered up $c[grey][$r%s$c[grey], $r%s$c[grey]]"), client.nickMaskString, oper.Name)) - server.snomasks.Send(sno.LocalOpers, fmt.Sprintf(ircfmt.Unescape("Client opered up $c[grey][$r%s$c[grey], $r%s$c[grey]]"), client.nickMaskString, oper.Name)) - - // client may now be unthrottled by the fakelag system + rb.Broadcast(nil, client.server.name, RPL_YOUREOPER, details.nick, client.t("You are now an IRC operator")) + rb.Broadcast(nil, client.server.name, "MODE", details.nick, applied.String()) for _, session := range client.Sessions() { + // client may now be unthrottled by the fakelag system session.resetFakelag() } - - return false } // PART {,} [] @@ -2641,7 +2660,7 @@ func webircHandler(server *Server, client *Client, msg ircmsg.IrcMessage, rb *Re if 0 < len(info.Password) && bcrypt.CompareHashAndPassword(info.Password, givenPassword) != nil { continue } - if 0 < len(info.Fingerprint) && client.certfp != info.Fingerprint { + if 0 < len(info.Fingerprint) && !utils.CertfpsMatch(info.Fingerprint, client.certfp) { continue } diff --git a/irc/help.go b/irc/help.go index 91d751be..c6a96963 100644 --- a/irc/help.go +++ b/irc/help.go @@ -349,7 +349,7 @@ The NPC command is used to send an action to the target as the source. Requires the roleplay mode (+E) to be set on the target.`, }, "oper": { - text: `OPER + text: `OPER [password] If the correct details are given, gives you IRCop privs.`, }, diff --git a/irc/responsebuffer.go b/irc/responsebuffer.go index 9df090a0..7fcb1b8f 100644 --- a/irc/responsebuffer.go +++ b/irc/responsebuffer.go @@ -77,6 +77,18 @@ func (rb *ResponseBuffer) Add(tags map[string]string, prefix string, command str rb.AddMessage(ircmsg.MakeMessage(tags, prefix, command, params...)) } +// Broadcast adds a standard new message to our queue, then sends an unlabeled copy +// to all other sessions. +func (rb *ResponseBuffer) Broadcast(tags map[string]string, prefix string, command string, params ...string) { + // can't reuse the IrcMessage object because of tag pollution :-\ + rb.Add(tags, prefix, command, params...) + for _, session := range rb.session.client.Sessions() { + if session != rb.session { + session.Send(tags, prefix, command, params...) + } + } +} + // AddFromClient adds a new message from a specific client to our queue. func (rb *ResponseBuffer) AddFromClient(time time.Time, msgid string, fromNickMask string, fromAccount string, tags map[string]string, command string, params ...string) { msg := ircmsg.MakeMessage(nil, fromNickMask, command, params...) diff --git a/irc/server.go b/irc/server.go index 5c56ac4d..bb498139 100644 --- a/irc/server.go +++ b/irc/server.go @@ -440,6 +440,9 @@ func (server *Server) playRegistrationBurst(session *Session) { if modestring != "+" { session.Send(nil, d.nickMask, RPL_UMODEIS, d.nick, modestring) } + + c.attemptAutoOper(session) + if server.logger.IsLoggingRawIO() { session.Send(nil, c.server.name, "NOTICE", d.nick, c.t("This server is in debug mode and is logging all user I/O. If you do not wish for everything you send to be readable by the server owner(s), please disconnect.")) } diff --git a/irc/utils/crypto.go b/irc/utils/crypto.go index a725b547..be4247ff 100644 --- a/irc/utils/crypto.go +++ b/irc/utils/crypto.go @@ -8,6 +8,7 @@ import ( "crypto/subtle" "encoding/base32" "encoding/base64" + "strings" ) var ( @@ -68,3 +69,15 @@ func GenerateSecretKey() string { rand.Read(buf[:]) return base64.RawURLEncoding.EncodeToString(buf[:]) } + +func normalizeCertfp(certfp string) string { + return strings.ToLower(strings.Replace(certfp, ":", "", -1)) +} + +// Convenience to compare certfps as returned by different tools, e.g., openssl vs. oragono +func CertfpsMatch(storedCertfp, suppliedCertfp string) bool { + if storedCertfp == "" { + return false + } + return normalizeCertfp(storedCertfp) == normalizeCertfp(suppliedCertfp) +} diff --git a/irc/utils/crypto_test.go b/irc/utils/crypto_test.go index c00ebad3..0b0b6967 100644 --- a/irc/utils/crypto_test.go +++ b/irc/utils/crypto_test.go @@ -81,3 +81,21 @@ func BenchmarkMungeSecretToken(b *testing.B) { t = MungeSecretToken(t) } } + +func TestCertfpComparisons(t *testing.T) { + opensslFP := "3D:6B:11:BF:B4:05:C3:F8:4B:38:CD:30:38:FB:EC:01:71:D5:03:54:79:04:07:88:4C:A5:5D:23:41:85:66:C9" + oragonoFP := "3d6b11bfb405c3f84b38cd3038fbec0171d50354790407884ca55d23418566c9" + badFP := "3d6b11bfb405c3f84b38cd3038fbec0171d50354790407884ca55d23418566c8" + if !CertfpsMatch(opensslFP, oragonoFP) { + t.Error("these certs should match") + } + if !CertfpsMatch(oragonoFP, opensslFP) { + t.Error("these certs should match") + } + if CertfpsMatch("", "") { + t.Error("empty stored certfp should not match empty provided certfp") + } + if CertfpsMatch(opensslFP, badFP) { + t.Error("these certs should not match") + } +} diff --git a/oragono.yaml b/oragono.yaml index 1f2d9ef4..10640958 100644 --- a/oragono.yaml +++ b/oragono.yaml @@ -120,10 +120,11 @@ server: webirc: # one webirc block -- should correspond to one set of gateways - - # tls fingerprint the gateway must connect with to use this webirc block - fingerprint: 938dd33f4b76dcaf7ce5eb25c852369cb4b8fb47ba22fc235aa29c6623a5f182 + # SHA-256 fingerprint of the TLS certificate the gateway must use to connect + # (comment this out to use passwords only) + fingerprint: "abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789" - # password the gateway uses to connect, made with oragono genpasswd + # password the gateway uses to connect, made with oragono genpasswd password: "$2a$04$sLEFDpIOyUp55e6gTMKbOeroT6tMXTjPFvA0eGvwvImVR9pkwv7ee" # addresses/CIDRs that can use this webirc command @@ -445,10 +446,20 @@ opers: # modes are the modes to auto-set upon opering-up modes: +is acjknoqtux - # password to login with /OPER command - # generated using "oragono genpasswd" + # operators can be authenticated either by password (with the /OPER command), + # or by certificate fingerprint, or both. if a password hash is set, then a + # password is required to oper up (e.g., /OPER dan mypassword). to generate + # the hash, use `oragono genpasswd`. password: "$2a$04$LiytCxaY0lI.guDj2pBN4eLRD5cdM2OLDwqmGAgB6M2OPirbF5Jcu" + # if a SHA-256 certificate fingerprint is configured here, then it will be + # required to /OPER. if you comment out the password hash above, then you can + # /OPER without a password. + #fingerprint: "abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789" + # if 'auto' is set (and no password hash is set), operator permissions will be + # granted automatically as soon as you connect with the right fingerprint. + #auto: true + # logging, takes inspiration from Insp logging: -