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 (
|
||||
"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",
|
||||
visibility = ["//visibility:private"],
|
||||
deps = [
|
||||
"@com_github_cloudflare_cfssl//errors",
|
||||
"@com_github_cloudflare_cfssl//helpers",
|
||||
"//certlib",
|
||||
"//lib",
|
||||
"@com_github_kr_text//:text",
|
||||
],
|
||||
)
|
||||
|
|
|
@ -12,12 +12,13 @@ import (
|
|||
"crypto/x509/pkix"
|
||||
"flag"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"io"
|
||||
"os"
|
||||
"sort"
|
||||
"strings"
|
||||
|
||||
"github.com/cloudflare/cfssl/helpers"
|
||||
"git.wntrmute.dev/kyle/goutils/certlib"
|
||||
"git.wntrmute.dev/kyle/goutils/lib"
|
||||
)
|
||||
|
||||
func certPublic(cert *x509.Certificate) string {
|
||||
|
@ -208,17 +209,17 @@ func displayCert(cert *x509.Certificate) {
|
|||
}
|
||||
|
||||
func displayAllCerts(in []byte, leafOnly bool) {
|
||||
certs, err := helpers.ParseCertificatesPEM(in)
|
||||
certs, err := certlib.ParseCertificatesPEM(in)
|
||||
if err != nil {
|
||||
certs, _, err = helpers.ParseCertificatesDER(in, "")
|
||||
certs, _, err = certlib.ParseCertificatesDER(in, "")
|
||||
if err != nil {
|
||||
Warn(TranslateCFSSLError(err), "failed to parse certificates")
|
||||
lib.Warn(err, "failed to parse certificates")
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
if len(certs) == 0 {
|
||||
Warnx("no certificates found")
|
||||
lib.Warnx("no certificates found")
|
||||
return
|
||||
}
|
||||
|
||||
|
@ -236,7 +237,7 @@ func displayAllCertsWeb(uri string, leafOnly bool) {
|
|||
ci := getConnInfo(uri)
|
||||
conn, err := tls.Dial("tcp", ci.Addr, permissiveConfig())
|
||||
if err != nil {
|
||||
Warn(err, "couldn't connect to %s", ci.Addr)
|
||||
lib.Warn(err, "couldn't connect to %s", ci.Addr)
|
||||
return
|
||||
}
|
||||
defer conn.Close()
|
||||
|
@ -252,11 +253,11 @@ func displayAllCertsWeb(uri string, leafOnly bool) {
|
|||
}
|
||||
conn.Close()
|
||||
} else {
|
||||
Warn(err, "TLS verification error with server name %s", ci.Host)
|
||||
lib.Warn(err, "TLS verification error with server name %s", ci.Host)
|
||||
}
|
||||
|
||||
if len(state.PeerCertificates) == 0 {
|
||||
Warnx("no certificates found")
|
||||
lib.Warnx("no certificates found")
|
||||
return
|
||||
}
|
||||
|
||||
|
@ -266,7 +267,7 @@ func displayAllCertsWeb(uri string, leafOnly bool) {
|
|||
}
|
||||
|
||||
if len(state.VerifiedChains) == 0 {
|
||||
Warnx("no verified chains found; using peer chain")
|
||||
lib.Warnx("no verified chains found; using peer chain")
|
||||
for i := range state.PeerCertificates {
|
||||
displayCert(state.PeerCertificates[i])
|
||||
}
|
||||
|
@ -289,9 +290,9 @@ func main() {
|
|||
flag.Parse()
|
||||
|
||||
if flag.NArg() == 0 || (flag.NArg() == 1 && flag.Arg(0) == "-") {
|
||||
certs, err := ioutil.ReadAll(os.Stdin)
|
||||
certs, err := io.ReadAll(os.Stdin)
|
||||
if err != nil {
|
||||
Warn(err, "couldn't read certificates from standard input")
|
||||
lib.Warn(err, "couldn't read certificates from standard input")
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
|
@ -306,9 +307,9 @@ func main() {
|
|||
if strings.HasPrefix(filename, "https://") {
|
||||
displayAllCertsWeb(filename, leafOnly)
|
||||
} else {
|
||||
in, err := ioutil.ReadFile(filename)
|
||||
in, err := os.ReadFile(filename)
|
||||
if err != nil {
|
||||
Warn(err, "couldn't read certificate")
|
||||
lib.Warn(err, "couldn't read certificate")
|
||||
continue
|
||||
}
|
||||
|
||||
|
|
|
@ -3,13 +3,10 @@ package main
|
|||
import (
|
||||
"crypto/tls"
|
||||
"crypto/x509"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net"
|
||||
"os"
|
||||
"strings"
|
||||
|
||||
cferr "github.com/cloudflare/cfssl/errors"
|
||||
"github.com/kr/text"
|
||||
)
|
||||
|
||||
|
@ -89,34 +86,6 @@ func sigAlgoHash(a x509.SignatureAlgorithm) string {
|
|||
}
|
||||
}
|
||||
|
||||
// TranslateCFSSLError turns a CFSSL error into a more readable string.
|
||||
func TranslateCFSSLError(err error) error {
|
||||
if err == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
// printing errors as json is terrible
|
||||
if cfsslError, ok := err.(*cferr.Error); ok {
|
||||
err = errors.New(cfsslError.Message)
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
// Warnx displays a formatted error message to standard error, à la
|
||||
// warnx(3).
|
||||
func Warnx(format string, a ...interface{}) (int, error) {
|
||||
format += "\n"
|
||||
return fmt.Fprintf(os.Stderr, format, a...)
|
||||
}
|
||||
|
||||
// Warn displays a formatted error message to standard output,
|
||||
// appending the error string, à la warn(3).
|
||||
func Warn(err error, format string, a ...interface{}) (int, error) {
|
||||
format += ": %v\n"
|
||||
a = append(a, err)
|
||||
return fmt.Fprintf(os.Stderr, format, a...)
|
||||
}
|
||||
|
||||
const maxLine = 78
|
||||
|
||||
func makeIndent(n int) string {
|
||||
|
|
|
@ -6,9 +6,9 @@ go_library(
|
|||
importpath = "git.wntrmute.dev/kyle/goutils/cmd/certexpiry",
|
||||
visibility = ["//visibility:private"],
|
||||
deps = [
|
||||
"//certlib",
|
||||
"//die",
|
||||
"//lib",
|
||||
"@com_github_cloudflare_cfssl//helpers",
|
||||
],
|
||||
)
|
||||
|
||||
|
|
|
@ -10,9 +10,9 @@ import (
|
|||
"strings"
|
||||
"time"
|
||||
|
||||
"git.wntrmute.dev/kyle/goutils/certlib"
|
||||
"git.wntrmute.dev/kyle/goutils/die"
|
||||
"git.wntrmute.dev/kyle/goutils/lib"
|
||||
"github.com/cloudflare/cfssl/helpers"
|
||||
)
|
||||
|
||||
var warnOnly bool
|
||||
|
@ -87,7 +87,7 @@ func main() {
|
|||
continue
|
||||
}
|
||||
|
||||
certs, err := helpers.ParseCertificatesPEM(in)
|
||||
certs, err := certlib.ParseCertificatesPEM(in)
|
||||
if err != nil {
|
||||
lib.Warn(err, "while parsing certificates")
|
||||
continue
|
||||
|
|
|
@ -6,9 +6,9 @@ go_library(
|
|||
importpath = "git.wntrmute.dev/kyle/goutils/cmd/certverify",
|
||||
visibility = ["//visibility:private"],
|
||||
deps = [
|
||||
"//certlib",
|
||||
"//die",
|
||||
"//lib",
|
||||
"@com_github_cloudflare_cfssl//helpers",
|
||||
"@com_github_cloudflare_cfssl//revoke",
|
||||
],
|
||||
)
|
||||
|
|
|
@ -8,14 +8,14 @@ import (
|
|||
"os"
|
||||
"time"
|
||||
|
||||
"git.wntrmute.dev/kyle/goutils/certlib"
|
||||
"git.wntrmute.dev/kyle/goutils/certlib/revoke"
|
||||
"git.wntrmute.dev/kyle/goutils/die"
|
||||
"git.wntrmute.dev/kyle/goutils/lib"
|
||||
"github.com/cloudflare/cfssl/helpers"
|
||||
"github.com/cloudflare/cfssl/revoke"
|
||||
)
|
||||
|
||||
func printRevocation(cert *x509.Certificate) {
|
||||
remaining := cert.NotAfter.Sub(time.Now())
|
||||
remaining := time.Until(cert.NotAfter)
|
||||
fmt.Printf("certificate expires in %s.\n", lib.Duration(remaining))
|
||||
|
||||
revoked, ok := revoke.VerifyCertificate(cert)
|
||||
|
@ -47,7 +47,7 @@ func main() {
|
|||
if verbose {
|
||||
fmt.Println("[+] loading root certificates from", caFile)
|
||||
}
|
||||
roots, err = helpers.LoadPEMCertPool(caFile)
|
||||
roots, err = certlib.LoadPEMCertPool(caFile)
|
||||
die.If(err)
|
||||
}
|
||||
|
||||
|
@ -57,7 +57,7 @@ func main() {
|
|||
if verbose {
|
||||
fmt.Println("[+] loading intermediate certificates from", intFile)
|
||||
}
|
||||
ints, err = helpers.LoadPEMCertPool(caFile)
|
||||
ints, err = certlib.LoadPEMCertPool(caFile)
|
||||
die.If(err)
|
||||
} else {
|
||||
ints = x509.NewCertPool()
|
||||
|
@ -71,7 +71,7 @@ func main() {
|
|||
fileData, err := ioutil.ReadFile(flag.Arg(0))
|
||||
die.If(err)
|
||||
|
||||
chain, err := helpers.ParseCertificatesPEM(fileData)
|
||||
chain, err := certlib.ParseCertificatesPEM(fileData)
|
||||
die.If(err)
|
||||
if verbose {
|
||||
fmt.Printf("[+] %s has %d certificates\n", flag.Arg(0), len(chain))
|
||||
|
|
|
@ -6,6 +6,7 @@ go_library(
|
|||
importpath = "git.wntrmute.dev/kyle/goutils/cmd/subjhash",
|
||||
visibility = ["//visibility:private"],
|
||||
deps = [
|
||||
"//certlib",
|
||||
"//die",
|
||||
"//lib",
|
||||
],
|
||||
|
|
|
@ -9,6 +9,7 @@ import (
|
|||
"io"
|
||||
"os"
|
||||
|
||||
"git.wntrmute.dev/kyle/goutils/certlib"
|
||||
"git.wntrmute.dev/kyle/goutils/die"
|
||||
"git.wntrmute.dev/kyle/goutils/lib"
|
||||
)
|
||||
|
@ -57,7 +58,7 @@ func getSubjectInfoHash(cert *x509.Certificate, issuer bool) []byte {
|
|||
|
||||
func printDigests(paths []string, issuer bool) {
|
||||
for _, path := range paths {
|
||||
cert, err := lib.LoadCertificate(path)
|
||||
cert, err := certlib.LoadCertificate(path)
|
||||
if err != nil {
|
||||
lib.Warn(err, "failed to load certificate from %s", path)
|
||||
continue
|
||||
|
@ -82,9 +83,9 @@ func matchDigests(paths []string, issuer bool) {
|
|||
snd := paths[1]
|
||||
paths = paths[2:]
|
||||
|
||||
fstCert, err := lib.LoadCertificate(fst)
|
||||
fstCert, err := certlib.LoadCertificate(fst)
|
||||
die.If(err)
|
||||
sndCert, err := lib.LoadCertificate(snd)
|
||||
sndCert, err := certlib.LoadCertificate(snd)
|
||||
die.If(err)
|
||||
if !bytes.Equal(getSubjectInfoHash(fstCert, issuer), getSubjectInfoHash(sndCert, issuer)) {
|
||||
lib.Warnx("certificates don't match: %s and %s", fst, snd)
|
||||
|
|
|
@ -4,6 +4,7 @@ go_library(
|
|||
name = "config",
|
||||
srcs = [
|
||||
"config.go",
|
||||
"path.go",
|
||||
"path_linux.go",
|
||||
],
|
||||
importpath = "git.wntrmute.dev/kyle/goutils/config",
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
//go:build ignore
|
||||
// +build ignore
|
||||
//go:build !linux
|
||||
// +build !linux
|
||||
|
||||
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/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-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20201010224723-4f7140c49acb/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
|
||||
golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
|
||||
golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220412211240-33da011f77ad h1:ntjMns5wyP/fN65tdBD4g8J5w8n015+iIIs9rtjXkY0=
|
||||
golang.org/x/sys v0.0.0-20220412211240-33da011f77ad/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1 h1:v+OssWQX+hTHEmOBgwxdZxK4zHq3yOs8F9J7mk0PY8E=
|
||||
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
|
||||
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
|
||||
google.golang.org/appengine v1.6.6/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc=
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
load("@io_bazel_rules_go//go:def.bzl", "go_library", "go_test")
|
||||
load("@io_bazel_rules_go//go:def.bzl", "go_library")
|
||||
|
||||
go_library(
|
||||
name = "lib",
|
||||
|
@ -107,11 +107,3 @@ go_library(
|
|||
"//conditions:default": [],
|
||||
}),
|
||||
)
|
||||
|
||||
go_test(
|
||||
name = "lib_test",
|
||||
size = "small",
|
||||
srcs = ["lib_test.go"],
|
||||
embed = [":lib"],
|
||||
deps = ["//assert"],
|
||||
)
|
||||
|
|
79
lib/lib.go
79
lib/lib.go
|
@ -2,11 +2,7 @@
|
|||
package lib
|
||||
|
||||
import (
|
||||
"crypto/x509"
|
||||
"encoding/pem"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"time"
|
||||
|
@ -107,78 +103,3 @@ func Duration(d time.Duration) string {
|
|||
s += fmt.Sprintf("%dh%s", hours, d)
|
||||
return s
|
||||
}
|
||||
|
||||
// ReadCertificate reads a DER or PEM-encoded certificate from the
|
||||
// byte slice.
|
||||
func ReadCertificate(in []byte) (cert *x509.Certificate, rest []byte, err error) {
|
||||
if len(in) == 0 {
|
||||
err = errors.New("lib: empty certificate")
|
||||
return
|
||||
}
|
||||
|
||||
if in[0] == '-' {
|
||||
p, remaining := pem.Decode(in)
|
||||
if p == nil {
|
||||
err = errors.New("lib: invalid PEM file")
|
||||
return
|
||||
}
|
||||
|
||||
rest = remaining
|
||||
if p.Type != "CERTIFICATE" {
|
||||
err = fmt.Errorf("lib: expected a CERTIFICATE PEM file, but have %s", p.Type)
|
||||
return
|
||||
}
|
||||
|
||||
in = p.Bytes
|
||||
}
|
||||
|
||||
cert, err = x509.ParseCertificate(in)
|
||||
return
|
||||
}
|
||||
|
||||
// ReadCertificates tries to read all the certificates in a
|
||||
// PEM-encoded collection.
|
||||
func ReadCertificates(in []byte) (certs []*x509.Certificate, err error) {
|
||||
var cert *x509.Certificate
|
||||
for {
|
||||
cert, in, err = ReadCertificate(in)
|
||||
if err != nil {
|
||||
break
|
||||
}
|
||||
|
||||
if cert == nil {
|
||||
break
|
||||
}
|
||||
|
||||
certs = append(certs, cert)
|
||||
if len(in) == 0 {
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
return certs, err
|
||||
}
|
||||
|
||||
// LoadCertificate tries to read a single certificate from disk. If
|
||||
// the file contains multiple certificates (e.g. a chain), only the
|
||||
// first certificate is returned.
|
||||
func LoadCertificate(path string) (*x509.Certificate, error) {
|
||||
in, err := ioutil.ReadFile(path)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
cert, _, err := ReadCertificate(in)
|
||||
return cert, err
|
||||
}
|
||||
|
||||
// LoadCertificates tries to read all the certificates in a file,
|
||||
// returning them in the order that it found them in the file.
|
||||
func LoadCertificates(path string) ([]*x509.Certificate, error) {
|
||||
in, err := ioutil.ReadFile(path)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return ReadCertificates(in)
|
||||
}
|
||||
|
|
|
@ -10,3 +10,14 @@ go_library(
|
|||
"@com_github_hashicorp_go_syslog//:go-syslog",
|
||||
],
|
||||
)
|
||||
|
||||
go_library(
|
||||
name = "log",
|
||||
srcs = ["logger.go"],
|
||||
importpath = "git.wntrmute.dev/kyle/goutils/log",
|
||||
visibility = ["//visibility:public"],
|
||||
deps = [
|
||||
"@com_github_davecgh_go_spew//spew",
|
||||
"@com_github_hashicorp_go_syslog//:go-syslog",
|
||||
],
|
||||
)
|
|
@ -1,5 +1,5 @@
|
|||
// Package syslog is a syslog-type facility for logging.
|
||||
package syslog
|
||||
package log
|
||||
|
||||
import (
|
||||
"fmt"
|
Loading…
Reference in New Issue