Skip to content
Merged
Show file tree
Hide file tree
Changes from 6 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion changelog.d/5-internal/WPB-6099
Original file line number Diff line number Diff line change
@@ -1 +1 @@
Feature enforceFileDownloadLocation lockstatus can be set with basic auth on staging
Version of rusty-jwt-tools bumped to v0.8.0
1 change: 1 addition & 0 deletions changelog.d/5-internal/WPB-6101
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Feature enforceFileDownloadLocation lockstatus can be set with basic auth on staging
3 changes: 2 additions & 1 deletion libs/jwt-tools/default.nix
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
# dependencies are added or removed.
{ mkDerivation
, base
, bytestring
, bytestring-conversion
, gitignoreSource
, hspec
Expand All @@ -25,7 +26,7 @@ mkDerivation {
transformers
];
librarySystemDepends = [ rusty_jwt_tools_ffi ];
testHaskellDepends = [ hspec imports transformers ];
testHaskellDepends = [ bytestring hspec imports transformers ];
description = "FFI to rusty-jwt-tools";
license = lib.licenses.agpl3Only;
}
3 changes: 2 additions & 1 deletion libs/jwt-tools/jwt-tools.cabal
Original file line number Diff line number Diff line change
Expand Up @@ -79,7 +79,8 @@ test-suite jwt-tools-tests
main-is: Spec.hs
type: exitcode-stdio-1.0
build-depends:
hspec
bytestring
, hspec
, imports
, jwt-tools
, transformers
Expand Down
36 changes: 32 additions & 4 deletions libs/jwt-tools/src/Data/Jwt/Tools.hs
Original file line number Diff line number Diff line change
Expand Up @@ -33,10 +33,12 @@ module Data.Jwt.Tools
ExpiryEpoch (..),
NowEpoch (..),
PemBundle (..),
Handle (..),
TeamId (..),
)
where

import Control.Exception
import Control.Exception hiding (handle)
import Control.Monad.Trans.Except
import Data.ByteString.Conversion
import Foreign.C.String (CString, newCString, peekCString)
Expand All @@ -50,6 +52,10 @@ type ProofCStr = CString

type UserIdCStr = CString

type TeamIdCStr = CString

type HandleCStr = CString

type ClientIdWord64 = Word64

type DomainCStr = CString
Expand All @@ -73,6 +79,8 @@ foreign import ccall unsafe "generate_dpop_access_token"
ProofCStr ->
UserIdCStr ->
ClientIdWord64 ->
HandleCStr ->
TeamIdCStr ->
DomainCStr ->
NonceCStr ->
UrlCStr ->
Expand All @@ -93,6 +101,8 @@ generateDpopAccessTokenFfi ::
ProofCStr ->
UserIdCStr ->
ClientIdWord64 ->
HandleCStr ->
TeamIdCStr ->
DomainCStr ->
NonceCStr ->
UrlCStr ->
Expand All @@ -102,8 +112,8 @@ generateDpopAccessTokenFfi ::
EpochWord64 ->
BackendBundleCStr ->
IO (Maybe (Ptr HsResult))
generateDpopAccessTokenFfi dpopProof user client domain nonce uri method maxSkewSecs expiration now backendKeys = do
ptr <- generate_dpop_access_token dpopProof user client domain nonce uri method maxSkewSecs expiration now backendKeys
generateDpopAccessTokenFfi dpopProof user client handle tid domain nonce uri method maxSkewSecs expiration now backendKeys = do
ptr <- generate_dpop_access_token dpopProof user client handle tid domain nonce uri method maxSkewSecs expiration now backendKeys
if ptr /= nullPtr
then pure $ Just ptr
else pure Nothing
Expand All @@ -127,6 +137,8 @@ generateDpopToken ::
Proof ->
UserId ->
ClientId ->
Handle ->
TeamId ->
Domain ->
Nonce ->
Uri ->
Expand All @@ -136,9 +148,11 @@ generateDpopToken ::
NowEpoch ->
PemBundle ->
ExceptT DPoPTokenGenerationError m ByteString
generateDpopToken dpopProof uid cid domain nonce uri method maxSkewSecs maxExpiration now backendPubkeyBundle = do
generateDpopToken dpopProof uid cid handle tid domain nonce uri method maxSkewSecs maxExpiration now backendPubkeyBundle = do
dpopProofCStr <- toCStr dpopProof
uidCStr <- toCStr uid
handleCStr <- toCStr handle
tidCStr <- toCStr tid
domainCStr <- toCStr domain
nonceCStr <- toCStr nonce
uriCStr <- toCStr uri
Expand All @@ -150,6 +164,8 @@ generateDpopToken dpopProof uid cid domain nonce uri method maxSkewSecs maxExpir
dpopProofCStr
uidCStr
(_unClientId cid)
handleCStr
tidCStr
domainCStr
nonceCStr
uriCStr
Expand Down Expand Up @@ -213,6 +229,14 @@ newtype ClientId = ClientId {_unClientId :: Word64}
deriving (Eq, Show)
deriving newtype (ToByteString)

