Merge pull request #47 from unifi-poller/dn2_ssl_cert_pin

Add ability to validate controller SSL cert against provided PEM.
This commit is contained in:
David Newhall 2021-03-14 18:54:41 -07:00 committed by GitHub
commit a63ee00563
2 changed files with 71 additions and 14 deletions

View File

@ -49,7 +49,7 @@ const (
// path returns the correct api path based on the new variable. // path returns the correct api path based on the new variable.
// new is based on the unifi-controller output. is it new or old output? // new is based on the unifi-controller output. is it new or old output?
func (u *Unifi) path(path string) string { func (u *Unifi) path(path string) string {
if u.New { if u.new {
if path == APILoginPath { if path == APILoginPath {
return APILoginPathNew return APILoginPathNew
} }
@ -82,16 +82,17 @@ type Devices struct {
} }
// Config is the data passed into our library. This configures things and allows // Config is the data passed into our library. This configures things and allows
// us to connect to a controller and write log messages. // us to connect to a controller and write log messages. Optional SSLCert is used
// for ssl cert pinning; provide the content of a PEM to validate the server's cert.
type Config struct { type Config struct {
User string User string
Pass string Pass string
URL string URL string
VerifySSL bool SSLCert [][]byte
New bool
ErrorLog Logger ErrorLog Logger
DebugLog Logger DebugLog Logger
Timeout time.Duration // how long to wait for replies, default: forever. Timeout time.Duration // how long to wait for replies, default: forever.
VerifySSL bool
} }
// Unifi is what you get in return for providing a password! Unifi represents // Unifi is what you get in return for providing a password! Unifi represents
@ -102,7 +103,22 @@ type Unifi struct {
*http.Client *http.Client
*Config *Config
*server *server
csrf string csrf string
fingerprints fingerprints
new bool
}
type fingerprints []string
// Contains returns true if the fingerprint is in the list.
func (f fingerprints) Contains(s string) bool {
for i := range f {
if s == f[i] {
return true
}
}
return false
} }
// server is the /status endpoint from the Unifi controller. // server is the /status endpoint from the Unifi controller.

View File

@ -8,8 +8,11 @@ package unifi
import ( import (
"bytes" "bytes"
"context" "context"
"crypto/sha256"
"crypto/tls" "crypto/tls"
"crypto/x509"
"encoding/json" "encoding/json"
"encoding/pem"
"fmt" "fmt"
"io" "io"
"io/ioutil" "io/ioutil"
@ -26,6 +29,7 @@ var (
ErrAuthenticationFailed = fmt.Errorf("authentication failed") ErrAuthenticationFailed = fmt.Errorf("authentication failed")
ErrInvalidStatusCode = fmt.Errorf("invalid status code from server") ErrInvalidStatusCode = fmt.Errorf("invalid status code from server")
ErrNoParams = fmt.Errorf("requedted PUT with no parameters") ErrNoParams = fmt.Errorf("requedted PUT with no parameters")
ErrInvalidSignature = fmt.Errorf("certificate signature does not match")
) )
// NewUnifi creates a http.Client with authenticated cookies. // NewUnifi creates a http.Client with authenticated cookies.
@ -37,6 +41,29 @@ func NewUnifi(config *Config) (*Unifi, error) {
return nil, fmt.Errorf("creating cookiejar: %w", err) return nil, fmt.Errorf("creating cookiejar: %w", err)
} }
u := newUnifi(config, jar)
for i, cert := range config.SSLCert {
p, _ := pem.Decode(cert)
u.fingerprints[i] = fmt.Sprintf("%x", sha256.Sum256(p.Bytes))
}
if err := u.checkNewStyleAPI(); err != nil {
return u, err
}
if err := u.Login(); err != nil {
return u, err
}
if err := u.GetServerData(); err != nil {
return u, fmt.Errorf("unable to get server version: %w", err)
}
return u, nil
}
func newUnifi(config *Config, jar http.CookieJar) *Unifi {
config.URL = strings.TrimRight(config.URL, "/") config.URL = strings.TrimRight(config.URL, "/")
if config.ErrorLog == nil { if config.ErrorLog == nil {
@ -53,24 +80,38 @@ func NewUnifi(config *Config) (*Unifi, error) {
Timeout: config.Timeout, Timeout: config.Timeout,
Jar: jar, Jar: jar,
Transport: &http.Transport{ Transport: &http.Transport{
TLSClientConfig: &tls.Config{InsecureSkipVerify: !config.VerifySSL}, // nolint: gosec TLSClientConfig: &tls.Config{
InsecureSkipVerify: !config.VerifySSL, // nolint: gosec
},
}, },
}, },
} }
if err := u.checkNewStyleAPI(); err != nil { if len(config.SSLCert) > 0 {
return u, err u.fingerprints = make(fingerprints, len(config.SSLCert))
u.Client.Transport = &http.Transport{
TLSClientConfig: &tls.Config{
InsecureSkipVerify: true, // nolint: gosec
VerifyPeerCertificate: u.verifyPeerCertificate,
},
}
} }
if err := u.Login(); err != nil { return u
return u, err }
func (u *Unifi) verifyPeerCertificate(certs [][]byte, chains [][]*x509.Certificate) error {
if len(u.fingerprints) == 0 {
return nil
} }
if err := u.GetServerData(); err != nil { for _, cert := range certs {
return u, fmt.Errorf("unable to get server version: %w", err) if u.fingerprints.Contains(fmt.Sprintf("%x", sha256.Sum256(cert))) {
return nil
}
} }
return u, nil return ErrInvalidSignature
} }
// Login is a helper method. It can be called to grab a new authentication cookie. // Login is a helper method. It can be called to grab a new authentication cookie.
@ -144,7 +185,7 @@ func (u *Unifi) checkNewStyleAPI() error {
if resp.StatusCode == http.StatusOK { if resp.StatusCode == http.StatusOK {
// The new version returns a "200" for a / request. // The new version returns a "200" for a / request.
u.New = true u.new = true
u.DebugLog("Using NEW UniFi controller API paths for %s", req.URL) u.DebugLog("Using NEW UniFi controller API paths for %s", req.URL)
} }