read the signed-in user's certificate store on Windows

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.
This commit is contained in:
mlsmaycon
2026-09-13 15:22:16 +02:00
parent 422912e7d5
commit e0ede59d68
10 changed files with 468 additions and 115 deletions
+61 -14
View File
@@ -14,9 +14,16 @@ to one WireGuard peer key and cannot be replayed by another peer.
| --- | --- | --- |
| macOS | System keychain | the daemon, directly |
| macOS | console user's login keychain | a helper in that user's desktop session |
| Windows | CNG / system certificate store | the daemon, directly |
| Windows | `LocalMachine\MY` | the service, directly |
| Windows | signed-in user's `CurrentUser\MY` | a helper launched with that session's token |
| Linux and others | PEM directory, `NB_CERT_STORE_DIR` or `/etc/netbird/certs` | the daemon, directly |
macOS and Windows both keep per-user certificates out of reach of a privileged daemon,
and both are handled the same way: the daemon reads the machine store itself and
launches `netbird posture cert-proof` as the signed-in user for the rest. Only the
signature and the chain come back. The helper, the request and response types and the
subcommand are shared; only the way the child is launched differs.
## macOS: why the daemon cannot read a login keychain
The daemon runs as root from a LaunchDaemon. Its keychain search list is the System
@@ -55,23 +62,63 @@ signature and the certificate chain come back.**
`netbird posture cert-proof` is hidden and not meant to be run by hand. It writes proofs
to stdout and every log line to stderr, so stdout stays parseable.
## Only the console user can be validated
## Windows: the service and the signed-in user
`LocalMachine\MY` is what the service reads, and it is where AD and Intune enrol device
certificates. `CurrentUser\MY` lives in the signed-in user's registry hive with private
keys protected by DPAPI against their profile, so it is only readable while running as
that user.
The failure mode differs from macOS in an important way: a service that opens
`CURRENT_USER` does **not** get an error. "Current user" resolves to the service
account's own hive, `HKU\S-1-5-18`, so it silently reads an empty and irrelevant store.
There is nothing to log. That is why the service only ever opens `LocalMachine` and asks
a helper for the rest.
Windows does let a privileged service assume a user identity, which macOS does not for
keychains, so no external tooling is involved:
```go
windows.WTSQueryUserToken(session, &token)
cmd.SysProcAttr = &syscall.SysProcAttr{Token: syscall.Token(token), CreationFlags: windows.CREATE_NO_WINDOW}
```
`CREATE_NO_WINDOW` matters: without it a console window flashes on the user's desktop on
every sync.
Session selection prefers the physical console, then falls back to any active session,
so remote desktop and VDI hosts work. `WTSQueryUserToken` needs `SE_TCB_NAME`, which
LocalSystem holds and an ordinary process does not, so a user-run `netbird up` skips the
helper and reads the machine store alone.
In-process impersonation would also work, but it is per-OS-thread while goroutines
migrate freely, so it would need `runtime.LockOSThread` around every key operation. The
child process avoids that class of bug entirely.
Unlike macOS, the Windows store acquires keys with `CRYPT_ACQUIRE_SILENT_FLAG`, so a key
that would need a prompt fails immediately instead of blocking. That also means a
smartcard PIN can never be satisfied this way.
## Only the signed-in user can be validated
This is the central limitation of the design, and it is deliberate.
A proof from a login keychain can only ever be produced for **the user whose desktop
session is currently open**. Consequences worth designing around:
A proof from a user store can only ever be produced for **the user whose session is
currently open**. Consequences worth designing around:
- **At the login window there is no user proof.** macOS reports no console user, or
attributes the console to root, and `CurrentConsoleUser` returns false for both. Only
System keychain device proofs are sent. A posture check that demands a user
certificate will fail on a Mac sitting at the lock screen before anyone logs in.
- **Logging out changes the answer.** Posture can flip between compliant and
non-compliant across logout, so management should treat "no proof" as its own state
rather than as a failed check, or users get disconnected at the login window.
- **Fast user switching picks one user.** Other logged-in users keep valid sessions and
unlocked keychains, but only the console user is asked. If you ever need all of them,
enumerate GUI sessions instead of the console user.
- **At the sign-in screen there is no user proof.** macOS reports no console user or
attributes the console to root, and `CurrentConsoleUser` returns false for both.
Windows reports no active session with a token. Only machine proofs are sent, so a
posture check that demands a user certificate fails on a machine nobody has signed
into yet.
- **Signing out changes the answer.** Posture can flip between compliant and
non-compliant across a sign-out, so management should treat "no proof" as its own
state rather than as a failed check, or users get disconnected at the sign-in screen.
- **One session is asked, not all of them.** macOS asks the console user, so other
fast-user-switched accounts are skipped even though their keychains are unlocked.
Windows prefers the console and otherwise takes the first active session. If you ever
need every signed-in user, both platforms would have to enumerate sessions and ask
each one.
- **A locked keychain still blocks signing.** A user can be logged in with their
keychain locked (locked on sleep, or manually). The helper then needs an unlock prompt
and may block, which is why the spawn has a 30s timeout and a failure is reported as
+4 -50
View File
@@ -3,8 +3,6 @@ package certproof
import (
"bytes"
"context"
"crypto/sha256"
"crypto/x509"
"encoding/json"
"fmt"
"os"
@@ -91,52 +89,8 @@ func collectAsConsoleUser(ctx context.Context, challenges []*proto.CertificateCh
return resp.Proofs, nil
}
func helperRequest(challenges []*proto.CertificateChallenge, peerKey []byte) HelperRequest {
req := HelperRequest{PeerKey: peerKey, Challenges: make([]HelperChallenge, 0, len(challenges))}
for _, challenge := range challenges {
req.Challenges = append(req.Challenges, HelperChallenge{
Nonce: challenge.GetNonce(),
CACertificates: challenge.GetCaCertificates(),
})
}
return req
}
// mergeProofs appends the user session proofs to the device proofs, dropping a leaf
// that both keychains hold so the same certificate is proven once.
func mergeProofs(device, user []certposture.Proof) []certposture.Proof {
if len(user) == 0 {
return device
}
seen := make(map[[sha256.Size]byte]struct{}, len(device))
for _, proof := range device {
if len(proof.Chain) > 0 {
seen[sha256.Sum256(proof.Chain[0])] = struct{}{}
}
}
merged := device
for _, proof := range user {
if len(proof.Chain) == 0 {
continue
}
fingerprint := sha256.Sum256(proof.Chain[0])
if _, done := seen[fingerprint]; done {
continue
}
seen[fingerprint] = struct{}{}
merged = append(merged, proof)
logUserProof(proof)
}
return merged
}
func logUserProof(proof certposture.Proof) {
leaf, err := x509.ParseCertificate(proof.Chain[0])
if err != nil {
log.Infof("certificate posture: console user proof carries an unparsable leaf: %v", err)
return
}
log.Infof("certificate posture: console user proved %q issued by %q", leaf.Subject, leaf.Issuer)
// helperStore is the store the helper reads. On macOS the keychain search list of the
// user's own session already is that user's keychain, so the platform default is right.
func helperStore() Store {
return DefaultStore()
}
+9 -3
View File
@@ -1,4 +1,4 @@
//go:build !darwin
//go:build !darwin && !windows
package certproof
@@ -10,8 +10,14 @@ import (
)
// CollectProofs answers the certificate challenges in checks from the platform store.
// Only macOS splits the work across a user session, so every other platform reads its
// store in the daemon itself.
// Only macOS and Windows keep per-user certificates out of reach of a privileged
// daemon, so every other platform reads its store in the daemon itself.
func CollectProofs(ctx context.Context, checks []*proto.Checks, peerKey []byte) []certposture.Proof {
return Collect(ctx, DefaultStore(), checks, peerKey)
}
// helperStore is the store the helper reads. Nothing launches a helper on these
// platforms, so it is the platform default.
func helperStore() Store {
return DefaultStore()
}
@@ -0,0 +1,101 @@
package certproof
import (
"bytes"
"context"
"encoding/json"
"fmt"
"os"
"os/exec"
"strings"
"syscall"
"time"
log "github.com/sirupsen/logrus"
"golang.org/x/sys/windows"
"github.com/netbirdio/netbird/shared/management/certposture"
"github.com/netbirdio/netbird/shared/management/proto"
)
const helperTimeout = 30 * time.Second
// CollectProofs answers the certificate challenges in checks from every store this
// machine can reach. The service reads the local machine store itself, where AD and
// Intune enrol device certificates, and reaches the signed-in user's store by launching
// a helper with that session's token. A machine at the sign-in screen therefore proves
// device certificates alone.
func CollectProofs(ctx context.Context, checks []*proto.Checks, peerKey []byte) []certposture.Proof {
challenges := certificateChallenges(checks)
if len(challenges) == 0 {
logNoChallenges(checks)
return nil
}
proofs := CollectChallenges(ctx, DefaultStore(), challenges, peerKey)
// The helper already runs as the signed-in user, and an ordinary process has no
// right to a session token, so only the service goes looking for one.
if !runningAsLocalSystem() {
return proofs
}
userProofs, err := collectAsDesktopUser(ctx, challenges, peerKey)
if err != nil {
log.Infof("certificate posture: user certificate store unavailable: %v", err)
}
return mergeProofs(proofs, userProofs)
}
// helperStore is the store the helper reads. It runs as the signed-in user, so it wants
// that user's store rather than the machine store the service already read.
func helperStore() Store {
return NewUserStore()
}
// collectAsDesktopUser runs the helper inside the interactive session of the signed-in
// user. Unlike a keychain on macOS, a Windows service can assume a user identity
// directly, so the session token goes straight into the child process.
func collectAsDesktopUser(ctx context.Context, challenges []*proto.CertificateChallenge, peerKey []byte) ([]certposture.Proof, error) {
user, ok := CurrentDesktopUser()
if !ok {
return nil, nil
}
defer user.Close()
binary, err := os.Executable()
if err != nil {
return nil, fmt.Errorf("resolve own binary: %w", err)
}
payload, err := json.Marshal(helperRequest(challenges, peerKey))
if err != nil {
return nil, fmt.Errorf("encode helper request: %w", err)
}
ctx, cancel := context.WithTimeout(ctx, helperTimeout)
defer cancel()
cmd := exec.CommandContext(ctx, binary, "posture", "cert-proof")
cmd.SysProcAttr = &syscall.SysProcAttr{
Token: syscall.Token(user.Token),
HideWindow: true,
CreationFlags: windows.CREATE_NO_WINDOW,
}
cmd.Stdin = bytes.NewReader(payload)
var stdout, stderr bytes.Buffer
cmd.Stdout = &stdout
cmd.Stderr = &stderr
log.Infof("certificate posture: asking the session of %q (session %d) to answer %d challenges", user.Name, user.Session, len(challenges))
if err := cmd.Run(); err != nil {
return nil, fmt.Errorf("run helper as %s: %w: %s", user.Name, err, strings.TrimSpace(stderr.String()))
}
var resp HelperResponse
if err := json.Unmarshal(stdout.Bytes(), &resp); err != nil {
return nil, fmt.Errorf("decode helper response: %w", err)
}
log.Infof("certificate posture: session of %q returned %d proofs", user.Name, len(resp.Proofs))
return resp.Proofs, nil
}
@@ -1,14 +1,9 @@
package certproof
import (
"crypto/sha256"
"testing"
"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 TestConsoleUser_OnlyADesktopSessionCanBeValidated(t *testing.T) {
@@ -42,29 +37,3 @@ func TestCurrentConsoleUser_AgreesWithItself(t *testing.T) {
assert.NotZero(t, user.UID, "a desktop session never belongs to uid 0")
assert.True(t, user.hasDesktop(), "a reported console user must be a desktop session")
}
func TestMergeProofs_ProvesACertificateHeldByBothKeychainsOnce(t *testing.T) {
ca := certtest.NewCA(t, "corp-root")
key := certtest.ECDSAKey(t)
shared := ca.Issue(t, key, "shared")
userOnly := ca.Issue(t, certtest.ECDSAKey(t), "user-only")
device := []certposture.Proof{{Chain: [][]byte{shared.Raw}}}
user := []certposture.Proof{{Chain: [][]byte{shared.Raw}}, {Chain: [][]byte{userOnly.Raw}}, {}}
merged := mergeProofs(device, user)
require.Len(t, merged, 2, "the shared leaf is proven once and the empty chain is dropped")
assert.Equal(t, shared.Raw, merged[0].Chain[0], "the device proof keeps its place")
assert.Equal(t, userOnly.Raw, merged[1].Chain[0], "the user-only certificate is appended")
}
func TestMergeProofs_KeepsDeviceProofsWhenNoUserSession(t *testing.T) {
ca := certtest.NewCA(t, "corp-root")
device := []certposture.Proof{{Chain: [][]byte{ca.Issue(t, certtest.ECDSAKey(t), "device").Raw}}}
merged := mergeProofs(device, nil)
require.Len(t, merged, 1, "a Mac at the login window still sends its device proof")
assert.Equal(t, sha256.Sum256(device[0].Chain[0]), sha256.Sum256(merged[0].Chain[0]), "the device proof is unchanged")
}
@@ -0,0 +1,126 @@
package certproof
import (
"fmt"
"unsafe"
log "github.com/sirupsen/logrus"
"golang.org/x/sys/windows"
)
const (
noActiveSession = 0xFFFFFFFF
// wtsCurrentServer is WTS_CURRENT_SERVER_HANDLE and wtsActive is WTSActive of
// WTS_CONNECTSTATE_CLASS. Neither is exported by x/sys/windows.
wtsCurrentServer = windows.Handle(0)
wtsActive = 0
)
// DesktopUser is an interactive session and the account signed into it. The user's
// certificate store is readable only from a process running as that account, because
// its private keys are protected against the user profile rather than the machine.
type DesktopUser struct {
Session uint32
Name string
Token windows.Token
}
// Close releases the session token.
func (u DesktopUser) Close() {
if err := u.Token.Close(); err != nil {
log.Debugf("failed closing desktop session token: %v", err)
}
}
// CurrentDesktopUser returns a token for the interactive user whose certificate store
// should be asked. The physical console comes first, and an active remote desktop
// session is used when nobody is at the console, which is how servers and VDI hosts are
// normally reached. The second return is false at the sign-in screen, where no
// interactive session exists and only machine certificates can be proven.
//
// Obtaining the token needs SE_TCB_NAME, which the LocalSystem service has and an
// ordinary process does not.
func CurrentDesktopUser() (DesktopUser, bool) {
if session := windows.WTSGetActiveConsoleSessionId(); session != noActiveSession {
if user, ok := desktopUser(session); ok {
return user, true
}
log.Infof("console session %d has nobody signed in, looking for an active remote session", session)
}
sessions, err := activeSessions()
if err != nil {
log.Infof("cannot enumerate terminal sessions: %v", err)
return DesktopUser{}, false
}
for _, session := range sessions {
if user, ok := desktopUser(session); ok {
return user, true
}
}
log.Info("no interactive session is signed in, no user certificate store is reachable")
return DesktopUser{}, false
}
func desktopUser(session uint32) (DesktopUser, bool) {
var token windows.Token
if err := windows.WTSQueryUserToken(session, &token); err != nil {
log.Debugf("no user token for session %d: %v", session, err)
return DesktopUser{}, false
}
name, err := tokenAccount(token)
if err != nil {
log.Infof("session %d token has no readable account: %v", session, err)
if closeErr := token.Close(); closeErr != nil {
log.Debugf("failed closing session token: %v", closeErr)
}
return DesktopUser{}, false
}
return DesktopUser{Session: session, Name: name, Token: token}, true
}
func tokenAccount(token windows.Token) (string, error) {
user, err := token.GetTokenUser()
if err != nil {
return "", fmt.Errorf("read token user: %w", err)
}
account, domain, _, err := user.User.Sid.LookupAccount("")
if err != nil {
return "", fmt.Errorf("look up account: %w", err)
}
if domain == "" {
return account, nil
}
return domain + `\` + account, nil
}
func activeSessions() ([]uint32, error) {
var info *windows.WTS_SESSION_INFO
var count uint32
if err := windows.WTSEnumerateSessions(wtsCurrentServer, 0, 1, &info, &count); err != nil {
return nil, fmt.Errorf("enumerate sessions: %w", err)
}
defer windows.WTSFreeMemory(uintptr(unsafe.Pointer(info)))
var active []uint32
for _, session := range unsafe.Slice(info, count) {
if session.State == wtsActive {
active = append(active, session.SessionID)
}
}
return active, nil
}
// runningAsLocalSystem reports whether this process is the service. The helper runs as
// the signed-in user and must read its own store rather than launching another helper.
func runningAsLocalSystem() bool {
user, err := windows.GetCurrentProcessToken().GetTokenUser()
if err != nil {
log.Debugf("failed reading own token user: %v", err)
return false
}
return user.User.Sid.IsWellKnown(windows.WinLocalSystemSid)
}
+1 -1
View File
@@ -36,7 +36,7 @@ type HelperResponse struct {
// lookup: the daemon cannot read a login keychain, so it launches this in the user's
// session instead. Only the signature crosses back, never the private key.
func RunHelper(ctx context.Context, in io.Reader, out io.Writer) error {
return runHelper(ctx, DefaultStore(), in, out)
return runHelper(ctx, helperStore(), in, out)
}
func runHelper(ctx context.Context, store Store, in io.Reader, out io.Writer) error {
+63
View File
@@ -0,0 +1,63 @@
//go:build darwin || windows
package certproof
import (
"crypto/sha256"
"crypto/x509"
log "github.com/sirupsen/logrus"
"github.com/netbirdio/netbird/shared/management/certposture"
"github.com/netbirdio/netbird/shared/management/proto"
)
func helperRequest(challenges []*proto.CertificateChallenge, peerKey []byte) HelperRequest {
req := HelperRequest{PeerKey: peerKey, Challenges: make([]HelperChallenge, 0, len(challenges))}
for _, challenge := range challenges {
req.Challenges = append(req.Challenges, HelperChallenge{
Nonce: challenge.GetNonce(),
CACertificates: challenge.GetCaCertificates(),
})
}
return req
}
// mergeProofs appends the user session proofs to the device proofs, dropping a leaf that
// both stores hold so the same certificate is proven once.
func mergeProofs(device, user []certposture.Proof) []certposture.Proof {
if len(user) == 0 {
return device
}
seen := make(map[[sha256.Size]byte]struct{}, len(device))
for _, proof := range device {
if len(proof.Chain) > 0 {
seen[sha256.Sum256(proof.Chain[0])] = struct{}{}
}
}
merged := device
for _, proof := range user {
if len(proof.Chain) == 0 {
continue
}
fingerprint := sha256.Sum256(proof.Chain[0])
if _, done := seen[fingerprint]; done {
continue
}
seen[fingerprint] = struct{}{}
merged = append(merged, proof)
logUserProof(proof)
}
return merged
}
func logUserProof(proof certposture.Proof) {
leaf, err := x509.ParseCertificate(proof.Chain[0])
if err != nil {
log.Infof("certificate posture: user proof carries an unparsable leaf: %v", err)
return
}
log.Infof("certificate posture: signed-in user proved %q issued by %q", leaf.Subject, leaf.Issuer)
}
@@ -0,0 +1,56 @@
//go:build darwin || windows
package certproof
import (
"crypto/sha256"
"testing"
"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"
"github.com/netbirdio/netbird/shared/management/proto"
)
func TestMergeProofs_ProvesACertificateHeldByBothStoresOnce(t *testing.T) {
ca := certtest.NewCA(t, "corp-root")
shared := ca.Issue(t, certtest.ECDSAKey(t), "shared")
userOnly := ca.Issue(t, certtest.ECDSAKey(t), "user-only")
device := []certposture.Proof{{Chain: [][]byte{shared.Raw}}}
user := []certposture.Proof{{Chain: [][]byte{shared.Raw}}, {Chain: [][]byte{userOnly.Raw}}, {}}
merged := mergeProofs(device, user)
require.Len(t, merged, 2, "the shared leaf is proven once and the empty chain is dropped")
assert.Equal(t, shared.Raw, merged[0].Chain[0], "the device proof keeps its place")
assert.Equal(t, userOnly.Raw, merged[1].Chain[0], "the user-only certificate is appended")
}
func TestMergeProofs_KeepsDeviceProofsWhenNoUserSession(t *testing.T) {
ca := certtest.NewCA(t, "corp-root")
device := []certposture.Proof{{Chain: [][]byte{ca.Issue(t, certtest.ECDSAKey(t), "device").Raw}}}
merged := mergeProofs(device, nil)
require.Len(t, merged, 1, "a machine with nobody signed in still sends its device proof")
assert.Equal(t, sha256.Sum256(device[0].Chain[0]), sha256.Sum256(merged[0].Chain[0]), "the device proof is unchanged")
}
func TestHelperRequest_CarriesEveryChallenge(t *testing.T) {
ca := certtest.NewCA(t, "corp-root")
challenges := []*proto.CertificateChallenge{
{Nonce: []byte("first"), CaCertificates: []string{ca.PEM}},
{Nonce: []byte("second")},
}
req := helperRequest(challenges, peerKey)
require.Len(t, req.Challenges, 2, "every challenge must reach the helper")
assert.Equal(t, peerKey, req.PeerKey, "the peer key binds the signature to this machine")
assert.Equal(t, []byte("first"), req.Challenges[0].Nonce, "the nonce must survive unchanged")
assert.Equal(t, []string{ca.PEM}, req.Challenges[0].CACertificates, "the accepted CAs must survive unchanged")
assert.Empty(t, req.Challenges[1].CACertificates, "a challenge without CAs stays without CAs")
}
@@ -46,35 +46,66 @@ 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{}
// 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{}
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(personalStore)
if err != nil || len(leaves) == 0 {
return nil, err
}
intermediates, err := storeCertificates(intermediateStore)
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 {
candidates = append(candidates, Candidate{Chain: buildChain(leaf, pool), Signer: &systemStoreSigner{leaf: leaf}})
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
leaf *x509.Certificate
location uint32
}
func (s *systemStoreSigner) Public() crypto.PublicKey {
@@ -86,7 +117,7 @@ func (s *systemStoreSigner) Sign(_ io.Reader, digest []byte, opts crypto.SignerO
if err != nil {
return nil, err
}
store, err := openStore(personalStore)
store, err := openStore(s.location, personalStore)
if err != nil {
return nil, err
}
@@ -164,8 +195,8 @@ func ncryptSignHash(key uintptr, padding unsafe.Pointer, digest, signature []byt
return result, nil
}
func storeCertificates(name string) ([]*x509.Certificate, error) {
store, err := openStore(name)
func storeCertificates(location uint32, name string) ([]*x509.Certificate, error) {
store, err := openStore(location, name)
if err != nil {
return nil, err
}
@@ -184,12 +215,12 @@ func storeCertificates(name string) ([]*x509.Certificate, error) {
return certs, err
}
func openStore(name string) (windows.Handle, error) {
func openStore(location uint32, 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)
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)