Overhauling certlib.

LICENSE to Apache 2.0.
This commit is contained in:
2025-11-15 22:00:29 -08:00
parent 8ed30e9960
commit f3b4838cf6
12 changed files with 574 additions and 211 deletions

33
certlib/certerr/doc.go Normal file
View File

@@ -0,0 +1,33 @@
// Package certerr provides typed errors and helpers for certificate-related
// operations across the repository. It standardizes error construction and
// matching so callers can reliably branch on error source/kind using the
// Go 1.13+ `errors.Is` and `errors.As` helpers.
//
// Guidelines
// - Always wrap underlying causes using the helper constructors or with
// fmt.Errorf("context: %w", err).
// - Do not include sensitive data (keys, passwords, tokens) in error
// messages; add only non-sensitive, actionable context.
// - Prefer programmatic checks via errors.Is (for sentinel errors) and
// errors.As (to retrieve *certerr.Error) rather than relying on error
// string contents.
//
// Typical usage
//
// if err := doParse(); err != nil {
// return certerr.ParsingError(certerr.ErrorSourceCertificate, err)
// }
//
// Callers may branch on error kinds and sources:
//
// var e *certerr.Error
// if errors.As(err, &e) {
// switch e.Kind {
// case certerr.KindParse:
// // handle parse error
// }
// }
//
// Sentinel errors are provided for common conditions like
// `certerr.ErrEncryptedPrivateKey` and can be matched with `errors.Is`.
package certerr

View File

@@ -37,6 +37,48 @@ const (
ErrorSourceKeypair ErrorSourceType = 5
)
// ErrorKind is a broad classification describing what went wrong.
type ErrorKind uint8
const (
KindParse ErrorKind = iota + 1
KindDecode
KindVerify
KindLoad
)
func (k ErrorKind) String() string {
switch k {
case KindParse:
return "parse"
case KindDecode:
return "decode"
case KindVerify:
return "verify"
case KindLoad:
return "load"
default:
return "unknown"
}
}
// Error is a typed, wrapped error with structured context for programmatic checks.
// It implements error and supports errors.Is/As via Unwrap.
type Error struct {
Source ErrorSourceType // which domain produced the error (certificate, private key, etc.)
Kind ErrorKind // operation category (parse, decode, verify, load)
Op string // optional operation or function name
Err error // wrapped cause
}
func (e *Error) Error() string {
// Keep message format consistent with existing helpers: "failed to <kind> <source>: <err>"
// Do not include Op by default to preserve existing output expectations.
return fmt.Sprintf("failed to %s %s: %v", e.Kind.String(), e.Source.String(), e.Err)
}
func (e *Error) Unwrap() error { return e.Err }
// InvalidPEMType is used to indicate that we were expecting one type of PEM
// file, but saw another.
type InvalidPEMType struct {
@@ -61,19 +103,19 @@ func ErrInvalidPEMType(have string, want ...string) error {
}
func LoadingError(t ErrorSourceType, err error) error {
return fmt.Errorf("failed to load %s from disk: %w", t, err)
return &Error{Source: t, Kind: KindLoad, Err: err}
}
func ParsingError(t ErrorSourceType, err error) error {
return fmt.Errorf("failed to parse %s: %w", t, err)
return &Error{Source: t, Kind: KindParse, Err: err}
}
func DecodeError(t ErrorSourceType, err error) error {
return fmt.Errorf("failed to decode %s: %w", t, err)
return &Error{Source: t, Kind: KindDecode, Err: err}
}
func VerifyError(t ErrorSourceType, err error) error {
return fmt.Errorf("failed to verify %s: %w", t, err)
return &Error{Source: t, Kind: KindVerify, Err: err}
}
var ErrEncryptedPrivateKey = errors.New("private key is encrypted")

View File

@@ -0,0 +1,55 @@
package certerr
import (
"errors"
"strings"
"testing"
)
func TestTypedErrorWrappingAndFormatting(t *testing.T) {
cause := errors.New("bad data")
err := DecodeError(ErrorSourceCertificate, cause)
// Ensure we can retrieve the typed error
var e *Error
if !errors.As(err, &e) {
t.Fatalf("expected errors.As to retrieve *certerr.Error, got %T", err)
}
if e.Kind != KindDecode {
t.Fatalf("unexpected kind: %v", e.Kind)
}
if e.Source != ErrorSourceCertificate {
t.Fatalf("unexpected source: %v", e.Source)
}
// Check message format (no trailing punctuation enforced by content)
msg := e.Error()
if !strings.Contains(msg, "failed to decode certificate") || !strings.Contains(msg, "bad data") {
t.Fatalf("unexpected error message: %q", msg)
}
}
func TestErrorsIsOnWrappedSentinel(t *testing.T) {
err := DecodeError(ErrorSourcePrivateKey, ErrEncryptedPrivateKey)
if !errors.Is(err, ErrEncryptedPrivateKey) {
t.Fatalf("expected errors.Is to match ErrEncryptedPrivateKey")
}
}
func TestInvalidPEMTypeMessageSingle(t *testing.T) {
err := ErrInvalidPEMType("FOO", "CERTIFICATE")
want := "invalid PEM type: have FOO, expected CERTIFICATE"
if err.Error() != want {
t.Fatalf("unexpected error message: got %q, want %q", err.Error(), want)
}
}
func TestInvalidPEMTypeMessageMultiple(t *testing.T) {
err := ErrInvalidPEMType("FOO", "CERTIFICATE", "NEW CERTIFICATE REQUEST")
if !strings.Contains(
err.Error(),
"invalid PEM type: have FOO, expected one of CERTIFICATE, NEW CERTIFICATE REQUEST",
) {
t.Fatalf("unexpected error message: %q", err.Error())
}
}