mirror of
https://github.com/netbirdio/netbird.git
synced 2026-10-27 23:09:08 +01:00
add keychain and cert store support
This commit is contained in:
@@ -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
|
||||||
|
}
|
||||||
@@ -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:])})
|
||||||
|
}
|
||||||
@@ -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)
|
||||||
|
}
|
||||||
@@ -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())
|
||||||
|
}
|
||||||
@@ -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)
|
||||||
|
}
|
||||||
@@ -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.
|
// certificates found in the local store, signing each challenge nonce for our peer key.
|
||||||
func (e *Engine) attachCertificateProofs(info *system.Info, checks []*mgmProto.Checks) {
|
func (e *Engine) attachCertificateProofs(info *system.Info, checks []*mgmProto.Checks) {
|
||||||
peerKey := e.config.WgPrivateKey.PublicKey()
|
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 {
|
func (e *Engine) currentSystemInfo(ctx context.Context) *system.Info {
|
||||||
|
|||||||
Reference in New Issue
Block a user