HTTP Signature verification: RSA-SHA256 and PEM/ASN1 support

Before, things worked like this:

* Only signatures of Ed25519 keys could be verified
* Key encoding placed the plain binary Ed25519 key in the PEM, instead of the
  key's ASN1 encoding

With this patch it now works like this:

* Ed25519 signatures are supported as before
* RSA keys are now supported too, assuming RSA-SHA256 signatures
* Both Ed25519 and RSA keys are encoded and decoded using actual PEM with ASN1
This commit is contained in:
fr33domlover 2019-03-10 23:15:42 +00:00
parent ef57f29a54
commit 2a39378468
14 changed files with 303 additions and 167 deletions

View file

@ -43,7 +43,7 @@ VerifKey
ident LocalURI ident LocalURI
instance InstanceId instance InstanceId
expires UTCTime Maybe expires UTCTime Maybe
public PublicKey public PublicVerifKey
sharer RemoteSharerId Maybe sharer RemoteSharerId Maybe
UniqueVerifKey instance ident UniqueVerifKey instance ident

View file

@ -0,0 +1,79 @@
{- This file is part of Vervis.
-
- Written in 2019 by fr33domlover <fr33domlover@riseup.net>.
-
- Copying is an act of love. Please copy, reuse and share.
-
- The author(s) have dedicated all copyright and related and neighboring
- rights to this software to the public domain worldwide. This software is
- distributed without any warranty.
-
- You should have received a copy of the CC0 Public Domain Dedication along
- with this software. If not, see
- <http://creativecommons.org/publicdomain/zero/1.0/>.
-}
module Crypto.PubKey.Encoding
( -- * Plain binary ASN1 encoding
--
-- These functions decode and encode a key using binary BER/DER ASN1
-- encoding. Use them if you need a way to serialize keys and don't care
-- about the format or the fact it's binary and not textual.
decodePubKeyASN1
, encodePubKeyASN1
-- * Textual PEM encoding
--
-- PEM is essentially a Base64 textual representation of ASN1 encoding.
-- It's a common standard format. Use these functions if you need to
-- serialize keys and you prefer a textual format or need
-- interoperability with cryptography related tools that expect PEM
-- files.
, decodePubKeyPEM
, encodePubKeyPEM
)
where
import Prelude
import Control.Exception
import Control.Monad
import Data.ASN1.BinaryEncoding
import Data.ASN1.Encoding
import Data.ASN1.Types
import Data.Bifunctor
import Data.ByteString (ByteString)
import Data.List
import Data.Text (Text)
import Data.PEM
import Data.Text.Encoding
import Data.X509
decodePubKeyASN1 :: ByteString -> Either String PubKey
decodePubKeyASN1 b = do
asn1s <- first displayException $ decodeASN1' BER b
(pkey, rest) <- fromASN1 asn1s
unless (null rest) $ Left "Remaining ASN1 stream isn't empty"
Right pkey
encodePubKeyASN1 :: PubKey -> ByteString
encodePubKeyASN1 pkey = encodeASN1' DER $ toASN1 pkey []
decodePubKeyPEM :: Text -> Either String PubKey
decodePubKeyPEM t = do
pems <- pemParseBS $ encodeUtf8 t
pem <-
case pems of
[] -> Left "Empty PEM"
[x] -> Right x
_ -> Left "Multiple PEM sections"
let name = pemName pem
unless
("PUBLIC KEY" `isSuffixOf` name && not ("PRIVATE" `isInfixOf` name)) $
Left "PEM name suggests it isn't a public key"
unless (null $ pemHeader pem) $ Left "PEM headers found"
decodePubKeyASN1 $ pemContent pem
encodePubKeyPEM :: PubKey -> Text
encodePubKeyPEM =
decodeUtf8 . pemWriteBS . PEM "PUBLIC KEY" [] . encodePubKeyASN1

View file