newtype Handle = Handle {_unHandle :: ByteString}
deriving (Eq, Show)
deriving newtype (ToByteString)

newtype TeamId = TeamId {_unTeamId :: ByteString}
deriving (Eq, Show)
deriving newtype (ToByteString)

newtype Domain = Domain {_unDomain :: ByteString}
deriving (Eq, Show)
deriving newtype (ToByteString)
Expand Down Expand Up @@ -323,4 +347,8 @@ data DPoPTokenGenerationError
UnsupportedApiVersion
| -- Bubbling up errors
UnsupportedScope
| -- Client handle does not match the supplied handle
DpopHandleMismatch
| -- Client team does not match the supplied team
DpopTeamMismatch
deriving (Eq, Show, Generic, Bounded, Enum)
42 changes: 20 additions & 22 deletions libs/jwt-tools/test/Spec.hs
Original file line number Diff line number Diff line change
Expand Up @@ -16,28 +16,24 @@
-- with this program. If not, see <https://www.gnu.org/licenses/>.

import Control.Monad.Trans.Except
import Data.ByteString.Char8 (split)
import Data.Jwt.Tools
import Imports
import Test.Hspec

main :: IO ()
main = hspec $ do
describe "generateDpopToken FFI when passing valid inputs" $ do
it "should return an access token" $ do
-- FUTUREWORK(leif): fix this test, we need new valid test data,
-- this test exists mainly for debugging purposes
-- a functionality test is also coverd in the integration tests in services/brig/test/integration/API/User/Client.hs (`testCreateAccessToken`)
pending
actual <- runExceptT $ generateDpopToken proof uid cid domain nonce uri method maxSkewSecs expires now pem
print actual
isRight actual `shouldBe` True
it "should return an access token with the correct header" $ do
actual <- runExceptT $ generateDpopToken proof uid cid handle tid domain nonce uri method maxSkewSecs expires now pem
-- The actual payload of the DPoP token is not deterministic as it depends on the current time.
-- We therefore only check the header, because if the header is correct, it means the token creation was successful.s
let expectedHeader = "eyJhbGciOiJFZERTQSIsInR5cCI6ImF0K2p3dCIsImp3ayI6eyJrdHkiOiJPS1AiLCJjcnYiOiJFZDI1NTE5IiwieCI6ImRZSTM4VWR4a3NDMEs0UXg2RTlKSzlZZkdtLWVoblkxOG9LbUhMMllzWmsifX0"
let actualHeader = either (const "") (head . split '.') actual
actualHeader `shouldBe` expectedHeader
describe "generateDpopToken FFI when passing a wrong nonce value" $ do
it "should return BackendNonceMismatchError" $ do
-- FUTUREWORK(leif): fix this test, we need new valid test data,
-- this test exists mainly for debugging purposes
-- a functionality test is also coverd in the integration tests in services/brig/test/integration/API/User/Client.hs (`testCreateAccessToken`)
pending
actual <- runExceptT $ generateDpopToken proof uid cid domain (Nonce "foobar") uri method maxSkewSecs expires now pem
actual <- runExceptT $ generateDpopToken proof uid cid handle tid domain (Nonce "foobar") uri method maxSkewSecs expires now pem
actual `shouldBe` Left BackendNonceMismatchError
describe "toResult" $ do
it "should convert to correct error" $ do
Expand Down Expand Up @@ -81,16 +77,16 @@ main = hspec $ do
toResult Nothing Nothing `shouldBe` Left UnknownError
where
token = ""
proof = Proof "eyJhbGciOiJFZERTQSIsInR5cCI6ImRwb3Arand0IiwiandrIjp7Imt0eSI6Ik9LUCIsImNydiI6IkVkMjU1MTkiLCJ4IjoidXE2c1hXcDdUM1E3YlNtUFd3eFNlRHJoUHFid1RfcTd4SFBQeGpGT0g5VSJ9fQ.eyJpYXQiOjE2OTQxMTc0MjgsImV4cCI6MTY5NDcyMjIyOCwibmJmIjoxNjk0MTE3NDIzLCJzdWIiOiJpbTp3aXJlYXBwPUlHOVl2enVXUUlLVWFSazEyRjVDSVEvOGUxODk2MjZlYWUwMTExZEBlbG5hLndpcmUubGluayIsImp0aSI6ImM0OGZmOTAyLTc5OGEtNDNjYi04YTk2LTE3NzM0NTgxNjIyMCIsIm5vbmNlIjoiR0FxNG5SajlSWVNzUnhoOVh1MWFtQSIsImh0bSI6IlBPU1QiLCJodHUiOiJodHRwczovL2VsbmEud2lyZS5saW5rL2NsaWVudHMvOGUxODk2MjZlYWUwMTExZC9hY2Nlc3MtdG9rZW4iLCJjaGFsIjoiMkxLbEFWMjR2VGtIMHlaaFdacEZrT01mSEE1d3lGQkgifQ.FW5i40CvndSSo3wQdA1DMUkGRmxk86cORAllwC2PCejVuk7TsdZuIKuJZFVa1VTJKWwNCPqPZ05Gsxxeh1DiDA"
uid = UserId "206f58bf-3b96-4082-9469-1935d85e4221"
cid = ClientId 10239098846720299293
domain = Domain "wire.com"
nonce = Nonce "GAq4nRj9RYSsRxh9Xu1amA"
uri = Uri "https://elna.wire.link/clients/10239098846720299293/access-token"
proof = Proof "eyJhbGciOiJFZERTQSIsImp3ayI6eyJjcnYiOiJFZDI1NTE5Iiwia3R5IjoiT0tQIiwieCI6Im5MSkdOLU9hNkpzcTNLY2xaZ2dMbDdVdkFWZG1CMFE2QzNONUJDZ3BoSHcifSwidHlwIjoiZHBvcCtqd3QifQ.eyJjaGFsIjoid2EyVnJrQ3RXMXNhdUoyRDN1S1k4cmM3eTRrbDR1c0giLCJleHAiOjE4MzExMjYxNjMsImhhbmRsZSI6IndpcmVhcHA6Ly8lNDBwaHVoaGliZGhxYnF4cnpibnNhZndAZXhhbXBsZS5jb20iLCJodG0iOiJQT1NUIiwiaHR1IjoiaHR0cHM6Ly9leGFtcGxlLmNvbS9jbGllbnRzL2NjNmU2NDBlMjk2ZThiYmEvYWNjZXNzLXRva2VuIiwiaWF0IjoxNzA0OTgyMTYzLCJqdGkiOiI2ZmM1OWU3Zi1iNjY2LTRmZmMtYjczOC00ZjQ3NjBjODg0Y2EiLCJuYmYiOjE3MDQ5ODIxNjMsIm5vbmNlIjoiVnZHYnc2ZVZUTkdTUWJLNVNlaVNiQSIsInN1YiI6IndpcmVhcHA6Ly9zZ3VNZUxKdFE2U3ZKUGNxUExiMkJnIWNjNmU2NDBlMjk2ZThiYmFAZXhhbXBsZS5jb20iLCJ0ZWFtIjoiNDAyNTE2ODAtMzVlMS00Mzc0LWIzYWEtNzU2MDBkZTc5ZTMzIn0.JgVXD2_E4j4sLcvD284Fj4z_6xmwA0czcP8wzHZmqPpel60HUqDVKDx5GmiWbFWix-E7ZXvYfvZ7NmxlDrgmAg"
uid = UserId "b20b8c78-b26d-43a4-af24-f72a3cb6f606"
cid = ClientId 14730821443162901434
domain = Domain "example.com"
nonce = Nonce "VvGbw6eVTNGSQbK5SeiSbA"
uri = Uri "https://example.com/clients/cc6e640e296e8bba/access-token"
method = POST
maxSkewSecs = MaxSkewSecs 5
now = NowEpoch 360
expires = ExpiryEpoch 2136351646
maxSkewSecs = MaxSkewSecs 1
now = NowEpoch 1704982162
expires = ExpiryEpoch 1831212562
pem =
PemBundle $
"-----BEGIN PRIVATE KEY-----\n\
Expand All @@ -99,3 +95,5 @@ main = hspec $ do
\-----BEGIN PUBLIC KEY-----\n\
\MCowBQYDK2VwAyEAdYI38UdxksC0K4Qx6E9JK9YfGm+ehnY18oKmHL2YsZk=\n\
\-----END PUBLIC KEY-----\n"
handle = Handle "phuhhibdhqbqxrzbnsafw"
tid = TeamId "40251680-35e1-4374-b3aa-75600de79e33"
15 changes: 10 additions & 5 deletions nix/pkgs/rusty_jwt_tools_ffi/default.nix
Original file line number Diff line number Diff line change
Expand Up @@ -10,14 +10,14 @@
# Cargo.lock file in its root (not at the ffi/ subpath).

