Skip to content

Commit ce8a9fd

Browse files
Merge #1588
1588: Support public key derivation in CLI r=Anviking a=Anviking ## Issue Number ADP-190, #1594 ## Overview <!-- Detail in a few bullet points the work accomplished in this PR --> - [x] Add new `XPrvOrXPub` type - [x] Add hex coders for `XPrvOrXPub` - [x] Test `derive . toPublic == toPublic . derive (for soft indices)` - [x] Implement `inspect` and `toPublic` using the `CliKeyScheme` instead of separate functions - [x] `derive` and `inspect` now accepts public keys as well - [x] Extend roundtrip tests to test `inspect` and `toPublic` - [ ] <s>Remove some boilerplate</s> - [ ] <s>Consider parameterising by `prv pub any` instead of just `key` for slightly more helpful error messages.</s> - [ ] <s>Maybe `Data.Codec` could be used?</s> ## Comments <!-- Additional comments or screenshots to attach if any --> <!-- Don't forget to: ✓ Self-review your changes to make sure nothing unexpected slipped through ✓ Assign yourself to the PR ✓ Assign one or several reviewer(s) ✓ Once created, link this PR to its corresponding ticket ✓ Assign the PR to a corresponding milestone ✓ Acknowledge any changes required to the Wiki --> Co-authored-by: Johannes Lund <johannes.lund@iohk.io>
2 parents 8303c90 + f2d933b commit ce8a9fd

File tree

2 files changed

+232
-59
lines changed

2 files changed

+232
-59
lines changed

lib/cli/src/Cardano/CLI.hs

Lines changed: 104 additions & 46 deletions
Original file line numberDiff line numberDiff line change
@@ -73,11 +73,13 @@ module Cardano.CLI
7373
, CliKeyScheme (..)
7474
, DerivationIndex (..)
7575
, DerivationPath (..)
76+
, XPrvOrXPub (..)
7677

7778
, newCliKeyScheme
78-
, xPrvToTextTransform
79+
, keyHexCodec
7980
, hoistKeyScheme
8081
, mapKey
82+
, firstHardenedIndex
8183

8284

8385
-- * Logging
@@ -167,6 +169,7 @@ import Cardano.Wallet.Primitive.AddressDerivation
167169
, SomeMnemonic (..)
168170
, WalletKey (..)
169171
, XPrv
172+
, XPub
170173
, deriveRewardAccount
171174
, hex
172175
, unXPrv
@@ -197,8 +200,6 @@ import Data.Bifunctor
197200
( bimap )
198201
import Data.ByteArray.Encoding
199202
( Base (Base16), convertFromBase )
200-
import Data.ByteString
201-
( ByteString )
202203
import Data.Char
203204
( toLower )
204205
import Data.List.Extra
@@ -390,8 +391,12 @@ data CliKeyScheme key m = CliKeyScheme
390391
{ allowedWordLengths :: [Int]
391392
, mnemonicToRootKey :: [Text] -> m key
392393
, deriveChildKey :: key -> DerivationIndex -> m key
394+
, toPublic :: key -> m key
395+
, inspect :: key -> m Text
393396
}
394397

398+
data XPrvOrXPub = AXPrv XPrv | AXPub XPub
399+
395400
-- | Change the underlying monad of a @CliKeyScheme@.
396401
hoistKeyScheme
397402
:: (forall a. m1 a -> m2 a)
@@ -401,27 +406,38 @@ hoistKeyScheme eta s = CliKeyScheme
401406
{ allowedWordLengths = allowedWordLengths s
402407
, mnemonicToRootKey = eta . mnemonicToRootKey s
403408
, deriveChildKey = \k i -> eta $ deriveChildKey s k i
409+
, toPublic = eta . toPublic s
410+
, inspect = eta . inspect s
404411
}
405412

406413
-- | Pair of functions representing the bidirectional transformation between
407-
-- a @XPrv@ and its 96-byte hex-encoded form.
408-
xPrvToTextTransform :: (XPrv -> Either String Text, Text -> Either String XPrv)
409-
xPrvToTextTransform = (xPrvToHexText, hexTextToXPrv)
414+
-- a @XPrvOrXPub@ and its hex-encoded form.
415+
keyHexCodec :: (XPrvOrXPub -> Either String Text, Text -> Either String XPrvOrXPub)
416+
keyHexCodec = (encode, decode)
410417
where
411-
xPrvToHexText :: XPrv -> Either String Text
412-
xPrvToHexText =
418+
encode :: XPrvOrXPub -> Either String Text
419+
encode (AXPub xpub) = return . T.pack . B8.unpack . hex . CC.unXPub $ xpub
420+
encode (AXPrv xprv) =
413421
fmap (T.pack . B8.unpack . hex)
414422
. left showErr
415-
. unXPrvStripPubCheckRoundtrip
423+
. unXPrvStripPubCheckRoundtrip $ xprv
416424
where
417425
-- NOTE: This error should never happen from using the CLI.
418426
showErr ErrCannotRoundtripToSameXPrv =
419427
"Internal error: Failed to safely encode an extended private key"
420428