@ -0,0 +1,82 @@
{- This file is part of Vervis.
-
- Written in 2019 by fr33domlover <fr33domlover@riseup.net>.
-
- Copying is an act of love. Please copy, reuse and share.
-
- The author(s) have dedicated all copyright and related and neighboring
- rights to this software to the public domain worldwide. This software is
- distributed without any warranty.
-
- You should have received a copy of the CC0 Public Domain Dedication along
- with this software. If not, see
- <http://creativecommons.org/publicdomain/zero/1.0/>.
-}
module Crypto.PublicVerifKey
( PublicVerifKey (..)
, fromEd25519
, decodePublicVerifKeyASN1
, encodePublicVerifKeyASN1
, decodePublicVerifKeyPEM
, encodePublicVerifKeyPEM
, verifySignature
)
where
import Prelude
import Control.Exception
import Control.Monad
import Crypto.Error
import Crypto.Hash.Algorithms
import Data.ByteString (ByteString)
import Data.Text (Text)
import Data.X509
import qualified Crypto.PubKey.Ed25519 as E
import qualified Crypto.PubKey.RSA as R
import qualified Crypto.PubKey.RSA.PKCS15 as R
import Crypto.PubKey.Encoding
data PublicVerifKey
= PublicVerifKeyEd25519 E.PublicKey
| PublicVerifKeyRSA R.PublicKey
fromEd25519 :: E.PublicKey -> PublicVerifKey
fromEd25519 = PublicVerifKeyEd25519
fromPubKey :: PubKey -> Either String PublicVerifKey
fromPubKey (PubKeyRSA k) = Right $ PublicVerifKeyRSA k
fromPubKey (PubKeyEd25519 k) = Right $ PublicVerifKeyEd25519 k
fromPubKey (PubKeyUnknown oid _) = Left $ "Unrecognized key type " ++ show oid
fromPubKey pkey =
Left $ "Unsupported key type " ++ takeWhile (/= ' ') (take 12 $ show pkey)
toPubKey :: PublicVerifKey -> PubKey
toPubKey (PublicVerifKeyEd25519 k) = PubKeyEd25519 k
toPubKey (PublicVerifKeyRSA k) = PubKeyRSA k
decodePublicVerifKeyASN1 :: ByteString -> Either String PublicVerifKey
decodePublicVerifKeyASN1 = fromPubKey <=< decodePubKeyASN1
encodePublicVerifKeyASN1 :: PublicVerifKey -> ByteString
encodePublicVerifKeyASN1 = encodePubKeyASN1 . toPubKey
decodePublicVerifKeyPEM :: Text -> Either String PublicVerifKey
decodePublicVerifKeyPEM = fromPubKey <=< decodePubKeyPEM
encodePublicVerifKeyPEM :: PublicVerifKey -> Text
encodePublicVerifKeyPEM = encodePubKeyPEM . toPubKey
verifySignature
:: PublicVerifKey -> ByteString -> ByteString -> Either String Bool
verifySignature (PublicVerifKeyEd25519 pk) msg sig = do
sig' <-
case E.signature sig of
CryptoFailed e -> Left $ displayException e
CryptoPassed s -> Right s
Right $ E.verify pk msg sig'
verifySignature (PublicVerifKeyRSA pk) msg sig =
Right $ R.verify (Just SHA256) pk msg sig

View file

@ -20,32 +20,21 @@ where
import Prelude import Prelude
import Control.Exception (displayException) import Control.Monad
import Control.Monad ((<=<)) import Data.Bifunctor
import Crypto.Error (CryptoFailable, eitherCryptoError)
import Crypto.PubKey.Ed25519 (PublicKey, publicKey)
import Data.Bifunctor (first)
import Data.ByteArray (convert)
import Data.ByteString (ByteString)
import Data.CaseInsensitive (CI) import Data.CaseInsensitive (CI)
import Database.Persist.Class import Database.Persist.Class
import Network.URI (URI, uriScheme, parseURI)
import qualified Data.CaseInsensitive as CI import qualified Data.CaseInsensitive as CI
import qualified Data.Text as T (pack) import qualified Data.Text as T
import Crypto.PublicVerifKey
instance (PersistField s, CI.FoldCase s) => PersistField (CI s) where instance (PersistField s, CI.FoldCase s) => PersistField (CI s) where
toPersistValue = toPersistValue . CI.original toPersistValue = toPersistValue . CI.original
fromPersistValue = fmap CI.mk . fromPersistValue fromPersistValue = fmap CI.mk . fromPersistValue
instance PersistField PublicKey where instance PersistField PublicVerifKey where
toPersistValue = toPersistValue . convert' toPersistValue = toPersistValue . encodePublicVerifKeyASN1
where fromPersistValue =
convert' :: PublicKey -> ByteString first T.pack . decodePublicVerifKeyASN1 <=< fromPersistValue
convert' = convert
fromPersistValue = toKey <=< fromPersistValue
where
publicKey' :: ByteString -> CryptoFailable PublicKey
publicKey' = publicKey
toKey =
first (T.pack . displayException) . eitherCryptoError . publicKey'

View file

@ -20,21 +20,16 @@ where
import Prelude import Prelude
import Crypto.PubKey.Ed25519 (PublicKey)
import Data.ByteArray (convert)
import Data.ByteString (ByteString)
import Data.CaseInsensitive (CI) import Data.CaseInsensitive (CI)
import Database.Persist.Sql import Database.Persist.Sql
import qualified Data.CaseInsensitive as CI import qualified Data.CaseInsensitive as CI
import Crypto.PublicVerifKey
import Database.Persist.Class.Local () import Database.Persist.Class.Local ()
instance (PersistFieldSql s, CI.FoldCase s) => PersistFieldSql (CI s) where instance (PersistFieldSql s, CI.FoldCase s) => PersistFieldSql (CI s) where
sqlType = sqlType . fmap CI.original sqlType = sqlType . fmap CI.original
instance PersistFieldSql PublicKey where instance PersistFieldSql PublicVerifKey where
sqlType = sqlType . fmap convert' sqlType = sqlType . fmap encodePublicVerifKeyASN1
where
convert' :: PublicKey -> ByteString
convert' = convert

View file

