Skip to content
This repository has been archived by the owner on Aug 18, 2020. It is now read-only.

Commit

Permalink
Merge pull request #3461 from input-output-hk/Squad1/CO-325/api-v1-im…
Browse files Browse the repository at this point in the history
…provements

[CO-325] API V1 Improvements - Part 2
  • Loading branch information
KtorZ authored Aug 24, 2018
2 parents 85ebea0 + 8c2b82b commit ea2a5b6
Show file tree
Hide file tree
Showing 44 changed files with 999 additions and 239 deletions.
5 changes: 4 additions & 1 deletion core/src/Pos/Core/Util/LogSafe.hs
Original file line number Diff line number Diff line change
Expand Up @@ -334,8 +334,11 @@ logErrorSP = logMessageSP Error
instance BuildableSafe a => Buildable (SecureLog [a]) where
build = bprint (buildSafeList secure) . getSecureLog

instance Buildable (SecureLog Bool) where
build _ = "<bool>"

instance Buildable (SecureLog Text) where
build _ = "<hidden>"
build _ = "<text>"

instance Buildable (SecureLog PassPhrase) where
build _ = "<passphrase>"
Expand Down
27 changes: 14 additions & 13 deletions core/test/Test/Pos/Core/ExampleHelpers.hs
Original file line number Diff line number Diff line change
Expand Up @@ -81,7 +81,6 @@ module Test.Pos.Core.ExampleHelpers
import Universum

import qualified Crypto.SCRAPE as Scrape
import qualified Crypto.Sign.Ed25519 as Ed25519
import Data.Coerce (coerce)
import Data.Fixed (Fixed (..))
import qualified Data.HashMap.Strict as HM
Expand Down Expand Up @@ -146,7 +145,7 @@ import Pos.Crypto (AbstractHash (..), EncShare (..),
redeemDeterministicKeyGen, redeemSign, safeCreatePsk,
sign, toVssPublicKey)
import Pos.Crypto.Signing (ProxyCert (..), ProxySecretKey (..),
PublicKey (..), RedeemPublicKey (..))
PublicKey (..))

import Test.Pos.Core.Gen (genProtocolConstants)
import Test.Pos.Crypto.Bi (getBytes)
Expand Down Expand Up @@ -630,17 +629,19 @@ exampleGenesisConfiguration_GCSpec =

exampleGenesisAvvmBalances :: GenesisAvvmBalances
exampleGenesisAvvmBalances =
GenesisAvvmBalances {getGenesisAvvmBalances =
(HM.fromList [(RedeemPublicKey (Ed25519.PublicKey fstRedKey)
, Coin {getCoin = 36524597913081152})
,(RedeemPublicKey (Ed25519.PublicKey sndRedKey)
,Coin {getCoin = 37343863242999412})
]) }
where
fstRedKey = hexToBS "e2a1773a2a82d10c30890cbf84eccbdc1aaaee9204\
\96424d36e868039d9cb519"
sndRedKey = hexToBS "9cdabcec332abbc6fdf883ca5bf3a8afddca69bfea\
\c14c013304da88ac032fe6"
GenesisAvvmBalances
{ getGenesisAvvmBalances = HM.fromList
[ ( exampleRedeemPublicKey' (0, 32)
, Coin {getCoin = 36524597913081152}
)
, ( exampleRedeemPublicKey' (32, 32)
, Coin {getCoin = 37343863242999412}
)
]
}
where
exampleRedeemPublicKey' :: (Int, Int) -> RedeemPublicKey
exampleRedeemPublicKey' (m, n) = fromJust (fst <$> redeemDeterministicKeyGen (getBytes m n))

