Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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
18 changes: 13 additions & 5 deletions services/brig/src/Brig/API/Client.hs
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,7 @@ 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
Expand Down Expand Up @@ -492,22 +493,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)
<*> 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 Down
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
23 changes: 17 additions & 6 deletions services/brig/src/Brig/Effects/JwtTools.hs
Original file line number Diff line number Diff line change
Expand Up @@ -6,13 +6,15 @@ import Brig.API.Types (CertEnrollmentError (..))
import Control.Monad.Trans.Except
import Data.ByteString.Conversion
import Data.Either.Extra
import Data.Handle (Handle, fromHandle)
import Data.Id
import Data.Jwt.Tools qualified as Jwt
import Data.Misc (HttpsUrl)
import Data.Nonce (Nonce)
import Data.PEMKeys
import Imports
import Network.HTTP.Types (StdMethod (..))
import Network.HTTP.Types qualified as HTTP
import Polysemy
import Wire.API.MLS.Credential (ClientIdentity (..))
import Wire.API.MLS.Epoch (Epoch (..))
Expand All @@ -26,6 +28,10 @@ data JwtTools m a where
Proof ->
-- | The qualified client ID associated with the currently logged on user
ClientIdentity ->
-- | The user's handle
Handle ->
-- | 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,20 +52,25 @@ 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 proof cid handle tid nonce uri method skew ex now pem ->
mapLeft RustError
<$> runExceptT
( DPoPAccessToken
<$> Jwt.generateDpopToken
(Jwt.Proof (toByteString' pr))
(Jwt.UserId (toByteString' (ciUser ci)))
(Jwt.ClientId (clientToWord64 (ciClient ci)))
(Jwt.Domain (toByteString' (ciDomain ci)))
(Jwt.Nonce (toByteString' n))
(Jwt.Proof (toByteString' proof))
(Jwt.UserId (toByteString' (ciUser cid)))
(Jwt.ClientId (clientToWord64 (ciClient cid)))
(Jwt.Handle (toByteString' (urlEncode (fromHandle (handle)))))
(Jwt.TeamId (toByteString' tid))
(Jwt.Domain (toByteString' (ciDomain cid)))
(Jwt.Nonce (toByteString' nonce))
(Jwt.Uri (toByteString' uri))
method
(Jwt.MaxSkewSecs skew)
(Jwt.ExpiryEpoch (epochNumber ex))
(Jwt.NowEpoch (epochNumber now))
(Jwt.PemBundle (toByteString' pem))
)
where
urlEncode :: Text -> Text
urlEncode = cs . HTTP.urlEncode False . cs
Loading