@ -35,11 +35,13 @@ import Data.ByteArray (convert)
import Data.ByteString (ByteString) import Data.ByteString (ByteString)
import Data.Time.Interval (TimeInterval, microseconds) import Data.Time.Interval (TimeInterval, microseconds)
import Data.PEM import Data.PEM
import Data.X509
import Network.HTTP.Signature (Signature (..)) import Network.HTTP.Signature (Signature (..))
import System.Directory (doesFileExist) import System.Directory (doesFileExist)
import qualified Data.ByteString as B (writeFile, readFile) import qualified Data.ByteString as B (writeFile, readFile)
import Crypto.PublicVerifKey
import Data.KeyFile import Data.KeyFile
-- | Ed25519 signing key, we generate it on the server and use for signing. We -- | Ed25519 signing key, we generate it on the server and use for signing. We
@ -172,14 +174,8 @@ actorKeyRotator interval keys =
error $ error $
"actorKeyRotator: interval out of range: " ++ show micros "actorKeyRotator: interval out of range: " ++ show micros
-- | The public key in PEM format, can be directly placed in responses. actorKeyPublicBin :: ActorKey -> PublicVerifKey
-- actorKeyPublicBin = fromEd25519 . actorKeyPublic
-- Well, right now it's actually just the public key in binary form, because
-- the type of publicKeyPem is PEM, so, I need to figure out etc. to see if
-- there's a nice way to reuse the PEM that is worth it. Even if not, that's
-- probably okay because the PEM rendering is hopefully trivial.
actorKeyPublicBin :: ActorKey -> ByteString
actorKeyPublicBin = convert . actorKeyPublic
actorKeySign :: ActorKey -> ByteString -> Signature actorKeySign :: ActorKey -> ByteString -> Signature
actorKeySign (ActorKey sec pub) = Signature . convert . sign sec pub actorKeySign (ActorKey sec pub) = Signature . convert . sign sec pub

View file

@ -63,6 +63,7 @@ import Yesod.Mail.Send
import qualified Network.HTTP.Signature as S (Algorithm (..)) import qualified Network.HTTP.Signature as S (Algorithm (..))
import Crypto.PublicVerifKey
import Network.FedURI import Network.FedURI
import Web.ActivityAccess import Web.ActivityAccess
import Web.ActivityPub hiding (PublicKey) import Web.ActivityPub hiding (PublicKey)
@ -604,12 +605,9 @@ instance YesodHttpSig App where
where where
toSeconds :: TimeInterval -> Second toSeconds :: TimeInterval -> Second
toSeconds = toTimeUnit toSeconds = toTimeUnit
httpVerifySig (Verification malgo (KeyId keyid) input (Signature sig)) = fmap HttpSigVerResult $ runExceptT $ do httpVerifySig (Verification malgo (KeyId keyid) input (Signature signature)) = fmap HttpSigVerResult $ runExceptT $ do
verifySigAlgo malgo
(host, luKey) <- f2l <$> parseKeyId keyid (host, luKey) <- f2l <$> parseKeyId keyid
signature <- parseSig sig
mluActorHeader <- getActorHeader host mluActorHeader <- getActorHeader host
let sigAlgo = isJust malgo
manager <- getsYesod appHttpManager manager <- getsYesod appHttpManager
(inboxOrVkid, vkd) <- do (inboxOrVkid, vkd) <- do
ments <- lift $ runDB $ do ments <- lift $ runDB $ do
@ -647,16 +645,17 @@ instance YesodHttpSig App where
, vkdShared = s , vkdShared = s
} }
) )
Nothing -> fetched2vkd luKey <$> fetchUnknownKey manager sigAlgo host mluActorHeader luKey Nothing -> fetched2vkd luKey <$> fetchUnknownKey manager malgo host mluActorHeader luKey
let verify' k = verify k input signature let verify k = ExceptT . pure $ verifySignature k input signature
errSig1 = throwE "Fetched fresh key; Ed25519 sig verification says not valid" errSig1 = throwE "Fetched fresh key; Crypto sig verification says not valid"
errSig2 = throwE "Used key from DB; Ed25519 sig verification says not valid; fetched fresh key; still not valid" errSig2 = throwE "Used key from DB; Crypto sig verification says not valid; fetched fresh key; still not valid"
errTime = throwE "Key expired" errTime = throwE "Key expired"
now <- liftIO getCurrentTime now <- liftIO getCurrentTime
let stillValid Nothing = True let stillValid Nothing = True
stillValid (Just expires) = expires > now stillValid (Just expires) = expires > now
if verify' (vkdKey vkd) && stillValid (vkdExpires vkd) valid1 <- verify $ vkdKey vkd
if valid1 && stillValid (vkdExpires vkd)
then case inboxOrVkid of then case inboxOrVkid of
Left uinb -> ExceptT $ withHostLock host $ runDB $ runExceptT $ addVerifKey host uinb vkd Left uinb -> ExceptT $ withHostLock host $ runDB $ runExceptT $ addVerifKey host uinb vkd
Right _ids -> return () Right _ids -> return ()
@ -670,12 +669,13 @@ instance YesodHttpSig App where
listed = withHostLock' host $ keyListedByActorShared iid vkid host luKey ua listed = withHostLock' host $ keyListedByActorShared iid vkid host luKey ua
(newKey, newExp) <- (newKey, newExp) <-
if vkdShared vkd if vkdShared vkd
then fetchKnownSharedKey manager listed sigAlgo host ua luKey then fetchKnownSharedKey manager listed malgo host ua luKey
else fetchKnownPersonalKey manager sigAlgo host ua luKey else fetchKnownPersonalKey manager malgo host ua luKey
if stillValid newExp if stillValid newExp
then return () then return ()
else errTime else errTime
if verify' newKey valid2 <- verify newKey
if valid2
then lift $ runDB $ updateVerifKey vkid vkd then lift $ runDB $ updateVerifKey vkid vkd
{ vkdKey = newKey { vkdKey = newKey
, vkdExpires = newExp , vkdExpires = newExp
@ -684,18 +684,10 @@ instance YesodHttpSig App where
return $ l2f host $ vkdActorId vkd return $ l2f host $ vkdActorId vkd
where where
verifySigAlgo = traverse_ $ \ algo ->
case algo of
S.AlgorithmEd25519 -> return ()
S.AlgorithmOther _ -> throwE "Unsupported algo in Sig header"
parseKeyId k = parseKeyId k =
case parseFedURI =<< (first displayException . decodeUtf8') k of case parseFedURI =<< (first displayException . decodeUtf8') k of
Left e -> throwE $ "keyId in Sig header isn't a valid FedURI: " ++ e Left e -> throwE $ "keyId in Sig header isn't a valid FedURI: " ++ e
Right u -> return u Right u -> return u
parseSig b =
case signature b of
CryptoPassed s -> return s
CryptoFailed e -> throwE "Parsing Ed25519 signature failed"
getActorHeader host = do getActorHeader host = do
bs <- lookupHeaders hActivityPubActor bs <- lookupHeaders hActivityPubActor
case bs of case bs of

View file

@ -288,11 +288,11 @@ getActorKey choose route = do
let (host, id_) = f2l $ route2uri route let (host, id_) = f2l $ route2uri route
selectRep $ selectRep $
provideAP $ Doc host PublicKey provideAP $ Doc host PublicKey
{ publicKeyId = id_ { publicKeyId = id_
, publicKeyExpires = Nothing , publicKeyExpires = Nothing
, publicKeyOwner = OwnerInstance , publicKeyOwner = OwnerInstance
, publicKeyPem = PEM "PUBLIC KEY" [] actorKey , publicKeyMaterial = actorKey
, publicKeyAlgo = Just AlgorithmEd25519 --, publicKeyAlgo = Just AlgorithmEd25519
} }
getActorKey1R :: Handler TypedContent getActorKey1R :: Handler TypedContent

View file

@ -189,6 +189,10 @@ changes =
, removeEntity "RepoRole" , removeEntity "RepoRole"
-- 41 -- 41
, addEntities model_2019_02_03_verifkey , addEntities model_2019_02_03_verifkey
-- 42
, unchecked $ lift $ do
deleteWhere ([] :: [Filter (VerifKeySharedUsage2019Generic SqlBackend)])
deleteWhere ([] :: [Filter (VerifKey2019Generic SqlBackend)])
] ]
migrateDB :: MonadIO m => ReaderT SqlBackend m (Either Text (Int, Int)) migrateDB :: MonadIO m => ReaderT SqlBackend m (Either Text (Int, Int))