exampleSharedSeed :: SharedSeed
exampleSharedSeed = SharedSeed (getBytes 8 32)
Expand Down
2 changes: 1 addition & 1 deletion core/test/golden/GenesisConfiguration_GCSpec
Original file line number Diff line number Diff line change
@@ -1 +1 @@
{"spec":{"avvmDistr":{"nNq87DMqu8b9-IPKW_Oor93Kab_qwUwBMwTaiKwDL-Y=":37343863242999412,"4qF3OiqC0QwwiQy_hOzL3Bqq7pIElkJNNuhoA52ctRk=":36524597913081152},"ftsSeed":"RTVTNGZTSDZldE5vdWlYZXpDeUVqS2MzdEc0amEwa0Y=","heavyDelegation":{"ed41bf35f4cd2109bdee27b0439942f6de3f55f0ac8d170de7136d3d":{"pskDelegatePk":"3cppv+rBTAEzBNqIrAMu5jKBqwNsGxuRiOSxdLMD9D5VFjsXjpmbn9UGN7Ltq4yFioeaw8S9PmEAlUGaGWllcw==","pskOmega":68300481033,"pskCert":"bae5422af5405e3803154a4ad986da5d14cf624d6701c5c78a79ec73777f74e13973af83752114d9f18166085997fc81e432cab7fee99a275d8bf138ad04e103","pskIssuerPk":"4qF3OiqC0QwwiQy/hOzL3Bqq7pIElkJNNuhoA52ctRkhsl7+Az2bANTwLM2c2rzsMyq7xv34g8pb86iv9KrCfg=="}},"blockVersionData":{"scriptVersion":999,"slotDuration":999,"maxBlockSize":999,"maxHeaderSize":999,"maxTxSize":999,"maxProposalSize":999,"mpcThd":9.9e-14,"heavyDelThd":9.9e-14,"updateVoteThd":9.9e-14,"updateProposalThd":9.9e-14,"updateImplicit":99,"softforkRule":{"initThd":9.9e-14,"minThd":9.9e-14,"thdDecrement":9.9e-14},"txFeePolicy":{"txSizeLinear":{"a":9.99e-7,"b":7.7e-8}},"unlockStakeEpoch":99},"protocolConstants":{"k":37,"protocolMagic":1783847074,"vssMaxTTL":1477558317,"vssMinTTL":744040476},"initializer":{"testBalance":{"poors":2448641325904532856,"richmen":14071205313513960321,"totalBalance":10953275486128625216,"richmenShare":4.2098713311249885,"useHDAddresses":true},"fakeAvvmBalance":{"count":17853231730478779264,"oneBalance":15087947214890024355},"avvmBalanceFactor":0.366832547637728,"useHeavyDlg":false,"seed":0}}}
{"spec":{"avvmDistr":{"dtXbK7ASFLEa611AHt7SDk_48fVbNja-Tj7PgbE7tPE=":37343863242999412,"U4lgqRZyawnwXJ1NSpIrhbThGs_MFDRnPZUBm3qaUtI=":36524597913081152},"ftsSeed":"RTVTNGZTSDZldE5vdWlYZXpDeUVqS2MzdEc0amEwa0Y=","heavyDelegation":{"ed41bf35f4cd2109bdee27b0439942f6de3f55f0ac8d170de7136d3d":{"pskDelegatePk":"3cppv+rBTAEzBNqIrAMu5jKBqwNsGxuRiOSxdLMD9D5VFjsXjpmbn9UGN7Ltq4yFioeaw8S9PmEAlUGaGWllcw==","pskOmega":68300481033,"pskCert":"bae5422af5405e3803154a4ad986da5d14cf624d6701c5c78a79ec73777f74e13973af83752114d9f18166085997fc81e432cab7fee99a275d8bf138ad04e103","pskIssuerPk":"4qF3OiqC0QwwiQy/hOzL3Bqq7pIElkJNNuhoA52ctRkhsl7+Az2bANTwLM2c2rzsMyq7xv34g8pb86iv9KrCfg=="}},"blockVersionData":{"scriptVersion":999,"slotDuration":999,"maxBlockSize":999,"maxHeaderSize":999,"maxTxSize":999,"maxProposalSize":999,"mpcThd":9.9e-14,"heavyDelThd":9.9e-14,"updateVoteThd":9.9e-14,"updateProposalThd":9.9e-14,"updateImplicit":99,"softforkRule":{"initThd":9.9e-14,"minThd":9.9e-14,"thdDecrement":9.9e-14},"txFeePolicy":{"txSizeLinear":{"a":9.99e-7,"b":7.7e-8}},"unlockStakeEpoch":99},"protocolConstants":{"k":37,"protocolMagic":1783847074,"vssMaxTTL":1477558317,"vssMinTTL":744040476},"initializer":{"testBalance":{"poors":2448641325904532856,"richmen":14071205313513960321,"totalBalance":10953275486128625216,"richmenShare":4.2098713311249885,"useHDAddresses":true},"fakeAvvmBalance":{"count":17853231730478779264,"oneBalance":15087947214890024355},"avvmBalanceFactor":0.366832547637728,"useHeavyDlg":false,"seed":0}}}
95 changes: 75 additions & 20 deletions crypto/Pos/Crypto/Orphans.hs
Original file line number Diff line number Diff line change
Expand Up @@ -8,55 +8,112 @@ import Universum

