1
0
MOVED:
* This codebase was originally part of r00t2.io/sysutils but has been
  split off to its own library.
This commit is contained in:
brent saner 2024-10-17 15:45:53 -04:00
commit bf887ce948
Signed by: bts
GPG Key ID: 8C004C2F93481F6B
11 changed files with 1810 additions and 0 deletions

36
.gitignore vendored Normal file
View File

@ -0,0 +1,36 @@
*.7z
*.bak
*.deb
*.jar
*.rar
*.run
*.sig
*.tar
*.tar.bz2
*.tar.gz
*.tar.xz
*.tbz
*.tbz2
*.tgz
*.txz
*.zip
.*.swp
.editix
.idea/

# https://github.com/github/gitignore/blob/master/Go.gitignore
# Binaries for programs and plugins
*.exe
*.exe~
*.dll
*.so
*.dylib

# Test binary, built with `go test -c`
*.test

# Output of the go coverage tool, specifically when used with LiteIDE
*.out

# Dependency directories (remove the comment below to include it)
# vendor/

9
LICENSE Normal file
View File

@ -0,0 +1,9 @@
Copyright (c) 2024 Brent Saner.

Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met:

1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer.
2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution.
3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission.

THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.

3
TODO Normal file
View File

@ -0,0 +1,3 @@
- PKCS#12/PFX parsing/support

- Move to struct tags and reflection, so it can not only be easier to maintain in the future but also be implemented in custom structs downstream.

134
consts.go Normal file
View File

@ -0,0 +1,134 @@
package cryptparse

import (
`crypto/tls`

`github.com/go-playground/validator/v10`
)

var (
tlsVerNmToUint map[string]uint16
tlsCipherNmToUint map[string]uint16
tlsCurveNmToCurve map[string]tls.CurveID
)

const (
MaxTlsCipher uint16 = tls.TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305_SHA256
MaxCurveId tls.CurveID = tls.X25519 // 29
MinTlsVer uint16 = tls.VersionSSL30
MaxTlsVer uint16 = tls.VersionTLS13
DefaultNetType string = "tcp"
)

// TlsUriParam* specifiy URL query parameters to parse a tls:// URI, and are used by TlsUri methods.
const (
/*
TlsUriParamCa specifies a path to a CA certificate PEM-encded DER file.

It may be specified multiple times in a TLS URI.
*/
TlsUriParamCa string = "pki_ca"
/*
TlsUriParamCert specifies a path to a client certificate PEM-encded DER file.

It may be specified multiple times in a TLS URI.
*/
TlsUriParamCert string = "pki_cert"
/*
TlsUriParamKey specifies a path to a private key as a PEM-encded file.

It may be PKCS#1, PKCS#8, or PEM-encoded ASN.1 DER EC key.

Supported private key types are RSA, ED25519, ECDSA, and ECDH.

It may be specified multiple times in a TLS URI.
*/
TlsUriParamKey string = "pki_key"
/*
TlsUriParamNoVerify, if `1`, `yes`, `y`, or `true` indicate
that the TLS connection should not require verification of
the remote end (e.g. hostname matches, trusted chain, etc.).

Any other value for this parameter will be parsed as "False"
(meaning the remote end's certificate SHOULD be verified).

Only the first defined instance is parsed.
*/
TlsUriParamNoVerify string = "no_verify"
/*
TlsUriParamSni indicates that the TLS connection should expect this hostname
instead of the hostname specified in the URI itself.

Only the first defined instance is parsed.
*/
TlsUriParamSni string = "sni"
/*
TlsUriParamCipher specifies one (or more) cipher(s)
to specify for the TLS connection cipher negotiation.
Note that TLS 1.3 has a fixed set of ciphers, and
this list may not be respected by the remote end.

The string may either be the name (as per
https://www.iana.org/assignments/tls-parameters/tls-parameters.xml)
or an int (normal, hex, etc. string representation).

It may be specified multiple times in a TLS URI.
*/
TlsUriParamCipher string = "cipher"
/*
TlsUriParamCurve specifies one (or more) curve(s)
to specify for the TLS connection cipher negotiation.

It may be specified multiple times in a TLS URI.
*/
TlsUriParamCurve string = "curve"
/*
TlsUriParamMinTls defines the minimum version of the
TLS protocol to use.
It is recommended to use "TLS_1.3".

Supported syntax formats include:

* TLS_1.3
* 1.3
* v1.3
* TLSv1.3
* 0x0304 (legacy_version, see RFC8446 § 4.1.2)
* 774 (0x0304 in int form)
* 0o1404 (0x0304 in octal form)

All evaluate to TLS 1.3 in this example.

Only the first defined instance is parsed.
*/
TlsUriParamMinTls string = "min_tls"
/*
TlsUriParamMaxTls defines the minimum version of the
TLS protocol to use.

See TlsUriParamMinTls for syntax of the value.

Only the first defined instance is parsed.
*/
TlsUriParamMaxTls string = "max_tls"
/*
TlsUriParamNet is used by TlsUri.ToConn and TlsUri.ToTlsConn to explicitly specify a network.

The default is "tcp".

See net.Dial()'s "network" parameter for valid network types.

Only the first defined instance is parsed.
*/
TlsUriParamNet string = "net"
)

var (
paramBoolValsTrue []string = []string{
"1", "yes", "y", "true",
}
paramBoolValsFalse []string = []string{
"0", "no", "n", "false",
}
validate *validator.Validate = validator.New(validator.WithRequiredStructEnabled())
)

13
errs.go Normal file
View File

@ -0,0 +1,13 @@
package cryptparse

import (
`errors`
)

var (
ErrBadTlsCipher error = errors.New("invalid TLS cipher suite")
ErrBadTlsCurve error = errors.New("invalid TLS curve")
ErrBadTlsVer error = errors.New("invalid TLS version")
ErrUnknownCipher error = errors.New("unknown TLS cipher")
ErrUnknownKey error = errors.New("unknown key type")
)

913
funcs.go Normal file
View File

@ -0,0 +1,913 @@
package cryptparse

import (
`bytes`
`crypto`
`crypto/ecdh`
`crypto/ecdsa`
`crypto/ed25519`
`crypto/rsa`
`crypto/tls`
`crypto/x509`
`encoding/pem`
`errors`
`net/url`
`os`
`strconv`
`strings`

`r00t2.io/sysutils/paths`
)