View file

@ -21,6 +21,8 @@ module Vervis.Migration.Model
, Workflow2016 , Workflow2016
, model_2016_09_01_rest , model_2016_09_01_rest
, model_2019_02_03_verifkey , model_2019_02_03_verifkey
, VerifKey2019Generic (..)
, VerifKeySharedUsage2019Generic (..)
) )
where where
@ -61,3 +63,6 @@ makeEntitiesMigration "2018"
model_2019_02_03_verifkey :: [Entity SqlBackend] model_2019_02_03_verifkey :: [Entity SqlBackend]
model_2019_02_03_verifkey = $(schema "2019_02_03_verifkey") model_2019_02_03_verifkey = $(schema "2019_02_03_verifkey")
makeEntitiesMigration "2019"
$(modelFile "migrations/2019_02_03_verifkey.model")

View file

@ -20,12 +20,12 @@ module Vervis.Model where
import ClassyPrelude.Conduit import ClassyPrelude.Conduit
import Yesod hiding (Header, parseTime) import Yesod hiding (Header, parseTime)
import Crypto.PubKey.Ed25519 (PublicKey)
import Database.Persist.Quasi import Database.Persist.Quasi
import Database.Persist.Sql (fromSqlKey) import Database.Persist.Sql (fromSqlKey)
import Text.Email.Validate (EmailAddress) import Text.Email.Validate (EmailAddress)
import Yesod.Auth.Account (PersistUserCredentials (..)) import Yesod.Auth.Account (PersistUserCredentials (..))
import Crypto.PublicVerifKey
import Database.Persist.EmailAddress import Database.Persist.EmailAddress
import Database.Persist.Graph.Class import Database.Persist.Graph.Class
import Network.FedURI (FedURI, LocalURI) import Network.FedURI (FedURI, LocalURI)

View file