import qualified Cardano.Crypto.Wallet as CC
import qualified Cardano.Crypto.Wallet.Encrypted as CC
import qualified Codec.CBOR.Encoding as E
import Crypto.Error (CryptoFailable (..))
import qualified Crypto.PubKey.Ed25519 as Ed25519
import qualified Crypto.SCRAPE as Scrape
import Crypto.Scrypt (EncryptedPass (..))
import qualified Crypto.Sign.Ed25519 as Ed25519
import Data.Aeson (FromJSON (..), ToJSON (..))
import qualified Data.ByteArray as BA
import qualified Data.ByteString as BS
import Data.Hashable (Hashable)
import Data.SafeCopy (base, deriveSafeCopySimple)
import Data.SafeCopy (SafeCopy (..), base, contain,
deriveSafeCopySimple, safeGet, safePut)
import qualified Data.Text as T
import Serokell.Util.Base64 (JsonByteString (..))

import Pos.Binary.Class (Bi (..), Size, decodeBinary, encodeBinary,
withWordSize)

instance Hashable Ed25519.PublicKey
instance Hashable Ed25519.SecretKey
instance Hashable Ed25519.Signature

instance NFData Ed25519.PublicKey
instance NFData Ed25519.SecretKey
instance NFData Ed25519.Signature
fromByteStringToBytes :: BS.ByteString -> BA.Bytes
fromByteStringToBytes = BA.convert

fromByteStringToScrubbedBytes :: BS.ByteString -> BA.ScrubbedBytes
fromByteStringToScrubbedBytes = BA.convert

toByteString :: (BA.ByteArrayAccess bin) => bin -> BS.ByteString
toByteString = BA.convert


instance Hashable Ed25519.PublicKey where
hashWithSalt salt pk = hashWithSalt salt $ toByteString pk

instance Hashable Ed25519.SecretKey where
hashWithSalt salt pk = hashWithSalt salt $ toByteString pk

instance Hashable Ed25519.Signature where
hashWithSalt salt pk = hashWithSalt salt $ toByteString pk


instance Ord Ed25519.PublicKey where
compare x1 x2 = compare (toByteString x1) (toByteString x2)

instance Ord Ed25519.SecretKey where
compare x1 x2 = compare (toByteString x1) (toByteString x2)

instance Ord Ed25519.Signature where
compare x1 x2 = compare (toByteString x1) (toByteString x2)



instance SafeCopy BA.Bytes where
putCopy s = contain $ safePut (toByteString s)
getCopy = contain $ fromByteStringToBytes <$> safeGet

instance SafeCopy BA.ScrubbedBytes where
putCopy s = contain $ safePut (toByteString s)
getCopy = contain $ fromByteStringToScrubbedBytes <$> safeGet


deriveSafeCopySimple 0 'base ''Ed25519.PublicKey
deriveSafeCopySimple 0 'base ''Ed25519.SecretKey
deriveSafeCopySimple 0 'base ''Ed25519.Signature


fromCryptoFailable :: MonadFail m => T.Text -> CryptoFailable a -> m a
fromCryptoFailable item (CryptoFailed e) = fail $ T.unpack $ "Pos.Crypto.Orphan." <> item <> " failed because " <> show e
fromCryptoFailable _ (CryptoPassed r) = return r


instance FromJSON Ed25519.PublicKey where
parseJSON v = Ed25519.PublicKey . getJsonByteString <$> parseJSON v
parseJSON v = do
res <- Ed25519.publicKey . fromByteStringToBytes . getJsonByteString <$> parseJSON v
fromCryptoFailable "parseJSON Ed25519.PublicKey" res

