Support for instance-scope keys when verifying HTTP signature

This commit is contained in:
fr33domlover 2019-02-06 02:48:23 +00:00
parent 400245cf34
commit 8166d5b5eb
5 changed files with 178 additions and 70 deletions

View file

@ -41,17 +41,24 @@ Person
VerifKey
ident URI
instance InstanceId
expires UTCTime Maybe
public PublicKey
sharer RemoteSharerId
sharer RemoteSharerId Maybe
UniqueVerifKey ident
RemoteSharer
ident URI
instance InstanceId
UniqueRemoteSharer ident
Instance
host Text
UniqueInstance host
SshKey
ident KyIdent
person PersonId

View file

@ -1,12 +1,19 @@
VerifKey
ident String
instance InstanceId
expires UTCTime Maybe
public ByteString
sharer RemoteSharerId
sharer RemoteSharerId Maybe
UniqueVerifKey ident
RemoteSharer
ident String
instance InstanceId
UniqueRemoteSharer ident
Instance
host Text
UniqueInstance host

View file

@ -18,6 +18,7 @@ module Data.Aeson.Local
, toEither
, fromEither
, frg
, parseHttpsURI'
, parseHttpsURI
, renderURI
, (.=?)
@ -29,7 +30,6 @@ import Prelude
import Control.Applicative ((<|>))
import Data.Aeson
import Data.Aeson.Types (Parser)
import Data.Maybe (isJust)
import Data.Text (Text)
import Network.URI
@ -56,16 +56,22 @@ fromEither (Right y) = Right' y
frg :: Text
frg = "https://forgefed.angeley.es/ns#"
parseHttpsURI :: Text -> Parser URI
parseHttpsURI t =
parseHttpsURI' :: Text -> Either String URI
parseHttpsURI' t =
case parseURI $ T.unpack t of
Nothing -> fail "Invalid absolute URI"
Nothing -> Left "Invalid absolute URI"
Just u ->
if uriScheme u == "https:"
then if isJust $ uriAuthority u
then return u
else fail "URI has empty authority"
else fail "URI scheme isn't https"
then case uriAuthority u of
Just a ->
if uriUserInfo a == "" && uriPort a == ""
then Right u
else Left "URI has userinfo or port"
Nothing -> Left "URI has empty authority"
else Left "URI scheme isn't https"
parseHttpsURI :: Text -> Parser URI
parseHttpsURI = either fail return . parseHttpsURI'
renderURI :: URI -> String
renderURI u = uriToString id u ""

View file

