{"record":{"id":"e4f1aeb3cf8b2edf","repo":"tinyhumansai/openhuman","slug":"ed25519-derivation-path-must-be-fully-hardened","errorCode":null,"errorMessage":"Ed25519 derivation path must be fully hardened: ${derivationPath}","messagePattern":"Ed25519 derivation path must be fully hardened: (.+?)","errorType":"exception","errorClass":"Error","httpStatus":null,"severity":"error","filePath":"app/src/utils/cryptoKeys.ts","lineNumber":152,"sourceCode":"\nfunction deriveSecp256k1PrivateKey(mnemonic: string, derivationPath: string): Uint8Array {\n  const seed = mnemonicToSeedSync(mnemonic);\n  const hdkey = HDKey.fromMasterSeed(seed);\n  const derived = hdkey.derive(derivationPath);\n  if (!derived.privateKey) {\n    throw new Error(`Failed to derive private key for path ${derivationPath}`);\n  }\n  return derived.privateKey;\n}\n\nfunction deriveSlip10Ed25519PrivateKey(seed: Uint8Array, derivationPath: string): Uint8Array {\n  let key = hmac(sha512, new TextEncoder().encode('ed25519 seed'), seed);\n  let privateKey = key.slice(0, 32);\n  let chainCode = key.slice(32);\n\n  for (const segment of derivationPath.split('/').slice(1)) {\n    if (!segment.endsWith(\"'\")) {\n      throw new Error(`Ed25519 derivation path must be fully hardened: ${derivationPath}`);\n    }\n    const index = Number.parseInt(segment.slice(0, -1), 10);\n    const hardened = (index + 0x80000000) >>> 0;\n    const data = new Uint8Array(37);\n    data[0] = 0;\n    data.set(privateKey, 1);\n    data[33] = (hardened >>> 24) & 0xff;\n    data[34] = (hardened >>> 16) & 0xff;\n    data[35] = (hardened >>> 8) & 0xff;\n    data[36] = hardened & 0xff;\n    key = hmac(sha512, chainCode, data);\n    privateKey = key.slice(0, 32);\n    chainCode = key.slice(32);\n  }\n\n  return privateKey;\n}\n","sourceCodeStart":134,"sourceCodeEnd":170,"githubUrl":"https://github.com/tinyhumansai/openhuman/blob/a221052e0df5b1f7598fceba7329fd1af95d6699/app/src/utils/cryptoKeys.ts#L134-L170","documentation":"SLIP-0010 ed25519 derivation (used for Solana-style keys) only supports hardened segments: ed25519 with non-hardened derivation cannot compute child public keys from public data, so every index must carry the `'` suffix. The loop rejects the first non-hardened segment it encounters.","triggerScenarios":"Calling the ed25519 helper with a secp256k1-style path such as m/44'/501'/0'/0 — the final segment lacks `'` and trips the check on the first iteration over it.","commonSituations":"Adding an Ed25519 chain and reusing the EVM path constant; user-configurable paths copied from BIP-44 docs; confusion between BIP-32 (mixed hardening allowed) and SLIP-0010 ed25519 (hardened only).","solutions":["Harden every segment: m/44'/501'/0'/0'","Normalize and validate paths on save: every segment must end with `'`","Add a test vector for the canonical Solana path"],"exampleFix":"// before\nconst path = \"m/44'/501'/0'/0\";\n\n// after\nconst path = \"m/44'/501'/0'/0'\";","handlingStrategy":"validation","validationCode":"function isHardenedEd25519Path(path: string): boolean {\n  return /^m(\\/\\d+')+$/.test(path);\n}\n// use before calling the ed25519 derivation helper","typeGuard":"function isFullyHardenedPath(p: string): boolean {\n  return p.split('/').slice(1).every((seg) => seg.endsWith(\"'\"));\n}","tryCatchPattern":null,"preventionTips":["Never reuse secp256k1/BIP-44 path constants for ed25519 chains","Validate that every segment ends with `'` when the path is configurable","Keep a canonical Solana vector (m/44'/501'/0'/0') in tests"],"tags":["crypto","ed25519","slip-10","derivation","solana"],"backgroundTag":null,"analyzedSha":"a221052e0df5b1f7598fceba7329fd1af95d6699","analyzedAt":"2026-08-16T12:47:06.542Z","schemaVersion":2},"datasetVersion":"2026-08-16T13:17:31.715Z"}