instance ToJSON Ed25519.PublicKey where
toJSON = toJSON . JsonByteString . Ed25519.openPublicKey
toJSON = toJSON . JsonByteString . toByteString

instance FromJSON Ed25519.Signature where
parseJSON v = Ed25519.Signature . getJsonByteString <$> parseJSON v
parseJSON v = do
res <- Ed25519.signature . fromByteStringToBytes . getJsonByteString <$> parseJSON v
fromCryptoFailable "parseJSON Ed25519.Signature" res

instance ToJSON Ed25519.Signature where
toJSON = toJSON . JsonByteString . Ed25519.unSignature
toJSON = toJSON . JsonByteString . toByteString



instance Bi Ed25519.PublicKey where
encode (Ed25519.PublicKey k) = encode k
decode = Ed25519.PublicKey <$> decode
encodedSizeExpr _ _ = bsSize 32
encode = E.encodeBytes . toByteString
decode = do
res <- Ed25519.publicKey . fromByteStringToBytes <$> decode
fromCryptoFailable "decode Ed25519.PublicKey" res

instance Bi Ed25519.SecretKey where
encode (Ed25519.SecretKey k) = encode k
decode = Ed25519.SecretKey <$> decode
encodedSizeExpr _ _ = bsSize 64
encode sk = E.encodeBytes $ BS.append (toByteString sk) (toByteString $ Ed25519.toPublic sk)
decode = do
res <- Ed25519.secretKey . fromByteStringToScrubbedBytes . BS.take Ed25519.secretKeySize <$> decode
fromCryptoFailable "decode Ed25519.SecretKey" res

instance Bi Ed25519.Signature where
encode (Ed25519.Signature s) = encode s
decode = Ed25519.Signature <$> decode
encodedSizeExpr _ _ = bsSize 64
encode = E.encodeBytes . toByteString
decode = do
res <- Ed25519.signature . fromByteStringToBytes <$> decode
fromCryptoFailable "decode Ed25519.Signature" res

-- Helper for encodedSizeExpr in Bi instances
bsSize :: Int -> Size
Expand Down Expand Up @@ -113,5 +170,3 @@ deriveSafeCopySimple 0 'base ''CC.EncryptedKey
deriveSafeCopySimple 0 'base ''CC.XSignature
deriveSafeCopySimple 0 'base ''CC.XPub
deriveSafeCopySimple 0 'base ''CC.XPrv


7 changes: 0 additions & 7 deletions crypto/Pos/Crypto/SafeCopy.hs
Original file line number Diff line number Diff line change
Expand Up @@ -13,11 +13,8 @@ import Universum
import qualified Cardano.Crypto.Wallet as CC
import qualified Cardano.Crypto.Wallet.Encrypted as CC
import Crypto.Hash (HashAlgorithm)
import qualified Crypto.Sign.Ed25519 as EDS25519

import Data.SafeCopy (SafeCopy (..), base, contain,
deriveSafeCopySimple, safeGet, safePut)

import Pos.Binary.Class (AsBinary (..), Bi)
import qualified Pos.Binary.Class as Bi
import Pos.Binary.SafeCopy (getCopyBi, putCopyBi)
Expand All @@ -38,10 +35,6 @@ deriveSafeCopySimple 0 'base ''CC.XSignature
deriveSafeCopySimple 0 'base ''CC.XPub
deriveSafeCopySimple 0 'base ''CC.XPrv

deriveSafeCopySimple 0 'base ''EDS25519.PublicKey
deriveSafeCopySimple 0 'base ''EDS25519.SecretKey
deriveSafeCopySimple 0 'base ''EDS25519.Signature

deriveSafeCopySimple 0 'base ''PublicKey
deriveSafeCopySimple 0 'base ''SecretKey

Expand Down
26 changes: 15 additions & 11 deletions crypto/Pos/Crypto/Signing/Redeem.hs
Original file line number Diff line number Diff line change
Expand Up @@ -8,11 +8,12 @@ module Pos.Crypto.Signing.Redeem

import Universum

import Crypto.Random (MonadRandom, getRandomBytes)
import Crypto.Error (maybeCryptoError)
import qualified Crypto.PubKey.Ed25519 as Ed25519
import Crypto.Random (MonadRandom)
import qualified Data.ByteArray as BA
import qualified Data.ByteString as BS
import Data.Coerce (coerce)

