certlib: add Fetcher
Fetcher is an interface and set of functions for retrieving a certificate (or chain of certificates) from a spec. It will determine whether the certificate spec is a file or a server, and fetch accordingly.
This commit is contained in:
175
certlib/fetch.go
Normal file
175
certlib/fetch.go
Normal file
@@ -0,0 +1,175 @@
|
||||
package certlib
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/tls"
|
||||
"crypto/x509"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"net"
|
||||
"os"
|
||||
|
||||
"git.wntrmute.dev/kyle/goutils/certlib/hosts"
|
||||
"git.wntrmute.dev/kyle/goutils/fileutil"
|
||||
"git.wntrmute.dev/kyle/goutils/lib"
|
||||
)
|
||||
|
||||
// FetcherOpts are options for fetching certificates. They are only applicable to ServerFetcher.
|
||||
type FetcherOpts struct {
|
||||
SkipVerify bool
|
||||
Roots *x509.CertPool
|
||||
}
|
||||
|
||||
// Fetcher is an interface for fetching certificates from a remote source. It
|
||||
// currently supports fetching from a server or a file.
|
||||
type Fetcher interface {
|
||||
Get() (*x509.Certificate, error)
|
||||
GetChain() ([]*x509.Certificate, error)
|
||||
String() string
|
||||
}
|
||||
|
||||
type ServerFetcher struct {
|
||||
host string
|
||||
port int
|
||||
insecure bool
|
||||
roots *x509.CertPool
|
||||
}
|
||||
|
||||
// WithRoots sets the roots for the ServerFetcher.
|
||||
func WithRoots(roots *x509.CertPool) func(*ServerFetcher) {
|
||||
return func(sf *ServerFetcher) {
|
||||
sf.roots = roots
|
||||
}
|
||||
}
|
||||
|
||||
// WithSkipVerify sets the insecure flag for the ServerFetcher.
|
||||
func WithSkipVerify() func(*ServerFetcher) {
|
||||
return func(sf *ServerFetcher) {
|
||||
sf.insecure = true
|
||||
}
|
||||
}
|
||||
|
||||
// ParseServer parses a server string into a ServerFetcher. It can be a URL or a
|
||||
// a host:port pair.
|
||||
func ParseServer(host string) (*ServerFetcher, error) {
|
||||
target, err := hosts.ParseHost(host)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to parse server: %w", err)
|
||||
}
|
||||
|
||||
return &ServerFetcher{
|
||||
host: target.Host,
|
||||
port: target.Port,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (sf *ServerFetcher) String() string {
|
||||
return fmt.Sprintf("tls://%s", net.JoinHostPort(sf.host, lib.Itoa(sf.port, -1)))
|
||||
}
|
||||
|
||||
func (sf *ServerFetcher) GetChain() ([]*x509.Certificate, error) {
|
||||
config := &tls.Config{
|
||||
InsecureSkipVerify: sf.insecure, // #nosec G402 - no shit sherlock
|
||||
RootCAs: sf.roots,
|
||||
}
|
||||
|
||||
dialer := &tls.Dialer{
|
||||
Config: config,
|
||||
}
|
||||
|
||||
hostSpec := net.JoinHostPort(sf.host, lib.Itoa(sf.port, -1))
|
||||
|
||||
netConn, err := dialer.DialContext(context.Background(), "tcp", hostSpec)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("dialing server: %w", err)
|
||||
}
|
||||
|
||||
conn, ok := netConn.(*tls.Conn)
|
||||
if !ok {
|
||||
return nil, errors.New("connection is not TLS")
|
||||
}
|
||||
defer conn.Close()
|
||||
|
||||
state := conn.ConnectionState()
|
||||
return state.PeerCertificates, nil
|
||||
}
|
||||
|
||||
func (sf *ServerFetcher) Get() (*x509.Certificate, error) {
|
||||
certs, err := sf.GetChain()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return certs[0], nil
|
||||
}
|
||||
|
||||
type FileFetcher struct {
|
||||
path string
|
||||
}
|
||||
|
||||
func NewFileFetcher(path string) *FileFetcher {
|
||||
return &FileFetcher{
|
||||
path: path,
|
||||
}
|
||||
}
|
||||
|
||||
func (ff *FileFetcher) String() string {
|
||||
return ff.path
|
||||
}
|
||||
|
||||
func (ff *FileFetcher) GetChain() ([]*x509.Certificate, error) {
|
||||
if ff.path == "-" {
|
||||
certData, err := io.ReadAll(os.Stdin)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to read from stdin: %w", err)
|
||||
}
|
||||
|
||||
return ParseCertificatesPEM(certData)
|
||||
}
|
||||
|
||||
certs, err := LoadCertificates(ff.path)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to load chain: %w", err)
|
||||
}
|
||||
|
||||
return certs, nil
|
||||
}
|
||||
|
||||
func (ff *FileFetcher) Get() (*x509.Certificate, error) {
|
||||
certs, err := ff.GetChain()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return certs[0], nil
|
||||
}
|
||||
|
||||
// GetCertificateChain fetches a certificate chain from a remote source.
|
||||
func GetCertificateChain(spec string, opts *FetcherOpts) ([]*x509.Certificate, error) {
|
||||
if fileutil.FileDoesExist(spec) {
|
||||
return NewFileFetcher(spec).GetChain()
|
||||
}
|
||||
|
||||
fetcher, err := ParseServer(spec)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if opts != nil {
|
||||
fetcher.insecure = opts.SkipVerify
|
||||
fetcher.roots = opts.Roots
|
||||
}
|
||||
|
||||
return fetcher.GetChain()
|
||||
}
|
||||
|
||||
// GetCertificate fetches the first certificate from a certificate chain.
|
||||
func GetCertificate(spec string, opts *FetcherOpts) (*x509.Certificate, error) {
|
||||
certs, err := GetCertificateChain(spec, opts)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return certs[0], nil
|
||||
}
|
||||
Reference in New Issue
Block a user