@ -46,9 +46,9 @@ import UnliftIO.MVar (withMVar)
import Yesod.Core import Yesod.Core
import Yesod.Persist.Core import Yesod.Persist.Core
import qualified Crypto.PubKey.Ed25519 as E
import qualified Data.HashMap.Strict as M import qualified Data.HashMap.Strict as M
import Crypto.PublicVerifKey
import Database.Persist.Local import Database.Persist.Local
import Network.FedURI import Network.FedURI
import Web.ActivityPub import Web.ActivityPub
@ -357,7 +357,7 @@ keyListedByActorShared iid vkid host luKey luActor = do
data VerifKeyDetail = VerifKeyDetail data VerifKeyDetail = VerifKeyDetail
{ vkdKeyId :: LocalURI { vkdKeyId :: LocalURI
, vkdKey :: E.PublicKey , vkdKey :: PublicVerifKey
, vkdExpires :: Maybe UTCTime , vkdExpires :: Maybe UTCTime
, vkdActorId :: LocalURI , vkdActorId :: LocalURI
, vkdShared :: Bool , vkdShared :: Bool

View file

@ -27,7 +27,7 @@ module Web.ActivityPub
-- ActivityPub actor document including a public key, with a 'FromJSON' -- ActivityPub actor document including a public key, with a 'FromJSON'
-- instance for fetching and a 'ToJSON' instance for publishing. -- instance for fetching and a 'ToJSON' instance for publishing.
, ActorType (..) , ActorType (..)
, Algorithm (..) --, Algorithm (..)
, Owner (..) , Owner (..)
, PublicKey (..) , PublicKey (..)
, Actor (..) , Actor (..)
@ -61,7 +61,6 @@ import Control.Monad (when, unless, (<=<), join)
import Control.Monad.IO.Class import Control.Monad.IO.Class
import Control.Monad.Trans.Except import Control.Monad.Trans.Except
import Control.Monad.Trans.Writer (Writer) import Control.Monad.Trans.Writer (Writer)
import Crypto.Error (CryptoFailable (..))
import Data.Aeson import Data.Aeson
import Data.Aeson.Encoding (pair) import Data.Aeson.Encoding (pair)
import Data.Aeson.Types (Parser, typeMismatch, listEncoding) import Data.Aeson.Types (Parser, typeMismatch, listEncoding)
@ -79,18 +78,19 @@ import Data.Time.Clock (UTCTime)
import Network.HTTP.Client hiding (Proxy, proxy) import Network.HTTP.Client hiding (Proxy, proxy)
import Network.HTTP.Client.Conduit.ActivityPub (httpAPEither) import Network.HTTP.Client.Conduit.ActivityPub (httpAPEither)
import Network.HTTP.Client.Signature (signRequest) import Network.HTTP.Client.Signature (signRequest)
import Network.HTTP.Signature (KeyId, Signature, HttpSigGenError)
import Network.HTTP.Simple (JSONException) import Network.HTTP.Simple (JSONException)
import Network.HTTP.Types.Header (HeaderName, hContentType) import Network.HTTP.Types.Header (HeaderName, hContentType)
import Network.URI import Network.URI
import Yesod.Core.Content (ContentType) import Yesod.Core.Content (ContentType)
import Yesod.Core.Handler (ProvidedRep, provideRepType) import Yesod.Core.Handler (ProvidedRep, provideRepType)
import qualified Crypto.PubKey.Ed25519 as E (PublicKey, publicKey) import qualified Data.ByteString.Char8 as BC
import qualified Data.HashMap.Strict as M (lookup) import qualified Data.HashMap.Strict as M (lookup)
import qualified Data.Text as T (pack, unpack) import qualified Data.Text as T (pack, unpack)
import qualified Data.Vector as V (fromList, toList) import qualified Data.Vector as V (fromList, toList)
import qualified Network.HTTP.Signature as S
import Crypto.PublicVerifKey
import Network.FedURI import Network.FedURI
import Data.Aeson.Local import Data.Aeson.Local
@ -163,20 +163,23 @@ instance ToJSON ActorType where
ActorTypePerson -> "Person" ActorTypePerson -> "Person"
ActorTypeOther t -> t ActorTypeOther t -> t
data Algorithm = AlgorithmEd25519 | AlgorithmOther Text {-
data Algorithm = AlgorithmEd25519 | AlgorithmRsaSha256 | AlgorithmOther Text
instance FromJSON Algorithm where instance FromJSON Algorithm where
parseJSON = withText "Algorithm" $ \ t -> parseJSON = withText "Algorithm" $ \ t -> pure
pure $ if t == frg <> "ed25519" | t == frg <> "ed25519" = AlgorithmEd25519
then AlgorithmEd25519 | t == frg <> "rsa-sha256" = AlgorithmRsaSha256
else AlgorithmOther t | otherwise = AlgorithmOther t
instance ToJSON Algorithm where instance ToJSON Algorithm where
toJSON = error "toJSON Algorithm" toJSON = error "toJSON Algorithm"
toEncoding algo = toEncoding algo =
toEncoding $ case algo of toEncoding $ case algo of
AlgorithmEd25519 -> frg <> "ed25519" AlgorithmEd25519 -> frg <> "ed25519"
AlgorithmOther t -> t AlgorithmRsaSha256 -> frg <> "rsa-sha256"
AlgorithmOther t -> t
-}
data Owner = OwnerInstance | OwnerActor LocalURI data Owner = OwnerInstance | OwnerActor LocalURI
@ -185,11 +188,11 @@ ownerShared OwnerInstance = True
ownerShared (OwnerActor _) = False ownerShared (OwnerActor _) = False
data PublicKey = PublicKey data PublicKey = PublicKey
{ publicKeyId :: LocalURI { publicKeyId :: LocalURI
, publicKeyExpires :: Maybe UTCTime , publicKeyExpires :: Maybe UTCTime
, publicKeyOwner :: Owner , publicKeyOwner :: Owner
, publicKeyPem :: PEM , publicKeyMaterial :: PublicVerifKey
, publicKeyAlgo :: Maybe Algorithm --, publicKeyAlgo :: Maybe Algorithm
} }
instance ActivityPub PublicKey where instance ActivityPub PublicKey where
@ -205,8 +208,10 @@ instance ActivityPub PublicKey where
PublicKey id_ PublicKey id_
<$> o .:? "expires" <$> o .:? "expires"
<*> (mkOwner shared =<< withHost host o "owner") <*> (mkOwner shared =<< withHost host o "owner")
<*> (parsePEM =<< o .: "publicKeyPem") <*> (either fail return . decodePublicVerifKeyPEM =<<
<*> o .:? (frg <> "algorithm") o .: "publicKeyPem"
)
-- <*> o .:? (frg <> "algorithm")
where where
withHost h o t = do withHost h o t = do
(h', lu) <- f2l <$> o .: t (h', lu) <- f2l <$> o .: t
@ -216,20 +221,12 @@ instance ActivityPub PublicKey where
mkOwner True (LocalURI "" "") = return OwnerInstance mkOwner True (LocalURI "" "") = return OwnerInstance
mkOwner True _ = fail "Shared key but owner isn't instance URI" mkOwner True _ = fail "Shared key but owner isn't instance URI"
mkOwner False lu = return $ OwnerActor lu mkOwner False lu = return $ OwnerActor lu
parsePEM t = toSeries host (PublicKey id_ mexpires owner mat)
case pemParseBS $ encodeUtf8 t of
Left e -> fail $ "PEM parsing failed: " ++ e
Right xs ->
case xs of
[] -> fail "Empty PEM"
[x] -> pure x
_ -> fail "Multiple PEM sections"
toSeries host (PublicKey id_ mexpires owner pem malgo)
= "@id" .= l2f host id_ = "@id" .= l2f host id_
<> "expires" .=? mexpires <> "expires" .=? mexpires
<> "owner" .= mkOwner host owner <> "owner" .= mkOwner host owner
<> "publicKeyPem" .= decodeUtf8 (pemWriteBS pem) <> "publicKeyPem" .= encodePublicVerifKeyPEM mat
<> (frg <> "algorithm") .=? malgo -- <> (frg <> "algorithm") .=? malgo
<> (frg <> "isShared") .= ownerShared owner <> (frg <> "isShared") .= ownerShared owner
where where
mkOwner h OwnerInstance = FedURI h "" "" mkOwner h OwnerInstance = FedURI h "" ""
@ -430,7 +427,7 @@ httpGetAP manager uri =
_ -> Left $ APGetErrorContentType "Multiple Content-Type" _ -> Left $ APGetErrorContentType "Multiple Content-Type"
data APPostError data APPostError
= APPostErrorSig HttpSigGenError = APPostErrorSig S.HttpSigGenError
| APPostErrorHTTP HttpException | APPostErrorHTTP HttpException
deriving Show deriving Show
@ -449,7 +446,7 @@ httpPostAP
=> Manager => Manager
-> FedURI -> FedURI
-> NonEmpty HeaderName -> NonEmpty HeaderName
-> (ByteString -> (KeyId, Signature)) -> (ByteString -> (S.KeyId, S.Signature))
-> Text -> Text
-> a -> a
-> m (Either APPostError (Response ())) -> m (Either APPostError (Response ()))
@ -474,8 +471,8 @@ httpPostAP manager uri headers sign uActor value = liftIO $ do
-- | Result of GETing the keyId URI and processing the JSON document. -- | Result of GETing the keyId URI and processing the JSON document.
data Fetched = Fetched data Fetched = Fetched
{ fetchedPublicKey :: E.PublicKey { fetchedPublicKey :: PublicVerifKey
-- ^ The Ed25519 public key corresponding to the URI we requested. -- ^ The Ed25519 or RSA public key corresponding to the URI we requested.
, fetchedKeyExpires :: Maybe UTCTime , fetchedKeyExpires :: Maybe UTCTime
-- ^ Optional expiration time declared for the key we received. -- ^ Optional expiration time declared for the key we received.
, fetchedActorId :: LocalURI , fetchedActorId :: LocalURI
@ -559,33 +556,38 @@ matchKeyObj luKey es =
then Just pk then Just pk
else Nothing else Nothing
verifyAlgo sigAlgo Nothing = verifyAlgo :: Maybe S.Algorithm -> PublicVerifKey -> Either String ()
Left $ verifyAlgo Nothing _ = Right ()
if sigAlgo verifyAlgo (Just a) k =
then "Algo mismatch, Ed25519 in Sig but none in actor" case a of
else "Algo not given in Sig nor actor" S.AlgorithmEd25519 ->
verifyAlgo sigAlgo (Just algo) = case k of
case algo of PublicVerifKeyEd25519 _ -> Right ()
AlgorithmEd25519 -> Right () PublicVerifKeyRSA _ ->
AlgorithmOther _ -> Left "Algo mismatch, algo is Ed25519 but actual key is RSA"
Left $ S.AlgorithmRsaSha256 ->
if sigAlgo case k of
then "Algo mismatch, Ed25519 in Sig but unsupported algo in actor" PublicVerifKeyEd25519 _ ->
else "No algo in Sig, unsupported algo in actor" Left
"Algo mismatch, algo is RSA-SHA256 but actual key is \
parseKey pem = \Ed25519"
case E.publicKey $ pemContent pem of PublicVerifKeyRSA _ -> Right ()
CryptoPassed k -> Right k S.AlgorithmOther b -> Left $ concat
CryptoFailed _ -> Left "Parsing Ed25519 public key failed" [ "Unrecognized algo "
, BC.unpack b
, ", actual key is "
, case k of
PublicVerifKeyEd25519 _ -> "Ed25519"
PublicVerifKeyRSA _ -> "RSA"
]
-- | Fetch a key we don't have cached locally. -- | Fetch a key we don't have cached locally.
fetchUnknownKey fetchUnknownKey
:: MonadIO m :: MonadIO m
=> Manager => Manager
-- ^ Manager for making HTTP requests -- ^ Manager for making HTTP requests
-> Bool -> Maybe S.Algorithm
-- ^ Whether the Ed25519 algorithm is specified explicitly in the HTTP -- ^ Signature algorithm possibly specified in the HTTP signature header
-- signature header
-> Text -> Text
-- ^ Instance host -- ^ Instance host
-> Maybe LocalURI -> Maybe LocalURI
@ -593,9 +595,9 @@ fetchUnknownKey
-> LocalURI -> LocalURI
-- ^ Key URI provided in HTTP signature header -- ^ Key URI provided in HTTP signature header
-> ExceptT String m Fetched -> ExceptT String m Fetched
fetchUnknownKey manager sigAlgo host mluActor luKey = do fetchUnknownKey manager malgo host mluActor luKey = do
obj <- fetchAPIDOrH manager publicKeyId host luKey obj <- fetchAPIDOrH manager publicKeyId host luKey
(pem, mkFetched, malgo) <- fetched <-
case obj of case obj of
Left pkey -> do Left pkey -> do
(oi, luActor) <- (oi, luActor) <-
@ -611,18 +613,13 @@ fetchUnknownKey manager sigAlgo host mluActor luKey = do
else throwE "Key's owner doesn't match actor header" else throwE "Key's owner doesn't match actor header"
return (False, owner) return (False, owner)
inbox <- actorInbox <$> ExceptT (keyListedByActor manager host luKey luActor) inbox <- actorInbox <$> ExceptT (keyListedByActor manager host luKey luActor)
return return Fetched
( publicKeyPem pkey { fetchedPublicKey = publicKeyMaterial pkey
, \ k -> , fetchedKeyExpires = publicKeyExpires pkey
Fetched , fetchedActorId = luActor
{ fetchedPublicKey = k , fetchedActorInbox = inbox
, fetchedKeyExpires = publicKeyExpires pkey , fetchedKeyShared = oi
, fetchedActorId = luActor }
, fetchedActorInbox = inbox
, fetchedKeyShared = oi
}
, publicKeyAlgo pkey
)
Right actor -> do Right actor -> do
if actorId actor == luKey { luriFragment = "" } if actorId actor == luKey { luriFragment = "" }
then return () then return ()
@ -638,23 +635,17 @@ fetchUnknownKey manager sigAlgo host mluActor luKey = do
if owner == actorId actor if owner == actorId actor
then return owner then return owner
else throwE "Actor's publicKey's owner doesn't match the actor's ID" else throwE "Actor's publicKey's owner doesn't match the actor's ID"
return return Fetched
( publicKeyPem pk { fetchedPublicKey = publicKeyMaterial pk
, \ k -> , fetchedKeyExpires = publicKeyExpires pk
Fetched , fetchedActorId = owner
{ fetchedPublicKey = k , fetchedActorInbox = actorInbox actor
, fetchedKeyExpires = publicKeyExpires pk , fetchedKeyShared = False
, fetchedActorId = owner }
, fetchedActorInbox = actorInbox actor ExceptT . pure $ verifyAlgo malgo $ fetchedPublicKey fetched
, fetchedKeyShared = False return fetched
}
, publicKeyAlgo pk
)
ExceptT . pure $ do
verifyAlgo sigAlgo malgo
mkFetched <$> parseKey pem
keyDetail pk = (publicKeyPem pk, publicKeyExpires pk, publicKeyAlgo pk) keyDetail pk = (publicKeyMaterial pk, publicKeyExpires pk)
-- | Fetch a personal key we already have cached locally, but we'd like to -- | Fetch a personal key we already have cached locally, but we'd like to
-- refresh the local copy by fetching the key again from the server. -- refresh the local copy by fetching the key again from the server.
@ -662,19 +653,18 @@ fetchKnownPersonalKey
:: MonadIO m :: MonadIO m
=> Manager => Manager
-- ^ Manager for making HTTP requests -- ^ Manager for making HTTP requests
-> Bool -> Maybe S.Algorithm
-- ^ Whether the Ed25519 algorithm is specified explicitly in the HTTP -- ^ Signature algorithm possibly specified in the HTTP signature header
-- signature header
-> Text -> Text
-- ^ Instance host -- ^ Instance host
-> LocalURI -> LocalURI
-- ^ Key owner actor ID URI -- ^ Key owner actor ID URI
-> LocalURI -> LocalURI
-- ^ Key URI -- ^ Key URI
-> ExceptT String m (E.PublicKey, Maybe UTCTime) -> ExceptT String m (PublicVerifKey, Maybe UTCTime)
fetchKnownPersonalKey manager sigAlgo host luOwner luKey = do fetchKnownPersonalKey manager malgo host luOwner luKey = do
obj <- fetchAPIDOrH manager publicKeyId host luKey obj <- fetchAPIDOrH manager publicKeyId host luKey
(pem, mexpires, malgo) <- (material, mexpires) <-
case obj of case obj of
Left pkey -> do Left pkey -> do
case publicKeyOwner pkey of case publicKeyOwner pkey of
@ -694,9 +684,8 @@ fetchKnownPersonalKey manager sigAlgo host luOwner luKey = do
when (owner /= luOwner) $ when (owner /= luOwner) $
throwE "Actor's publicKey's owner doesn't match the actor's ID" throwE "Actor's publicKey's owner doesn't match the actor's ID"
return $ keyDetail pk return $ keyDetail pk
ExceptT . pure $ do ExceptT . pure $ verifyAlgo malgo material
verifyAlgo sigAlgo malgo return (material, mexpires)
(, mexpires) <$> parseKey pem
-- | Fetch a shared key we already have cached locally, but we'd like to -- | Fetch a shared key we already have cached locally, but we'd like to
-- refresh the local copy by fetching the key again from the server. -- refresh the local copy by fetching the key again from the server.
@ -707,17 +696,16 @@ fetchKnownSharedKey
-> ExceptT String m () -> ExceptT String m ()
-- ^ Action which checks whether the actor from HTTP actor header lists the -- ^ Action which checks whether the actor from HTTP actor header lists the
-- key, potentually updating our local cache if needed. -- key, potentually updating our local cache if needed.
-> Bool -> Maybe S.Algorithm
-- ^ Whether the Ed25519 algorithm is specified explicitly in the HTTP -- ^ Signature algorithm possibly specified in the HTTP signature header
-- signature header
-> Text -> Text
-- ^ Instance host -- ^ Instance host
-> LocalURI -> LocalURI
-- ^ Actor ID from HTTP actor header -- ^ Actor ID from HTTP actor header
-> LocalURI -> LocalURI
-- ^ Key URI -- ^ Key URI
-> ExceptT String m (E.PublicKey, Maybe UTCTime) -> ExceptT String m (PublicVerifKey, Maybe UTCTime)
fetchKnownSharedKey manager listed sigAlgo host luActor luKey = do fetchKnownSharedKey manager listed malgo host luActor luKey = do
obj <- fetchAPIDOrH manager publicKeyId host luKey obj <- fetchAPIDOrH manager publicKeyId host luKey
pkey <- pkey <-
case obj :: Either PublicKey Actor of case obj :: Either PublicKey Actor of
@ -727,7 +715,6 @@ fetchKnownSharedKey manager listed sigAlgo host luActor luKey = do
OwnerInstance -> return () OwnerInstance -> return ()
OwnerActor _owner -> throwE "Shared key became personal" OwnerActor _owner -> throwE "Shared key became personal"
listed listed
let (pem, mexpires, malgo) = keyDetail pkey let (material, mexpires) = keyDetail pkey
ExceptT . pure $ do ExceptT . pure $ verifyAlgo malgo material
verifyAlgo sigAlgo malgo return (material, mexpires)
(, mexpires) <$> parseKey pem

View file

@ -40,6 +40,8 @@ flag library-only
library library
exposed-modules: Control.Applicative.Local exposed-modules: Control.Applicative.Local
Control.Concurrent.Local Control.Concurrent.Local
Crypto.PubKey.Encoding
Crypto.PublicVerifKey
Darcs.Local.Repository Darcs.Local.Repository
Data.Aeson.Encode.Pretty.ToEncoding Data.Aeson.Encode.Pretty.ToEncoding
Data.Aeson.Local Data.Aeson.Local
@ -208,6 +210,9 @@ library
build-depends: aeson build-depends: aeson
-- For activity JSOn display in /inbox test page -- For activity JSOn display in /inbox test page
, aeson-pretty , aeson-pretty
-- for encoding and decoding of crypto public keys
, asn1-encoding
, asn1-types
-- for parsing commands sent over SSH and Darcs patch -- for parsing commands sent over SSH and Darcs patch
-- metadata -- metadata
, attoparsec , attoparsec
@ -336,6 +341,8 @@ library
, wai-extra , wai-extra
, wai-logger , wai-logger
, warp , warp
-- for encoding and decoding of crypto public keys
, x509
, xss-sanitize , xss-sanitize
, yaml , yaml
, yesod , yesod