import qualified Crypto.Sign.Ed25519 as Ed25519
import Pos.Binary.Class (Bi, Raw)
import qualified Pos.Binary.Class as Bi
import Pos.Crypto.Configuration (ProtocolMagic)
Expand All @@ -27,18 +28,21 @@ import Pos.Crypto.Signing.Types.Redeem
-- from "Pos.Crypto.Random" because the OpenSSL generator is probably safer
-- than the default IO generator.
redeemKeyGen :: MonadRandom m => m (RedeemPublicKey, RedeemSecretKey)
redeemKeyGen =
getRandomBytes 32 >>=
maybe err pure . redeemDeterministicKeyGen
where
err = error "Pos.Crypto.RedeemSigning.redeemKeyGen: createKeypairFromSeed_ failed"
redeemKeyGen = do
sk <- Ed25519.generateSecretKey
return (RedeemPublicKey $ Ed25519.toPublic sk, RedeemSecretKey sk)

fromByteStringToBytes :: BS.ByteString -> BA.Bytes
fromByteStringToBytes = BA.convert

-- | Create key pair deterministically from 32 bytes.
redeemDeterministicKeyGen
:: BS.ByteString
-> Maybe (RedeemPublicKey, RedeemSecretKey)
redeemDeterministicKeyGen seed =
bimap RedeemPublicKey RedeemSecretKey <$> Ed25519.createKeypairFromSeed_ seed
case maybeCryptoError $ Ed25519.secretKey $ fromByteStringToBytes seed of
Just r -> Just (RedeemPublicKey $ Ed25519.toPublic r, RedeemSecretKey r)
Nothing -> fail "Pos.Crypto.Signing.Redeem.hs redeemDeterministicKeyGen failed"

----------------------------------------------------------------------------
-- Redeem signatures
Expand All @@ -62,7 +66,7 @@ redeemSignRaw
-> ByteString
-> RedeemSignature Raw
redeemSignRaw pm mbTag (RedeemSecretKey k) x =
RedeemSignature (Ed25519.dsign k (tag <> x))
RedeemSignature (Ed25519.sign k (Ed25519.toPublic k) (fromByteStringToBytes $ tag <> x) )
where
tag = maybe mempty (signTag pm) mbTag

Expand All @@ -84,6 +88,6 @@ redeemVerifyRaw
-> RedeemSignature Raw
-> Bool
redeemVerifyRaw pm mbTag (RedeemPublicKey k) x (RedeemSignature s) =
Ed25519.dverify k (tag <> x) s
Ed25519.verify k (fromByteStringToBytes $ tag <> x) s
where
tag = maybe mempty (signTag pm) mbTag
23 changes: 15 additions & 8 deletions crypto/Pos/Crypto/Signing/Types/Redeem.hs
Original file line number Diff line number Diff line change
Expand Up @@ -14,11 +14,13 @@ import Universum

