Files
goutils/certlib/certlib.go
Kyle Isom 02fb85aec0 certlib: update FileKind with algo information.
Additionally, key algo wasn't being set on PEM files.
2025-11-19 14:46:17 -08:00

297 lines
6.1 KiB
Go

package certlib
import (
"bytes"
"crypto"
"crypto/dsa"
"crypto/ecdsa"
"crypto/ed25519"
"crypto/elliptic"
"crypto/rsa"
"crypto/x509"
"encoding/pem"
"errors"
"fmt"
"os"
"strings"
"git.wntrmute.dev/kyle/goutils/certlib/certerr"
)
// ReadCertificate reads a DER or PEM-encoded certificate from the
// byte slice.
func ReadCertificate(in []byte) (*x509.Certificate, []byte, error) {
in = bytes.TrimSpace(in)
if len(in) == 0 {
return nil, nil, certerr.ParsingError(certerr.ErrorSourceCertificate, certerr.ErrEmptyCertificate)
}
if in[0] == '-' {
p, remaining := pem.Decode(in)
if p == nil {
return nil, nil, certerr.ParsingError(certerr.ErrorSourceCertificate, errors.New("invalid PEM file"))
}
rest := remaining
if p.Type != pemTypeCertificate {
return nil, rest, certerr.ParsingError(
certerr.ErrorSourceCertificate,
certerr.ErrInvalidPEMType(p.Type, pemTypeCertificate),
)
}
in = p.Bytes
cert, err := x509.ParseCertificate(in)
if err != nil {
return nil, rest, certerr.ParsingError(certerr.ErrorSourceCertificate, err)
}
return cert, rest, nil
}
cert, err := x509.ParseCertificate(in)
if err != nil {
return nil, nil, certerr.ParsingError(certerr.ErrorSourceCertificate, err)
}
return cert, nil, nil
}
// ReadCertificates tries to read all the certificates in a
// PEM-encoded collection.
func ReadCertificates(in []byte) ([]*x509.Certificate, error) {
var cert *x509.Certificate
var certs []*x509.Certificate
var err error
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 := os.ReadFile(path)
if err != nil {
return nil, certerr.LoadingError(certerr.ErrorSourceCertificate, 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 := os.ReadFile(path)
if err != nil {
return nil, certerr.LoadingError(certerr.ErrorSourceCertificate, err)
}
return ReadCertificates(in)
}
func PoolFromBytes(certBytes []byte) (*x509.CertPool, error) {
pool := x509.NewCertPool()
certs, err := ReadCertificates(certBytes)
if err != nil {
return nil, fmt.Errorf("failed to read certificates: %w", err)
}
for _, cert := range certs {
pool.AddCert(cert)
}
return pool, nil
}
func ExportPrivateKeyPEM(priv crypto.PrivateKey) ([]byte, error) {
keyDER, err := x509.MarshalPKCS8PrivateKey(priv)
if err != nil {
return nil, err
}
return pem.EncodeToMemory(&pem.Block{Type: pemTypePrivateKey, Bytes: keyDER}), nil
}
func LoadCSR(path string) (*x509.CertificateRequest, error) {
in, err := os.ReadFile(path)
if err != nil {
return nil, certerr.LoadingError(certerr.ErrorSourceCSR, err)
}
req, _, err := ParseCSR(in)
return req, err
}
func ExportCSRAsPEM(req *x509.CertificateRequest) []byte {
return pem.EncodeToMemory(&pem.Block{Type: pemTypeCertificateRequest, Bytes: req.Raw})
}
type FileFormat uint8
const (
FormatPEM FileFormat = iota + 1
FormatDER
)
func (f FileFormat) String() string {
switch f {
case FormatPEM:
return "PEM"
case FormatDER:
return "DER"
default:
return "unknown"
}
}
type KeyAlgo struct {
Type x509.PublicKeyAlgorithm
Size int
curve elliptic.Curve
}
func (ka KeyAlgo) String() string {
switch ka.Type {
case x509.RSA:
return fmt.Sprintf("RSA-%d", ka.Size)
case x509.ECDSA:
return fmt.Sprintf("ECDSA-%s", ka.curve.Params().Name)
case x509.Ed25519:
return "Ed25519"
case x509.DSA:
return "DSA"
default:
return "unknown"
}
}
func publicKeyAlgoFromPublicKey(key crypto.PublicKey) KeyAlgo {
switch key := key.(type) {
case *rsa.PublicKey:
return KeyAlgo{
Type: x509.RSA,
Size: key.N.BitLen(),
}
case *ecdsa.PublicKey:
return KeyAlgo{
Type: x509.ECDSA,
curve: key.Curve,
Size: key.Params().BitSize,
}
case *ed25519.PublicKey:
return KeyAlgo{
Type: x509.Ed25519,
}
case *dsa.PublicKey:
return KeyAlgo{
Type: x509.DSA,
}
default:
return KeyAlgo{
Type: x509.UnknownPublicKeyAlgorithm,
}
}
}
func publicKeyAlgoFromKey(key crypto.PrivateKey) KeyAlgo {
switch key := key.(type) {
case *rsa.PrivateKey:
return KeyAlgo{
Type: x509.RSA,
Size: key.PublicKey.N.BitLen(),
}
case *ecdsa.PrivateKey:
return KeyAlgo{
Type: x509.ECDSA,
curve: key.PublicKey.Curve,
Size: key.Params().BitSize,
}
case *ed25519.PrivateKey:
return KeyAlgo{
Type: x509.Ed25519,
}
case *dsa.PrivateKey:
return KeyAlgo{
Type: x509.DSA,
}
default:
return KeyAlgo{
Type: x509.UnknownPublicKeyAlgorithm,
}
}
}
func publicKeyAlgoFromCert(cert *x509.Certificate) KeyAlgo {
return publicKeyAlgoFromPublicKey(cert.PublicKey)
}
func publicKeyAlgoFromCSR(csr *x509.CertificateRequest) KeyAlgo {
return publicKeyAlgoFromPublicKey(csr.PublicKeyAlgorithm)
}
type FileType struct {
Format FileFormat
Type string
Algo KeyAlgo
}
func (ft FileType) String() string {
if ft.Type == "" {
return ft.Format.String()
}
return fmt.Sprintf("%s %s (%s)", ft.Algo, ft.Type, ft.Format)
}
// FileKind returns the file type of the given file.
func FileKind(path string) (*FileType, error) {
data, err := os.ReadFile(path)
if err != nil {
return nil, err
}
ft := &FileType{Format: FormatDER}
block, _ := pem.Decode(data)
if block != nil {
data = block.Bytes
ft.Type = strings.ToLower(block.Type)
ft.Format = FormatPEM
}
cert, err := x509.ParseCertificate(data)
if err == nil {
ft.Algo = publicKeyAlgoFromCert(cert)
return ft, nil
}
csr, err := x509.ParseCertificateRequest(data)
if err == nil {
ft.Algo = publicKeyAlgoFromCSR(csr)
return ft, nil
}
priv, err := x509.ParsePKCS8PrivateKey(data)
if err == nil {
ft.Algo = publicKeyAlgoFromKey(priv)
return ft, nil
}
return nil, errors.New("certlib; unknown file type")
}