let
version = "0.5.0";
version = "0.8.0";
src = fetchFromGitHub {
owner = "wireapp";
repo = "rusty-jwt-tools";
rev = "6704e08376bb49168133d8f4ce66155adeb6bfb0";
sha256 = "sha256-ocmeFXjU3psCO+hpDuEAIzYIm4QzP+jHJR/V8yyw6Lw=";
rev = "064d531b6f0d0b502755dceb3ab73f0f9ad02143";
sha256 = "sha256-OqL4ue6swci3JqQKNmzcvpGxQAMhF8bHTXMp6dvIn9o=";
};
cargoLockFile = builtins.toFile "cargo.lock" (builtins.readFile "${src}/ffi/Cargo.lock");
cargoLockFile = builtins.toFile "cargo.lock" (builtins.readFile "${src}/Cargo.lock");

in
rustPlatform.buildRustPackage {
Expand All @@ -29,7 +29,12 @@ rustPlatform.buildRustPackage {
outputHashes = {
# if any of these need updating, replace / create new key with
# lib.fakeSha256, rebuild, and replace with actual hash.
"jwt-simple-0.11.4" = "sha256-zLKEvL6M7WD7F7HIABqq4b2rmlCS88QXDsj4JhAPe7o=";
"biscuit-0.6.0-beta1" = "sha256-no7b4Un+7AES7EwWdZh/oeIa4w0caKLAUFsHWqgJOrg=";
"certval-0.1.4" = "sha256-mUg3Kx1I/r9zBoB7tDaZsykFkE+tsN+Rem6DjUOZbuU=";
"jwt-simple-0.12.1" = "sha256-5PAOwulL8j6f4Ycoa5Q+1dqEA24uN8rJt+i2RebL6eo=";
"rcgen-0.9.2" = "sha256-3jFzInwdzFBot+L2Vm5NLF1ml33GH2+Iv3LqqGhLxFs=";
"ring-0.17.0-not-released-yet" = "sha256-TP8yZo64J/d1fw8l2J4+ol70EcHvpvHJBdpF3A+6Dgo=";
"x509-ocsp-0.2.1" = "sha256-Tdswn977QtS+i69q82dF/nkXIblUaCsqPD2SqUIYLWc=";
};
};

Expand Down
23 changes: 18 additions & 5 deletions services/brig/src/Brig/API/Client.hs
Original file line number Diff line number Diff line change
Expand Up @@ -75,17 +75,20 @@ import Brig.User.Email
import Cassandra (MonadClient)
import Control.Error
import Control.Lens (view)
import Control.Monad.Trans.Except (except)
import Data.ByteString.Conversion
import Data.Code as Code
import Data.Domain
import Data.Either.Extra (mapLeft)
import Data.Handle (fromHandle)
import Data.Id (ClientId, ConnId, UserId)
import Data.List.Split (chunksOf)
import Data.Map.Strict qualified as Map
import Data.Misc (PlainTextPassword6)
import Data.Qualified
import Data.Set qualified as Set
import Imports
import Network.HTTP.Types qualified as HTTP
import Network.HTTP.Types.Method (StdMethod)
import Network.Wai.Utilities
import Polysemy (Member)
Expand Down Expand Up @@ -492,22 +495,29 @@ createAccessToken ::
createAccessToken luid cid method link proof = do
let domain = tDomain luid
let uid = tUnqualified luid
(tid, handle) <- do
mUser <- lift $ wrapClient (Data.lookupUser NoPendingInvitations uid)
except $
(,)
<$> note NotATeamUser (userTeam =<< mUser)
<*> (urlEncode . fromHandle <$> (note MissingHandle (userHandle =<< mUser)))
nonce <- ExceptT $ note NonceNotFound <$> wrapClient (Nonce.lookupAndDeleteNonce uid (cs $ toByteString cid))
httpsUrl <- do
let urlBs = "https://" <> toByteString' domain <> "/" <> cs (toUrlPiece link)
maybe (throwE MisconfiguredRequestUrl) pure $ fromByteString $ urlBs
httpsUrl <- except $ note MisconfiguredRequestUrl $ fromByteString $ "https://" <> toByteString' domain <> "/" <> cs (toUrlPiece link)
maxSkewSeconds <- Opt.setDpopMaxSkewSecs <$> view settings
expiresIn <- Opt.setDpopTokenExpirationTimeSecs <$> view settings
now <- fromUTCTime <$> lift (liftSem Now.get)
let expiresAt = now & addToEpoch expiresIn
pathToKeys <- ExceptT $ note KeyBundleError . Opt.setPublicKeyBundle <$> view settings
pubKeyBundle <- ExceptT $ note KeyBundleError <$> liftSem (PublicKeyBundle.get pathToKeys)
pubKeyBundle <- do
pathToKeys <- ExceptT $ note KeyBundleError . Opt.setPublicKeyBundle <$> view settings
ExceptT $ note KeyBundleError <$> liftSem (PublicKeyBundle.get pathToKeys)
token <-
ExceptT $
liftSem $
JwtTools.generateDPoPAccessToken
proof
(ClientIdentity domain uid cid)
handle
tid
nonce
httpsUrl
method
Expand All @@ -516,3 +526,6 @@ createAccessToken luid cid method link proof = do
now
pubKeyBundle
pure $ (DPoPAccessTokenResponse token DPoP expiresIn, NoStore)
where
urlEncode :: Text -> Text
urlEncode = cs . HTTP.urlEncode False . cs
4 changes: 4 additions & 0 deletions services/brig/src/Brig/API/Error.hs
Original file line number Diff line number Diff line change
Expand Up @@ -215,10 +215,14 @@ certEnrollmentError (RustError InvalidBackendKeys) = StdError $ Wai.mkError stat
certEnrollmentError (RustError InvalidClientId) = StdError $ Wai.mkError status400 "invalid-client-id" "Bubbling up errors"
certEnrollmentError (RustError UnsupportedApiVersion) = StdError $ Wai.mkError status400 "unsupported-api-version" "Bubbling up errors"
certEnrollmentError (RustError UnsupportedScope) = StdError $ Wai.mkError status400 "unsupported-scope" "Bubbling up errors"
certEnrollmentError (RustError DpopHandleMismatch) = StdError $ Wai.mkError status400 "dpop-handle-mismatch" "Bubbling up errors"
certEnrollmentError (RustError DpopTeamMismatch) = StdError $ Wai.mkError status400 "dpop-team-mismatch" "Bubbling up errors"
certEnrollmentError NonceNotFound = StdError $ Wai.mkError status400 "client-token-bad-nonce" "The client sent an unacceptable anti-replay nonce"
certEnrollmentError MisconfiguredRequestUrl = StdError $ Wai.mkError status500 "misconfigured-request-url" "The request url cannot be derived from optSettings.setFederationDomain in brig.yaml"
certEnrollmentError KeyBundleError = StdError $ Wai.mkError status404 "no-server-key-bundle" "The key bundle required for the certificate enrollment process could not be found"
certEnrollmentError ClientIdSyntaxError = StdError $ Wai.mkError status400 "client-token-id-parse-error" "The client id could not be parsed"
certEnrollmentError NotATeamUser = StdError $ Wai.mkError status400 "not-a-team-user" "The user is not a team user"
certEnrollmentError MissingHandle = StdError $ Wai.mkError status400 "missing-handle" "The user has no handle"

fedError :: FederationError -> Error
fedError = StdError . federationErrorToWai
Expand Down
2 changes: 2 additions & 0 deletions services/brig/src/Brig/API/Types.hs
Original file line number Diff line number Diff line change
Expand Up @@ -208,6 +208,8 @@ data CertEnrollmentError
| KeyBundleError
| MisconfiguredRequestUrl
| ClientIdSyntaxError
| NotATeamUser
| MissingHandle

-------------------------------------------------------------------------------
-- Exceptions
Expand Down
8 changes: 7 additions & 1 deletion services/brig/src/Brig/Effects/JwtTools.hs
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,10 @@ data JwtTools m a where
Proof ->
-- | The qualified client ID associated with the currently logged on user
ClientIdentity ->
-- | The user's handle
Text ->

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I thought we had a Handle type.

-- | The user's team ID
TeamId ->
-- | The most recent DPoP nonce provided by the backend to the current client
Nonce ->
-- | The HTTPS URI on the backend for the DPoP auth token endpoint
Expand All @@ -46,14 +50,16 @@ makeSem ''JwtTools

interpretJwtTools :: Member (Embed IO) r => Sem (JwtTools ': r) a -> Sem r a
interpretJwtTools = interpret $ \case
GenerateDPoPAccessToken pr ci n uri method skew ex now pem ->
GenerateDPoPAccessToken pr ci h t n uri method skew ex now pem ->

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: good opportunity to use readable names for this, what do you think?

mapLeft RustError
<$> runExceptT
( DPoPAccessToken
<$> Jwt.generateDpopToken
(Jwt.Proof (toByteString' pr))
(Jwt.UserId (toByteString' (ciUser ci)))
(Jwt.ClientId (clientToWord64 (ciClient ci)))
(Jwt.Handle (toByteString' h))
(Jwt.TeamId (toByteString' t))
(Jwt.Domain (toByteString' (ciDomain ci)))
(Jwt.Nonce (toByteString' n))
(Jwt.Uri (toByteString' uri))
Expand Down
Loading