mirror of
https://github.com/netbirdio/netbird.git
synced 2026-09-23 07:09:08 +02:00
split cert and key location and allow key lookup on tpm
This commit is contained in:
@@ -16,7 +16,7 @@ to one WireGuard peer key and cannot be replayed by another peer.
|
||||
| macOS | console user's login keychain | a helper in that user's desktop session |
|
||||
| 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 |
|
||||
| Linux and others | PEM directory: `CertStoreDir` in the profile config, else `NB_CERT_STORE_DIR`, else `/etc/netbird/certs` | the daemon, directly |
|
||||
| Linux | a `TSS2 PRIVATE KEY` file in that directory, signed by the TPM | the daemon, through `/dev/tpmrm0` |
|
||||
| Linux | a PKCS#11 token, tpm2-pkcs11 for one, enabled by `CertPKCS11PIN` in the profile config | the daemon, through the token's module, in builds with the `pkcs11` tag |
|
||||
|
||||
@@ -170,9 +170,17 @@ or `pin-source` naming a file, and `CertPKCS11PIN` takes precedence over both. W
|
||||
PIN no login happens, and tpm2-pkcs11 then shows no private keys at all. Every other
|
||||
attribute is ignored.
|
||||
|
||||
Certificates and private keys are paired by `CKA_ID`, which is what `tpm2_ptool addcert`
|
||||
and `pkcs11-tool` set. Chains are completed from the other certificates on the token. Each
|
||||
operation opens a session, logs in, works, logs out and closes, so no token handle
|
||||
The certificate may live on the token or in the PEM directory: `CertStoreDir` in the
|
||||
profile config, else `NB_CERT_STORE_DIR`, else `/etc/netbird/certs`. On the token,
|
||||
certificates and private keys are paired by `CKA_ID`,
|
||||
which is what `tpm2_ptool addcert` and `pkcs11-tool` set. In the directory, a certificate
|
||||
file without a key of its own is paired with the token key whose public key it carries, so
|
||||
`device.pem` alone next to a key that only the TPM holds is enough; the token's public key
|
||||
object, which `tpm2_ptool addkey` and `import` create alongside the private one, is what
|
||||
the store compares against. Chains are completed from the certificates on the token and in
|
||||
the directory together, so intermediates may sit in either place.
|
||||
|
||||
Each operation opens a session, logs in, works, logs out and closes, so no token handle
|
||||
outlives a call, and the PEM directory keeps working when the token does not: the two are
|
||||
queried together and a failing token is logged rather than hiding file certificates.
|
||||
|
||||
|
||||
@@ -24,7 +24,7 @@ const helperTimeout = 30 * time.Second
|
||||
// installs device identities, and reaches the console user's login keychain only by
|
||||
// launching a helper into that user's session. A Mac sitting at the login window
|
||||
// therefore yields device proofs alone.
|
||||
func CollectProofs(ctx context.Context, checks []*proto.Checks, peerKey []byte, _ PKCS11Config) []certposture.Proof {
|
||||
func CollectProofs(ctx context.Context, checks []*proto.Checks, peerKey []byte, _ Config) []certposture.Proof {
|
||||
challenges := certificateChallenges(checks)
|
||||
if len(challenges) == 0 {
|
||||
logNoChallenges(checks)
|
||||
|
||||
@@ -9,12 +9,12 @@ import (
|
||||
"github.com/netbirdio/netbird/shared/management/proto"
|
||||
)
|
||||
|
||||
// CollectProofs answers the certificate challenges in checks from the platform store,
|
||||
// joined by the PKCS#11 token that token names when it names one. 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, token PKCS11Config) []certposture.Proof {
|
||||
return Collect(ctx, storeWithToken(token), checks, peerKey)
|
||||
// CollectProofs answers the certificate challenges in checks from the PEM directory cfg
|
||||
// names, joined by its PKCS#11 token when it names one. 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, cfg Config) []certposture.Proof {
|
||||
return Collect(ctx, storeWithToken(cfg), checks, peerKey)
|
||||
}
|
||||
|
||||
// helperStore is the store the helper reads. Nothing launches a helper on these
|
||||
|
||||
@@ -25,7 +25,7 @@ const helperTimeout = 30 * time.Second
|
||||
// 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, _ PKCS11Config) []certposture.Proof {
|
||||
func CollectProofs(ctx context.Context, checks []*proto.Checks, peerKey []byte, _ Config) []certposture.Proof {
|
||||
challenges := certificateChallenges(checks)
|
||||
if len(challenges) == 0 {
|
||||
logNoChallenges(checks)
|
||||
|
||||
@@ -23,23 +23,28 @@ type PKCS11Config struct {
|
||||
}
|
||||
|
||||
// PKCS11Store yields the identities of a PKCS#11 token, which is how tpm2-pkcs11 exposes
|
||||
// TPM-held keys on Linux. Certificates and keys are paired by CKA_ID, the convention
|
||||
// tpm2_ptool addcert and pkcs11-tool follow, and every signature happens on the token.
|
||||
// TPM-held keys on Linux. Certificates on the token are paired with keys by CKA_ID, the
|
||||
// convention tpm2_ptool addcert and pkcs11-tool follow; certificate files in the PEM
|
||||
// directory by public key. Every signature happens on the token.
|
||||
type PKCS11Store struct {
|
||||
uri *pkcs11.URI
|
||||
pin string
|
||||
uri *pkcs11.URI
|
||||
pin string
|
||||
certDir string
|
||||
}
|
||||
|
||||
// NewPKCS11Store parses cfg.URI, standing in the bare defaults when it is empty.
|
||||
func NewPKCS11Store(cfg PKCS11Config) (*PKCS11Store, error) {
|
||||
// NewPKCS11Store parses cfg.URI, standing in the bare defaults when it is empty. Files in
|
||||
// certDir without a key of their own are paired with the token's keys by public key.
|
||||
func NewPKCS11Store(cfg PKCS11Config, certDir string) (*PKCS11Store, error) {
|
||||
store := &PKCS11Store{uri: &pkcs11.URI{}, pin: cfg.PIN, certDir: certDir}
|
||||
if cfg.URI == "" {
|
||||
return &PKCS11Store{uri: &pkcs11.URI{}, pin: cfg.PIN}, nil
|
||||
return store, nil
|
||||
}
|
||||
parsed, err := pkcs11.ParseURI(cfg.URI)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &PKCS11Store{uri: parsed, pin: cfg.PIN}, nil
|
||||
store.uri = parsed
|
||||
return store, nil
|
||||
}
|
||||
|
||||
func (s *PKCS11Store) Candidates(_ context.Context) ([]Candidate, error) {
|
||||
@@ -53,25 +58,115 @@ func (s *PKCS11Store) Candidates(_ context.Context) ([]Candidate, error) {
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
log.Infof("%s holds %d certificates", s, len(certs))
|
||||
fileChains, err := s.fileChains()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
log.Infof("%s holds %d certificates, %d certificate files without a key wait for its keys", s, len(certs), len(fileChains))
|
||||
|
||||
pool := make([]*x509.Certificate, 0, len(certs))
|
||||
for _, cert := range certs {
|
||||
pool = append(pool, cert.cert)
|
||||
}
|
||||
for _, chain := range fileChains {
|
||||
pool = append(pool, chain...)
|
||||
}
|
||||
|
||||
var candidates []Candidate
|
||||
for _, cert := range certs {
|
||||
if _, err := privateKey(session, cert.id); err != nil {
|
||||
log.Infof("%s certificate %q has no usable private key: %v", s, cert.cert.Subject, err)
|
||||
continue
|
||||
}
|
||||
chain := buildChain(cert.cert, pool)
|
||||
log.Infof("%s candidate %q issued by %q built a chain of %d certificates", s, cert.cert.Subject, cert.cert.Issuer, len(chain))
|
||||
candidates = append(candidates, Candidate{Chain: chain, Signer: &pkcs11Signer{store: s, leaf: cert.cert, id: cert.id}})
|
||||
candidates = append(candidates, s.candidate(cert.cert, cert.id, pool))
|
||||
}
|
||||
if len(fileChains) == 0 {
|
||||
return candidates, nil
|
||||
}
|
||||
|
||||
keys, err := tokenPublicKeys(session)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
for _, chain := range fileChains {
|
||||
leaf := chain[0]
|
||||
id, ok := keys.idFor(leaf.PublicKey)
|
||||
if !ok {
|
||||
log.Debugf("%s holds no key for certificate %q from %s", s, leaf.Subject, s.certDir)
|
||||
continue
|
||||
}
|
||||
candidates = append(candidates, s.candidate(leaf, id, pool))
|
||||
}
|
||||
return candidates, nil
|
||||
}
|
||||
|
||||
func (s *PKCS11Store) candidate(leaf *x509.Certificate, id []byte, pool []*x509.Certificate) Candidate {
|
||||
chain := buildChain(leaf, pool)
|
||||
log.Infof("%s candidate %q issued by %q built a chain of %d certificates", s, leaf.Subject, leaf.Issuer, len(chain))
|
||||
return Candidate{Chain: chain, Signer: &pkcs11Signer{store: s, leaf: leaf, id: id}}
|
||||
}
|
||||
|
||||
// fileChains reads the certificate files in the PEM directory that carry no key of their
|
||||
// own; the file store answers for the ones that do.
|
||||
func (s *PKCS11Store) fileChains() ([][]*x509.Certificate, error) {
|
||||
if s.certDir == "" {
|
||||
return nil, nil
|
||||
}
|
||||
paths, err := certFiles(s.certDir)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
var chains [][]*x509.Certificate
|
||||
for _, path := range paths {
|
||||
chain, signer, err := loadPEM(path)
|
||||
if err != nil || signer != nil {
|
||||
continue
|
||||
}
|
||||
chains = append(chains, chain)
|
||||
}
|
||||
return chains, nil
|
||||
}
|
||||
|
||||
type tokenKey struct {
|
||||
id []byte
|
||||
public crypto.PublicKey
|
||||
}
|
||||
|
||||
type tokenKeys []tokenKey
|
||||
|
||||
func tokenPublicKeys(session *pkcs11.Session) (tokenKeys, error) {
|
||||
objects, err := session.FindObjects(pkcs11.Attribute{Type: pkcs11.AttrClass, Value: pkcs11.ULong(pkcs11.ClassPublicKey)})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
keys := make(tokenKeys, 0, len(objects))
|
||||
for _, object := range objects {
|
||||
id, err := session.Attribute(object, pkcs11.AttrID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
public, err := session.PublicKey(object)
|
||||
if err != nil {
|
||||
log.Debugf("skipping public key on PKCS#11 token: %v", err)
|
||||
continue
|
||||
}
|
||||
keys = append(keys, tokenKey{id: id, public: public})
|
||||
}
|
||||
return keys, nil
|
||||
}
|
||||
|
||||
// idFor finds the token key whose public half is pub, so a certificate kept outside the
|
||||
// token is still signed for by the key inside it.
|
||||
func (k tokenKeys) idFor(pub crypto.PublicKey) ([]byte, bool) {
|
||||
for _, key := range k {
|
||||
equaler, ok := key.public.(interface{ Equal(crypto.PublicKey) bool })
|
||||
if ok && len(key.id) > 0 && equaler.Equal(pub) {
|
||||
return key.id, true
|
||||
}
|
||||
}
|
||||
return nil, false
|
||||
}
|
||||
|
||||
func (s *PKCS11Store) String() string {
|
||||
if s.uri.Token == "" {
|
||||
return "PKCS#11 token"
|
||||
|
||||
@@ -52,15 +52,7 @@ func TestStores_KeepsFileCertificatesWhenTokenFails(t *testing.T) {
|
||||
// It imports a key and its certificate as token objects, then proves the certificate
|
||||
// through the store the way the daemon would. Every run adds one more identity to the token.
|
||||
func TestCollect_PKCS11TokenEndToEnd(t *testing.T) {
|
||||
uri := os.Getenv(testPKCS11URIEnv)
|
||||
if uri == "" {
|
||||
t.Skipf("set %s to a PKCS#11 URI with a PIN to run", testPKCS11URIEnv)
|
||||
}
|
||||
store, err := NewPKCS11Store(PKCS11Config{URI: uri})
|
||||
require.NoError(t, err)
|
||||
if _, err := pkcs11.Load(store.uri.Module()); errors.Is(err, pkcs11.ErrUnsupported) {
|
||||
t.Skip(err)
|
||||
}
|
||||
store, uri := pkcs11TestStore(t, "")
|
||||
|
||||
keys := map[string]crypto.Signer{"ecdsa": certtest.ECDSAKey(t), "rsa": certtest.RSAKey(t)}
|
||||
for name, key := range keys {
|
||||
@@ -83,25 +75,20 @@ func TestCollect_PKCS11TokenEndToEnd(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
// Attribute and key types the import needs and the store does not.
|
||||
// Attribute types the import needs and the store does not.
|
||||
const (
|
||||
attrPrivate = 0x2
|
||||
attrIssuer = 0x81
|
||||
attrSerialNumber = 0x82
|
||||
attrKeyType = 0x100
|
||||
attrSensitive = 0x103
|
||||
attrSign = 0x108
|
||||
attrModulus = 0x120
|
||||
attrPublicExponent = 0x122
|
||||
attrVerify = 0x10a
|
||||
attrPrivateExponent = 0x123
|
||||
attrPrime1 = 0x124
|
||||
attrPrime2 = 0x125
|
||||
attrExponent1 = 0x126
|
||||
attrExponent2 = 0x127
|
||||
attrCoefficient = 0x128
|
||||
attrECParams = 0x180
|
||||
keyTypeRSA = 0x0
|
||||
keyTypeEC = 0x3
|
||||
)
|
||||
|
||||
var (
|
||||
@@ -111,7 +98,71 @@ var (
|
||||
oidP256 = []byte{0x06, 0x08, 0x2a, 0x86, 0x48, 0xce, 0x3d, 0x03, 0x01, 0x07}
|
||||
)
|
||||
|
||||
// importIdentity stores key and leaf on the token the way tpm2_ptool import and addcert
|
||||
// do: private and public key objects plus the certificate, all under one CKA_ID.
|
||||
func importIdentity(t *testing.T, uri string, key crypto.Signer, leaf *x509.Certificate) {
|
||||
t.Helper()
|
||||
id := importKey(t, uri, key, leaf.Subject.CommonName)
|
||||
importCertificate(t, uri, leaf, id)
|
||||
}
|
||||
|
||||
func importKey(t *testing.T, uri string, key crypto.Signer, label string) []byte {
|
||||
t.Helper()
|
||||
session := readWriteSession(t, uri)
|
||||
defer session.Close()
|
||||
|
||||
id := make([]byte, 8)
|
||||
_, err := rand.Read(id)
|
||||
require.NoError(t, err)
|
||||
|
||||
private := []pkcs11.Attribute{
|
||||
attr(pkcs11.AttrClass, pkcs11.ULong(pkcs11.ClassPrivateKey)),
|
||||
attr(pkcs11.AttrToken, ckTrue),
|
||||
attr(attrPrivate, ckTrue),
|
||||
attr(attrSensitive, ckTrue),
|
||||
attr(attrSign, ckTrue),
|
||||
attr(pkcs11.AttrLabel, []byte(label)),
|
||||
attr(pkcs11.AttrID, id),
|
||||
}
|
||||
_, err = session.CreateObject(append(private, privateKeyAttributes(t, key)...)...)
|
||||
require.NoError(t, err, "import private key")
|
||||
|
||||
public := []pkcs11.Attribute{
|
||||
attr(pkcs11.AttrClass, pkcs11.ULong(pkcs11.ClassPublicKey)),
|
||||
attr(pkcs11.AttrToken, ckTrue),
|
||||
attr(attrPrivate, ckFalse),
|
||||
attr(attrVerify, ckTrue),
|
||||
attr(pkcs11.AttrLabel, []byte(label)),
|
||||
attr(pkcs11.AttrID, id),
|
||||
}
|
||||
_, err = session.CreateObject(append(public, publicKeyAttributes(t, key)...)...)
|
||||
require.NoError(t, err, "import public key")
|
||||
return id
|
||||
}
|
||||
|
||||
func importCertificate(t *testing.T, uri string, leaf *x509.Certificate, id []byte) {
|
||||
t.Helper()
|
||||
session := readWriteSession(t, uri)
|
||||
defer session.Close()
|
||||
|
||||
serial, err := asn1.Marshal(leaf.SerialNumber)
|
||||
require.NoError(t, err)
|
||||
_, err = session.CreateObject(
|
||||
attr(pkcs11.AttrClass, pkcs11.ULong(pkcs11.ClassCertificate)),
|
||||
attr(pkcs11.AttrCertificateType, pkcs11.ULong(pkcs11.CertificateX509)),
|
||||
attr(pkcs11.AttrToken, ckTrue),
|
||||
attr(attrPrivate, ckFalse),
|
||||
attr(pkcs11.AttrLabel, []byte(leaf.Subject.CommonName)),
|
||||
attr(pkcs11.AttrID, id),
|
||||
attr(pkcs11.AttrSubject, leaf.RawSubject),
|
||||
attr(attrIssuer, leaf.RawIssuer),
|
||||
attr(attrSerialNumber, serial),
|
||||
attr(pkcs11.AttrValue, leaf.Raw),
|
||||
)
|
||||
require.NoError(t, err, "import certificate")
|
||||
}
|
||||
|
||||
func readWriteSession(t *testing.T, uri string) *pkcs11.Session {
|
||||
t.Helper()
|
||||
parsed, err := pkcs11.ParseURI(uri)
|
||||
require.NoError(t, err)
|
||||
@@ -121,57 +172,24 @@ func importIdentity(t *testing.T, uri string, key crypto.Signer, leaf *x509.Cert
|
||||
require.NoError(t, err)
|
||||
session, err := module.OpenReadWriteSession(parsed.Token, pin)
|
||||
require.NoError(t, err)
|
||||
defer session.Close()
|
||||
|
||||
id := make([]byte, 8)
|
||||
_, err = rand.Read(id)
|
||||
require.NoError(t, err)
|
||||
label := []byte(leaf.Subject.CommonName)
|
||||
serial, err := asn1.Marshal(leaf.SerialNumber)
|
||||
require.NoError(t, err)
|
||||
|
||||
_, err = session.CreateObject(
|
||||
attr(pkcs11.AttrClass, pkcs11.ULong(pkcs11.ClassCertificate)),
|
||||
attr(pkcs11.AttrCertificateType, pkcs11.ULong(pkcs11.CertificateX509)),
|
||||
attr(pkcs11.AttrToken, ckTrue),
|
||||
attr(attrPrivate, ckFalse),
|
||||
attr(pkcs11.AttrLabel, label),
|
||||
attr(pkcs11.AttrID, id),
|
||||
attr(pkcs11.AttrSubject, leaf.RawSubject),
|
||||
attr(attrIssuer, leaf.RawIssuer),
|
||||
attr(attrSerialNumber, serial),
|
||||
attr(pkcs11.AttrValue, leaf.Raw),
|
||||
)
|
||||
require.NoError(t, err, "import certificate")
|
||||
|
||||
template := []pkcs11.Attribute{
|
||||
attr(pkcs11.AttrClass, pkcs11.ULong(pkcs11.ClassPrivateKey)),
|
||||
attr(pkcs11.AttrToken, ckTrue),
|
||||
attr(attrPrivate, ckTrue),
|
||||
attr(attrSensitive, ckTrue),
|
||||
attr(attrSign, ckTrue),
|
||||
attr(pkcs11.AttrLabel, label),
|
||||
attr(pkcs11.AttrID, id),
|
||||
}
|
||||
_, err = session.CreateObject(append(template, keyAttributes(t, key)...)...)
|
||||
require.NoError(t, err, "import private key")
|
||||
return session
|
||||
}
|
||||
|
||||
func keyAttributes(t *testing.T, key crypto.Signer) []pkcs11.Attribute {
|
||||
func privateKeyAttributes(t *testing.T, key crypto.Signer) []pkcs11.Attribute {
|
||||
t.Helper()
|
||||
switch k := key.(type) {
|
||||
case *ecdsa.PrivateKey:
|
||||
return []pkcs11.Attribute{
|
||||
attr(attrKeyType, pkcs11.ULong(keyTypeEC)),
|
||||
attr(attrECParams, oidP256),
|
||||
attr(pkcs11.AttrKeyType, pkcs11.ULong(pkcs11.KeyEC)),
|
||||
attr(pkcs11.AttrECParams, oidP256),
|
||||
attr(pkcs11.AttrValue, k.D.FillBytes(make([]byte, 32))),
|
||||
}
|
||||
case *rsa.PrivateKey:
|
||||
k.Precompute()
|
||||
return []pkcs11.Attribute{
|
||||
attr(attrKeyType, pkcs11.ULong(keyTypeRSA)),
|
||||
attr(attrModulus, k.N.Bytes()),
|
||||
attr(attrPublicExponent, big.NewInt(int64(k.E)).Bytes()),
|
||||
attr(pkcs11.AttrKeyType, pkcs11.ULong(pkcs11.KeyRSA)),
|
||||
attr(pkcs11.AttrModulus, k.N.Bytes()),
|
||||
attr(pkcs11.AttrPublicExponent, big.NewInt(int64(k.E)).Bytes()),
|
||||
attr(attrPrivateExponent, k.D.Bytes()),
|
||||
attr(attrPrime1, k.Primes[0].Bytes()),
|
||||
attr(attrPrime2, k.Primes[1].Bytes()),
|
||||
@@ -184,10 +202,107 @@ func keyAttributes(t *testing.T, key crypto.Signer) []pkcs11.Attribute {
|
||||
return nil
|
||||
}
|
||||
|
||||
// publicKeyAttributes describes the CKO_PUBLIC_KEY object tokens keep next to a private
|
||||
// key, which is what the store reads to pair a certificate file with its key.
|
||||
func publicKeyAttributes(t *testing.T, key crypto.Signer) []pkcs11.Attribute {
|
||||
t.Helper()
|
||||
switch k := key.(type) {
|
||||
case *ecdsa.PrivateKey:
|
||||
point := append([]byte{4}, k.X.FillBytes(make([]byte, 32))...)
|
||||
point = append(point, k.Y.FillBytes(make([]byte, 32))...)
|
||||
wrapped, err := asn1.Marshal(point)
|
||||
require.NoError(t, err)
|
||||
return []pkcs11.Attribute{
|
||||
attr(pkcs11.AttrKeyType, pkcs11.ULong(pkcs11.KeyEC)),
|
||||
attr(pkcs11.AttrECParams, oidP256),
|
||||
attr(pkcs11.AttrECPoint, wrapped),
|
||||
}
|
||||
case *rsa.PrivateKey:
|
||||
return []pkcs11.Attribute{
|
||||
attr(pkcs11.AttrKeyType, pkcs11.ULong(pkcs11.KeyRSA)),
|
||||
attr(pkcs11.AttrModulus, k.N.Bytes()),
|
||||
attr(pkcs11.AttrPublicExponent, big.NewInt(int64(k.E)).Bytes()),
|
||||
}
|
||||
}
|
||||
t.Fatalf("unsupported key %T", key)
|
||||
return nil
|
||||
}
|
||||
|
||||
func attr(typ uint, value []byte) pkcs11.Attribute {
|
||||
return pkcs11.Attribute{Type: typ, Value: value}
|
||||
}
|
||||
|
||||
// pkcs11TestStore builds the store for the token NB_TEST_PKCS11_URI names, skipping when
|
||||
// no token is configured or this build lacks PKCS#11 support.
|
||||
func pkcs11TestStore(t *testing.T, certDir string) (*PKCS11Store, string) {
|
||||
t.Helper()
|
||||
uri := os.Getenv(testPKCS11URIEnv)
|
||||
if uri == "" {
|
||||
t.Skipf("set %s to a PKCS#11 URI with a PIN to run", testPKCS11URIEnv)
|
||||
}
|
||||
store, err := NewPKCS11Store(PKCS11Config{URI: uri}, certDir)
|
||||
require.NoError(t, err)
|
||||
if _, err := pkcs11.Load(store.uri.Module()); errors.Is(err, pkcs11.ErrUnsupported) {
|
||||
t.Skip(err)
|
||||
}
|
||||
return store, uri
|
||||
}
|
||||
|
||||
// TestCollect_PKCS11KeyWithFileCertificate covers the split layout: the key lives on the
|
||||
// token, the certificate is a PEM file in the directory, and the two are paired by public
|
||||
// key because nothing on the token carries the certificate's CKA_ID.
|
||||
func TestCollect_PKCS11KeyWithFileCertificate(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
store, uri := pkcs11TestStore(t, dir)
|
||||
|
||||
keys := map[string]crypto.Signer{"ecdsa": certtest.ECDSAKey(t), "rsa": certtest.RSAKey(t)}
|
||||
for name, key := range keys {
|
||||
t.Run(name, func(t *testing.T) {
|
||||
ca := certtest.NewCA(t, "corp-file-"+name)
|
||||
leaf := ca.Issue(t, key, "device-file-"+name)
|
||||
importKey(t, uri, key, "device-file-"+name)
|
||||
writeFile(t, dir, "device-"+name+".pem", certtest.CertPEM(leaf))
|
||||
|
||||
challenger := certposture.NewChallenger([]byte("secret"))
|
||||
now := time.Now()
|
||||
nonce := challenger.Nonce(peerKey, now)
|
||||
checks := []*proto.Checks{{CertificateChallenge: &proto.CertificateChallenge{Nonce: nonce, CaCertificates: []string{ca.PEM}}}}
|
||||
|
||||
proofs := Collect(context.Background(), store, checks, peerKey)
|
||||
require.Len(t, proofs, 1, "the token key must prove the certificate kept on disk")
|
||||
chain, err := challenger.Verify(proofs[0], peerKey, now)
|
||||
require.NoError(t, err)
|
||||
assert.True(t, leaf.Equal(chain[0]), "proof must carry the certificate from the directory")
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestPKCS11Store_FileChains(t *testing.T) {
|
||||
ca := certtest.NewCA(t, "corp")
|
||||
dir := t.TempDir()
|
||||
// Only certificate files without a key of their own belong to the token; the file
|
||||
// store answers for the others, and non-certificate files are ignored.
|
||||
writeFile(t, dir, "device.pem", certtest.CertPEM(ca.Issue(t, certtest.ECDSAKey(t), "device")))
|
||||
writeFile(t, dir, "ca.crt", ca.PEM)
|
||||
keyed := certtest.ECDSAKey(t)
|
||||
writeFile(t, dir, "inline.pem", certtest.CertPEM(ca.Issue(t, keyed, "inline"))+certtest.KeyPEM(t, keyed))
|
||||
writeFile(t, dir, "sibling.crt", certtest.CertPEM(ca.Issue(t, keyed, "sibling")))
|
||||
writeFile(t, dir, "sibling.key", certtest.KeyPEM(t, keyed))
|
||||
writeFile(t, dir, "notes.txt", "not a certificate")
|
||||
|
||||
chains, err := (&PKCS11Store{uri: &pkcs11.URI{}, certDir: dir}).fileChains()
|
||||
require.NoError(t, err)
|
||||
var subjects []string
|
||||
for _, chain := range chains {
|
||||
subjects = append(subjects, chain[0].Subject.CommonName)
|
||||
}
|
||||
assert.ElementsMatch(t, []string{"device", "corp"}, subjects, "only key-less certificate files are left to the token")
|
||||
|
||||
chains, err = (&PKCS11Store{uri: &pkcs11.URI{}}).fileChains()
|
||||
require.NoError(t, err)
|
||||
assert.Empty(t, chains, "no directory configured means no file certificates")
|
||||
}
|
||||
|
||||
func TestNewPKCS11Store_PIN(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
@@ -202,7 +317,7 @@ func TestNewPKCS11Store_PIN(t *testing.T) {
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
store, err := NewPKCS11Store(tt.cfg)
|
||||
store, err := NewPKCS11Store(tt.cfg, "")
|
||||
require.NoError(t, err)
|
||||
pin, err := store.userPIN()
|
||||
require.NoError(t, err)
|
||||
@@ -211,6 +326,6 @@ func TestNewPKCS11Store_PIN(t *testing.T) {
|
||||
})
|
||||
}
|
||||
|
||||
_, err := NewPKCS11Store(PKCS11Config{URI: "not-a-pkcs11-uri", PIN: "1234"})
|
||||
_, err := NewPKCS11Store(PKCS11Config{URI: "not-a-pkcs11-uri", PIN: "1234"}, "")
|
||||
assert.Error(t, err, "a malformed URI must not be silently replaced by the defaults")
|
||||
}
|
||||
|
||||
@@ -33,6 +33,21 @@ type Store interface {
|
||||
Candidates(ctx context.Context) ([]Candidate, error)
|
||||
}
|
||||
|
||||
// Config selects where the Linux daemon looks for certificates: Dir is the PEM directory,
|
||||
// empty for NB_CERT_STORE_DIR or /etc/netbird/certs, and PKCS11 names a token whose keys
|
||||
// sign for certificates on the token or in that directory.
|
||||
type Config struct {
|
||||
Dir string
|
||||
PKCS11 PKCS11Config
|
||||
}
|
||||
|
||||
func (c Config) dir() string {
|
||||
if c.Dir != "" {
|
||||
return c.Dir
|
||||
}
|
||||
return StoreDir()
|
||||
}
|
||||
|
||||
// FileStore reads PEM files from a directory. A file holds the chain (leaf first) and
|
||||
// either its private key or a sibling "<name>.key" file holds it. The key is a plain
|
||||
// PKCS#8, EC or RSA key, or a TSS2 key the TPM signs with.
|
||||
@@ -52,55 +67,76 @@ func StoreDir() string {
|
||||
}
|
||||
|
||||
func (s *FileStore) Candidates(_ context.Context) ([]Candidate, error) {
|
||||
entries, err := os.ReadDir(s.dir)
|
||||
if errors.Is(err, os.ErrNotExist) {
|
||||
return nil, nil
|
||||
}
|
||||
paths, err := certFiles(s.dir)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("read certificate store %s: %w", s.dir, err)
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var candidates []Candidate
|
||||
for _, entry := range entries {
|
||||
if entry.IsDir() || !isCertFile(entry.Name()) {
|
||||
continue
|
||||
}
|
||||
path := filepath.Join(s.dir, entry.Name())
|
||||
candidate, err := s.load(path)
|
||||
for _, path := range paths {
|
||||
chain, signer, err := loadPEM(path)
|
||||
if err != nil {
|
||||
log.Warnf("skipping certificate %s: %v", path, err)
|
||||
continue
|
||||
}
|
||||
candidates = append(candidates, candidate)
|
||||
if signer == nil {
|
||||
log.Debugf("certificate %s has no key file, only a token can sign for it", path)
|
||||
continue
|
||||
}
|
||||
candidates = append(candidates, Candidate{Chain: chain, Signer: signer})
|
||||
}
|
||||
return candidates, nil
|
||||
}
|
||||
|
||||
func (s *FileStore) load(path string) (Candidate, error) {
|
||||
// certFiles lists the certificate files in dir, none when the directory does not exist.
|
||||
func certFiles(dir string) ([]string, error) {
|
||||
entries, err := os.ReadDir(dir)
|
||||
if errors.Is(err, os.ErrNotExist) {
|
||||
return nil, nil
|
||||
}
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("read certificate store %s: %w", dir, err)
|
||||
}
|
||||
var paths []string
|
||||
for _, entry := range entries {
|
||||
if !entry.IsDir() && isCertFile(entry.Name()) {
|
||||
paths = append(paths, filepath.Join(dir, entry.Name()))
|
||||
}
|
||||
}
|
||||
return paths, nil
|
||||
}
|
||||
|
||||
// loadPEM reads a certificate file and its private key, held in the file itself or in
|
||||
// the sibling "<name>.key" file. The signer is nil when neither holds a key.
|
||||
func loadPEM(path string) ([]*x509.Certificate, crypto.Signer, error) {
|
||||
data, err := os.ReadFile(path)
|
||||
if err != nil {
|
||||
return Candidate{}, err
|
||||
return nil, nil, err
|
||||
}
|
||||
chain, signer, err := parsePEM(data)
|
||||
if err != nil {
|
||||
return Candidate{}, err
|
||||
return nil, nil, err
|
||||
}
|
||||
if len(chain) == 0 {
|
||||
return Candidate{}, errors.New("no certificate")
|
||||
return nil, nil, errors.New("no certificate")
|
||||
}
|
||||
if signer != nil {
|
||||
return chain, signer, nil
|
||||
}
|
||||
keyData, err := os.ReadFile(strings.TrimSuffix(path, filepath.Ext(path)) + ".key")
|
||||
if errors.Is(err, os.ErrNotExist) {
|
||||
return chain, nil, nil
|
||||
}
|
||||
if err != nil {
|
||||
return nil, nil, fmt.Errorf("read key file: %w", err)
|
||||
}
|
||||
if _, signer, err = parsePEM(keyData); err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
if signer == nil {
|
||||
keyData, err := os.ReadFile(strings.TrimSuffix(path, filepath.Ext(path)) + ".key")
|
||||
if err != nil {
|
||||
return Candidate{}, fmt.Errorf("no private key: %w", err)
|
||||
}
|
||||
if _, signer, err = parsePEM(keyData); err != nil {
|
||||
return Candidate{}, err
|
||||
}
|
||||
if signer == nil {
|
||||
return Candidate{}, errors.New("no private key in key file")
|
||||
}
|
||||
return nil, nil, errors.New("no private key in key file")
|
||||
}
|
||||
return Candidate{Chain: chain, Signer: signer}, nil
|
||||
return chain, signer, nil
|
||||
}
|
||||
|
||||
func parsePEM(data []byte) ([]*x509.Certificate, crypto.Signer, error) {
|
||||
|
||||
@@ -9,13 +9,14 @@ func DefaultStore() Store {
|
||||
return NewFileStore(StoreDir())
|
||||
}
|
||||
|
||||
// storeWithToken joins DefaultStore with the PKCS#11 token cfg names, when it names one.
|
||||
func storeWithToken(cfg PKCS11Config) Store {
|
||||
files := DefaultStore()
|
||||
if cfg.URI == "" && cfg.PIN == "" {
|
||||
// storeWithToken reads the PEM directory cfg names, joined by the PKCS#11 token when cfg
|
||||
// names one. The token pairs the directory's key-less certificates with its own keys.
|
||||
func storeWithToken(cfg Config) Store {
|
||||
files := NewFileStore(cfg.dir())
|
||||
if cfg.PKCS11.URI == "" && cfg.PKCS11.PIN == "" {
|
||||
return files
|
||||
}
|
||||
token, err := NewPKCS11Store(cfg)
|
||||
token, err := NewPKCS11Store(cfg.PKCS11, cfg.dir())
|
||||
if err != nil {
|
||||
log.Warnf("ignoring PKCS#11 URI: %v", err)
|
||||
return files
|
||||
|
||||
@@ -6,20 +6,31 @@ import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestStoreWithToken(t *testing.T) {
|
||||
assert.IsType(t, &FileStore{}, storeWithToken(PKCS11Config{}), "nothing configured reads the PEM directory alone")
|
||||
assert.IsType(t, &FileStore{}, storeWithToken(PKCS11Config{URI: "not-a-pkcs11-uri"}), "an invalid URI must not hide the PEM directory")
|
||||
dir := t.TempDir()
|
||||
|
||||
files, ok := storeWithToken(Config{Dir: dir}).(*FileStore)
|
||||
require.True(t, ok, "a directory alone reads that directory alone")
|
||||
assert.Equal(t, dir, files.dir, "the configured directory replaces the default")
|
||||
|
||||
files, ok = storeWithToken(Config{}).(*FileStore)
|
||||
require.True(t, ok, "nothing configured reads the PEM directory alone")
|
||||
assert.Equal(t, StoreDir(), files.dir, "no directory configured falls back to the environment or the default")
|
||||
|
||||
assert.IsType(t, &FileStore{}, storeWithToken(Config{PKCS11: PKCS11Config{URI: "not-a-pkcs11-uri"}}), "an invalid URI must not hide the PEM directory")
|
||||
|
||||
for name, cfg := range map[string]PKCS11Config{
|
||||
"pin alone": {PIN: "1234"},
|
||||
"uri alone": {URI: "pkcs11:token=netbird?pin-value=1234"},
|
||||
} {
|
||||
store, ok := storeWithToken(cfg).(Stores)
|
||||
if assert.True(t, ok, "%s joins the token to the PEM directory", name) {
|
||||
assert.Len(t, store, 2, name)
|
||||
assert.IsType(t, &PKCS11Store{}, store[1], name)
|
||||
}
|
||||
store, ok := storeWithToken(Config{Dir: dir, PKCS11: cfg}).(Stores)
|
||||
require.True(t, ok, "%s joins the token to the PEM directory", name)
|
||||
require.Len(t, store, 2, name)
|
||||
token, ok := store[1].(*PKCS11Store)
|
||||
require.True(t, ok, name)
|
||||
assert.Equal(t, dir, token.certDir, "%s: the token pairs certificates from the same directory", name)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -672,7 +672,10 @@ func createEngineConfig(key wgtypes.Key, config *profilemanager.Config, peerConf
|
||||
|
||||
LazyConnection: lazyconn.ParseState(config.LazyConnection),
|
||||
|
||||
CertPKCS11: certproof.PKCS11Config{URI: config.CertPKCS11URI, PIN: config.CertPKCS11PIN},
|
||||
CertStore: certproof.Config{
|
||||
Dir: config.CertStoreDir,
|
||||
PKCS11: certproof.PKCS11Config{URI: config.CertPKCS11URI, PIN: config.CertPKCS11PIN},
|
||||
},
|
||||
|
||||
MTU: selectMTU(config.MTU, peerConfig.Mtu),
|
||||
LogPath: logPath,
|
||||
|
||||
@@ -171,7 +171,7 @@ type EngineConfig struct {
|
||||
|
||||
MTU uint16
|
||||
|
||||
CertPKCS11 certproof.PKCS11Config
|
||||
CertStore certproof.Config
|
||||
|
||||
// for debug bundle generation
|
||||
ProfileConfig *profilemanager.Config
|
||||
@@ -1298,7 +1298,7 @@ func (e *Engine) applyInfoFlags(info *system.Info) {
|
||||
// certificates reachable on this device, 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.CollectProofs(e.ctx, checks, peerKey[:], e.config.CertPKCS11)
|
||||
info.CertificateProofs = certproof.CollectProofs(e.ctx, checks, peerKey[:], e.config.CertStore)
|
||||
}
|
||||
|
||||
func (e *Engine) currentSystemInfo(ctx context.Context) *system.Info {
|
||||
|
||||
@@ -3,7 +3,6 @@
|
||||
package pkcs11
|
||||
|
||||
import (
|
||||
"encoding/binary"
|
||||
"errors"
|
||||
"fmt"
|
||||
"runtime"
|
||||
@@ -327,10 +326,3 @@ func first(attrs []attribute) *attribute {
|
||||
}
|
||||
return &attrs[0]
|
||||
}
|
||||
|
||||
// ULong encodes an integer attribute value the way the module reads a CK_ULONG.
|
||||
func ULong(v uint) []byte {
|
||||
buf := make([]byte, unsafe.Sizeof(ulong(0)))
|
||||
binary.NativeEndian.PutUint64(buf, uint64(v))
|
||||
return buf
|
||||
}
|
||||
|
||||
@@ -2,13 +2,6 @@
|
||||
|
||||
package pkcs11
|
||||
|
||||
import "encoding/binary"
|
||||
|
||||
func load(string) (driver, error) {
|
||||
return nil, ErrUnsupported
|
||||
}
|
||||
|
||||
// ULong encodes an integer attribute value; no module ever reads it in this build.
|
||||
func ULong(v uint) []byte {
|
||||
return binary.NativeEndian.AppendUint64(nil, uint64(v))
|
||||
}
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
package pkcs11
|
||||
|
||||
import (
|
||||
"encoding/binary"
|
||||
"errors"
|
||||
"fmt"
|
||||
"sync"
|
||||
@@ -23,8 +24,16 @@ const (
|
||||
AttrLabel = 0x3
|
||||
AttrValue = 0x11
|
||||
AttrCertificateType = 0x80
|
||||
AttrKeyType = 0x100
|
||||
AttrSubject = 0x101
|
||||
AttrID = 0x102
|
||||
AttrModulus = 0x120
|
||||
AttrPublicExponent = 0x122
|
||||
AttrECParams = 0x180
|
||||
AttrECPoint = 0x181
|
||||
|
||||
KeyRSA = 0x0
|
||||
KeyEC = 0x3
|
||||
|
||||
MechRSAPKCSPSS = 0xd
|
||||
MechSHA256 = 0x250
|
||||
@@ -233,3 +242,18 @@ type driver interface {
|
||||
sign(session uint, mech Mechanism, key Object, data []byte) ([]byte, error)
|
||||
createObject(session uint, template []Attribute) (Object, error)
|
||||
}
|
||||
|
||||
// ulongSize is the width of CK_ULONG on the 64-bit platforms the driver builds for.
|
||||
const ulongSize = 8
|
||||
|
||||
// ULong encodes an integer attribute value the way the module reads a CK_ULONG.
|
||||
func ULong(v uint) []byte {
|
||||
return binary.NativeEndian.AppendUint64(nil, uint64(v))
|
||||
}
|
||||
|
||||
func ulongValue(b []byte) (uint, error) {
|
||||
if len(b) != ulongSize {
|
||||
return 0, fmt.Errorf("CK_ULONG value has %d bytes", len(b))
|
||||
}
|
||||
return uint(binary.NativeEndian.Uint64(b)), nil
|
||||
}
|
||||
|
||||
@@ -0,0 +1,95 @@
|
||||
package pkcs11
|
||||
|
||||
import (
|
||||
"crypto"
|
||||
"crypto/ecdsa"
|
||||
"crypto/elliptic"
|
||||
"crypto/rsa"
|
||||
"encoding/asn1"
|
||||
"errors"
|
||||
"fmt"
|
||||
"math"
|
||||
"math/big"
|
||||
)
|
||||
|
||||
var curvesByOID = map[string]elliptic.Curve{
|
||||
"1.2.840.10045.3.1.7": elliptic.P256(),
|
||||
"1.3.132.0.34": elliptic.P384(),
|
||||
"1.3.132.0.35": elliptic.P521(),
|
||||
}
|
||||
|
||||
// PublicKey reads a CKO_PUBLIC_KEY object as a Go public key. RSA and EC keys are
|
||||
// supported, the two kinds a certificate posture proof can be signed with.
|
||||
func (s *Session) PublicKey(obj Object) (crypto.PublicKey, error) {
|
||||
raw, err := s.Attribute(obj, AttrKeyType)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
keyType, err := ulongValue(raw)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("CKA_KEY_TYPE: %w", err)
|
||||
}
|
||||
switch keyType {
|
||||
case KeyRSA:
|
||||
modulus, exponent, err := s.attributes(obj, AttrModulus, AttrPublicExponent)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return rsaPublicKey(modulus, exponent)
|
||||
case KeyEC:
|
||||
params, point, err := s.attributes(obj, AttrECParams, AttrECPoint)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return ecPublicKey(params, point)
|
||||
}
|
||||
return nil, fmt.Errorf("unsupported key type 0x%x", keyType)
|
||||
}
|
||||
|
||||
func (s *Session) attributes(obj Object, first, second uint) ([]byte, []byte, error) {
|
||||
a, err := s.Attribute(obj, first)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
b, err := s.Attribute(obj, second)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
return a, b, nil
|
||||
}
|
||||
|
||||
func rsaPublicKey(modulus, exponent []byte) (*rsa.PublicKey, error) {
|
||||
e := new(big.Int).SetBytes(exponent)
|
||||
if e.Sign() <= 0 || e.Cmp(big.NewInt(math.MaxInt32)) > 0 {
|
||||
return nil, errors.New("CKA_PUBLIC_EXPONENT is out of range")
|
||||
}
|
||||
return &rsa.PublicKey{N: new(big.Int).SetBytes(modulus), E: int(e.Int64())}, nil
|
||||
}
|
||||
|
||||
// ecPublicKey decodes CKA_EC_PARAMS, the named curve OID, and CKA_EC_POINT, the
|
||||
// uncompressed point wrapped in a DER OCTET STRING, which some modules hand out bare.
|
||||
func ecPublicKey(params, point []byte) (*ecdsa.PublicKey, error) {
|
||||
var oid asn1.ObjectIdentifier
|
||||
if _, err := asn1.Unmarshal(params, &oid); err != nil {
|
||||
return nil, fmt.Errorf("CKA_EC_PARAMS: %w", err)
|
||||
}
|
||||
curve, ok := curvesByOID[oid.String()]
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("unsupported curve %s", oid)
|
||||
}
|
||||
size := (curve.Params().BitSize + 7) / 8
|
||||
raw := point
|
||||
if len(raw) != 1+2*size {
|
||||
if _, err := asn1.Unmarshal(point, &raw); err != nil {
|
||||
return nil, fmt.Errorf("CKA_EC_POINT: %w", err)
|
||||
}
|
||||
}
|
||||
if len(raw) != 1+2*size || raw[0] != 4 {
|
||||
return nil, errors.New("CKA_EC_POINT is not an uncompressed point")
|
||||
}
|
||||
return &ecdsa.PublicKey{
|
||||
Curve: curve,
|
||||
X: new(big.Int).SetBytes(raw[1 : 1+size]),
|
||||
Y: new(big.Int).SetBytes(raw[1+size:]),
|
||||
}, nil
|
||||
}
|
||||
@@ -0,0 +1,87 @@
|
||||
package pkcs11
|
||||
|
||||
import (
|
||||
"crypto/ecdsa"
|
||||
"crypto/elliptic"
|
||||
"crypto/rand"
|
||||
"crypto/rsa"
|
||||
"encoding/asn1"
|
||||
"math/big"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestECPublicKey(t *testing.T) {
|
||||
curves := []struct {
|
||||
name string
|
||||
curve elliptic.Curve
|
||||
oid asn1.ObjectIdentifier
|
||||
}{
|
||||
{"P-256", elliptic.P256(), asn1.ObjectIdentifier{1, 2, 840, 10045, 3, 1, 7}},
|
||||
{"P-384", elliptic.P384(), asn1.ObjectIdentifier{1, 3, 132, 0, 34}},
|
||||
}
|
||||
for _, tt := range curves {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
key, err := ecdsa.GenerateKey(tt.curve, rand.Reader)
|
||||
require.NoError(t, err)
|
||||
params, err := asn1.Marshal(tt.oid)
|
||||
require.NoError(t, err)
|
||||
point := uncompressedPoint(key)
|
||||
wrapped, err := asn1.Marshal(point)
|
||||
require.NoError(t, err)
|
||||
|
||||
// PKCS#11 wraps the point in an OCTET STRING, but some modules return it bare.
|
||||
for form, encoded := range map[string][]byte{"DER octet string": wrapped, "bare point": point} {
|
||||
pub, err := ecPublicKey(params, encoded)
|
||||
require.NoError(t, err, form)
|
||||
assert.True(t, key.PublicKey.Equal(pub), "%s must decode to the generated key", form)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestECPublicKey_Rejections(t *testing.T) {
|
||||
key, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
|
||||
require.NoError(t, err)
|
||||
p256, err := asn1.Marshal(asn1.ObjectIdentifier{1, 2, 840, 10045, 3, 1, 7})
|
||||
require.NoError(t, err)
|
||||
brainpool, err := asn1.Marshal(asn1.ObjectIdentifier{1, 3, 36, 3, 3, 2, 8, 1, 1, 7})
|
||||
require.NoError(t, err)
|
||||
point := uncompressedPoint(key)
|
||||
|
||||
_, err = ecPublicKey(brainpool, point)
|
||||
assert.Error(t, err, "curves the proof cannot use must be rejected")
|
||||
_, err = ecPublicKey(p256, point[:len(point)-1])
|
||||
assert.Error(t, err, "a truncated point must be rejected")
|
||||
_, err = ecPublicKey([]byte("junk"), point)
|
||||
assert.Error(t, err, "malformed parameters must be rejected")
|
||||
}
|
||||
|
||||
func TestRSAPublicKey(t *testing.T) {
|
||||
key, err := rsa.GenerateKey(rand.Reader, 2048)
|
||||
require.NoError(t, err)
|
||||
|
||||
pub, err := rsaPublicKey(key.N.Bytes(), big.NewInt(int64(key.E)).Bytes())
|
||||
require.NoError(t, err)
|
||||
assert.True(t, key.PublicKey.Equal(pub), "modulus and exponent must decode to the generated key")
|
||||
|
||||
_, err = rsaPublicKey(key.N.Bytes(), nil)
|
||||
assert.Error(t, err, "a missing exponent must be rejected")
|
||||
}
|
||||
|
||||
func TestULongRoundTrip(t *testing.T) {
|
||||
v, err := ulongValue(ULong(ClassPrivateKey))
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, uint(ClassPrivateKey), v)
|
||||
|
||||
_, err = ulongValue([]byte{1, 2, 3})
|
||||
assert.Error(t, err, "a value of the wrong width must be rejected")
|
||||
}
|
||||
|
||||
func uncompressedPoint(key *ecdsa.PrivateKey) []byte {
|
||||
size := (key.Curve.Params().BitSize + 7) / 8
|
||||
point := append([]byte{4}, key.X.FillBytes(make([]byte, size))...)
|
||||
return append(point, key.Y.FillBytes(make([]byte, size))...)
|
||||
}
|
||||
@@ -186,6 +186,11 @@ type Config struct {
|
||||
|
||||
ClientCertKeyPair *tls.Certificate `json:"-"`
|
||||
|
||||
// CertStoreDir is the directory of PEM certificates, with their keys or with keys a
|
||||
// PKCS#11 token holds, that answer certificate posture checks on Linux. Empty means
|
||||
// NB_CERT_STORE_DIR or /etc/netbird/certs; see client/internal/certproof/README.md.
|
||||
CertStoreDir string
|
||||
|
||||
// CertPKCS11PIN is the user PIN of the PKCS#11 token, tpm2-pkcs11 for one, whose
|
||||
// certificates answer certificate posture checks on Linux. Setting it enables the
|
||||
// token store; see client/internal/certproof/README.md.
|
||||
|
||||
Reference in New Issue
Block a user