diff --git a/.idea/.gitignore b/.idea/.gitignore new file mode 100644 index 0000000..13566b8 --- /dev/null +++ b/.idea/.gitignore @@ -0,0 +1,8 @@ +# Default ignored files +/shelf/ +/workspace.xml +# Editor-based HTTP Client requests +/httpRequests/ +# Datasource local storage ignored files +/dataSources/ +/dataSources.local.xml diff --git a/.idea/goutils.iml b/.idea/goutils.iml new file mode 100644 index 0000000..5e764c4 --- /dev/null +++ b/.idea/goutils.iml @@ -0,0 +1,9 @@ + + + + + + + + + \ No newline at end of file diff --git a/.idea/modules.xml b/.idea/modules.xml new file mode 100644 index 0000000..aa5694d --- /dev/null +++ b/.idea/modules.xml @@ -0,0 +1,8 @@ + + + + + + + + \ No newline at end of file diff --git a/.idea/vcs.xml b/.idea/vcs.xml new file mode 100644 index 0000000..94a25f7 --- /dev/null +++ b/.idea/vcs.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/certlib/BUILD.bazel b/certlib/BUILD.bazel new file mode 100644 index 0000000..9371ce9 --- /dev/null +++ b/certlib/BUILD.bazel @@ -0,0 +1,31 @@ +load("@io_bazel_rules_go//go:def.bzl", "go_library", "go_test") + +go_library( + name = "certlib", + srcs = [ + "certlib.go", + "der_helpers.go", + "ed25519.go", + "errors.go", + "helpers.go", + ], + importpath = "git.wntrmute.dev/kyle/goutils/certlib", + visibility = ["//visibility:public"], + deps = [ + "@com_github_cloudflare_cfssl//crypto/pkcs7", + "@com_github_cloudflare_cfssl//helpers/derhelpers", + "@com_github_google_certificate_transparency_go//:certificate-transparency-go", + "@com_github_google_certificate_transparency_go//tls", + "@com_github_google_certificate_transparency_go//x509", + "@org_golang_x_crypto//ocsp", + "@org_golang_x_crypto//pkcs12", + ], +) + +go_test( + name = "certlib_test", + srcs = ["certlib_test.go"], + embed = [":certlib"], + deps = ["//assert"], + size = "small", +) diff --git a/certlib/certerr/errors.go b/certlib/certerr/errors.go new file mode 100644 index 0000000..865f98d --- /dev/null +++ b/certlib/certerr/errors.go @@ -0,0 +1,79 @@ +package certerr + +import ( + "errors" + "fmt" + "strings" +) + +// ErrEmptyCertificate indicates that a certificate could not be processed +// because there was no data to process. +var ErrEmptyCertificate = errors.New("certlib: empty certificate") + +type ErrorSourceType uint8 + +func (t ErrorSourceType) String() string { + switch t { + case ErrorSourceCertificate: + return "certificate" + case ErrorSourcePrivateKey: + return "private key" + case ErrorSourceCSR: + return "CSR" + case ErrorSourceSCTList: + return "SCT list" + case ErrorSourceKeypair: + return "TLS keypair" + default: + panic(fmt.Sprintf("unknown error source %d", t)) + } +} + +const ( + ErrorSourceCertificate ErrorSourceType = 1 + ErrorSourcePrivateKey ErrorSourceType = 2 + ErrorSourceCSR ErrorSourceType = 3 + ErrorSourceSCTList ErrorSourceType = 4 + ErrorSourceKeypair ErrorSourceType = 5 +) + +// InvalidPEMType is used to indicate that we were expecting one type of PEM +// file, but saw another. +type InvalidPEMType struct { + have string + want []string +} + +func (err *InvalidPEMType) Error() string { + if len(err.want) == 1 { + return fmt.Sprintf("invalid PEM type: have %s, expected %s", err.have, err.want[0]) + } else { + return fmt.Sprintf("invalid PEM type: have %s, expected one of %s", err.have, strings.Join(err.want, ", ")) + } +} + +// ErrInvalidPEMType returns a new InvalidPEMType error. +func ErrInvalidPEMType(have string, want ...string) error { + return &InvalidPEMType{ + have: have, + want: want, + } +} + +func LoadingError(t ErrorSourceType, err error) error { + return fmt.Errorf("failed to load %s from disk: %w", t, err) +} + +func ParsingError(t ErrorSourceType, err error) error { + return fmt.Errorf("failed to parse %s: %w", t, err) +} + +func DecodeError(t ErrorSourceType, err error) error { + return fmt.Errorf("failed to decode %s: %w", t, err) +} + +func VerifyError(t ErrorSourceType, err error) error { + return fmt.Errorf("failed to verify %s: %w", t, err) +} + +var ErrEncryptedPrivateKey = errors.New("private key is encrypted") diff --git a/certlib/certlib.go b/certlib/certlib.go new file mode 100644 index 0000000..72c9f15 --- /dev/null +++ b/certlib/certlib.go @@ -0,0 +1,85 @@ +package certlib + +import ( + "crypto/x509" + "encoding/pem" + "errors" + "io/ioutil" + + "git.wntrmute.dev/kyle/goutils/certlib/certerr" +) + +// ReadCertificate reads a DER or PEM-encoded certificate from the +// byte slice. +func ReadCertificate(in []byte) (cert *x509.Certificate, rest []byte, err error) { + if len(in) == 0 { + err = certerr.ErrEmptyCertificate + return + } + + if in[0] == '-' { + p, remaining := pem.Decode(in) + if p == nil { + err = errors.New("certlib: invalid PEM file") + return + } + + rest = remaining + if p.Type != "CERTIFICATE" { + err = certerr.ErrInvalidPEMType(p.Type, "CERTIFICATE") + return + } + + in = p.Bytes + } + + cert, err = x509.ParseCertificate(in) + return +} + +// ReadCertificates tries to read all the certificates in a +// PEM-encoded collection. +func ReadCertificates(in []byte) (certs []*x509.Certificate, err error) { + var cert *x509.Certificate + for { + cert, in, err = ReadCertificate(in) + if err != nil { + break + } + + if cert == nil { + break + } + + certs = append(certs, cert) + if len(in) == 0 { + break + } + } + + return certs, err +} + +// LoadCertificate tries to read a single certificate from disk. If +// the file contains multiple certificates (e.g. a chain), only the +// first certificate is returned. +func LoadCertificate(path string) (*x509.Certificate, error) { + in, err := ioutil.ReadFile(path) + if err != nil { + return nil, err + } + + cert, _, err := ReadCertificate(in) + return cert, err +} + +// LoadCertificates tries to read all the certificates in a file, +// returning them in the order that it found them in the file. +func LoadCertificates(path string) ([]*x509.Certificate, error) { + in, err := ioutil.ReadFile(path) + if err != nil { + return nil, err + } + + return ReadCertificates(in) +} diff --git a/lib/lib_test.go b/certlib/certlib_test.go similarity index 99% rename from lib/lib_test.go rename to certlib/certlib_test.go index 8fc1015..fffd83d 100644 --- a/lib/lib_test.go +++ b/certlib/certlib_test.go @@ -1,4 +1,4 @@ -package lib +package certlib import ( "fmt" diff --git a/certlib/der_helpers.go b/certlib/der_helpers.go new file mode 100644 index 0000000..1a54351 --- /dev/null +++ b/certlib/der_helpers.go @@ -0,0 +1,75 @@ +package certlib + +// Originally from CFSSL, mostly written by me originally, and licensed under: + +/* +Copyright (c) 2014 CloudFlare Inc. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions +are met: + +Redistributions of source code must retain the above copyright notice, +this list of conditions and the following disclaimer. + +Redistributions in binary form must reproduce the above copyright notice, +this list of conditions and the following disclaimer in the documentation +and/or other materials provided with the distribution. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED +TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR +PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF +LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING +NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +*/ + +// I've modified it for use in my own code e.g. by removing the CFSSL errors +// and replacing them with sane ones. + +import ( + "crypto" + "crypto/ecdsa" + "crypto/ed25519" + "crypto/rsa" + "crypto/x509" + "fmt" + + "git.wntrmute.dev/kyle/goutils/certlib/certerr" +) + +// ParsePrivateKeyDER parses a PKCS #1, PKCS #8, ECDSA, or Ed25519 DER-encoded +// private key. The key must not be in PEM format. If an error is returned, it +// may contain information about the private key, so care should be taken when +// displaying it directly. +func ParsePrivateKeyDER(keyDER []byte) (key crypto.Signer, err error) { + generalKey, err := x509.ParsePKCS8PrivateKey(keyDER) + if err != nil { + generalKey, err = x509.ParsePKCS1PrivateKey(keyDER) + if err != nil { + generalKey, err = x509.ParseECPrivateKey(keyDER) + if err != nil { + generalKey, err = ParseEd25519PrivateKey(keyDER) + if err != nil { + return nil, certerr.ParsingError(certerr.ErrorSourcePrivateKey, err) + } + } + } + } + + switch generalKey := generalKey.(type) { + case *rsa.PrivateKey: + return generalKey, nil + case *ecdsa.PrivateKey: + return generalKey, nil + case ed25519.PrivateKey: + return generalKey, nil + default: + return nil, certerr.ParsingError(certerr.ErrorSourcePrivateKey, fmt.Errorf("unknown key type %t", generalKey)) + } +} diff --git a/certlib/ed25519.go b/certlib/ed25519.go new file mode 100644 index 0000000..ac58b8b --- /dev/null +++ b/certlib/ed25519.go @@ -0,0 +1,164 @@ +package certlib + +import ( + "crypto" + "crypto/ed25519" + "crypto/x509/pkix" + "encoding/asn1" + "errors" +) + +// Originally from CFSSL, mostly written by me originally, and licensed under: + +/* +Copyright (c) 2014 CloudFlare Inc. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions +are met: + +Redistributions of source code must retain the above copyright notice, +this list of conditions and the following disclaimer. + +Redistributions in binary form must reproduce the above copyright notice, +this list of conditions and the following disclaimer in the documentation +and/or other materials provided with the distribution. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED +TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR +PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF +LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING +NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +*/ + +// I've modified it for use in my own code e.g. by removing the CFSSL errors +// and replacing them with sane ones. + +var errEd25519WrongID = errors.New("incorrect object identifier") +var errEd25519WrongKeyType = errors.New("incorrect key type") + +// ed25519OID is the OID for the Ed25519 signature scheme: see +// https://datatracker.ietf.org/doc/draft-ietf-curdle-pkix-04. +var ed25519OID = asn1.ObjectIdentifier{1, 3, 101, 112} + +// subjectPublicKeyInfo reflects the ASN.1 object defined in the X.509 standard. +// +// This is defined in crypto/x509 as "publicKeyInfo". +type subjectPublicKeyInfo struct { + Algorithm pkix.AlgorithmIdentifier + PublicKey asn1.BitString +} + +// MarshalEd25519PublicKey creates a DER-encoded SubjectPublicKeyInfo for an +// ed25519 public key, as defined in +// https://tools.ietf.org/html/draft-ietf-curdle-pkix-04. This is analogous to +// MarshalPKIXPublicKey in crypto/x509, which doesn't currently support Ed25519. +func MarshalEd25519PublicKey(pk crypto.PublicKey) ([]byte, error) { + pub, ok := pk.(ed25519.PublicKey) + if !ok { + return nil, errEd25519WrongKeyType + } + + spki := subjectPublicKeyInfo{ + Algorithm: pkix.AlgorithmIdentifier{ + Algorithm: ed25519OID, + }, + PublicKey: asn1.BitString{ + BitLength: len(pub) * 8, + Bytes: pub, + }, + } + + return asn1.Marshal(spki) +} + +// ParseEd25519PublicKey returns the Ed25519 public key encoded by the input. +func ParseEd25519PublicKey(der []byte) (crypto.PublicKey, error) { + var spki subjectPublicKeyInfo + if rest, err := asn1.Unmarshal(der, &spki); err != nil { + return nil, err + } else if len(rest) > 0 { + return nil, errors.New("SubjectPublicKeyInfo too long") + } + + if !spki.Algorithm.Algorithm.Equal(ed25519OID) { + return nil, errEd25519WrongID + } + + if spki.PublicKey.BitLength != ed25519.PublicKeySize*8 { + return nil, errors.New("SubjectPublicKeyInfo PublicKey length mismatch") + } + + return ed25519.PublicKey(spki.PublicKey.Bytes), nil +} + +// oneAsymmetricKey reflects the ASN.1 structure for storing private keys in +// https://tools.ietf.org/html/draft-ietf-curdle-pkix-04, excluding the optional +// fields, which we don't use here. +// +// This is identical to pkcs8 in crypto/x509. +type oneAsymmetricKey struct { + Version int + Algorithm pkix.AlgorithmIdentifier + PrivateKey []byte +} + +// curvePrivateKey is the innter type of the PrivateKey field of +// oneAsymmetricKey. +type curvePrivateKey []byte + +// MarshalEd25519PrivateKey returns a DER encoding of the input private key as +// specified in https://tools.ietf.org/html/draft-ietf-curdle-pkix-04. +func MarshalEd25519PrivateKey(sk crypto.PrivateKey) ([]byte, error) { + priv, ok := sk.(ed25519.PrivateKey) + if !ok { + return nil, errEd25519WrongKeyType + } + + // Marshal the innter CurvePrivateKey. + curvePrivateKey, err := asn1.Marshal(priv.Seed()) + if err != nil { + return nil, err + } + + // Marshal the OneAsymmetricKey. + asym := oneAsymmetricKey{ + Version: 0, + Algorithm: pkix.AlgorithmIdentifier{ + Algorithm: ed25519OID, + }, + PrivateKey: curvePrivateKey, + } + return asn1.Marshal(asym) +} + +// ParseEd25519PrivateKey returns the Ed25519 private key encoded by the input. +func ParseEd25519PrivateKey(der []byte) (crypto.PrivateKey, error) { + asym := new(oneAsymmetricKey) + if rest, err := asn1.Unmarshal(der, asym); err != nil { + return nil, err + } else if len(rest) > 0 { + return nil, errors.New("OneAsymmetricKey too long") + } + + // Check that the key type is correct. + if !asym.Algorithm.Algorithm.Equal(ed25519OID) { + return nil, errEd25519WrongID + } + + // Unmarshal the inner CurvePrivateKey. + seed := new(curvePrivateKey) + if rest, err := asn1.Unmarshal(asym.PrivateKey, seed); err != nil { + return nil, err + } else if len(rest) > 0 { + return nil, errors.New("CurvePrivateKey too long") + } + + return ed25519.NewKeyFromSeed(*seed), nil +} diff --git a/certlib/helpers.go b/certlib/helpers.go new file mode 100644 index 0000000..5458244 --- /dev/null +++ b/certlib/helpers.go @@ -0,0 +1,630 @@ +package certlib + +// Originally from CFSSL, mostly written by me originally, and licensed under: + +/* +Copyright (c) 2014 CloudFlare Inc. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions +are met: + +Redistributions of source code must retain the above copyright notice, +this list of conditions and the following disclaimer. + +Redistributions in binary form must reproduce the above copyright notice, +this list of conditions and the following disclaimer in the documentation +and/or other materials provided with the distribution. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED +TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR +PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF +LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING +NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +*/ + +// I've modified it for use in my own code e.g. by removing the CFSSL errors +// and replacing them with sane ones. + +import ( + "bytes" + "crypto" + "crypto/ecdsa" + "crypto/elliptic" + "crypto/rsa" + "crypto/tls" + "crypto/x509" + "crypto/x509/pkix" + "encoding/asn1" + "encoding/pem" + "errors" + "fmt" + "os" + "strings" + "time" + + "git.wntrmute.dev/kyle/goutils/certlib/certerr" + "git.wntrmute.dev/kyle/goutils/certlib/pkcs7" + + ct "github.com/google/certificate-transparency-go" + cttls "github.com/google/certificate-transparency-go/tls" + ctx509 "github.com/google/certificate-transparency-go/x509" + "golang.org/x/crypto/ocsp" + "golang.org/x/crypto/pkcs12" +) + +// OneYear is a time.Duration representing a year's worth of seconds. +const OneYear = 8760 * time.Hour + +// OneDay is a time.Duration representing a day's worth of seconds. +const OneDay = 24 * time.Hour + +// DelegationUsage is the OID for the DelegationUseage extensions +var DelegationUsage = asn1.ObjectIdentifier{1, 3, 6, 1, 4, 1, 44363, 44} + +// DelegationExtension +var DelegationExtension = pkix.Extension{ + Id: DelegationUsage, + Critical: false, + Value: []byte{0x05, 0x00}, // ASN.1 NULL +} + +// InclusiveDate returns the time.Time representation of a date - 1 +// nanosecond. This allows time.After to be used inclusively. +func InclusiveDate(year int, month time.Month, day int) time.Time { + return time.Date(year, month, day, 0, 0, 0, 0, time.UTC).Add(-1 * time.Nanosecond) +} + +// Jul2012 is the July 2012 CAB Forum deadline for when CAs must stop +// issuing certificates valid for more than 5 years. +var Jul2012 = InclusiveDate(2012, time.July, 01) + +// Apr2015 is the April 2015 CAB Forum deadline for when CAs must stop +// issuing certificates valid for more than 39 months. +var Apr2015 = InclusiveDate(2015, time.April, 01) + +// KeyLength returns the bit size of ECDSA or RSA PublicKey +func KeyLength(key interface{}) int { + if key == nil { + return 0 + } + if ecdsaKey, ok := key.(*ecdsa.PublicKey); ok { + return ecdsaKey.Curve.Params().BitSize + } else if rsaKey, ok := key.(*rsa.PublicKey); ok { + return rsaKey.N.BitLen() + } + + return 0 +} + +// ExpiryTime returns the time when the certificate chain is expired. +func ExpiryTime(chain []*x509.Certificate) (notAfter time.Time) { + if len(chain) == 0 { + return + } + + notAfter = chain[0].NotAfter + for _, cert := range chain { + if notAfter.After(cert.NotAfter) { + notAfter = cert.NotAfter + } + } + return +} + +// MonthsValid returns the number of months for which a certificate is valid. +func MonthsValid(c *x509.Certificate) int { + issued := c.NotBefore + expiry := c.NotAfter + years := (expiry.Year() - issued.Year()) + months := years*12 + int(expiry.Month()) - int(issued.Month()) + + // Round up if valid for less than a full month + if expiry.Day() > issued.Day() { + months++ + } + return months +} + +// ValidExpiry determines if a certificate is valid for an acceptable +// length of time per the CA/Browser Forum baseline requirements. +// See https://cabforum.org/wp-content/uploads/CAB-Forum-BR-1.3.0.pdf +func ValidExpiry(c *x509.Certificate) bool { + issued := c.NotBefore + + var maxMonths int + switch { + case issued.After(Apr2015): + maxMonths = 39 + case issued.After(Jul2012): + maxMonths = 60 + case issued.Before(Jul2012): + maxMonths = 120 + } + + if MonthsValid(c) > maxMonths { + return false + } + return true +} + +// SignatureString returns the TLS signature string corresponding to +// an X509 signature algorithm. +func SignatureString(alg x509.SignatureAlgorithm) string { + switch alg { + case x509.MD2WithRSA: + return "MD2WithRSA" + case x509.MD5WithRSA: + return "MD5WithRSA" + case x509.SHA1WithRSA: + return "SHA1WithRSA" + case x509.SHA256WithRSA: + return "SHA256WithRSA" + case x509.SHA384WithRSA: + return "SHA384WithRSA" + case x509.SHA512WithRSA: + return "SHA512WithRSA" + case x509.DSAWithSHA1: + return "DSAWithSHA1" + case x509.DSAWithSHA256: + return "DSAWithSHA256" + case x509.ECDSAWithSHA1: + return "ECDSAWithSHA1" + case x509.ECDSAWithSHA256: + return "ECDSAWithSHA256" + case x509.ECDSAWithSHA384: + return "ECDSAWithSHA384" + case x509.ECDSAWithSHA512: + return "ECDSAWithSHA512" + default: + return "Unknown Signature" + } +} + +// HashAlgoString returns the hash algorithm name contains in the signature +// method. +func HashAlgoString(alg x509.SignatureAlgorithm) string { + switch alg { + case x509.MD2WithRSA: + return "MD2" + case x509.MD5WithRSA: + return "MD5" + case x509.SHA1WithRSA: + return "SHA1" + case x509.SHA256WithRSA: + return "SHA256" + case x509.SHA384WithRSA: + return "SHA384" + case x509.SHA512WithRSA: + return "SHA512" + case x509.DSAWithSHA1: + return "SHA1" + case x509.DSAWithSHA256: + return "SHA256" + case x509.ECDSAWithSHA1: + return "SHA1" + case x509.ECDSAWithSHA256: + return "SHA256" + case x509.ECDSAWithSHA384: + return "SHA384" + case x509.ECDSAWithSHA512: + return "SHA512" + default: + return "Unknown Hash Algorithm" + } +} + +// StringTLSVersion returns underlying enum values from human names for TLS +// versions, defaults to current golang default of TLS 1.0 +func StringTLSVersion(version string) uint16 { + switch version { + case "1.2": + return tls.VersionTLS12 + case "1.1": + return tls.VersionTLS11 + default: + return tls.VersionTLS10 + } +} + +// EncodeCertificatesPEM encodes a number of x509 certificates to PEM +func EncodeCertificatesPEM(certs []*x509.Certificate) []byte { + var buffer bytes.Buffer + for _, cert := range certs { + pem.Encode(&buffer, &pem.Block{ + Type: "CERTIFICATE", + Bytes: cert.Raw, + }) + } + + return buffer.Bytes() +} + +// EncodeCertificatePEM encodes a single x509 certificates to PEM +func EncodeCertificatePEM(cert *x509.Certificate) []byte { + return EncodeCertificatesPEM([]*x509.Certificate{cert}) +} + +// ParseCertificatesPEM parses a sequence of PEM-encoded certificate and returns them, +// can handle PEM encoded PKCS #7 structures. +func ParseCertificatesPEM(certsPEM []byte) ([]*x509.Certificate, error) { + var certs []*x509.Certificate + var err error + certsPEM = bytes.TrimSpace(certsPEM) + for len(certsPEM) > 0 { + var cert []*x509.Certificate + cert, certsPEM, err = ParseOneCertificateFromPEM(certsPEM) + if err != nil { + return nil, certerr.ParsingError(certerr.ErrorSourceCertificate, err) + } else if cert == nil { + break + } + + certs = append(certs, cert...) + } + if len(certsPEM) > 0 { + return nil, certerr.DecodeError(certerr.ErrorSourceCertificate, errors.New("trailing data at end of certificate")) + } + return certs, nil +} + +// ParseCertificatesDER parses a DER encoding of a certificate object and possibly private key, +// either PKCS #7, PKCS #12, or raw x509. +func ParseCertificatesDER(certsDER []byte, password string) (certs []*x509.Certificate, key crypto.Signer, err error) { + certsDER = bytes.TrimSpace(certsDER) + pkcs7data, err := pkcs7.ParsePKCS7(certsDER) + if err != nil { + var pkcs12data interface{} + certs = make([]*x509.Certificate, 1) + pkcs12data, certs[0], err = pkcs12.Decode(certsDER, password) + if err != nil { + certs, err = x509.ParseCertificates(certsDER) + if err != nil { + return nil, nil, certerr.DecodeError(certerr.ErrorSourceCertificate, err) + } + } else { + key = pkcs12data.(crypto.Signer) + } + } else { + if pkcs7data.ContentInfo != "SignedData" { + return nil, nil, certerr.DecodeError(certerr.ErrorSourceCertificate, errors.New("can only extract certificates from signed data content info")) + } + certs = pkcs7data.Content.SignedData.Certificates + } + if certs == nil { + return nil, key, certerr.DecodeError(certerr.ErrorSourceCertificate, errors.New("no certificates decoded")) + } + return certs, key, nil +} + +// ParseSelfSignedCertificatePEM parses a PEM-encoded certificate and check if it is self-signed. +func ParseSelfSignedCertificatePEM(certPEM []byte) (*x509.Certificate, error) { + cert, err := ParseCertificatePEM(certPEM) + if err != nil { + return nil, err + } + + if err := cert.CheckSignature(cert.SignatureAlgorithm, cert.RawTBSCertificate, cert.Signature); err != nil { + return nil, certerr.VerifyError(certerr.ErrorSourceCertificate, err) + } + return cert, nil +} + +// ParseCertificatePEM parses and returns a PEM-encoded certificate, +// can handle PEM encoded PKCS #7 structures. +func ParseCertificatePEM(certPEM []byte) (*x509.Certificate, error) { + certPEM = bytes.TrimSpace(certPEM) + cert, rest, err := ParseOneCertificateFromPEM(certPEM) + if err != nil { + return nil, certerr.ParsingError(certerr.ErrorSourceCertificate, err) + } else if cert == nil { + return nil, certerr.DecodeError(certerr.ErrorSourceCertificate, errors.New("no certificate decoded")) + } else if len(rest) > 0 { + return nil, certerr.ParsingError(certerr.ErrorSourceCertificate, errors.New("the PEM file should contain only one object")) + } else if len(cert) > 1 { + return nil, certerr.ParsingError(certerr.ErrorSourceCertificate, errors.New("the PKCS7 object in the PEM file should contain only one certificate")) + } + return cert[0], nil +} + +// ParseOneCertificateFromPEM attempts to parse one PEM encoded certificate object, +// either a raw x509 certificate or a PKCS #7 structure possibly containing +// multiple certificates, from the top of certsPEM, which itself may +// contain multiple PEM encoded certificate objects. +func ParseOneCertificateFromPEM(certsPEM []byte) ([]*x509.Certificate, []byte, error) { + + block, rest := pem.Decode(certsPEM) + if block == nil { + return nil, rest, nil + } + + cert, err := x509.ParseCertificate(block.Bytes) + if err != nil { + pkcs7data, err := pkcs7.ParsePKCS7(block.Bytes) + if err != nil { + return nil, rest, err + } + if pkcs7data.ContentInfo != "SignedData" { + return nil, rest, errors.New("only PKCS #7 Signed Data Content Info supported for certificate parsing") + } + certs := pkcs7data.Content.SignedData.Certificates + if certs == nil { + return nil, rest, errors.New("PKCS #7 structure contains no certificates") + } + return certs, rest, nil + } + var certs = []*x509.Certificate{cert} + return certs, rest, nil +} + +// LoadPEMCertPool loads a pool of PEM certificates from file. +func LoadPEMCertPool(certsFile string) (*x509.CertPool, error) { + if certsFile == "" { + return nil, nil + } + pemCerts, err := os.ReadFile(certsFile) + if err != nil { + return nil, err + } + + return PEMToCertPool(pemCerts) +} + +// PEMToCertPool concerts PEM certificates to a CertPool. +func PEMToCertPool(pemCerts []byte) (*x509.CertPool, error) { + if len(pemCerts) == 0 { + return nil, nil + } + + certPool := x509.NewCertPool() + if !certPool.AppendCertsFromPEM(pemCerts) { + return nil, errors.New("failed to load cert pool") + } + + return certPool, nil +} + +// ParsePrivateKeyPEM parses and returns a PEM-encoded private +// key. The private key may be either an unencrypted PKCS#8, PKCS#1, +// or elliptic private key. +func ParsePrivateKeyPEM(keyPEM []byte) (key crypto.Signer, err error) { + return ParsePrivateKeyPEMWithPassword(keyPEM, nil) +} + +// ParsePrivateKeyPEMWithPassword parses and returns a PEM-encoded private +// key. The private key may be a potentially encrypted PKCS#8, PKCS#1, +// or elliptic private key. +func ParsePrivateKeyPEMWithPassword(keyPEM []byte, password []byte) (key crypto.Signer, err error) { + keyDER, err := GetKeyDERFromPEM(keyPEM, password) + if err != nil { + return nil, err + } + + return ParsePrivateKeyDER(keyDER) +} + +// GetKeyDERFromPEM parses a PEM-encoded private key and returns DER-format key bytes. +func GetKeyDERFromPEM(in []byte, password []byte) ([]byte, error) { + // Ignore any EC PARAMETERS blocks when looking for a key (openssl includes + // them by default). + var keyDER *pem.Block + for { + keyDER, in = pem.Decode(in) + if keyDER == nil || keyDER.Type != "EC PARAMETERS" { + break + } + } + if keyDER != nil { + if procType, ok := keyDER.Headers["Proc-Type"]; ok { + if strings.Contains(procType, "ENCRYPTED") { + if password != nil { + return x509.DecryptPEMBlock(keyDER, password) + } + return nil, certerr.DecodeError(certerr.ErrorSourcePrivateKey, certerr.ErrEncryptedPrivateKey) + } + } + return keyDER.Bytes, nil + } + + return nil, certerr.DecodeError(certerr.ErrorSourcePrivateKey, errors.New("failed to decode private key")) +} + +// ParseCSR parses a PEM- or DER-encoded PKCS #10 certificate signing request. +func ParseCSR(in []byte) (csr *x509.CertificateRequest, rest []byte, err error) { + in = bytes.TrimSpace(in) + p, rest := pem.Decode(in) + if p != nil { + if p.Type != "NEW CERTIFICATE REQUEST" && p.Type != "CERTIFICATE REQUEST" { + return nil, rest, certerr.ParsingError(certerr.ErrorSourceCSR, certerr.ErrInvalidPEMType(p.Type, "NEW CERTIFICATE REQUEST", "CERTIFICATE REQUEST")) + } + + csr, err = x509.ParseCertificateRequest(p.Bytes) + } else { + csr, err = x509.ParseCertificateRequest(in) + } + + if err != nil { + return nil, rest, err + } + + err = csr.CheckSignature() + if err != nil { + return nil, rest, err + } + + return csr, rest, nil +} + +// ParseCSRPEM parses a PEM-encoded certificate signing request. +// It does not check the signature. This is useful for dumping data from a CSR +// locally. +func ParseCSRPEM(csrPEM []byte) (*x509.CertificateRequest, error) { + block, _ := pem.Decode([]byte(csrPEM)) + if block == nil { + return nil, certerr.DecodeError(certerr.ErrorSourceCSR, errors.New("PEM block is empty")) + } + csrObject, err := x509.ParseCertificateRequest(block.Bytes) + + if err != nil { + return nil, err + } + + return csrObject, nil +} + +// SignerAlgo returns an X.509 signature algorithm from a crypto.Signer. +func SignerAlgo(priv crypto.Signer) x509.SignatureAlgorithm { + switch pub := priv.Public().(type) { + case *rsa.PublicKey: + bitLength := pub.N.BitLen() + switch { + case bitLength >= 4096: + return x509.SHA512WithRSA + case bitLength >= 3072: + return x509.SHA384WithRSA + case bitLength >= 2048: + return x509.SHA256WithRSA + default: + return x509.SHA1WithRSA + } + case *ecdsa.PublicKey: + switch pub.Curve { + case elliptic.P521(): + return x509.ECDSAWithSHA512 + case elliptic.P384(): + return x509.ECDSAWithSHA384 + case elliptic.P256(): + return x509.ECDSAWithSHA256 + default: + return x509.ECDSAWithSHA1 + } + default: + return x509.UnknownSignatureAlgorithm + } +} + +// LoadClientCertificate load key/certificate from pem files +func LoadClientCertificate(certFile string, keyFile string) (*tls.Certificate, error) { + if certFile != "" && keyFile != "" { + cert, err := tls.LoadX509KeyPair(certFile, keyFile) + if err != nil { + return nil, certerr.LoadingError(certerr.ErrorSourceKeypair, err) + } + return &cert, nil + } + return nil, nil +} + +// CreateTLSConfig creates a tls.Config object from certs and roots +func CreateTLSConfig(remoteCAs *x509.CertPool, cert *tls.Certificate) *tls.Config { + var certs []tls.Certificate + if cert != nil { + certs = []tls.Certificate{*cert} + } + return &tls.Config{ + Certificates: certs, + RootCAs: remoteCAs, + } +} + +// SerializeSCTList serializes a list of SCTs. +func SerializeSCTList(sctList []ct.SignedCertificateTimestamp) ([]byte, error) { + list := ctx509.SignedCertificateTimestampList{} + for _, sct := range sctList { + sctBytes, err := cttls.Marshal(sct) + if err != nil { + return nil, err + } + list.SCTList = append(list.SCTList, ctx509.SerializedSCT{Val: sctBytes}) + } + return cttls.Marshal(list) +} + +// DeserializeSCTList deserializes a list of SCTs. +func DeserializeSCTList(serializedSCTList []byte) ([]ct.SignedCertificateTimestamp, error) { + var sctList ctx509.SignedCertificateTimestampList + rest, err := cttls.Unmarshal(serializedSCTList, &sctList) + if err != nil { + return nil, err + } + if len(rest) != 0 { + return nil, certerr.ParsingError(certerr.ErrorSourceSCTList, errors.New("serialized SCT list contained trailing garbage")) + } + + list := make([]ct.SignedCertificateTimestamp, len(sctList.SCTList)) + for i, serializedSCT := range sctList.SCTList { + var sct ct.SignedCertificateTimestamp + rest, err := cttls.Unmarshal(serializedSCT.Val, &sct) + if err != nil { + return nil, err + } + if len(rest) != 0 { + return nil, certerr.ParsingError(certerr.ErrorSourceSCTList, errors.New("serialized SCT list contained trailing garbage")) + } + list[i] = sct + } + return list, nil +} + +// SCTListFromOCSPResponse extracts the SCTList from an ocsp.Response, +// returning an empty list if the SCT extension was not found or could not be +// unmarshalled. +func SCTListFromOCSPResponse(response *ocsp.Response) ([]ct.SignedCertificateTimestamp, error) { + // This loop finds the SCTListExtension in the OCSP response. + var SCTListExtension, ext pkix.Extension + for _, ext = range response.Extensions { + // sctExtOid is the ObjectIdentifier of a Signed Certificate Timestamp. + sctExtOid := asn1.ObjectIdentifier{1, 3, 6, 1, 4, 1, 11129, 2, 4, 5} + if ext.Id.Equal(sctExtOid) { + SCTListExtension = ext + break + } + } + + // This code block extracts the sctList from the SCT extension. + var sctList []ct.SignedCertificateTimestamp + var err error + if numBytes := len(SCTListExtension.Value); numBytes != 0 { + var serializedSCTList []byte + rest := make([]byte, numBytes) + copy(rest, SCTListExtension.Value) + for len(rest) != 0 { + rest, err = asn1.Unmarshal(rest, &serializedSCTList) + if err != nil { + return nil, certerr.ParsingError(certerr.ErrorSourceSCTList, err) + } + } + sctList, err = DeserializeSCTList(serializedSCTList) + } + return sctList, err +} + +// ReadBytes reads a []byte either from a file or an environment variable. +// If valFile has a prefix of 'env:', the []byte is read from the environment +// using the subsequent name. If the prefix is 'file:' the []byte is read from +// the subsequent file. If no prefix is provided, valFile is assumed to be a +// file path. +func ReadBytes(valFile string) ([]byte, error) { + switch splitVal := strings.SplitN(valFile, ":", 2); len(splitVal) { + case 1: + return os.ReadFile(valFile) + case 2: + switch splitVal[0] { + case "env": + return []byte(os.Getenv(splitVal[1])), nil + case "file": + return os.ReadFile(splitVal[1]) + default: + return nil, fmt.Errorf("unknown prefix: %s", splitVal[0]) + } + default: + return nil, fmt.Errorf("multiple prefixes: %s", + strings.Join(splitVal[:len(splitVal)-1], ", ")) + } +} diff --git a/certlib/pkcs7/pkcs7.go b/certlib/pkcs7/pkcs7.go new file mode 100644 index 0000000..bfea534 --- /dev/null +++ b/certlib/pkcs7/pkcs7.go @@ -0,0 +1,220 @@ +package pkcs7 + +// Originally from CFSSL, and licensed under: + +/* +Copyright (c) 2014 CloudFlare Inc. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions +are met: + +Redistributions of source code must retain the above copyright notice, +this list of conditions and the following disclaimer. + +Redistributions in binary form must reproduce the above copyright notice, +this list of conditions and the following disclaimer in the documentation +and/or other materials provided with the distribution. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED +TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR +PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF +LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING +NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +*/ + +// I've modified it for use in my own code e.g. by removing the CFSSL errors +// and replacing them with sane ones. + +// Package pkcs7 implements the subset of the CMS PKCS #7 datatype that is typically +// used to package certificates and CRLs. Using openssl, every certificate converted +// to PKCS #7 format from another encoding such as PEM conforms to this implementation. +// reference: https://www.openssl.org/docs/man1.1.0/apps/crl2pkcs7.html +// +// PKCS #7 Data type, reference: https://tools.ietf.org/html/rfc2315 +// +// The full pkcs#7 cryptographic message syntax allows for cryptographic enhancements, +// for example data can be encrypted and signed and then packaged through pkcs#7 to be +// sent over a network and then verified and decrypted. It is asn1, and the type of +// PKCS #7 ContentInfo, which comprises the PKCS #7 structure, is: +// +// ContentInfo ::= SEQUENCE { +// contentType ContentType, +// content [0] EXPLICIT ANY DEFINED BY contentType OPTIONAL +// } +// +// There are 6 possible ContentTypes, data, signedData, envelopedData, +// signedAndEnvelopedData, digestedData, and encryptedData. Here signedData, Data, and encrypted +// Data are implemented, as the degenerate case of signedData without a signature is the typical +// format for transferring certificates and CRLS, and Data and encryptedData are used in PKCS #12 +// formats. +// The ContentType signedData has the form: +// +// signedData ::= SEQUENCE { +// version Version, +// digestAlgorithms DigestAlgorithmIdentifiers, +// contentInfo ContentInfo, +// certificates [0] IMPLICIT ExtendedCertificatesAndCertificates OPTIONAL +// crls [1] IMPLICIT CertificateRevocationLists OPTIONAL, +// signerInfos SignerInfos +// } +// +// As of yet signerInfos and digestAlgorithms are not parsed, as they are not relevant to +// this system's use of PKCS #7 data. Version is an integer type, note that PKCS #7 is +// recursive, this second layer of ContentInfo is similar ignored for our degenerate +// usage. The ExtendedCertificatesAndCertificates type consists of a sequence of choices +// between PKCS #6 extended certificates and x509 certificates. Any sequence consisting +// of any number of extended certificates is not yet supported in this implementation. +// +// The ContentType Data is simply a raw octet string and is parsed directly into a Go []byte slice. +// +// The ContentType encryptedData is the most complicated and its form can be gathered by +// the go type below. It essentially contains a raw octet string of encrypted data and an +// algorithm identifier for use in decrypting this data. + +import ( + "crypto/x509" + "crypto/x509/pkix" + "encoding/asn1" + "errors" + + "git.wntrmute.dev/kyle/goutils/certlib/certerr" +) + +// Types used for asn1 Unmarshaling. + +type signedData struct { + Version int + DigestAlgorithms asn1.RawValue + ContentInfo asn1.RawValue + Certificates asn1.RawValue `asn1:"optional" asn1:"tag:0"` + Crls asn1.RawValue `asn1:"optional"` + SignerInfos asn1.RawValue +} + +type initPKCS7 struct { + Raw asn1.RawContent + ContentType asn1.ObjectIdentifier + Content asn1.RawValue `asn1:"tag:0,explicit,optional"` +} + +// Object identifier strings of the three implemented PKCS7 types. +const ( + ObjIDData = "1.2.840.113549.1.7.1" + ObjIDSignedData = "1.2.840.113549.1.7.2" + ObjIDEncryptedData = "1.2.840.113549.1.7.6" +) + +// PKCS7 represents the ASN1 PKCS #7 Content type. It contains one of three +// possible types of Content objects, as denoted by the object identifier in +// the ContentInfo field, the other two being nil. SignedData +// is the degenerate SignedData Content info without signature used +// to hold certificates and crls. Data is raw bytes, and EncryptedData +// is as defined in PKCS #7 standard. +type PKCS7 struct { + Raw asn1.RawContent + ContentInfo string + Content Content +} + +// Content implements three of the six possible PKCS7 data types. Only one is non-nil. +type Content struct { + Data []byte + SignedData SignedData + EncryptedData EncryptedData +} + +// SignedData defines the typical carrier of certificates and crls. +type SignedData struct { + Raw asn1.RawContent + Version int + Certificates []*x509.Certificate + Crl *x509.RevocationList +} + +// Data contains raw bytes. Used as a subtype in PKCS12. +type Data struct { + Bytes []byte +} + +// EncryptedData contains encrypted data. Used as a subtype in PKCS12. +type EncryptedData struct { + Raw asn1.RawContent + Version int + EncryptedContentInfo EncryptedContentInfo +} + +// EncryptedContentInfo is a subtype of PKCS7EncryptedData. +type EncryptedContentInfo struct { + Raw asn1.RawContent + ContentType asn1.ObjectIdentifier + ContentEncryptionAlgorithm pkix.AlgorithmIdentifier + EncryptedContent []byte `asn1:"tag:0,optional"` +} + +// ParsePKCS7 attempts to parse the DER encoded bytes of a +// PKCS7 structure. +func ParsePKCS7(raw []byte) (msg *PKCS7, err error) { + + var pkcs7 initPKCS7 + _, err = asn1.Unmarshal(raw, &pkcs7) + if err != nil { + return nil, certerr.ParsingError(certerr.ErrorSourceCertificate, err) + } + + msg = new(PKCS7) + msg.Raw = pkcs7.Raw + msg.ContentInfo = pkcs7.ContentType.String() + switch { + case msg.ContentInfo == ObjIDData: + msg.ContentInfo = "Data" + _, err = asn1.Unmarshal(pkcs7.Content.Bytes, &msg.Content.Data) + if err != nil { + return nil, certerr.ParsingError(certerr.ErrorSourceCertificate, err) + } + case msg.ContentInfo == ObjIDSignedData: + msg.ContentInfo = "SignedData" + var signedData signedData + _, err = asn1.Unmarshal(pkcs7.Content.Bytes, &signedData) + if err != nil { + return nil, certerr.ParsingError(certerr.ErrorSourceCertificate, err) + } + if len(signedData.Certificates.Bytes) != 0 { + msg.Content.SignedData.Certificates, err = x509.ParseCertificates(signedData.Certificates.Bytes) + if err != nil { + return nil, certerr.ParsingError(certerr.ErrorSourceCertificate, err) + } + } + if len(signedData.Crls.Bytes) != 0 { + msg.Content.SignedData.Crl, err = x509.ParseRevocationList(signedData.Crls.Bytes) + if err != nil { + return nil, certerr.ParsingError(certerr.ErrorSourceCertificate, err) + } + } + msg.Content.SignedData.Version = signedData.Version + msg.Content.SignedData.Raw = pkcs7.Content.Bytes + case msg.ContentInfo == ObjIDEncryptedData: + msg.ContentInfo = "EncryptedData" + var encryptedData EncryptedData + _, err = asn1.Unmarshal(pkcs7.Content.Bytes, &encryptedData) + if err != nil { + return nil, certerr.ParsingError(certerr.ErrorSourceCertificate, err) + } + if encryptedData.Version != 0 { + return nil, certerr.ParsingError(certerr.ErrorSourceCertificate, errors.New("only PKCS #7 encryptedData version 0 is supported")) + } + msg.Content.EncryptedData = encryptedData + + default: + return nil, certerr.ParsingError(certerr.ErrorSourceCertificate, errors.New("only PKCS# 7 content of type data, signed data or encrypted data can be parsed")) + } + + return msg, nil + +} diff --git a/certlib/revoke/BUILD.bazel b/certlib/revoke/BUILD.bazel new file mode 100644 index 0000000..d072b92 --- /dev/null +++ b/certlib/revoke/BUILD.bazel @@ -0,0 +1,13 @@ +load("@io_bazel_rules_go//go:def.bzl", "go_library") + +go_library( + name = "revoke", + srcs = ["revoke.go"], + importpath = "git.wntrmute.dev/kyle/goutils/certlib/revoke", + visibility = ["//visibility:public"], + deps = [ + "//certlib", + "@com_github_cloudflare_cfssl//log", + "@org_golang_x_crypto//ocsp", + ], +) diff --git a/certlib/revoke/revoke.go b/certlib/revoke/revoke.go new file mode 100644 index 0000000..707ca56 --- /dev/null +++ b/certlib/revoke/revoke.go @@ -0,0 +1,363 @@ +// Package revoke provides functionality for checking the validity of +// a cert. Specifically, the temporal validity of the certificate is +// checked first, then any CRL and OCSP url in the cert is checked. +package revoke + +import ( + "bytes" + "crypto" + "crypto/x509" + "encoding/base64" + "encoding/pem" + "errors" + "fmt" + "io" + "net/http" + neturl "net/url" + "sync" + "time" + + "git.wntrmute.dev/kyle/goutils/certlib" + "git.wntrmute.dev/kyle/goutils/log" + "golang.org/x/crypto/ocsp" +) + +// Originally from CFSSL, mostly written by me originally, and licensed under: + +/* +Copyright (c) 2014 CloudFlare Inc. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions +are met: + +Redistributions of source code must retain the above copyright notice, +this list of conditions and the following disclaimer. + +Redistributions in binary form must reproduce the above copyright notice, +this list of conditions and the following disclaimer in the documentation +and/or other materials provided with the distribution. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED +TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR +PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF +LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING +NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +*/ + +// I've modified it for use in my own code e.g. by removing the CFSSL errors +// and replacing them with sane ones. + +// HTTPClient is an instance of http.Client that will be used for all HTTP requests. +var HTTPClient = http.DefaultClient + +// HardFail determines whether the failure to check the revocation +// status of a certificate (i.e. due to network failure) causes +// verification to fail (a hard failure). +var HardFail = false + +// CRLSet associates a PKIX certificate list with the URL the CRL is +// fetched from. +var CRLSet = map[string]*x509.RevocationList{} +var crlLock = new(sync.Mutex) + +// We can't handle LDAP certificates, so this checks to see if the +// URL string points to an LDAP resource so that we can ignore it. +func ldapURL(url string) bool { + u, err := neturl.Parse(url) + if err != nil { + log.Warningf("error parsing url %s: %v", url, err) + return false + } + if u.Scheme == "ldap" { + return true + } + return false +} + +// revCheck should check the certificate for any revocations. It +// returns a pair of booleans: the first indicates whether the certificate +// is revoked, the second indicates whether the revocations were +// successfully checked.. This leads to the following combinations: +// +// - false, false: an error was encountered while checking revocations. +// - false, true: the certificate was checked successfully, and it is not revoked. +// - true, true: the certificate was checked successfully, and it is revoked. +// - true, false: failure to check revocation status causes verification to fail +func revCheck(cert *x509.Certificate) (revoked, ok bool, err error) { + for _, url := range cert.CRLDistributionPoints { + if ldapURL(url) { + log.Infof("skipping LDAP CRL: %s", url) + continue + } + + if revoked, ok, err := certIsRevokedCRL(cert, url); !ok { + log.Warning("error checking revocation via CRL") + if HardFail { + return true, false, err + } + return false, false, err + } else if revoked { + log.Info("certificate is revoked via CRL") + return true, true, err + } + } + + if revoked, ok, err := certIsRevokedOCSP(cert, HardFail); !ok { + log.Warning("error checking revocation via OCSP") + if HardFail { + return true, false, err + } + return false, false, err + } else if revoked { + log.Info("certificate is revoked via OCSP") + return true, true, err + } + + return false, true, nil +} + +// fetchCRL fetches and parses a CRL. +func fetchCRL(url string) (*x509.RevocationList, error) { + resp, err := HTTPClient.Get(url) + if err != nil { + return nil, err + } + defer resp.Body.Close() + + if resp.StatusCode >= 300 { + return nil, errors.New("failed to retrieve CRL") + } + + body, err := crlRead(resp.Body) + if err != nil { + return nil, err + } + return x509.ParseRevocationList(body) +} + +func getIssuer(cert *x509.Certificate) *x509.Certificate { + var issuer *x509.Certificate + var err error + for _, issuingCert := range cert.IssuingCertificateURL { + issuer, err = fetchRemote(issuingCert) + if err != nil { + continue + } + break + } + + return issuer + +} + +// check a cert against a specific CRL. Returns the same bool pair +// as revCheck, plus an error if one occurred. +func certIsRevokedCRL(cert *x509.Certificate, url string) (revoked, ok bool, err error) { + crlLock.Lock() + crl, ok := CRLSet[url] + if ok && crl == nil { + ok = false + delete(CRLSet, url) + } + crlLock.Unlock() + + var shouldFetchCRL = true + if ok { + if time.Now().After(crl.ThisUpdate) { + shouldFetchCRL = false + } + } + + issuer := getIssuer(cert) + + if shouldFetchCRL { + var err error + crl, err = fetchCRL(url) + if err != nil { + log.Warningf("failed to fetch CRL: %v", err) + return false, false, err + } + + // check CRL signature + if issuer != nil { + err = crl.CheckSignatureFrom(issuer) + if err != nil { + log.Warningf("failed to verify CRL: %v", err) + return false, false, err + } + } + + crlLock.Lock() + CRLSet[url] = crl + crlLock.Unlock() + } + + for _, revoked := range crl.RevokedCertificates { + if cert.SerialNumber.Cmp(revoked.SerialNumber) == 0 { + log.Info("Serial number match: intermediate is revoked.") + return true, true, err + } + } + + return false, true, err +} + +// VerifyCertificate ensures that the certificate passed in hasn't +// expired and checks the CRL for the server. +func VerifyCertificate(cert *x509.Certificate) (revoked, ok bool) { + revoked, ok, _ = VerifyCertificateError(cert) + return revoked, ok +} + +// VerifyCertificateError ensures that the certificate passed in hasn't +// expired and checks the CRL for the server. +func VerifyCertificateError(cert *x509.Certificate) (revoked, ok bool, err error) { + if !time.Now().Before(cert.NotAfter) { + msg := fmt.Sprintf("Certificate expired %s\n", cert.NotAfter) + log.Info(msg) + return true, true, fmt.Errorf(msg) + } else if !time.Now().After(cert.NotBefore) { + msg := fmt.Sprintf("Certificate isn't valid until %s\n", cert.NotBefore) + log.Info(msg) + return true, true, fmt.Errorf(msg) + } + return revCheck(cert) +} + +func fetchRemote(url string) (*x509.Certificate, error) { + resp, err := HTTPClient.Get(url) + if err != nil { + return nil, err + } + defer resp.Body.Close() + + in, err := remoteRead(resp.Body) + if err != nil { + return nil, err + } + + p, _ := pem.Decode(in) + if p != nil { + return certlib.ParseCertificatePEM(in) + } + + return x509.ParseCertificate(in) +} + +var ocspOpts = ocsp.RequestOptions{ + Hash: crypto.SHA1, +} + +func certIsRevokedOCSP(leaf *x509.Certificate, strict bool) (revoked, ok bool, e error) { + var err error + + ocspURLs := leaf.OCSPServer + if len(ocspURLs) == 0 { + // OCSP not enabled for this certificate. + return false, true, nil + } + + issuer := getIssuer(leaf) + + if issuer == nil { + return false, false, nil + } + + ocspRequest, err := ocsp.CreateRequest(leaf, issuer, &ocspOpts) + if err != nil { + return revoked, ok, err + } + + for _, server := range ocspURLs { + resp, err := sendOCSPRequest(server, ocspRequest, leaf, issuer) + if err != nil { + if strict { + return revoked, ok, err + } + continue + } + + // There wasn't an error fetching the OCSP status. + ok = true + + if resp.Status != ocsp.Good { + // The certificate was revoked. + revoked = true + } + + return revoked, ok, err + } + return revoked, ok, err +} + +// sendOCSPRequest attempts to request an OCSP response from the +// server. The error only indicates a failure to *fetch* the +// certificate, and *does not* mean the certificate is valid. +func sendOCSPRequest(server string, req []byte, leaf, issuer *x509.Certificate) (*ocsp.Response, error) { + var resp *http.Response + var err error + if len(req) > 256 { + buf := bytes.NewBuffer(req) + resp, err = HTTPClient.Post(server, "application/ocsp-request", buf) + } else { + reqURL := server + "/" + neturl.QueryEscape(base64.StdEncoding.EncodeToString(req)) + resp, err = HTTPClient.Get(reqURL) + } + + if err != nil { + return nil, err + } + defer resp.Body.Close() + + if resp.StatusCode != http.StatusOK { + return nil, errors.New("failed to retrieve OSCP") + } + + body, err := ocspRead(resp.Body) + if err != nil { + return nil, err + } + + switch { + case bytes.Equal(body, ocsp.UnauthorizedErrorResponse): + return nil, errors.New("OSCP unauthorized") + case bytes.Equal(body, ocsp.MalformedRequestErrorResponse): + return nil, errors.New("OSCP malformed") + case bytes.Equal(body, ocsp.InternalErrorErrorResponse): + return nil, errors.New("OSCP internal error") + case bytes.Equal(body, ocsp.TryLaterErrorResponse): + return nil, errors.New("OSCP try later") + case bytes.Equal(body, ocsp.SigRequredErrorResponse): + return nil, errors.New("OSCP signature required") + } + + return ocsp.ParseResponseForCert(body, leaf, issuer) +} + +var crlRead = io.ReadAll + +// SetCRLFetcher sets the function to use to read from the http response body +func SetCRLFetcher(fn func(io.Reader) ([]byte, error)) { + crlRead = fn +} + +var remoteRead = io.ReadAll + +// SetRemoteFetcher sets the function to use to read from the http response body +func SetRemoteFetcher(fn func(io.Reader) ([]byte, error)) { + remoteRead = fn +} + +var ocspRead = io.ReadAll + +// SetOCSPFetcher sets the function to use to read from the http response body +func SetOCSPFetcher(fn func(io.Reader) ([]byte, error)) { + ocspRead = fn +} diff --git a/certlib/revoke/revoke_test.go b/certlib/revoke/revoke_test.go new file mode 100644 index 0000000..145634f --- /dev/null +++ b/certlib/revoke/revoke_test.go @@ -0,0 +1,262 @@ +package revoke + +import ( + "crypto/x509" + "encoding/pem" + "fmt" + "os" + "testing" + "time" +) + +// Originally from CFSSL, mostly written by me originally, and licensed under: + +/* +Copyright (c) 2014 CloudFlare Inc. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions +are met: + +Redistributions of source code must retain the above copyright notice, +this list of conditions and the following disclaimer. + +Redistributions in binary form must reproduce the above copyright notice, +this list of conditions and the following disclaimer in the documentation +and/or other materials provided with the distribution. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED +TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR +PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF +LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING +NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +*/ + +// I've modified it for use in my own code e.g. by removing the CFSSL errors +// and replacing them with sane ones. + +// The first three test cases represent known revoked, expired, and good +// certificates that were checked on the date listed in the log. The +// good certificate will eventually need to be replaced in year 2029. + +// If there is a soft-fail, the test will pass to mimic the default +// behaviour used in this software. However, it will print a warning +// to indicate that this is the case. + +// 2014/05/22 14:18:17 Certificate expired 2014-04-04 14:14:20 +0000 UTC +// 2014/05/22 14:18:17 Revoked certificate: misc/intermediate_ca/ActalisServerAuthenticationCA.crt +var expiredCert = mustParse(`-----BEGIN CERTIFICATE----- +MIIEXTCCA8agAwIBAgIEBycURTANBgkqhkiG9w0BAQUFADB1MQswCQYDVQQGEwJV +UzEYMBYGA1UEChMPR1RFIENvcnBvcmF0aW9uMScwJQYDVQQLEx5HVEUgQ3liZXJU +cnVzdCBTb2x1dGlvbnMsIEluYy4xIzAhBgNVBAMTGkdURSBDeWJlclRydXN0IEds +b2JhbCBSb290MB4XDTA3MDQwNDE0MTUxNFoXDTE0MDQwNDE0MTQyMFowejELMAkG +A1UEBhMCSVQxFzAVBgNVBAoTDkFjdGFsaXMgUy5wLkEuMScwJQYDVQQLEx5DZXJ0 +aWZpY2F0aW9uIFNlcnZpY2UgUHJvdmlkZXIxKTAnBgNVBAMTIEFjdGFsaXMgU2Vy +dmVyIEF1dGhlbnRpY2F0aW9uIENBMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIB +CgKCAQEAv6P0bhXbUQkVW8ox0HJ+sP5+j6pTwS7yg/wGEUektB/G1duQiT1v21fo +LANr6F353jILQDCpHIfal3MhbSsHEMKU7XaqsyLWV93bcIKbIloS/eXDfkog6KB3 +u0JHgrtNz584Jg/OLm9feffNbCJ38TiLo0/UWkAQ6PQWaOwZEgyKjVI5F3swoTB3 +g0LZAzegvkU00Kfp13cSg+cJeU4SajwtfQ+g6s6dlaekaHy/0ef46PfiHHRuhEhE +JWIpDtUN2ywTT33MSSUe5glDIiXYfcamJQrebzGsHEwyqI195Yaxb+FLNND4n3HM +e7EI2OrLyT+r/WMvQbl+xNihwtv+HwIDAQABo4IBbzCCAWswEgYDVR0TAQH/BAgw +BgEB/wIBADBTBgNVHSAETDBKMEgGCSsGAQQBsT4BADA7MDkGCCsGAQUFBwIBFi1o +dHRwOi8vd3d3LnB1YmxpYy10cnVzdC5jb20vQ1BTL09tbmlSb290Lmh0bWwwDgYD +VR0PAQH/BAQDAgEGMIGJBgNVHSMEgYEwf6F5pHcwdTELMAkGA1UEBhMCVVMxGDAW +BgNVBAoTD0dURSBDb3Jwb3JhdGlvbjEnMCUGA1UECxMeR1RFIEN5YmVyVHJ1c3Qg +U29sdXRpb25zLCBJbmMuMSMwIQYDVQQDExpHVEUgQ3liZXJUcnVzdCBHbG9iYWwg +Um9vdIICAaUwRQYDVR0fBD4wPDA6oDigNoY0aHR0cDovL3d3dy5wdWJsaWMtdHJ1 +c3QuY29tL2NnaS1iaW4vQ1JMLzIwMTgvY2RwLmNybDAdBgNVHQ4EFgQUpi6OuXYt +oxHC3cTezVLuraWpAFEwDQYJKoZIhvcNAQEFBQADgYEAAtjJBwjsvw7DBs+v7BQz +gSGeg6nbYUuPL7+1driT5XsUKJ7WZjiwW2zW/WHZ+zGo1Ev8Dc574RpSrg/EIlfH +TpBiBuFgiKtJksKdoxPZGSI8FitwcgeW+y8wotmm0CtDzWN27g2kfSqHb5eHfZY5 +sESPRwHkcMUNdAp37FLweUw= +-----END CERTIFICATE-----`) + +// 2014/05/22 14:18:31 Serial number match: intermediate is revoked. +// 2014/05/22 14:18:31 certificate is revoked via CRL +// 2014/05/22 14:18:31 Revoked certificate: misc/intermediate_ca/MobileArmorEnterpriseCA.crt +var revokedCert = mustParse(`-----BEGIN CERTIFICATE----- +MIIEEzCCAvugAwIBAgILBAAAAAABGMGjftYwDQYJKoZIhvcNAQEFBQAwcTEoMCYG +A1UEAxMfR2xvYmFsU2lnbiBSb290U2lnbiBQYXJ0bmVycyBDQTEdMBsGA1UECxMU +Um9vdFNpZ24gUGFydG5lcnMgQ0ExGTAXBgNVBAoTEEdsb2JhbFNpZ24gbnYtc2Ex +CzAJBgNVBAYTAkJFMB4XDTA4MDMxODEyMDAwMFoXDTE4MDMxODEyMDAwMFowJTEj +MCEGA1UEAxMaTW9iaWxlIEFybW9yIEVudGVycHJpc2UgQ0EwggEiMA0GCSqGSIb3 +DQEBAQUAA4IBDwAwggEKAoIBAQCaEjeDR73jSZVlacRn5bc5VIPdyouHvGIBUxyS +C6483HgoDlWrWlkEndUYFjRPiQqJFthdJxfglykXD+btHixMIYbz/6eb7hRTdT9w +HKsfH+wTBIdb5AZiNjkg3QcCET5HfanJhpREjZWP513jM/GSrG3VwD6X5yttCIH1 +NFTDAr7aqpW/UPw4gcPfkwS92HPdIkb2DYnsqRrnKyNValVItkxJiotQ1HOO3YfX +ivGrHIbJdWYg0rZnkPOgYF0d+aIA4ZfwvdW48+r/cxvLevieuKj5CTBZZ8XrFt8r +JTZhZljbZvnvq/t6ZIzlwOj082f+lTssr1fJ3JsIPnG2lmgTAgMBAAGjgfcwgfQw +DgYDVR0PAQH/BAQDAgEGMBIGA1UdEwEB/wQIMAYBAf8CAQEwHQYDVR0OBBYEFIZw +ns4uzXdLX6xDRXUzFgZxWM7oME0GA1UdIARGMEQwQgYJKwYBBAGgMgE8MDUwMwYI +KwYBBQUHAgIwJxolaHR0cDovL3d3dy5nbG9iYWxzaWduLmNvbS9yZXBvc2l0b3J5 +LzA/BgNVHR8EODA2MDSgMqAwhi5odHRwOi8vY3JsLmdsb2JhbHNpZ24ubmV0L1Jv +b3RTaWduUGFydG5lcnMuY3JsMB8GA1UdIwQYMBaAFFaE7LVxpedj2NtRBNb65vBI +UknOMA0GCSqGSIb3DQEBBQUAA4IBAQBZvf+2xUJE0ekxuNk30kPDj+5u9oI3jZyM +wvhKcs7AuRAbcxPtSOnVGNYl8By7DPvPun+U3Yci8540y143RgD+kz3jxIBaoW/o +c4+X61v6DBUtcBPEt+KkV6HIsZ61SZmc/Y1I2eoeEt6JYoLjEZMDLLvc1cK/+wpg +dUZSK4O9kjvIXqvsqIOlkmh/6puSugTNao2A7EIQr8ut0ZmzKzMyZ0BuQhJDnAPd +Kz5vh+5tmytUPKA8hUgmLWe94lMb7Uqq2wgZKsqun5DAWleKu81w7wEcOrjiiB+x +jeBHq7OnpWm+ccTOPCE6H4ZN4wWVS7biEBUdop/8HgXBPQHWAdjL +-----END CERTIFICATE-----`) + +// A Comodo intermediate CA certificate with issuer url, CRL url and OCSP url +var goodComodoCA = (`-----BEGIN CERTIFICATE----- +MIIGCDCCA/CgAwIBAgIQKy5u6tl1NmwUim7bo3yMBzANBgkqhkiG9w0BAQwFADCB +hTELMAkGA1UEBhMCR0IxGzAZBgNVBAgTEkdyZWF0ZXIgTWFuY2hlc3RlcjEQMA4G +A1UEBxMHU2FsZm9yZDEaMBgGA1UEChMRQ09NT0RPIENBIExpbWl0ZWQxKzApBgNV +BAMTIkNPTU9ETyBSU0EgQ2VydGlmaWNhdGlvbiBBdXRob3JpdHkwHhcNMTQwMjEy +MDAwMDAwWhcNMjkwMjExMjM1OTU5WjCBkDELMAkGA1UEBhMCR0IxGzAZBgNVBAgT +EkdyZWF0ZXIgTWFuY2hlc3RlcjEQMA4GA1UEBxMHU2FsZm9yZDEaMBgGA1UEChMR +Q09NT0RPIENBIExpbWl0ZWQxNjA0BgNVBAMTLUNPTU9ETyBSU0EgRG9tYWluIFZh +bGlkYXRpb24gU2VjdXJlIFNlcnZlciBDQTCCASIwDQYJKoZIhvcNAQEBBQADggEP +ADCCAQoCggEBAI7CAhnhoFmk6zg1jSz9AdDTScBkxwtiBUUWOqigwAwCfx3M28Sh +bXcDow+G+eMGnD4LgYqbSRutA776S9uMIO3Vzl5ljj4Nr0zCsLdFXlIvNN5IJGS0 +Qa4Al/e+Z96e0HqnU4A7fK31llVvl0cKfIWLIpeNs4TgllfQcBhglo/uLQeTnaG6 +ytHNe+nEKpooIZFNb5JPJaXyejXdJtxGpdCsWTWM/06RQ1A/WZMebFEh7lgUq/51 +UHg+TLAchhP6a5i84DuUHoVS3AOTJBhuyydRReZw3iVDpA3hSqXttn7IzW3uLh0n +c13cRTCAquOyQQuvvUSH2rnlG51/ruWFgqUCAwEAAaOCAWUwggFhMB8GA1UdIwQY +MBaAFLuvfgI9+qbxPISOre44mOzZMjLUMB0GA1UdDgQWBBSQr2o6lFoL2JDqElZz +30O0Oija5zAOBgNVHQ8BAf8EBAMCAYYwEgYDVR0TAQH/BAgwBgEB/wIBADAdBgNV +HSUEFjAUBggrBgEFBQcDAQYIKwYBBQUHAwIwGwYDVR0gBBQwEjAGBgRVHSAAMAgG +BmeBDAECATBMBgNVHR8ERTBDMEGgP6A9hjtodHRwOi8vY3JsLmNvbW9kb2NhLmNv +bS9DT01PRE9SU0FDZXJ0aWZpY2F0aW9uQXV0aG9yaXR5LmNybDBxBggrBgEFBQcB +AQRlMGMwOwYIKwYBBQUHMAKGL2h0dHA6Ly9jcnQuY29tb2RvY2EuY29tL0NPTU9E +T1JTQUFkZFRydXN0Q0EuY3J0MCQGCCsGAQUFBzABhhhodHRwOi8vb2NzcC5jb21v +ZG9jYS5jb20wDQYJKoZIhvcNAQEMBQADggIBAE4rdk+SHGI2ibp3wScF9BzWRJ2p +mj6q1WZmAT7qSeaiNbz69t2Vjpk1mA42GHWx3d1Qcnyu3HeIzg/3kCDKo2cuH1Z/ +e+FE6kKVxF0NAVBGFfKBiVlsit2M8RKhjTpCipj4SzR7JzsItG8kO3KdY3RYPBps +P0/HEZrIqPW1N+8QRcZs2eBelSaz662jue5/DJpmNXMyYE7l3YphLG5SEXdoltMY +dVEVABt0iN3hxzgEQyjpFv3ZBdRdRydg1vs4O2xyopT4Qhrf7W8GjEXCBgCq5Ojc +2bXhc3js9iPc0d1sjhqPpepUfJa3w/5Vjo1JXvxku88+vZbrac2/4EjxYoIQ5QxG +V/Iz2tDIY+3GH5QFlkoakdH368+PUq4NCNk+qKBR6cGHdNXJ93SrLlP7u3r7l+L4 +HyaPs9Kg4DdbKDsx5Q5XLVq4rXmsXiBmGqW5prU5wfWYQ//u+aen/e7KJD2AFsQX +j4rBYKEMrltDR5FL1ZoXX/nUh8HCjLfn4g8wGTeGrODcQgPmlKidrv0PJFGUzpII +0fxQ8ANAe4hZ7Q7drNJ3gjTcBpUC2JD5Leo31Rpg0Gcg19hCC0Wvgmje3WYkN5Ap +lBlGGSW4gNfL1IYoakRwJiNiqZ+Gb7+6kHDSVneFeO/qJakXzlByjAA6quPbYzSf ++AZxAeKCINT+b72x +-----END CERTIFICATE-----`) + +var goodCert = mustParse(goodComodoCA) + +func mustParse(pemData string) *x509.Certificate { + block, _ := pem.Decode([]byte(pemData)) + if block == nil { + panic("Invalid PEM data.") + } else if block.Type != "CERTIFICATE" { + panic("Invalid PEM type.") + } + + cert, err := x509.ParseCertificate([]byte(block.Bytes)) + if err != nil { + panic(err.Error()) + } + return cert +} + +func TestRevoked(t *testing.T) { + if revoked, ok := VerifyCertificate(revokedCert); !ok { + fmt.Fprintf(os.Stderr, "Warning: soft fail checking revocation") + } else if !revoked { + t.Fatalf("revoked certificate should have been marked as revoked") + } +} + +func TestExpired(t *testing.T) { + if revoked, ok := VerifyCertificate(expiredCert); !ok { + fmt.Fprintf(os.Stderr, "Warning: soft fail checking revocation") + } else if !revoked { + t.Fatalf("expired certificate should have been marked as revoked") + } +} + +func TestGood(t *testing.T) { + if revoked, ok := VerifyCertificate(goodCert); !ok { + fmt.Fprintf(os.Stderr, "Warning: soft fail checking revocation") + } else if revoked { + t.Fatalf("good certificate should not have been marked as revoked") + } + +} + +func TestLdap(t *testing.T) { + ldapCert := mustParse(goodComodoCA) + ldapCert.CRLDistributionPoints = append(ldapCert.CRLDistributionPoints, "ldap://myldap.example.com") + if revoked, ok := VerifyCertificate(ldapCert); revoked || !ok { + t.Fatalf("ldap certificate should have been recognized") + } +} + +func TestLdapURLErr(t *testing.T) { + if ldapURL(":") { + t.Fatalf("bad url does not cause error") + } +} + +func TestCertNotYetValid(t *testing.T) { + notReadyCert := expiredCert + notReadyCert.NotBefore = time.Date(3000, time.January, 1, 1, 1, 1, 1, time.Local) + notReadyCert.NotAfter = time.Date(3005, time.January, 1, 1, 1, 1, 1, time.Local) + if revoked, _ := VerifyCertificate(expiredCert); !revoked { + t.Fatalf("not yet verified certificate should have been marked as revoked") + } +} + +func TestCRLFetchError(t *testing.T) { + ldapCert := mustParse(goodComodoCA) + ldapCert.CRLDistributionPoints[0] = "" + if revoked, ok := VerifyCertificate(ldapCert); ok || revoked { + t.Fatalf("Fetching error not encountered") + } + HardFail = true + if revoked, ok := VerifyCertificate(ldapCert); ok || !revoked { + t.Fatalf("Fetching error not encountered, hardfail not registered") + } + HardFail = false +} + +func TestBadCRLSet(t *testing.T) { + ldapCert := mustParse(goodComodoCA) + ldapCert.CRLDistributionPoints[0] = "" + CRLSet[""] = nil + certIsRevokedCRL(ldapCert, "") + if _, ok := CRLSet[""]; ok { + t.Fatalf("key emptystring should be deleted from CRLSet") + } + delete(CRLSet, "") + +} + +func TestCachedCRLSet(t *testing.T) { + VerifyCertificate(goodCert) + if revoked, ok := VerifyCertificate(goodCert); !ok || revoked { + t.Fatalf("Previously fetched CRL's should be read smoothly and unrevoked") + } +} + +func TestRemoteFetchError(t *testing.T) { + + badurl := ":" + + if _, err := fetchRemote(badurl); err == nil { + t.Fatalf("fetching bad url should result in non-nil error") + } + +} + +func TestNoOCSPServers(t *testing.T) { + badIssuer := goodCert + badIssuer.IssuingCertificateURL = []string{" "} + certIsRevokedOCSP(badIssuer, true) + noOCSPCert := goodCert + noOCSPCert.OCSPServer = make([]string, 0) + if revoked, ok, _ := certIsRevokedOCSP(noOCSPCert, true); revoked || !ok { + t.Fatalf("OCSP falsely registered as enabled for this certificate") + } +} diff --git a/cmd/certdump/BUILD.bazel b/cmd/certdump/BUILD.bazel index b67008e..c28e35d 100644 --- a/cmd/certdump/BUILD.bazel +++ b/cmd/certdump/BUILD.bazel @@ -9,8 +9,8 @@ go_library( importpath = "git.wntrmute.dev/kyle/goutils/cmd/certdump", visibility = ["//visibility:private"], deps = [ - "@com_github_cloudflare_cfssl//errors", - "@com_github_cloudflare_cfssl//helpers", + "//certlib", + "//lib", "@com_github_kr_text//:text", ], ) diff --git a/cmd/certdump/certdump.go b/cmd/certdump/certdump.go index 05d7e60..237866d 100644 --- a/cmd/certdump/certdump.go +++ b/cmd/certdump/certdump.go @@ -12,12 +12,13 @@ import ( "crypto/x509/pkix" "flag" "fmt" - "io/ioutil" + "io" "os" "sort" "strings" - "github.com/cloudflare/cfssl/helpers" + "git.wntrmute.dev/kyle/goutils/certlib" + "git.wntrmute.dev/kyle/goutils/lib" ) func certPublic(cert *x509.Certificate) string { @@ -208,17 +209,17 @@ func displayCert(cert *x509.Certificate) { } func displayAllCerts(in []byte, leafOnly bool) { - certs, err := helpers.ParseCertificatesPEM(in) + certs, err := certlib.ParseCertificatesPEM(in) if err != nil { - certs, _, err = helpers.ParseCertificatesDER(in, "") + certs, _, err = certlib.ParseCertificatesDER(in, "") if err != nil { - Warn(TranslateCFSSLError(err), "failed to parse certificates") + lib.Warn(err, "failed to parse certificates") return } } if len(certs) == 0 { - Warnx("no certificates found") + lib.Warnx("no certificates found") return } @@ -236,7 +237,7 @@ func displayAllCertsWeb(uri string, leafOnly bool) { ci := getConnInfo(uri) conn, err := tls.Dial("tcp", ci.Addr, permissiveConfig()) if err != nil { - Warn(err, "couldn't connect to %s", ci.Addr) + lib.Warn(err, "couldn't connect to %s", ci.Addr) return } defer conn.Close() @@ -252,11 +253,11 @@ func displayAllCertsWeb(uri string, leafOnly bool) { } conn.Close() } else { - Warn(err, "TLS verification error with server name %s", ci.Host) + lib.Warn(err, "TLS verification error with server name %s", ci.Host) } if len(state.PeerCertificates) == 0 { - Warnx("no certificates found") + lib.Warnx("no certificates found") return } @@ -266,7 +267,7 @@ func displayAllCertsWeb(uri string, leafOnly bool) { } if len(state.VerifiedChains) == 0 { - Warnx("no verified chains found; using peer chain") + lib.Warnx("no verified chains found; using peer chain") for i := range state.PeerCertificates { displayCert(state.PeerCertificates[i]) } @@ -289,9 +290,9 @@ func main() { flag.Parse() if flag.NArg() == 0 || (flag.NArg() == 1 && flag.Arg(0) == "-") { - certs, err := ioutil.ReadAll(os.Stdin) + certs, err := io.ReadAll(os.Stdin) if err != nil { - Warn(err, "couldn't read certificates from standard input") + lib.Warn(err, "couldn't read certificates from standard input") os.Exit(1) } @@ -306,9 +307,9 @@ func main() { if strings.HasPrefix(filename, "https://") { displayAllCertsWeb(filename, leafOnly) } else { - in, err := ioutil.ReadFile(filename) + in, err := os.ReadFile(filename) if err != nil { - Warn(err, "couldn't read certificate") + lib.Warn(err, "couldn't read certificate") continue } diff --git a/cmd/certdump/util.go b/cmd/certdump/util.go index c4a577a..c445dd3 100644 --- a/cmd/certdump/util.go +++ b/cmd/certdump/util.go @@ -3,13 +3,10 @@ package main import ( "crypto/tls" "crypto/x509" - "errors" "fmt" "net" - "os" "strings" - cferr "github.com/cloudflare/cfssl/errors" "github.com/kr/text" ) @@ -89,34 +86,6 @@ func sigAlgoHash(a x509.SignatureAlgorithm) string { } } -// TranslateCFSSLError turns a CFSSL error into a more readable string. -func TranslateCFSSLError(err error) error { - if err == nil { - return nil - } - - // printing errors as json is terrible - if cfsslError, ok := err.(*cferr.Error); ok { - err = errors.New(cfsslError.Message) - } - return err -} - -// Warnx displays a formatted error message to standard error, à la -// warnx(3). -func Warnx(format string, a ...interface{}) (int, error) { - format += "\n" - return fmt.Fprintf(os.Stderr, format, a...) -} - -// Warn displays a formatted error message to standard output, -// appending the error string, à la warn(3). -func Warn(err error, format string, a ...interface{}) (int, error) { - format += ": %v\n" - a = append(a, err) - return fmt.Fprintf(os.Stderr, format, a...) -} - const maxLine = 78 func makeIndent(n int) string { diff --git a/cmd/certexpiry/BUILD.bazel b/cmd/certexpiry/BUILD.bazel index f236109..3f9e546 100644 --- a/cmd/certexpiry/BUILD.bazel +++ b/cmd/certexpiry/BUILD.bazel @@ -6,9 +6,9 @@ go_library( importpath = "git.wntrmute.dev/kyle/goutils/cmd/certexpiry", visibility = ["//visibility:private"], deps = [ + "//certlib", "//die", "//lib", - "@com_github_cloudflare_cfssl//helpers", ], ) diff --git a/cmd/certexpiry/main.go b/cmd/certexpiry/main.go index 5be5b60..1ce62a8 100644 --- a/cmd/certexpiry/main.go +++ b/cmd/certexpiry/main.go @@ -10,9 +10,9 @@ import ( "strings" "time" + "git.wntrmute.dev/kyle/goutils/certlib" "git.wntrmute.dev/kyle/goutils/die" "git.wntrmute.dev/kyle/goutils/lib" - "github.com/cloudflare/cfssl/helpers" ) var warnOnly bool @@ -87,7 +87,7 @@ func main() { continue } - certs, err := helpers.ParseCertificatesPEM(in) + certs, err := certlib.ParseCertificatesPEM(in) if err != nil { lib.Warn(err, "while parsing certificates") continue diff --git a/cmd/certverify/BUILD.bazel b/cmd/certverify/BUILD.bazel index 957f782..d64ab7b 100644 --- a/cmd/certverify/BUILD.bazel +++ b/cmd/certverify/BUILD.bazel @@ -6,9 +6,9 @@ go_library( importpath = "git.wntrmute.dev/kyle/goutils/cmd/certverify", visibility = ["//visibility:private"], deps = [ + "//certlib", "//die", "//lib", - "@com_github_cloudflare_cfssl//helpers", "@com_github_cloudflare_cfssl//revoke", ], ) diff --git a/cmd/certverify/main.go b/cmd/certverify/main.go index dd2b47f..3f64992 100644 --- a/cmd/certverify/main.go +++ b/cmd/certverify/main.go @@ -8,14 +8,14 @@ import ( "os" "time" + "git.wntrmute.dev/kyle/goutils/certlib" + "git.wntrmute.dev/kyle/goutils/certlib/revoke" "git.wntrmute.dev/kyle/goutils/die" "git.wntrmute.dev/kyle/goutils/lib" - "github.com/cloudflare/cfssl/helpers" - "github.com/cloudflare/cfssl/revoke" ) func printRevocation(cert *x509.Certificate) { - remaining := cert.NotAfter.Sub(time.Now()) + remaining := time.Until(cert.NotAfter) fmt.Printf("certificate expires in %s.\n", lib.Duration(remaining)) revoked, ok := revoke.VerifyCertificate(cert) @@ -47,7 +47,7 @@ func main() { if verbose { fmt.Println("[+] loading root certificates from", caFile) } - roots, err = helpers.LoadPEMCertPool(caFile) + roots, err = certlib.LoadPEMCertPool(caFile) die.If(err) } @@ -57,7 +57,7 @@ func main() { if verbose { fmt.Println("[+] loading intermediate certificates from", intFile) } - ints, err = helpers.LoadPEMCertPool(caFile) + ints, err = certlib.LoadPEMCertPool(caFile) die.If(err) } else { ints = x509.NewCertPool() @@ -71,7 +71,7 @@ func main() { fileData, err := ioutil.ReadFile(flag.Arg(0)) die.If(err) - chain, err := helpers.ParseCertificatesPEM(fileData) + chain, err := certlib.ParseCertificatesPEM(fileData) die.If(err) if verbose { fmt.Printf("[+] %s has %d certificates\n", flag.Arg(0), len(chain)) diff --git a/cmd/subjhash/BUILD.bazel b/cmd/subjhash/BUILD.bazel index 6ec9f61..785ed80 100644 --- a/cmd/subjhash/BUILD.bazel +++ b/cmd/subjhash/BUILD.bazel @@ -6,6 +6,7 @@ go_library( importpath = "git.wntrmute.dev/kyle/goutils/cmd/subjhash", visibility = ["//visibility:private"], deps = [ + "//certlib", "//die", "//lib", ], diff --git a/cmd/subjhash/main.go b/cmd/subjhash/main.go index b2f656e..d034949 100644 --- a/cmd/subjhash/main.go +++ b/cmd/subjhash/main.go @@ -9,6 +9,7 @@ import ( "io" "os" + "git.wntrmute.dev/kyle/goutils/certlib" "git.wntrmute.dev/kyle/goutils/die" "git.wntrmute.dev/kyle/goutils/lib" ) @@ -57,7 +58,7 @@ func getSubjectInfoHash(cert *x509.Certificate, issuer bool) []byte { func printDigests(paths []string, issuer bool) { for _, path := range paths { - cert, err := lib.LoadCertificate(path) + cert, err := certlib.LoadCertificate(path) if err != nil { lib.Warn(err, "failed to load certificate from %s", path) continue @@ -82,9 +83,9 @@ func matchDigests(paths []string, issuer bool) { snd := paths[1] paths = paths[2:] - fstCert, err := lib.LoadCertificate(fst) + fstCert, err := certlib.LoadCertificate(fst) die.If(err) - sndCert, err := lib.LoadCertificate(snd) + sndCert, err := certlib.LoadCertificate(snd) die.If(err) if !bytes.Equal(getSubjectInfoHash(fstCert, issuer), getSubjectInfoHash(sndCert, issuer)) { lib.Warnx("certificates don't match: %s and %s", fst, snd) diff --git a/config/BUILD.bazel b/config/BUILD.bazel index 21cfcff..c42f763 100644 --- a/config/BUILD.bazel +++ b/config/BUILD.bazel @@ -4,6 +4,7 @@ go_library( name = "config", srcs = [ "config.go", + "path.go", "path_linux.go", ], importpath = "git.wntrmute.dev/kyle/goutils/config", diff --git a/config/path.go b/config/path.go index 2da08bc..fd09523 100644 --- a/config/path.go +++ b/config/path.go @@ -1,5 +1,5 @@ -//go:build ignore -// +build ignore +//go:build !linux +// +build !linux package config diff --git a/go.mod b/go.mod index 36f10c0..9781b81 100644 --- a/go.mod +++ b/go.mod @@ -15,3 +15,10 @@ require ( ) require github.com/davecgh/go-spew v1.1.1 + +require ( + github.com/google/certificate-transparency-go v1.0.21 // indirect + github.com/kr/fs v0.1.0 // indirect + github.com/pmezard/go-difflib v1.0.0 // indirect + gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c // indirect +) diff --git a/go.sum b/go.sum index 72c9020..72136aa 100644 --- a/go.sum +++ b/go.sum @@ -81,22 +81,16 @@ golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks= golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20201010224723-4f7140c49acb/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= -golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220412211240-33da011f77ad h1:ntjMns5wyP/fN65tdBD4g8J5w8n015+iIIs9rtjXkY0= golang.org/x/sys v0.0.0-20220412211240-33da011f77ad/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1 h1:v+OssWQX+hTHEmOBgwxdZxK4zHq3yOs8F9J7mk0PY8E= -golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= -golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= google.golang.org/appengine v1.6.6/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= diff --git a/lib/BUILD.bazel b/lib/BUILD.bazel index 3497e70..f265df3 100644 --- a/lib/BUILD.bazel +++ b/lib/BUILD.bazel @@ -1,4 +1,4 @@ -load("@io_bazel_rules_go//go:def.bzl", "go_library", "go_test") +load("@io_bazel_rules_go//go:def.bzl", "go_library") go_library( name = "lib", @@ -107,11 +107,3 @@ go_library( "//conditions:default": [], }), ) - -go_test( - name = "lib_test", - size = "small", - srcs = ["lib_test.go"], - embed = [":lib"], - deps = ["//assert"], -) diff --git a/lib/lib.go b/lib/lib.go index 30191d5..dd01d1a 100644 --- a/lib/lib.go +++ b/lib/lib.go @@ -2,11 +2,7 @@ package lib import ( - "crypto/x509" - "encoding/pem" - "errors" "fmt" - "io/ioutil" "os" "path/filepath" "time" @@ -107,78 +103,3 @@ func Duration(d time.Duration) string { s += fmt.Sprintf("%dh%s", hours, d) return s } - -// ReadCertificate reads a DER or PEM-encoded certificate from the -// byte slice. -func ReadCertificate(in []byte) (cert *x509.Certificate, rest []byte, err error) { - if len(in) == 0 { - err = errors.New("lib: empty certificate") - return - } - - if in[0] == '-' { - p, remaining := pem.Decode(in) - if p == nil { - err = errors.New("lib: invalid PEM file") - return - } - - rest = remaining - if p.Type != "CERTIFICATE" { - err = fmt.Errorf("lib: expected a CERTIFICATE PEM file, but have %s", p.Type) - return - } - - in = p.Bytes - } - - cert, err = x509.ParseCertificate(in) - return -} - -// ReadCertificates tries to read all the certificates in a -// PEM-encoded collection. -func ReadCertificates(in []byte) (certs []*x509.Certificate, err error) { - var cert *x509.Certificate - for { - cert, in, err = ReadCertificate(in) - if err != nil { - break - } - - if cert == nil { - break - } - - certs = append(certs, cert) - if len(in) == 0 { - break - } - } - - return certs, err -} - -// LoadCertificate tries to read a single certificate from disk. If -// the file contains multiple certificates (e.g. a chain), only the -// first certificate is returned. -func LoadCertificate(path string) (*x509.Certificate, error) { - in, err := ioutil.ReadFile(path) - if err != nil { - return nil, err - } - - cert, _, err := ReadCertificate(in) - return cert, err -} - -// LoadCertificates tries to read all the certificates in a file, -// returning them in the order that it found them in the file. -func LoadCertificates(path string) ([]*x509.Certificate, error) { - in, err := ioutil.ReadFile(path) - if err != nil { - return nil, err - } - - return ReadCertificates(in) -} diff --git a/syslog/BUILD.bazel b/log/BUILD.bazel similarity index 54% rename from syslog/BUILD.bazel rename to log/BUILD.bazel index 68a4e7b..9188e49 100644 --- a/syslog/BUILD.bazel +++ b/log/BUILD.bazel @@ -10,3 +10,14 @@ go_library( "@com_github_hashicorp_go_syslog//:go-syslog", ], ) + +go_library( + name = "log", + srcs = ["logger.go"], + importpath = "git.wntrmute.dev/kyle/goutils/log", + visibility = ["//visibility:public"], + deps = [ + "@com_github_davecgh_go_spew//spew", + "@com_github_hashicorp_go_syslog//:go-syslog", + ], +) diff --git a/syslog/logger.go b/log/logger.go similarity index 99% rename from syslog/logger.go rename to log/logger.go index fee0f11..2d1e65b 100644 --- a/syslog/logger.go +++ b/log/logger.go @@ -1,5 +1,5 @@ // Package syslog is a syslog-type facility for logging. -package syslog +package log import ( "fmt"