// FromURL returns a *TlsUri from a *url.URL.
func FromURL(u *url.URL) (t *TlsUri) {

var newU *url.URL

if u == nil {
return
}

newU = new(url.URL)
*newU = *u
if u.User != nil {
newU.User = new(url.Userinfo)
*newU.User = *u.User
}

newU.Scheme = "tls"

t = &TlsUri{
URL: newU,
}

return
}

// IsMatchedPair returns true if the privateKey is paired with the cert.
func IsMatchedPair(privKey crypto.PrivateKey, cert *x509.Certificate) (isMatched bool, err error) {

var pubkey crypto.PublicKey

if cert == nil || privKey == nil {
return
}

pubkey = cert.PublicKey

switch k := privKey.(type) {
case *rsa.PrivateKey:
if p, ok := pubkey.(*rsa.PublicKey); ok {
isMatched = k.PublicKey.Equal(p)
return
}
case ed25519.PrivateKey:
if p, ok := pubkey.(ed25519.PublicKey); ok {
// Order is flipped here because unlike the other key types, an ed25519.PrivateKey is just a []byte.
isMatched = p.Equal(k.Public())
return
}
case *ecdh.PrivateKey:
if p, ok := pubkey.(*ecdh.PublicKey); ok {
isMatched = k.PublicKey().Equal(p)
return
}
case *ecdsa.PrivateKey:
if p, ok := pubkey.(*ecdsa.PublicKey); ok {
isMatched = k.PublicKey.Equal(p)
return
}
}

// If we got here, we can't determine either the private key type or the cert's public key type.
err = ErrUnknownKey

return
}

/*
ParseTlsCipher parses string s and attempts to derive a TLS cipher suite (as a uint16) from it.
Use ParseTlsCipherSuite if you wish for a tls.CipherSuite instead.

The string may either be the name (as per https://www.iana.org/assignments/tls-parameters/tls-parameters.xml)
or an int (normal, hex, etc. string representation).

If none is found, the default is MaxTlsCipher.
*/
func ParseTlsCipher(s string) (cipherSuite uint16, err error) {

var nm string
var n uint64
var i uint16
var ok bool

if n, err = strconv.ParseUint(s, 10, 16); err != nil {
if errors.Is(err, strconv.ErrSyntax) {
// It's a name; parse below.
err = nil
} else {
return
}
} else {
// It's a number.
if nm = tls.CipherSuiteName(uint16(n)); strings.HasPrefix(nm, "0x") {
// ...but invalid.
err = ErrBadTlsCipher
return
} else {
// Valid (as number). Return it.
cipherSuite = uint16(n)
return
}
}

s = strings.ToUpper(s)
s = strings.ReplaceAll(s, " ", "_")

// We build a dynamic map of cipher suite names to uint16s (if not already created).
if tlsCipherNmToUint == nil {
tlsCipherNmToUint = make(map[string]uint16)
for i = 0; i <= MaxTlsCipher; i++ {
if nm = tls.CipherSuiteName(i); !strings.HasPrefix(nm, "0x") {
tlsCipherNmToUint[nm] = i
}
}
}

cipherSuite = MaxTlsCipher
if i, ok = tlsCipherNmToUint[s]; ok {
cipherSuite = i
}

return
}

// ParseTlsCipherStrict is like ParseTlsCipher, but an ErrBadTlsCipher or ErrUnknownCipher error will be raised if no matching cipher is found.
func ParseTlsCipherStrict(s string) (cipherSuite uint16, err error) {

var nm string
var n uint64
var i uint16
var ok bool

if n, err = strconv.ParseUint(s, 10, 16); err != nil {
if errors.Is(err, strconv.ErrSyntax) {
// It's a name; parse below.
err = nil
} else {
return
}
} else {
// It's a number.
if nm = tls.CipherSuiteName(uint16(n)); strings.HasPrefix(nm, "0x") {
// ...but invalid.
err = ErrBadTlsCipher
return
} else {
// Valid (as number). Return it.
cipherSuite = uint16(n)
return
}
}

s = strings.ToUpper(s)
s = strings.ReplaceAll(s, " ", "_")

// We build a dynamic map of cipher suite names to uint16s (if not already created).
if tlsCipherNmToUint == nil {
tlsCipherNmToUint = make(map[string]uint16)
for i = 0; i <= MaxTlsCipher; i++ {
if nm = tls.CipherSuiteName(i); !strings.HasPrefix(nm, "0x") {
tlsCipherNmToUint[nm] = i
}
}
}

if i, ok = tlsCipherNmToUint[s]; ok {
cipherSuite = i
} else {
err = ErrUnknownCipher
}

return
}

/*
ParseTlsCiphers parses s as a comma-separated list of cipher suite names/integers and returns a slice of suites.

See ParseTlsCipher for details, as this is mostly just a wrapper around it.

If no cipher suites are found, cipherSuites will only contain MaxTlsCipher.
*/
func ParseTlsCiphers(s string) (cipherSuites []uint16) {

var suiteNms []string
var cipher uint16
var err error

suiteNms = strings.Split(s, ",")
cipherSuites = make([]uint16, 0, len(suiteNms))

for _, nm := range suiteNms {
if cipher, err = ParseTlsCipher(nm); err != nil {
err = nil
continue
}
cipherSuites = append(cipherSuites, cipher)
}

if len(cipherSuites) == 0 {
cipherSuites = []uint16{tls.TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305_SHA256}
}

return
}

// ParseTlsCipherSuite is like ParseTlsCipher but returns a *tls.CipherSuite instead of a uint16 TLS cipher identifier.
func ParseTlsCipherSuite(s string) (cipherSuite *tls.CipherSuite, err error) {

var cipherId uint16

if cipherId, err = ParseTlsCipher(s); err != nil {
return
}

for _, v := range tls.CipherSuites() {
if v.ID == cipherId {
cipherSuite = v
return
}
}
for _, v := range tls.InsecureCipherSuites() {
if v.ID == cipherId {
cipherSuite = v
return
}
}

return
}

// ParseTlsCipherSuiteStrict is like ParseTlsCipherSuite, but an ErrBadTlsCipher or ErrUnknownCipher error will be raised if no matching cipher is found.
func ParseTlsCipherSuiteStrict(s string) (cipherSuite *tls.CipherSuite, err error) {

var cipherId uint16

if cipherId, err = ParseTlsCipherStrict(s); err != nil {
return
}

for _, v := range tls.CipherSuites() {
if v.ID == cipherId {
cipherSuite = v
return
}
}
for _, v := range tls.InsecureCipherSuites() {
if v.ID == cipherId {
cipherSuite = v
return
}
}

return
}

