From 4524878ea45d7538d741b68d9be9c61e07b30c56 Mon Sep 17 00:00:00 2001 From: Samuel Leathers Date: Sun, 12 Jul 2026 20:49:02 -0400 Subject: [PATCH 1/2] Fix encryptedDerivePublic to return Either instead of throwing on hardened indices Replace both partial `error` branches in `encryptedDerivePublic` with structured failures: hardened indices now return `Left XPrvHardenedDerivationUnsupported` and FFI failures return `Left XPrvInternalError`. The return type changes from the pure `(PublicKey, ChainCode)` to `Either XPrvError (PublicKey, ChainCode)`, matching the style of the sibling `encryptedDerivePrivate` API and eliminating the crash vector when callers forward attacker-controlled derivation paths. --- .../src/Cardano/Crypto/WalletHD/Encrypted.hs | 25 ++++++++++--------- .../Cardano/Crypto/Wallet/RoundTripSpec.hs | 12 +++++---- 2 files changed, 20 insertions(+), 17 deletions(-) diff --git a/cardano-crypto-wallet/src/Cardano/Crypto/WalletHD/Encrypted.hs b/cardano-crypto-wallet/src/Cardano/Crypto/WalletHD/Encrypted.hs index 556a733d1..0968ed929 100644 --- a/cardano-crypto-wallet/src/Cardano/Crypto/WalletHD/Encrypted.hs +++ b/cardano-crypto-wallet/src/Cardano/Crypto/WalletHD/Encrypted.hs @@ -427,6 +427,7 @@ data XPrvError | XPrvInvalidChainCode | XPrvPublicKeyMismatch | XPrvInternalError + | XPrvHardenedDerivationUnsupported deriving (Eq, Show) newtype EncryptedKey = EncryptedKey ByteString @@ -673,16 +674,15 @@ encryptedDerivePublic :: DerivationScheme -> (PublicKey, ChainCode) -> DerivationIndex -> - (PublicKey, ChainCode) + Either XPrvError (PublicKey, ChainCode) encryptedDerivePublic dscheme (publicKey, cc) childIndex - | childIndex >= 0x80000000 = - error "encryptedDerivePublic: cannot derive hardened key from public key" - | otherwise = unsafePerformIO $ do - fmap (first PublicKey) $ psbCreateResult $ \publicKeyPtrOut -> - fmap ChainCode $ psbCreate $ \ccOutPtr -> - withPublicKeyPtr publicKey $ \publicKeyPtr -> - withChainCodePtr cc $ \chainCodePtr -> do - r <- + | childIndex >= 0x80000000 = Left XPrvHardenedDerivationUnsupported + | otherwise = unsafePerformIO $ + withPublicKeyPtr publicKey $ \publicKeyPtr -> + withChainCodePtr cc $ \chainCodePtr -> do + (pubKeyBytes, (ccBytes, r)) <- + psbCreateResult $ \publicKeyPtrOut -> + psbCreateResult $ \ccOutPtr -> wallet_derive_public publicKeyPtr chainCodePtr @@ -690,9 +690,10 @@ encryptedDerivePublic dscheme (publicKey, cc) childIndex (PublicKeyPtr publicKeyPtrOut) (ChainCodePtr ccOutPtr) (dschemeToC dscheme) - if r /= 0 - then error "encryptedDerivePublic: hardened index check failed" - else pure () + pure $ + if r /= 0 + then Left XPrvInternalError + else Right (PublicKey pubKeyBytes, ChainCode ccBytes) encryptedPublic :: HasCallStack => EncryptedKey -> PublicKey encryptedPublic eKey@(EncryptedKey eKeyBytes) = diff --git a/cardano-crypto-wallet/test/Test/Cardano/Crypto/Wallet/RoundTripSpec.hs b/cardano-crypto-wallet/test/Test/Cardano/Crypto/Wallet/RoundTripSpec.hs index 6e4cdf93a..82c2d03d5 100644 --- a/cardano-crypto-wallet/test/Test/Cardano/Crypto/Wallet/RoundTripSpec.hs +++ b/cardano-crypto-wallet/test/Test/Cardano/Crypto/Wallet/RoundTripSpec.hs @@ -82,8 +82,9 @@ tests = describe "RoundTrip" $ do case res' of Left err -> expectationFailure $ "derivePrivate failed: " ++ show err Right child -> - let (derivedPub, _) = encryptedDerivePublic DerivationScheme2 (pub, cc) idx - in encryptedPublic child `shouldBe` derivedPub + case encryptedDerivePublic DerivationScheme2 (pub, cc) idx of + Left err -> expectationFailure $ "derivePublic failed: " ++ show err + Right (derivedPub, _) -> encryptedPublic child `shouldBe` derivedPub it "encryptedDerivePublic is consistent for both schemes" $ do res <- encryptedCreate testSeed testPass testCC @@ -92,9 +93,10 @@ tests = describe "RoundTrip" $ do Right key -> do let pub = encryptedPublic key cc = encryptedChainCode key - (pub1, _) = encryptedDerivePublic DerivationScheme1 (pub, cc) 0 - (pub2, _) = encryptedDerivePublic DerivationScheme2 (pub, cc) 0 - pub1 `shouldNotBe` pub2 + case (encryptedDerivePublic DerivationScheme1 (pub, cc) 0, encryptedDerivePublic DerivationScheme2 (pub, cc) 0) of + (Right (pub1, _), Right (pub2, _)) -> pub1 `shouldNotBe` pub2 + (Left err, _) -> expectationFailure $ "derivePublic scheme1 failed: " ++ show err + (_, Left err) -> expectationFailure $ "derivePublic scheme2 failed: " ++ show err prop "mkEncryptedKey . unEncryptedKey is identity" $ \(key :: EncryptedKey) -> From c767a53cb7f86ed0a8b26b7814e32ad3e46a553e Mon Sep 17 00:00:00 2001 From: Samuel Leathers Date: Mon, 13 Jul 2026 09:48:25 -0400 Subject: [PATCH 2/2] Bump cardano-crypto-wallet to 0.2.0.0; fix fourmolu formatting Breaking API change from previous commit: version bump, changelog entry, and fourmolu formatting fix in RoundTripSpec. --- cardano-crypto-wallet/CHANGELOG.md | 6 ++++-- cardano-crypto-wallet/cardano-crypto-wallet.cabal | 2 +- .../test/Test/Cardano/Crypto/Wallet/RoundTripSpec.hs | 4 +++- 3 files changed, 8 insertions(+), 4 deletions(-) diff --git a/cardano-crypto-wallet/CHANGELOG.md b/cardano-crypto-wallet/CHANGELOG.md index 5cb314893..89e8ea26e 100644 --- a/cardano-crypto-wallet/CHANGELOG.md +++ b/cardano-crypto-wallet/CHANGELOG.md @@ -1,8 +1,10 @@ # Changelog for cardano-crypto-wallet -## 0.1.0.1 +## 0.2.0.0 -* +* Breaking: `encryptedDerivePublic` now returns `Either XPrvError (PublicKey, ChainCode)` + instead of throwing on hardened indices. +* Breaking: `XPrvHardenedDerivationUnsupported` constructor added to `XPrvError`. ## 0.1.0.0 diff --git a/cardano-crypto-wallet/cardano-crypto-wallet.cabal b/cardano-crypto-wallet/cardano-crypto-wallet.cabal index 3d7f8dab4..8e00cd372 100644 --- a/cardano-crypto-wallet/cardano-crypto-wallet.cabal +++ b/cardano-crypto-wallet/cardano-crypto-wallet.cabal @@ -1,6 +1,6 @@ cabal-version: 3.0 name: cardano-crypto-wallet -version: 0.1.0.0 +version: 0.2.0.0 synopsis: Authenticated v2 envelope for HD wallet keys description: Provides authenticated encryption and key derivation for HD wallet keys, diff --git a/cardano-crypto-wallet/test/Test/Cardano/Crypto/Wallet/RoundTripSpec.hs b/cardano-crypto-wallet/test/Test/Cardano/Crypto/Wallet/RoundTripSpec.hs index 82c2d03d5..db1ea868e 100644 --- a/cardano-crypto-wallet/test/Test/Cardano/Crypto/Wallet/RoundTripSpec.hs +++ b/cardano-crypto-wallet/test/Test/Cardano/Crypto/Wallet/RoundTripSpec.hs @@ -93,7 +93,9 @@ tests = describe "RoundTrip" $ do Right key -> do let pub = encryptedPublic key cc = encryptedChainCode key - case (encryptedDerivePublic DerivationScheme1 (pub, cc) 0, encryptedDerivePublic DerivationScheme2 (pub, cc) 0) of + case ( encryptedDerivePublic DerivationScheme1 (pub, cc) 0 + , encryptedDerivePublic DerivationScheme2 (pub, cc) 0 + ) of (Right (pub1, _), Right (pub2, _)) -> pub1 `shouldNotBe` pub2 (Left err, _) -> expectationFailure $ "derivePublic scheme1 failed: " ++ show err (_, Left err) -> expectationFailure $ "derivePublic scheme2 failed: " ++ show err