slackhq/nebula · error

got an empty secret key

Error message

got an empty secret key

What it means

DeriveNoise performs an ECDH derive via the HSM and requires at least NoiseKeySize bytes of derived key material. If the PKCS#11 C_Derive call returns a nil or zero-length secret key, this error is returned instead of silently producing a useless Noise key.

Source

Thrown at pkclient/pkclient_cgo.go:196

		pkcs11.NewAttribute(pkcs11.CKA_EXTRACTABLE, true),
		pkcs11.NewAttribute(pkcs11.CKA_ENCRYPT, true),
		pkcs11.NewAttribute(pkcs11.CKA_DECRYPT, true),
		pkcs11.NewAttribute(pkcs11.CKA_WRAP, true),
		pkcs11.NewAttribute(pkcs11.CKA_UNWRAP, true),
		pkcs11.NewAttribute(pkcs11.CKA_VALUE_LEN, NoiseKeySize),
	}

	// Set up the parameters which include the peer's public key
	ecdhParams := pkcs11.NewECDH1DeriveParams(pkcs11.CKD_NULL, nil, peerPubKey)
	mech := pkcs11.NewMechanism(pkcs11.CKM_ECDH1_DERIVE, ecdhParams)
	sk := p11.PrivateKey(c.privKeyObj)

	tmpKey, err := sk.Derive(*mech, attrTemplate)
	if err != nil {
		return nil, err
	}
	if tmpKey == nil || len(tmpKey) == 0 {
		return nil, fmt.Errorf("got an empty secret key")
	}
	secret := make([]byte, NoiseKeySize)
	copy(secret[:], tmpKey[:NoiseKeySize])
	return secret, nil
}

func (c *PKClient) GetPubKey() ([]byte, error) {
	d, err := c.privKeyObj.Attribute(pkcs11.CKA_PUBLIC_KEY_INFO)
	if err != nil {
		return nil, err
	}
	if d != nil && len(d) > 0 {
		return formatPubkeyFromPublicKeyInfoAttr(d)
	}
	c.pubKeyObj, err = c.findDeriveKey(c.id, c.label, false)
	if err != nil {
		return nil, fmt.Errorf("pkcs11 module gave us a nil CKA_PUBLIC_KEY_INFO, and looking up the public key also failed: %w", err)
	}

View on GitHub (pinned to dd8f660c0a)

Solutions

  1. Validate the peer public key (65-byte uncompressed EC point) before calling DeriveNoise.
  2. Retry the derive once; transient token states can produce empty outputs.
  3. Update or replace the HSM vendor PKCS#11 module if empty derives reproduce consistently.
  4. Run pkclient.Test() against the token to verify the derive path works end-to-end.
Defensive patterns

Strategy: retry

Validate before calling

// Validate peer public key length before DeriveNoise
func validPeerKey(pub []byte) bool { return len(pub) == 65 && pub[0] == 0x04 }

Type guard

func isUsableSecret(k []byte) bool { return len(k) >= NoiseKeySize }

Try / catch

secret, err := client.DeriveNoise(peerPub)
if err != nil {
  if strings.Contains(err.Error(), "got an empty secret key") {
    // transient token state: one retry after relogin
    client.Close()
    if c2, e := pkclient.New(hsmPath, slot, pin, id, label); e == nil { client = c2; secret, err = client.DeriveNoise(peerPub) }
  }
  if err != nil { return err }
}

Prevention

When it happens

Trigger: Calling DeriveNoise (directly or via Test) when the HSM's derive operation succeeds but returns an empty/nil key object — e.g. a mechanism the token claims to support but produces no output, or a malformed peer public key attribute.

Common situations: HSM firmware bug or misbehaving PKCS#11 module returning success with no key; peer public key bytes corrupted before the call; token in a bad state after login/logout races.

Related errors


AI-assisted analysis of slackhq/nebula@dd8f660c0a (2026-09-03). Data as JSON: /api/errors/d600c4d7e7a7ab00. Report an issue: GitHub.