// ParseTlsCipherSuites is like ParseTlsCiphers but returns a []*tls.CipherSuite instead of a []uint16 of TLS cipher identifiers.
func ParseTlsCipherSuites(s string) (cipherSuites []*tls.CipherSuite, err error) {

var found bool
var cipherIds []uint16

cipherIds = ParseTlsCiphers(s)

for _, cipherId := range cipherIds {
found = false
for _, v := range tls.CipherSuites() {
if v.ID == cipherId {
cipherSuites = append(cipherSuites, v)
found = true
break
}
}
if !found {
for _, v := range tls.InsecureCipherSuites() {
if v.ID == cipherId {
cipherSuites = append(cipherSuites, v)
break
}
}
}
}

return
}

/*
ParseTlsCurve parses string s and attempts to derive a tls.CurveID from it.

The string may either be the name (as per // https://www.iana.org/assignments/tls-parameters/tls-parameters.xml#tls-parameters-8)
or an int (normal, hex, etc. string representation).
*/
func ParseTlsCurve(s string) (curve tls.CurveID, err error) {

var i tls.CurveID
var n uint64
var ok bool

if n, err = strconv.ParseUint(s, 10, 16); err != nil {
if errors.Is(err, strconv.ErrSyntax) {
// It's a name; parse below.
err = nil
} else {
return
}
} else {
// It's a number.
if strings.HasPrefix(tls.CurveID(uint16(n)).String(), "CurveID(") {
// ...but invalid.
err = ErrBadTlsCurve
return
} else {
// Valid (as number). Return it.
curve = tls.CurveID(uint16(n))
return
}
}

// It seems to be a name. Normalize...
s = strings.ToUpper(s)

// Unfortunately there's no "tls.CurveIDName()" function.
// They do have a .String() method though.
if tlsCurveNmToCurve == nil {
tlsCurveNmToCurve = make(map[string]tls.CurveID)
for i = 0; i <= MaxCurveId; i++ {
if strings.HasPrefix(i.String(), "CurveID(") {
continue
}
tlsCurveNmToCurve[i.String()] = i
// It's normally mixed-case; we want to be able to look it up in a normalized all-caps as well.
tlsCurveNmToCurve[strings.ToUpper(i.String())] = i
// The normal name, except for X25519, has "Curve" in the front. We add it without that prefix as well.
tlsCurveNmToCurve[strings.TrimPrefix(i.String(), "Curve")] = i
}
}

curve = MaxCurveId
if _, ok = tlsCurveNmToCurve[s]; ok {
curve = tlsCurveNmToCurve[s]
}

return
}

/*
ParseTlsCurves parses s as a comma-separated list of tls.CurveID names/integers and returns a slice of tls.CurveID.

See ParseTlsCurve for details, as this is mostly just a wrapper around it.

If no curves are found, curves will only contain MaxCurveId.
*/
func ParseTlsCurves(s string) (curves []tls.CurveID) {

var curveNms []string
var curve tls.CurveID
var err error

curveNms = strings.Split(s, ",")
curves = make([]tls.CurveID, 0, len(curveNms))

for _, nm := range curveNms {
if curve, err = ParseTlsCurve(nm); err != nil {
err = nil
continue
}
curves = append(curves, curve)
}

if len(curves) == 0 {
curves = []tls.CurveID{MaxCurveId}
}

return
}

/*
ParseTlsUri parses a "TLS URI"'s query parameters. All certs and keys must be in PEM format.

You probably don't need this and should instead be using TlsUri.ToTlsConfig.
It just wraps this, but is probably more convenient.
*/
func ParseTlsUri(tlsUri *url.URL) (tlsConf *tls.Config, err error) {

var b []byte
var rootCAs *x509.CertPool
var intermediateCAs []*x509.Certificate
var privKeys []crypto.PrivateKey
var tlsCerts []tls.Certificate
var allowInvalid bool
var ciphers []uint16
var curves []tls.CurveID
var params map[string][]string
var ok bool
var val string
var minVer uint16
var maxVer uint16
var buf *bytes.Buffer = new(bytes.Buffer)
var srvNm string = tlsUri.Hostname()

params = tlsUri.Query()

if params == nil {
tlsConf = &tls.Config{
ServerName: srvNm,
}
return
}

// These are all filepath(s).
for _, k := range []string{
TlsUriParamCa,
TlsUriParamCert,
TlsUriParamKey,
} {
if _, ok = params[k]; ok {
for idx, _ := range params[k] {
if err = paths.RealPath(&params[k][idx]); err != nil {
return
}
}
}
}

// CA cert(s).
buf.Reset()
if _, ok = params[TlsUriParamCa]; ok {
rootCAs = x509.NewCertPool()
for _, c := range params[TlsUriParamCa] {
if b, err = os.ReadFile(c); err != nil {
if errors.Is(err, os.ErrNotExist) {
err = nil
continue
}
}
buf.Write(b)
}
if rootCAs, _, intermediateCAs, err = ParseCA(buf.Bytes()); err != nil {
return
}
} else {
if rootCAs, err = x509.SystemCertPool(); err != nil {
return
}
}

// Keys. These are done first so we can match to a client certificate.
buf.Reset()
if _, ok = params[TlsUriParamKey]; ok {
for _, k := range params[TlsUriParamKey] {
if b, err = os.ReadFile(k); err != nil {
if errors.Is(err, os.ErrNotExist) {
err = nil
continue
} else {
return
}
}
buf.Write(b)
}
if privKeys, err = ParsePrivateKey(buf.Bytes()); err != nil {
return
}
}

// (Client) Certificate(s).
buf.Reset()
if _, ok = params[TlsUriParamCert]; ok {
for _, c := range params[TlsUriParamCert] {
if b, err = os.ReadFile(c); err != nil {
if errors.Is(err, os.ErrNotExist) {
err = nil
continue
} else {
return
}
}
buf.Write(b)
}
if tlsCerts, err = ParseLeafCert(buf.Bytes(), privKeys, intermediateCAs...); err != nil {
return
}
}

// Hostname (Override).
if _, ok = params[TlsUriParamSni]; ok {
srvNm = params[TlsUriParamSni][0]
}

// Disable Verification.
if _, ok = params[TlsUriParamNoVerify]; ok {
val = strings.ToLower(params[TlsUriParamNoVerify][0])
for _, i := range paramBoolValsTrue {
if i == val {
allowInvalid = true
break
}
}
}

// Ciphers.
if _, ok = params[TlsUriParamCipher]; ok {
ciphers = ParseTlsCiphers(strings.Join(params[TlsUriParamCipher], ","))
}

// Minimum TLS Protocol Version.
if _, ok = params[TlsUriParamMinTls]; ok {
if minVer, err = ParseTlsVersion(params[TlsUriParamMinTls][0]); err != nil {
return
}
}

// Maximum TLS Protocol Version.
if _, ok = params[TlsUriParamMaxTls]; ok {
if maxVer, err = ParseTlsVersion(params[TlsUriParamMaxTls][0]); err != nil {
return
}
}

// Curves.
if _, ok = params[TlsUriParamCurve]; ok {
curves = ParseTlsCurves(strings.Join(params[TlsUriParamCurve], ","))
}

tlsConf = &tls.Config{
Certificates: tlsCerts,
RootCAs: rootCAs,
ServerName: srvNm,
InsecureSkipVerify: allowInvalid,
CipherSuites: ciphers,
MinVersion: minVer,
MaxVersion: maxVer,
CurvePreferences: curves,
}

return
}

