ADDED:
* cryptparse, which makes managing certs/keys MUCH much easier (and
  better) than the stdlib utilities.
This commit is contained in:
brent saner 2024-06-21 17:18:19 -04:00
parent c0c924b75a
commit db20c70d86
Signed by: bts
GPG Key ID: 8C004C2F93481F6B
9 changed files with 1333 additions and 13 deletions

3
cryptparse/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.

123
cryptparse/consts.go Normal file
View File

@ -0,0 +1,123 @@
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
)

// TlsUriParam* specifiy URL query parameters to parse a tls:// URI.
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"
)

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

12
cryptparse/errs.go Normal file
View File

@ -0,0 +1,12 @@
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")
ErrUnknownKey error = errors.New("unknown key type")
)

751
cryptparse/funcs.go Normal file
View File

@ -0,0 +1,751 @@
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.VersionName(i); !strings.HasPrefix(nm, "0x") {
tlsCipherNmToUint[nm] = i
}
}
}

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

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
}

// 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
}

/*
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
}

/*
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
}
}

// TODO

return
}

// SplitPem splits a single block of bytes into one (or more) (encoding/)pem.Blocks.
func SplitPem(pemRaw []byte) (blocks []*pem.Block, err error) {

var block *pem.Block
var rest []byte

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

return
}

217
cryptparse/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
}

159
cryptparse/funcs_tlsuri.go Normal file
View File

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

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

/*
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
}

// 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.
func (t *TlsUri) ToURL() (u *url.URL) {

if t == nil {
return
}

u = t.URL

return
}

30
cryptparse/types.go Normal file
View File

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

import (
`encoding/xml`
`net/url`
)

// 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 string `json:"sni_name" xml:"sniName,attr" yaml:"SniName" toml:"SniName" required:"true" validate:"required"`
SkipVerify bool `json:"skip_verify,omitempty" xml:"skipVerify,attr,omitempty" yaml:"SkipVerify,omitempty" toml:"SkipVerify,omitempty"`
Certs []*TlsFlatCert `json:"certs,omitempty" xml:"certs>cert,omitempty" yaml:"Certs,omitempty" toml:"Certs,omitempty" validate:"omitempty,dive"`
CaFiles []string `json:"ca_files,omitempty" xml:"roots>ca,omitempty" yaml:"CaFiles,omitempty" toml:"CaFiles,omitempty" validate:"omitempty,dive,filepath"`
CipherSuites []string `json:"cipher_suites,omitempty" xml:"ciphers,omitempty" yaml:"CipherSuites,omitempty" toml:"CipherSuites,omitempty"`
MinTlsProtocol *string `json:"min_tls_protocol,omitempty" xml:"minTlsProtocol,attr,omitempty" yaml:"MinTlsProtocol,omitempty" toml:"MinTlsProtocol,omitempty"`
MaxTlsProtocol *string `json:"max_tls_protocol,omitempty" xml:"maxTlsProtocol,attr,omitempty" yaml:"MaxTlsProtocol,omitempty" toml:"MaxTlsProtocol,omitempty"`
Curves []string `json:"curves,omitempty" xml:"curves>curve,omitempty" yaml:"Curves,omitempty" toml:"Curves,omitempty" validate:"omitempty,dive"`
}

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

type TlsUri struct {
*url.URL
}

13
go.mod
View File

@ -5,14 +5,21 @@ go 1.21
require (
github.com/davecgh/go-spew v1.1.1
github.com/g0rbe/go-chattr v1.0.1
github.com/google/uuid v1.6.0
github.com/go-playground/validator/v10 v10.22.0
github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510
golang.org/x/sys v0.19.0
honnef.co/go/augeas v0.0.0-20161110001225-ca62e35ed6b8
r00t2.io/goutils v1.6.0
)

require (
github.com/coreos/go-systemd v0.0.0-20191104093116-d3cd4ed1dbcf // indirect
github.com/godbus/dbus v4.1.0+incompatible // indirect
github.com/gabriel-vasile/mimetype v1.4.3 // indirect
github.com/go-playground/locales v0.14.1 // indirect
github.com/go-playground/universal-translator v0.18.1 // indirect
github.com/leodido/go-urn v1.4.0 // indirect
golang.org/x/crypto v0.19.0 // indirect
golang.org/x/net v0.21.0 // indirect
golang.org/x/text v0.14.0 // indirect
)

// Pending https://github.com/g0rbe/go-chattr/pull/3

38
go.sum
View File

@ -1,22 +1,40 @@
github.com/coreos/go-systemd v0.0.0-20191104093116-d3cd4ed1dbcf h1:iW4rZ826su+pqaw19uhpSCzhj44qo35pNgKFGqzDKkU=
github.com/coreos/go-systemd v0.0.0-20191104093116-d3cd4ed1dbcf/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/godbus/dbus v4.1.0+incompatible h1:WqqLRTsQic3apZUK9qC5sGNfXthmPXzUZ7nQPrNITa4=
github.com/godbus/dbus v4.1.0+incompatible/go.mod h1:/YcGZj5zSblfDWMMoOzV4fas9FZnQYTkDnsGvmh2Grw=
github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I=
github.com/gabriel-vasile/mimetype v1.4.3 h1:in2uUcidCuFcDKtdcBxlR0rJ1+fsokWf+uqxgUFjbI0=
github.com/gabriel-vasile/mimetype v1.4.3/go.mod h1:d8uq/6HKRL6CGdk+aubisF/M5GcPfT7nKyLpA0lbSSk=
github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s=
github.com/go-playground/assert/v2 v2.2.0/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4=
github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA=
github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY=
github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY=
github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY=
github.com/go-playground/validator/v10 v10.22.0 h1:k6HsTZ0sTnROkhS//R0O+55JgM8C4Bx7ia+JlgcnOao=
github.com/go-playground/validator/v10 v10.22.0/go.mod h1:dbuPbCMFw/DrkbEynArYaCwl3amGuJotoKCe95atGMM=
github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 h1:El6M4kTTCOh6aBiKaUGG7oYTSPP8MxqL4YI3kZKwcP4=
github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510/go.mod h1:pupxD2MaaD3pAXIBCelhxNneeOaAeabZDe5s4K6zSpQ=
github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/johnnybubonic/go-chattr v0.0.0-20240126141003-459f46177b13 h1:tgEbuE4bNVjaCWWIB1u9lDzGqH/ZdBTg33+4vNW2rjg=
github.com/johnnybubonic/go-chattr v0.0.0-20240126141003-459f46177b13/go.mod h1:yQc6VPJfpDDC1g+W2t47+yYmzBNioax/GLiyJ25/IOs=
github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ=
github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk=
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
golang.org/x/crypto v0.19.0 h1:ENy+Az/9Y1vSrlrvBSyna3PITt4tiZLf7sgCjZBX7Wo=
golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU=
golang.org/x/net v0.21.0 h1:AQyQV4dYCvJ7vGmJyKki9+PBdyvhkSd8EIx/qb0AYv4=
golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44=
golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.19.0 h1:q5f1RH2jigJ1MoAWp2KTp3gm5zAGFUTarQZ5U386+4o=
golang.org/x/sys v0.19.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
r00t2.io/goutils v1.4.0 h1:/x/etLpMFv3+j1aPtT7KK2G0uOk+gQkGvXIYBCdjn3E=
r00t2.io/goutils v1.4.0/go.mod h1:9ObJI9S71wDLTOahwoOPs19DhZVYrOh4LEHmQ8SW4Lk=
r00t2.io/goutils v1.5.0 h1:haVk+wUK1BAk8f4UFGjy3ov3DwGMauZAOv/XYdb9isQ=
r00t2.io/goutils v1.5.0/go.mod h1:9ObJI9S71wDLTOahwoOPs19DhZVYrOh4LEHmQ8SW4Lk=
golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ=
golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
honnef.co/go/augeas v0.0.0-20161110001225-ca62e35ed6b8 h1:FW42yWB1sGClqswyHIB68wo0+oPrav1IuQ+Tdy8Qp8E=
honnef.co/go/augeas v0.0.0-20161110001225-ca62e35ed6b8/go.mod h1:44w9OfBSQ9l3o59rc2w3AnABtE44bmtNnRMNC7z+oKE=
r00t2.io/goutils v1.6.0 h1:oBC6PgBv0y/fdHeCmWgORHpBiU8uWw7IfFQJX5rIuzY=
r00t2.io/goutils v1.6.0/go.mod h1:9ObJI9S71wDLTOahwoOPs19DhZVYrOh4LEHmQ8SW4Lk=
r00t2.io/sysutils v1.1.1/go.mod h1:Wlfi1rrJpoKBOjWiYM9rw2FaiZqraD6VpXyiHgoDo/o=