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.
// new is based on the unifi-controller output. is it new or old output?
func (u *Unifi) path(path string) string {
if u.New {
if u.new {
if path == APILoginPath {
return APILoginPathNew
}
@ -82,16 +82,17 @@ type Devices struct {
}
// 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 {
User string
Pass string
URL string
VerifySSL bool
New bool
SSLCert [][]byte
ErrorLog Logger
DebugLog Logger
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
@ -102,7 +103,22 @@ type Unifi struct {
*http.Client
*Config
*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.

View File

@ -8,8 +8,11 @@ package unifi
import (
"bytes"
"context"
"crypto/sha256"
"crypto/tls"
"crypto/x509"
"encoding/json"
"encoding/pem"
"fmt"
"io"
"io/ioutil"
@ -26,6 +29,7 @@ var (
ErrAuthenticationFailed = fmt.Errorf("authentication failed")
ErrInvalidStatusCode = fmt.Errorf("invalid status code from server")
ErrNoParams = fmt.Errorf("requedted PUT with no parameters")
ErrInvalidSignature = fmt.Errorf("certificate signature does not match")
)
// 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)
}
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, "/")
if config.ErrorLog == nil {
@ -53,24 +80,38 @@ func NewUnifi(config *Config) (*Unifi, error) {
Timeout: config.Timeout,
Jar: jar,
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 {
return u, err
if len(config.SSLCert) > 0 {
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, err
return u
}
func (u *Unifi) verifyPeerCertificate(certs [][]byte, chains [][]*x509.Certificate) error {
if len(u.fingerprints) == 0 {
return nil
}
if err := u.GetServerData(); err != nil {
return u, fmt.Errorf("unable to get server version: %w", err)
for _, cert := range certs {
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.
@ -144,7 +185,7 @@ func (u *Unifi) checkNewStyleAPI() error {
if resp.StatusCode == http.StatusOK {
// 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)
}