Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 7 additions & 2 deletions wolfx509/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,8 +17,13 @@ entirely on wolfCrypt-owned key handles.
- `GenerateP256Key()` — convenience factory returning a
`*handles.EccKey`.
- `(c *Certificate) Verify(opts VerifyOptions)` — chain verification
via wolfSSL's CertManager. Note: `VerifyOptions.CurrentTime` is
currently ignored; wolfSSL uses the system clock.
via wolfSSL's `X509_verify_cert`. Only self-signed certificates in
`Roots` are trust anchors; unlike `crypto/x509`, a non-self-signed CA
in `Roots` is not trusted unless its self-signed root is also there.
A non-zero `VerifyOptions.CurrentTime` is rejected, because
wolfSSL always uses the system clock. Concurrent calls that share a
`Roots` pool run one at a time, because wolfSSL doesn't support
concurrent verification on one X509_STORE.
- `(c *Certificate) PublicECCRawXY()` — raw `(X, Y)` point export
for ECDSA-P256 leaves.

Expand Down
122 changes: 75 additions & 47 deletions wolfx509/cert_pool.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
package wolfx509

import (
"encoding/pem"
"errors"
"fmt"
"runtime"
Expand All @@ -29,68 +30,95 @@ import (
wolfSSL "github.com/wolfssl/go-wolfssl"
)

// CertPool is a set of trusted CA certificates used during chain
// verification. It wraps a wolfSSL WOLFSSL_CERT_MANAGER; certificates
// are added via AppendCertsFromPEM or AddCert.
// CertPool is a set of certificates for chain verification.
type CertPool struct {
mu sync.RWMutex
cm *wolfSSL.WOLFSSL_CERT_MANAGER
mu sync.RWMutex
store *wolfSSL.WOLFSSL_X509_STORE
certs []*wolfSSL.WOLFSSL_X509 // pool's own references; Free releases them.
}

// NewCertPool returns an empty pool backed by a fresh CertManager.
// NewCertPool returns an empty pool.
func NewCertPool() *CertPool {
p := &CertPool{cm: wolfSSL.WolfSSL_CertManagerNew()}
runtime.SetFinalizer(p, (*CertPool).finalize)
return p
pool := &CertPool{store: wolfSSL.WolfSSL_X509_STORE_new()}
runtime.SetFinalizer(pool, (*CertPool).finalize)
return pool
}

// Free releases the underlying CertManager. Safe to call multiple times.
func (p *CertPool) Free() {
p.mu.Lock()
defer p.mu.Unlock()
if p.cm != nil {
wolfSSL.WolfSSL_CertManagerFree(p.cm)
p.cm = nil
// Free releases the store and the pool's certificate references. Callers'
// *Certificate values are not affected.
func (pool *CertPool) Free() {
pool.mu.Lock()
defer pool.mu.Unlock()
if pool.store != nil {
wolfSSL.WolfSSL_X509_STORE_free(pool.store)
pool.store = nil
}
runtime.SetFinalizer(p, nil)
for _, x509 := range pool.certs {
wolfSSL.WolfSSL_X509_free(x509)
}
pool.certs = nil
runtime.SetFinalizer(pool, nil)
}

func (p *CertPool) finalize() { p.Free() }
func (pool *CertPool) finalize() { pool.Free() }

// AppendCertsFromPEM loads one or more PEM-encoded CA certificates into the
// pool. Returns true on success. Mirrors crypto/x509.CertPool.AppendCertsFromPEM.
func (p *CertPool) AppendCertsFromPEM(pem []byte) bool {
if len(pem) == 0 {
return false
}
// Write lock: wolfSSL_CertManagerLoadCABuffer mutates the cert manager,
// and concurrent loads on the same manager are not safe.
p.mu.Lock()
defer p.mu.Unlock()
if p.cm == nil {
return false
// AppendCertsFromPEM parses PEM-encoded certificates and adds each via
// AddCert. Returns true if any cert was added.
func (pool *CertPool) AppendCertsFromPEM(pemCerts []byte) bool {
var ok bool
rest := pemCerts
for len(rest) > 0 {
var block *pem.Block
block, rest = pem.Decode(rest)
if block == nil {
break
}
if block.Type != "CERTIFICATE" {
continue
}
cert, err := ParseCertificate(block.Bytes)
if err != nil {
continue
}
if pool.AddCert(cert) == nil {
ok = true
}
cert.Free() // the pool holds its own reference
}
return wolfSSL.WolfSSL_CertManagerLoadCABuffer(p.cm, pem, wolfSSL.SSL_FILETYPE_PEM) == wolfSSL.WOLFSSL_SUCCESS
return ok
}

// AddCert appends a single certificate to the pool via wolfSSL's
// CertManagerLoadCABuffer in DER mode. Unlike crypto/x509.CertPool.AddCert
// (which is infallible because the cert is already parsed in Go memory),
// wolfSSL re-parses the DER inside its cert manager and can reject it.
// Callers must check the returned error; a silent failure here leads to
// chains not verifying later with no clue why.
func (p *CertPool) AddCert(c *Certificate) error {
if c == nil || len(c.Raw) == 0 {
return errors.New("wolfx509: AddCert: nil certificate or empty DER")
// AddCert adds cert to the pool. The caller keeps
// ownership of cert: the pool takes its own reference to the underlying X509,
// so cert may be freed, or added to other pools, independently.
func (pool *CertPool) AddCert(cert *Certificate) error {
if cert == nil {
return errors.New("wolfx509: AddCert: nil certificate")
}
cert.mu.RLock()
x509 := cert.x
ret := wolfSSL.WOLFSSL_SUCCESS
if x509 != nil {
ret = wolfSSL.WolfSSL_X509_up_ref(x509)
}
cert.mu.RUnlock()
if x509 == nil {
return errors.New("wolfx509: AddCert: certificate is not parsed (use ParseCertificate) or was freed")
}
if ret != wolfSSL.WOLFSSL_SUCCESS {
return fmt.Errorf("wolfx509: AddCert: X509_up_ref failed (%d)", ret)
}
// Write lock: see AppendCertsFromPEM.
p.mu.Lock()
defer p.mu.Unlock()
if p.cm == nil {
return errors.New("wolfx509: AddCert: pool already freed")
// From here on x509 is our reference: release it on any failure.
pool.mu.Lock()
defer pool.mu.Unlock()
if pool.store == nil {
wolfSSL.WolfSSL_X509_free(x509)
return errors.New("wolfx509: AddCert: pool was freed or failed to initialize")
}
if ret := wolfSSL.WolfSSL_CertManagerLoadCABuffer(p.cm, c.Raw, wolfSSL.SSL_FILETYPE_ASN1); ret != wolfSSL.WOLFSSL_SUCCESS {
return fmt.Errorf("wolfx509: AddCert: wolfSSL rejected DER (%d)", ret)
if ret := wolfSSL.WolfSSL_X509_STORE_add_cert(pool.store, x509); ret != wolfSSL.WOLFSSL_SUCCESS {
wolfSSL.WolfSSL_X509_free(x509)
return fmt.Errorf("wolfx509: AddCert: X509_STORE_add_cert failed (%d)", ret)
}
pool.certs = append(pool.certs, x509)
return nil
}
114 changes: 72 additions & 42 deletions wolfx509/verify.go
Original file line number Diff line number Diff line change
Expand Up @@ -30,29 +30,14 @@ import (
// VerifyOptions carries the parameters for certificate chain verification.
// Mirrors crypto/x509.VerifyOptions (minimal subset).
type VerifyOptions struct {
// Roots is the set of trusted CA certificates. Required: Verify
// returns an error if Roots is nil. Self-signed leaves must be in
// the pool to validate.
Roots *CertPool

// Intermediates is unused — wolfSSL's CertManager-based path can't
// consume a separate intermediates pool. Verify rejects opts where
// Intermediates is non-nil rather than silently dropping the data;
// callers should AppendCertsFromPEM intermediates into Roots.
Intermediates *CertPool

// DNSName, if non-empty, is also checked against the leaf's SANs/CN
// after chain verification succeeds.
DNSName string

// CurrentTime is unused — wolfSSL always uses the system clock.
// Verify rejects opts where CurrentTime is non-zero.
CurrentTime time.Time
Roots *CertPool // trust anchors; required.
Intermediates *CertPool // wire-presented intermediates; optional.
DNSName string // checked against SANs after chain verify.
CurrentTime time.Time // rejected if non-zero (wolfSSL uses system clock).
}

// Verify validates c against opts.Roots, then (if DNSName is set) runs
// VerifyHostname. Returns a chain slice for crypto/x509 API symmetry;
// current callers only check err.
// Verify runs wolfSSL_X509_verify_cert against Roots, using Intermediates
// as the untrusted chain. wolfSSL enforces CA:TRUE per RFC 5280.
func (c *Certificate) Verify(opts VerifyOptions) (chains [][]*Certificate, err error) {
c.mu.RLock()
defer c.mu.RUnlock()
Expand All @@ -62,33 +47,79 @@ func (c *Certificate) Verify(opts VerifyOptions) (chains [][]*Certificate, err e
if opts.Roots == nil {
return nil, fmt.Errorf("%w: no roots provided", ErrVerifyFailed)
}
// Fail loudly: wolfSSL's CertManager-based path doesn't honor Intermediates
// or CurrentTime (see field docs). Callers must AppendCertsFromPEM
// intermediates into Roots and accept the system clock for time checks.
if opts.Intermediates != nil {
return nil, fmt.Errorf("%w: Intermediates not supported; load into Roots instead", ErrVerifyFailed)
}
if !opts.CurrentTime.IsZero() {
return nil, fmt.Errorf("%w: CurrentTime not supported; wolfSSL uses the system clock", ErrVerifyFailed)
}

opts.Roots.mu.RLock()
defer opts.Roots.mu.RUnlock()
if opts.Roots.cm == nil {
return nil, fmt.Errorf("%w: roots pool is closed", ErrVerifyFailed)
// Take our own reference on each intermediate so we don't need to hold
// the Intermediates lock during verification; a concurrent Free of that
// pool can't release them while wolfSSL is using them.
var intermediates []*wolfSSL.WOLFSSL_X509
defer func() {
for _, x509 := range intermediates {
wolfSSL.WolfSSL_X509_free(x509)
}
}()
if opts.Intermediates != nil {
opts.Intermediates.mu.RLock()
upRefOK := true
for _, x509 := range opts.Intermediates.certs {
if wolfSSL.WolfSSL_X509_up_ref(x509) != wolfSSL.WOLFSSL_SUCCESS {
upRefOK = false
break
}
intermediates = append(intermediates, x509)
}
opts.Intermediates.mu.RUnlock()
if !upRefOK {
return nil, fmt.Errorf("%w: X509_up_ref failed on intermediate", ErrVerifyFailed)
}
}

stack := wolfSSL.WolfSSL_sk_X509_new_null()
if stack == nil {
return nil, fmt.Errorf("%w: sk_X509_new_null failed", ErrVerifyFailed)
}
defer wolfSSL.WolfSSL_sk_X509_free(stack)
for _, x509 := range intermediates {
if ret := wolfSSL.WolfSSL_sk_X509_push(stack, x509); ret <= 0 {
return nil, fmt.Errorf("%w: sk_X509_push failed (ret=%d)", ErrVerifyFailed, ret)
}
}

// Exclusive lock: X509_verify_cert mutates the store (injects
// intermediates into store->certs, loads/unloads TEMP_CA signers in its
// CertManager), and wolfSSL does not support concurrent verifies on one
// store.
opts.Roots.mu.Lock()
defer opts.Roots.mu.Unlock()
if opts.Roots.store == nil {
return nil, fmt.Errorf("%w: roots pool was freed or failed to initialize", ErrVerifyFailed)
}

ret := wolfSSL.WolfSSL_CertManagerVerifyBuffer(opts.Roots.cm, c.Raw)
if ret != wolfSSL.WOLFSSL_SUCCESS {
baseErr := fmt.Errorf("%w: wolfSSL_CertManagerVerifyBuffer ret=%d", ErrVerifyFailed, ret)
// wolfSSL's CertManager returns ASN_NO_SIGNER_E (-188) when the
// chain can't be rooted in our CA pool. Surface that as an
// UnknownAuthorityError for stdlib-style errors.As dispatch.
const ASN_NO_SIGNER_E = -188
if ret == ASN_NO_SIGNER_E {
return nil, UnknownAuthorityError{Cert: c, err: baseErr}
ctx := wolfSSL.WolfSSL_X509_STORE_CTX_new()
if ctx == nil {
return nil, fmt.Errorf("%w: X509_STORE_CTX_new failed", ErrVerifyFailed)
}
defer wolfSSL.WolfSSL_X509_STORE_CTX_free(ctx)

if ret := wolfSSL.WolfSSL_X509_STORE_CTX_init(ctx, opts.Roots.store, c.x, stack); ret != wolfSSL.WOLFSSL_SUCCESS {
return nil, fmt.Errorf("%w: X509_STORE_CTX_init failed (ret=%d)", ErrVerifyFailed, ret)
}

if ret := wolfSSL.WolfSSL_X509_verify_cert(ctx); ret != wolfSSL.WOLFSSL_SUCCESS {
code := wolfSSL.WolfSSL_X509_STORE_CTX_get_error(ctx)
if code == 0 {
return nil, fmt.Errorf("%w: X509_verify_cert failed (ret=%d)", ErrVerifyFailed, ret)
}
verifyErr := fmt.Errorf("%w: X509_V_ERR %d", ErrVerifyFailed, code)
// Callers may check errors.As(err, &UnknownAuthorityError{}) as with crypto/x509, so return it for "no trusted issuer" codes.
switch code {
case wolfSSL.WOLFSSL_X509_V_ERR_UNABLE_TO_GET_ISSUER_CERT_LOCALLY,
wolfSSL.WOLFSSL_X509_V_ERR_DEPTH_ZERO_SELF_SIGNED_CERT:
return nil, UnknownAuthorityError{Cert: c, err: verifyErr}
}
return nil, baseErr
return nil, verifyErr
}

if opts.DNSName != "" {
Expand All @@ -97,6 +128,5 @@ func (c *Certificate) Verify(opts VerifyOptions) (chains [][]*Certificate, err e
}
}

// Return a placeholder single-element chain — callers only check err.
return [][]*Certificate{{c}}, nil
}
Loading
Loading