From ef5ef16de38e2486e3c1705a63f2e5180480f86e Mon Sep 17 00:00:00 2001 From: brent saner Date: Thu, 17 Oct 2024 15:20:27 -0400 Subject: [PATCH] v2.0.0 API CHANGES: * The struct tags for marshaling/unmarshaling on TlsFlat* have changed. ADDED: * Stubs for further chain processing/associations/correlation --- cryptparse/funcs.go | 95 ++++++++++++++++++++++++++++++++-- cryptparse/funcs_pemblocks.go | 89 ++++++++++++++++++++++++++++++++ cryptparse/types.go | 97 ++++++++++++++++++++++++++++++----- go.mod | 1 + go.sum | 2 + 5 files changed, 268 insertions(+), 16 deletions(-) create mode 100644 cryptparse/funcs_pemblocks.go diff --git a/cryptparse/funcs.go b/cryptparse/funcs.go index 61052b8..5677991 100644 --- a/cryptparse/funcs.go +++ b/cryptparse/funcs.go @@ -665,6 +665,34 @@ func ParseCA(certRaw []byte) (certPool *x509.CertPool, rootCerts []*x509.Certifi return } +/* + ParseDhParams parses PEM bytes and returns parsed DH parameters. + + Concatenated PEM files are supported. + + TODO: Currently not fully implemented; params will always be nil. +*/ +/* +func ParseDhParams(dhRaw []byte) (params []*dhparam.DH, err error) { + + var pemBlocks *PemBlocks + + if dhRaw == nil || len(dhRaw) == 0 { + return + } + if pemBlocks, err = SplitPemBlocks(dhRaw); err != nil { + return + } + if pemBlocks == nil || len(*pemBlocks) == 0 { + return + } + + // TODO + + return +} +*/ + /* ParseLeafCert parses PEM bytes from a (client) certificate file, iterates over a slice of crypto.PrivateKey (finding one that matches), and returns one (or more) tls.Certificate. @@ -767,6 +795,49 @@ func ParseLeafCert(certRaw []byte, keys []crypto.PrivateKey, intermediates ...*x return } +/* + ParseLeafCertSimple is like ParseLeafCert, but *only* returns leaf certificates; + no key correlation or chain building/association occurs. + + TODO: Currently not fully implemented. +*/ +/* +func ParseLeafCertSimple() () { + + // TODO + + return +} +*/ + +/* + ParsePemBundle splits a combined PEM (also referred to as "bundled PEMs") into one or more TlsPkiChains. + + (combinedRaw must be the PEM-encoded bytes, not the decoded contained bytes.) + + TODO: Currently not fully implemented. +*/ +/* +func ParsePemBundle(combinedRaw []byte) (chains []*TlsPkiChain, err error) { + + var roots []*x509.Certificate + var inters []*x509.Certificate + var keys []crypto.PrivateKey + var certs []tls.Certificate + + if _, roots, inters, err = ParseCA(combinedRaw); err != nil { + return + } + if keys, err = ParsePrivateKey(combinedRaw); err != nil { + return + } + + // TODO + + return +} +*/ + /* ParsePrivateKey parses PEM bytes to a private key. Multiple keys may be concatenated in the same file. @@ -807,20 +878,36 @@ func ParsePrivateKey(keyRaw []byte) (keys []crypto.PrivateKey, err error) { } } - // TODO + return +} + +// SplitPem splits a single block of bytes into one (or more) (encoding/)pem.Blocks. Currently err is not used, but is reserved for future use. +func SplitPem(pemRaw []byte) (blocks []*pem.Block, err error) { + + var pemBlocks *PemBlocks + + if pemBlocks, err = SplitPemBlocks(pemRaw); err != nil { + return + } + + blocks = pemBlocks.Split() return } -// SplitPem splits a single block of bytes into one (or more) (encoding/)pem.Blocks. -func SplitPem(pemRaw []byte) (blocks []*pem.Block, err error) { +// SplitPemBlocks splits a single block of bytes into a PemBlocks. Currently err is not used, but is reserved for future use. +func SplitPemBlocks(pemRaw []byte) (blocks *PemBlocks, err error) { var block *pem.Block + var nativeBlocks []*pem.Block var rest []byte for block, rest = pem.Decode(pemRaw); block != nil; block, rest = pem.Decode(rest) { - blocks = append(blocks, block) + nativeBlocks = append(nativeBlocks, block) } + blocks = new(PemBlocks) + *blocks = nativeBlocks + return } diff --git a/cryptparse/funcs_pemblocks.go b/cryptparse/funcs_pemblocks.go new file mode 100644 index 0000000..bb928ad --- /dev/null +++ b/cryptparse/funcs_pemblocks.go @@ -0,0 +1,89 @@ +package cryptparse + +import ( + `bytes` + `encoding/pem` +) + +// Bytes returns a combined PEM bytes of all blocks in a PemBlocks. Any nil, empty, or otherwise invalid blocks are skipped. +func (p *PemBlocks) Bytes() (combined []byte) { + + var err error + var buf *bytes.Buffer = new(bytes.Buffer) + + for _, block := range p.Split() { + if block == nil || block.Bytes == nil || block.Type == "" { + continue + } + // We've ruled out "contextual" errors, so we ignore it here. + if err = pem.Encode(buf, block); err != nil { + continue + } + } + + combined = buf.Bytes() + _ = err + + return +} + +// BytesStrict is like Bytes but is much more strict/safe (invalid/empty/nil blocks are not skipped) and will return any errors on encoding. +func (p *PemBlocks) BytesStrict() (combined []byte, err error) { + + var buf *bytes.Buffer = new(bytes.Buffer) + + for _, block := range p.Split() { + if err = pem.Encode(buf, block); err != nil { + return + } + } + + combined = buf.Bytes() + + return +} + +// BytesSplit returns separate PEM bytes of each block in a PemBlocks. Any nil, empty, or otherwise invalid blocks are skipped. +func (p *PemBlocks) BytesSplit() (pems [][]byte) { + + var b []byte + + for _, block := range p.Split() { + if block == nil || block.Bytes == nil || block.Type == "" { + continue + } + // We've ruled out "contextual" errors, so we ignore it here. + b = pem.EncodeToMemory(block) + pems = append(pems, b) + } + + return +} + +// BytesSplitStrict is like BytesSplit but is much more strict/safe (invalid/empty/nil blocks are not skipped) and will return any errors on encoding. +func (p *PemBlocks) BytesSplitStrict() (pems [][]byte, err error) { + + var buf *bytes.Buffer = new(bytes.Buffer) + + for _, block := range p.Split() { + buf.Reset() + if err = pem.Encode(buf, block); err != nil { + return + } + pems = append(pems, buf.Bytes()) + } + + return +} + +// Split returns a more primitive-friendly representation of a PemBlocks. +func (p *PemBlocks) Split() (native []*pem.Block) { + + if p == nil { + return + } + + native = *p + + return +} diff --git a/cryptparse/types.go b/cryptparse/types.go index 263849d..1ebdfde 100644 --- a/cryptparse/types.go +++ b/cryptparse/types.go @@ -1,28 +1,101 @@ package cryptparse import ( + `crypto` + `crypto/tls` + `crypto/x509` + `encoding/pem` `encoding/xml` `net/url` + + `github.com/Luzifer/go-dhparam` ) +// PemBlocks is a combined set of multiple pem.Blocks. +type PemBlocks []*pem.Block + // TlsFlat provides an easy structure to marshal/unmarshal a tls.Config from/to a data structure (JSON, XML, etc.). type TlsFlat struct { - XMLName xml.Name `xml:"tlsConfig" json:"-" yaml:"-" toml:"-"` - SniName 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"` + XMLName xml.Name `xml:"tlsConfig" json:"-" yaml:"-" toml:"-"` + // SniName represents the expected Server Name Indicator's name. See TlsUriParamSni. + SniName string `json:"sni_name" toml:"SNIName" yaml:"SNI Name" xml:"sniName,attr" required:"true" validate:"required"` + // SkipVerify, if true, will bypass certificate verification. You generally should not enable this. See TlsUriParamNoVerify. + SkipVerify bool `json:"skip_verify,omitempty" toml:"SkipVerify,omitempty" yaml:"Skip Verification,omitempty" xml:"skipVerify,attr,omitempty"` + // Certs contains 0 or more TlsFlatCert certificate definitions. See TlsUriParamCert and TlsUriParamKey as well. + Certs []*TlsFlatCert `json:"certs,omitempty" toml:"Certs,omitempty" yaml:"Certificates,omitempty" xml:"certs>cert,omitempty"validate:"omitempty,dive"` + // CaFiles contains filepaths to CA certificates/"trust anchors" in PEM format. They may be combined. See TlsUriParamCa. + CaFiles []string `json:"ca_files,omitempty" toml:"CaFiles,omitempty" yaml:"CA Files,omitempty" xml:"roots>ca,omitempty" validate:"omitempty,dive,filepath"` + // CipherSuites represents desired ciphers/cipher suites for this TLS environment. See TlsUriParamCipher. + CipherSuites []string `json:"cipher_suites,omitempty" toml:"CipherSuites,omitempty" yaml:"Cipher Suites,omitempty" xml:"ciphers,omitempty"` + // Curves specifies desired cryptographic curves to be used. See TlsUriParamCurve. + Curves []string `json:"curves,omitempty" xml:"curves>curve,omitempty" yaml:"Curves,omitempty" toml:"Curves,omitempty" validate:"omitempty,dive"` + // MinTlsProtocol specifies the minimum TLS version. See TlsUriParamMinTls. + MinTlsProtocol *string `json:"min_tls_protocol,omitempty" xml:"minTlsProtocol,attr,omitempty" yaml:"MinTlsProtocol,omitempty" toml:"MinTlsProtocol,omitempty"` + // MaxTlsProtocol specifies the maximum TLS version. See TlsUriParamMaxTls. + MaxTlsProtocol *string `json:"max_tls_protocol,omitempty" xml:"maxTlsProtocol,attr,omitempty" yaml:"MaxTlsProtocol,omitempty" toml:"MaxTlsProtocol,omitempty"` } // TlsFlatCert represents a certificate (and, possibly, paired key). type TlsFlatCert struct { - XMLName xml.Name `xml:"cert" json:"-" yaml:"-" toml:"-"` - KeyFile *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"` + XMLName xml.Name `xml:"cert" json:"-" yaml:"-" toml:"-"` + // KeyFile is a filepath to a PEM-encoded key file. See TlsUriParamKey. + KeyFile *string `json:"key,omitempty" xml:"key,attr,omitempty" yaml:"Key,omitempty" toml:"Key,omitempty" validate:"omitempty,filepath"` + // CertFile is a filepath to a PEM-encoded certificate file. See TlsUriParamCert. + CertFile string `json:"cert" xml:",chardata" yaml:"Certificate" toml:"Certificate" required:"true" validate:"required,filepath"` +} + +// TlsPkiChain contains a whole X.509 PKI chain -- Root CA(s) (trust anchors) which sign Intermediate(s) which sign Certificate(s). +type TlsPkiChain struct { + /* + Roots are all trust anchors/root certificates. + + Roots are certificates that are self-signed and can issue certificates/sign CSRs. + */ + Roots []*x509.Certificate + // RootsPool is an x509.CertPool representation of Roots. + RootsPool *x509.CertPool + /* + Intermediates are signers that should not be trusted directly, but instead included in the verification/validation chain. + + Intermediates are certificates that are NOT self-signed (they should be signed by at least one Roots/RootsPool) + but CAN issue certificates/sign CSRs. + */ + Intermediates []*x509.Certificate + // IntermediatesPool is an x509.CertPool representation of Intermediates. + IntermediatesPool *x509.CertPool + /* + Certificates are "leaf certificates"; typically these are the certificates used directly by servers/users. + + A certificate is considered a Certificate here if it is NOT self-signed and is NOT able to issue certificates/sign CSRs. + */ + Certificates []*tls.Certificate + // CertificatesPool is an x509.CertPool representation of Certificates. + CertificatesPool *x509.CertPool + /* + UnmatchedCerts contains Certificates that: + * Do not match any of Roots/RootsPool as its signer, and/or + * Do not match any Intermediates/IntermediatesPool as its signer, and/or + * Does not meet requirements for Roots/RootsPool, and/or + * Does not meet requirements for Intermediates/IntermediatesPool, and/or + * Has no matching crypto.PrivateKey found. + + These should generally *never* be used if they were parsed in. + They represent "stray" certificates that have no logical chain/path found + and are likely unusable for purposes of this environment. + */ + UnmatchedCerts []*x509.Certificate + // UnmatchedCertsPool is an x509.CertPool representation of UnmatchedCerts. + UnmatchedCertsPool *x509.CertPool + /* + UnmatchedKeys represent parsed private keys that have no matching corresponding certifificate. + + These should generally *never* be used if they were parsed in. + They represent "stray" keys that have no logical chain/path found + and are likely unusable for purposes of this environment. + */ + UnmatchedKeys []crypto.PrivateKey + // DhParams represent any found DH parameters. This will usually be empty. + DhParams []*dhparam.DH } type TlsUri struct { diff --git a/go.mod b/go.mod index 7fbbe59..b98a8e7 100644 --- a/go.mod +++ b/go.mod @@ -3,6 +3,7 @@ module r00t2.io/sysutils go 1.21 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=