// ParseTlsVersion parses string s and attempts to derive a TLS version from it. If none is found, tlsVer will be 0.
func ParseTlsVersion(s string) (tlsVer uint16, err error) {

var nm string
var n uint64
var i uint16
var ok bool

if n, err = strconv.ParseUint(s, 10, 16); err != nil {
if errors.Is(err, strconv.ErrSyntax) {
// It's a name; parse below.
err = nil
} else {
return
}
} else {
// It's a number.
if nm = tls.VersionName(uint16(n)); strings.HasPrefix(nm, "0x") {
// ...but invalid.
err = ErrBadTlsVer
return
} else {
// Valid (as number). Return it.
tlsVer = uint16(n)
return
}
}

// If we get here, it should be parsed as a version string.
s = strings.ToUpper(s)
s = strings.ReplaceAll(s, "_", " ")
s = strings.ReplaceAll(s, "V", " ")
s = strings.TrimSpace(s)
if !strings.HasPrefix(s, "SSL") && !strings.HasPrefix(s, "TLS ") {
s = "TLS " + s
}

// We build a dynamic map of version names to uint16s (if not already created).
if tlsVerNmToUint == nil {
tlsVerNmToUint = make(map[string]uint16)
for i = MinTlsVer; i <= MaxTlsVer; i++ {
if nm = tls.VersionName(i); !strings.HasPrefix(nm, "0x") {
tlsVerNmToUint[nm] = i
}
}
}

if i, ok = tlsVerNmToUint[s]; ok {
tlsVer = i
}

return
}

/*
ParseCA parses PEM bytes and returns an *x509.CertPool in caCerts.

Concatenated PEM files are supported.

Any keys found will be filtered out, as will any leaf certificates.

Any *intermediate* CAs (the certificate is a CA but it is not self-signed) will be returned separate from
certPool.

Ordering from the file is preserved in the returned slices.
*/
func ParseCA(certRaw []byte) (certPool *x509.CertPool, rootCerts []*x509.Certificate, intermediateCerts []*x509.Certificate, err error) {

var pemBlocks []*pem.Block
var cert *x509.Certificate
var certs []*x509.Certificate

if pemBlocks, err = SplitPem(certRaw); err != nil {
return
}

// Filter out keys etc. and non-CA certs.
for _, b := range pemBlocks {
if b.Type != "CERTIFICATE" {
continue
}
if cert, err = x509.ParseCertificate(b.Bytes); err != nil {
return
}
if !cert.IsCA {
continue
}
certs = append(certs, cert)
}

for _, cert = range certs {
if bytes.Equal(cert.RawIssuer, cert.RawSubject) {
// It's a root/self-signed.
rootCerts = append(rootCerts, cert)
} else {
// It's an intermediate.
intermediateCerts = append(intermediateCerts, cert)
}
}

if rootCerts != nil {
certPool = x509.NewCertPool()
for _, cert = range rootCerts {
certPool.AddCert(cert)
}
}

return
}

/*
ParseDhParams parses PEM bytes and returns parsed DH parameters.

Concatenated PEM files are supported.

TODO: Currently not fully implemented; params will always be nil.
*/
/*
func ParseDhParams(dhRaw []byte) (params []*dhparam.DH, err error) {

var pemBlocks *PemBlocks

if dhRaw == nil || len(dhRaw) == 0 {
return
}
if pemBlocks, err = SplitPemBlocks(dhRaw); err != nil {
return
}
if pemBlocks == nil || len(*pemBlocks) == 0 {
return
}

// TODO

return
}
*/

/*
ParseLeafCert parses PEM bytes from a (client) certificate file, iterates over a slice of
crypto.PrivateKey (finding one that matches), and returns one (or more) tls.Certificate.

The key may also be combined with the certificate in the same file.

If no private key matches or no client cert is found in the file, tlsCerts will be nil/missing
that certificate but no error will be returned.
This behavior can be avoided by passing a nil slice to keys.

Any leaf certificates ("server" certificate, as opposed to a signer/issuer) found in the file
will be assumed to be the desired one(s).

Any additional/supplementary intermediates may be provided. Any present in the PEM bytes (certRaw) will be included.

Any *root* CAs found will be discarded. They should/can be extracted seperately via ParseCA.

The parsed and paired certificates and keys can be found in each respective tls.Certificate.Leaf and tls.Certificate.PrivateKey.
Any certs without a corresponding key will be discarded.
*/
func ParseLeafCert(certRaw []byte, keys []crypto.PrivateKey, intermediates ...*x509.Certificate) (tlsCerts []tls.Certificate, err error) {

var pemBlocks []*pem.Block
var cert *x509.Certificate
var certs []*x509.Certificate
var caCerts []*x509.Certificate
var parsedKeys []crypto.PrivateKey
var isMatched bool
var foundKey crypto.PrivateKey
var interBytes [][]byte
var skipKeyPair bool = keys == nil
var parsedKeysBuf *bytes.Buffer = new(bytes.Buffer)

if pemBlocks, err = SplitPem(certRaw); err != nil {
return
}

for _, b := range pemBlocks {
if strings.Contains(b.Type, "PRIVATE KEY") {
parsedKeysBuf.Write(pem.EncodeToMemory(b))
continue
}
if b.Type != "CERTIFICATE" {
continue
}
if cert, err = x509.ParseCertificate(b.Bytes); err != nil {
return
}
if cert.IsCA {
if bytes.Equal(cert.RawIssuer, cert.RawSubject) {
caCerts = append(caCerts, cert)
} else {
intermediates = append(intermediates, cert)
}
}
certs = append(certs, cert)
}

if intermediates != nil && len(intermediates) != 0 {
interBytes = make([][]byte, len(intermediates))
for _, i := range intermediates {
interBytes = append(interBytes, i.Raw)
}
}

if parsedKeysBuf.Len() != 0 {
if parsedKeys, err = ParsePrivateKey(parsedKeysBuf.Bytes()); err != nil {
return
}
keys = append(keys, parsedKeys...)
}

// Now pair the certs and keys, and combine as a tls.Certificate.
for _, cert = range certs {
foundKey = nil
for _, k := range keys {
if isMatched, err = IsMatchedPair(k, cert); err != nil {
return
}
if isMatched {
foundKey = k
break
}
}
if foundKey == nil && !skipKeyPair {
continue
}
tlsCerts = append(
tlsCerts,
tls.Certificate{
Certificate: append([][]byte{cert.Raw}, interBytes...),
PrivateKey: foundKey,
Leaf: cert,
},
)
}

_ = caCerts

return
}

