mirror of
https://github.com/netbirdio/netbird.git
synced 2026-09-27 09:09:07 +02:00
295 lines
7.8 KiB
Go
295 lines
7.8 KiB
Go
package certproof
|
|
|
|
import (
|
|
"context"
|
|
"crypto"
|
|
"crypto/sha256"
|
|
"crypto/x509"
|
|
"errors"
|
|
"fmt"
|
|
"io"
|
|
|
|
log "github.com/sirupsen/logrus"
|
|
|
|
"github.com/netbirdio/netbird/client/internal/pkcs11"
|
|
)
|
|
|
|
// PKCS11Config names the token whose certificates the store yields. URI is an RFC 7512
|
|
// PKCS#11 URI, or empty for the first token the p11-kit proxy exposes. PIN is the user
|
|
// PIN, and takes precedence over a pin-value or pin-source the URI carries.
|
|
type PKCS11Config struct {
|
|
URI string
|
|
PIN string
|
|
}
|
|
|
|
// PKCS11Store yields the identities of a PKCS#11 token, which is how tpm2-pkcs11 exposes
|
|
// TPM-held keys on Linux. Certificates on the token are paired with keys by CKA_ID, the
|
|
// convention tpm2_ptool addcert and pkcs11-tool follow; certificate files in the PEM
|
|
// directory by public key. Every signature happens on the token.
|
|
type PKCS11Store struct {
|
|
uri *pkcs11.URI
|
|
pin string
|
|
certDir string
|
|
}
|
|
|
|
// NewPKCS11Store parses cfg.URI, standing in the bare defaults when it is empty. Files in
|
|
// certDir without a key of their own are paired with the token's keys by public key.
|
|
func NewPKCS11Store(cfg PKCS11Config, certDir string) (*PKCS11Store, error) {
|
|
store := &PKCS11Store{uri: &pkcs11.URI{}, pin: cfg.PIN, certDir: certDir}
|
|
if cfg.URI == "" {
|
|
return store, nil
|
|
}
|
|
parsed, err := pkcs11.ParseURI(cfg.URI)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
store.uri = parsed
|
|
return store, nil
|
|
}
|
|
|
|
func (s *PKCS11Store) Candidates(_ context.Context) ([]Candidate, error) {
|
|
session, err := s.open()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer session.Close()
|
|
|
|
certs, err := tokenCertificates(session)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
fileChains, err := s.fileChains()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
log.Infof("%s holds %d certificates, %d certificate files without a key wait for its keys", s, len(certs), len(fileChains))
|
|
|
|
pool := make([]*x509.Certificate, 0, len(certs))
|
|
for _, cert := range certs {
|
|
pool = append(pool, cert.cert)
|
|
}
|
|
for _, chain := range fileChains {
|
|
pool = append(pool, chain...)
|
|
}
|
|
|
|
var candidates []Candidate
|
|
for _, cert := range certs {
|
|
if _, err := privateKey(session, cert.id); err != nil {
|
|
log.Infof("%s certificate %q has no usable private key: %v", s, cert.cert.Subject, err)
|
|
continue
|
|
}
|
|
candidates = append(candidates, s.candidate(cert.cert, cert.id, pool))
|
|
}
|
|
if len(fileChains) == 0 {
|
|
return candidates, nil
|
|
}
|
|
|
|
keys, err := tokenPublicKeys(session)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
for _, chain := range fileChains {
|
|
leaf := chain[0]
|
|
id, ok := keys.idFor(leaf.PublicKey)
|
|
if !ok {
|
|
log.Debugf("%s holds no key for certificate %q from %s", s, leaf.Subject, s.certDir)
|
|
continue
|
|
}
|
|
candidates = append(candidates, s.candidate(leaf, id, pool))
|
|
}
|
|
return candidates, nil
|
|
}
|
|
|
|
func (s *PKCS11Store) candidate(leaf *x509.Certificate, id []byte, pool []*x509.Certificate) Candidate {
|
|
chain := buildChain(leaf, pool)
|
|
log.Infof("%s candidate %q issued by %q built a chain of %d certificates", s, leaf.Subject, leaf.Issuer, len(chain))
|
|
return Candidate{Chain: chain, Signer: &pkcs11Signer{store: s, leaf: leaf, id: id}}
|
|
}
|
|
|
|
// fileChains reads the certificate files in the PEM directory that carry no key of their
|
|
// own; the file store answers for the ones that do.
|
|
func (s *PKCS11Store) fileChains() ([][]*x509.Certificate, error) {
|
|
if s.certDir == "" {
|
|
return nil, nil
|
|
}
|
|
paths, err := certFiles(s.certDir)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
var chains [][]*x509.Certificate
|
|
for _, path := range paths {
|
|
chain, signer, err := loadPEM(path)
|
|
if err != nil || signer != nil {
|
|
continue
|
|
}
|
|
chains = append(chains, chain)
|
|
}
|
|
return chains, nil
|
|
}
|
|
|
|
type tokenKey struct {
|
|
id []byte
|
|
public crypto.PublicKey
|
|
}
|
|
|
|
type tokenKeys []tokenKey
|
|
|
|
func tokenPublicKeys(session *pkcs11.Session) (tokenKeys, error) {
|
|
objects, err := session.FindObjects(pkcs11.Attribute{Type: pkcs11.AttrClass, Value: pkcs11.ULong(pkcs11.ClassPublicKey)})
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
keys := make(tokenKeys, 0, len(objects))
|
|
for _, object := range objects {
|
|
id, err := session.Attribute(object, pkcs11.AttrID)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
public, err := session.PublicKey(object)
|
|
if err != nil {
|
|
log.Debugf("skipping public key on PKCS#11 token: %v", err)
|
|
continue
|
|
}
|
|
keys = append(keys, tokenKey{id: id, public: public})
|
|
}
|
|
return keys, nil
|
|
}
|
|
|
|
// idFor finds the token key whose public half is pub, so a certificate kept outside the
|
|
// token is still signed for by the key inside it.
|
|
func (k tokenKeys) idFor(pub crypto.PublicKey) ([]byte, bool) {
|
|
for _, key := range k {
|
|
equaler, ok := key.public.(interface{ Equal(crypto.PublicKey) bool })
|
|
if ok && len(key.id) > 0 && equaler.Equal(pub) {
|
|
return key.id, true
|
|
}
|
|
}
|
|
return nil, false
|
|
}
|
|
|
|
func (s *PKCS11Store) String() string {
|
|
if s.uri.Token == "" {
|
|
return "PKCS#11 token"
|
|
}
|
|
return fmt.Sprintf("PKCS#11 token %q", s.uri.Token)
|
|
}
|
|
|
|
func (s *PKCS11Store) open() (*pkcs11.Session, error) {
|
|
module, err := pkcs11.Load(s.uri.Module())
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
pin, err := s.userPIN()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return module.OpenSession(s.uri.Token, pin)
|
|
}
|
|
|
|
func (s *PKCS11Store) userPIN() ([]byte, error) {
|
|
if s.pin != "" {
|
|
return []byte(s.pin), nil
|
|
}
|
|
return s.uri.PIN()
|
|
}
|
|
|
|
type tokenCertificate struct {
|
|
cert *x509.Certificate
|
|
id []byte
|
|
}
|
|
|
|
func tokenCertificates(session *pkcs11.Session) ([]tokenCertificate, error) {
|
|
objects, err := session.FindObjects(
|
|
pkcs11.Attribute{Type: pkcs11.AttrClass, Value: pkcs11.ULong(pkcs11.ClassCertificate)},
|
|
pkcs11.Attribute{Type: pkcs11.AttrCertificateType, Value: pkcs11.ULong(pkcs11.CertificateX509)},
|
|
)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
certs := make([]tokenCertificate, 0, len(objects))
|
|
for _, object := range objects {
|
|
der, err := session.Attribute(object, pkcs11.AttrValue)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
cert, err := x509.ParseCertificate(der)
|
|
if err != nil {
|
|
log.Warnf("skipping unparsable certificate on PKCS#11 token: %v", err)
|
|
continue
|
|
}
|
|
id, err := session.Attribute(object, pkcs11.AttrID)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
certs = append(certs, tokenCertificate{cert: cert, id: id})
|
|
}
|
|
return certs, nil
|
|
}
|
|
|
|
var errNoPrivateKey = errors.New("no private key shares the certificate's CKA_ID")
|
|
|
|
func privateKey(session *pkcs11.Session, id []byte) (pkcs11.Object, error) {
|
|
if len(id) == 0 {
|
|
return 0, errNoPrivateKey
|
|
}
|
|
keys, err := session.FindObjects(
|
|
pkcs11.Attribute{Type: pkcs11.AttrClass, Value: pkcs11.ULong(pkcs11.ClassPrivateKey)},
|
|
pkcs11.Attribute{Type: pkcs11.AttrID, Value: id},
|
|
)
|
|
if err != nil {
|
|
return 0, err
|
|
}
|
|
if len(keys) == 0 {
|
|
return 0, errNoPrivateKey
|
|
}
|
|
return keys[0], nil
|
|
}
|
|
|
|
// pkcs11Signer holds only the certificate and its CKA_ID; the key is looked up in a fresh
|
|
// session at signing time so no token handle outlives a call.
|
|
type pkcs11Signer struct {
|
|
store *PKCS11Store
|
|
leaf *x509.Certificate
|
|
id []byte
|
|
}
|
|
|
|
func (s *pkcs11Signer) Public() crypto.PublicKey {
|
|
return s.leaf.PublicKey
|
|
}
|
|
|
|
func (s *pkcs11Signer) Sign(_ io.Reader, digest []byte, opts crypto.SignerOpts) ([]byte, error) {
|
|
scheme, err := schemeFor(s.leaf.PublicKey, opts)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
session, err := s.store.open()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer session.Close()
|
|
|
|
key, err := privateKey(session, s.id)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
signature, err := session.Sign(pkcs11Mechanism(scheme), key, digest)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if scheme == schemeRSAPSSSHA256 {
|
|
return signature, nil
|
|
}
|
|
return ecdsaSignatureASN1(signature)
|
|
}
|
|
|
|
// pkcs11Mechanism maps a signature scheme onto the token mechanism that consumes a digest.
|
|
func pkcs11Mechanism(scheme sigScheme) pkcs11.Mechanism {
|
|
if scheme == schemeRSAPSSSHA256 {
|
|
return pkcs11.Mechanism{
|
|
Type: pkcs11.MechRSAPKCSPSS,
|
|
PSS: &pkcs11.PSSParams{Hash: pkcs11.MechSHA256, MGF: pkcs11.MGF1SHA256, SaltLen: sha256.Size},
|
|
}
|
|
}
|
|
return pkcs11.Mechanism{Type: pkcs11.MechECDSA}
|
|
}
|