From 236165bec84b06c97f89429509b32bf8b70b5135 Mon Sep 17 00:00:00 2001 From: brent saner Date: Thu, 17 Oct 2024 15:20:27 -0400 Subject: [PATCH] v1.8.0 API CHANGES: * Technically should be a v2.x, but cryptparse has been moved to its own module: r00t2.io/cryptparse --- cryptparse/TODO | 3 - cryptparse/consts.go | 134 ------ cryptparse/doc.go | 5 + cryptparse/errs.go | 13 - cryptparse/funcs.go | 826 ------------------------------------ cryptparse/funcs_test.go | 37 -- cryptparse/funcs_tlsflat.go | 217 ---------- cryptparse/funcs_tlsuri.go | 256 ----------- cryptparse/types.go | 30 -- go.mod | 5 +- go.sum | 2 + 11 files changed, 10 insertions(+), 1518 deletions(-) delete mode 100644 cryptparse/TODO delete mode 100644 cryptparse/consts.go create mode 100644 cryptparse/doc.go delete mode 100644 cryptparse/errs.go delete mode 100644 cryptparse/funcs.go delete mode 100644 cryptparse/funcs_test.go delete mode 100644 cryptparse/funcs_tlsflat.go delete mode 100644 cryptparse/funcs_tlsuri.go delete mode 100644 cryptparse/types.go diff --git a/cryptparse/TODO b/cryptparse/TODO deleted file mode 100644 index a02d066..0000000 --- a/cryptparse/TODO +++ /dev/null @@ -1,3 +0,0 @@ -- PKCS#12/PFX parsing/support - -- Move to struct tags and reflection, so it can not only be easier to maintain in the future but also be implemented in custom structs downstream. diff --git a/cryptparse/consts.go b/cryptparse/consts.go deleted file mode 100644 index 0b3577a..0000000 --- a/cryptparse/consts.go +++ /dev/null @@ -1,134 +0,0 @@ -package cryptparse - -import ( - `crypto/tls` - - `github.com/go-playground/validator/v10` -) - -var ( - tlsVerNmToUint map[string]uint16 - tlsCipherNmToUint map[string]uint16 - tlsCurveNmToCurve map[string]tls.CurveID -) - -const ( - MaxTlsCipher uint16 = tls.TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305_SHA256 - MaxCurveId tls.CurveID = tls.X25519 // 29 - MinTlsVer uint16 = tls.VersionSSL30 - MaxTlsVer uint16 = tls.VersionTLS13 - DefaultNetType string = "tcp" -) - -// TlsUriParam* specifiy URL query parameters to parse a tls:// URI, and are used by TlsUri methods. -const ( - /* - TlsUriParamCa specifies a path to a CA certificate PEM-encded DER file. - - It may be specified multiple times in a TLS URI. - */ - TlsUriParamCa string = "pki_ca" - /* - TlsUriParamCert specifies a path to a client certificate PEM-encded DER file. - - It may be specified multiple times in a TLS URI. - */ - TlsUriParamCert string = "pki_cert" - /* - TlsUriParamKey specifies a path to a private key as a PEM-encded file. - - It may be PKCS#1, PKCS#8, or PEM-encoded ASN.1 DER EC key. - - Supported private key types are RSA, ED25519, ECDSA, and ECDH. - - It may be specified multiple times in a TLS URI. - */ - TlsUriParamKey string = "pki_key" - /* - TlsUriParamNoVerify, if `1`, `yes`, `y`, or `true` indicate - that the TLS connection should not require verification of - the remote end (e.g. hostname matches, trusted chain, etc.). - - Any other value for this parameter will be parsed as "False" - (meaning the remote end's certificate SHOULD be verified). - - Only the first defined instance is parsed. - */ - TlsUriParamNoVerify string = "no_verify" - /* - TlsUriParamSni indicates that the TLS connection should expect this hostname - instead of the hostname specified in the URI itself. - - Only the first defined instance is parsed. - */ - TlsUriParamSni string = "sni" - /* - TlsUriParamCipher specifies one (or more) cipher(s) - to specify for the TLS connection cipher negotiation. - Note that TLS 1.3 has a fixed set of ciphers, and - this list may not be respected by the remote end. - - The string may either be the name (as per - https://www.iana.org/assignments/tls-parameters/tls-parameters.xml) - or an int (normal, hex, etc. string representation). - - It may be specified multiple times in a TLS URI. - */ - TlsUriParamCipher string = "cipher" - /* - TlsUriParamCurve specifies one (or more) curve(s) - to specify for the TLS connection cipher negotiation. - - It may be specified multiple times in a TLS URI. - */ - TlsUriParamCurve string = "curve" - /* - TlsUriParamMinTls defines the minimum version of the - TLS protocol to use. - It is recommended to use "TLS_1.3". - - Supported syntax formats include: - - * TLS_1.3 - * 1.3 - * v1.3 - * TLSv1.3 - * 0x0304 (legacy_version, see RFC8446 ยง 4.1.2) - * 774 (0x0304 in int form) - * 0o1404 (0x0304 in octal form) - - All evaluate to TLS 1.3 in this example. - - Only the first defined instance is parsed. - */ - TlsUriParamMinTls string = "min_tls" - /* - TlsUriParamMaxTls defines the minimum version of the - TLS protocol to use. - - See TlsUriParamMinTls for syntax of the value. - - Only the first defined instance is parsed. - */ - TlsUriParamMaxTls string = "max_tls" - /* - TlsUriParamNet is used by TlsUri.ToConn and TlsUri.ToTlsConn to explicitly specify a network. - - The default is "tcp". - - See net.Dial()'s "network" parameter for valid network types. - - Only the first defined instance is parsed. - */ - TlsUriParamNet string = "net" -) - -var ( - paramBoolValsTrue []string = []string{ - "1", "yes", "y", "true", - } - paramBoolValsFalse []string = []string{ - "0", "no", "n", "false", - } - validate *validator.Validate = validator.New(validator.WithRequiredStructEnabled()) -) diff --git a/cryptparse/doc.go b/cryptparse/doc.go new file mode 100644 index 0000000..d06c8da --- /dev/null +++ b/cryptparse/doc.go @@ -0,0 +1,5 @@ +/* + CRYPTPARSE HAS MOVED. + + It is now its own module: r00t2.io/cryptparse +*/ diff --git a/cryptparse/errs.go b/cryptparse/errs.go deleted file mode 100644 index 1cdf056..0000000 --- a/cryptparse/errs.go +++ /dev/null @@ -1,13 +0,0 @@ -package cryptparse - -import ( - `errors` -) - -var ( - ErrBadTlsCipher error = errors.New("invalid TLS cipher suite") - ErrBadTlsCurve error = errors.New("invalid TLS curve") - ErrBadTlsVer error = errors.New("invalid TLS version") - ErrUnknownCipher error = errors.New("unknown TLS cipher") - ErrUnknownKey error = errors.New("unknown key type") -) diff --git a/cryptparse/funcs.go b/cryptparse/funcs.go deleted file mode 100644 index 61052b8..0000000 --- a/cryptparse/funcs.go +++ /dev/null @@ -1,826 +0,0 @@ -package cryptparse - -import ( - `bytes` - `crypto` - `crypto/ecdh` - `crypto/ecdsa` - `crypto/ed25519` - `crypto/rsa` - `crypto/tls` - `crypto/x509` - `encoding/pem` - `errors` - `net/url` - `os` - `strconv` - `strings` - - `r00t2.io/sysutils/paths` -) - -// FromURL returns a *TlsUri from a *url.URL. -func FromURL(u *url.URL) (t *TlsUri) { - - var newU *url.URL - - if u == nil { - return - } - - newU = new(url.URL) - *newU = *u - if u.User != nil { - newU.User = new(url.Userinfo) - *newU.User = *u.User - } - - newU.Scheme = "tls" - - t = &TlsUri{ - URL: newU, - } - - return -} - -// IsMatchedPair returns true if the privateKey is paired with the cert. -func IsMatchedPair(privKey crypto.PrivateKey, cert *x509.Certificate) (isMatched bool, err error) { - - var pubkey crypto.PublicKey - - if cert == nil || privKey == nil { - return - } - - pubkey = cert.PublicKey - - switch k := privKey.(type) { - case *rsa.PrivateKey: - if p, ok := pubkey.(*rsa.PublicKey); ok { - isMatched = k.PublicKey.Equal(p) - return - } - case ed25519.PrivateKey: - if p, ok := pubkey.(ed25519.PublicKey); ok { - // Order is flipped here because unlike the other key types, an ed25519.PrivateKey is just a []byte. - isMatched = p.Equal(k.Public()) - return - } - case *ecdh.PrivateKey: - if p, ok := pubkey.(*ecdh.PublicKey); ok { - isMatched = k.PublicKey().Equal(p) - return - } - case *ecdsa.PrivateKey: - if p, ok := pubkey.(*ecdsa.PublicKey); ok { - isMatched = k.PublicKey.Equal(p) - return - } - } - - // If we got here, we can't determine either the private key type or the cert's public key type. - err = ErrUnknownKey - - return -} - -/* - ParseTlsCipher parses string s and attempts to derive a TLS cipher suite (as a uint16) from it. - Use ParseTlsCipherSuite if you wish for a tls.CipherSuite instead. - - The string may either be the name (as per https://www.iana.org/assignments/tls-parameters/tls-parameters.xml) - or an int (normal, hex, etc. string representation). - - If none is found, the default is MaxTlsCipher. -*/ -func ParseTlsCipher(s string) (cipherSuite uint16, err error) { - - var nm string - var n uint64 - var i uint16 - var ok bool - - if n, err = strconv.ParseUint(s, 10, 16); err != nil { - if errors.Is(err, strconv.ErrSyntax) { - // It's a name; parse below. - err = nil - } else { - return - } - } else { - // It's a number. - if nm = tls.CipherSuiteName(uint16(n)); strings.HasPrefix(nm, "0x") { - // ...but invalid. - err = ErrBadTlsCipher - return - } else { - // Valid (as number). Return it. - cipherSuite = uint16(n) - return - } - } - - s = strings.ToUpper(s) - s = strings.ReplaceAll(s, " ", "_") - - // We build a dynamic map of cipher suite names to uint16s (if not already created). - if tlsCipherNmToUint == nil { - tlsCipherNmToUint = make(map[string]uint16) - for i = 0; i <= MaxTlsCipher; i++ { - if nm = tls.CipherSuiteName(i); !strings.HasPrefix(nm, "0x") { - tlsCipherNmToUint[nm] = i - } - } - } - - cipherSuite = MaxTlsCipher - if i, ok = tlsCipherNmToUint[s]; ok { - cipherSuite = i - } - - return -} - -// ParseTlsCipherStrict is like ParseTlsCipher, but an ErrBadTlsCipher or ErrUnknownCipher error will be raised if no matching cipher is found. -func ParseTlsCipherStrict(s string) (cipherSuite uint16, err error) { - - var nm string - var n uint64 - var i uint16 - var ok bool - - if n, err = strconv.ParseUint(s, 10, 16); err != nil { - if errors.Is(err, strconv.ErrSyntax) { - // It's a name; parse below. - err = nil - } else { - return - } - } else { - // It's a number. - if nm = tls.CipherSuiteName(uint16(n)); strings.HasPrefix(nm, "0x") { - // ...but invalid. - err = ErrBadTlsCipher - return - } else { - // Valid (as number). Return it. - cipherSuite = uint16(n) - return - } - } - - s = strings.ToUpper(s) - s = strings.ReplaceAll(s, " ", "_") - - // We build a dynamic map of cipher suite names to uint16s (if not already created). - if tlsCipherNmToUint == nil { - tlsCipherNmToUint = make(map[string]uint16) - for i = 0; i <= MaxTlsCipher; i++ { - if nm = tls.CipherSuiteName(i); !strings.HasPrefix(nm, "0x") { - tlsCipherNmToUint[nm] = i - } - } - } - - if i, ok = tlsCipherNmToUint[s]; ok { - cipherSuite = i - } else { - err = ErrUnknownCipher - } - - return -} - -/* - ParseTlsCiphers parses s as a comma-separated list of cipher suite names/integers and returns a slice of suites. - - See ParseTlsCipher for details, as this is mostly just a wrapper around it. - - If no cipher suites are found, cipherSuites will only contain MaxTlsCipher. -*/ -func ParseTlsCiphers(s string) (cipherSuites []uint16) { - - var suiteNms []string - var cipher uint16 - var err error - - suiteNms = strings.Split(s, ",") - cipherSuites = make([]uint16, 0, len(suiteNms)) - - for _, nm := range suiteNms { - if cipher, err = ParseTlsCipher(nm); err != nil { - err = nil - continue - } - cipherSuites = append(cipherSuites, cipher) - } - - if len(cipherSuites) == 0 { - cipherSuites = []uint16{tls.TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305_SHA256} - } - - return -} - -// ParseTlsCipherSuite is like ParseTlsCipher but returns a *tls.CipherSuite instead of a uint16 TLS cipher identifier. -func ParseTlsCipherSuite(s string) (cipherSuite *tls.CipherSuite, err error) { - - var cipherId uint16 - - if cipherId, err = ParseTlsCipher(s); err != nil { - return - } - - for _, v := range tls.CipherSuites() { - if v.ID == cipherId { - cipherSuite = v - return - } - } - for _, v := range tls.InsecureCipherSuites() { - if v.ID == cipherId { - cipherSuite = v - return - } - } - - return -} - -// ParseTlsCipherSuiteStrict is like ParseTlsCipherSuite, but an ErrBadTlsCipher or ErrUnknownCipher error will be raised if no matching cipher is found. -func ParseTlsCipherSuiteStrict(s string) (cipherSuite *tls.CipherSuite, err error) { - - var cipherId uint16 - - if cipherId, err = ParseTlsCipherStrict(s); err != nil { - return - } - - for _, v := range tls.CipherSuites() { - if v.ID == cipherId { - cipherSuite = v - return - } - } - for _, v := range tls.InsecureCipherSuites() { - if v.ID == cipherId { - cipherSuite = v - return - } - } - - return -} - -// ParseTlsCipherSuites is like ParseTlsCiphers but returns a []*tls.CipherSuite instead of a []uint16 of TLS cipher identifiers. -func ParseTlsCipherSuites(s string) (cipherSuites []*tls.CipherSuite, err error) { - - var found bool - var cipherIds []uint16 - - cipherIds = ParseTlsCiphers(s) - - for _, cipherId := range cipherIds { - found = false - for _, v := range tls.CipherSuites() { - if v.ID == cipherId { - cipherSuites = append(cipherSuites, v) - found = true - break - } - } - if !found { - for _, v := range tls.InsecureCipherSuites() { - if v.ID == cipherId { - cipherSuites = append(cipherSuites, v) - break - } - } - } - } - - return -} - -/* - ParseTlsCurve parses string s and attempts to derive a tls.CurveID from it. - - The string may either be the name (as per // https://www.iana.org/assignments/tls-parameters/tls-parameters.xml#tls-parameters-8) - or an int (normal, hex, etc. string representation). -*/ -func ParseTlsCurve(s string) (curve tls.CurveID, err error) { - - var i tls.CurveID - var n uint64 - var ok bool - - if n, err = strconv.ParseUint(s, 10, 16); err != nil { - if errors.Is(err, strconv.ErrSyntax) { - // It's a name; parse below. - err = nil - } else { - return - } - } else { - // It's a number. - if strings.HasPrefix(tls.CurveID(uint16(n)).String(), "CurveID(") { - // ...but invalid. - err = ErrBadTlsCurve - return - } else { - // Valid (as number). Return it. - curve = tls.CurveID(uint16(n)) - return - } - } - - // It seems to be a name. Normalize... - s = strings.ToUpper(s) - - // Unfortunately there's no "tls.CurveIDName()" function. - // They do have a .String() method though. - if tlsCurveNmToCurve == nil { - tlsCurveNmToCurve = make(map[string]tls.CurveID) - for i = 0; i <= MaxCurveId; i++ { - if strings.HasPrefix(i.String(), "CurveID(") { - continue - } - tlsCurveNmToCurve[i.String()] = i - // It's normally mixed-case; we want to be able to look it up in a normalized all-caps as well. - tlsCurveNmToCurve[strings.ToUpper(i.String())] = i - // The normal name, except for X25519, has "Curve" in the front. We add it without that prefix as well. - tlsCurveNmToCurve[strings.TrimPrefix(i.String(), "Curve")] = i - } - } - - curve = MaxCurveId - if _, ok = tlsCurveNmToCurve[s]; ok { - curve = tlsCurveNmToCurve[s] - } - - return -} - -/* - ParseTlsCurves parses s as a comma-separated list of tls.CurveID names/integers and returns a slice of tls.CurveID. - - See ParseTlsCurve for details, as this is mostly just a wrapper around it. - - If no curves are found, curves will only contain MaxCurveId. -*/ -func ParseTlsCurves(s string) (curves []tls.CurveID) { - - var curveNms []string - var curve tls.CurveID - var err error - - curveNms = strings.Split(s, ",") - curves = make([]tls.CurveID, 0, len(curveNms)) - - for _, nm := range curveNms { - if curve, err = ParseTlsCurve(nm); err != nil { - err = nil - continue - } - curves = append(curves, curve) - } - - if len(curves) == 0 { - curves = []tls.CurveID{MaxCurveId} - } - - return -} - -/* - ParseTlsUri parses a "TLS URI"'s query parameters. All certs and keys must be in PEM format. - - You probably don't need this and should instead be using TlsUri.ToTlsConfig. - It just wraps this, but is probably more convenient. -*/ -func ParseTlsUri(tlsUri *url.URL) (tlsConf *tls.Config, err error) { - - var b []byte - var rootCAs *x509.CertPool - var intermediateCAs []*x509.Certificate - var privKeys []crypto.PrivateKey - var tlsCerts []tls.Certificate - var allowInvalid bool - var ciphers []uint16 - var curves []tls.CurveID - var params map[string][]string - var ok bool - var val string - var minVer uint16 - var maxVer uint16 - var buf *bytes.Buffer = new(bytes.Buffer) - var srvNm string = tlsUri.Hostname() - - params = tlsUri.Query() - - if params == nil { - tlsConf = &tls.Config{ - ServerName: srvNm, - } - return - } - - // These are all filepath(s). - for _, k := range []string{ - TlsUriParamCa, - TlsUriParamCert, - TlsUriParamKey, - } { - if _, ok = params[k]; ok { - for idx, _ := range params[k] { - if err = paths.RealPath(¶ms[k][idx]); err != nil { - return - } - } - } - } - - // CA cert(s). - buf.Reset() - if _, ok = params[TlsUriParamCa]; ok { - rootCAs = x509.NewCertPool() - for _, c := range params[TlsUriParamCa] { - if b, err = os.ReadFile(c); err != nil { - if errors.Is(err, os.ErrNotExist) { - err = nil - continue - } - } - buf.Write(b) - } - if rootCAs, _, intermediateCAs, err = ParseCA(buf.Bytes()); err != nil { - return - } - } else { - if rootCAs, err = x509.SystemCertPool(); err != nil { - return - } - } - - // Keys. These are done first so we can match to a client certificate. - buf.Reset() - if _, ok = params[TlsUriParamKey]; ok { - for _, k := range params[TlsUriParamKey] { - if b, err = os.ReadFile(k); err != nil { - if errors.Is(err, os.ErrNotExist) { - err = nil - continue - } else { - return - } - } - buf.Write(b) - } - if privKeys, err = ParsePrivateKey(buf.Bytes()); err != nil { - return - } - } - - // (Client) Certificate(s). - buf.Reset() - if _, ok = params[TlsUriParamCert]; ok { - for _, c := range params[TlsUriParamCert] { - if b, err = os.ReadFile(c); err != nil { - if errors.Is(err, os.ErrNotExist) { - err = nil - continue - } else { - return - } - } - buf.Write(b) - } - if tlsCerts, err = ParseLeafCert(buf.Bytes(), privKeys, intermediateCAs...); err != nil { - return - } - } - - // Hostname (Override). - if _, ok = params[TlsUriParamSni]; ok { - srvNm = params[TlsUriParamSni][0] - } - - // Disable Verification. - if _, ok = params[TlsUriParamNoVerify]; ok { - val = strings.ToLower(params[TlsUriParamNoVerify][0]) - for _, i := range paramBoolValsTrue { - if i == val { - allowInvalid = true - break - } - } - } - - // Ciphers. - if _, ok = params[TlsUriParamCipher]; ok { - ciphers = ParseTlsCiphers(strings.Join(params[TlsUriParamCipher], ",")) - } - - // Minimum TLS Protocol Version. - if _, ok = params[TlsUriParamMinTls]; ok { - if minVer, err = ParseTlsVersion(params[TlsUriParamMinTls][0]); err != nil { - return - } - } - - // Maximum TLS Protocol Version. - if _, ok = params[TlsUriParamMaxTls]; ok { - if maxVer, err = ParseTlsVersion(params[TlsUriParamMaxTls][0]); err != nil { - return - } - } - - // Curves. - if _, ok = params[TlsUriParamCurve]; ok { - curves = ParseTlsCurves(strings.Join(params[TlsUriParamCurve], ",")) - } - - tlsConf = &tls.Config{ - Certificates: tlsCerts, - RootCAs: rootCAs, - ServerName: srvNm, - InsecureSkipVerify: allowInvalid, - CipherSuites: ciphers, - MinVersion: minVer, - MaxVersion: maxVer, - CurvePreferences: curves, - } - - return -} - -// ParseTlsVersion parses string s and attempts to derive a TLS version from it. If none is found, tlsVer will be 0. -func ParseTlsVersion(s string) (tlsVer uint16, err error) { - - var nm string - var n uint64 - var i uint16 - var ok bool - - if n, err = strconv.ParseUint(s, 10, 16); err != nil { - if errors.Is(err, strconv.ErrSyntax) { - // It's a name; parse below. - err = nil - } else { - return - } - } else { - // It's a number. - if nm = tls.VersionName(uint16(n)); strings.HasPrefix(nm, "0x") { - // ...but invalid. - err = ErrBadTlsVer - return - } else { - // Valid (as number). Return it. - tlsVer = uint16(n) - return - } - } - - // If we get here, it should be parsed as a version string. - s = strings.ToUpper(s) - s = strings.ReplaceAll(s, "_", " ") - s = strings.ReplaceAll(s, "V", " ") - s = strings.TrimSpace(s) - if !strings.HasPrefix(s, "SSL") && !strings.HasPrefix(s, "TLS ") { - s = "TLS " + s - } - - // We build a dynamic map of version names to uint16s (if not already created). - if tlsVerNmToUint == nil { - tlsVerNmToUint = make(map[string]uint16) - for i = MinTlsVer; i <= MaxTlsVer; i++ { - if nm = tls.VersionName(i); !strings.HasPrefix(nm, "0x") { - tlsVerNmToUint[nm] = i - } - } - } - - if i, ok = tlsVerNmToUint[s]; ok { - tlsVer = i - } - - return -} - -/* - ParseCA parses PEM bytes and returns an *x509.CertPool in caCerts. - - Concatenated PEM files are supported. - - Any keys found will be filtered out, as will any leaf certificates. - - Any *intermediate* CAs (the certificate is a CA but it is not self-signed) will be returned separate from - certPool. - - Ordering from the file is preserved in the returned slices. -*/ -func ParseCA(certRaw []byte) (certPool *x509.CertPool, rootCerts []*x509.Certificate, intermediateCerts []*x509.Certificate, err error) { - - var pemBlocks []*pem.Block - var cert *x509.Certificate - var certs []*x509.Certificate - - if pemBlocks, err = SplitPem(certRaw); err != nil { - return - } - - // Filter out keys etc. and non-CA certs. - for _, b := range pemBlocks { - if b.Type != "CERTIFICATE" { - continue - } - if cert, err = x509.ParseCertificate(b.Bytes); err != nil { - return - } - if !cert.IsCA { - continue - } - certs = append(certs, cert) - } - - for _, cert = range certs { - if bytes.Equal(cert.RawIssuer, cert.RawSubject) { - // It's a root/self-signed. - rootCerts = append(rootCerts, cert) - } else { - // It's an intermediate. - intermediateCerts = append(intermediateCerts, cert) - } - } - - if rootCerts != nil { - certPool = x509.NewCertPool() - for _, cert = range rootCerts { - certPool.AddCert(cert) - } - } - - return -} - -/* - 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 -} diff --git a/cryptparse/funcs_test.go b/cryptparse/funcs_test.go deleted file mode 100644 index b491a5a..0000000 --- a/cryptparse/funcs_test.go +++ /dev/null @@ -1,37 +0,0 @@ -package cryptparse - -import ( - `crypto/tls` - "testing" -) - -func TestCiphers(t *testing.T) { - - var err error - var cs *tls.CipherSuite - - // Good ciphers - for _, cn := range []string{ - "TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305_SHA256", - "tls ecdhe ecdsa with chacha20 poly1305 sha256", - } { - if cs, err = ParseTlsCipherSuiteStrict(cn); err != nil { - t.Fatalf("ERROR parsing good cipher '%s': %v", cn, err) - } - if cs.Name != cn { - t.Logf("Cipher name change: '%s' => '%s'", cn, cs.Name) - } - t.Logf("Cipher for '%s':\n%#v", cn, cs) - } - - // Bad ciphers - for _, cn := range []string{ - "TLS_BAD_CIPHER", - } { - if cs, err = ParseTlsCipherSuiteStrict(cn); err == nil { - t.Fatalf("ERROR parsing bad cipher '%s'; err is nil", cn) - } - } - - _ = cs -} diff --git a/cryptparse/funcs_tlsflat.go b/cryptparse/funcs_tlsflat.go deleted file mode 100644 index b205e75..0000000 --- a/cryptparse/funcs_tlsflat.go +++ /dev/null @@ -1,217 +0,0 @@ -package cryptparse - -import ( - `bytes` - `crypto` - `crypto/tls` - `crypto/x509` - `errors` - `fmt` - `net/url` - `os` - `strings` - - `r00t2.io/sysutils/paths` -) - -// Normalize ensures that all specified filepaths are absolute, etc. -func (t *TlsFlat) Normalize() (err error) { - - if t.Certs != nil { - for _, c := range t.Certs { - if err = paths.RealPath(&c.CertFile); err != nil { - return - } - if c.KeyFile != nil { - if err = paths.RealPath(c.KeyFile); err != nil { - return - } - } - } - } - if t.CaFiles != nil { - for idx, _ := range t.CaFiles { - if err = paths.RealPath(&t.CaFiles[idx]); err != nil { - return - } - } - } - - return -} - -/* - ToTlsConfig returns a tls.Config from a TlsFlat. Note that it will have Normalize called on it. - - Unfortunately it's not possible for this library to do the reverse, as CA certificates are not able to be extracted from an x509.CertPool. -*/ -func (t *TlsFlat) ToTlsConfig() (tlsConf *tls.Config, err error) { - - var b []byte - var rootCAs *x509.CertPool - var intermediateCAs []*x509.Certificate - var privKeys []crypto.PrivateKey - var tlsCerts []tls.Certificate - var parsedTlsCerts []tls.Certificate - var ciphers []uint16 - var curves []tls.CurveID - var minVer uint16 - var maxVer uint16 - var buf *bytes.Buffer = new(bytes.Buffer) - var srvNm string = t.SniName - - // Normalize any filepaths before validation. - if err = t.Normalize(); err != nil { - return - } - - // And validate. - if err = validate.Struct(t); err != nil { - return - } - - // CA cert(s). - buf.Reset() - if t.CaFiles != nil { - rootCAs = x509.NewCertPool() - for _, c := range t.CaFiles { - if b, err = os.ReadFile(c); err != nil { - if errors.Is(err, os.ErrNotExist) { - err = nil - continue - } - } - buf.Write(b) - } - if rootCAs, _, intermediateCAs, err = ParseCA(buf.Bytes()); err != nil { - return - } - } else { - if rootCAs, err = x509.SystemCertPool(); err != nil { - return - } - } - - // Keys and Certs. They are assumed to be matched. - if t.Certs != nil { - for _, c := range t.Certs { - privKeys = nil - if c.KeyFile != nil { - if b, err = os.ReadFile(*c.KeyFile); err != nil { - return - } - if privKeys, err = ParsePrivateKey(b); err != nil { - return - } - } - if b, err = os.ReadFile(c.CertFile); err != nil { - return - } - if parsedTlsCerts, err = ParseLeafCert(b, privKeys, intermediateCAs...); err != nil { - return - } - tlsCerts = append(tlsCerts, parsedTlsCerts...) - } - } - - // Ciphers. - if t.CipherSuites != nil { - ciphers = ParseTlsCiphers(strings.Join(t.CipherSuites, ",")) - } - - // Minimum TLS Protocol Version. - if t.MinTlsProtocol != nil { - if minVer, err = ParseTlsVersion(*t.MinTlsProtocol); err != nil { - return - } - } - - // Maximum TLS Protocol Version. - if t.MaxTlsProtocol != nil { - if maxVer, err = ParseTlsVersion(*t.MaxTlsProtocol); err != nil { - return - } - } - - // Curves. - if t.Curves != nil { - curves = ParseTlsCurves(strings.Join(t.Curves, ",")) - } - - tlsConf = &tls.Config{ - Certificates: tlsCerts, - RootCAs: rootCAs, - ServerName: srvNm, - InsecureSkipVerify: t.SkipVerify, - CipherSuites: ciphers, - MinVersion: minVer, - MaxVersion: maxVer, - CurvePreferences: curves, - } - return -} - -// ToTlsUri returns a TlsUri from a TlsFlat. -func (t *TlsFlat) ToTlsUri() (tlsUri *TlsUri, err error) { - - var u *url.URL - - if u, err = url.Parse(fmt.Sprintf("tls://%v/", t.SniName)); err != nil { - return - } - - // CA cert(s). - if t.CaFiles != nil { - for _, c := range t.CaFiles { - u.Query().Add(TlsUriParamCa, c) - } - } - - // Keys and Certs. - if t.Certs != nil { - for _, c := range t.Certs { - u.Query().Add(TlsUriParamCert, c.CertFile) - if c.KeyFile != nil { - u.Query().Add(TlsUriParamKey, *c.KeyFile) - } - } - } - - // Enforce the SNI hostname. - u.Query().Add(TlsUriParamSni, t.SniName) - - // Disable Verification. - if t.SkipVerify { - u.Query().Add(TlsUriParamNoVerify, "1") - } - - // Ciphers. - if t.CipherSuites != nil { - for _, c := range t.CipherSuites { - u.Query().Add(TlsUriParamCipher, c) - } - } - - // Minimum TLS Protocol Version. - if t.MinTlsProtocol != nil { - u.Query().Add(TlsUriParamMinTls, *t.MinTlsProtocol) - } - - // Maximum TLS Protocol Version. - if t.MaxTlsProtocol != nil { - u.Query().Add(TlsUriParamMaxTls, *t.MaxTlsProtocol) - } - - // Curves. - if t.Curves != nil { - for _, c := range t.Curves { - u.Query().Add(TlsUriParamCurve, c) - } - } - - tlsUri = &TlsUri{ - URL: u, - } - - return -} diff --git a/cryptparse/funcs_tlsuri.go b/cryptparse/funcs_tlsuri.go deleted file mode 100644 index 1586603..0000000 --- a/cryptparse/funcs_tlsuri.go +++ /dev/null @@ -1,256 +0,0 @@ -package cryptparse - -import ( - `crypto` - `crypto/tls` - `net` - `net/url` - `os` - `strings` -) - -/* - WithConn returns a (crypto/)tls.Conn from an existing/already dialed net.Conn. - - underlying should be a "bare" net.Conn; behavior is undefined/unknown if the underlying conn is already a (crypto/)tls.Conn. -*/ -func (t *TlsUri) WithConn(underlying net.Conn) (conn *tls.Conn, err error) { - - var cfg *tls.Config - - if cfg, err = t.ToTlsConfig(); err != nil { - return - } - - conn = tls.Client(underlying, cfg) - - return -} - -/* - ToConn returns a "bare" net.Conn (already dialed) from a TlsUri. - - Note that this does NOT include the TLS configured or initialized; use TlsUri.ToTlsConn for that. - (A (crypto/)tls.Conn conforms to net.Conn.) - - An error will be returned if no port is explicitly defined in the TlsUri. -*/ -func (t *TlsUri) ToConn() (conn net.Conn, err error) { - - var ok bool - var connHost string - var params map[string][]string - var netType string = DefaultNetType - - params = t.Query() - - if params != nil { - if _, ok = params[TlsUriParamNet]; ok { - netType = params[TlsUriParamNet][0] - } - } - netType = strings.ToLower(netType) - - switch netType { - case "unix", "unixgram", "unixpacket": - connHost = t.Path - default: - connHost = t.Host - } - - if conn, err = net.Dial(netType, connHost); err != nil { - return - } - - return -} - -/* - ToTlsConfig returns a *tls.Config from a TlsUri. - - Unfortunately it's not possible for this library to do the reverse, as CA certificates are not able to be extracted from an x509.CertPool. -*/ -func (t *TlsUri) ToTlsConfig() (cfg *tls.Config, err error) { - - if cfg, err = ParseTlsUri(t.URL); err != nil { - return - } - - return -} - -/* - ToTlsConn returns a (crypto/)tls.Conn (already dialed) from a TlsUri. - - An error will be returned if no port is explicitly defined in the TlsUri. -*/ -func (t *TlsUri) ToTlsConn() (conn *tls.Conn, err error) { - - var ok bool - var cfg *tls.Config - var connHost string - var params map[string][]string - var netType string = DefaultNetType - - if cfg, err = t.ToTlsConfig(); err != nil { - return - } - - params = t.Query() - - if params != nil { - if _, ok = params[TlsUriParamNet]; ok { - netType = params[TlsUriParamNet][0] - } - } - netType = strings.ToLower(netType) - - switch netType { - case "unix", "unixgram", "unixpacket": - connHost = t.Path - default: - connHost = t.Host - } - - if conn, err = tls.Dial(netType, connHost, cfg); err != nil { - return - } - - return -} - -// ToTlsFlat returns a *TlsFlat from a TlsUri. -func (t *TlsUri) ToTlsFlat() (tlsFlat *TlsFlat, err error) { - - var b []byte - var params url.Values - var paramMap map[string][]string - // These also have maps so they can backmap filenames. - var privKeys []crypto.PrivateKey - var privKeyMap map[string][]crypto.PrivateKey - var tlsCerts []tls.Certificate - var tlsCertMap map[string][]tls.Certificate - var isMatch bool - var fCert *TlsFlatCert - var val string - var f TlsFlat = TlsFlat{ - SniName: t.Hostname(), - SkipVerify: false, - Certs: nil, - CaFiles: nil, - CipherSuites: nil, - MinTlsProtocol: nil, - MaxTlsProtocol: nil, - Curves: nil, - } - - params = t.Query() - paramMap = params - - if params == nil { - tlsFlat = &f - return - } - - // CA cert(s). - if t.Query().Has(TlsUriParamCa) { - f.CaFiles = append(f.CaFiles, paramMap[TlsUriParamCa]...) - } - - // Keys and Certs. These are done first so we can match to a client certificate. - if t.Query().Has(TlsUriParamKey) { - privKeyMap = make(map[string][]crypto.PrivateKey) - for _, kFile := range paramMap[TlsUriParamKey] { - if b, err = os.ReadFile(kFile); err != nil { - return - } - if privKeyMap[kFile], err = ParsePrivateKey(b); err != nil { - return - } - privKeys = append(privKeys, privKeyMap[kFile]...) - } - } - if t.Query().Has(TlsUriParamCert) { - tlsCertMap = make(map[string][]tls.Certificate) - for _, cFile := range paramMap[TlsUriParamCert] { - if b, err = os.ReadFile(cFile); err != nil { - return - } - if tlsCertMap[cFile], err = ParseLeafCert(b, privKeys); err != nil { - return - } - tlsCerts = append(tlsCerts, tlsCertMap[cFile]...) - } - } - // We then correlate. Whew, lads. - for cFile, c := range tlsCertMap { - for _, cert := range c { - for kFile, k := range privKeyMap { - if isMatch, err = IsMatchedPair(k, cert.Leaf); err != nil { - return - } else if isMatch { - fCert = &TlsFlatCert{ - CertFile: cFile, - KeyFile: new(string), - } - *fCert.KeyFile = kFile - f.Certs = append(f.Certs, fCert) - } - } - } - } - - // Hostname. - if t.Query().Has(TlsUriParamSni) { - f.SniName = t.Query().Get(TlsUriParamSni) - } - - // Disable verification. - if t.Query().Has(TlsUriParamNoVerify) { - val = strings.ToLower(t.Query().Get(TlsUriParamNoVerify)) - for _, i := range paramBoolValsTrue { - if val == i { - f.SkipVerify = true - break - } - } - } - - // Ciphers. - if t.Query().Has(TlsUriParamCipher) { - f.CipherSuites = params[TlsUriParamCipher] - } - - // Minimum TLS Protocol Version. - if t.Query().Has(TlsUriParamMinTls) { - f.MinTlsProtocol = new(string) - *f.MinTlsProtocol = t.Query().Get(TlsUriParamMinTls) - } - - // Maximum TLS Protocol Version. - if t.Query().Has(TlsUriParamMaxTls) { - f.MaxTlsProtocol = new(string) - *f.MaxTlsProtocol = t.Query().Get(TlsUriParamMaxTls) - } - - // Curves. - if t.Query().Has(TlsUriParamCurve) { - f.Curves = params[TlsUriParamCurve] - } - - tlsFlat = &f - - return -} - -// ToURL returns the *url.URL representation of a TlsUri. Note that the params will remain, so remove them explicitly if needed. -func (t *TlsUri) ToURL() (u *url.URL) { - - if t == nil { - return - } - - u = t.URL - - return -} diff --git a/cryptparse/types.go b/cryptparse/types.go deleted file mode 100644 index 263849d..0000000 --- a/cryptparse/types.go +++ /dev/null @@ -1,30 +0,0 @@ -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 -} diff --git a/go.mod b/go.mod index 7fbbe59..37623a1 100644 --- a/go.mod +++ b/go.mod @@ -1,8 +1,9 @@ -module r00t2.io/sysutils +module r00t2.io/sysutils/v2 -go 1.21 +go 1.23.2 require ( + github.com/Luzifer/go-dhparam v1.2.0 github.com/davecgh/go-spew v1.1.1 github.com/g0rbe/go-chattr v1.0.1 github.com/go-playground/validator/v10 v10.22.0 diff --git a/go.sum b/go.sum index d6c1fd0..f748db6 100644 --- a/go.sum +++ b/go.sum @@ -1,3 +1,5 @@ +github.com/Luzifer/go-dhparam v1.2.0 h1:YwDf15FTsVriTynCv1qF+1Inh6E8Dg1+28tPEA3pvFo= +github.com/Luzifer/go-dhparam v1.2.0/go.mod h1:hnazoxBTsXnRvGXAosio70Tb1lWowquyhVdvsXdlIPc= 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=