/*
ParseLeafCertSimple is like ParseLeafCert, but *only* returns leaf certificates;
no key correlation or chain building/association occurs.

TODO: Currently not fully implemented.
*/
/*
func ParseLeafCertSimple() () {

// TODO

return
}
*/

/*
ParsePemBundle splits a combined PEM (also referred to as "bundled PEMs") into one or more TlsPkiChains.

(combinedRaw must be the PEM-encoded bytes, not the decoded contained bytes.)

TODO: Currently not fully implemented.
*/
/*
func ParsePemBundle(combinedRaw []byte) (chains []*TlsPkiChain, err error) {

var roots []*x509.Certificate
var inters []*x509.Certificate
var keys []crypto.PrivateKey
var certs []tls.Certificate

if _, roots, inters, err = ParseCA(combinedRaw); err != nil {
return
}
if keys, err = ParsePrivateKey(combinedRaw); err != nil {
return
}

// TODO

return
}
*/

/*
ParsePrivateKey parses PEM bytes to a private key. Multiple keys may be concatenated in the same file.

Any public keys, certificates, etc. found will be discarded.
*/
func ParsePrivateKey(keyRaw []byte) (keys []crypto.PrivateKey, err error) {

var privKey crypto.PrivateKey
var pemBlocks []*pem.Block

if pemBlocks, err = SplitPem(keyRaw); err != nil {
return
}

for _, b := range pemBlocks {
if !strings.Contains(b.Type, "PRIVATE KEY") {
continue
}
switch b.Type {
case "RSA PRIVATE KEY": // PKCS#1
if privKey, err = x509.ParsePKCS1PrivateKey(b.Bytes); err != nil {
return
}
keys = append(keys, privKey)
case "EC PRIVATE KEY": // SEC 1, ASN.1 DER
if privKey, err = x509.ParseECPrivateKey(b.Bytes); err != nil {
return
}
keys = append(keys, privKey)
case "PRIVATE KEY": // PKCS#8
if privKey, err = x509.ParsePKCS8PrivateKey(b.Bytes); err != nil {
return
}
keys = append(keys, privKey)
default:
err = ErrUnknownKey
return
}
}

return
}

// SplitPem splits a single block of bytes into one (or more) (encoding/)pem.Blocks. Currently err is not used, but is reserved for future use.
func SplitPem(pemRaw []byte) (blocks []*pem.Block, err error) {

var pemBlocks *PemBlocks

if pemBlocks, err = SplitPemBlocks(pemRaw); err != nil {
return
}

blocks = pemBlocks.Split()

return
}

// SplitPemBlocks splits a single block of bytes into a PemBlocks. Currently err is not used, but is reserved for future use.
func SplitPemBlocks(pemRaw []byte) (blocks *PemBlocks, err error) {

var block *pem.Block
var nativeBlocks []*pem.Block
var rest []byte

for block, rest = pem.Decode(pemRaw); block != nil; block, rest = pem.Decode(rest) {
nativeBlocks = append(nativeBlocks, block)
}

blocks = new(PemBlocks)
*blocks = nativeBlocks

return
}

89
funcs_pemblocks.go Normal file
View File

@ -0,0 +1,89 @@
package cryptparse

import (
`bytes`
`encoding/pem`
)

// Bytes returns a combined PEM bytes of all blocks in a PemBlocks. Any nil, empty, or otherwise invalid blocks are skipped.
func (p *PemBlocks) Bytes() (combined []byte) {

var err error
var buf *bytes.Buffer = new(bytes.Buffer)

for _, block := range p.Split() {
if block == nil || block.Bytes == nil || block.Type == "" {
continue
}
// We've ruled out "contextual" errors, so we ignore it here.
if err = pem.Encode(buf, block); err != nil {
continue
}
}

combined = buf.Bytes()
_ = err

return
}

// BytesStrict is like Bytes but is much more strict/safe (invalid/empty/nil blocks are not skipped) and will return any errors on encoding.
func (p *PemBlocks) BytesStrict() (combined []byte, err error) {

var buf *bytes.Buffer = new(bytes.Buffer)

for _, block := range p.Split() {
if err = pem.Encode(buf, block); err != nil {
return
}
}

combined = buf.Bytes()

return
}

// BytesSplit returns separate PEM bytes of each block in a PemBlocks. Any nil, empty, or otherwise invalid blocks are skipped.
func (p *PemBlocks) BytesSplit() (pems [][]byte) {

var b []byte

for _, block := range p.Split() {
if block == nil || block.Bytes == nil || block.Type == "" {
continue
}
// We've ruled out "contextual" errors, so we ignore it here.
b = pem.EncodeToMemory(block)
pems = append(pems, b)
}

return
}

// BytesSplitStrict is like BytesSplit but is much more strict/safe (invalid/empty/nil blocks are not skipped) and will return any errors on encoding.
func (p *PemBlocks) BytesSplitStrict() (pems [][]byte, err error) {

var buf *bytes.Buffer = new(bytes.Buffer)

for _, block := range p.Split() {
buf.Reset()
if err = pem.Encode(buf, block); err != nil {
return
}
pems = append(pems, buf.Bytes())
}

return
}