import Control.Exception.Safe (Exception (..))
import Control.Lens (_Left)
import qualified Crypto.Sign.Ed25519 as Ed25519
import Crypto.Error (CryptoFailable (..))
import qualified Crypto.PubKey.Ed25519 as Ed25519
import Data.Aeson (FromJSONKey (..), FromJSONKeyFunction (..),
ToJSONKey (..), ToJSONKeyFunction (..))
import Data.Aeson.Encoding (text)
import Data.Aeson.TH (defaultOptions, deriveJSON)
import qualified Data.ByteArray as BA
import qualified Data.ByteString as BS
import Data.Hashable (Hashable)
import Data.SafeCopy (SafeCopy (..), base, contain,
Expand Down Expand Up @@ -70,22 +72,25 @@ newtype RedeemSecretKey = RedeemSecretKey Ed25519.SecretKey

deriving instance Bi RedeemSecretKey

fromPublicKeyToByteString :: Ed25519.PublicKey -> BS.ByteString
fromPublicKeyToByteString = BA.convert

redeemPkB64F :: Format r (RedeemPublicKey -> r)
redeemPkB64F =
later $ \(RedeemPublicKey pk) -> formatBase64 $ Ed25519.openPublicKey pk
later $ \(RedeemPublicKey pk) -> formatBase64 $ fromPublicKeyToByteString pk

-- | Base64url Format for 'RedeemPublicKey'.
redeemPkB64UrlF :: Format r (RedeemPublicKey -> r)
redeemPkB64UrlF =
later $ \(RedeemPublicKey pk) ->
B.build $ B64.encodeUrl $ Ed25519.openPublicKey pk
B.build $ B64.encodeUrl $ fromPublicKeyToByteString pk

redeemPkB64ShortF :: Format r (RedeemPublicKey -> r)
redeemPkB64ShortF = fitLeft 8 %. redeemPkB64F

-- | Public key derivation function.
redeemToPublic :: RedeemSecretKey -> RedeemPublicKey
redeemToPublic (RedeemSecretKey k) = RedeemPublicKey (Ed25519.secretToPublicKey k)
redeemToPublic (RedeemSecretKey k) = RedeemPublicKey (Ed25519.toPublic k)

instance B.Buildable RedeemPublicKey where
build = bprint ("redeem_pk:"%redeemPkB64F)
Expand Down Expand Up @@ -148,10 +153,12 @@ fromAvvmPk addrText = do
redeemPkBuild :: ByteString -> RedeemPublicKey
redeemPkBuild bs
| BS.length bs /= 32 =
error $
"consRedeemPk: failed to form pk, wrong bs length: " <> show (BS.length bs) <>
", when should be 32"
| otherwise = RedeemPublicKey $ Ed25519.PublicKey $ bs
error $
"consRedeemPk: failed to form pk, wrong bs length: " <> show (BS.length bs) <>
", when should be 32"
| otherwise = case Ed25519.publicKey $ (BA.convert bs :: BA.Bytes) of
CryptoPassed r -> RedeemPublicKey r
CryptoFailed e -> error $ mappend "Pos.Crypto.Signing.Types.Redeem.hs consRedeemPk failed because " (T.pack $ show e)

----------------------------------------------------------------------------
-- Helpers
Expand Down
1 change: 0 additions & 1 deletion crypto/cardano-sl-crypto.cabal
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,6 @@ library
, cryptonite
, cryptonite-openssl
, data-default
, ed25519
, formatting
, hashable
, lens
Expand Down
10 changes: 9 additions & 1 deletion docs/tls-authentication.md
Original file line number Diff line number Diff line change
Expand Up @@ -108,11 +108,19 @@ $ cardano-node \
All those options are actually optional. When missing, the node looks for default development
certificates and key in `<repo-root>/scripts/tls-files/`.

#### Disable TLS (Not Recommended)
### Disabling TLS (Not Recommended)

#### Fully Turn Off TLS

If needed, you can disable TLS by providing the `--no-tls` flag to the wallet or by running a
wallet in debug mode with `--wallet-debug` turned on.

#### Turn Off Client Certificates Verification

It is possible to lower the TLS requirements down a bit by disabling only client certificates
verification. The communication will still be done in a TLS tunnel though, the server won't
require nor verify any client certificate presented to it. To do so, simply provide the
`--no-client-auth` flag upon starting a wallet node.

### Contacting Cardano-SL Backend

Expand Down
1 change: 0 additions & 1 deletion explorer/scripts/fetch_dependencies.sh
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,6 @@ https://github.com/serokell/log-warper.git
https://github.com/serokell/kademlia.git
https://github.com/serokell/rocksdb-haskell.git
https://github.com/serokell/time-warp-nt.git
https://github.com/thoughtpolice/hs-ed25519.git
https://github.com/serokell/network-transport.git
https://github.com/serokell/network-transport-tcp.git
https://github.com/input-output-hk/cardano-crypto.git
Expand Down
1 change: 0 additions & 1 deletion explorer/scripts/generate_cabal2nix.sh
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,6 @@ https://github.com/serokell/log-warper.git
https://github.com/serokell/kademlia.git
https://github.com/serokell/rocksdb-haskell.git
https://github.com/serokell/time-warp-nt.git
https://github.com/thoughtpolice/hs-ed25519.git
https://github.com/serokell/network-transport.git
https://github.com/serokell/network-transport-tcp.git
https://github.com/input-output-hk/cardano-crypto.git
Expand Down
Loading

0 comments on commit ea2a5b6

Please sign in to comment.