diff --git a/client/internal/certproof/keychain_darwin.go b/client/internal/certproof/keychain_darwin.go new file mode 100644 index 000000000..0e757ce9d --- /dev/null +++ b/client/internal/certproof/keychain_darwin.go @@ -0,0 +1,281 @@ +package certproof + +import ( + "bytes" + "context" + "crypto" + "crypto/x509" + "errors" + "fmt" + "io" + "sync" + "unsafe" + + "github.com/ebitengine/purego" + log "github.com/sirupsen/logrus" +) + +const ( + securityFramework = "/System/Library/Frameworks/Security.framework/Security" + coreFoundationFramework = "/System/Library/Frameworks/CoreFoundation.framework/CoreFoundation" + + errSecItemNotFound = -25300 +) + +var ( + keychainOnce sync.Once + keychainErr error + + secItemCopyMatching func(query uintptr, result *uintptr) int32 + secIdentityCopyCertificate func(identity uintptr, cert *uintptr) int32 + secIdentityCopyPrivateKey func(identity uintptr, key *uintptr) int32 + secCertificateCopyData func(cert uintptr) uintptr + secKeyCreateSignature func(key, algorithm, data uintptr, err *uintptr) uintptr + + cfDictionaryCreate func(alloc uintptr, keys, values *uintptr, count int, keyCallBacks, valueCallBacks uintptr) uintptr + cfArrayGetCount func(array uintptr) int + cfArrayGetValueAtIndex func(array uintptr, index int) uintptr + cfDataCreate func(alloc uintptr, data *byte, length int) uintptr + cfDataGetLength func(data uintptr) int + cfDataGetBytePtr func(data uintptr) unsafe.Pointer + cfErrorGetCode func(err uintptr) int + cfRelease func(ref uintptr) + + kSecClass, kSecClassIdentity, kSecClassCertificate, kSecMatchLimit, kSecMatchLimitAll, kSecReturnRef uintptr + kSecKeyAlgorithmECDSASHA256, kSecKeyAlgorithmECDSASHA384, kSecKeyAlgorithmRSAPSSSHA256 uintptr + kCFBooleanTrue, kCFTypeDictionaryKeyCallBacks, kCFTypeDictionaryValueCallBacks uintptr +) + +// DefaultStore is the keychain search list of the daemon, which for the root daemon is +// the System keychain where MDM installs device identities. +func DefaultStore() Store { + return NewKeychainStore() +} + +// KeychainStore yields the identities of the process's keychain search list, reached +// through purego so the client keeps building with CGO_ENABLED=0. +type KeychainStore struct{} + +func NewKeychainStore() *KeychainStore { + return &KeychainStore{} +} + +func (s *KeychainStore) Candidates(_ context.Context) ([]Candidate, error) { + if err := loadKeychain(); err != nil { + return nil, err + } + var leaves []*x509.Certificate + err := eachIdentity(func(_ uintptr, der []byte) (bool, error) { + cert, err := x509.ParseCertificate(der) + if err != nil { + log.Warnf("skipping keychain identity: %v", err) + return false, nil + } + leaves = append(leaves, cert) + return false, nil + }) + if err != nil || len(leaves) == 0 { + return nil, err + } + pool, err := keychainCertificates() + if err != nil { + return nil, err + } + candidates := make([]Candidate, 0, len(leaves)) + for _, leaf := range leaves { + candidates = append(candidates, Candidate{Chain: buildChain(leaf, pool), Signer: &keychainSigner{leaf: leaf}}) + } + return candidates, nil +} + +// keychainSigner holds only the certificate; the identity is looked up again at signing +// time so no keychain references outlive a call. +type keychainSigner struct { + leaf *x509.Certificate +} + +func (s *keychainSigner) Public() crypto.PublicKey { + return s.leaf.PublicKey +} + +func (s *keychainSigner) Sign(_ io.Reader, digest []byte, opts crypto.SignerOpts) ([]byte, error) { + scheme, err := schemeFor(s.leaf.PublicKey, opts) + if err != nil { + return nil, err + } + algorithm := keychainAlgorithm(scheme) + var signature []byte + err = eachIdentity(func(identity uintptr, der []byte) (bool, error) { + if !bytes.Equal(der, s.leaf.Raw) { + return false, nil + } + signature, err = signWithIdentity(identity, algorithm, digest) + return true, err + }) + if err != nil { + return nil, err + } + if signature == nil { + return nil, errors.New("certificate is no longer in the keychain") + } + return signature, nil +} + +func keychainAlgorithm(scheme sigScheme) uintptr { + switch scheme { + case schemeECDSASHA384: + return kSecKeyAlgorithmECDSASHA384 + case schemeRSAPSSSHA256: + return kSecKeyAlgorithmRSAPSSSHA256 + default: + return kSecKeyAlgorithmECDSASHA256 + } +} + +func signWithIdentity(identity, algorithm uintptr, digest []byte) ([]byte, error) { + var key uintptr + if status := secIdentityCopyPrivateKey(identity, &key); status != 0 { + return nil, fmt.Errorf("SecIdentityCopyPrivateKey: %d", status) + } + defer cfRelease(key) + + data := cfDataCreate(0, &digest[0], len(digest)) + defer cfRelease(data) + + var cfErr uintptr + signature := secKeyCreateSignature(key, algorithm, data, &cfErr) + if signature == 0 { + defer cfRelease(cfErr) + return nil, fmt.Errorf("SecKeyCreateSignature: CFError %d", cfErrorGetCode(cfErr)) + } + defer cfRelease(signature) + return dataBytes(signature), nil +} + +func eachIdentity(fn func(identity uintptr, der []byte) (bool, error)) error { + return eachMatching(kSecClassIdentity, func(identity uintptr) (bool, error) { + var cert uintptr + if status := secIdentityCopyCertificate(identity, &cert); status != 0 { + return true, fmt.Errorf("SecIdentityCopyCertificate: %d", status) + } + der := certificateDER(cert) + cfRelease(cert) + return fn(identity, der) + }) +} + +func keychainCertificates() ([]*x509.Certificate, error) { + var certs []*x509.Certificate + err := eachMatching(kSecClassCertificate, func(item uintptr) (bool, error) { + if cert, err := x509.ParseCertificate(certificateDER(item)); err == nil { + certs = append(certs, cert) + } + return false, nil + }) + return certs, err +} + +func eachMatching(class uintptr, fn func(item uintptr) (bool, error)) error { + keys := []uintptr{kSecClass, kSecMatchLimit, kSecReturnRef} + values := []uintptr{class, kSecMatchLimitAll, kCFBooleanTrue} + query := cfDictionaryCreate(0, &keys[0], &values[0], len(keys), kCFTypeDictionaryKeyCallBacks, kCFTypeDictionaryValueCallBacks) + defer cfRelease(query) + + var items uintptr + switch status := secItemCopyMatching(query, &items); status { + case 0: + case errSecItemNotFound: + return nil + default: + return fmt.Errorf("SecItemCopyMatching: %d", status) + } + defer cfRelease(items) + for i, n := 0, cfArrayGetCount(items); i < n; i++ { + if stop, err := fn(cfArrayGetValueAtIndex(items, i)); stop || err != nil { + return err + } + } + return nil +} + +func certificateDER(cert uintptr) []byte { + data := secCertificateCopyData(cert) + defer cfRelease(data) + return dataBytes(data) +} + +func dataBytes(data uintptr) []byte { + return bytes.Clone(unsafe.Slice((*byte)(cfDataGetBytePtr(data)), cfDataGetLength(data))) +} + +func loadKeychain() error { + keychainOnce.Do(func() { keychainErr = resolveKeychain() }) + return keychainErr +} + +func resolveKeychain() error { + security, err := purego.Dlopen(securityFramework, purego.RTLD_LAZY|purego.RTLD_GLOBAL) + if err != nil { + return fmt.Errorf("open %s: %w", securityFramework, err) + } + coreFoundation, err := purego.Dlopen(coreFoundationFramework, purego.RTLD_LAZY|purego.RTLD_GLOBAL) + if err != nil { + return fmt.Errorf("open %s: %w", coreFoundationFramework, err) + } + + for _, fn := range []struct { + ptr any + lib uintptr + name string + }{ + {&secItemCopyMatching, security, "SecItemCopyMatching"}, + {&secIdentityCopyCertificate, security, "SecIdentityCopyCertificate"}, + {&secIdentityCopyPrivateKey, security, "SecIdentityCopyPrivateKey"}, + {&secCertificateCopyData, security, "SecCertificateCopyData"}, + {&secKeyCreateSignature, security, "SecKeyCreateSignature"}, + {&cfDictionaryCreate, coreFoundation, "CFDictionaryCreate"}, + {&cfArrayGetCount, coreFoundation, "CFArrayGetCount"}, + {&cfArrayGetValueAtIndex, coreFoundation, "CFArrayGetValueAtIndex"}, + {&cfDataCreate, coreFoundation, "CFDataCreate"}, + {&cfDataGetLength, coreFoundation, "CFDataGetLength"}, + {&cfDataGetBytePtr, coreFoundation, "CFDataGetBytePtr"}, + {&cfErrorGetCode, coreFoundation, "CFErrorGetCode"}, + {&cfRelease, coreFoundation, "CFRelease"}, + } { + symbol, err := purego.Dlsym(fn.lib, fn.name) + if err != nil { + return fmt.Errorf("resolve %s: %w", fn.name, err) + } + purego.RegisterFunc(fn.ptr, symbol) + } + + for _, global := range []struct { + ptr *uintptr + lib uintptr + name string + deref bool + }{ + {&kSecClass, security, "kSecClass", true}, + {&kSecClassIdentity, security, "kSecClassIdentity", true}, + {&kSecClassCertificate, security, "kSecClassCertificate", true}, + {&kSecMatchLimit, security, "kSecMatchLimit", true}, + {&kSecMatchLimitAll, security, "kSecMatchLimitAll", true}, + {&kSecReturnRef, security, "kSecReturnRef", true}, + {&kSecKeyAlgorithmECDSASHA256, security, "kSecKeyAlgorithmECDSASignatureDigestX962SHA256", true}, + {&kSecKeyAlgorithmECDSASHA384, security, "kSecKeyAlgorithmECDSASignatureDigestX962SHA384", true}, + {&kSecKeyAlgorithmRSAPSSSHA256, security, "kSecKeyAlgorithmRSASignatureDigestPSSSHA256", true}, + {&kCFBooleanTrue, coreFoundation, "kCFBooleanTrue", true}, + {&kCFTypeDictionaryKeyCallBacks, coreFoundation, "kCFTypeDictionaryKeyCallBacks", false}, + {&kCFTypeDictionaryValueCallBacks, coreFoundation, "kCFTypeDictionaryValueCallBacks", false}, + } { + addr, err := purego.Dlsym(global.lib, global.name) + if err != nil { + return fmt.Errorf("resolve %s: %w", global.name, err) + } + if global.deref { + addr = **(**uintptr)(unsafe.Pointer(&addr)) + } + *global.ptr = addr + } + return nil +} diff --git a/client/internal/certproof/keystore.go b/client/internal/certproof/keystore.go new file mode 100644 index 000000000..7ce676b4c --- /dev/null +++ b/client/internal/certproof/keystore.go @@ -0,0 +1,77 @@ +package certproof + +import ( + "crypto" + "crypto/ecdsa" + "crypto/rsa" + "crypto/x509" + "encoding/asn1" + "errors" + "fmt" + "math/big" + "slices" +) + +var errUnsupportedScheme = errors.New("unsupported signature scheme for OS keystore") + +type sigScheme int + +const ( + schemeECDSASHA256 sigScheme = iota + 1 + schemeECDSASHA384 + schemeRSAPSSSHA256 +) + +// schemeFor maps a crypto.Signer request onto the schemes the OS keystores perform. +func schemeFor(pub crypto.PublicKey, opts crypto.SignerOpts) (sigScheme, error) { + switch pub.(type) { + case *ecdsa.PublicKey: + switch opts.HashFunc() { + case crypto.SHA256: + return schemeECDSASHA256, nil + case crypto.SHA384: + return schemeECDSASHA384, nil + } + case *rsa.PublicKey: + if pss, ok := opts.(*rsa.PSSOptions); ok && pss.Hash == crypto.SHA256 { + return schemeRSAPSSSHA256, nil + } + } + return 0, fmt.Errorf("%w: %T with %v", errUnsupportedScheme, pub, opts.HashFunc()) +} + +// buildChain extends leaf with the issuers found in pool up to a self-signed certificate. +func buildChain(leaf *x509.Certificate, pool []*x509.Certificate) []*x509.Certificate { + chain := []*x509.Certificate{leaf} + current := leaf + for current.CheckSignatureFrom(current) != nil { + issuer := issuerIn(current, pool, chain) + if issuer == nil { + break + } + chain = append(chain, issuer) + current = issuer + } + return chain +} + +func issuerIn(cert *x509.Certificate, pool, seen []*x509.Certificate) *x509.Certificate { + for _, candidate := range pool { + if slices.ContainsFunc(seen, candidate.Equal) { + continue + } + if cert.CheckSignatureFrom(candidate) == nil { + return candidate + } + } + return nil +} + +// ecdsaSignatureASN1 converts the fixed-width r||s form emitted by CNG into the DER form Go verifies. +func ecdsaSignatureASN1(raw []byte) ([]byte, error) { + if len(raw) == 0 || len(raw)%2 != 0 { + return nil, errors.New("malformed raw ECDSA signature") + } + half := len(raw) / 2 + return asn1.Marshal(struct{ R, S *big.Int }{new(big.Int).SetBytes(raw[:half]), new(big.Int).SetBytes(raw[half:])}) +} diff --git a/client/internal/certproof/keystore_test.go b/client/internal/certproof/keystore_test.go new file mode 100644 index 000000000..592d7414f --- /dev/null +++ b/client/internal/certproof/keystore_test.go @@ -0,0 +1,87 @@ +package certproof + +import ( + "crypto" + "crypto/ecdsa" + "crypto/rand" + "crypto/rsa" + "crypto/sha256" + "crypto/x509" + "testing" + "time" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/netbirdio/netbird/shared/management/certposture" + "github.com/netbirdio/netbird/shared/management/certposture/certtest" +) + +func TestBuildChain_FollowsIssuersThroughThePool(t *testing.T) { + root := certtest.NewCA(t, "root") + intermediate := certtest.NewIntermediate(t, root, "intermediate") + unrelated := certtest.NewCA(t, "unrelated") + leaf := intermediate.Issue(t, certtest.ECDSAKey(t), "device") + pool := []*x509.Certificate{unrelated.Cert, root.Cert, leaf, intermediate.Cert} + + chain := buildChain(leaf, pool) + + require.Equal(t, []*x509.Certificate{leaf, intermediate.Cert, root.Cert}, chain) + roots, err := certposture.ParseCAs([]string{root.PEM}) + require.NoError(t, err) + assert.NoError(t, certposture.VerifyChain(chain, roots, time.Now())) +} + +func TestBuildChain_StopsWhereThePoolEnds(t *testing.T) { + root := certtest.NewCA(t, "root") + intermediate := certtest.NewIntermediate(t, root, "intermediate") + leaf := intermediate.Issue(t, certtest.ECDSAKey(t), "device") + + assert.Equal(t, []*x509.Certificate{leaf}, buildChain(leaf, nil)) + assert.Equal(t, []*x509.Certificate{leaf, intermediate.Cert}, buildChain(leaf, []*x509.Certificate{intermediate.Cert})) +} + +func TestSchemeFor(t *testing.T) { + ecKey := certtest.ECDSAKey(t) + rsaKey := certtest.RSAKey(t) + pss := &rsa.PSSOptions{SaltLength: rsa.PSSSaltLengthEqualsHash, Hash: crypto.SHA256} + + tests := []struct { + name string + pub crypto.PublicKey + opts crypto.SignerOpts + want sigScheme + }{ + {"ecdsa sha256", ecKey.Public(), crypto.SHA256, schemeECDSASHA256}, + {"ecdsa sha384", ecKey.Public(), crypto.SHA384, schemeECDSASHA384}, + {"rsa pss sha256", rsaKey.Public(), pss, schemeRSAPSSSHA256}, + {"rsa pkcs1v15", rsaKey.Public(), crypto.SHA256, 0}, + {"ed25519", certtest.Ed25519Key(t).Public(), crypto.Hash(0), 0}, + } + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + got, err := schemeFor(tc.pub, tc.opts) + if tc.want == 0 { + assert.ErrorIs(t, err, errUnsupportedScheme) + return + } + require.NoError(t, err) + assert.Equal(t, tc.want, got) + }) + } +} + +func TestECDSASignatureASN1(t *testing.T) { + key := certtest.ECDSAKey(t).(*ecdsa.PrivateKey) + digest := sha256.Sum256([]byte("nonce")) + r, s, err := ecdsa.Sign(rand.Reader, key, digest[:]) + require.NoError(t, err) + raw := append(r.FillBytes(make([]byte, 32)), s.FillBytes(make([]byte, 32))...) + + der, err := ecdsaSignatureASN1(raw) + require.NoError(t, err) + assert.True(t, ecdsa.VerifyASN1(&key.PublicKey, digest[:], der)) + + _, err = ecdsaSignatureASN1(raw[:63]) + assert.Error(t, err) +} diff --git a/client/internal/certproof/store_other.go b/client/internal/certproof/store_other.go new file mode 100644 index 000000000..3888eaf6f --- /dev/null +++ b/client/internal/certproof/store_other.go @@ -0,0 +1,8 @@ +//go:build !darwin && !windows + +package certproof + +// DefaultStore is the PEM directory named by NB_CERT_STORE_DIR, or /etc/netbird/certs. +func DefaultStore() Store { + return NewFileStore(StoreDir()) +} diff --git a/client/internal/certproof/systemstore_windows.go b/client/internal/certproof/systemstore_windows.go new file mode 100644 index 000000000..62e86e438 --- /dev/null +++ b/client/internal/certproof/systemstore_windows.go @@ -0,0 +1,220 @@ +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 the local machine's personal store, completing +// their chains from the intermediate CA store. Keys are used through CNG and never exported. +type SystemStore struct{} + +func NewSystemStore() *SystemStore { + return &SystemStore{} +} + +func (s *SystemStore) Candidates(_ context.Context) ([]Candidate, error) { + leaves, err := storeCertificates(personalStore) + if err != nil || len(leaves) == 0 { + return nil, err + } + intermediates, err := storeCertificates(intermediateStore) + if err != nil { + return nil, err + } + pool := slices.Concat(intermediates, leaves) + candidates := make([]Candidate, 0, len(leaves)) + for _, leaf := range leaves { + candidates = append(candidates, Candidate{Chain: buildChain(leaf, pool), Signer: &systemStoreSigner{leaf: leaf}}) + } + return candidates, nil +} + +// 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 +} + +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(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(name string) ([]*x509.Certificate, error) { + store, err := openStore(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(name string) (windows.Handle, error) { + namePtr, err := windows.UTF16PtrFromString(name) + if err != nil { + return 0, err + } + flags := uint32(windows.CERT_SYSTEM_STORE_LOCAL_MACHINE | 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) +} diff --git a/client/internal/engine.go b/client/internal/engine.go index 54489ac38..d5e4f094c 100644 --- a/client/internal/engine.go +++ b/client/internal/engine.go @@ -1296,7 +1296,7 @@ func (e *Engine) applyInfoFlags(info *system.Info) { // certificates found in the local store, signing each challenge nonce for our peer key. func (e *Engine) attachCertificateProofs(info *system.Info, checks []*mgmProto.Checks) { peerKey := e.config.WgPrivateKey.PublicKey() - info.CertificateProofs = certproof.Collect(e.ctx, certproof.NewFileStore(certproof.StoreDir()), checks, peerKey[:]) + info.CertificateProofs = certproof.Collect(e.ctx, certproof.DefaultStore(), checks, peerKey[:]) } func (e *Engine) currentSystemInfo(ctx context.Context) *system.Info {