// Split returns a more primitive-friendly representation of a PemBlocks.
func (p *PemBlocks) Split() (native []*pem.Block) {

if p == nil {
return
}

native = *p

return
}

37
funcs_test.go Normal file
View File

@ -0,0 +1,37 @@
package cryptparse

import (
`crypto/tls`
"testing"
)

func TestCiphers(t *testing.T) {

var err error
var cs *tls.CipherSuite

// Good ciphers
for _, cn := range []string{
"TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305_SHA256",
"tls ecdhe ecdsa with chacha20 poly1305 sha256",
} {
if cs, err = ParseTlsCipherSuiteStrict(cn); err != nil {
t.Fatalf("ERROR parsing good cipher '%s': %v", cn, err)
}
if cs.Name != cn {
t.Logf("Cipher name change: '%s' => '%s'", cn, cs.Name)
}
t.Logf("Cipher for '%s':\n%#v", cn, cs)
}

// Bad ciphers
for _, cn := range []string{
"TLS_BAD_CIPHER",
} {
if cs, err = ParseTlsCipherSuiteStrict(cn); err == nil {
t.Fatalf("ERROR parsing bad cipher '%s'; err is nil", cn)
}
}

_ = cs
}

217
funcs_tlsflat.go Normal file
View File

@ -0,0 +1,217 @@
package cryptparse

import (
`bytes`
`crypto`
`crypto/tls`
`crypto/x509`
`errors`
`fmt`
`net/url`
`os`
`strings`

`r00t2.io/sysutils/paths`
)

// Normalize ensures that all specified filepaths are absolute, etc.
func (t *TlsFlat) Normalize() (err error) {

if t.Certs != nil {
for _, c := range t.Certs {
if err = paths.RealPath(&c.CertFile); err != nil {
return
}
if c.KeyFile != nil {
if err = paths.RealPath(c.KeyFile); err != nil {
return
}
}
}
}
if t.CaFiles != nil {
for idx, _ := range t.CaFiles {
if err = paths.RealPath(&t.CaFiles[idx]); err != nil {
return
}
}
}

return
}

/*
ToTlsConfig returns a tls.Config from a TlsFlat. Note that it will have Normalize called on it.

Unfortunately it's not possible for this library to do the reverse, as CA certificates are not able to be extracted from an x509.CertPool.
*/
func (t *TlsFlat) ToTlsConfig() (tlsConf *tls.Config, err error) {

var b []byte
var rootCAs *x509.CertPool
var intermediateCAs []*x509.Certificate
var privKeys []crypto.PrivateKey
var tlsCerts []tls.Certificate
var parsedTlsCerts []tls.Certificate
var ciphers []uint16
var curves []tls.CurveID
var minVer uint16
var maxVer uint16
var buf *bytes.Buffer = new(bytes.Buffer)
var srvNm string = t.SniName

// Normalize any filepaths before validation.
if err = t.Normalize(); err != nil {
return
}

// And validate.
if err = validate.Struct(t); err != nil {
return
}

// CA cert(s).
buf.Reset()
if t.CaFiles != nil {
rootCAs = x509.NewCertPool()
for _, c := range t.CaFiles {
if b, err = os.ReadFile(c); err != nil {
if errors.Is(err, os.ErrNotExist) {
err = nil
continue
}
}
buf.Write(b)
}
if rootCAs, _, intermediateCAs, err = ParseCA(buf.Bytes()); err != nil {
return
}
} else {
if rootCAs, err = x509.SystemCertPool(); err != nil {
return
}
}

// Keys and Certs. They are assumed to be matched.
if t.Certs != nil {
for _, c := range t.Certs {
privKeys = nil
if c.KeyFile != nil {
if b, err = os.ReadFile(*c.KeyFile); err != nil {
return
}
if privKeys, err = ParsePrivateKey(b); err != nil {
return
}
}
if b, err = os.ReadFile(c.CertFile); err != nil {
return
}
if parsedTlsCerts, err = ParseLeafCert(b, privKeys, intermediateCAs...); err != nil {
return
}
tlsCerts = append(tlsCerts, parsedTlsCerts...)
}
}

// Ciphers.
if t.CipherSuites != nil {
ciphers = ParseTlsCiphers(strings.Join(t.CipherSuites, ","))
}

// Minimum TLS Protocol Version.
if t.MinTlsProtocol != nil {
if minVer, err = ParseTlsVersion(*t.MinTlsProtocol); err != nil {
return
}
}

// Maximum TLS Protocol Version.
if t.MaxTlsProtocol != nil {
if maxVer, err = ParseTlsVersion(*t.MaxTlsProtocol); err != nil {
return
}
}

// Curves.
if t.Curves != nil {
curves = ParseTlsCurves(strings.Join(t.Curves, ","))
}

tlsConf = &tls.Config{
Certificates: tlsCerts,
RootCAs: rootCAs,
ServerName: srvNm,
InsecureSkipVerify: t.SkipVerify,
CipherSuites: ciphers,
MinVersion: minVer,
MaxVersion: maxVer,
CurvePreferences: curves,
}
return
}

// ToTlsUri returns a TlsUri from a TlsFlat.
func (t *TlsFlat) ToTlsUri() (tlsUri *TlsUri, err error) {

var u *url.URL

if u, err = url.Parse(fmt.Sprintf("tls://%v/", t.SniName)); err != nil {
return
}

// CA cert(s).
if t.CaFiles != nil {
for _, c := range t.CaFiles {
u.Query().Add(TlsUriParamCa, c)
}
}

// Keys and Certs.
if t.Certs != nil {
for _, c := range t.Certs {
u.Query().Add(TlsUriParamCert, c.CertFile)
if c.KeyFile != nil {
u.Query().Add(TlsUriParamKey, *c.KeyFile)
}
}
}

// Enforce the SNI hostname.
u.Query().Add(TlsUriParamSni, t.SniName)

// Disable Verification.
if t.SkipVerify {
u.Query().Add(TlsUriParamNoVerify, "1")
}

// Ciphers.
if t.CipherSuites != nil {
for _, c := range t.CipherSuites {
u.Query().Add(TlsUriParamCipher, c)
}
}

// Minimum TLS Protocol Version.
if t.MinTlsProtocol != nil {
u.Query().Add(TlsUriParamMinTls, *t.MinTlsProtocol)
}

// Maximum TLS Protocol Version.
if t.MaxTlsProtocol != nil {
u.Query().Add(TlsUriParamMaxTls, *t.MaxTlsProtocol)
}

// Curves.
if t.Curves != nil {
for _, c := range t.Curves {
u.Query().Add(TlsUriParamCurve, c)
}
}

tlsUri = &TlsUri{
URL: u,
}

return
}

