urbit/pkg/hs/urbit-king/lib/Urbit/Vere/Dawn.hs

527 lines
17 KiB
Haskell
Raw Normal View History

2020-01-23 07:16:09 +03:00
{-|
Use L2 to access PKI information.
2020-01-23 07:16:09 +03:00
-}
2020-09-04 21:44:26 +03:00
module Urbit.Vere.Dawn ( dawnVent
, dawnCometList
, renderShip
, mineComet
-- Used only in testing
, mix
, shas
, shaf
, deriveCode
, cometFingerprintBS
, cometFingerprint
2020-09-04 21:44:26 +03:00
) where
2020-01-23 07:16:09 +03:00
import Urbit.Arvo.Common
import Urbit.Arvo.Event hiding (Address)
import Urbit.Prelude hiding (rights, to, (.=))
import Prelude (read)
2019-10-09 23:39:11 +03:00
import Data.Bits (xor)
import Data.List (nub)
import Data.Text (splitOn)
import Data.Aeson
import Data.HexString
import qualified Crypto.Hash.SHA256 as SHA256
2019-10-09 23:39:11 +03:00
import qualified Crypto.Hash.SHA512 as SHA512
import qualified Crypto.Sign.Ed25519 as Ed
import qualified Data.Binary as B
import qualified Data.ByteString as BS
import qualified Data.ByteString.Char8 as C
2020-09-04 21:44:26 +03:00
import qualified Data.ByteString.Lazy as L
2019-10-09 23:39:11 +03:00
import qualified Network.HTTP.Client as C
import qualified Urbit.Ob as Ob
import qualified Network.HTTP.Client.TLS as TLS
import qualified Network.HTTP.Types as HT
-- Conversion Utilities --------------------------------------------------------
2019-09-25 03:15:00 +03:00
passFromText :: Text -> Text -> Int -> Pass
passFromText enc aut sut
| sut /= 1 = Pass (Ed.PublicKey mempty) (Ed.PublicKey mempty)
| otherwise = Pass (Ed.PublicKey $ grab aut) (Ed.PublicKey $ grab enc)
where
grab = reverse . toBytes . hexString . removePrefix . encodeUtf8
2019-09-21 02:10:03 +03:00
clanFromShip :: Ship -> Ob.Class
clanFromShip = Ob.clan . Ob.patp . fromIntegral
shipSein :: Ship -> Ship
shipSein = Ship . fromIntegral . Ob.fromPatp . Ob.sein . Ob.patp . fromIntegral
renderShip :: Ship -> Text
renderShip = Ob.renderPatp . Ob.patp . fromIntegral
onLeft :: (a -> b) -> Either a c -> Either b c
2020-09-04 21:44:26 +03:00
onLeft fun = bimap fun id
2019-09-21 02:10:03 +03:00
-- Data Validation -------------------------------------------------------------
-- Derive public key structure from the key derivation seed structure
ringToPass :: Ring -> Pass
ringToPass Ring{..} = Pass{..}
2019-09-25 03:15:00 +03:00
where
passCrypt = decode ringCrypt
passSign = decode ringSign
decode = fst . fromJust . Ed.createKeypairFromSeed_
2019-10-10 02:58:54 +03:00
fromJust = \case
Nothing -> error "Invalid seed passed to createKeypairFromSeed"
Just x -> x
-- JSONRPC Functions -----------------------------------------------------------
-- Network.JSONRPC appeared to not like something about the JSON that Infura
-- returned; it just hung? Also no documentation.
--
-- Our use case here is simple enough.
-- So, like with Vere, we roll our own.
2020-09-04 21:44:26 +03:00
dawnSendHTTP :: String -> L.ByteString -> RIO e (Either Int L.ByteString)
dawnSendHTTP endpoint requestData = liftIO do
manager <- C.newManager TLS.tlsManagerSettings
initialRequest <- C.parseRequest endpoint
let request = initialRequest
{ C.method = "POST"
, C.requestBody = C.RequestBodyLBS $ requestData
2020-09-04 21:44:26 +03:00
, C.requestHeaders = [("Accept", "application/json"),
("Content-Type", "application/json"),
("Charsets", "utf-8")]
}
response <- C.httpLbs request manager
-- Return body if 200.
let code = HT.statusCode $ C.responseStatus response
case code of
200 -> pure $ Right $ C.responseBody response
2020-09-04 21:44:26 +03:00
_ -> pure $ Left code
class RequestMethod m where
getRequestMethod :: m -> Text
data RawResponse r = RawResponse
{ rrId :: Int
, rrResult :: r
}
deriving (Show)
instance FromJSON r => FromJSON (RawResponse r) where
parseJSON = withObject "Response" $ \v -> do
rawId <- v .: "id"
rrResult <- v .: "result"
let rrId = read rawId
pure RawResponse{..}
-- Given a list of methods and parameters, return a list of decoded responses.
dawnPostRequests :: forall req e resp res
. (ToJSON req, RequestMethod req, FromJSON res)
=> String
-> (req -> res -> resp)
-> [req]
-> RIO e [resp]
dawnPostRequests endpoint responseBuilder requests = do
-- Encode our input requests
let requestPayload =
2020-09-04 21:44:26 +03:00
encode $ Array $ fromList $ fmap toFullRequest $ zip [0..] requests
-- Send to the server
responses <- dawnSendHTTP endpoint requestPayload >>= \case
Left err -> error $ "error fetching " <> endpoint <> ": HTTP " <> (show err)
Right x -> pure x
-- Get a list of the result texts in the order of the submitted requests
rawSorted <- case decode responses of
Nothing -> error $ "couldn't decode json"
Just x -> pure $ map rrResult $ sortOn rrId x
-- Build the final result structure by calling the passed in builder with the
2020-09-04 21:44:26 +03:00
-- request (some outputs need data from the request structure, eitherwise,
-- we'd lean on FromJSON).
let results = map (uncurry responseBuilder) (zip requests rawSorted)
pure results
where
toFullRequest :: (Int, req) -> Value
2020-09-04 21:44:26 +03:00
toFullRequest (rid, req) = object [ "jsonrpc" .= ("2.0" :: Text)
, "method" .= getRequestMethod req
, "params" .= req
, "id" .= (show rid)
2020-09-04 21:44:26 +03:00
]
-- Azimuth JSON Requests -------------------------------------------------------
data PointResponse = PointResponse
--NOTE also contains dominion and ownership, but not actually used here
--TODO remove me!
{ -- prDominion :: Text
-- prOwnership :: PointOwnership
prNetwork :: PointNetwork
} deriving (Show, Eq, Generic)
instance FromJSON PointResponse where
parseJSON (Object o) = do
prNetwork <- o .: "network"
pure PointResponse{..}
parseJSON _ = do error "failed to parse PointResponse json"
data PointOwnership = PointOwnership
{ poTransferProxy :: PointAddress
, poVotingProxy :: PointAddress
, poSpawnProxy :: PointAddress
, poManagementProxy :: PointAddress
, poOwner :: PointAddress
} deriving (Show, Eq, Generic)
instance FromJSON PointOwnership where
parseJSON (Object o) = do
poTransferProxy <- o .: "transferProxy"
poVotingProxy <- o .: "votingProxy"
poSpawnProxy <- o .: "spawnProxy"
poManagementProxy <- o .: "managementProxy"
poOwner <- o .: "owner"
pure PointOwnership{..}
parseJSON _ = do error "failed to parse PointOwnership json"
data PointAddress = PointAddress
{ paAddress :: EthAddr
, paNonce :: Int
} deriving (Show, Eq, Generic)
instance FromJSON PointAddress where
parseJSON (Object o) = do
paAddress <- o .: "address"
paNonce <- o .: "nonce"
pure PointAddress{..}
parseJSON _ = do error "failed to parse PointAddress json"
data PointNetwork = PointNetwork
{ pnKeys :: PointKeys
, pnSponsor :: PointSponsor
, pnRift :: ContNum
} deriving (Show, Eq, Generic)
instance FromJSON PointNetwork where
parseJSON (Object o) = do
pnKeys <- o .: "keys"
pnSponsor <- o .: "sponsor"
pnRift <- o .: "rift"
pure PointNetwork{..}
parseJSON _ = do error "failed to parse PointNetwork json"
data PointKeys = PointKeys
{ pkLife :: Life
, pkSuite :: Int
, pkAuth :: Text --NOTE 0xhex string
, pkCrypt :: Text --NOTE 0xhex string
} deriving (Show, Eq, Generic)
instance FromJSON PointKeys where
parseJSON (Object o) = do
pkLife <- o .: "life"
pkSuite <- o .: "suite"
pkAuth <- o .: "auth"
pkCrypt <- o .: "crypt"
pure PointKeys{..}
parseJSON _ = do error "failed to parse PointKeys json"
data PointSponsor = PointSponsor
{ psHas :: Bool
, psWho :: Text
} deriving (Show, Eq, Generic)
instance FromJSON PointSponsor where
parseJSON (Object o) = do
psHas <- o .: "has"
psWho <- o .: "who"
pure PointSponsor{..}
parseJSON _ = do error "failed to parse PointSponsor json"
data PointRequest = PointRequest Ship
instance RequestMethod PointRequest where
getRequestMethod _ = "getPoint"
instance ToJSON PointRequest where
toJSON (PointRequest point) = object [ "ship" .= renderShip point ]
parseAzimuthPoint :: PointRequest -> PointResponse -> EthPoint
parseAzimuthPoint (PointRequest point) response = EthPoint{..}
where
net = prNetwork response
key = pnKeys net
-- Vere doesn't set ownership information, neither did the old Dawn.hs
-- implementation.
epOwn = (0, 0, 0, 0)
sponsorShip = Ob.parsePatp $ psWho $ pnSponsor net
epNet = if (pkLife key) == 0
then Nothing
else case sponsorShip of
Left _ -> Nothing
Right s -> Just
( fromIntegral $ pkLife key
, passFromText (pkCrypt key) (pkAuth key) (pkSuite key)
, fromIntegral $ pnRift net
, (psHas $ pnSponsor net, Ship $ fromIntegral $ Ob.fromPatp s)
, Nothing --NOTE goes unused currently, so we simply put Nothing
)
-- I don't know what this is supposed to be, other than the old Dawn.hs and
-- dawn.c do the same thing.
-- zero-fill spawn data
epKid = case clanFromShip (Ship $ fromIntegral point) of
Ob.Galaxy -> Just (0, setToHoonSet mempty)
Ob.Star -> Just (0, setToHoonSet mempty)
_ -> Nothing
-- Preprocess data from a point request into the form used in the galaxy table.
parseGalaxyTableEntry :: PointRequest -> PointResponse -> (Ship, (Rift, Life, Pass))
parseGalaxyTableEntry (PointRequest point) response = (ship, (rift, life, pass))
where
net = prNetwork response
keys = pnKeys net
ship = Ship $ fromIntegral point
rift = fromIntegral $ pnRift net
life = fromIntegral $ pkLife keys
pass = passFromText (pkCrypt keys) (pkAuth keys) (pkSuite keys)
removePrefix :: ByteString -> ByteString
removePrefix withOhEx
| prefix == "0x" = suffix
| otherwise = error "not prefixed with 0x"
where
(prefix, suffix) = splitAt 2 withOhEx
data TurfRequest = TurfRequest ()
instance RequestMethod TurfRequest where
getRequestMethod _ = "getDns"
instance ToJSON TurfRequest where
toJSON _ = object [] --NOTE getDns takes no parameters
parseTurfResponse :: TurfRequest -> [Text] -> [Turf]
parseTurfResponse _ = map turf
where
turf t = Turf $ fmap Cord $ reverse $ splitOn "." t
-- Azimuth Functions -----------------------------------------------------------
2019-10-10 02:58:54 +03:00
retrievePoint :: String -> Ship -> RIO e EthPoint
retrievePoint endpoint ship =
dawnPostRequests endpoint parseAzimuthPoint [PointRequest ship]
>>= \case
[x] -> pure x
_ -> error "JSON server returned multiple return values."
2021-06-03 02:04:45 +03:00
validateFeedAndGetSponsor :: String
-> Feed
-> RIO e (Seed, Ship)
validateFeedAndGetSponsor endpoint = \case
2021-06-03 02:04:45 +03:00
Feed0 s -> do
2021-06-17 15:48:09 +03:00
r <- validateSeed s
case r of
Left e -> error e
Right r -> pure (s, r)
2021-06-17 15:48:09 +03:00
Feed1 s -> validateGerms s
2021-06-03 02:04:45 +03:00
where
2021-06-17 15:48:09 +03:00
validateGerms Germs{..} =
case gFeed of
2021-06-17 15:48:09 +03:00
[] -> error "no usable keys in keyfile"
(Germ{..}:f) -> do
let seed = Seed gShip gLife gRing Nothing
r :: Either String Ship
<- validateSeed seed
case r of
2021-06-17 15:48:09 +03:00
Left _ -> validateGerms $ Germs gShip f
Right r -> pure (seed, r)
2021-06-03 02:04:45 +03:00
validateSeed (Seed ship life ring oaf) =
case clanFromShip ship of
Ob.Comet -> validateComet
Ob.Moon -> validateMoon
_ -> validateRest
where
validateComet = do
-- A comet address is the fingerprint of the keypair
let shipFromPass = cometFingerprint $ ringToPass ring
if (ship /= shipFromPass) then
pure $ Left ("comet name doesn't match fingerprint " <>
show ship <> " vs " <>
show shipFromPass)
else if (life /= 1) then
pure $ Left "comet can never be re-keyed"
else
pure $ Right (shipSein ship)
2021-06-03 02:04:45 +03:00
validateMoon = do
-- TODO: The current code in zuse does nothing, but we should be able
-- to try to validate the oath against the current as exists planet
-- on chain.
pure $ Right $ shipSein ship
2021-06-03 02:04:45 +03:00
validateRest = do
putStrLn ("boot: retrieving " <> renderShip ship <> "'s public keys")
--TODO could cache this lookup
whoP <- retrievePoint endpoint ship
2021-06-03 02:04:45 +03:00
case epNet whoP of
Nothing -> pure $ Left "ship not keyed"
2021-06-03 02:04:45 +03:00
Just (netLife, pass, contNum, (hasSponsor, who), _) -> do
if (netLife /= life) then
pure $ Left ("keyfile life mismatch; keyfile claims life " <>
show life <> ", but Azimuth claims life " <>
show netLife)
else if ((ringToPass ring) /= pass) then
pure $ Left "keyfile does not match Azimuth"
2021-06-03 02:04:45 +03:00
-- TODO: The hoon code does a breach check, but the C code never
-- supplies the data necessary for it to function.
else
pure $ Right who
-- Walk through the sponsorship chain retrieving the actual sponsorship chain
-- as it exists on Azimuth.
getSponsorshipChain :: String -> Ship -> RIO e [(Ship,EthPoint)]
getSponsorshipChain endpoint = loop
where
loop ship = do
2020-09-04 21:44:26 +03:00
putStrLn ("boot: retrieving keys for sponsor " <> renderShip ship)
ethPoint <- retrievePoint endpoint ship
2019-10-10 02:58:54 +03:00
case (clanFromShip ship, epNet ethPoint) of
(Ob.Comet, _) -> error "Comets cannot be sponsors"
(Ob.Moon, _) -> error "Moons cannot be sponsors"
2019-10-10 02:58:54 +03:00
2019-10-11 01:01:50 +03:00
(_, Nothing) ->
error $ unpack ("Ship " <> renderShip ship <> " not booted")
2019-10-10 02:58:54 +03:00
(Ob.Galaxy, Just _) -> pure [(ship, ethPoint)]
2019-10-11 01:01:50 +03:00
(_, Just (_, _, _, (False, _), _)) ->
error $ unpack ("Ship " <> renderShip ship <> " has no sponsor")
2019-10-10 02:58:54 +03:00
2019-10-11 01:01:50 +03:00
(_, Just (_, _, _, (True, sponsor), _)) -> do
chain <- loop sponsor
2020-09-04 21:44:26 +03:00
pure $ chain <> [(ship, ethPoint)]
-- Produces either an error or a validated boot event structure.
dawnVent :: HasLogFunc e => String -> Feed -> RIO e (Either Text Dawn)
2021-06-03 02:04:45 +03:00
dawnVent provider feed =
-- The type checker can't figure this out on its own.
(onLeft tshow :: Either SomeException Dawn -> Either Text Dawn) <$> try do
putStrLn ("boot: requesting L2 Azimuth information from " <> pack provider)
(dSeed, immediateSponsor)
<- validateFeedAndGetSponsor provider feed
dSponsor <- getSponsorshipChain provider immediateSponsor
putStrLn "boot: retrieving galaxy table"
dCzar <- (mapToHoonMap . mapFromList) <$>
(dawnPostRequests provider parseGalaxyTableEntry (map (PointRequest . Ship . fromIntegral) [0..255]))
putStrLn "boot: retrieving network domains"
dTurf <- (dawnPostRequests provider parseTurfResponse [TurfRequest ()])
>>= \case
[] -> pure []
[t] -> pure (nub t)
_ -> error "too many turf responses"
let dNode = Nothing
2019-09-26 01:16:48 +03:00
--NOTE blocknum of 0 is fine because jael ignores it.
-- should probably be removed from dawn event.
let dBloq = 0
pure MkDawn{..}
-- Comet List ------------------------------------------------------------------
dawnCometList :: RIO e [Ship]
dawnCometList = do
-- Get the jamfile with the list of stars accepting comets right now.
manager <- io $ C.newManager TLS.tlsManagerSettings
request <- io $ C.parseRequest "https://bootstrap.urbit.org/comet-stars.jam"
response <- io $ C.httpLbs (C.setRequestCheckStatus request) manager
let body = toStrict $ C.responseBody response
noun <- cueBS body & either throwIO pure
fromNounErr noun & either (throwIO . uncurry ParseErr) pure
2019-10-09 23:39:11 +03:00
-- Comet Mining ----------------------------------------------------------------
mix :: BS.ByteString -> BS.ByteString -> BS.ByteString
mix a b = BS.pack $ loop (BS.unpack a) (BS.unpack b)
where
loop [] [] = []
loop a [] = a
loop [] b = b
loop (x:xs) (y:ys) = (xor x y) : loop xs ys
shax :: BS.ByteString -> BS.ByteString
shax = SHA256.hash
shas :: BS.ByteString -> BS.ByteString -> BS.ByteString
shas salt = shax . mix salt . shax
2019-10-09 23:39:11 +03:00
shaf :: BS.ByteString -> BS.ByteString -> BS.ByteString
shaf salt ruz = (mix a b)
where
haz = shas salt ruz
a = (take 16 haz)
b = (drop 16 haz)
2019-10-09 23:39:11 +03:00
-- Given a ring, derives the network login code.
--
-- Note that the network code is a patp, not a patq: the bytes have been
-- scrambled.
deriveCode :: Ring -> Ob.Patp
deriveCode Ring {..} = Ob.patp $
bytesAtom $
take 8 $
shaf (C.pack "pass") $
shax $
C.singleton 'B' <> ringSign <> ringCrypt
cometFingerprintBS :: Pass -> ByteString
cometFingerprintBS = (shaf $ C.pack "bfig") . passToBS
2019-10-09 23:39:11 +03:00
cometFingerprint :: Pass -> Ship
cometFingerprint = Ship . B.decode . fromStrict . reverse . cometFingerprintBS
2019-10-09 23:39:11 +03:00
tryMineComet :: Set Ship -> Word64 -> Maybe Seed
tryMineComet ships seed =
if member shipSponsor ships
then Just $ Seed shipName 1 ring Nothing
else Nothing
where
-- Hash the incoming seed into a 64 bytes.
baseHash = SHA512.hash $ toStrict $ B.encode seed
signSeed = (take 32 baseHash)
ringSeed = (drop 32 baseHash)
ring = Ring signSeed ringSeed
pass = ringToPass ring
2019-10-09 23:39:11 +03:00
shipName = cometFingerprint pass
shipSponsor = shipSein shipName
mineComet :: Set Ship -> Word64 -> Seed
mineComet ships = loop
where
loop eny =
2019-10-09 23:39:11 +03:00
case (tryMineComet ships eny) of
Nothing -> loop (eny + 1)
Just x -> x