working on removing dependency on cfssl.
This commit is contained in:
parent
34982c122f
commit
984baa6bb4
|
@ -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
|
|
@ -0,0 +1,9 @@
|
||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<module type="WEB_MODULE" version="4">
|
||||||
|
<component name="Go" enabled="true" />
|
||||||
|
<component name="NewModuleRootManager">
|
||||||
|
<content url="file://$MODULE_DIR$" />
|
||||||
|
<orderEntry type="inheritedJdk" />
|
||||||
|
<orderEntry type="sourceFolder" forTests="false" />
|
||||||
|
</component>
|
||||||
|
</module>
|
|
@ -0,0 +1,8 @@
|
||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<project version="4">
|
||||||
|
<component name="ProjectModuleManager">
|
||||||
|
<modules>
|
||||||
|
<module fileurl="file://$PROJECT_DIR$/.idea/goutils.iml" filepath="$PROJECT_DIR$/.idea/goutils.iml" />
|
||||||
|
</modules>
|
||||||
|
</component>
|
||||||
|
</project>
|
|
@ -0,0 +1,6 @@
|
||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<project version="4">
|
||||||
|
<component name="VcsDirectoryMappings">
|
||||||
|
<mapping directory="$PROJECT_DIR$" vcs="Git" />
|
||||||
|
</component>
|
||||||
|
</project>
|
|
@ -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",
|
||||||
|
)
|
|
@ -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")
|
|
@ -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)
|
||||||
|
}
|
|
@ -1,4 +1,4 @@
|
||||||
package lib
|
package certlib
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
|
@ -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))
|
||||||
|
}
|
||||||
|
}
|
|
@ -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
|
||||||
|
}
|
|
@ -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], ", "))
|
||||||
|
}
|
||||||
|
}
|
|
@ -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
|
||||||
|
|
||||||
|
}
|
|
@ -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",
|
||||||
|
],
|
||||||
|
)
|
|
@ -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
|
||||||
|
}
|
|
@ -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")
|
||||||
|
}
|
||||||
|
}
|
|
@ -9,8 +9,8 @@ go_library(
|
||||||
importpath = "git.wntrmute.dev/kyle/goutils/cmd/certdump",
|
importpath = "git.wntrmute.dev/kyle/goutils/cmd/certdump",
|
||||||
visibility = ["//visibility:private"],
|
visibility = ["//visibility:private"],
|
||||||
deps = [
|
deps = [
|
||||||
"@com_github_cloudflare_cfssl//errors",
|
"//certlib",
|
||||||
"@com_github_cloudflare_cfssl//helpers",
|
"//lib",
|
||||||
"@com_github_kr_text//:text",
|
"@com_github_kr_text//:text",
|
||||||
],
|
],
|
||||||
)
|
)
|
||||||
|
|
|
@ -12,12 +12,13 @@ import (
|
||||||
"crypto/x509/pkix"
|
"crypto/x509/pkix"
|
||||||
"flag"
|
"flag"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io/ioutil"
|
"io"
|
||||||
"os"
|
"os"
|
||||||
"sort"
|
"sort"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/cloudflare/cfssl/helpers"
|
"git.wntrmute.dev/kyle/goutils/certlib"
|
||||||
|
"git.wntrmute.dev/kyle/goutils/lib"
|
||||||
)
|
)
|
||||||
|
|
||||||
func certPublic(cert *x509.Certificate) string {
|
func certPublic(cert *x509.Certificate) string {
|
||||||
|
@ -208,17 +209,17 @@ func displayCert(cert *x509.Certificate) {
|
||||||
}
|
}
|
||||||
|
|
||||||
func displayAllCerts(in []byte, leafOnly bool) {
|
func displayAllCerts(in []byte, leafOnly bool) {
|
||||||
certs, err := helpers.ParseCertificatesPEM(in)
|
certs, err := certlib.ParseCertificatesPEM(in)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
certs, _, err = helpers.ParseCertificatesDER(in, "")
|
certs, _, err = certlib.ParseCertificatesDER(in, "")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
Warn(TranslateCFSSLError(err), "failed to parse certificates")
|
lib.Warn(err, "failed to parse certificates")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if len(certs) == 0 {
|
if len(certs) == 0 {
|
||||||
Warnx("no certificates found")
|
lib.Warnx("no certificates found")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -236,7 +237,7 @@ func displayAllCertsWeb(uri string, leafOnly bool) {
|
||||||
ci := getConnInfo(uri)
|
ci := getConnInfo(uri)
|
||||||
conn, err := tls.Dial("tcp", ci.Addr, permissiveConfig())
|
conn, err := tls.Dial("tcp", ci.Addr, permissiveConfig())
|
||||||
if err != nil {
|
if err != nil {
|
||||||
Warn(err, "couldn't connect to %s", ci.Addr)
|
lib.Warn(err, "couldn't connect to %s", ci.Addr)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
defer conn.Close()
|
defer conn.Close()
|
||||||
|
@ -252,11 +253,11 @@ func displayAllCertsWeb(uri string, leafOnly bool) {
|
||||||
}
|
}
|
||||||
conn.Close()
|
conn.Close()
|
||||||
} else {
|
} 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 {
|
if len(state.PeerCertificates) == 0 {
|
||||||
Warnx("no certificates found")
|
lib.Warnx("no certificates found")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -266,7 +267,7 @@ func displayAllCertsWeb(uri string, leafOnly bool) {
|
||||||
}
|
}
|
||||||
|
|
||||||
if len(state.VerifiedChains) == 0 {
|
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 {
|
for i := range state.PeerCertificates {
|
||||||
displayCert(state.PeerCertificates[i])
|
displayCert(state.PeerCertificates[i])
|
||||||
}
|
}
|
||||||
|
@ -289,9 +290,9 @@ func main() {
|
||||||
flag.Parse()
|
flag.Parse()
|
||||||
|
|
||||||
if flag.NArg() == 0 || (flag.NArg() == 1 && flag.Arg(0) == "-") {
|
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 {
|
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)
|
os.Exit(1)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -306,9 +307,9 @@ func main() {
|
||||||
if strings.HasPrefix(filename, "https://") {
|
if strings.HasPrefix(filename, "https://") {
|
||||||
displayAllCertsWeb(filename, leafOnly)
|
displayAllCertsWeb(filename, leafOnly)
|
||||||
} else {
|
} else {
|
||||||
in, err := ioutil.ReadFile(filename)
|
in, err := os.ReadFile(filename)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
Warn(err, "couldn't read certificate")
|
lib.Warn(err, "couldn't read certificate")
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -3,13 +3,10 @@ package main
|
||||||
import (
|
import (
|
||||||
"crypto/tls"
|
"crypto/tls"
|
||||||
"crypto/x509"
|
"crypto/x509"
|
||||||
"errors"
|
|
||||||
"fmt"
|
"fmt"
|
||||||
"net"
|
"net"
|
||||||
"os"
|
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
cferr "github.com/cloudflare/cfssl/errors"
|
|
||||||
"github.com/kr/text"
|
"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
|
const maxLine = 78
|
||||||
|
|
||||||
func makeIndent(n int) string {
|
func makeIndent(n int) string {
|
||||||
|
|
|
@ -6,9 +6,9 @@ go_library(
|
||||||
importpath = "git.wntrmute.dev/kyle/goutils/cmd/certexpiry",
|
importpath = "git.wntrmute.dev/kyle/goutils/cmd/certexpiry",
|
||||||
visibility = ["//visibility:private"],
|
visibility = ["//visibility:private"],
|
||||||
deps = [
|
deps = [
|
||||||
|
"//certlib",
|
||||||
"//die",
|
"//die",
|
||||||
"//lib",
|
"//lib",
|
||||||
"@com_github_cloudflare_cfssl//helpers",
|
|
||||||
],
|
],
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
|
@ -10,9 +10,9 @@ import (
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"git.wntrmute.dev/kyle/goutils/certlib"
|
||||||
"git.wntrmute.dev/kyle/goutils/die"
|
"git.wntrmute.dev/kyle/goutils/die"
|
||||||
"git.wntrmute.dev/kyle/goutils/lib"
|
"git.wntrmute.dev/kyle/goutils/lib"
|
||||||
"github.com/cloudflare/cfssl/helpers"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
var warnOnly bool
|
var warnOnly bool
|
||||||
|
@ -87,7 +87,7 @@ func main() {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
certs, err := helpers.ParseCertificatesPEM(in)
|
certs, err := certlib.ParseCertificatesPEM(in)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
lib.Warn(err, "while parsing certificates")
|
lib.Warn(err, "while parsing certificates")
|
||||||
continue
|
continue
|
||||||
|
|
|
@ -6,9 +6,9 @@ go_library(
|
||||||
importpath = "git.wntrmute.dev/kyle/goutils/cmd/certverify",
|
importpath = "git.wntrmute.dev/kyle/goutils/cmd/certverify",
|
||||||
visibility = ["//visibility:private"],
|
visibility = ["//visibility:private"],
|
||||||
deps = [
|
deps = [
|
||||||
|
"//certlib",
|
||||||
"//die",
|
"//die",
|
||||||
"//lib",
|
"//lib",
|
||||||
"@com_github_cloudflare_cfssl//helpers",
|
|
||||||
"@com_github_cloudflare_cfssl//revoke",
|
"@com_github_cloudflare_cfssl//revoke",
|
||||||
],
|
],
|
||||||
)
|
)
|
||||||
|
|
|
@ -8,14 +8,14 @@ import (
|
||||||
"os"
|
"os"
|
||||||
"time"
|
"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/die"
|
||||||
"git.wntrmute.dev/kyle/goutils/lib"
|
"git.wntrmute.dev/kyle/goutils/lib"
|
||||||
"github.com/cloudflare/cfssl/helpers"
|
|
||||||
"github.com/cloudflare/cfssl/revoke"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
func printRevocation(cert *x509.Certificate) {
|
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))
|
fmt.Printf("certificate expires in %s.\n", lib.Duration(remaining))
|
||||||
|
|
||||||
revoked, ok := revoke.VerifyCertificate(cert)
|
revoked, ok := revoke.VerifyCertificate(cert)
|
||||||
|
@ -47,7 +47,7 @@ func main() {
|
||||||
if verbose {
|
if verbose {
|
||||||
fmt.Println("[+] loading root certificates from", caFile)
|
fmt.Println("[+] loading root certificates from", caFile)
|
||||||
}
|
}
|
||||||
roots, err = helpers.LoadPEMCertPool(caFile)
|
roots, err = certlib.LoadPEMCertPool(caFile)
|
||||||
die.If(err)
|
die.If(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -57,7 +57,7 @@ func main() {
|
||||||
if verbose {
|
if verbose {
|
||||||
fmt.Println("[+] loading intermediate certificates from", intFile)
|
fmt.Println("[+] loading intermediate certificates from", intFile)
|
||||||
}
|
}
|
||||||
ints, err = helpers.LoadPEMCertPool(caFile)
|
ints, err = certlib.LoadPEMCertPool(caFile)
|
||||||
die.If(err)
|
die.If(err)
|
||||||
} else {
|
} else {
|
||||||
ints = x509.NewCertPool()
|
ints = x509.NewCertPool()
|
||||||
|
@ -71,7 +71,7 @@ func main() {
|
||||||
fileData, err := ioutil.ReadFile(flag.Arg(0))
|
fileData, err := ioutil.ReadFile(flag.Arg(0))
|
||||||
die.If(err)
|
die.If(err)
|
||||||
|
|
||||||
chain, err := helpers.ParseCertificatesPEM(fileData)
|
chain, err := certlib.ParseCertificatesPEM(fileData)
|
||||||
die.If(err)
|
die.If(err)
|
||||||
if verbose {
|
if verbose {
|
||||||
fmt.Printf("[+] %s has %d certificates\n", flag.Arg(0), len(chain))
|
fmt.Printf("[+] %s has %d certificates\n", flag.Arg(0), len(chain))
|
||||||
|
|
|
@ -6,6 +6,7 @@ go_library(
|
||||||
importpath = "git.wntrmute.dev/kyle/goutils/cmd/subjhash",
|
importpath = "git.wntrmute.dev/kyle/goutils/cmd/subjhash",
|
||||||
visibility = ["//visibility:private"],
|
visibility = ["//visibility:private"],
|
||||||
deps = [
|
deps = [
|
||||||
|
"//certlib",
|
||||||
"//die",
|
"//die",
|
||||||
"//lib",
|
"//lib",
|
||||||
],
|
],
|
||||||
|
|
|
@ -9,6 +9,7 @@ import (
|
||||||
"io"
|
"io"
|
||||||
"os"
|
"os"
|
||||||
|
|
||||||
|
"git.wntrmute.dev/kyle/goutils/certlib"
|
||||||
"git.wntrmute.dev/kyle/goutils/die"
|
"git.wntrmute.dev/kyle/goutils/die"
|
||||||
"git.wntrmute.dev/kyle/goutils/lib"
|
"git.wntrmute.dev/kyle/goutils/lib"
|
||||||
)
|
)
|
||||||
|
@ -57,7 +58,7 @@ func getSubjectInfoHash(cert *x509.Certificate, issuer bool) []byte {
|
||||||
|
|
||||||
func printDigests(paths []string, issuer bool) {
|
func printDigests(paths []string, issuer bool) {
|
||||||
for _, path := range paths {
|
for _, path := range paths {
|
||||||
cert, err := lib.LoadCertificate(path)
|
cert, err := certlib.LoadCertificate(path)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
lib.Warn(err, "failed to load certificate from %s", path)
|
lib.Warn(err, "failed to load certificate from %s", path)
|
||||||
continue
|
continue
|
||||||
|
@ -82,9 +83,9 @@ func matchDigests(paths []string, issuer bool) {
|
||||||
snd := paths[1]
|
snd := paths[1]
|
||||||
paths = paths[2:]
|
paths = paths[2:]
|
||||||
|
|
||||||
fstCert, err := lib.LoadCertificate(fst)
|
fstCert, err := certlib.LoadCertificate(fst)
|
||||||
die.If(err)
|
die.If(err)
|
||||||
sndCert, err := lib.LoadCertificate(snd)
|
sndCert, err := certlib.LoadCertificate(snd)
|
||||||
die.If(err)
|
die.If(err)
|
||||||
if !bytes.Equal(getSubjectInfoHash(fstCert, issuer), getSubjectInfoHash(sndCert, issuer)) {
|
if !bytes.Equal(getSubjectInfoHash(fstCert, issuer), getSubjectInfoHash(sndCert, issuer)) {
|
||||||
lib.Warnx("certificates don't match: %s and %s", fst, snd)
|
lib.Warnx("certificates don't match: %s and %s", fst, snd)
|
||||||
|
|
|
@ -4,6 +4,7 @@ go_library(
|
||||||
name = "config",
|
name = "config",
|
||||||
srcs = [
|
srcs = [
|
||||||
"config.go",
|
"config.go",
|
||||||
|
"path.go",
|
||||||
"path_linux.go",
|
"path_linux.go",
|
||||||
],
|
],
|
||||||
importpath = "git.wntrmute.dev/kyle/goutils/config",
|
importpath = "git.wntrmute.dev/kyle/goutils/config",
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
//go:build ignore
|
//go:build !linux
|
||||||
// +build ignore
|
// +build !linux
|
||||||
|
|
||||||
package config
|
package config
|
||||||
|
|
||||||
|
|
7
go.mod
7
go.mod
|
@ -15,3 +15,10 @@ require (
|
||||||
)
|
)
|
||||||
|
|
||||||
require github.com/davecgh/go-spew v1.1.1
|
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
|
||||||
|
)
|
||||||
|
|
6
go.sum
6
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-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-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-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-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-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-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-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 h1:ntjMns5wyP/fN65tdBD4g8J5w8n015+iIIs9rtjXkY0=
|
||||||
golang.org/x/sys v0.0.0-20220412211240-33da011f77ad/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
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 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.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.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.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-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||||
golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
|
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=
|
google.golang.org/appengine v1.6.6/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc=
|
||||||
|
|
|
@ -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(
|
go_library(
|
||||||
name = "lib",
|
name = "lib",
|
||||||
|
@ -107,11 +107,3 @@ go_library(
|
||||||
"//conditions:default": [],
|
"//conditions:default": [],
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
|
|
||||||
go_test(
|
|
||||||
name = "lib_test",
|
|
||||||
size = "small",
|
|
||||||
srcs = ["lib_test.go"],
|
|
||||||
embed = [":lib"],
|
|
||||||
deps = ["//assert"],
|
|
||||||
)
|
|
||||||
|
|
79
lib/lib.go
79
lib/lib.go
|
@ -2,11 +2,7 @@
|
||||||
package lib
|
package lib
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"crypto/x509"
|
|
||||||
"encoding/pem"
|
|
||||||
"errors"
|
|
||||||
"fmt"
|
"fmt"
|
||||||
"io/ioutil"
|
|
||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"time"
|
"time"
|
||||||
|
@ -107,78 +103,3 @@ func Duration(d time.Duration) string {
|
||||||
s += fmt.Sprintf("%dh%s", hours, d)
|
s += fmt.Sprintf("%dh%s", hours, d)
|
||||||
return s
|
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)
|
|
||||||
}
|
|
||||||
|
|
|
@ -10,3 +10,14 @@ go_library(
|
||||||
"@com_github_hashicorp_go_syslog//:go-syslog",
|
"@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",
|
||||||
|
],
|
||||||
|
)
|
|
@ -1,5 +1,5 @@
|
||||||
// Package syslog is a syslog-type facility for logging.
|
// Package syslog is a syslog-type facility for logging.
|
||||||
package syslog
|
package log
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
Loading…
Reference in New Issue