From 1471dc29ed5aad5ced8c6581f48ab87d44cf3dbd Mon Sep 17 00:00:00 2001 From: brent saner Date: Sat, 25 Jan 2025 16:11:19 -0500 Subject: [PATCH] reflection work so far... --- .githooks/pre-commit/01-go_gen.py | 4 + .gitignore | 8 + _extra/gen_test_pki/_testdata/.keep | 0 _extra/gen_test_pki/consts.go | 144 ++++++ _extra/gen_test_pki/funcs.go | 476 ++++++++++++++++++ _extra/gen_test_pki/go.mod | 9 + _extra/gen_test_pki/go.sum | 6 + _extra/gen_test_pki/init.go | 21 + _extra/gen_test_pki/main.go | 13 + _extra/gen_test_pki/types.go | 18 + _testdata/.keep | 0 consts.go | 208 ++++++-- consts_param_map.go | 45 ++ errs.go | 4 + funcs.go | 234 +++++++-- funcs_fieldmap_tlsuri.go | 173 +++++++ funcs_reflectflat.go | 65 +++ funcs_reflecturi.go | 60 +++ funcs_test.go | 137 +++++ funcs_tlsflat.go | 26 +- funcs_tlsflat.go.old | 223 ++++++++ funcs_tlsuri.go | 155 ++++-- go.mod | 24 + go.sum | 42 ++ internal/constmap/consts.go | 32 ++ internal/constmap/funcs.go | 94 ++++ internal/constmap/main.go | 57 +++ internal/constmap/tpl/consts_param_map.go.tpl | 24 + internal/constmap/types.go | 8 + tlsuri_test.go | 4 + types.go | 76 ++- 31 files changed, 2240 insertions(+), 150 deletions(-) create mode 100755 .githooks/pre-commit/01-go_gen.py create mode 100644 _extra/gen_test_pki/_testdata/.keep create mode 100644 _extra/gen_test_pki/consts.go create mode 100644 _extra/gen_test_pki/funcs.go create mode 100644 _extra/gen_test_pki/go.mod create mode 100644 _extra/gen_test_pki/go.sum create mode 100644 _extra/gen_test_pki/init.go create mode 100644 _extra/gen_test_pki/main.go create mode 100644 _extra/gen_test_pki/types.go create mode 100644 _testdata/.keep create mode 100644 consts_param_map.go create mode 100644 funcs_fieldmap_tlsuri.go create mode 100644 funcs_reflectflat.go create mode 100644 funcs_reflecturi.go create mode 100644 funcs_tlsflat.go.old create mode 100644 go.mod create mode 100644 go.sum create mode 100644 internal/constmap/consts.go create mode 100644 internal/constmap/funcs.go create mode 100644 internal/constmap/main.go create mode 100644 internal/constmap/tpl/consts_param_map.go.tpl create mode 100644 internal/constmap/types.go create mode 100644 tlsuri_test.go diff --git a/.githooks/pre-commit/01-go_gen.py b/.githooks/pre-commit/01-go_gen.py new file mode 100755 index 0000000..e23a2c1 --- /dev/null +++ b/.githooks/pre-commit/01-go_gen.py @@ -0,0 +1,4 @@ +#!/bin/bash + +go generate +git add consts_param_map.go diff --git a/.gitignore b/.gitignore index 5f32697..f3946bb 100644 --- a/.gitignore +++ b/.gitignore @@ -26,6 +26,14 @@ *.so *.dylib +# The test data is intended to be generated locally. +# There's no reason to pollute the repo with it. +_extra/gen_test_pki/_testdata/* +!_extra/gen_test_pki/_testdata/.keep +_extra/gen_test_pki/gen_test_pki +_testdata/* +!_testdata/.keep + # Test binary, built with `go test -c` *.test diff --git a/_extra/gen_test_pki/_testdata/.keep b/_extra/gen_test_pki/_testdata/.keep new file mode 100644 index 0000000..e69de29 diff --git a/_extra/gen_test_pki/consts.go b/_extra/gen_test_pki/consts.go new file mode 100644 index 0000000..14f670a --- /dev/null +++ b/_extra/gen_test_pki/consts.go @@ -0,0 +1,144 @@ +package main + +import ( + `crypto/x509` + `crypto/x509/pkix` + `embed` + `net` + `time` +) + +var ( + pairTypes []string = []string{ + "ca", + "inter", + "leaf_server", + "leaf_user", + } + + keyTypes []string = []string{ + /* + Per: + https://pkg.go.dev/crypto/x509#CreateCertificate + https://pkg.go.dev/crypto/x509#CreateCertificateRequest + ECDH keys are not supported for certificates (only ECDSA, ED25519, and RSA). + */ + // "ecdh", + "ecdsa", + "ed25519", + "rsa", + } + + // Populated by init. + pairs map[string]*Pair = make(map[string]*Pair) +) + +var ( + //go:embed "_testdata/*" + pems embed.FS +) + +const ( + caCn string = "gen_test_pki Root CA" + interCn string = "gen_test_pki Intermediate CA" + serverCn string = "server.example.com" + userCn string = "username@example.com" +) + +var ( + pkixCommon *pkix.Name = &pkix.Name{ + Country: []string{ + "XX", + }, + Organization: []string{ + "An Example Organization", + }, + OrganizationalUnit: []string{ + "An Example Department", + }, + Locality: []string{ + "Some City", + }, + Province: []string{ + "Some State", + }, + StreetAddress: []string{ + "123 Example Street", + }, + PostalCode: []string{ + "12345", + }, + // SerialNumber: "", // SerialNumber should be blank, and contextually generated via getSerial(). + // CommonName: "", // CommonName should be blank, and contextually generated via getSubj(). + Names: nil, + ExtraNames: nil, + } + certTpl map[string]*x509.Certificate = map[string]*x509.Certificate{ + "ca": &x509.Certificate{ + SerialNumber: getSerial(), + Subject: getSubj(caCn), + NotBefore: time.Now().Add(time.Second * -10), + NotAfter: time.Now().Add(10 * 365 * 24 * time.Hour), // (about) 10 years + KeyUsage: x509.KeyUsageCertSign | x509.KeyUsageCRLSign, + BasicConstraintsValid: true, + IsCA: true, + MaxPathLen: 1, + }, + "inter": &x509.Certificate{ + SerialNumber: getSerial(), + Subject: getSubj(interCn), + NotBefore: time.Now().Add(time.Second * -9), + NotAfter: time.Now().Add(9 * 365 * 24 * time.Hour), // (about) 9 years + KeyUsage: x509.KeyUsageCertSign | x509.KeyUsageCRLSign, + BasicConstraintsValid: true, + IsCA: true, + MaxPathLen: 0, + }, + "leaf_server": &x509.Certificate{ + SerialNumber: getSerial(), + Subject: getSubj(serverCn), + NotBefore: time.Now().Add(time.Second * -8), + NotAfter: time.Now().Add(9 * 365 * 24 * time.Hour), // (about) 8 years + KeyUsage: x509.KeyUsageDigitalSignature | x509.KeyUsageKeyEncipherment, + ExtKeyUsage: []x509.ExtKeyUsage{ + x509.ExtKeyUsageServerAuth, + }, + }, + "leaf_user": &x509.Certificate{ + SerialNumber: getSerial(), + Subject: getSubj(userCn), + NotBefore: time.Now().Add(time.Second * -8), + NotAfter: time.Now().Add(9 * 365 * 24 * time.Hour), // (about) 8 years + KeyUsage: x509.KeyUsageDigitalSignature | x509.KeyUsageKeyEncipherment, + ExtKeyUsage: []x509.ExtKeyUsage{ + x509.ExtKeyUsageClientAuth, + }, + }, + } + csrs map[string]*x509.CertificateRequest = map[string]*x509.CertificateRequest{ + "inter": &x509.CertificateRequest{ + Subject: getSubj(interCn), + }, + "leaf_server": &x509.CertificateRequest{ + Subject: getSubj(serverCn), + IPAddresses: []net.IP{ + net.IP(net.ParseIP("127.0.0.1")), + net.IP(net.ParseIP("::ffff:127.0.0.1")), + net.IP(net.ParseIP("::1")), + }, + }, + "leaf_user": &x509.CertificateRequest{ + Subject: getSubj(userCn), + }, + } + parents map[string]string = map[string]string{ + "inter": "ca", + "leaf_server": "inter", + "leaf_user": "inter", + } + certgenOrder []string = []string{ + "inter", + "leaf_server", + "leaf_user", + } +) diff --git a/_extra/gen_test_pki/funcs.go b/_extra/gen_test_pki/funcs.go new file mode 100644 index 0000000..61fd406 --- /dev/null +++ b/_extra/gen_test_pki/funcs.go @@ -0,0 +1,476 @@ +package main + +import ( + `bytes` + `crypto` + `crypto/ecdh` + `crypto/ecdsa` + `crypto/ed25519` + `crypto/elliptic` + `crypto/rand` + `crypto/rsa` + `crypto/x509` + `crypto/x509/pkix` + `encoding/binary` + `encoding/pem` + `errors` + `fmt` + `log` + `math/big` + `os` + `path/filepath` + + `github.com/brunoga/deep` + `github.com/google/uuid` +) + +/* + getKeyFpath returns a (relative) path for a key PEM file (PKCS#8). + + This is used both when fetching from embed.FS and when generating new keys. +*/ +func getKeyFpath(pairType, keyType string) (path string) { + + path = filepath.Join("_testdata", fmt.Sprintf("%s_%s_key.pem", pairType, keyType)) + + return +} + +/* + getCertFpath returns a (relative) path for a cert PEM file. + + This is used both when fetching from embed.FS and when generating new certs. +*/ +func getCertFpath(pairType, keyType string) (path string) { + + path = filepath.Join("_testdata", fmt.Sprintf("%s_%s_cert.pem", pairType, keyType)) + + return +} + +/* + getChainFpath returns a (relative) path for a chained cert PEM file. + + This is used both when fetching from embed.FS and when generating new certs. +*/ +func getChainFpath(pairType, keyType string) (path string) { + + path = filepath.Join("_testdata", fmt.Sprintf("%s_%s_cert_chained.pem", pairType, keyType)) + + return +} + +/* + getCsrFpath returns a (relative) path for a CSR PEM file. + + This is used both when fetching from embed.FS and when generating new CSRs. +*/ +func getCsrFpath(pairType, keyType string) (path string) { + + path = filepath.Join("_testdata", fmt.Sprintf("%s_%s_csr.pem", pairType, keyType)) + + return +} + +/* + getKeypair takes cert type t and key type kt and returns the crypto.Private and crypto.Public keys for it. + + It assumes that loadKeys() at the *least* has already been called. +*/ +func getKeypair(t, kt string) (priv crypto.PrivateKey, pub crypto.PublicKey) { + + priv = pairs[t].privKeys[kt] + + switch k := priv.(type) { + case *ecdh.PrivateKey: + pub = k.Public() + case *ecdsa.PrivateKey: + pub = k.Public() + case ed25519.PrivateKey: // This is correct. Unlike other kt's, ed25519 doesn't use pointers. + pub = k.Public() + case *rsa.PrivateKey: + pub = k.Public() + } + + return +} + +/* + getSerial returns a (pseudo-)random certificate based on a UUIDv4 (RFC 4122 type 4). + + This guarantees not only that renewals (if issued/implemented) are reasonably guaranteed + to be different from the past issuance but also that the serial issuance is non-sequential + (both are common modern requirements of modern browser-trusted CAs; see + https://cabforum.org/working-groups/server/baseline-requirements/documents/) +*/ +func getSerial() (serial *big.Int) { + + var b []byte + var n int64 + var u uuid.UUID = uuid.New() + + b = u[:] + + n = int64(binary.BigEndian.Uint64(b)) + // Serials must be positive. + if n < 0 { + n = -n + } + + serial = big.NewInt(n) + + return +} + +// getTpl returns a version of certificate template tpl with a randomized serial. +func getTpl(tpl *x509.Certificate) (newTpl *x509.Certificate) { + + newTpl = new(x509.Certificate) + *newTpl = *tpl + + newTpl.SerialNumber = getSerial() + + return +} + +// getSubj returns a cert/CSR-specific pkix.Name from a given cn (commonName). +func getSubj(cn string) (newSubj pkix.Name) { + + newSubj = deep.MustCopy(*pkixCommon) + newSubj.CommonName = cn + + return +} + +// loadKeys either loads from pems or generates and writes out the PEM keys. +func loadKeys() (err error) { + + var b []byte + var t string + var kt string + var ok bool + var pemBlock *pem.Block + var keybuf *bytes.Buffer = new(bytes.Buffer) + + // Load in any existing keys. + for _, t = range pairTypes { + for _, kt = range keyTypes { + log.Printf("Loading %s key %s\n", t, kt) + if b, err = pems.ReadFile(getKeyFpath(t, kt)); err != nil { + if errors.Is(err, os.ErrNotExist) { + // Will generate missing below + pairs[t] = &Pair{ + pairType: t, + keyBytes: make(map[string][]byte), + privKeys: make(map[string]crypto.PrivateKey), + certBytes: make(map[string][]byte), + certs: make(map[string]*x509.Certificate), + csrBytes: make(map[string][]byte), + csrs: make(map[string]*x509.CertificateRequest), + chainParentBytes: make(map[string][]byte), + chainParent: make(map[string]*x509.Certificate), + } + err = nil + continue + } + return + } + if _, ok = pairs[t]; !ok { + pairs[t] = &Pair{ + pairType: t, + keyBytes: make(map[string][]byte), + privKeys: make(map[string]crypto.PrivateKey), + certBytes: make(map[string][]byte), + certs: make(map[string]*x509.Certificate), + csrBytes: make(map[string][]byte), + csrs: make(map[string]*x509.CertificateRequest), + } + } + pairs[t].keyBytes[kt] = b + if pairs[t].privKeys[kt], err = x509.ParsePKCS8PrivateKey(b); err != nil { + return + } + } + } + // Generate any missing keys. + for _, t = range pairTypes { + for _, kt = range keyTypes { + if _, ok = pairs[t].privKeys[kt]; !ok { + log.Printf("Generating %s key %s\n", t, kt) + keybuf.Reset() + switch kt { + case "ecdh": + if pairs[t].privKeys[kt], err = ecdh.X25519().GenerateKey(rand.Reader); err != nil { + return + } + case "ecdsa": + if pairs[t].privKeys[kt], err = ecdsa.GenerateKey(elliptic.P521(), rand.Reader); err != nil { + return + } + case "ed25519": + if _, pairs[t].privKeys[kt], err = ed25519.GenerateKey(rand.Reader); err != nil { + return + } + case "rsa": + if pairs[t].privKeys[kt], err = rsa.GenerateKey(rand.Reader, 4096); err != nil { + return + } + } + if b, err = x509.MarshalPKCS8PrivateKey(pairs[t].privKeys[kt]); err != nil { + log.Panicln(err) + } + pemBlock = &pem.Block{ + Type: "PRIVATE KEY", + Headers: nil, + Bytes: b, + } + b = pem.EncodeToMemory(pemBlock) + pairs[t].keyBytes[kt] = b + if err = os.WriteFile(getKeyFpath(t, kt), b, 0o0600); err != nil { + return + } + } + } + } + + return +} + +// loadCerts combines all loadCert* functions in the proper order. It is expected that loadKeys has already been run. +func loadCerts() (err error) { + + var b []byte + var t string + var kt string + var tkt [2]string + var chainMissing [][2]string = make([][2]string, 0, (len(certgenOrder)-1)*len(keyTypes)) + + if err = loadCertCa(); err != nil { + return + } + if err = loadCertIssued(); err != nil { + return + } + + // And create chained certs of leaves so they fully validate. + for _, t = range certgenOrder { + if t == "inter" { + continue // Don't bother with chaining the intermediate. If we play around with multiple intermediates, we will. + } + for _, kt = range keyTypes { + pairs[t].chainParent[kt] = pairs[parents[t]].certs[kt] + log.Printf("Loading %s chain %s\n", t, kt) + if b, err = pems.ReadFile(getChainFpath(t, kt)); err != nil { + if errors.Is(err, os.ErrNotExist) { + err = nil + chainMissing = append(chainMissing, [2]string{t, kt}) + continue + } + return + } + // Found + pairs[t].chainParentBytes[kt] = b + } + } + + // "Generate" missing. + for _, tkt = range chainMissing { + t = tkt[0] + kt = tkt[1] + log.Printf("Building %s chain %s\n", t, kt) + b = append(pairs[t].certBytes[kt], pairs[parents[t]].certBytes[kt]...) + if err = os.WriteFile(getChainFpath(t, kt), b, 0o0600); err != nil { + return + } + } + + return +} + +// loadCertCa loads (or generates) the root CA/anchor. It is expected that loadKeys has already been run. +func loadCertCa() (err error) { + + var b []byte + var kt string + var ok bool + var privKey crypto.PrivateKey + var pubKey crypto.PublicKey + var pemBlock *pem.Block + var ktTpl *x509.Certificate + + for _, kt = range keyTypes { + log.Printf("Loading CA certificate %s\n", kt) + if b, err = pems.ReadFile(getCertFpath("ca", kt)); err != nil { + if errors.Is(err, os.ErrNotExist) { + // Will generate missing below. + err = nil + continue + } + return + } + // Assume the mapped Pair exists per loadKeys. + pairs["ca"].certBytes[kt] = b + pemBlock, _ = pem.Decode(b) + if pairs["ca"].certs[kt], err = x509.ParseCertificate(pemBlock.Bytes); err != nil { + return + } + } + + // Generate missing CA certs. + for _, kt = range keyTypes { + log.Printf("Generating CA certificate %s\n", kt) + if _, ok = pairs["ca"].certs[kt]; !ok { + ktTpl = getTpl(certTpl["ca"]) + privKey, pubKey = getKeypair("ca", kt) + // Specifying the same cert template for both the template and parent params creates a self-signed. + if b, err = x509.CreateCertificate( + rand.Reader, + ktTpl, + ktTpl, + pubKey, + privKey, + ); err != nil { + return + } + if pairs["ca"].certs[kt], err = x509.ParseCertificate(b); err != nil { + return + } + pemBlock = &pem.Block{ + Type: "CERTIFICATE", + Headers: nil, + Bytes: b, + } + b = pem.EncodeToMemory(pemBlock) + pairs["ca"].certBytes[kt] = b + if err = os.WriteFile(getCertFpath("ca", kt), b, 0o0600); err != nil { + return + } + } + } + + return +} + +// loadCertIssued handles the intermediate, "server" leaf, and "user" leaf. +func loadCertIssued() (err error) { + + var b []byte + var ok bool + var t string + var kt string + var tkt [2]string + var ktMap map[string]bool + var caCert *x509.Certificate + var caPrivKey crypto.PrivateKey + var certPrivKey crypto.PrivateKey + var certPubKey crypto.PublicKey + var pemBlock *pem.Block + var ktTpl *x509.Certificate + // map[][]; map so we can condense dupes + var certMissing map[string]map[string]bool = make(map[string]map[string]bool) + var csrMissing [][2]string = make([][2]string, 0, len(certgenOrder)*len(keyTypes)) + + // CSRS + // Find existing CSRs and certs + for _, t = range certgenOrder { + for _, kt = range keyTypes { + log.Printf("Loading %s CSR %s\n", t, kt) + if b, err = pems.ReadFile(getCsrFpath(t, kt)); err != nil { + if errors.Is(err, os.ErrNotExist) { + err = nil + csrMissing = append(csrMissing, [2]string{t, kt}) + continue + } + return + } + // Assume the mapped Pair exists per loadKeys. + pairs[t].csrBytes[kt] = b + pemBlock, _ = pem.Decode(b) + if pairs[t].csrs[kt], err = x509.ParseCertificateRequest(pemBlock.Bytes); err != nil { + return + } + log.Printf("Loading %s certificate %s\n", t, kt) + if b, err = pems.ReadFile(getCertFpath(t, kt)); err != nil { + if errors.Is(err, os.ErrNotExist) { + err = nil + if _, ok = certMissing[t]; !ok { + certMissing[t] = make(map[string]bool) + } + certMissing[t][kt] = true + continue + } + } + pairs[t].certBytes[kt] = b + pemBlock, _ = pem.Decode(b) + if pairs[t].certs[kt], err = x509.ParseCertificate(pemBlock.Bytes); err != nil { + return + } + } + } + + // Generate missing CSRs. + for _, tkt = range csrMissing { + t = tkt[0] + kt = tkt[1] + log.Printf("Generating %s CSR %s\n", t, kt) + certPrivKey, certPubKey = getKeypair(t, kt) + if b, err = x509.CreateCertificateRequest(rand.Reader, csrs[t], certPrivKey); err != nil { + return + } + if pairs[t].csrs[kt], err = x509.ParseCertificateRequest(b); err != nil { + return + } + pemBlock = &pem.Block{ + Type: "CERTIFICATE REQUEST", + Headers: nil, + Bytes: b, + } + b = pem.EncodeToMemory(pemBlock) + pairs[t].csrBytes[kt] = b + if err = os.WriteFile(getCsrFpath(t, kt), b, 0o0600); err != nil { + return + } + if _, ok = certMissing[t]; !ok { + certMissing[t] = make(map[string]bool) + } + certMissing[t][kt] = true + } + + // Force re-gen of certs for above new CSRs and gen missing. + for _, t = range certgenOrder { + if ktMap, ok = certMissing[t]; !ok { + continue + } + for kt, _ = range ktMap { + log.Printf("Generating %s certificate %s\n", t, kt) + caCert = pairs[parents[t]].certs[kt] + caPrivKey = pairs[parents[t]].privKeys[kt] + _, certPubKey = getKeypair(t, kt) + ktTpl = getTpl(certTpl[t]) + if b, err = x509.CreateCertificate( + rand.Reader, + ktTpl, + caCert, + certPubKey, + caPrivKey, + ); err != nil { + return + } + if pairs[t].certs[kt], err = x509.ParseCertificate(b); err != nil { + return + } + pemBlock = &pem.Block{ + Type: "CERTIFICATE", + Headers: nil, + Bytes: b, + } + b = pem.EncodeToMemory(pemBlock) + pairs[t].certBytes[kt] = b + if err = os.WriteFile(getCertFpath(t, kt), b, 0o0600); err != nil { + return + } + } + } + + return +} diff --git a/_extra/gen_test_pki/go.mod b/_extra/gen_test_pki/go.mod new file mode 100644 index 0000000..285eebd --- /dev/null +++ b/_extra/gen_test_pki/go.mod @@ -0,0 +1,9 @@ +module r00t2.io/cryptparse/_extra/gen_test_pki + +go 1.23.2 + +require ( + github.com/brunoga/deep v1.2.4 + github.com/davecgh/go-spew v1.1.1 + github.com/google/uuid v1.6.0 +) diff --git a/_extra/gen_test_pki/go.sum b/_extra/gen_test_pki/go.sum new file mode 100644 index 0000000..4694a8c --- /dev/null +++ b/_extra/gen_test_pki/go.sum @@ -0,0 +1,6 @@ +github.com/brunoga/deep v1.2.4 h1:Aj9E9oUbE+ccbyh35VC/NHlzzjfIVU69BXu2mt2LmL8= +github.com/brunoga/deep v1.2.4/go.mod h1:GDV6dnXqn80ezsLSZ5Wlv1PdKAWAO4L5PnKYtv2dgaI= +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/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= +github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= diff --git a/_extra/gen_test_pki/init.go b/_extra/gen_test_pki/init.go new file mode 100644 index 0000000..bf0b35d --- /dev/null +++ b/_extra/gen_test_pki/init.go @@ -0,0 +1,21 @@ +package main + +import ( + `log` +) + +func init() { + + var err error + + // Keys need to be loaded/generated first for everything. + if err = loadKeys(); err != nil { + log.Panicln(err) + } + + // Then the certificates. + if err = loadCerts(); err != nil { + log.Panicln(err) + } + +} diff --git a/_extra/gen_test_pki/main.go b/_extra/gen_test_pki/main.go new file mode 100644 index 0000000..d90f926 --- /dev/null +++ b/_extra/gen_test_pki/main.go @@ -0,0 +1,13 @@ +package main + +import ( + `fmt` +) + +func main() { + + // Everything's handled in init() + + fmt.Println("Done.") + +} diff --git a/_extra/gen_test_pki/types.go b/_extra/gen_test_pki/types.go new file mode 100644 index 0000000..f782a27 --- /dev/null +++ b/_extra/gen_test_pki/types.go @@ -0,0 +1,18 @@ +package main + +import ( + `crypto` + `crypto/x509` +) + +type Pair struct { + pairType string // see pairTypes + keyBytes map[string][]byte // see keyTypes for keys for this map + privKeys map[string]crypto.PrivateKey // see keyTypes for keys for this map + certBytes map[string][]byte // see keyTypes for keys for this map + certs map[string]*x509.Certificate // see keyTypes for keys for this map + csrs map[string]*x509.CertificateRequest // see keyTypes for keys for this map; does not exist for ca + csrBytes map[string][]byte // see keyTypes for keys for this map; does not exist for ca + chainParentBytes map[string][]byte + chainParent map[string]*x509.Certificate +} diff --git a/_testdata/.keep b/_testdata/.keep new file mode 100644 index 0000000..e69de29 diff --git a/consts.go b/consts.go index 0b3577a..c8397a7 100644 --- a/consts.go +++ b/consts.go @@ -12,6 +12,10 @@ var ( tlsCurveNmToCurve map[string]tls.CurveID ) +const ( + dfltStructTag string = "tlsUri" +) + const ( MaxTlsCipher uint16 = tls.TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305_SHA256 MaxCurveId tls.CurveID = tls.X25519 // 29 @@ -20,50 +24,56 @@ const ( DefaultNetType string = "tcp" ) -// TlsUriParam* specifiy URL query parameters to parse a tls:// URI, and are used by TlsUri methods. +const ( + // KeyLogEnv specifies the TLS key log file. + // !! ONLY USE THIS FOR DEBUGGING !! + KeyLogEnv string = "SSLKEYLOGFILE" + /* + KeyLogEnvVal specifies the special ParamKeylog value to use the + value of the environment variable as named in KeyLogEnv. + */ + KeyLogEnvVal string = "_env_" + // KeyLogBufVal specifies the special ParamKeylog value to use an in-memory buffer. + KeyLogBufVal string = "_buf_" +) + +//go:generate go run ./internal/constmap + +/* + TlsUriParam* specifiy URL query parameters to parse a tls:// URI, and are used by TlsUri methods. + + NOTE: If these consts' type or "Param*" prefix changes, internal/constmap/consts.go will also need to be changed. + The above go:generate creates (within this main module namespace): + tlsUriParamStrMap (a map of string( const name) => ) + tlsUriStrParamMap (a map of => string( const name)) +*/ const ( /* - TlsUriParamCa specifies a path to a CA certificate PEM-encded DER file. + ParamCa specifies a path to a CA certificate PEM-encded DER file. + + If not specified, the system's roots/trust anchors are used. + Files specified here will be included *in addition to* any + embedded root anchors found in the ParamCert parameter's + value, if using concatenated cert chains. It may be specified multiple times in a TLS URI. */ - TlsUriParamCa string = "pki_ca" + ParamCa tlsUriParam = "pki_ca" /* - TlsUriParamCert specifies a path to a client certificate PEM-encded DER file. + ParamCert specifies a path to a leaf certificate PEM-encded DER file. + + It may (and should/must) include any intermediate certificates necessary for + validation chain on the remote end. + It may include trust anchors, which will be considered *in addition to* + any ParamCa. + It may include a corresponding private key, which will be included for consideration + *in addition to* any ParamKey. It may be specified multiple times in a TLS URI. */ - TlsUriParamCert string = "pki_cert" + ParamCert tlsUriParam = "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) + ParamCipher 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. @@ -74,16 +84,82 @@ const ( It may be specified multiple times in a TLS URI. */ - TlsUriParamCipher string = "cipher" + ParamCipher tlsUriParam = "cipher" /* - TlsUriParamCurve specifies one (or more) curve(s) + ParamCurve 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" + ParamCurve tlsUriParam = "curve" /* - TlsUriParamMinTls defines the minimum version of the + ParamIgnoreMissing, if `1`, `yes`, `y`, or `true` indicates + that missing cert/ca/key files should not return an error if they do not exist. + + Only the first defined instance is parsed. + */ + ParamIgnoreMissing tlsUriParam = "ignore_missing" + /* + ParamKey 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, and ECDSA. + (Technically ECDH keys are supported, but cannot be paired with certificates + so trying to use them may result in errors or undefined behavior. + Future versions may support this as a parameter for *kex* in a TLS + *connection*, but this is unplanned at the moment.) + + It may be specified multiple times in a TLS URI. + */ + ParamKey tlsUriParam = "pki_key" + /* + ParamKeylog is a way to specify the SSLKEYLOGFILE. + + This parameter's value can be: + + * a filepath; parent directories will attempt to be created if + they do not exist, and the file will be truncated if it exists. + The consumer/downstream is responsible for calling .Close() + on it when done. + * the special string as defined by KeyLogEnvVal to use the filepath + of whatever is set in the environment variable as defined by + KeyLogEnv (which is likely the default variable name). + It is assumed to be a filepath. + The consumer/downstream is responsible for calling .Close() + on it when done. + * the special string as defined by KeyLogBufVal to use an in-memory + buffer instead + + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + !! DO NOT, UNDER ANY CIRCUMSTANCES, ENABLE THIS UNLESS YOU ARE !! + !! ABSOLUTELY SURE WHAT YOU ARE DOING. !! + !! IT SEVERELY COMPROMISES SECURITY !! + !! AND IS ONLY INTENDED FOR DEBUGGING PURPOSES! !! + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + + See https://www.ietf.org/archive/id/draft-thomson-tls-keylogfile-00.html + for details. + + The exact type of the returned tls.Config.KeyLogWriter will be: + + * an *os.File if a filepath or the KeyLogEnvVal value was specified + * a *bytes.Buffer if the KeyLogBufVal value was specified + + Only the first defined instance is parsed. + */ + ParamKeylog tlsUriParam = "debug_keylog" + /* + ParamMaxTls defines the minimum version of the + TLS protocol to use. + + See ParamMinTls for syntax of the value. + + Only the first defined instance is parsed. + */ + ParamMaxTls tlsUriParam = "max_tls" + /* + ParamMinTls defines the minimum version of the TLS protocol to use. It is recommended to use "TLS_1.3". @@ -101,18 +177,36 @@ const ( Only the first defined instance is parsed. */ - TlsUriParamMinTls string = "min_tls" + ParamMinTls tlsUriParam = "min_tls" /* - TlsUriParamMaxTls defines the minimum version of the - TLS protocol to use. + ParamMtlsCa specifies a path to a CA certificate PEM-encoded DER file. - See TlsUriParamMinTls for syntax of the value. + Unlike ParamCa, this is explicitly used to validate clients + (see ParamMtlsMode). - Only the first defined instance is parsed. + If not specified (and ParamMtlsMode is anything *but* `NoClientCert`/`0`), + the same evaluated roots/trust anchors used for ParamCa will be used. + + It may be specified multiple times in a TLS URI. */ - TlsUriParamMaxTls string = "max_tls" + ParamMtlsCa tlsUriParam = "mtls_ca" /* - TlsUriParamNet is used by TlsUri.ToConn and TlsUri.ToTlsConn to explicitly specify a network. + ParamMtlsMode specifies if TLS client certificate auth should be used or not, + and what mode/type of requirement. + This is only useful if running a server/listener with mTLS auth. + Clients should leave it empty. + Servers/listeners should leave it empty if they do not wish to use + mTLS auth for clients. + + The string may either be the name (as per + https://pkg.go.dev/crypto/tls#ClientAuthType) + or an int (normal, hex, etc. string representation) of the constant's value. + + See also ParamMtlsCa. + */ + ParamMtlsMode tlsUriParam = "mtls_auth" + /* + ParamNet is used by TlsUri.ToConn and TlsUri.ToTlsConn to explicitly specify a network. The default is "tcp". @@ -120,15 +214,33 @@ const ( Only the first defined instance is parsed. */ - TlsUriParamNet string = "net" + ParamNet tlsUriParam = "net" + /* + ParamNoVerify, if `1`, `yes`, `y`, or `true` indicates + 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. + */ + ParamNoVerify tlsUriParam = "no_verify" + /* + ParamSni 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. + */ + ParamSni tlsUriParam = "sni" ) var ( paramBoolValsTrue []string = []string{ - "1", "yes", "y", "true", + "true", "yes", "y", "1", } paramBoolValsFalse []string = []string{ - "0", "no", "n", "false", + "false", "no", "n", "0", } validate *validator.Validate = validator.New(validator.WithRequiredStructEnabled()) ) diff --git a/consts_param_map.go b/consts_param_map.go new file mode 100644 index 0000000..59f1e7f --- /dev/null +++ b/consts_param_map.go @@ -0,0 +1,45 @@ +package cryptparse + +/* + THIS FILE IS AUTOMATICALLY GENERATED. + DO NOT EDIT. + SEE internal/constmap/ FOR DETAILS. +*/ + +var ( + // tlsUriParamStrMap contains a map of the constant string *name* of a tlsUriParam as mapped to its *value* (at time of generation). + tlsUriParamStrMap map[string]string = map[string]string{ + "ParamCa": "pki_ca", + "ParamCert": "pki_cert", + "ParamCipher": "cipher", + "ParamCurve": "curve", + "ParamIgnoreMissing": "ignore_missing", + "ParamKey": "pki_key", + "ParamKeylog": "debug_keylog", + "ParamMaxTls": "max_tls", + "ParamMinTls": "min_tls", + "ParamMtlsCa": "mtls_ca", + "ParamMtlsMode": "mtls_auth", + "ParamNet": "net", + "ParamNoVerify": "no_verify", + "ParamSni": "sni", + } + + // tlsUriStrParamMap contains a map of the *value* (at time of generation) of tlsUriParam constants to the constant string *name*. + tlsUriStrParamMap map[string]string = map[string]string{ + "pki_ca": "ParamCa", + "pki_cert": "ParamCert", + "cipher": "ParamCipher", + "curve": "ParamCurve", + "ignore_missing": "ParamIgnoreMissing", + "pki_key": "ParamKey", + "debug_keylog": "ParamKeylog", + "max_tls": "ParamMaxTls", + "min_tls": "ParamMinTls", + "mtls_ca": "ParamMtlsCa", + "mtls_auth": "ParamMtlsMode", + "net": "ParamNet", + "no_verify": "ParamNoVerify", + "sni": "ParamSni", + } +) diff --git a/errs.go b/errs.go index 1cdf056..eaf87fe 100644 --- a/errs.go +++ b/errs.go @@ -5,9 +5,13 @@ import ( ) var ( + ErrBadMtlsMode error = errors.New("invalid TLS client cert auth/mTLS mode/type") + ErrBadPortRange error = errors.New("invalid port; must be between 0 and 65535") ErrBadTlsCipher error = errors.New("invalid TLS cipher suite") ErrBadTlsCurve error = errors.New("invalid TLS curve") ErrBadTlsVer error = errors.New("invalid TLS version") + ErrReqParams error = errors.New("a nil/uninitialized requestUriParams was passed") + ErrReqStruct error = errors.New("invalid type; a pointer to a struct is required") ErrUnknownCipher error = errors.New("unknown TLS cipher") ErrUnknownKey error = errors.New("unknown key type") ) diff --git a/funcs.go b/funcs.go index 5677991..dcbd4ff 100644 --- a/funcs.go +++ b/funcs.go @@ -11,11 +11,14 @@ import ( `crypto/x509` `encoding/pem` `errors` + `io` `net/url` `os` + `path/filepath` `strconv` `strings` + `r00t2.io/sysutils/envs` `r00t2.io/sysutils/paths` ) @@ -85,6 +88,60 @@ func IsMatchedPair(privKey crypto.PrivateKey, cert *x509.Certificate) (isMatched return } +/* + ParseMtlsMode parses string s and attempts to derive a TLS client certificate + auth mode from it. + + The string may either be the name (as per https://pkg.go.dev/crypto/tls#ClientAuthType) + or an int (normal, hex, etc. string representation). +*/ +func ParseMtlsMode(s string) (mode tls.ClientAuthType, err error) { + + var nm string + var n int64 + var m tls.ClientAuthType + + if n, err = strconv.ParseInt(s, 10, 64); err != nil { + if errors.Is(err, strconv.ErrSyntax) { + // It's a name; parse below. + err = nil + } else { + return + } + } else { + // It's a number. + m = tls.ClientAuthType(n) + if !strings.HasPrefix(m.String(), "ClientAuthType(") { + // It's valid; send it. + mode = m + return + } else { + // It's invalid. + err = ErrBadMtlsMode + return + } + } + + // It's a name. First normalize the string so we don't need to do so many transforms. + nm = strings.ToLower(strings.TrimSpace(s)) + // Then keep going until we either find it or we run out of valid auth types. + for i := 0; ; i++ { + m = tls.ClientAuthType(i) + if strings.ToLower(m.String()) == nm { + // Found; send it. + mode = m + break + } + if strings.HasPrefix(m.String(), "ClientAuthType(") { + // We've reached the end of valid auth names and it still wasn't found. + err = ErrBadMtlsMode + return + } + } + + 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. @@ -403,34 +460,42 @@ func ParseTlsUri(tlsUri *url.URL) (tlsConf *tls.Config, err error) { var b []byte var rootCAs *x509.CertPool + var mtlsCAs *x509.CertPool var intermediateCAs []*x509.Certificate + var concatCAs []*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 ignoreMissing bool + var keylog io.Writer + var clientAuthType tls.ClientAuthType + var params map[tlsUriParam][]string = make(map[tlsUriParam][]string) var buf *bytes.Buffer = new(bytes.Buffer) var srvNm string = tlsUri.Hostname() - params = tlsUri.Query() - - if params == nil { + if tlsUri.Query() == nil || len(tlsUri.Query()) == 0 { tlsConf = &tls.Config{ ServerName: srvNm, } return } + for k, v := range tlsUri.Query() { + params[tlsUriParam(k)] = v + } + // These are all filepath(s). - for _, k := range []string{ - TlsUriParamCa, - TlsUriParamCert, - TlsUriParamKey, + for _, k := range []tlsUriParam{ + ParamCa, + ParamCert, + ParamKey, + ParamMtlsCa, } { if _, ok = params[k]; ok { for idx, _ := range params[k] { @@ -441,15 +506,59 @@ func ParseTlsUri(tlsUri *url.URL) (tlsConf *tls.Config, err error) { } } + if _, ok = params[ParamIgnoreMissing]; ok { + val = strings.ToLower(params[ParamIgnoreMissing][0]) + for _, i := range paramBoolValsTrue { + if i == val { + ignoreMissing = true + break + } + } + } + + // This *might* be a filepath. + if _, ok = params[ParamKeylog]; ok { + switch params[ParamKeylog][0] { + case KeyLogBufVal: + keylog = new(bytes.Buffer) + case KeyLogEnvVal: + val = params[ParamKeylog][0] + if envs.HasEnv(val) { + val = os.Getenv(val) + if err = paths.RealPath(&val); err != nil { + return + } + if err = os.MkdirAll(filepath.Dir(val), 0700); err != nil { + return + } + if keylog, err = os.OpenFile(val, os.O_CREATE|os.O_TRUNC|os.O_WRONLY, 0o0600); err != nil { + return + } + } + default: + if err = paths.RealPath(¶ms[ParamKeylog][0]); err != nil { + return + } + if err = os.MkdirAll(filepath.Dir(params[ParamKeylog][0]), 0700); err != nil { + return + } + if keylog, err = os.OpenFile(params[ParamKeylog][0], os.O_CREATE|os.O_TRUNC|os.O_WRONLY, 0o0600); err != nil { + return + } + } + } + // CA cert(s). buf.Reset() - if _, ok = params[TlsUriParamCa]; ok { + if _, ok = params[ParamCa]; ok { rootCAs = x509.NewCertPool() - for _, c := range params[TlsUriParamCa] { + for _, c := range params[ParamCa] { if b, err = os.ReadFile(c); err != nil { - if errors.Is(err, os.ErrNotExist) { + if errors.Is(err, os.ErrNotExist) && ignoreMissing { err = nil continue + } else { + return } } buf.Write(b) @@ -463,12 +572,12 @@ func ParseTlsUri(tlsUri *url.URL) (tlsConf *tls.Config, err error) { } } - // Keys. These are done first so we can match to a client certificate. + // Keys. These are done first so we can match to a leaf certificate. buf.Reset() - if _, ok = params[TlsUriParamKey]; ok { - for _, k := range params[TlsUriParamKey] { + if _, ok = params[ParamKey]; ok { + for _, k := range params[ParamKey] { if b, err = os.ReadFile(k); err != nil { - if errors.Is(err, os.ErrNotExist) { + if errors.Is(err, os.ErrNotExist) && ignoreMissing { err = nil continue } else { @@ -482,12 +591,12 @@ func ParseTlsUri(tlsUri *url.URL) (tlsConf *tls.Config, err error) { } } - // (Client) Certificate(s). + // (Leaf) Certificate(s). buf.Reset() - if _, ok = params[TlsUriParamCert]; ok { - for _, c := range params[TlsUriParamCert] { + if _, ok = params[ParamCert]; ok { + for _, c := range params[ParamCert] { if b, err = os.ReadFile(c); err != nil { - if errors.Is(err, os.ErrNotExist) { + if errors.Is(err, os.ErrNotExist) && ignoreMissing { err = nil continue } else { @@ -496,19 +605,24 @@ func ParseTlsUri(tlsUri *url.URL) (tlsConf *tls.Config, err error) { } buf.Write(b) } - if tlsCerts, err = ParseLeafCert(buf.Bytes(), privKeys, intermediateCAs...); err != nil { + if tlsCerts, concatCAs, err = ParseLeafCert(buf.Bytes(), privKeys, intermediateCAs...); err != nil { return } + if concatCAs != nil { + for _, ca := range concatCAs { + rootCAs.AddCert(ca) + } + } } // Hostname (Override). - if _, ok = params[TlsUriParamSni]; ok { - srvNm = params[TlsUriParamSni][0] + if _, ok = params[ParamSni]; ok { + srvNm = params[ParamSni][0] } // Disable Verification. - if _, ok = params[TlsUriParamNoVerify]; ok { - val = strings.ToLower(params[TlsUriParamNoVerify][0]) + if _, ok = params[ParamNoVerify]; ok { + val = strings.ToLower(params[ParamNoVerify][0]) for _, i := range paramBoolValsTrue { if i == val { allowInvalid = true @@ -517,39 +631,73 @@ func ParseTlsUri(tlsUri *url.URL) (tlsConf *tls.Config, err error) { } } + // Client/mTLS cert auth mode. + if _, ok = params[ParamMtlsMode]; ok { + val = params[ParamMtlsMode][0] + if clientAuthType, err = ParseMtlsMode(val); err != nil { + return + } + } + // Client/mTLS roots. + buf.Reset() + if clientAuthType != tls.NoClientCert { + if _, ok = params[ParamMtlsCa]; ok { + mtlsCAs = x509.NewCertPool() + for _, c := range params[ParamMtlsCa] { + if b, err = os.ReadFile(c); err != nil { + if errors.Is(err, os.ErrNotExist) && ignoreMissing { + err = nil + continue + } else { + return + } + } + buf.Write(b) + } + if mtlsCAs, _, _, err = ParseCA(buf.Bytes()); err != nil { + return + } + } else { + mtlsCAs = rootCAs.Clone() + } + } + // Ciphers. - if _, ok = params[TlsUriParamCipher]; ok { - ciphers = ParseTlsCiphers(strings.Join(params[TlsUriParamCipher], ",")) + if _, ok = params[ParamCipher]; ok { + ciphers = ParseTlsCiphers(strings.Join(params[ParamCipher], ",")) } // Minimum TLS Protocol Version. - if _, ok = params[TlsUriParamMinTls]; ok { - if minVer, err = ParseTlsVersion(params[TlsUriParamMinTls][0]); err != nil { + if _, ok = params[ParamMinTls]; ok { + if minVer, err = ParseTlsVersion(params[ParamMinTls][0]); err != nil { return } } // Maximum TLS Protocol Version. - if _, ok = params[TlsUriParamMaxTls]; ok { - if maxVer, err = ParseTlsVersion(params[TlsUriParamMaxTls][0]); err != nil { + if _, ok = params[ParamMaxTls]; ok { + if maxVer, err = ParseTlsVersion(params[ParamMaxTls][0]); err != nil { return } } // Curves. - if _, ok = params[TlsUriParamCurve]; ok { - curves = ParseTlsCurves(strings.Join(params[TlsUriParamCurve], ",")) + if _, ok = params[ParamCurve]; ok { + curves = ParseTlsCurves(strings.Join(params[ParamCurve], ",")) } tlsConf = &tls.Config{ Certificates: tlsCerts, RootCAs: rootCAs, ServerName: srvNm, + ClientAuth: clientAuthType, + ClientCAs: mtlsCAs, InsecureSkipVerify: allowInvalid, CipherSuites: ciphers, MinVersion: minVer, MaxVersion: maxVer, CurvePreferences: curves, + KeyLogWriter: keylog, } return @@ -694,36 +842,36 @@ func ParseDhParams(dhRaw []byte) (params []*dhparam.DH, err error) { */ /* - ParseLeafCert parses PEM bytes from a (client) certificate file, iterates over a slice of + ParseLeafCert parses PEM bytes from a 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 + If no private key matches or no leaf 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 + Any leaf certificates ("server"/"client" 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 additional/supplementary intermediates may be provided. Any present in the PEM bytes (certRaw) will be included + in the tls.Certificate.Certificates. - Any *root* CAs found will be discarded. They should/can be extracted seperately via ParseCA. + Any *root* CAs found will be split out separately into caCerts and NOT included. + 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. + The parsed and paired certificates and keys can be found in each respective tls.Certificate[n].Leaf and tls.Certificate[n].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) { +func ParseLeafCert(certRaw []byte, keys []crypto.PrivateKey, intermediates ...*x509.Certificate) (tlsCerts []tls.Certificate, caCerts []*x509.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 { @@ -777,7 +925,7 @@ func ParseLeafCert(certRaw []byte, keys []crypto.PrivateKey, intermediates ...*x break } } - if foundKey == nil && !skipKeyPair { + if foundKey == nil { continue } tlsCerts = append( @@ -790,8 +938,6 @@ func ParseLeafCert(certRaw []byte, keys []crypto.PrivateKey, intermediates ...*x ) } - _ = caCerts - return } diff --git a/funcs_fieldmap_tlsuri.go b/funcs_fieldmap_tlsuri.go new file mode 100644 index 0000000..0ac2b54 --- /dev/null +++ b/funcs_fieldmap_tlsuri.go @@ -0,0 +1,173 @@ +package cryptparse + +import ( + `reflect` + `sync` + + `r00t2.io/goutils/multierr` + `r00t2.io/goutils/structutils` +) + +/* + FieldMapTlsURI returns TlsUri tu from struct, map, or slice s. + + If a map, the keys should be the Param* tlsUriParam contants' *values* + (e.g. map[string][]string{"ignore_missing": []string{"true"}}). + If a slice, it should be a slice of strings (or nested other supported type) + which follow the pattern `=` where `` is one of the + Param* tlsUriParam constants' *values*. + Obviously in both these cases, the host/port will not be set. + See the NOTE below. + + The default is to recurse into e.g. nested structs and sub-structs + unless they are tagged with the value "-". + + This function operates on the struct tag `tlsUri` (e.g. `tlsUri:"-"`, `tlsUri:"ca"`, etc.). + If you need to use an explicit/different struct tag name, use FieldMapTlsURIWithTag. + + See the struct tags on TlsFlat for an example of usage. + + NOTE: The host and port are not currently directly + configurable via struct tags. These can be set + via the SetHost, SetPort, SetPortStr, SetHostPort, + and/or SetHostPortStr on the returned TlsUri. +*/ +func FieldMapTlsURI[T any](s T) (tu *TlsUri, err error) { + + if tu, err = FieldMapTlsURIWithTag(s, dfltStructTag); err != nil { + return + } + + return +} + +// FieldMapTlsURIWithTag is exactly like FieldMapTlsURI but allows specifying an explict tag name. +func FieldMapTlsURIWithTag[T any](s T, tagName string,) (tu *TlsUri, err error) { + + var ptrVal reflect.Value + var sVal reflect.Value = reflect.ValueOf(s) + var sType reflect.Type = sVal.Type() + var params *tlsUriParams + + *params = make(map[tlsUriParam][]string) + + if sVal.IsNil() || sVal.IsZero() || !sVal.IsValid() { + return + } + ptrVal = sVal.Elem() + + if err = fieldMapTlsURIStruct(params, ptrVal, tagName); err != nil { + return + } + + // TODO + + return +} + +// fieldMapTlsURIStruct parses a reflect.Value and populates params with any matching fields found. +func fieldMapTlsURIStruct(params *tlsUriParams, v reflect.Value, tagName string) (err error) { + + var field reflect.StructField + var fieldVal reflect.Value + var wg sync.WaitGroup + var errChan chan error + var numJobs int + var doneChan chan bool = make(chan bool, 1) + var mErr *multierr.MultiError = multierr.NewMultiError(nil) + var t reflect.Type = v.Type() + var kind reflect.Kind = t.Kind() + + if params == nil { + err = ErrReqParams + return + } + + if kind != reflect.Struct { + err = ErrReqStruct + return + } + + numJobs = v.NumField() + wg.Add(numJobs) + errChan = make(chan error, numJobs) + + for i := 0; i < v.NumField(); i++ { + field = t.Field(i) + fieldVal = v.Field(i) + + go func(f reflect.StructField, fv reflect.Value) { + var fErr error + + defer wg.Done() + + if fErr = fieldMapTlsURIStructField(params, f, fv, tagName); fErr != nil { + errChan <- fErr + return + } + }(field, fieldVal) + } + + go func() { + wg.Wait() + close(errChan) + doneChan <- true + }() + + <-doneChan + + for i := 0; i < numJobs; i++ { + if err = <-errChan; err != nil { + mErr.AddError(err) + err = nil + } + } + + if !mErr.IsEmpty() { + err = mErr + return + } + + // TODO + + return +} + +// fieldMapTlsURIStructField parses a single struct field. +func fieldMapTlsURIStructField(params *tlsUriParams, field reflect.StructField, fv reflect.Value, tagName string) (err error) { + + var parsedTagOpts map[string]bool + + if params == nil { + err = ErrReqParams + return + } + + if !fv.CanSet() { + return + } + + // Skip if explicitly instructed to do so. + parsedTagOpts = structutils.TagToBoolMap(field, tagName, structutils.TagMapTrim) + if parsedTagOpts["-"] { + return + } + + if fv.Kind() == reflect.Ptr { + err = fieldMapTlsURIStructField(params, field, fv.Elem(), tagName) + } else { + err = fieldMapTlsUriValue(params, fv, tagName) + } + + // TODO + + return +} + +// fieldMapTlsUriValue is a type dispatcher for a reflected value. +func fieldMapTlsUriValue(params *tlsUriParams, v reflect.Value, tagName string) (err error) { + + // TODO + + return +} diff --git a/funcs_reflectflat.go b/funcs_reflectflat.go new file mode 100644 index 0000000..9255c3b --- /dev/null +++ b/funcs_reflectflat.go @@ -0,0 +1,65 @@ +package cryptparse + +import ( + `fmt` + `net/url` + `reflect` + + `r00t2.io/goutils/structutils` +) + +/* + reflectFlatUri is the reflection used for a TlsFlat when converting to a TlsUri. +*/ +func reflectFlatUri(s *TlsFlat, tagName string) (tu *TlsUri, err error) { + + var uri *url.URL + var field reflect.StructField + var fieldVal reflect.Value + var params url.Values = make(url.Values) + var sVal reflect.Value = reflect.ValueOf(s) + var sType reflect.Type = sVal.Type() + var kind reflect.Kind = sType.Kind() + var parsedTagOpts map[string]bool + + if s == nil { + return + } + + if uri, err = url.Parse(fmt.Sprintf("tls://%s", s.Host)); err != nil { + return + } + if s.Port != nil { + uri.Host = fmt.Sprintf("%s:%d", uri.Host, *s.Port) + } + + for i := 0; i < sType.NumField(); i++ { + field = sType.Field(i) + fieldVal = sVal.Field(i) + if !fieldVal.CanSet() { + continue + } + + parsedTagOpts = structutils.TagToBoolMap(field, "tlsUri") + if parsedTagOpts["-"] { + continue + } + + } + + // TODO + + if len(params) != 0 { + if uri.Path == "" { + uri.Path = "/" + } + if uri, err = url.Parse(fmt.Sprintf("%s%s", uri.String(), params.Encode())); err != nil { + return + } + } + tu = &TlsUri{ + URL: uri, + } + + return +} diff --git a/funcs_reflecturi.go b/funcs_reflecturi.go new file mode 100644 index 0000000..0c0dc6f --- /dev/null +++ b/funcs_reflecturi.go @@ -0,0 +1,60 @@ +package cryptparse + +import ( + `reflect` + `strconv` +) + +/* + reflectUriFlat is the reflection used for a TlsUri when converting to a TlsFlat. +*/ +func reflectUriFlat(s *TlsUri, tagName string) (f *TlsFlat, err error) { + + var ptrVal reflect.Value + var ptrType reflect.Type + var ptrKind reflect.Kind + var n uint64 + var flat TlsFlat + var sVal reflect.Value = reflect.ValueOf(s) + var sType reflect.Type = sVal.Type() + var kind reflect.Kind = sType.Kind() + var params map[tlsUriParam][]string = make(map[tlsUriParam][]string) + + if s == nil { + return + } + + for k, v := range params { + if tlsUriParam(k) != "" { + params[k] = v + } + } + + flat = TlsFlat{ + Host: s.Hostname(), + Port: nil, + SniName: nil, + SkipVerify: false, + Certs: nil, + CaFiles: nil, + CipherSuites: nil, + Curves: nil, + MinTlsProtocol: nil, + MaxTlsProtocol: nil, + } + + if s.Port() != "" { + f.Port = new(uint16) + if n, err = strconv.ParseUint(s.Port(), 10, 16); err != nil { + return + } + *f.Port = uint16(n) + } + + // TODO: lookup map, map[string]tlsUriParam where key is name of constant as string, and value is that corresponding value. + // Use that value to lookup in params. + + f = &flat + + return +} diff --git a/funcs_test.go b/funcs_test.go index b491a5a..dc1da3a 100644 --- a/funcs_test.go +++ b/funcs_test.go @@ -2,9 +2,117 @@ package cryptparse import ( `crypto/tls` + `embed` + `errors` + `fmt` + `io/fs` + `net/url` + `os` + `strings` "testing" ) +var ( + // Generated from ../_extra/gen_test_pki + + //go:embed "_testdata" + testPems embed.FS + testTmpPemFiles map[string]*os.File + testKt string = "ed25519" +) + +func testInit(t *testing.T) (err error) { + + var n string + var nkt string + var b []byte + var names []fs.DirEntry + var ok bool + + if testTmpPemFiles == nil { + testTmpPemFiles = make(map[string]*os.File) + } + + if names, err = testPems.ReadDir("_testdata"); err != nil { + return + } + // only ".keep" is present. + if len(names) == 1 { + t.Fatalf( + "There aren't any test PEMs."+ + "You must `go run *.go` in _extras/gen_test_pki and copy the %s PEMs into _testdata.", + testKt, + ) + } + + // populate tmpFiles from the embed.FS `pems` and write out to temp files. + for _, p := range []string{ + "ca", + "inter", + "leaf_server", + "leaf_user", + } { + for _, pt := range []string{ + "cert", + "csr", + "key", + } { + n = fmt.Sprintf("%s_%s", p, pt) + nkt = fmt.Sprintf("%s_%s_%s", p, testKt, pt) + if _, ok = testTmpPemFiles[n]; !ok { + if b, err = testPems.ReadFile(fmt.Sprintf("_testdata/%s.pem", nkt)); err != nil { + t.Fatalf("Read '%s' failed: %v", nkt, err) + } + if testTmpPemFiles[n], err = os.CreateTemp("", fmt.Sprintf(".*.%s.pem", n)); err != nil { + t.Fatalf("Create temp file for %s failed: %v", n, err) + } + if _, err = testTmpPemFiles[n].Write(b); err != nil { + t.Fatalf("Write to %s failed: %v", n, err) + } + if err = testTmpPemFiles[n].Close(); err != nil { + t.Fatalf("Closing %s failed: %v", n, err) + } + } + } + if strings.HasPrefix(p, "leaf_") { + n = fmt.Sprintf("%s_chained", p) + nkt = fmt.Sprintf("%s_%s_cert_chained.pem", p, testKt) + if _, ok = testTmpPemFiles[n]; !ok { + if b, err = testPems.ReadFile(fmt.Sprintf("_testdata/%s.pem", nkt)); err != nil { + t.Fatalf("Read '%s' failed: %v", nkt, err) + } + if testTmpPemFiles[n], err = os.CreateTemp("", fmt.Sprintf(".*.%s.pem", n)); err != nil { + t.Fatalf("Create temp file for %s failed: %v", n, err) + } + if _, err = testTmpPemFiles[n].Write(b); err != nil { + t.Fatalf("Write to %s failed: %v", n, err) + } + if err = testTmpPemFiles[n].Close(); err != nil { + t.Fatalf("Closing %s failed: %v", n, err) + } + } + } + } + + t.Cleanup(func() { + var cErr error + for k, f := range testTmpPemFiles { + if cErr = f.Close(); cErr != nil && !errors.Is(cErr, os.ErrClosed) { + t.Logf("Error when closing %s '%s': %v", k, f.Name(), cErr) + cErr = nil + } else if cErr != nil { + cErr = nil + } + if cErr = os.Remove(f.Name()); cErr != nil { + t.Logf("Error when removing %s '%s': %v", k, f.Name(), cErr) + cErr = nil + } + } + }) + + return +} + func TestCiphers(t *testing.T) { var err error @@ -35,3 +143,32 @@ func TestCiphers(t *testing.T) { _ = cs } + +func TestTlsUri(t *testing.T) { + + var err error + var uStr string + var u *url.URL + var tlsU *TlsUri + + if err = testInit(t); err != nil { + t.Fatal(err) + } + uStr = fmt.Sprintf( + "https://:9091/?"+ + "pki_ca=%s&"+ // testTmpFiles["ca_cert"] + "pki_cert=%s&"+ // testTmpFiles["leaf_server_chained"] + "pki_key=%s&"+ // testTmpFiles["leaf_server_key"] + "min_tls=1.2&max_tls=1.2&"+ + "sni=server.example.com", + testTmpPemFiles["ca_cert"], testTmpPemFiles["leaf_server_chained"], testTmpPemFiles["leaf_server_key"], + ) + if u, err = url.Parse(uStr); err != nil { + t.Fatalf("Failed to parse URL string '%s': %v", uStr, err) + } + + tlsU = &TlsUri{ + URL: u, + } + +} diff --git a/funcs_tlsflat.go b/funcs_tlsflat.go index b205e75..a285df7 100644 --- a/funcs_tlsflat.go +++ b/funcs_tlsflat.go @@ -57,6 +57,7 @@ func (t *TlsFlat) ToTlsConfig() (tlsConf *tls.Config, err error) { var curves []tls.CurveID var minVer uint16 var maxVer uint16 + var concatCAs []*x509.Certificate var buf *bytes.Buffer = new(bytes.Buffer) var srvNm string = t.SniName @@ -107,10 +108,15 @@ func (t *TlsFlat) ToTlsConfig() (tlsConf *tls.Config, err error) { if b, err = os.ReadFile(c.CertFile); err != nil { return } - if parsedTlsCerts, err = ParseLeafCert(b, privKeys, intermediateCAs...); err != nil { + if parsedTlsCerts, concatCAs, err = ParseLeafCert(b, privKeys, intermediateCAs...); err != nil { return } tlsCerts = append(tlsCerts, parsedTlsCerts...) + if concatCAs != nil { + for _, ca := range concatCAs { + rootCAs.AddCert(ca) + } + } } } @@ -163,49 +169,49 @@ func (t *TlsFlat) ToTlsUri() (tlsUri *TlsUri, err error) { // CA cert(s). if t.CaFiles != nil { for _, c := range t.CaFiles { - u.Query().Add(TlsUriParamCa, c) + u.Query().Add(ParamCa, c) } } // Keys and Certs. if t.Certs != nil { for _, c := range t.Certs { - u.Query().Add(TlsUriParamCert, c.CertFile) + u.Query().Add(ParamCert, c.CertFile) if c.KeyFile != nil { - u.Query().Add(TlsUriParamKey, *c.KeyFile) + u.Query().Add(ParamKey, *c.KeyFile) } } } // Enforce the SNI hostname. - u.Query().Add(TlsUriParamSni, t.SniName) + u.Query().Add(ParamSni, t.SniName) // Disable Verification. if t.SkipVerify { - u.Query().Add(TlsUriParamNoVerify, "1") + u.Query().Add(ParamNoVerify, "1") } // Ciphers. if t.CipherSuites != nil { for _, c := range t.CipherSuites { - u.Query().Add(TlsUriParamCipher, c) + u.Query().Add(ParamCipher, c) } } // Minimum TLS Protocol Version. if t.MinTlsProtocol != nil { - u.Query().Add(TlsUriParamMinTls, *t.MinTlsProtocol) + u.Query().Add(ParamMinTls, *t.MinTlsProtocol) } // Maximum TLS Protocol Version. if t.MaxTlsProtocol != nil { - u.Query().Add(TlsUriParamMaxTls, *t.MaxTlsProtocol) + u.Query().Add(ParamMaxTls, *t.MaxTlsProtocol) } // Curves. if t.Curves != nil { for _, c := range t.Curves { - u.Query().Add(TlsUriParamCurve, c) + u.Query().Add(ParamCurve, c) } } diff --git a/funcs_tlsflat.go.old b/funcs_tlsflat.go.old new file mode 100644 index 0000000..a285df7 --- /dev/null +++ b/funcs_tlsflat.go.old @@ -0,0 +1,223 @@ +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 concatCAs []*x509.Certificate + 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, concatCAs, err = ParseLeafCert(b, privKeys, intermediateCAs...); err != nil { + return + } + tlsCerts = append(tlsCerts, parsedTlsCerts...) + if concatCAs != nil { + for _, ca := range concatCAs { + rootCAs.AddCert(ca) + } + } + } + } + + // 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(ParamCa, c) + } + } + + // Keys and Certs. + if t.Certs != nil { + for _, c := range t.Certs { + u.Query().Add(ParamCert, c.CertFile) + if c.KeyFile != nil { + u.Query().Add(ParamKey, *c.KeyFile) + } + } + } + + // Enforce the SNI hostname. + u.Query().Add(ParamSni, t.SniName) + + // Disable Verification. + if t.SkipVerify { + u.Query().Add(ParamNoVerify, "1") + } + + // Ciphers. + if t.CipherSuites != nil { + for _, c := range t.CipherSuites { + u.Query().Add(ParamCipher, c) + } + } + + // Minimum TLS Protocol Version. + if t.MinTlsProtocol != nil { + u.Query().Add(ParamMinTls, *t.MinTlsProtocol) + } + + // Maximum TLS Protocol Version. + if t.MaxTlsProtocol != nil { + u.Query().Add(ParamMaxTls, *t.MaxTlsProtocol) + } + + // Curves. + if t.Curves != nil { + for _, c := range t.Curves { + u.Query().Add(ParamCurve, c) + } + } + + tlsUri = &TlsUri{ + URL: u, + } + + return +} diff --git a/funcs_tlsuri.go b/funcs_tlsuri.go index 1586603..ed61e24 100644 --- a/funcs_tlsuri.go +++ b/funcs_tlsuri.go @@ -3,12 +3,108 @@ package cryptparse import ( `crypto` `crypto/tls` + `fmt` `net` `net/url` `os` + `strconv` `strings` ) +/* + SetHost allows one to explicitly set the host component of the URI. + A host can be removed from a TlsUri by invoking this method with an empty hostAddr string. + + No validation is performed. +*/ +func (t *TlsUri) SetHost(hostAddr string) { + + if t == nil { + return + } + + if t.Port() == "" { + t.Host = hostAddr + } else { + t.Host = fmt.Sprintf("%s:%s", hostAddr, t.Port()) + } + + return +} + +/* + SetHostPort is a small wrapper around the SetHost and SetPort methods, combining them into one. + + Refer to the comments for each on usage. +*/ +func (t *TlsUri) SetHostPort(host string, port *uint16) { + + t.SetPort(port) + t.SetHost(host) + + return +} + +/* + SetHostPortStr is a small wrapper around the SetHost and SetPortStr methods, combining them into one. + + Refer to the comments for each on usage. +*/ +func (t *TlsUri) SetHostPortStr(host string, port string) (err error) { + + if err = t.SetPortStr(port); err != nil { + return + } + t.SetHost(host) + + return +} + +/* + SetPort allows one to explicitly set the port component of the URI. + A port can be removed from a TlsUri by invoking this method with a nil port. +*/ +func (t *TlsUri) SetPort(port *uint16) { + + if t == nil { + return + } + + if port == nil { + t.Host = t.Hostname() + } else { + t.Host = fmt.Sprintf("%s:%d", t.Hostname(), *port) + } + + return +} + +/* + SetPortStr allows one to specify the port number as a string instead of a uint16 ptr. + If port is an empty string, any existing defined port will be removed from t. +*/ +func (t *TlsUri) SetPortStr(port string) (err error) { + + var n uint64 + var u uint16 + + if port == "" { + t.Host = t.Hostname() + } else { + if n, err = strconv.ParseUint(port, 10, 16); err == nil { + return + } + if n > 65535 { + err = ErrBadPortRange + return + } + u = uint16(n) + t.Host = fmt.Sprintf("%s:%d", t.Hostname(), u) + } + + return +} + /* WithConn returns a (crypto/)tls.Conn from an existing/already dialed net.Conn. @@ -45,8 +141,8 @@ func (t *TlsUri) ToConn() (conn net.Conn, err error) { params = t.Query() if params != nil { - if _, ok = params[TlsUriParamNet]; ok { - netType = params[TlsUriParamNet][0] + if _, ok = params[ParamNet]; ok { + netType = params[ParamNet][0] } } netType = strings.ToLower(netType) @@ -99,8 +195,8 @@ func (t *TlsUri) ToTlsConn() (conn *tls.Conn, err error) { params = t.Query() if params != nil { - if _, ok = params[TlsUriParamNet]; ok { - netType = params[TlsUriParamNet][0] + if _, ok = params[ParamNet]; ok { + netType = params[ParamNet][0] } } netType = strings.ToLower(netType) @@ -124,7 +220,6 @@ 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 @@ -133,6 +228,8 @@ func (t *TlsUri) ToTlsFlat() (tlsFlat *TlsFlat, err error) { var isMatch bool var fCert *TlsFlatCert var val string + var ok bool + var paramMap map[tlsUriParam][]string = make(map[tlsUriParam][]string) var f TlsFlat = TlsFlat{ SniName: t.Hostname(), SkipVerify: false, @@ -145,22 +242,24 @@ func (t *TlsUri) ToTlsFlat() (tlsFlat *TlsFlat, err error) { } 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]...) + for k, v := range params { + paramMap[tlsUriParam(k)] = v } - // Keys and Certs. These are done first so we can match to a client certificate. - if t.Query().Has(TlsUriParamKey) { + // CA cert(s). + if _, ok = paramMap[ParamCa]; ok { + f.CaFiles = append(f.CaFiles, paramMap[ParamCa]...) + } + + // Keys and Certs. These are done first so we can match to a leaf certificate. + if _, ok = paramMap[ParamKey]; ok { privKeyMap = make(map[string][]crypto.PrivateKey) - for _, kFile := range paramMap[TlsUriParamKey] { + for _, kFile := range paramMap[ParamKey] { if b, err = os.ReadFile(kFile); err != nil { return } @@ -170,13 +269,13 @@ func (t *TlsUri) ToTlsFlat() (tlsFlat *TlsFlat, err error) { privKeys = append(privKeys, privKeyMap[kFile]...) } } - if t.Query().Has(TlsUriParamCert) { + if t_, ok = paramMap[ParamCert]; ok { tlsCertMap = make(map[string][]tls.Certificate) - for _, cFile := range paramMap[TlsUriParamCert] { + for _, cFile := range paramMap[ParamCert] { if b, err = os.ReadFile(cFile); err != nil { return } - if tlsCertMap[cFile], err = ParseLeafCert(b, privKeys); err != nil { + if tlsCertMap[cFile], _, err = ParseLeafCert(b, privKeys); err != nil { return } tlsCerts = append(tlsCerts, tlsCertMap[cFile]...) @@ -201,13 +300,13 @@ func (t *TlsUri) ToTlsFlat() (tlsFlat *TlsFlat, err error) { } // Hostname. - if t.Query().Has(TlsUriParamSni) { - f.SniName = t.Query().Get(TlsUriParamSni) + if t.Query().Has(ParamSni) { + f.SniName = t.Query().Get(ParamSni) } // Disable verification. - if t.Query().Has(TlsUriParamNoVerify) { - val = strings.ToLower(t.Query().Get(TlsUriParamNoVerify)) + if t.Query().Has(ParamNoVerify) { + val = strings.ToLower(t.Query().Get(ParamNoVerify)) for _, i := range paramBoolValsTrue { if val == i { f.SkipVerify = true @@ -217,25 +316,25 @@ func (t *TlsUri) ToTlsFlat() (tlsFlat *TlsFlat, err error) { } // Ciphers. - if t.Query().Has(TlsUriParamCipher) { - f.CipherSuites = params[TlsUriParamCipher] + if t.Query().Has(ParamCipher) { + f.CipherSuites = params[ParamCipher] } // Minimum TLS Protocol Version. - if t.Query().Has(TlsUriParamMinTls) { + if t.Query().Has(ParamMinTls) { f.MinTlsProtocol = new(string) - *f.MinTlsProtocol = t.Query().Get(TlsUriParamMinTls) + *f.MinTlsProtocol = t.Query().Get(ParamMinTls) } // Maximum TLS Protocol Version. - if t.Query().Has(TlsUriParamMaxTls) { + if t.Query().Has(ParamMaxTls) { f.MaxTlsProtocol = new(string) - *f.MaxTlsProtocol = t.Query().Get(TlsUriParamMaxTls) + *f.MaxTlsProtocol = t.Query().Get(ParamMaxTls) } // Curves. - if t.Query().Has(TlsUriParamCurve) { - f.Curves = params[TlsUriParamCurve] + if t.Query().Has(ParamCurve) { + f.Curves = params[ParamCurve] } tlsFlat = &f diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..6734b5a --- /dev/null +++ b/go.mod @@ -0,0 +1,24 @@ +module r00t2.io/cryptparse + +go 1.23.2 + +require ( + github.com/Luzifer/go-dhparam v1.3.0 + github.com/davecgh/go-spew v1.1.1 + github.com/go-playground/validator/v10 v10.22.1 + github.com/oriser/regroup v0.0.0-20240925165441-f6bb0e08289e + r00t2.io/sysutils v1.7.1 +) + +require ( + github.com/gabriel-vasile/mimetype v1.4.6 // 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.28.0 // indirect + golang.org/x/exp v0.0.0-20230425010034-47ecfdc1ba53 // indirect + golang.org/x/net v0.30.0 // indirect + golang.org/x/sys v0.26.0 // indirect + golang.org/x/text v0.19.0 // indirect + r00t2.io/goutils v1.6.0 // indirect +) diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..7924296 --- /dev/null +++ b/go.sum @@ -0,0 +1,42 @@ +github.com/Luzifer/go-dhparam v1.3.0 h1:GyB+YSU2jpUbCR9SjvT8W575BPeLzd2Tt2/3BirUFKM= +github.com/Luzifer/go-dhparam v1.3.0/go.mod h1:zOdP6tT8XK6Gndh6p0GrvOpUaFawGLSzT6+n1CHY9Hk= +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/gabriel-vasile/mimetype v1.4.6 h1:3+PzJTKLkvgjeTbts6msPJt4DixhT4YtFNf1gtGe3zc= +github.com/gabriel-vasile/mimetype v1.4.6/go.mod h1:JX1qVKqZd40hUPpAfiNTe0Sne7hdfKSbOqqmkq8GCXc= +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.1 h1:40JcKH+bBNGFczGuoBYgX4I6m/i27HYW8P9FDk5PbgA= +github.com/go-playground/validator/v10 v10.22.1/go.mod h1:dbuPbCMFw/DrkbEynArYaCwl3amGuJotoKCe95atGMM= +github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +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/oriser/regroup v0.0.0-20240925165441-f6bb0e08289e h1:cL0lMYYEbfEUBghQd4ytnl8B8Ktdm+JremTyAagegZ0= +github.com/oriser/regroup v0.0.0-20240925165441-f6bb0e08289e/go.mod h1:tUOeYZJlwO7jSmM5ko1jTCiQaWQMvh58IENEfjwYzh8= +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.28.0 h1:GBDwsMXVQi34v5CCYUm2jkJvu4cbtru2U4TN2PSyQnw= +golang.org/x/crypto v0.28.0/go.mod h1:rmgy+3RHxRZMyY0jjAJShp2zgEdOqj2AO7U0pYmeQ7U= +golang.org/x/exp v0.0.0-20230425010034-47ecfdc1ba53 h1:5llv2sWeaMSnA3w2kS57ouQQ4pudlXrR0dCgw51QK9o= +golang.org/x/exp v0.0.0-20230425010034-47ecfdc1ba53/go.mod h1:V1LtkGg67GoY2N1AnLN78QLrzxkLyJw7RJb1gzOOz9w= +golang.org/x/net v0.30.0 h1:AcW1SDZMkb8IpzCdQUaIq2sP4sZ4zw+55h6ynffypl4= +golang.org/x/net v0.30.0/go.mod h1:2wGyMJ5iFasEhkwi13ChkO/t1ECNC4X4eBKkVFyYFlU= +golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.26.0 h1:KHjCJyddX0LoSTb3J+vWpupP9p0oznkqVk/IfjymZbo= +golang.org/x/sys v0.26.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/text v0.19.0 h1:kTxAhCbGbxhK0IwgSKiMO5awPoDQ0RpfiVYBfK860YM= +golang.org/x/text v0.19.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +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= +r00t2.io/sysutils v1.7.1 h1:OXvzcpGC+WCHusDKCZqsDqUYr73qBbWqt6mM/qY/OHs= +r00t2.io/sysutils v1.7.1/go.mod h1:Sk/7riJp9fteeW9STkdQ/k22huL1J6r05n6wLh5byHY= diff --git a/internal/constmap/consts.go b/internal/constmap/consts.go new file mode 100644 index 0000000..d73b78b --- /dev/null +++ b/internal/constmap/consts.go @@ -0,0 +1,32 @@ +package main + +import ( + _ `embed` + `text/template` + + `github.com/oriser/regroup` +) + +const ( + pfx string = "Param" + matchType string = "tlsUriParam" +) + +var ( + //go:embed "tpl/consts_param_map.go.tpl" + constmapTplBytes []byte + tpl = template.Must(template.New("consts").Parse(string(constmapTplBytes))) +) + +var ( + // If we restructure, these paths will need to be changed. + // constsPath string = filepath.Join("..", "..", "consts.go") + // outPath string = filepath.Join("..", "..", "consts_param_map.go") + constsPath string = "consts.go" + outPath string = "consts_param_map.go" +) + +var ( + // The most complex part about this pattern is it has to quote the backticks as their own string addition. + stripQuotesPtrn *regroup.ReGroup = regroup.MustCompile(`^(` + "`" + `(?P.+)` + "`" + `|"(?P.+)")$`) +) diff --git a/internal/constmap/funcs.go b/internal/constmap/funcs.go new file mode 100644 index 0000000..6f0cf89 --- /dev/null +++ b/internal/constmap/funcs.go @@ -0,0 +1,94 @@ +package main + +import ( + `errors` + `go/ast` + `go/token` + `log` + `strings` + + `github.com/oriser/regroup` +) + +/* + getParamSpec takes an ast.Decl d and returns a slice of + ParamConst found in it. + + If no ParamConst are found, foundParams will be nil. +*/ +func getValueSpec(d ast.Decl) (foundParams []*ParamConst) { + + var ok bool + var idx int + var gd *ast.GenDecl + var spec ast.Spec + var vs *ast.ValueSpec + var vsId *ast.Ident + var nm *ast.Ident + var bl *ast.BasicLit + + if gd, ok = d.(*ast.GenDecl); !ok || gd.Tok != token.CONST { + return + } + + for _, spec = range gd.Specs { + if vs, ok = spec.(*ast.ValueSpec); !ok { + continue + } + if vs.Type != nil { + if vsId, ok = vs.Type.(*ast.Ident); !ok || vsId.Name != matchType { + continue + } + } + for idx, nm = range vs.Names { + if !strings.HasPrefix(nm.Name, pfx) { + continue + } + if bl, ok = vs.Values[idx].(*ast.BasicLit); !ok { + continue + } + + foundParams = append( + foundParams, + &ParamConst{ + ConstName: nm.Name, + // UriParamName: bl.Value, + UriParamName: stripQuotes(bl.Value), + }, + ) + } + } + + return +} + +/* + stripQuotes removes Golang-AST defining quotes. + This probably doesn't work for multiline, but should be fine for our purposes. +*/ +func stripQuotes(inStr string) (outStr string) { + + var err error + var matches map[string]string + var nomchErr *regroup.NoMatchFoundError = new(regroup.NoMatchFoundError) + + outStr = inStr + + if matches, err = stripQuotesPtrn.Groups(inStr); err != nil { + if errors.As(err, &nomchErr) { + err = nil + return + } else { + log.Panicln(err) + } + } + + for _, v := range matches { + if v != "" { + outStr = v + return + } + } + + return +} diff --git a/internal/constmap/main.go b/internal/constmap/main.go new file mode 100644 index 0000000..0653742 --- /dev/null +++ b/internal/constmap/main.go @@ -0,0 +1,57 @@ +package main + +import ( + `bytes` + `fmt` + `go/ast` + `go/parser` + `go/token` + `log` + `os` + + `r00t2.io/sysutils/paths` +) + +/* + DO NOT RUN THIS ANYWHERE BUT FROM WHERE /consts.go IS LOCATED. +*/ + +// I *cannot believe* a library does not exist that will do this for me. + +func main() { + var err error + var tfs *token.FileSet + var af *ast.File + var foundParams []*ParamConst + var paramConsts []*ParamConst + var buf *bytes.Buffer = new(bytes.Buffer) + + if err = paths.RealPath(&constsPath); err != nil { + return + } + if err = paths.RealPath(&outPath); err != nil { + return + } + + tfs = token.NewFileSet() + + if af, err = parser.ParseFile(tfs, constsPath, nil, parser.AllErrors|parser.ParseComments); err != nil { + log.Panicln(err) + } + + for _, d := range af.Decls { + if foundParams = getValueSpec(d); foundParams == nil { + continue + } + paramConsts = append(paramConsts, foundParams...) + } + + if err = tpl.Execute(buf, paramConsts); err != nil { + log.Panicln(err) + } + + if err = os.WriteFile(outPath, buf.Bytes(), 0644); err != nil { + log.Panicln(err) + } + fmt.Printf("++ Generated %s ++\n", outPath) +} diff --git a/internal/constmap/tpl/consts_param_map.go.tpl b/internal/constmap/tpl/consts_param_map.go.tpl new file mode 100644 index 0000000..a889b1d --- /dev/null +++ b/internal/constmap/tpl/consts_param_map.go.tpl @@ -0,0 +1,24 @@ +{{- /*gotype: r00t2.io/cryptparse/internal/constmap.ParamConsts*/ -}} +package cryptparse + +/* + THIS FILE IS AUTOMATICALLY GENERATED. + DO NOT EDIT. + SEE internal/constmap/ FOR DETAILS. +*/ + +var ( + // tlsUriParamStrMap contains a map of the constant string *name* of a tlsUriParam as mapped to its *value* (at time of generation). + tlsUriParamStrMap map[string]string = map[string]string{ + {{- range $p := . }} + {{ printf "%#v" $p.ConstName }}: {{ printf "%#v" $p.UriParamName }}, + {{- end }} + } + + // tlsUriStrParamMap contains a map of the *value* (at time of generation) of tlsUriParam constants to the constant string *name*. + tlsUriStrParamMap map[string]string = map[string]string{ + {{- range $p := . }} + {{ printf "%#v" $p.UriParamName }}: {{ printf "%#v" $p.ConstName }}, + {{- end }} + } +) diff --git a/internal/constmap/types.go b/internal/constmap/types.go new file mode 100644 index 0000000..c754096 --- /dev/null +++ b/internal/constmap/types.go @@ -0,0 +1,8 @@ +package main + +type ParamConsts []*ParamConst + +type ParamConst struct { + ConstName string + UriParamName string +} diff --git a/tlsuri_test.go b/tlsuri_test.go new file mode 100644 index 0000000..ae98532 --- /dev/null +++ b/tlsuri_test.go @@ -0,0 +1,4 @@ +package cryptparse + +// TODO; see _testdata +// need to cover intermediates diff --git a/types.go b/types.go index 1ebdfde..c4fafc9 100644 --- a/types.go +++ b/types.go @@ -11,40 +11,76 @@ import ( `github.com/Luzifer/go-dhparam` ) +// tlsUriParam is an unexported type used to define TlsUri parameter names (and thus tags). +type tlsUriParam string + +// tlsUriParams is a collection of tlsUriParam and their value(s). +type tlsUriParams map[tlsUriParam][]string + // 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.). +// TlsFlat provides an easy structure to marshal/unmarshal a tls.Config and/or a TlsUri 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. + // Host is the host name. It may or may not be the same as SniName, and may be an empty string. + Host string `json:"host,omitempty" toml:"Host,omitempty" yaml:"Host,omitempty" xml:"host,attr,omitempty" tlsUri:"-"` // No reflection is done as it's directly managed. + // Port is the port number, if specified. Only relevant for listeners/clients and TlsUri. + Port *uint16 `json:"port,omitempty" toml:"Port,omitempty" yaml:"Port,omitempty" xml:"port,attr,omitempty" tlsUri:"-"` // No reflection is done as it's directly managed. + // CaFiles contains filepaths to CA certificates/"trust anchors" in PEM format. They may be combined. See ParamCa. + CaFiles []string `json:"ca_files,omitempty" toml:"CaFiles,omitempty" yaml:"CA Files,omitempty" xml:"roots>ca,omitempty" tlsUri:"ParamCa" validate:"omitempty,dive,filepath"` + // Certs contains 0 or more TlsFlatCert certificate definitions. See ParamCert and ParamKey as well. + Certs []*TlsFlatCert `json:"certs,omitempty" toml:"Certs,omitempty" yaml:"Certificates,omitempty" xml:"certs>cert,omitempty" validate:"omitempty,dive"` + // CipherSuites represents desired ciphers/cipher suites for this TLS environment. See ParamCipher. + CipherSuites []string `json:"cipher_suites,omitempty" toml:"CipherSuites,omitempty" yaml:"Cipher Suites,omitempty" xml:"ciphers,omitempty" tlsUri:"ParamCipher" validate:"omitempty,dive"` + // Curves specifies desired cryptographic curves to be used. See ParamCurve. + Curves []string `json:"curves,omitempty" toml:"Curves,omitempty" yaml:"Curves,omitempty" xml:"curves>curve,omitempty" tlsUri:"ParamCurve" validate:"omitempty,dive"` + // IgnoreMissing, if true, specifies that missing files should be ignored instead of throwing an error. + IgnoreMissing bool `json:"ignore_missing,omitempty" toml:"IgnoreMissing,omitempty" yaml:"Ignore Missing,omitempty" xml:"ignoreMissing,attr,omitempty" tlsUri:"ParamIgnoreMissing"` + /* + Keylog specifies an SSLKEYLOGFILE. + + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + !! DO NOT, UNDER ANY CIRCUMSTANCES, ENABLE THIS UNLESS YOU ARE !! + !! ABSOLUTELY SURE WHAT YOU ARE DOING. !! + !! IT SEVERELY COMPROMISES SECURITY !! + !! AND IS ONLY INTENDED FOR DEBUGGING PURPOSES! !! + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + + See ParamKeylog for details and special values. + */ + Keylog *string `json:"keylog,omitempty" toml:"Keylog,omitempty" yaml:"Keylog,omitempty" xml:"keylog,attr,omitempty" validate:"omitempty,dive"` + // MaxTlsProtocol specifies the maximum TLS version. See ParamMaxTls. + MaxTlsProtocol *string `json:"max_tls_protocol,omitempty" xml:"maxTlsProtocol,attr,omitempty" yaml:"MaxTlsProtocol,omitempty" toml:"MaxTlsProtocol,omitempty" tlsUri:"ParamMaxTls"` + // MinTlsProtocol specifies the minimum TLS version. See ParamMinTls. + MinTlsProtocol *string `json:"min_tls_protocol,omitempty" xml:"minTlsProtocol,attr,omitempty" yaml:"MinTlsProtocol,omitempty" toml:"MinTlsProtocol,omitempty" tlsUri:"ParamMinTls"` + // MutualTlsCAs specify path(s) to CA certificates/"trust anchors" in PEM format. See ParamMtlsCa. + MutualTlsCAs []string `json:"mtls_ca,omitempty" toml:"mTLSRoots,omitempty" yaml:"MTLS CA Files,omitempty" xml:"mTlsRoots>ca,omitempty" tlsUri:"ParamMtlsCa"` + // MutualTls specifies mutual TLS and, if enabled, what type/mode/level of required validation. See ParamMtlsMode. + MutualTls *string `json:"mtls_auth" toml:"mTLS,omitempty" yaml:"mTLS Type,omitempty" xml:"mtlsAuth,attr,omitempty" tlsUri:"ParamMtlsMode"` + // NetMode is the "network type" as found in e.g. net.Dial. See ParamNet for details. + NetMode *string + // SkipVerify, if true, will bypass certificate verification. You generally should not enable this. See ParamNoVerify. 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"` + /* + SniName represents the expected Server Name Indicator's name. If not nil, Host will be used to connect/listen + and this name will be used for certificate validation/verification. + See ParamSni. + */ + SniName *string `json:"sni_name" toml:"SNIName" yaml:"SNI Name" xml:"sniName,attr" tlsUri:"ParamSni" required:"true" validate:"required"` } // 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"` + // KeyFile is a filepath to a PEM-encoded key file. See ParamKey. + KeyFile *string `json:"key,omitempty" xml:"key,attr,omitempty" yaml:"Key,omitempty" toml:"Key,omitempty" tlsUri:"ParamKey" validate:"omitempty,filepath"` + // CertFile is a filepath to a PEM-encoded certificate file. See ParamCert. + CertFile string `json:"cert" xml:",chardata" yaml:"Certificate" toml:"Certificate" required:"true" tlsUri:"ParamCert" validate:"required,filepath"` } // TlsPkiChain contains a whole X.509 PKI chain -- Root CA(s) (trust anchors) which sign Intermediate(s) which sign Certificate(s). +// TODO type TlsPkiChain struct { /* Roots are all trust anchors/root certificates.