mirror of
https://github.com/netbirdio/netbird.git
synced 2026-10-09 23:19:11 +02:00
252 lines
7.7 KiB
Go
252 lines
7.7 KiB
Go
package certproof
|
|
|
|
import (
|
|
"bytes"
|
|
"context"
|
|
"crypto"
|
|
"crypto/sha256"
|
|
"crypto/x509"
|
|
"errors"
|
|
"fmt"
|
|
"io"
|
|
"slices"
|
|
"unsafe"
|
|
|
|
log "github.com/sirupsen/logrus"
|
|
"golang.org/x/sys/windows"
|
|
)
|
|
|
|
const (
|
|
personalStore = "MY"
|
|
intermediateStore = "CA"
|
|
|
|
cryptAcquireSilentFlag = 0x00000040
|
|
cryptAcquirePreferNCryptKeyFlag = 0x00020000
|
|
certNCryptKeySpec = 0xFFFFFFFF
|
|
bcryptPadPSS = 0x00000008
|
|
)
|
|
|
|
var (
|
|
crypt32 = windows.NewLazySystemDLL("crypt32.dll")
|
|
ncrypt = windows.NewLazySystemDLL("ncrypt.dll")
|
|
|
|
procCryptAcquireCertificatePrivateKey = crypt32.NewProc("CryptAcquireCertificatePrivateKey")
|
|
procNCryptSignHash = ncrypt.NewProc("NCryptSignHash")
|
|
procNCryptFreeObject = ncrypt.NewProc("NCryptFreeObject")
|
|
)
|
|
|
|
type bcryptPSSPaddingInfo struct {
|
|
algID *uint16
|
|
salt uint32
|
|
}
|
|
|
|
// DefaultStore is the local machine's personal certificate store, where device
|
|
// certificates enrolled through AD or Intune are kept.
|
|
func DefaultStore() Store {
|
|
return NewSystemStore()
|
|
}
|
|
|
|
// SystemStore yields the identities of a personal certificate store, completing their
|
|
// chains from the matching intermediate CA store. Keys are used through CNG and never
|
|
// exported.
|
|
//
|
|
// The location decides whose certificates these are. The local machine store is the one
|
|
// a service reads; the current user store lives in the signed-in user's registry hive
|
|
// with keys protected against their profile, so it is only readable while running as
|
|
// that user.
|
|
type SystemStore struct {
|
|
location uint32
|
|
}
|
|
|
|
// NewSystemStore reads the local machine store, which is what the daemon uses.
|
|
func NewSystemStore() *SystemStore {
|
|
return &SystemStore{location: windows.CERT_SYSTEM_STORE_LOCAL_MACHINE}
|
|
}
|
|
|
|
// NewUserStore reads the calling user's personal store. It is only useful in a process
|
|
// already running as that user, which is what the posture helper is.
|
|
func NewUserStore() *SystemStore {
|
|
return &SystemStore{location: windows.CERT_SYSTEM_STORE_CURRENT_USER}
|
|
}
|
|
|
|
func (s *SystemStore) Candidates(_ context.Context) ([]Candidate, error) {
|
|
leaves, err := storeCertificates(s.location, personalStore)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
intermediates, err := storeCertificates(s.location, intermediateStore)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
log.Infof("certificate store %s holds %d personal certificates and %d intermediates", s, len(leaves), len(intermediates))
|
|
if len(leaves) == 0 {
|
|
return nil, nil
|
|
}
|
|
|
|
pool := slices.Concat(intermediates, leaves)
|
|
candidates := make([]Candidate, 0, len(leaves))
|
|
for _, leaf := range leaves {
|
|
chain := buildChain(leaf, pool)
|
|
log.Infof("certificate store %s candidate %q issued by %q built a chain of %d certificates", s, leaf.Subject, leaf.Issuer, len(chain))
|
|
candidates = append(candidates, Candidate{Chain: chain, Signer: &systemStoreSigner{leaf: leaf, location: s.location}})
|
|
}
|
|
return candidates, nil
|
|
}
|
|
|
|
// String names the store location the way the Windows documentation does.
|
|
func (s *SystemStore) String() string {
|
|
if s.location == windows.CERT_SYSTEM_STORE_CURRENT_USER {
|
|
return "CurrentUser"
|
|
}
|
|
return "LocalMachine"
|
|
}
|
|
|
|
// systemStoreSigner holds only the certificate; the store entry and its key are acquired
|
|
// at signing time so no handles outlive a call.
|
|
type systemStoreSigner struct {
|
|
leaf *x509.Certificate
|
|
location uint32
|
|
}
|
|
|
|
func (s *systemStoreSigner) Public() crypto.PublicKey {
|
|
return s.leaf.PublicKey
|
|
}
|
|
|
|
func (s *systemStoreSigner) Sign(_ io.Reader, digest []byte, opts crypto.SignerOpts) ([]byte, error) {
|
|
scheme, err := schemeFor(s.leaf.PublicKey, opts)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
store, err := openStore(s.location, personalStore)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer func() { _ = windows.CertCloseStore(store, 0) }()
|
|
|
|
var signature []byte
|
|
err = eachCertificate(store, func(ctx *windows.CertContext) (bool, error) {
|
|
if !bytes.Equal(encodedCert(ctx), s.leaf.Raw) {
|
|
return false, nil
|
|
}
|
|
signature, err = signWithContext(ctx, scheme, digest)
|
|
return true, err
|
|
})
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if signature == nil {
|
|
return nil, errors.New("certificate is no longer in the personal store")
|
|
}
|
|
return signature, nil
|
|
}
|
|
|
|
func signWithContext(ctx *windows.CertContext, scheme sigScheme, digest []byte) ([]byte, error) {
|
|
var key uintptr
|
|
var keySpec uint32
|
|
var callerFree int32
|
|
ok, _, err := procCryptAcquireCertificatePrivateKey.Call(uintptr(unsafe.Pointer(ctx)), cryptAcquireSilentFlag|cryptAcquirePreferNCryptKeyFlag, 0,
|
|
uintptr(unsafe.Pointer(&key)), uintptr(unsafe.Pointer(&keySpec)), uintptr(unsafe.Pointer(&callerFree)))
|
|
if ok == 0 {
|
|
return nil, fmt.Errorf("acquire private key: %w", err)
|
|
}
|
|
if keySpec != certNCryptKeySpec {
|
|
if callerFree != 0 {
|
|
_ = windows.CryptReleaseContext(windows.Handle(key), 0)
|
|
}
|
|
return nil, errors.New("legacy CryptoAPI keys are not supported")
|
|
}
|
|
if callerFree != 0 {
|
|
defer func() { _, _, _ = procNCryptFreeObject.Call(key) }()
|
|
}
|
|
|
|
var padding unsafe.Pointer
|
|
var flags uintptr
|
|
if scheme == schemeRSAPSSSHA256 {
|
|
algID, _ := windows.UTF16PtrFromString("SHA256")
|
|
info := bcryptPSSPaddingInfo{algID: algID, salt: sha256.Size}
|
|
padding, flags = unsafe.Pointer(&info), bcryptPadPSS
|
|
}
|
|
size, err := ncryptSignHash(key, padding, digest, nil, flags)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
signature := make([]byte, size)
|
|
if size, err = ncryptSignHash(key, padding, digest, signature, flags); err != nil {
|
|
return nil, err
|
|
}
|
|
signature = signature[:size]
|
|
if scheme == schemeRSAPSSSHA256 {
|
|
return signature, nil
|
|
}
|
|
return ecdsaSignatureASN1(signature)
|
|
}
|
|
|
|
func ncryptSignHash(key uintptr, padding unsafe.Pointer, digest, signature []byte, flags uintptr) (uint32, error) {
|
|
var result uint32
|
|
var signaturePtr uintptr
|
|
if len(signature) > 0 {
|
|
signaturePtr = uintptr(unsafe.Pointer(&signature[0]))
|
|
}
|
|
status, _, _ := procNCryptSignHash.Call(key, uintptr(padding), uintptr(unsafe.Pointer(&digest[0])), uintptr(len(digest)),
|
|
signaturePtr, uintptr(len(signature)), uintptr(unsafe.Pointer(&result)), flags)
|
|
if uint32(status) != 0 {
|
|
return 0, fmt.Errorf("NCryptSignHash: 0x%08x", uint32(status))
|
|
}
|
|
return result, nil
|
|
}
|
|
|
|
func storeCertificates(location uint32, name string) ([]*x509.Certificate, error) {
|
|
store, err := openStore(location, name)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer func() { _ = windows.CertCloseStore(store, 0) }()
|
|
|
|
var certs []*x509.Certificate
|
|
err = eachCertificate(store, func(ctx *windows.CertContext) (bool, error) {
|
|
cert, err := x509.ParseCertificate(bytes.Clone(encodedCert(ctx)))
|
|
if err != nil {
|
|
log.Warnf("skipping certificate in %s store: %v", name, err)
|
|
return false, nil
|
|
}
|
|
certs = append(certs, cert)
|
|
return false, nil
|
|
})
|
|
return certs, err
|
|
}
|
|
|
|
func openStore(location uint32, name string) (windows.Handle, error) {
|
|
namePtr, err := windows.UTF16PtrFromString(name)
|
|
if err != nil {
|
|
return 0, err
|
|
}
|
|
flags := location | uint32(windows.CERT_STORE_READONLY_FLAG|windows.CERT_STORE_OPEN_EXISTING_FLAG)
|
|
store, err := windows.CertOpenStore(windows.CERT_STORE_PROV_SYSTEM, 0, 0, flags, uintptr(unsafe.Pointer(namePtr)))
|
|
if err != nil {
|
|
return 0, fmt.Errorf("open %s certificate store: %w", name, err)
|
|
}
|
|
return store, nil
|
|
}
|
|
|
|
func eachCertificate(store windows.Handle, fn func(*windows.CertContext) (bool, error)) error {
|
|
var ctx *windows.CertContext
|
|
for {
|
|
next, err := windows.CertEnumCertificatesInStore(store, ctx)
|
|
if next == nil {
|
|
if errors.Is(err, windows.Errno(windows.CRYPT_E_NOT_FOUND)) || errors.Is(err, windows.ERROR_NO_MORE_FILES) {
|
|
return nil
|
|
}
|
|
return fmt.Errorf("enumerate certificates: %w", err)
|
|
}
|
|
ctx = next
|
|
if stop, err := fn(ctx); stop || err != nil {
|
|
_ = windows.CertFreeCertificateContext(ctx)
|
|
return err
|
|
}
|
|
}
|
|
}
|
|
|
|
func encodedCert(ctx *windows.CertContext) []byte {
|
|
return unsafe.Slice(ctx.EncodedCert, ctx.Length)
|
|
}
|