421-
hexTextToXPrv :: Text -> Either String XPrv
422-
hexTextToXPrv txt = do
429+
decode :: Text -> Either String XPrvOrXPub
430+
decode txt = do
423431
bytes <- fromHex $ B8.pack $ T.unpack . T.strip $ txt
424-
left showErr $ xPrvFromStrippedPubXPrvCheckRoundtrip bytes
432+
case BS.length bytes of
433+
96 -> fmap AXPrv $ left showErr $ xPrvFromStrippedPubXPrvCheckRoundtrip bytes
434+
64 -> fmap AXPub . CC.xpub $ bytes
435+
n -> Left . mconcat $
436+
[ "Expected key to be 96 bytes in the case of a private key"
437+
, " and, 64 bytes for public keys. This key is "
438+
, show n
439+
, " bytes."
440+
]
425441
where
426442
showErr (ErrInputLengthMismatch expected actual) = mconcat
427443
[ "Expected extended private key to be "
@@ -452,6 +468,8 @@ mapKey (f, g) s = CliKeyScheme
452468
, deriveChildKey = \k i -> do
453469
k' <- g k
454470
(deriveChildKey s k' i) >>= f
471+
, toPublic = g >=> toPublic s >=> f
472+
, inspect = g >=> inspect s
455473
}
456474

457475
eitherToIO :: Either String a -> IO a
@@ -469,7 +487,7 @@ instance ToText DerivationPath where
469487

470488
newtype DerivationIndex = DerivationIndex { unDerivationIndex :: Word32 }
471489
deriving (Show, Eq)
472-
deriving newtype (Bounded, Enum)
490+
deriving newtype (Bounded, Enum, Ord)
473491

474492
firstHardenedIndex :: Word32
475493
firstHardenedIndex = getIndex $ minBound @(Index 'Hardened 'AddressK)
@@ -532,7 +550,7 @@ instance ToText DerivationIndex where
532550
then show (i - firstHardenedIndex) ++ "H"
533551
else show i
534552

535-
newCliKeyScheme :: ByronWalletStyle -> CliKeyScheme XPrv (Either String)
553+
newCliKeyScheme :: ByronWalletStyle -> CliKeyScheme XPrvOrXPub (Either String)
536554
newCliKeyScheme = \case
537555
Random ->
538556
-- NOTE
@@ -546,24 +564,30 @@ newCliKeyScheme = \case
546564
in
547565
CliKeyScheme
548566
(apiAllowedLengths proxy)
549-
(fmap icarusKeyFromSeed . seedFromMnemonic proxy)
567+
(fmap (AXPrv . icarusKeyFromSeed) . seedFromMnemonic proxy)
550568
(derive CC.DerivationScheme2)
569+
(toPub)
570+
insp
551571
Trezor ->
552572
let
553573
proxy = Proxy @'API.Trezor
554574
in
555575
CliKeyScheme
556576
(apiAllowedLengths proxy)
557-
(fmap icarusKeyFromSeed . seedFromMnemonic proxy)
577+
(fmap (AXPrv . icarusKeyFromSeed) . seedFromMnemonic proxy)
558578
(derive CC.DerivationScheme2)
579+
(toPub)
580+
insp
559581
Ledger ->
560582
let
561583
proxy = Proxy @'API.Ledger
562584
in
563585
CliKeyScheme
564586
(apiAllowedLengths proxy)
565-
(fmap ledgerKeyFromSeed . seedFromMnemonic proxy)
587+
(fmap (AXPrv . ledgerKeyFromSeed) . seedFromMnemonic proxy)
566588
(derive CC.DerivationScheme2)
589+
(toPub)
590+
insp
567591
where
568592
seedFromMnemonic
569593
:: forall (s :: ByronWalletStyle).
@@ -588,18 +612,58 @@ newCliKeyScheme = \case
588612
ledgerKeyFromSeed = Icarus.getKey
589613
. flip Icarus.generateKeyFromHardwareLedger pass
590614

615+
toPub (AXPrv xprv) = return . AXPub . CC.toXPub $ xprv
616+
toPub (AXPub _) = Left "Input is already a public key."
617+
591618
derive
592619
:: CC.DerivationScheme
593-
-> XPrv
620+
-> XPrvOrXPub
594621
-> DerivationIndex
595-
-> Either String XPrv
596-
derive scheme1Or2 k i =
597-
return
622+
-> Either String XPrvOrXPub
623+
derive scheme1Or2 (AXPrv k) i =
624+
return . AXPrv
598625
$ CC.deriveXPrv
599626
scheme1Or2
600627
pass
601628
k
602629
(unDerivationIndex i)
630+
derive scheme1Or2 (AXPub k) i =
631+
maybe (Left err) (return . AXPub)
632+
$ CC.deriveXPub
633+
scheme1Or2
634+
k
635+
(unDerivationIndex i)
636+
where
637+
err = mconcat
638+
[ T.unpack (toText i)
639+
, " is a hardened index. Public key derivation is only possible for"
640+
, " soft indices. \nIf the index is correct, please use the"
641+
, " corresponding private key as input."
642+
]
643+
644+
insp (AXPrv key) = do
645+
let bytes = CC.unXPrv key
646+
let (xprv, rest) = BS.splitAt 64 bytes
647+
let (_pub, cc) = BS.splitAt 32 rest
648+
let encodeToHex = T.pack . B8.unpack . hex
649+
return $ mconcat
650+
[ "extended private key: "
651+
, encodeToHex xprv
652+
, "\n"
653+
, "chain code: "
654+
, encodeToHex cc
655+
]
656+
insp (AXPub key) = do
657+
let bytes = CC.unXPub key
658+
let (xpub, cc) = BS.splitAt 32 bytes
659+
let encodeToHex = T.pack . B8.unpack . hex
660+
return $ mconcat
661+
[ "extended public key: "
662+
, encodeToHex xpub
663+
, "\n"
664+
, "chain code: "
665+
, encodeToHex cc
666+
]
603667

604668
-- We don't use passwords to encrypt the keys here.
605669
pass = mempty
@@ -624,7 +688,7 @@ cmdKeyRoot =
624688
scheme :: CliKeyScheme Text IO
625689
scheme =
626690
hoistKeyScheme eitherToIO
627-
. mapKey xPrvToTextTransform
691+
. mapKey keyHexCodec
628692
$ newCliKeyScheme keyType
629693

630694
data KeyChildArgs = KeyChildArgs
@@ -653,7 +717,7 @@ cmdKeyChild =
653717
scheme :: CliKeyScheme Text IO
654718
scheme =
655719
hoistKeyScheme eitherToIO
656-
. mapKey xPrvToTextTransform
720+
. mapKey keyHexCodec
657721
$ newCliKeyScheme Icarus
658722

659723
newtype KeyPublicArgs = KeyPublicArgs
@@ -668,11 +732,16 @@ cmdKeyPublic =
668732
cmd = fmap exec $
669733
KeyPublicArgs <$> keyArgument
670734

671-
exec (KeyPublicArgs hexPrv) = do
672-
let (_, decodePrv) = xPrvToTextTransform
673-
let encodePub = T.pack . B8.unpack . hex . CC.unXPub
674-
pubHex <- eitherToIO $ encodePub . CC.toXPub <$> decodePrv hexPrv
675-
TIO.putStrLn pubHex
735+
exec (KeyPublicArgs key) = do
736+
pub <- toPublic scheme key
737+
TIO.putStrLn pub
738+
739+
scheme :: CliKeyScheme Text IO
740+
scheme =
741+
hoistKeyScheme eitherToIO
742+
. mapKey keyHexCodec
743+
$ newCliKeyScheme Icarus
744+
676745

677746
newtype KeyInspectArgs = KeyInspectArgs
678747
{ _key :: Text
@@ -686,25 +755,14 @@ cmdKeyInspect =
686755
cmd = fmap exec $
687756
KeyInspectArgs <$> keyArgument
688757

689-
exec (KeyInspectArgs hexKey) = do
690-
let (_, hex2key) = xPrvToTextTransform
691-
res <- eitherToIO $ do
692-
-- Extract extended private key and chain code from a @XPrv@
693-
keyBytes <- CC.unXPrv <$> hex2key hexKey
694-
let (xprv, rest) = BS.splitAt 64 keyBytes
695-
let (_pub, cc) = BS.splitAt 32 rest
696-
return $ mconcat
697-
[ "extended private key: "
698-
, toHex xprv
699-
, "\n"
700-
, "chain code: "
701-
, toHex cc
702-
]
703-
TIO.putStrLn res
758+
exec (KeyInspectArgs key) =
759+
inspect scheme key >>= TIO.putStrLn
704760

705-
where
706-
toHex :: ByteString -> Text
707-
toHex = T.pack . B8.unpack . hex
761+
scheme :: CliKeyScheme Text IO
762+
scheme =
763+
hoistKeyScheme eitherToIO
764+
. mapKey keyHexCodec
765+
$ newCliKeyScheme Icarus
708766

709767
{-------------------------------------------------------------------------------
710768
Commands - 'mnemonic'

0 commit comments

Comments
 (0)