@ -22,7 +22,9 @@ import Control.Monad.Trans.Except
import Control.Monad.Trans.Maybe
import Crypto.Error (CryptoFailable (..))
import Crypto.PubKey.Ed25519 (publicKey, signature, verify)
import Data.Maybe (fromJust)
import Data.PEM (pemContent)
import Data.Text.Encoding (decodeUtf8')
import Data.Time.Interval (TimeInterval, fromTimeUnit, toTimeUnit)
import Data.Time.Units (Second, Minute, Day)
import Database.Persist.Sql (ConnectionPool, runSqlPool)
@ -30,7 +32,7 @@ import Graphics.SVGFonts.ReadFont (PreparedFont)
import Network.HTTP.Client (Manager, HttpException, requestFromURI, responseBody)
import Network.HTTP.Simple (httpJSONEither, setRequestManager, addRequestHeader)
import Network.HTTP.Types.Header (hHost)
import Network.URI (URI (uriFragment), parseURI)
import Network.URI (URI, uriAuthority, uriFragment, uriRegName, parseURI)
import Text.Shakespeare.Text (textFile)
import Text.Hamlet (hamletFile)
--import Text.Jasmine (minifym)
@ -57,6 +59,7 @@ import qualified Network.HTTP.Signature as S (Algorithm (..))
import Web.ActivityPub
import Data.Aeson.Local (parseHttpsURI')
import Text.Email.Local
import Text.Jasmine.Local (discardm)
@ -560,7 +563,7 @@ unsafeHandler = Unsafe.fakeHandlerGetLogger appLogger
instance YesodHttpSig App where
data HttpSigVerResult App = HttpSigVerResult (Either String URI)
httpSigVerHeaders = const [hRequestTarget, hHost]
httpSigVerHeaders = const [hRequestTarget, hHost, "ActivityPub-Actor"]
httpSigVerSeconds =
fromIntegral . toSeconds . appHttpSigTimeLimit . appSettings
where
@ -580,23 +583,41 @@ instance YesodHttpSig App where
case signature sig of
CryptoPassed s -> Right s
CryptoFailed e -> Left "Parsing Ed25519 signature failed"
(mvkid, key, mexpires, uActor) <- do
muActorHeader <- do
bs <- lookupHeaders "ActivityPub-Actor"
case bs of
[] -> return Nothing
[b] -> fmap Just . ExceptT . pure $ do
t <- first displayException $ decodeUtf8' b
parseHttpsURI' t
_ -> throwE "Multiple ActivityPub-Actor headers"
(mvkid, key, mexpires, uActor, host, shared) <- do
ments <- lift $ runDB $ do
mvk <- getBy $ UniqueVerifKey u
for mvk $ \ vk@(Entity _ verifkey) -> do
remote <- getJust $ verifKeySharer verifkey
return (vk, remote)
mremote <- traverse getJust $ verifKeySharer verifkey
return (vk, mremote)
case ments of
Just (Entity vkid vk, remote) ->
Just (Entity vkid vk, mremote) -> do
(ua, s) <-
case mremote of
Just remote -> return (remoteSharerIdent remote, False)
Nothing ->
case muActorHeader of
Nothing -> throwE "Got a sig with an instance key, but actor header not specified!"
Just u -> return (u, True)
let uKey = verifKeyIdent vk
return
( Just vkid
, verifKeyPublic vk
, verifKeyExpires vk
, remoteSharerIdent remote
, ua
, T.pack $ uriRegName $ fromJust $ uriAuthority uKey
, s
)
Nothing -> do
(k, mexp, ua) <- fetchKey' u
return (Nothing, k, mexp, ua)
Fetched k mexp ua h s <- fetchKey' muActorHeader u
return (Nothing, k, mexp, ua, h, s)
let verify' k = verify k input signature
errSig = throwE "Ed25519 sig verification says not valid"
errTime = throwE "Key expired"
@ -609,8 +630,15 @@ instance YesodHttpSig App where
then return (not existsInDB, key, mexpires)
else if existsInDB
then do
(newKey, newExp, newActor) <- fetchKey' u
if newActor == uActor
Fetched newKey newExp newActor h s <- fetchKey' muActorHeader u
if shared == s
then return ()
else throwE "Key scope changed, we reject that"
if shared
then if h == host
then return ()
else fail "BUG! We re-fetched a key and the host changed"
else if newActor == uActor
then return ()
else throwE "Key owner changed, we reject that"
if stillValid newExp
@ -624,18 +652,40 @@ instance YesodHttpSig App where
else errTime
when write $ ExceptT $ runDB $
case mvkid of
Nothing ->
if shared
then do
ment <- getBy $ UniqueInstance host
case ment of
Nothing -> do
iid <- insert $ Instance host
insert_ $ VerifKey u iid mexpires' key' Nothing
return $ Right ()
Just (Entity iid _) -> do
n <- count [VerifKeyInstance ==. iid, VerifKeySharer ==. Nothing]
if n < 2
then do
insert_ $ VerifKey u iid mexpires' key' Nothing
return $ Right ()
else return $ Left "We already store 2 keys"
else do
ment <- getBy $ UniqueRemoteSharer uActor
case ment of
Nothing -> do
rsid <- insert $ RemoteSharer uActor
insert_ $ VerifKey u mexpires' key' rsid
iid <- do
ment2 <- getBy $ UniqueInstance host
case ment2 of
Nothing -> insert $ Instance host
Just (Entity i _) -> return i
rsid <- insert $ RemoteSharer uActor iid
insert_ $ VerifKey u iid mexpires' key' (Just rsid)
return $ Right ()
Just (Entity rsid rs) -> do
n <- count [VerifKeySharer ==. rsid]
n <- count [VerifKeySharer ==. Just rsid]
if n < 2
then do
insert_ $ VerifKey u mexpires' key' rsid
let iid = remoteSharerInstance rs
insert_ $ VerifKey u iid mexpires' key' (Just rsid)
return $ Right ()
else return $ Left "We already store 2 keys"
Just vkid -> do
@ -644,9 +694,9 @@ instance YesodHttpSig App where
return $ Right ()
return uActor
where
fetchKey' u = do
fetchKey' mua uk = do
manager <- getsYesod appHttpManager
ExceptT $ fetchKey manager (isJust malgo) u
ExceptT $ fetchKey manager (isJust malgo) mua uk
instance YesodBreadcrumbs App where
breadcrumb route = return $ case route of

View file

@ -36,6 +36,7 @@ module Web.ActivityPub
, APGetError (..)
, httpGetAP
, httpPostAP
, Fetched (..)
, fetchKey
)
where
@ -72,7 +73,7 @@ import Yesod.Core.Handler (ProvidedRep, provideRepType)
import qualified Crypto.PubKey.Ed25519 as E (PublicKey, publicKey)
import qualified Data.HashMap.Strict as M (lookup)
import qualified Data.Text as T (unpack)
import qualified Data.Text as T (pack, unpack)
import qualified Data.Vector as V (fromList, toList)
import Data.Aeson.Local
@ -361,57 +362,88 @@ httpPostAP manager uri headers sign value =
where
consHeader n b r = r { requestHeaders = (n, b) : requestHeaders r }
-- | Result of GETing the keyId URI and processing the JSON document.
data Fetched = Fetched
{ fetchedPublicKey :: E.PublicKey
-- ^ The Ed25519 public key corresponding to the URI we requested.
, fetchedKeyExpires :: Maybe UTCTime
-- ^ Optional expiration time declared for the key we received.
, fetchedActorId :: URI
-- ^ The @id URI of the actor for whom the key's signature applies.
, fetchedHost :: Text
-- ^ The domain name of the instance from which we got the key.
, fetchedKeyShared :: Bool
-- ^ Whether the key we received is shared. A shared key can sign
-- requests for any actor on the same instance, while a personal key is
-- only for one actor. Knowing whether the key is shared will allow us
-- when receiving more requests, whether to accept signatures made on
-- different actors, or allow only a single permanent actor for the key
-- we received.
}
fetchKey
:: MonadIO m
=> Manager
-> Bool
-> Maybe URI
-> URI
-> m (Either String (E.PublicKey, Maybe UTCTime, URI))
fetchKey manager sigAlgo u = runExceptT $ do
-> m (Either String Fetched)
fetchKey manager sigAlgo muActor uKey = runExceptT $ do
let fetch :: (MonadIO m, FromJSON a) => URI -> ExceptT String m a
fetch u = ExceptT $ bimap displayException responseBody <$> httpGetAP manager u
obj <- fetch u
(actor, pkey, separate) <-
case obj of
Left' pkey -> do
if publicKeyId pkey == u
then return ()
else throwE "Public key's ID doesn't match the keyid URI"
let authority =
case uriAuthority u of
obj <- fetch uKey
let inztance = uKey { uriPath = "", uriQuery = "", uriFragment = "" }
authority =
case uriAuthority uKey of
Nothing -> error "BUG! We were supposed to verify URI authority is non-empty!"
Just a -> a
(actor, pkey, shared) <-
case obj of
Left' pkey -> do
if publicKeyId pkey == uKey
then return ()
else throwE "Public key's ID doesn't match the keyid URI"
if uriAuthority (publicKeyOwner pkey) == Just authority
then return ()
else throwE "Actor and key on different domains, we reject"
actor <- fetch $ publicKeyOwner pkey
uActor <-
if publicKeyShared pkey
then case muActor of
Nothing -> throwE "Key is shared but actor header not specified!"
Just u -> return u
else return $ publicKeyOwner pkey
actor <- fetch uActor
let PublicKeySet k1 mk2 = actorPublicKeys actor
match (Left uri) = uri == u
match (Left uri) = uri == uKey
match (Right _) = False
if match k1 || maybe False match mk2
then return (actor, pkey, True)
then return (actor, pkey, publicKeyShared pkey)
else throwE "Actor publicKey has no URI matching pkey @id"
Right' actor -> do
if actorId actor == u { uriFragment = "" }
if actorId actor == uKey { uriFragment = "" }
then return ()
else throwE "Actor ID doesn't match the keyid URI we fetched"
case muActor of
Nothing -> return ()
Just u ->
if actorId actor == u
then return ()
else throwE "Key's owner doesn't match actor header"
let PublicKeySet k1 mk2 = actorPublicKeys actor
match (Left _) = Nothing
match (Right pk) =
if publicKeyId pk == u
if publicKeyId pk == uKey
then Just pk
else Nothing
case match k1 <|> (match =<< mk2) of
Nothing -> throwE "keyId resolved to actor which doesn't have a key object with that ID"
Just pk -> return (actor, pk, False)
Just pk ->
if publicKeyShared pk
then throwE "Actor's publicKey is shared, but embedded in actor document! We allow shared keys only if they're in a separate document"
else return (actor, pk, False)
ExceptT . pure $ do
if publicKeyShared pkey
then do
if separate
then Right ()
else Left "Actor's publicKey is shared, but embedded in actor document! We allow shared keys only if they're in a separate document"
let inztance = u { uriPath = "", uriQuery = "", uriFragment = "" }
if publicKeyOwner pkey == inztance
if shared
then if publicKeyOwner pkey == inztance
then Right ()
else Left "Key is shared but its owner isn't the top-level instance URI"
else if publicKeyOwner pkey == actorId actor
@ -432,5 +464,11 @@ fetchKey manager sigAlgo u = runExceptT $ do
then "Algo mismatch, Ed25519 in Sig but unsupported algo in actor"
else "No algo in Sig, unsupported algo in actor"
case E.publicKey $ pemContent $ publicKeyPem pkey of
CryptoPassed k -> Right (k, publicKeyExpires pkey, actorId actor)
CryptoFailed e -> Left "Parsing Ed25519 public key failed"
CryptoPassed k -> Right Fetched
{ fetchedPublicKey = k
, fetchedKeyExpires = publicKeyExpires pkey
, fetchedActorId = actorId actor
, fetchedHost = T.pack $ uriRegName authority
, fetchedKeyShared = shared
}
CryptoFailed _ -> Left "Parsing Ed25519 public key failed"