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"