256
funcs_tlsuri.go Normal file
View File

@ -0,0 +1,256 @@
package cryptparse

import (
`crypto`
`crypto/tls`
`net`
`net/url`
`os`
`strings`
)

/*
WithConn returns a (crypto/)tls.Conn from an existing/already dialed net.Conn.

underlying should be a "bare" net.Conn; behavior is undefined/unknown if the underlying conn is already a (crypto/)tls.Conn.
*/
func (t *TlsUri) WithConn(underlying net.Conn) (conn *tls.Conn, err error) {

var cfg *tls.Config

if cfg, err = t.ToTlsConfig(); err != nil {
return
}

conn = tls.Client(underlying, cfg)

return
}

/*
ToConn returns a "bare" net.Conn (already dialed) from a TlsUri.

Note that this does NOT include the TLS configured or initialized; use TlsUri.ToTlsConn for that.
(A (crypto/)tls.Conn conforms to net.Conn.)

An error will be returned if no port is explicitly defined in the TlsUri.
*/
func (t *TlsUri) ToConn() (conn net.Conn, err error) {

var ok bool
var connHost string
var params map[string][]string
var netType string = DefaultNetType

params = t.Query()

if params != nil {
if _, ok = params[TlsUriParamNet]; ok {
netType = params[TlsUriParamNet][0]
}
}
netType = strings.ToLower(netType)

switch netType {
case "unix", "unixgram", "unixpacket":
connHost = t.Path
default:
connHost = t.Host
}

if conn, err = net.Dial(netType, connHost); err != nil {
return
}

return
}

/*
ToTlsConfig returns a *tls.Config from a TlsUri.

Unfortunately it's not possible for this library to do the reverse, as CA certificates are not able to be extracted from an x509.CertPool.
*/
func (t *TlsUri) ToTlsConfig() (cfg *tls.Config, err error) {

if cfg, err = ParseTlsUri(t.URL); err != nil {
return
}

return
}

/*
ToTlsConn returns a (crypto/)tls.Conn (already dialed) from a TlsUri.

An error will be returned if no port is explicitly defined in the TlsUri.
*/
func (t *TlsUri) ToTlsConn() (conn *tls.Conn, err error) {

var ok bool
var cfg *tls.Config
var connHost string
var params map[string][]string
var netType string = DefaultNetType

if cfg, err = t.ToTlsConfig(); err != nil {
return
}

params = t.Query()

if params != nil {
if _, ok = params[TlsUriParamNet]; ok {
netType = params[TlsUriParamNet][0]
}
}
netType = strings.ToLower(netType)

switch netType {
case "unix", "unixgram", "unixpacket":
connHost = t.Path
default:
connHost = t.Host
}

if conn, err = tls.Dial(netType, connHost, cfg); err != nil {
return
}

return
}

// ToTlsFlat returns a *TlsFlat from a TlsUri.
func (t *TlsUri) ToTlsFlat() (tlsFlat *TlsFlat, err error) {

var b []byte
var params url.Values
var paramMap map[string][]string
// These also have maps so they can backmap filenames.
var privKeys []crypto.PrivateKey
var privKeyMap map[string][]crypto.PrivateKey
var tlsCerts []tls.Certificate
var tlsCertMap map[string][]tls.Certificate
var isMatch bool
var fCert *TlsFlatCert
var val string
var f TlsFlat = TlsFlat{
SniName: t.Hostname(),
SkipVerify: false,
Certs: nil,
CaFiles: nil,
CipherSuites: nil,
MinTlsProtocol: nil,
MaxTlsProtocol: nil,
Curves: nil,
}

params = t.Query()
paramMap = params

if params == nil {
tlsFlat = &f
return
}

// CA cert(s).
if t.Query().Has(TlsUriParamCa) {
f.CaFiles = append(f.CaFiles, paramMap[TlsUriParamCa]...)
}

// Keys and Certs. These are done first so we can match to a client certificate.
if t.Query().Has(TlsUriParamKey) {
privKeyMap = make(map[string][]crypto.PrivateKey)
for _, kFile := range paramMap[TlsUriParamKey] {
if b, err = os.ReadFile(kFile); err != nil {
return
}
if privKeyMap[kFile], err = ParsePrivateKey(b); err != nil {
return
}
privKeys = append(privKeys, privKeyMap[kFile]...)
}
}
if t.Query().Has(TlsUriParamCert) {
tlsCertMap = make(map[string][]tls.Certificate)
for _, cFile := range paramMap[TlsUriParamCert] {
if b, err = os.ReadFile(cFile); err != nil {
return
}
if tlsCertMap[cFile], err = ParseLeafCert(b, privKeys); err != nil {
return
}
tlsCerts = append(tlsCerts, tlsCertMap[cFile]...)
}
}
// We then correlate. Whew, lads.
for cFile, c := range tlsCertMap {
for _, cert := range c {
for kFile, k := range privKeyMap {
if isMatch, err = IsMatchedPair(k, cert.Leaf); err != nil {
return
} else if isMatch {
fCert = &TlsFlatCert{
CertFile: cFile,
KeyFile: new(string),
}
*fCert.KeyFile = kFile
f.Certs = append(f.Certs, fCert)
}
}
}
}

// Hostname.
if t.Query().Has(TlsUriParamSni) {
f.SniName = t.Query().Get(TlsUriParamSni)
}

// Disable verification.
if t.Query().Has(TlsUriParamNoVerify) {
val = strings.ToLower(t.Query().Get(TlsUriParamNoVerify))
for _, i := range paramBoolValsTrue {
if val == i {
f.SkipVerify = true
break
}
}
}

// Ciphers.
if t.Query().Has(TlsUriParamCipher) {
f.CipherSuites = params[TlsUriParamCipher]
}

// Minimum TLS Protocol Version.
if t.Query().Has(TlsUriParamMinTls) {
f.MinTlsProtocol = new(string)
*f.MinTlsProtocol = t.Query().Get(TlsUriParamMinTls)
}

// Maximum TLS Protocol Version.
if t.Query().Has(TlsUriParamMaxTls) {
f.MaxTlsProtocol = new(string)
*f.MaxTlsProtocol = t.Query().Get(TlsUriParamMaxTls)
}

// Curves.
if t.Query().Has(TlsUriParamCurve) {
f.Curves = params[TlsUriParamCurve]
}

tlsFlat = &f

return
}

