Initial commit of pkg functions
This commit is contained in:
parent
c325fa17e5
commit
6b6866077c
|
@ -0,0 +1,72 @@
|
||||||
|
/*
|
||||||
|
Package ca implements a disk file-backed certificate authority with a built-in
|
||||||
|
CRL signer. The certificate authority can issue new certificates (including the
|
||||||
|
creation of intermediate CAs) and revoke existing certificates, resulting in a
|
||||||
|
fresh CRL.
|
||||||
|
*/
|
||||||
|
package ca
|
||||||
|
|
||||||
|
import (
|
||||||
|
"crypto/rsa"
|
||||||
|
"crypto/sha1"
|
||||||
|
"crypto/x509"
|
||||||
|
"encoding/asn1"
|
||||||
|
"fmt"
|
||||||
|
"path/filepath"
|
||||||
|
|
||||||
|
"src.lwithers.me.uk/go/rsa/pkg/pemfile"
|
||||||
|
)
|
||||||
|
|
||||||
|
type CA struct {
|
||||||
|
dir string
|
||||||
|
|
||||||
|
key *rsa.PrivateKey
|
||||||
|
root *x509.Certificate
|
||||||
|
|
||||||
|
crl *x509.RevocationList
|
||||||
|
crlKey *rsa.PrivateKey
|
||||||
|
crlCert *x509.Certificate
|
||||||
|
}
|
||||||
|
|
||||||
|
const (
|
||||||
|
rootKeyFilename = "root-key.pem"
|
||||||
|
rootCertFilename = "root-cert.pem"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Open an existing certificate authority in the given directory.
|
||||||
|
func Open(dir string) (*CA, error) {
|
||||||
|
ca := &CA{
|
||||||
|
dir: dir,
|
||||||
|
}
|
||||||
|
var err error
|
||||||
|
ca.key, err = pemfile.ReadKey(filepath.Join(dir, rootKeyFilename))
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("while opening CA (root key): %w", err)
|
||||||
|
}
|
||||||
|
ca.root, err = pemfile.ReadCert(filepath.Join(dir, rootCertFilename))
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("while opening CA (root cert): %w", err)
|
||||||
|
}
|
||||||
|
if err = ca.loadCRLState(); err != nil {
|
||||||
|
return nil, fmt.Errorf("while opening CA (CRL state): %w", err)
|
||||||
|
}
|
||||||
|
return ca, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ca *CA) GetRoot() *x509.Certificate {
|
||||||
|
return ca.root
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ca *CA) GetCRLSigner() *x509.Certificate {
|
||||||
|
return ca.crlCert
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ca *CA) GetCRL() *x509.RevocationList {
|
||||||
|
return ca.crl
|
||||||
|
}
|
||||||
|
|
||||||
|
func ComputeSubjectKeyId(key *rsa.PublicKey) []byte {
|
||||||
|
der, _ := asn1.Marshal(key)
|
||||||
|
h := sha1.Sum(der)
|
||||||
|
return h[:]
|
||||||
|
}
|
|
@ -0,0 +1,162 @@
|
||||||
|
/*
|
||||||
|
Package ca implements a disk file-backed certificate authority with a built-in
|
||||||
|
CRL signer. The certificate authority can issue new certificates (including the
|
||||||
|
creation of intermediate CAs) and revoke existing certificates, resulting in a
|
||||||
|
fresh CRL.
|
||||||
|
*/
|
||||||
|
package ca
|
||||||
|
|
||||||
|
import (
|
||||||
|
"crypto/rand"
|
||||||
|
"crypto/rsa"
|
||||||
|
"crypto/x509"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"io/fs"
|
||||||
|
"math/big"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"src.lwithers.me.uk/go/rsa/pkg/pemfile"
|
||||||
|
"src.lwithers.me.uk/go/rsa/pkg/serial"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
crlSignerBits = 3072
|
||||||
|
)
|
||||||
|
|
||||||
|
func Create(dir string, template *x509.Certificate, key *rsa.PrivateKey) (*CA, error) {
|
||||||
|
return createCA(dir, template, template, key, key)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ca *CA) CreateIntermediate(dir string, template *x509.Certificate, key *rsa.PrivateKey) (*CA, error) {
|
||||||
|
return createCA(dir, template, ca.root, key, ca.key)
|
||||||
|
}
|
||||||
|
|
||||||
|
// createCA holds the shared code between Create and CreateIntermediate. All
|
||||||
|
// that really differs between those operations is the signing key and whether
|
||||||
|
// or not there is a parent certificate.
|
||||||
|
func createCA(dir string, template, parent *x509.Certificate, newRootKey, signingKey *rsa.PrivateKey) (*CA, error) {
|
||||||
|
// template must have at least the common name set
|
||||||
|
if template.Subject.CommonName == "" {
|
||||||
|
return nil, errors.New("template CA certificate must have common name set")
|
||||||
|
}
|
||||||
|
|
||||||
|
// directory must not exist
|
||||||
|
if _, err := os.Lstat(dir); !errors.Is(err, fs.ErrNotExist) {
|
||||||
|
return nil, &fs.PathError{
|
||||||
|
Op: "mkdir",
|
||||||
|
Path: dir,
|
||||||
|
Err: fs.ErrExist,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// create the directory
|
||||||
|
if err := os.Mkdir(dir, 0700); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// write out the private key
|
||||||
|
der, err := x509.MarshalPKCS8PrivateKey(newRootKey)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to marshal private key: %w", err)
|
||||||
|
}
|
||||||
|
if err = pemfile.Write(filepath.Join(dir, rootKeyFilename), pemfile.TypePKCS8PrivateKey, der); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// ensure the template is valid for a root CA
|
||||||
|
template.BasicConstraintsValid = true
|
||||||
|
template.IsCA = true
|
||||||
|
template.KeyUsage |= x509.KeyUsageCertSign
|
||||||
|
if template.SerialNumber == nil {
|
||||||
|
template.SerialNumber = serial.Rand()
|
||||||
|
}
|
||||||
|
if template.NotBefore.IsZero() {
|
||||||
|
template.NotBefore = time.Now().Add(-2 * time.Hour)
|
||||||
|
}
|
||||||
|
if template.NotAfter.IsZero() {
|
||||||
|
template.NotAfter = time.Now().Add(24 * 365 * 40 * time.Hour)
|
||||||
|
}
|
||||||
|
|
||||||
|
// create the self-signed certificate; ensure we can parse it, and write
|
||||||
|
// it to disk
|
||||||
|
der, err = x509.CreateCertificate(rand.Reader, template, parent, &newRootKey.PublicKey, signingKey)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to create/sign new root CA certificate: %w", err)
|
||||||
|
}
|
||||||
|
root, err := x509.ParseCertificate(der)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to parse just-signed root CA certificate: %w", err)
|
||||||
|
}
|
||||||
|
if err = pemfile.Write(filepath.Join(dir, rootCertFilename), pemfile.TypeX509Certificate, der); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// now create a CRL signer key and write it to disk
|
||||||
|
crlKey, err := rsa.GenerateKey(rand.Reader, crlSignerBits)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to generate new CRL signer key: %w", err)
|
||||||
|
}
|
||||||
|
der, err = x509.MarshalPKCS8PrivateKey(crlKey)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to marshal CRL signer private key: %w", err)
|
||||||
|
}
|
||||||
|
if err = pemfile.Write(filepath.Join(dir, crlKeyFilename), pemfile.TypePKCS8PrivateKey, der); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// create a template for a CRL signing certificate
|
||||||
|
crlTemplate := &x509.Certificate{
|
||||||
|
SerialNumber: serial.Rand(),
|
||||||
|
Subject: template.Subject,
|
||||||
|
SubjectKeyId: ComputeSubjectKeyId(&crlKey.PublicKey),
|
||||||
|
NotBefore: template.NotBefore,
|
||||||
|
NotAfter: template.NotAfter,
|
||||||
|
KeyUsage: x509.KeyUsageCRLSign,
|
||||||
|
BasicConstraintsValid: true,
|
||||||
|
}
|
||||||
|
crlTemplate.Subject.CommonName += " CRL signer"
|
||||||
|
|
||||||
|
// create the CRL signing certificate, parse it, and write it to disk
|
||||||
|
der, err = x509.CreateCertificate(rand.Reader, crlTemplate, root, &crlKey.PublicKey, newRootKey)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to sign CRL signer certificate: %w", err)
|
||||||
|
}
|
||||||
|
crlCert, err := x509.ParseCertificate(der)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to parse just-signed CRL signer certificate: %w", err)
|
||||||
|
}
|
||||||
|
if err = pemfile.Write(filepath.Join(dir, crlCertFilename), pemfile.TypeX509Certificate, der); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// finally, create an empty CRL
|
||||||
|
crl := &x509.RevocationList{
|
||||||
|
Number: big.NewInt(1),
|
||||||
|
ThisUpdate: template.NotBefore,
|
||||||
|
NextUpdate: template.NotAfter,
|
||||||
|
}
|
||||||
|
der, err = x509.CreateRevocationList(rand.Reader, crl, crlCert, crlKey)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to create empty CRL: %w", err)
|
||||||
|
}
|
||||||
|
crl, err = x509.ParseRevocationList(der)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to parse just-created CRL: %w", err)
|
||||||
|
}
|
||||||
|
if err = pemfile.Write(filepath.Join(dir, crlFilename), pemfile.TypeX509CRL, der); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// all done
|
||||||
|
return &CA{
|
||||||
|
dir: dir,
|
||||||
|
key: newRootKey,
|
||||||
|
root: root,
|
||||||
|
crl: crl,
|
||||||
|
crlKey: crlKey,
|
||||||
|
crlCert: crlCert,
|
||||||
|
}, nil
|
||||||
|
}
|
|
@ -0,0 +1,87 @@
|
||||||
|
/*
|
||||||
|
Package ca implements a disk file-backed certificate authority with a built-in
|
||||||
|
CRL signer. The certificate authority can issue new certificates (including the
|
||||||
|
creation of intermediate CAs) and revoke existing certificates, resulting in a
|
||||||
|
fresh CRL.
|
||||||
|
*/
|
||||||
|
package ca
|
||||||
|
|
||||||
|
import (
|
||||||
|
"crypto/rand"
|
||||||
|
"crypto/x509"
|
||||||
|
"crypto/x509/pkix"
|
||||||
|
"fmt"
|
||||||
|
"math/big"
|
||||||
|
"path/filepath"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"src.lwithers.me.uk/go/rsa/pkg/pemfile"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
crlFilename = "crl.pem"
|
||||||
|
crlKeyFilename = "crl-signer-key.pem"
|
||||||
|
crlCertFilename = "crl-signer-cert.pem"
|
||||||
|
)
|
||||||
|
|
||||||
|
func (ca *CA) loadCRLState() error {
|
||||||
|
var err error
|
||||||
|
ca.crl, err = pemfile.ReadCRL(filepath.Join(ca.dir, crlFilename))
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
ca.crlKey, err = pemfile.ReadKey(filepath.Join(ca.dir, crlKeyFilename))
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
ca.crlCert, err = pemfile.ReadCert(filepath.Join(ca.dir, crlCertFilename))
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ca *CA) Revoke(serial *big.Int) (*x509.RevocationList, error) {
|
||||||
|
// if the certificate is already revoked, update its revocation time
|
||||||
|
var found bool
|
||||||
|
for i := range ca.crl.RevokedCertificates {
|
||||||
|
if ca.crl.RevokedCertificates[i].SerialNumber.Cmp(serial) == 0 {
|
||||||
|
ca.crl.RevokedCertificates[i].RevocationTime = time.Now()
|
||||||
|
found = true
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// otherwise, add a new entry
|
||||||
|
if !found {
|
||||||
|
n := big.NewInt(0)
|
||||||
|
n.SetBytes(serial.Bytes())
|
||||||
|
ca.crl.RevokedCertificates = append(ca.crl.RevokedCertificates,
|
||||||
|
pkix.RevokedCertificate{
|
||||||
|
SerialNumber: n,
|
||||||
|
RevocationTime: time.Now(),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// increment issue number
|
||||||
|
ca.crl.Number.Add(ca.crl.Number, big.NewInt(1))
|
||||||
|
ca.crl.ThisUpdate = time.Now()
|
||||||
|
|
||||||
|
// create new DER-form certificate
|
||||||
|
der, err := x509.CreateRevocationList(rand.Reader, ca.crl, ca.crlCert, ca.crlKey)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to sign new CRL: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ensure we can parse it
|
||||||
|
newCRL, err := x509.ParseRevocationList(der)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to parse newly-signed CRL: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// save to disk
|
||||||
|
if err := pemfile.Write(filepath.Join(ca.dir, crlFilename), pemfile.TypeX509CRL, der); err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to save CRL: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// update in-memory copy
|
||||||
|
ca.crl = newCRL
|
||||||
|
return ca.crl, nil
|
||||||
|
}
|
|
@ -0,0 +1,57 @@
|
||||||
|
/*
|
||||||
|
Package ca implements a disk file-backed certificate authority with a built-in
|
||||||
|
CRL signer. The certificate authority can issue new certificates (including the
|
||||||
|
creation of intermediate CAs) and revoke existing certificates, resulting in a
|
||||||
|
fresh CRL.
|
||||||
|
*/
|
||||||
|
package ca
|
||||||
|
|
||||||
|
import (
|
||||||
|
"crypto/rand"
|
||||||
|
"crypto/rsa"
|
||||||
|
"crypto/x509"
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
|
||||||
|
"src.lwithers.me.uk/go/rsa/pkg/pemfile"
|
||||||
|
"src.lwithers.me.uk/go/rsa/pkg/serial"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
auditLogDir = "audit.log"
|
||||||
|
)
|
||||||
|
|
||||||
|
func (ca *CA) Sign(template *x509.Certificate, key *rsa.PublicKey) (*x509.Certificate, string, error) {
|
||||||
|
if template.SerialNumber == nil {
|
||||||
|
template.SerialNumber = serial.Rand()
|
||||||
|
}
|
||||||
|
|
||||||
|
// sign the certificate
|
||||||
|
der, err := x509.CreateCertificate(rand.Reader, template, ca.root, key, ca.key)
|
||||||
|
if err != nil {
|
||||||
|
return nil, "",
|
||||||
|
fmt.Errorf("failed to sign certificate: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// parse the just-signed certificate
|
||||||
|
cert, err := x509.ParseCertificate(der)
|
||||||
|
if err != nil {
|
||||||
|
return nil, "",
|
||||||
|
fmt.Errorf("failed to parse certificate after signing: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// save the newly-signed cert in the audit log
|
||||||
|
auditDir := filepath.Join(ca.dir, auditLogDir, template.SerialNumber.Text(16))
|
||||||
|
if err := os.MkdirAll(auditDir, 0700); err != nil {
|
||||||
|
return nil, "",
|
||||||
|
fmt.Errorf("failed to create audit log directory: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := pemfile.Write(filepath.Join(auditDir, "cert.pem"), pemfile.TypeX509Certificate, der); err != nil {
|
||||||
|
return nil, "",
|
||||||
|
fmt.Errorf("failed to save certificate in audit log: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return cert, auditDir, nil
|
||||||
|
}
|
|
@ -0,0 +1,7 @@
|
||||||
|
module src.lwithers.me.uk/go/rsa/pkg
|
||||||
|
|
||||||
|
go 1.20
|
||||||
|
|
||||||
|
require src.lwithers.me.uk/go/writefile v1.0.1
|
||||||
|
|
||||||
|
require golang.org/x/sys v0.5.0 // indirect
|
|
@ -0,0 +1,6 @@
|
||||||
|
golang.org/x/sys v0.0.0-20191228213918-04cbcbbfeed8 h1:JA8d3MPx/IToSyXZG/RhwYEtfrKO1Fxrqe8KrkiLXKM=
|
||||||
|
golang.org/x/sys v0.0.0-20191228213918-04cbcbbfeed8/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
|
golang.org/x/sys v0.5.0 h1:MUK/U/4lj1t1oPg0HfuXDN/Z1wv31ZJ/YcPiGccS4DU=
|
||||||
|
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
|
src.lwithers.me.uk/go/writefile v1.0.1 h1:bwBGtvyZfCxFIM14e1aYgJWlZuowKkwJx53OJlUPd0s=
|
||||||
|
src.lwithers.me.uk/go/writefile v1.0.1/go.mod h1:NahlmRCtB7kg4ai+zHZgxXdUs+MR8VqWG8mql35TsxA=
|
|
@ -0,0 +1,283 @@
|
||||||
|
package inspect
|
||||||
|
|
||||||
|
import (
|
||||||
|
"crypto/rsa"
|
||||||
|
"crypto/x509"
|
||||||
|
"errors"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
// DERCertificate attempts to parse a DER-form X.509 certificate, and returns
|
||||||
|
// information about it.
|
||||||
|
func DERCertificateInfo(loc string, der []byte) Info {
|
||||||
|
c, err := x509.ParseCertificate(der)
|
||||||
|
if err != nil {
|
||||||
|
return &BadInfo{
|
||||||
|
Typ: TypeX509Certificate,
|
||||||
|
Loc: loc,
|
||||||
|
Underlying: err,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return CertificateInfo(loc, c)
|
||||||
|
}
|
||||||
|
|
||||||
|
// CertificateInfo returns structured information about the given certificate.
|
||||||
|
// It can return BadInfo if the certificate is not for an RSA key. Note that
|
||||||
|
// the returned Info structure holds a reference to the passed certificate.
|
||||||
|
func CertificateInfo(loc string, cert *x509.Certificate) Info {
|
||||||
|
pubkey, ok := cert.PublicKey.(*rsa.PublicKey)
|
||||||
|
if !ok {
|
||||||
|
return &BadInfo{
|
||||||
|
Typ: TypeX509Certificate,
|
||||||
|
Loc: loc,
|
||||||
|
Underlying: errors.New("certificate is not for an RSA key"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
c := &Certificate{
|
||||||
|
Loc: loc,
|
||||||
|
Key: PublicKeyInfo(loc, pubkey),
|
||||||
|
Cert: cert,
|
||||||
|
}
|
||||||
|
|
||||||
|
if (cert.KeyUsage & x509.KeyUsageDigitalSignature) != 0 {
|
||||||
|
c.ValidUses = append(c.ValidUses, "DigitalSignature")
|
||||||
|
}
|
||||||
|
if (cert.KeyUsage & x509.KeyUsageCertSign) != 0 {
|
||||||
|
c.ValidUses = append(c.ValidUses, "CertSign")
|
||||||
|
}
|
||||||
|
if (cert.KeyUsage & x509.KeyUsageCRLSign) != 0 {
|
||||||
|
c.ValidUses = append(c.ValidUses, "CRLSign")
|
||||||
|
}
|
||||||
|
for _, use := range cert.ExtKeyUsage {
|
||||||
|
switch use {
|
||||||
|
case x509.ExtKeyUsageAny:
|
||||||
|
c.ValidUses = append(c.ValidUses, "any")
|
||||||
|
case x509.ExtKeyUsageServerAuth:
|
||||||
|
c.ValidUses = append(c.ValidUses, "ServerAuth")
|
||||||
|
case x509.ExtKeyUsageClientAuth:
|
||||||
|
c.ValidUses = append(c.ValidUses, "ClientAuth")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := cert.CheckSignatureFrom(cert); err == nil {
|
||||||
|
c.IsSelfSigned = true
|
||||||
|
}
|
||||||
|
|
||||||
|
return c
|
||||||
|
}
|
||||||
|
|
||||||
|
type Certificate struct {
|
||||||
|
// Loc is the location the certificate was encountered.
|
||||||
|
Loc string
|
||||||
|
|
||||||
|
// Key holds information about the public key of the certificate.
|
||||||
|
Key *PublicKey
|
||||||
|
|
||||||
|
// Cert is the original certificate.
|
||||||
|
Cert *x509.Certificate
|
||||||
|
|
||||||
|
// The following section contains any computed values that are not
|
||||||
|
// immediately obtainable from the certificate.
|
||||||
|
|
||||||
|
// ValidUses is a set of named certificate uses.
|
||||||
|
ValidUses []string
|
||||||
|
|
||||||
|
// IsSelfSigned reports whether the certificate is self-signed.
|
||||||
|
IsSelfSigned bool
|
||||||
|
}
|
||||||
|
|
||||||
|
// Type indicates this is an X.509 certificate.
|
||||||
|
func (c *Certificate) Type() Type {
|
||||||
|
return TypeX509Certificate
|
||||||
|
}
|
||||||
|
|
||||||
|
// Location returns the location at which the certificate was encountered.
|
||||||
|
func (c *Certificate) Location() string {
|
||||||
|
return c.Loc
|
||||||
|
}
|
||||||
|
|
||||||
|
// Info returns structured information about the X.509 Certificate.
|
||||||
|
func (c *Certificate) Info() []Section {
|
||||||
|
var s []Section
|
||||||
|
s = append(s, c.UsageSection())
|
||||||
|
s = append(s, c.ValiditySection())
|
||||||
|
s = append(s, c.SubjectSection())
|
||||||
|
s = append(s, c.IssuerSection())
|
||||||
|
if c.Cert.IsCA {
|
||||||
|
s = append(s, c.CASection())
|
||||||
|
}
|
||||||
|
s = append(s, c.Key.PublicKeyInfoSection())
|
||||||
|
return s
|
||||||
|
}
|
||||||
|
|
||||||
|
// UsageSection returns information regarding the key usage bits.
|
||||||
|
func (c *Certificate) UsageSection() Section {
|
||||||
|
return Section{
|
||||||
|
Title: "Certificate usage",
|
||||||
|
Fields: []Field{
|
||||||
|
Field{
|
||||||
|
Key: "Is CA",
|
||||||
|
Value: c.Cert.IsCA,
|
||||||
|
},
|
||||||
|
Field{
|
||||||
|
Key: "Self-signed",
|
||||||
|
Value: c.IsSelfSigned,
|
||||||
|
},
|
||||||
|
Field{
|
||||||
|
Key: "Valid uses",
|
||||||
|
Value: strings.Join(c.ValidUses, " | "),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ValiditySection returns information regarding the validity of the certificate.
|
||||||
|
func (c *Certificate) ValiditySection() Section {
|
||||||
|
return Section{
|
||||||
|
Title: "Validity",
|
||||||
|
Fields: []Field{
|
||||||
|
Field{
|
||||||
|
Key: "Valid from",
|
||||||
|
Value: c.Cert.NotBefore,
|
||||||
|
},
|
||||||
|
Field{
|
||||||
|
Key: "Valid until",
|
||||||
|
Value: c.Cert.NotAfter,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// SubjectSection returns information about the Subject of the certificate.
|
||||||
|
func (c *Certificate) SubjectSection() Section {
|
||||||
|
f := []Field{
|
||||||
|
Field{
|
||||||
|
Key: "Description",
|
||||||
|
Value: c.Cert.Subject.CommonName,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
f = appendX509DNField(f, c.Cert.Subject)
|
||||||
|
if len(c.Cert.DNSNames) > 0 {
|
||||||
|
f = append(f, Field{
|
||||||
|
Key: "Hostnames",
|
||||||
|
Value: c.Cert.DNSNames,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
if len(c.Cert.IPAddresses) > 0 {
|
||||||
|
var ips []string
|
||||||
|
for _, ip := range c.Cert.IPAddresses {
|
||||||
|
ips = append(ips, ip.String())
|
||||||
|
}
|
||||||
|
f = append(f, Field{
|
||||||
|
Key: "IP addresses",
|
||||||
|
Value: ips,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
if len(c.Cert.SubjectKeyId) > 0 {
|
||||||
|
f = append(f, Field{
|
||||||
|
Key: "Key ID",
|
||||||
|
Value: FormatHexBytes(c.Cert.SubjectKeyId),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
return Section{
|
||||||
|
Title: "Subject",
|
||||||
|
Fields: f,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// IssuerSection returns information about the Issuer of the certificate.
|
||||||
|
func (c *Certificate) IssuerSection() Section {
|
||||||
|
f := []Field{
|
||||||
|
Field{
|
||||||
|
Key: "Serial",
|
||||||
|
Value: FormatHexBytes(c.Cert.SerialNumber.Bytes()),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
if c.Cert.Issuer.CommonName != "" {
|
||||||
|
f = append(f, Field{
|
||||||
|
Key: "Description",
|
||||||
|
Value: c.Cert.Issuer.CommonName,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
f = appendX509DNField(f, c.Cert.Issuer)
|
||||||
|
if len(c.Cert.AuthorityKeyId) > 0 {
|
||||||
|
f = append(f, Field{
|
||||||
|
Key: "Key ID",
|
||||||
|
Value: FormatHexBytes(c.Cert.AuthorityKeyId),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
return Section{
|
||||||
|
Title: "Issuer",
|
||||||
|
Fields: f,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// CASection returns information about a CA certificate.
|
||||||
|
func (c *Certificate) CASection() Section {
|
||||||
|
var f []Field
|
||||||
|
|
||||||
|
switch {
|
||||||
|
case c.Cert.MaxPathLen < 0,
|
||||||
|
c.Cert.MaxPathLen == 0 && !c.Cert.MaxPathLenZero:
|
||||||
|
f = append(f, Field{
|
||||||
|
Key: "Max path len",
|
||||||
|
Value: "unlimited",
|
||||||
|
})
|
||||||
|
default:
|
||||||
|
f = append(f, Field{
|
||||||
|
Key: "Max path len",
|
||||||
|
Value: c.Cert.MaxPathLen,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(c.Cert.PermittedDNSDomains) > 0 {
|
||||||
|
f = append(f, Field{
|
||||||
|
Key: "Permitted DNS names",
|
||||||
|
Value: c.Cert.PermittedDNSDomains,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(c.Cert.ExcludedDNSDomains) > 0 {
|
||||||
|
f = append(f, Field{
|
||||||
|
Key: "Excluded DNS names",
|
||||||
|
Value: c.Cert.ExcludedDNSDomains,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(c.Cert.PermittedIPRanges) > 0 {
|
||||||
|
var ips []string
|
||||||
|
for _, ip := range c.Cert.PermittedIPRanges {
|
||||||
|
ips = append(ips, ip.String())
|
||||||
|
}
|
||||||
|
f = append(f, Field{
|
||||||
|
Key: "Permitted IP addresses",
|
||||||
|
Value: ips,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(c.Cert.ExcludedIPRanges) > 0 {
|
||||||
|
var ips []string
|
||||||
|
for _, ip := range c.Cert.ExcludedIPRanges {
|
||||||
|
ips = append(ips, ip.String())
|
||||||
|
}
|
||||||
|
f = append(f, Field{
|
||||||
|
Key: "Excluded IP addresses",
|
||||||
|
Value: ips,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(c.Cert.CRLDistributionPoints) > 0 {
|
||||||
|
f = append(f, Field{
|
||||||
|
Key: "CRL distribution points",
|
||||||
|
Value: c.Cert.CRLDistributionPoints,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
return Section{
|
||||||
|
Title: "Certificate authority",
|
||||||
|
Fields: f,
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,119 @@
|
||||||
|
package inspect
|
||||||
|
|
||||||
|
import (
|
||||||
|
"crypto/x509"
|
||||||
|
)
|
||||||
|
|
||||||
|
// DERCRLInfo attempts to parse a DER-form X.509 CRL and returns information
|
||||||
|
// about it.
|
||||||
|
func DERCRLInfo(loc string, der []byte) Info {
|
||||||
|
crl, err := x509.ParseRevocationList(der)
|
||||||
|
if err != nil {
|
||||||
|
return &BadInfo{
|
||||||
|
Typ: TypeX509CRL,
|
||||||
|
Loc: loc,
|
||||||
|
Underlying: err,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return CRLInfo(loc, crl)
|
||||||
|
}
|
||||||
|
|
||||||
|
// CRLInfo returns information about a CRL. Note it holds a reference to the
|
||||||
|
// crl argument.
|
||||||
|
func CRLInfo(loc string, crl *x509.RevocationList) *CRL {
|
||||||
|
return &CRL{
|
||||||
|
Loc: loc,
|
||||||
|
CRL: crl,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// CRL holds structured information about a CRL. It implements Info.
|
||||||
|
type CRL struct {
|
||||||
|
// Loc is the location the CRL was encountered.
|
||||||
|
Loc string
|
||||||
|
|
||||||
|
// CRL is the raw CRL.
|
||||||
|
CRL *x509.RevocationList
|
||||||
|
}
|
||||||
|
|
||||||
|
// Type indicates this is a private key.
|
||||||
|
func (crl *CRL) Type() Type {
|
||||||
|
return TypeX509CRL
|
||||||
|
}
|
||||||
|
|
||||||
|
// Location returns the location data stored by PrivateKeyInfo.
|
||||||
|
func (crl *CRL) Location() string {
|
||||||
|
return crl.Loc
|
||||||
|
}
|
||||||
|
|
||||||
|
// Info returns structured information about the prvate key.
|
||||||
|
func (crl *CRL) Info() []Section {
|
||||||
|
return []Section{
|
||||||
|
crl.CRLSection(),
|
||||||
|
crl.IssuerSection(),
|
||||||
|
crl.RevocationSection(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// CRLSection returns metadata about the CRL itself.
|
||||||
|
func (crl *CRL) CRLSection() Section {
|
||||||
|
return Section{
|
||||||
|
Title: "Metadata",
|
||||||
|
Fields: []Field{
|
||||||
|
Field{
|
||||||
|
Key: "Serial number",
|
||||||
|
Value: FormatHexBytes(crl.CRL.Number.Bytes()),
|
||||||
|
},
|
||||||
|
Field{
|
||||||
|
Key: "Valid from",
|
||||||
|
Value: crl.CRL.ThisUpdate,
|
||||||
|
},
|
||||||
|
Field{
|
||||||
|
Key: "Valid until",
|
||||||
|
Value: crl.CRL.NextUpdate,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (crl *CRL) IssuerSection() Section {
|
||||||
|
f := []Field{
|
||||||
|
Field{
|
||||||
|
Key: "Description",
|
||||||
|
Value: crl.CRL.Issuer.CommonName,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
f = appendX509DNField(f, crl.CRL.Issuer)
|
||||||
|
if len(crl.CRL.AuthorityKeyId) > 0 {
|
||||||
|
f = append(f, Field{
|
||||||
|
Key: "Key ID",
|
||||||
|
Value: FormatHexBytes(crl.CRL.AuthorityKeyId),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
return Section{
|
||||||
|
Title: "Issuer",
|
||||||
|
Fields: f,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (crl *CRL) RevocationSection() Section {
|
||||||
|
revoked := make([]string, len(crl.CRL.RevokedCertificates))
|
||||||
|
for i := range crl.CRL.RevokedCertificates {
|
||||||
|
revoked[i] = FormatHexBytes(crl.CRL.RevokedCertificates[i].SerialNumber.Bytes())
|
||||||
|
}
|
||||||
|
|
||||||
|
return Section{
|
||||||
|
Title: "Revoked certificates",
|
||||||
|
Fields: []Field{
|
||||||
|
Field{
|
||||||
|
Key: "Count",
|
||||||
|
Value: len(revoked),
|
||||||
|
},
|
||||||
|
Field{
|
||||||
|
Key: "Revoked",
|
||||||
|
Value: revoked,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,103 @@
|
||||||
|
package inspect
|
||||||
|
|
||||||
|
import (
|
||||||
|
"crypto/rsa"
|
||||||
|
"crypto/x509"
|
||||||
|
"errors"
|
||||||
|
)
|
||||||
|
|
||||||
|
// DERCSRInfo attempts to parse a DER-form X.509 CSR and returns information
|
||||||
|
// about it.
|
||||||
|
func DERCSRInfo(loc string, der []byte) Info {
|
||||||
|
csr, err := x509.ParseCertificateRequest(der)
|
||||||
|
if err != nil {
|
||||||
|
return &BadInfo{
|
||||||
|
Typ: TypeX509CSR,
|
||||||
|
Loc: loc,
|
||||||
|
Underlying: err,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return CSRInfo(loc, csr)
|
||||||
|
}
|
||||||
|
|
||||||
|
// CSRInfo returns information about a CSR. Note it holds a reference to the
|
||||||
|
// csr argument.
|
||||||
|
func CSRInfo(loc string, csr *x509.CertificateRequest) Info {
|
||||||
|
key, ok := csr.PublicKey.(*rsa.PublicKey)
|
||||||
|
if !ok {
|
||||||
|
return &BadInfo{
|
||||||
|
Typ: TypeX509CSR,
|
||||||
|
Loc: loc,
|
||||||
|
Underlying: errors.New("CSR public key is not RSA"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return &CSR{
|
||||||
|
Loc: loc,
|
||||||
|
CSR: csr,
|
||||||
|
Public: PublicKeyInfo(loc, key),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// CSR holds structured information about a CSR. It implements Info.
|
||||||
|
type CSR struct {
|
||||||
|
// Loc is the location the CSR was encountered.
|
||||||
|
Loc string
|
||||||
|
|
||||||
|
// CSR is the raw CSR.
|
||||||
|
CSR *x509.CertificateRequest
|
||||||
|
|
||||||
|
// Public holds information about the public portion of the key.
|
||||||
|
Public *PublicKey
|
||||||
|
}
|
||||||
|
|
||||||
|
// Type indicates this is a private key.
|
||||||
|
func (csr *CSR) Type() Type {
|
||||||
|
return TypeX509CSR
|
||||||
|
}
|
||||||
|
|
||||||
|
// Location returns the location data stored by PrivateKeyInfo.
|
||||||
|
func (csr *CSR) Location() string {
|
||||||
|
return csr.Loc
|
||||||
|
}
|
||||||
|
|
||||||
|
// Info returns structured information about the prvate key.
|
||||||
|
func (csr *CSR) Info() []Section {
|
||||||
|
return []Section{
|
||||||
|
csr.SubjectSection(),
|
||||||
|
csr.Public.PublicKeyInfoSection(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// SubjectSection returns information about the subject.
|
||||||
|
func (csr *CSR) SubjectSection() Section {
|
||||||
|
f := []Field{
|
||||||
|
Field{
|
||||||
|
Key: "Description",
|
||||||
|
Value: csr.CSR.Subject.CommonName,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
f = appendX509DNField(f, csr.CSR.Subject)
|
||||||
|
if len(csr.CSR.DNSNames) > 0 {
|
||||||
|
f = append(f, Field{
|
||||||
|
Key: "Hostnames",
|
||||||
|
Value: csr.CSR.DNSNames,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
if len(csr.CSR.IPAddresses) > 0 {
|
||||||
|
var ips []string
|
||||||
|
for _, ip := range csr.CSR.IPAddresses {
|
||||||
|
ips = append(ips, ip.String())
|
||||||
|
}
|
||||||
|
f = append(f, Field{
|
||||||
|
Key: "IP addresses",
|
||||||
|
Value: ips,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
return Section{
|
||||||
|
Title: "Subject",
|
||||||
|
Fields: f,
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,110 @@
|
||||||
|
/*
|
||||||
|
Package inspect describes RSA-related objects such as private keys and X.509
|
||||||
|
certificates using structured, annotated key-value pairs.
|
||||||
|
*/
|
||||||
|
package inspect
|
||||||
|
|
||||||
|
// Type of item.
|
||||||
|
type Type int
|
||||||
|
|
||||||
|
const (
|
||||||
|
TypePrivateKey Type = iota
|
||||||
|
TypePublicKey
|
||||||
|
TypeX509Certificate
|
||||||
|
TypeX509CRL
|
||||||
|
TypeX509CSR
|
||||||
|
TypeTLSConnectionState
|
||||||
|
)
|
||||||
|
|
||||||
|
func (t Type) String() string {
|
||||||
|
switch t {
|
||||||
|
case TypePrivateKey:
|
||||||
|
return "RSA private key"
|
||||||
|
case TypePublicKey:
|
||||||
|
return "RSA public key"
|
||||||
|
case TypeX509Certificate:
|
||||||
|
return "X.509 certificate"
|
||||||
|
case TypeX509CRL:
|
||||||
|
return "certificate revocation list"
|
||||||
|
case TypeX509CSR:
|
||||||
|
return "certificate signing request"
|
||||||
|
case TypeTLSConnectionState:
|
||||||
|
return "TLS connection state"
|
||||||
|
default:
|
||||||
|
return "???"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Info about an item.
|
||||||
|
type Info interface {
|
||||||
|
// Type of item.
|
||||||
|
Type() Type
|
||||||
|
|
||||||
|
// Location where the item was encountered (e.g. line number for a file,
|
||||||
|
// or position in certificate chain for TLS handshake.
|
||||||
|
Location() string
|
||||||
|
|
||||||
|
// Info returns a structured set of information about the item.
|
||||||
|
Info() []Section
|
||||||
|
}
|
||||||
|
|
||||||
|
// Section is a set of related fields.
|
||||||
|
type Section struct {
|
||||||
|
// Title of section.
|
||||||
|
Title string
|
||||||
|
|
||||||
|
// Fields contain a related set of key, value pairs.
|
||||||
|
Fields []Field
|
||||||
|
}
|
||||||
|
|
||||||
|
// Field is an aspect of information about an item.
|
||||||
|
type Field struct {
|
||||||
|
// Key is the name of the aspect.
|
||||||
|
Key string
|
||||||
|
|
||||||
|
// Value is its value.
|
||||||
|
Value any
|
||||||
|
}
|
||||||
|
|
||||||
|
// BadInfo is an item which cannot be parsed, e.g. an invalid DER-encoded item
|
||||||
|
// within a PEM block. It implements both error and Info.
|
||||||
|
type BadInfo struct {
|
||||||
|
Typ Type
|
||||||
|
Loc string
|
||||||
|
Underlying error
|
||||||
|
}
|
||||||
|
|
||||||
|
// Type reports the type of item that was being parsed.
|
||||||
|
func (b *BadInfo) Type() Type {
|
||||||
|
return b.Typ
|
||||||
|
}
|
||||||
|
|
||||||
|
// Location reports further information about the location of the item.
|
||||||
|
func (b *BadInfo) Location() string {
|
||||||
|
return b.Loc
|
||||||
|
}
|
||||||
|
|
||||||
|
// Info reports an "Error encountered" section with an "Error" field.
|
||||||
|
func (b *BadInfo) Info() []Section {
|
||||||
|
return []Section{
|
||||||
|
Section{
|
||||||
|
Title: "Error encountered",
|
||||||
|
Fields: []Field{
|
||||||
|
Field{
|
||||||
|
Key: "Error",
|
||||||
|
Value: b.Underlying,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Error returns the underlying error string.
|
||||||
|
func (b *BadInfo) Error() string {
|
||||||
|
return b.Underlying.Error()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Unwrap returns the underlying error.
|
||||||
|
func (b *BadInfo) Unwrap() error {
|
||||||
|
return b.Underlying
|
||||||
|
}
|
|
@ -0,0 +1,44 @@
|
||||||
|
package inspect
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"encoding/pem"
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
"src.lwithers.me.uk/go/rsa/pkg/pemfile"
|
||||||
|
)
|
||||||
|
|
||||||
|
// LoadPEM returns a set of information about a PEM file.
|
||||||
|
func LoadPEM(data []byte) []Info {
|
||||||
|
var (
|
||||||
|
info []Info
|
||||||
|
line int = 1
|
||||||
|
)
|
||||||
|
for {
|
||||||
|
p, rest := pem.Decode(data)
|
||||||
|
if p == nil {
|
||||||
|
return info
|
||||||
|
}
|
||||||
|
loc := fmt.Sprintf("line %d", line)
|
||||||
|
|
||||||
|
switch p.Type {
|
||||||
|
case pemfile.TypePKCS1PrivateKey:
|
||||||
|
info = append(info, PKCS1PrivateKeyInfo(loc, p.Bytes))
|
||||||
|
case pemfile.TypePKCS8PrivateKey:
|
||||||
|
info = append(info, PKCS8PrivateKeyInfo(loc, p.Bytes))
|
||||||
|
case pemfile.TypePKCS1PublicKey:
|
||||||
|
info = append(info, PKCS1PublicKeyInfo(loc, p.Bytes))
|
||||||
|
case pemfile.TypePKIXPublicKey:
|
||||||
|
info = append(info, PKIXPublicKeyInfo(loc, p.Bytes))
|
||||||
|
case pemfile.TypeX509Certificate:
|
||||||
|
info = append(info, DERCertificateInfo(loc, p.Bytes))
|
||||||
|
case pemfile.TypeX509CRL:
|
||||||
|
info = append(info, DERCRLInfo(loc, p.Bytes))
|
||||||
|
case pemfile.TypeX509CSR:
|
||||||
|
info = append(info, DERCSRInfo(loc, p.Bytes))
|
||||||
|
}
|
||||||
|
|
||||||
|
line += bytes.Count(data[:len(data)-len(rest)], []byte{'\n'})
|
||||||
|
data = rest
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,112 @@
|
||||||
|
package inspect
|
||||||
|
|
||||||
|
import (
|
||||||
|
"crypto/rsa"
|
||||||
|
"crypto/x509"
|
||||||
|
"errors"
|
||||||
|
)
|
||||||
|
|
||||||
|
// PKCS1PrivateKeyInfo attempts to parse a DER-form PKCS#1-encoded private RSA
|
||||||
|
// key, and returns information about it.
|
||||||
|
func PKCS1PrivateKeyInfo(loc string, der []byte) Info {
|
||||||
|
key, err := x509.ParsePKCS1PrivateKey(der)
|
||||||
|
if err != nil {
|
||||||
|
return &BadInfo{
|
||||||
|
Typ: TypePrivateKey,
|
||||||
|
Loc: loc,
|
||||||
|
Underlying: err,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return PrivateKeyInfo(loc, key)
|
||||||
|
}
|
||||||
|
|
||||||
|
// PKCS1PrivateKeyInfo attempts to parse a DER-form PKCS#8-encoded private RSA
|
||||||
|
// key, and returns information about it.
|
||||||
|
func PKCS8PrivateKeyInfo(loc string, der []byte) Info {
|
||||||
|
key, err := x509.ParsePKCS8PrivateKey(der)
|
||||||
|
if err != nil {
|
||||||
|
return &BadInfo{
|
||||||
|
Typ: TypePrivateKey,
|
||||||
|
Loc: loc,
|
||||||
|
Underlying: err,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
rsakey, ok := key.(*rsa.PrivateKey)
|
||||||
|
if !ok {
|
||||||
|
return &BadInfo{
|
||||||
|
Typ: TypePrivateKey,
|
||||||
|
Underlying: errors.New("PKCS#8 private key ytpe is not RSA"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return PrivateKeyInfo(loc, rsakey)
|
||||||
|
}
|
||||||
|
|
||||||
|
// PrivateKeyInfo returns structured information about the given RSA private key.
|
||||||
|
func PrivateKeyInfo(loc string, key *rsa.PrivateKey) *PrivateKey {
|
||||||
|
pl := make([]int, len(key.Primes))
|
||||||
|
for i, n := range key.Primes {
|
||||||
|
pl[i] = n.BitLen()
|
||||||
|
}
|
||||||
|
|
||||||
|
return &PrivateKey{
|
||||||
|
Loc: loc,
|
||||||
|
Primes: len(key.Primes),
|
||||||
|
PrimeLens: pl,
|
||||||
|
Public: PublicKeyInfo(loc, &key.PublicKey),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// PrivateKey holds structured information about an RSA private key. It
|
||||||
|
// implements Info.
|
||||||
|
type PrivateKey struct {
|
||||||
|
// Loc is the location the key was encountered.
|
||||||
|
Loc string
|
||||||
|
|
||||||
|
// Primes is the number of primes, ≥ 2.
|
||||||
|
Primes int
|
||||||
|
|
||||||
|
// PrimeLens holds the bit length of each prime.
|
||||||
|
PrimeLens []int
|
||||||
|
|
||||||
|
// Public holds information about the public portion of the key.
|
||||||
|
Public *PublicKey
|
||||||
|
}
|
||||||
|
|
||||||
|
// Type indicates this is a private key.
|
||||||
|
func (priv *PrivateKey) Type() Type {
|
||||||
|
return TypePrivateKey
|
||||||
|
}
|
||||||
|
|
||||||
|
// Location returns the location data stored by PrivateKeyInfo.
|
||||||
|
func (priv *PrivateKey) Location() string {
|
||||||
|
return priv.Loc
|
||||||
|
}
|
||||||
|
|
||||||
|
// Info returns structured information about the prvate key.
|
||||||
|
func (priv *PrivateKey) Info() []Section {
|
||||||
|
return []Section{
|
||||||
|
priv.PrivateKeyInfoSection(),
|
||||||
|
priv.Public.PublicKeyInfoSection(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// PrivateKeyInfoSection returns the RSA private key specific information
|
||||||
|
// section.
|
||||||
|
func (priv *PrivateKey) PrivateKeyInfoSection() Section {
|
||||||
|
return Section{
|
||||||
|
Title: "RSA private key",
|
||||||
|
Fields: []Field{
|
||||||
|
Field{
|
||||||
|
Key: "Primes",
|
||||||
|
Value: priv.Primes,
|
||||||
|
},
|
||||||
|
Field{
|
||||||
|
Key: "Prime lengths",
|
||||||
|
Value: priv.PrimeLens,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,145 @@
|
||||||
|
package inspect
|
||||||
|
|
||||||
|
import (
|
||||||
|
"crypto/rsa"
|
||||||
|
"crypto/sha512"
|
||||||
|
"crypto/x509"
|
||||||
|
"encoding/binary"
|
||||||
|
"errors"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
// PKCS1PublicKeyInfo attempts to parse a DER-form PKCS#1-encoded public RSA
|
||||||
|
// key, and returns information about it.
|
||||||
|
func PKCS1PublicKeyInfo(loc string, der []byte) Info {
|
||||||
|
key, err := x509.ParsePKCS1PublicKey(der)
|
||||||
|
if err != nil {
|
||||||
|
return &BadInfo{
|
||||||
|
Typ: TypePublicKey,
|
||||||
|
Loc: loc,
|
||||||
|
Underlying: err,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return PublicKeyInfo(loc, key)
|
||||||
|
}
|
||||||
|
|
||||||
|
// PKCS1PublicKeyInfo attempts to parse a DER-form PKIX-encoded public RSA
|
||||||
|
// key, and returns information about it.
|
||||||
|
func PKIXPublicKeyInfo(loc string, der []byte) Info {
|
||||||
|
key, err := x509.ParsePKIXPublicKey(der)
|
||||||
|
if err != nil {
|
||||||
|
return &BadInfo{
|
||||||
|
Typ: TypePublicKey,
|
||||||
|
Loc: loc,
|
||||||
|
Underlying: err,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
rsakey, ok := key.(*rsa.PublicKey)
|
||||||
|
if !ok {
|
||||||
|
return &BadInfo{
|
||||||
|
Typ: TypePublicKey,
|
||||||
|
Loc: loc,
|
||||||
|
Underlying: errors.New("PKIX public key type is not RSA"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return PublicKeyInfo(loc, rsakey)
|
||||||
|
}
|
||||||
|
|
||||||
|
// PublicKeyInfo returns structured information about the given RSA public key.
|
||||||
|
func PublicKeyInfo(loc string, key *rsa.PublicKey) *PublicKey {
|
||||||
|
return &PublicKey{
|
||||||
|
Loc: loc,
|
||||||
|
Bits: key.N.BitLen(),
|
||||||
|
Fingerprint: KeyFingerprint(key),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// PublicKey holds structured information about an RSA public key. It
|
||||||
|
// implements Info.
|
||||||
|
type PublicKey struct {
|
||||||
|
// Loc is the location the key was encountered.
|
||||||
|
Loc string
|
||||||
|
|
||||||
|
// Bits is the key size.
|
||||||
|
Bits int
|
||||||
|
|
||||||
|
// Fingerprint of the modulus, which can be used to compare keys.
|
||||||
|
Fingerprint Fingerprint
|
||||||
|
}
|
||||||
|
|
||||||
|
// Type indicates this a a public key.
|
||||||
|
func (pub *PublicKey) Type() Type {
|
||||||
|
return TypePublicKey
|
||||||
|
}
|
||||||
|
|
||||||
|
// Location returns the location data stored by PublicKeyInfo.
|
||||||
|
func (pub *PublicKey) Location() string {
|
||||||
|
return pub.Loc
|
||||||
|
}
|
||||||
|
|
||||||
|
// Info returns structured information about the public key.
|
||||||
|
func (pub *PublicKey) Info() []Section {
|
||||||
|
return []Section{
|
||||||
|
pub.PublicKeyInfoSection(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// PublicKeyInfoSection returns the RSA public key specific information
|
||||||
|
// section.
|
||||||
|
func (pub *PublicKey) PublicKeyInfoSection() Section {
|
||||||
|
return Section{
|
||||||
|
Title: "RSA public key",
|
||||||
|
Fields: []Field{
|
||||||
|
Field{
|
||||||
|
Key: "Bits",
|
||||||
|
Value: pub.Bits,
|
||||||
|
},
|
||||||
|
Field{
|
||||||
|
Key: "Fingerprint",
|
||||||
|
Value: pub.Fingerprint,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fingerprint of a key.
|
||||||
|
type Fingerprint []byte
|
||||||
|
|
||||||
|
// KeyFingerprint computes the fingerprint of an RSA key. It only requires the
|
||||||
|
// public section of the key for this.
|
||||||
|
func KeyFingerprint(key *rsa.PublicKey) Fingerprint {
|
||||||
|
f := sha512.New512_224()
|
||||||
|
var b [8]byte
|
||||||
|
E := uint64(key.E)
|
||||||
|
binary.BigEndian.PutUint64(b[:], E)
|
||||||
|
f.Write(b[:])
|
||||||
|
f.Write(key.N.Bytes())
|
||||||
|
return Fingerprint(f.Sum(nil))
|
||||||
|
}
|
||||||
|
|
||||||
|
// String returns a nicely-formatted hex string that is easy to read/compare.
|
||||||
|
func (f Fingerprint) String() string {
|
||||||
|
return FormatHexBytes([]byte(f))
|
||||||
|
}
|
||||||
|
|
||||||
|
const hexDig = "0123456789ABCDEF"
|
||||||
|
|
||||||
|
// FormatHexBytes returns a nicely-formatted hex string that is easy to read/compare.
|
||||||
|
func FormatHexBytes(b []byte) string {
|
||||||
|
var s strings.Builder
|
||||||
|
|
||||||
|
for len(b) > 1 {
|
||||||
|
s.WriteByte(hexDig[b[0]>>4])
|
||||||
|
s.WriteByte(hexDig[b[0]&15])
|
||||||
|
s.WriteByte(':')
|
||||||
|
b = b[1:]
|
||||||
|
}
|
||||||
|
if len(b) > 0 {
|
||||||
|
s.WriteByte(hexDig[b[0]>>4])
|
||||||
|
s.WriteByte(hexDig[b[0]&15])
|
||||||
|
}
|
||||||
|
return s.String()
|
||||||
|
}
|
|
@ -0,0 +1,158 @@
|
||||||
|
package inspect
|
||||||
|
|
||||||
|
import (
|
||||||
|
"crypto/tls"
|
||||||
|
"fmt"
|
||||||
|
)
|
||||||
|
|
||||||
|
// ConnectionState returns a set of information about a TLS ConnectionState.
|
||||||
|
func ConnectionState(c tls.ConnectionState) []Info {
|
||||||
|
var info = []Info{
|
||||||
|
&TLSConnectionState{
|
||||||
|
Version: c.Version,
|
||||||
|
CipherSuite: c.CipherSuite,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for i, cert := range c.PeerCertificates {
|
||||||
|
loc := "leaf certificate"
|
||||||
|
if i > 0 {
|
||||||
|
loc = fmt.Sprintf("intermediate certificate #%d", i)
|
||||||
|
}
|
||||||
|
info = append(info, CertificateInfo(loc, cert))
|
||||||
|
}
|
||||||
|
|
||||||
|
return info
|
||||||
|
}
|
||||||
|
|
||||||
|
// PrivateKeyInfo returns structured information about the given RSA private key.
|
||||||
|
/*
|
||||||
|
func PrivateKeyInfo(loc string, key *rsa.PrivateKey) *PrivateKey {
|
||||||
|
pl := make([]int, len(key.Primes))
|
||||||
|
for i, n := range key.Primes {
|
||||||
|
pl[i] = n.BitLen()
|
||||||
|
}
|
||||||
|
|
||||||
|
return &PrivateKey{
|
||||||
|
Loc: loc,
|
||||||
|
Primes: len(key.Primes),
|
||||||
|
PrimeLens: pl,
|
||||||
|
Public: PublicKeyInfo(loc, &key.PublicKey),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
*/
|
||||||
|
|
||||||
|
// TLSSecurity provides an indicative security assessment of a negotiated TLS
|
||||||
|
// connection.
|
||||||
|
type TLSSecurity int
|
||||||
|
|
||||||
|
const (
|
||||||
|
TLSSecurityStrong = iota
|
||||||
|
TLSSecurityWeak
|
||||||
|
TLSSecurityInsecure
|
||||||
|
)
|
||||||
|
|
||||||
|
func (ts TLSSecurity) String() string {
|
||||||
|
switch ts {
|
||||||
|
case TLSSecurityStrong:
|
||||||
|
return "strong"
|
||||||
|
case TLSSecurityWeak:
|
||||||
|
return "weak"
|
||||||
|
case TLSSecurityInsecure:
|
||||||
|
return "insecure"
|
||||||
|
}
|
||||||
|
return "???"
|
||||||
|
}
|
||||||
|
|
||||||
|
// TLSConnectionState holds structured information about a negotiated TLS
|
||||||
|
// connection. It does not include the peer's certificate information.
|
||||||
|
type TLSConnectionState struct {
|
||||||
|
// Version of TLS protocol used.
|
||||||
|
Version uint16
|
||||||
|
|
||||||
|
// CipherSuite negotiated.
|
||||||
|
CipherSuite uint16
|
||||||
|
}
|
||||||
|
|
||||||
|
// Type indicates this is a private key.
|
||||||
|
func (tcs *TLSConnectionState) Type() Type {
|
||||||
|
return TypeTLSConnectionState
|
||||||
|
}
|
||||||
|
|
||||||
|
// Location returns the location data stored by PrivateKeyInfo.
|
||||||
|
func (tcs *TLSConnectionState) Location() string {
|
||||||
|
return "TLS handshake"
|
||||||
|
}
|
||||||
|
|
||||||
|
// Info returns structured information about the prvate key.
|
||||||
|
func (tcs *TLSConnectionState) Info() []Section {
|
||||||
|
var security TLSSecurity = TLSSecurityStrong
|
||||||
|
|
||||||
|
ciphersuite := tls.CipherSuiteName(tcs.CipherSuite)
|
||||||
|
|
||||||
|
switch tcs.CipherSuite {
|
||||||
|
case tls.TLS_RSA_WITH_RC4_128_SHA,
|
||||||
|
tls.TLS_RSA_WITH_3DES_EDE_CBC_SHA,
|
||||||
|
tls.TLS_RSA_WITH_AES_128_CBC_SHA,
|
||||||
|
tls.TLS_RSA_WITH_AES_256_CBC_SHA,
|
||||||
|
tls.TLS_RSA_WITH_AES_128_CBC_SHA256,
|
||||||
|
tls.TLS_RSA_WITH_AES_128_GCM_SHA256,
|
||||||
|
tls.TLS_RSA_WITH_AES_256_GCM_SHA384:
|
||||||
|
// using plain RSA for key exchange isn't good
|
||||||
|
security = TLSSecurityInsecure
|
||||||
|
|
||||||
|
case tls.TLS_ECDHE_RSA_WITH_RC4_128_SHA,
|
||||||
|
tls.TLS_ECDHE_RSA_WITH_3DES_EDE_CBC_SHA:
|
||||||
|
// broken/bad symmetric ciphers
|
||||||
|
security = TLSSecurityInsecure
|
||||||
|
|
||||||
|
case tls.TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA,
|
||||||
|
tls.TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA,
|
||||||
|
tls.TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA256:
|
||||||
|
// CBC now considered weaker
|
||||||
|
security = TLSSecurityWeak
|
||||||
|
|
||||||
|
default:
|
||||||
|
// allow Go's opinion to augment our own
|
||||||
|
for _, bad := range tls.InsecureCipherSuites() {
|
||||||
|
if tcs.CipherSuite == bad.ID {
|
||||||
|
security = TLSSecurityInsecure
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var version string
|
||||||
|
switch tcs.Version {
|
||||||
|
case tls.VersionTLS10:
|
||||||
|
version = "TLSv1.0"
|
||||||
|
security = TLSSecurityInsecure
|
||||||
|
case tls.VersionTLS11:
|
||||||
|
version = "TLSv1.1"
|
||||||
|
security = TLSSecurityInsecure
|
||||||
|
case tls.VersionTLS12:
|
||||||
|
version = "TLSv1.2"
|
||||||
|
case tls.VersionTLS13:
|
||||||
|
version = "TLSv1.3"
|
||||||
|
}
|
||||||
|
|
||||||
|
return []Section{
|
||||||
|
Section{
|
||||||
|
Title: "TLS protocol negotatiated",
|
||||||
|
Fields: []Field{
|
||||||
|
Field{
|
||||||
|
Key: "Version",
|
||||||
|
Value: version,
|
||||||
|
},
|
||||||
|
Field{
|
||||||
|
Key: "Cipher suite",
|
||||||
|
Value: ciphersuite,
|
||||||
|
},
|
||||||
|
Field{
|
||||||
|
Key: "Security",
|
||||||
|
Value: security,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,18 @@
|
||||||
|
package inspect
|
||||||
|
|
||||||
|
import "crypto/x509/pkix"
|
||||||
|
|
||||||
|
// appendX509DNField will append a field containing the X.509 distinguished name
|
||||||
|
// (approximately) if it contains anything more than a common name.
|
||||||
|
func appendX509DNField(in []Field, name pkix.Name) []Field {
|
||||||
|
name.CommonName = ""
|
||||||
|
s := name.String()
|
||||||
|
if s == "" {
|
||||||
|
return in
|
||||||
|
}
|
||||||
|
|
||||||
|
return append(in, Field{
|
||||||
|
Key: "Other",
|
||||||
|
Value: s,
|
||||||
|
})
|
||||||
|
}
|
|
@ -0,0 +1,185 @@
|
||||||
|
package pemfile
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bufio"
|
||||||
|
"crypto/rsa"
|
||||||
|
"crypto/x509"
|
||||||
|
"encoding/pem"
|
||||||
|
"errors"
|
||||||
|
"io/fs"
|
||||||
|
"os"
|
||||||
|
|
||||||
|
"src.lwithers.me.uk/go/writefile"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
TypePKCS1PrivateKey = "RSA PRIVATE KEY"
|
||||||
|
TypePKCS8PrivateKey = "PRIVATE KEY"
|
||||||
|
TypePKCS1PublicKey = "RSA PUBLIC KEY"
|
||||||
|
TypePKIXPublicKey = "PUBLIC KEY"
|
||||||
|
TypeX509Certificate = "CERTIFICATE"
|
||||||
|
TypeX509CSR = "CERTIFICATE REQUEST"
|
||||||
|
TypeX509CRL = "X509 CRL"
|
||||||
|
)
|
||||||
|
|
||||||
|
func Write(filename, typ string, der []byte) error {
|
||||||
|
finalFname, f, err := writefile.New(filename)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
defer writefile.Abort(f)
|
||||||
|
|
||||||
|
out := bufio.NewWriter(f)
|
||||||
|
pem.Encode(out, &pem.Block{
|
||||||
|
Type: typ,
|
||||||
|
Bytes: der,
|
||||||
|
})
|
||||||
|
if err = out.Flush(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return writefile.Commit(finalFname, f)
|
||||||
|
}
|
||||||
|
|
||||||
|
func ReadKey(filename string) (*rsa.PrivateKey, error) {
|
||||||
|
raw, err := os.ReadFile(filename)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
for {
|
||||||
|
block, rest := pem.Decode(raw)
|
||||||
|
raw = rest
|
||||||
|
switch {
|
||||||
|
case block == nil:
|
||||||
|
return nil, &fs.PathError{
|
||||||
|
Op: "decode",
|
||||||
|
Path: filename,
|
||||||
|
Err: errors.New("no private key PEM block found"),
|
||||||
|
}
|
||||||
|
|
||||||
|
case block.Type == TypePKCS1PrivateKey:
|
||||||
|
key, err := x509.ParsePKCS1PrivateKey(block.Bytes)
|
||||||
|
if err != nil {
|
||||||
|
return nil, &fs.PathError{
|
||||||
|
Op: "parse key",
|
||||||
|
Path: filename,
|
||||||
|
Err: err,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return key, nil
|
||||||
|
|
||||||
|
case block.Type == TypePKCS8PrivateKey:
|
||||||
|
key, err := x509.ParsePKCS8PrivateKey(block.Bytes)
|
||||||
|
if err != nil {
|
||||||
|
return nil, &fs.PathError{
|
||||||
|
Op: "parse key",
|
||||||
|
Path: filename,
|
||||||
|
Err: err,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
rsakey, ok := key.(*rsa.PrivateKey)
|
||||||
|
if !ok {
|
||||||
|
return nil, &fs.PathError{
|
||||||
|
Op: "parse key",
|
||||||
|
Path: filename,
|
||||||
|
Err: errors.New("not an RSA key"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return rsakey, nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func ReadCert(filename string) (*x509.Certificate, error) {
|
||||||
|
raw, err := os.ReadFile(filename)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
for {
|
||||||
|
block, rest := pem.Decode(raw)
|
||||||
|
raw = rest
|
||||||
|
switch {
|
||||||
|
case block == nil:
|
||||||
|
return nil, &fs.PathError{
|
||||||
|
Op: "decode",
|
||||||
|
Path: filename,
|
||||||
|
Err: errors.New("no certificate PEM block found"),
|
||||||
|
}
|
||||||
|
|
||||||
|
case block.Type == TypeX509Certificate:
|
||||||
|
cert, err := x509.ParseCertificate(block.Bytes)
|
||||||
|
if err != nil {
|
||||||
|
return nil, &fs.PathError{
|
||||||
|
Op: "parse cert",
|
||||||
|
Path: filename,
|
||||||
|
Err: err,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return cert, nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func ReadCRL(filename string) (*x509.RevocationList, error) {
|
||||||
|
raw, err := os.ReadFile(filename)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
for {
|
||||||
|
block, rest := pem.Decode(raw)
|
||||||
|
raw = rest
|
||||||
|
switch {
|
||||||
|
case block == nil:
|
||||||
|
return nil, &fs.PathError{
|
||||||
|
Op: "decode",
|
||||||
|
Path: filename,
|
||||||
|
Err: errors.New("no CRL PEM block found"),
|
||||||
|
}
|
||||||
|
|
||||||
|
case block.Type == TypeX509CRL:
|
||||||
|
crl, err := x509.ParseRevocationList(block.Bytes)
|
||||||
|
if err != nil {
|
||||||
|
return nil, &fs.PathError{
|
||||||
|
Op: "parse cert",
|
||||||
|
Path: filename,
|
||||||
|
Err: err,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return crl, nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func ReadCSR(filename string) (*x509.CertificateRequest, error) {
|
||||||
|
raw, err := os.ReadFile(filename)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
for {
|
||||||
|
block, rest := pem.Decode(raw)
|
||||||
|
raw = rest
|
||||||
|
switch {
|
||||||
|
case block == nil:
|
||||||
|
return nil, &fs.PathError{
|
||||||
|
Op: "decode",
|
||||||
|
Path: filename,
|
||||||
|
Err: errors.New("no CSR PEM block found"),
|
||||||
|
}
|
||||||
|
|
||||||
|
case block.Type == TypeX509CSR:
|
||||||
|
csr, err := x509.ParseCertificateRequest(block.Bytes)
|
||||||
|
if err != nil {
|
||||||
|
return nil, &fs.PathError{
|
||||||
|
Op: "parse CSR",
|
||||||
|
Path: filename,
|
||||||
|
Err: err,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return csr, nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,21 @@
|
||||||
|
/*
|
||||||
|
Package serial provides serial number related functionality.
|
||||||
|
*/
|
||||||
|
package serial
|
||||||
|
|
||||||
|
import (
|
||||||
|
"crypto/rand"
|
||||||
|
"io"
|
||||||
|
"math/big"
|
||||||
|
)
|
||||||
|
|
||||||
|
const randBits = 160
|
||||||
|
|
||||||
|
// Rand returns a random serial number with 159 bits of entropy.
|
||||||
|
func Rand() *big.Int {
|
||||||
|
q := make([]byte, randBits/8)
|
||||||
|
io.ReadFull(rand.Reader, q)
|
||||||
|
// this just means all serial numbers are justified to the same bitlen, which is nice for tools!
|
||||||
|
q[0] |= 0x80
|
||||||
|
return big.NewInt(0).SetBytes(q)
|
||||||
|
}
|
Loading…
Reference in New Issue