OpenNHP/opennhp · critical

private key parse error

Error message

private key parse error %v

What it means

UdpAC.Start fails with "private key parse error %v" when the AC's config.PrivateKeyBase64 cannot be decoded with base64.StdEncoding. The decoded bytes must be a valid Curve25519/SM2 private key for core.NewDevice; a decode failure aborts startup before any device is created.

Solutions

  1. Regenerate the key with `./nhp-acd keygen --curve --json` and paste only the base64 body (no quotes, no whitespace)
  2. Strip whitespace/newlines before decoding: base64.StdEncoding.DecodeString(strings.TrimSpace(...)) or wrap in strings.Join(strings.Fields(k), "")
  3. Confirm the key matches the configured cipher scheme (curve vs SM2)
  4. Validate the config at load time with a base64 decode check so the failure surfaces with a clear field name

Example fix

// before
prk, err := base64.StdEncoding.DecodeString(a.config.PrivateKeyBase64)
if err != nil {
	return fmt.Errorf("private key parse error %v", err)
}
// after
cleaned := strings.Join(strings.Fields(a.config.PrivateKeyBase64), "")
prk, err := base64.StdEncoding.DecodeString(cleaned)
if err != nil {
	return fmt.Errorf("private key parse error (invalid base64 in config.toml PrivateKeyBase64): %w", err)
}
Defensive patterns

Strategy: validation

Validate before calling

k := strings.TrimSpace(conf.PrivateKeyBase64)
if k == "" { return fmt.Errorf("privateKeyBase64 is empty") }
if _, err := base64.StdEncoding.DecodeString(k); err != nil {
	return fmt.Errorf("privateKeyBase64 is not valid std base64: %w", err)
}

Try / catch

prk, err := base64.StdEncoding.DecodeString(a.config.PrivateKeyBase64)
if err != nil {
	return fmt.Errorf("invalid privateKeyBase64 in config.toml: %w", err)
}

Prevention

When it happens

Trigger: PrivateKeyBase64 in config.toml contains whitespace/newlines, uses URL-safe base64 or raw hex, includes the '-----BEGIN...' PEM wrapper, or is empty/placeholder text generated outside `keygen --curve/--sm2 --json`.

Common situations: Hand-editing config.toml and pasting a key with quotes/line breaks; copying an ed25519 or hex key from another tool; mixing curve and SM2 key formats; shell truncating the key during provisioning.

Understand the failure class

Background: "Invalid ... format", "must be in format X", "does not look like a ..." — invalid argument format errors across CLI tools and libraries — this error's family across 17 libraries.

Related errors


AI-assisted analysis of OpenNHP/opennhp@6e04ca5ff0 (2026-09-07). Data as JSON: /api/errors/22b64baa9b517f13. Report an issue: GitHub.

Appendix: source

Thrown at endpoints/ac/udpac.go:136

		a.ipset, err = utils.NewIPSet(false)
		if err != nil {
			log.Error("ipset command not found")
			return
		}
	case FilterMode_EBPFXDP:
		err = ebpflocal.EbpfEngineLoad(dirPath, logLevel, a.config.ACId)
		if err != nil {
			return err
		}
	default:
		log.Error("[HandleAccessControl] unsupported FilterMode: %d (expected 0=IPTABLES or 1=EBPFXDP)", a.config.FilterMode)
		return
	}

	prk, err := base64.StdEncoding.DecodeString(a.config.PrivateKeyBase64)
	if err != nil {
		log.Error("private key parse error %v\n", err)
		return fmt.Errorf("private key parse error %v", err)
	}

	a.device = core.NewDevice(core.NHP_AC, prk, nil)
	if a.device == nil {
		log.Critical("failed to create device %v\n", err)
		return fmt.Errorf("failed to create device %v", err)
	}

	a.remoteConnectionMap = make(map[string]*UdpConn)
	a.serverPeerMap = make(map[string]*core.UdpPeer)
	a.tokenStore = common.NewTokenStore[*AccessEntry]()

	if a.etcdConn != nil {
		_ = a.loadRemoteConfig()
	} else {
		// load http config and turn on http server if needed
		_ = a.loadHttpConfig()

View on GitHub (pinned to 6e04ca5ff0)