// ToURL returns the *url.URL representation of a TlsUri. Note that the params will remain, so remove them explicitly if needed.
func (t *TlsUri) ToURL() (u *url.URL) {

if t == nil {
return
}

u = t.URL

return
}

103
types.go Normal file
View File

@ -0,0 +1,103 @@
package cryptparse

import (
`crypto`
`crypto/tls`
`crypto/x509`
`encoding/pem`
`encoding/xml`
`net/url`

`github.com/Luzifer/go-dhparam`
)

// PemBlocks is a combined set of multiple pem.Blocks.
type PemBlocks []*pem.Block

// TlsFlat provides an easy structure to marshal/unmarshal a tls.Config from/to a data structure (JSON, XML, etc.).
type TlsFlat struct {
XMLName xml.Name `xml:"tlsConfig" json:"-" yaml:"-" toml:"-"`
// SniName represents the expected Server Name Indicator's name. See TlsUriParamSni.
SniName string `json:"sni_name" toml:"SNIName" yaml:"SNI Name" xml:"sniName,attr" required:"true" validate:"required"`
// SkipVerify, if true, will bypass certificate verification. You generally should not enable this. See TlsUriParamNoVerify.
SkipVerify bool `json:"skip_verify,omitempty" toml:"SkipVerify,omitempty" yaml:"Skip Verification,omitempty" xml:"skipVerify,attr,omitempty"`
// Certs contains 0 or more TlsFlatCert certificate definitions. See TlsUriParamCert and TlsUriParamKey as well.
Certs []*TlsFlatCert `json:"certs,omitempty" toml:"Certs,omitempty" yaml:"Certificates,omitempty" xml:"certs>cert,omitempty"validate:"omitempty,dive"`
// CaFiles contains filepaths to CA certificates/"trust anchors" in PEM format. They may be combined. See TlsUriParamCa.
CaFiles []string `json:"ca_files,omitempty" toml:"CaFiles,omitempty" yaml:"CA Files,omitempty" xml:"roots>ca,omitempty" validate:"omitempty,dive,filepath"`
// CipherSuites represents desired ciphers/cipher suites for this TLS environment. See TlsUriParamCipher.
CipherSuites []string `json:"cipher_suites,omitempty" toml:"CipherSuites,omitempty" yaml:"Cipher Suites,omitempty" xml:"ciphers,omitempty"`
// Curves specifies desired cryptographic curves to be used. See TlsUriParamCurve.
Curves []string `json:"curves,omitempty" xml:"curves>curve,omitempty" yaml:"Curves,omitempty" toml:"Curves,omitempty" validate:"omitempty,dive"`
// MinTlsProtocol specifies the minimum TLS version. See TlsUriParamMinTls.
MinTlsProtocol *string `json:"min_tls_protocol,omitempty" xml:"minTlsProtocol,attr,omitempty" yaml:"MinTlsProtocol,omitempty" toml:"MinTlsProtocol,omitempty"`
// MaxTlsProtocol specifies the maximum TLS version. See TlsUriParamMaxTls.
MaxTlsProtocol *string `json:"max_tls_protocol,omitempty" xml:"maxTlsProtocol,attr,omitempty" yaml:"MaxTlsProtocol,omitempty" toml:"MaxTlsProtocol,omitempty"`
}

// TlsFlatCert represents a certificate (and, possibly, paired key).
type TlsFlatCert struct {
XMLName xml.Name `xml:"cert" json:"-" yaml:"-" toml:"-"`
// KeyFile is a filepath to a PEM-encoded key file. See TlsUriParamKey.
KeyFile *string `json:"key,omitempty" xml:"key,attr,omitempty" yaml:"Key,omitempty" toml:"Key,omitempty" validate:"omitempty,filepath"`
// CertFile is a filepath to a PEM-encoded certificate file. See TlsUriParamCert.
CertFile string `json:"cert" xml:",chardata" yaml:"Certificate" toml:"Certificate" required:"true" validate:"required,filepath"`
}

// TlsPkiChain contains a whole X.509 PKI chain -- Root CA(s) (trust anchors) which sign Intermediate(s) which sign Certificate(s).
type TlsPkiChain struct {
/*
Roots are all trust anchors/root certificates.

Roots are certificates that are self-signed and can issue certificates/sign CSRs.
*/
Roots []*x509.Certificate
// RootsPool is an x509.CertPool representation of Roots.
RootsPool *x509.CertPool
/*
Intermediates are signers that should not be trusted directly, but instead included in the verification/validation chain.

Intermediates are certificates that are NOT self-signed (they should be signed by at least one Roots/RootsPool)
but CAN issue certificates/sign CSRs.
*/
Intermediates []*x509.Certificate
// IntermediatesPool is an x509.CertPool representation of Intermediates.
IntermediatesPool *x509.CertPool
/*
Certificates are "leaf certificates"; typically these are the certificates used directly by servers/users.

A certificate is considered a Certificate here if it is NOT self-signed and is NOT able to issue certificates/sign CSRs.
*/
Certificates []*tls.Certificate
// CertificatesPool is an x509.CertPool representation of Certificates.
CertificatesPool *x509.CertPool
/*
UnmatchedCerts contains Certificates that:
* Do not match any of Roots/RootsPool as its signer, and/or
* Do not match any Intermediates/IntermediatesPool as its signer, and/or
* Does not meet requirements for Roots/RootsPool, and/or
* Does not meet requirements for Intermediates/IntermediatesPool, and/or
* Has no matching crypto.PrivateKey found.

These should generally *never* be used if they were parsed in.
They represent "stray" certificates that have no logical chain/path found
and are likely unusable for purposes of this environment.
*/
UnmatchedCerts []*x509.Certificate
// UnmatchedCertsPool is an x509.CertPool representation of UnmatchedCerts.
UnmatchedCertsPool *x509.CertPool
/*
UnmatchedKeys represent parsed private keys that have no matching corresponding certifificate.

These should generally *never* be used if they were parsed in.
They represent "stray" keys that have no logical chain/path found
and are likely unusable for purposes of this environment.
*/
UnmatchedKeys []crypto.PrivateKey
// DhParams represent any found DH parameters. This will usually be empty.
DhParams []*dhparam.DH
}

type TlsUri struct {
*url.URL
}