mirror of
https://github.com/netbirdio/netbird.git
synced 2026-09-14 10:49:07 +02:00
A service reads LocalMachine\MY, where AD and Intune enrol device certificates. CurrentUser\MY lives in the signed-in user's registry hive with keys protected against their profile, and a service that opens it does not fail: "current user" resolves to HKU\S-1-5-18, so it silently reads the service account's own empty store. The service therefore reads the machine store itself and launches "netbird posture cert-proof" with the session token for the rest, mirroring the macOS console user helper. Windows lets a privileged service assume a user identity, so the token goes straight into the child process and no external tooling is involved. CREATE_NO_WINDOW keeps a console window from flashing on the desktop every sync. In-process impersonation would also work but is per OS thread while goroutines migrate, so the child process avoids that class of bug. Session selection prefers the physical console and falls back to any active session, so remote desktop and VDI hosts are covered. WTSQueryUserToken needs SE_TCB_NAME, so a user-run client skips the helper and reads the machine store alone. SystemStore takes a store location, gaining NewUserStore alongside NewSystemStore and the per candidate logging macOS already had. The request building and proof merging move to helper_spawn.go, shared by both platforms, and helperStore picks what the helper reads per platform.
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)
|
|
}
|