From bf887ce9481c2d455debadc6bb7af5d82fb74866 Mon Sep 17 00:00:00 2001 From: brent saner Date: Thu, 17 Oct 2024 15:45:53 -0400 Subject: [PATCH] v0.0.1 MOVED: * This codebase was originally part of r00t2.io/sysutils but has been split off to its own library. --- .gitignore | 36 ++ LICENSE | 9 + TODO | 3 + consts.go | 134 +++++++ errs.go | 13 + funcs.go | 913 +++++++++++++++++++++++++++++++++++++++++++++ funcs_pemblocks.go | 89 +++++ funcs_test.go | 37 ++ funcs_tlsflat.go | 217 +++++++++++ funcs_tlsuri.go | 256 +++++++++++++ types.go | 103 +++++ 11 files changed, 1810 insertions(+) create mode 100644 .gitignore create mode 100644 LICENSE create mode 100644 TODO create mode 100644 consts.go create mode 100644 errs.go create mode 100644 funcs.go create mode 100644 funcs_pemblocks.go create mode 100644 funcs_test.go create mode 100644 funcs_tlsflat.go create mode 100644 funcs_tlsuri.go create mode 100644 types.go diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..5f32697 --- /dev/null +++ b/.gitignore @@ -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/ diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..ddd318c --- /dev/null +++ b/LICENSE @@ -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. diff --git a/TODO b/TODO new file mode 100644 index 0000000..a02d066 --- /dev/null +++ b/TODO @@ -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. diff --git a/consts.go b/consts.go new file mode 100644 index 0000000..0b3577a --- /dev/null +++ b/consts.go @@ -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()) +) diff --git a/errs.go b/errs.go new file mode 100644 index 0000000..1cdf056 --- /dev/null +++ b/errs.go @@ -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") +) diff --git a/funcs.go b/funcs.go new file mode 100644 index 0000000..5677991 --- /dev/null +++ b/funcs.go @@ -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(¶ms[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 +} diff --git a/funcs_pemblocks.go b/funcs_pemblocks.go new file mode 100644 index 0000000..bb928ad --- /dev/null +++ b/funcs_pemblocks.go @@ -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 +} diff --git a/funcs_test.go b/funcs_test.go new file mode 100644 index 0000000..b491a5a --- /dev/null +++ b/funcs_test.go @@ -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 +} diff --git a/funcs_tlsflat.go b/funcs_tlsflat.go new file mode 100644 index 0000000..b205e75 --- /dev/null +++ b/funcs_tlsflat.go @@ -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 +} diff --git a/funcs_tlsuri.go b/funcs_tlsuri.go new file mode 100644 index 0000000..1586603 --- /dev/null +++ b/funcs_tlsuri.go @@ -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 +} diff --git a/types.go b/types.go new file mode 100644 index 0000000..1ebdfde --- /dev/null +++ b/types.go @@ -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 +}