Skip to content

Commit 0a2d1d6

Browse files
committed
Fix handling of HashJWTs so they're still compatible with old UCM versions
1 parent 1e7de43 commit 0a2d1d6

File tree

5 files changed

+56
-38
lines changed

5 files changed

+56
-38
lines changed

app/Env.hs

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -91,8 +91,14 @@ withEnv action = do
9191
let acceptedAudiences = Set.singleton apiOrigin
9292
let legacyKey = JWT.KeyDescription {JWT.key = hs256Key, JWT.alg = JWT.HS256}
9393
let signingKey = JWT.KeyDescription {JWT.key = edDSAKey, JWT.alg = JWT.Ed25519}
94-
let rotatedKeys = Set.empty
95-
jwtSettings <- case JWT.defaultJWTSettings signingKey (Just legacyKey) rotatedKeys acceptedAudiences apiOrigin of
94+
hashJWTJWK <- case JWT.keyDescToJWK legacyKey of
95+
Left err -> throwIO err
96+
Right (_thumbprint, jwk) -> pure jwk
97+
-- I explicitly add the legacy key to the validation keys, so that the thumbprinted
98+
-- version of the key is used for validation, which is needed for HashJWTs which are signed
99+
-- with a 'kid'.
100+
let validationKeys = Set.fromList [legacyKey]
101+
jwtSettings <- case JWT.defaultJWTSettings signingKey (Just legacyKey) validationKeys acceptedAudiences apiOrigin of
96102
Left cryptoError -> throwIO cryptoError
97103
Right settings -> pure settings
98104
let cookieSettings = Cookies.defaultCookieSettings Deployment.onLocal (Just (realToFrac cookieSessionTTL))

share-auth/src/Share/JWT.hs

Lines changed: 42 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,9 @@ module Share.JWT
1010
ServantAuth.ToJWT (..),
1111
ServantAuth.FromJWT (..),
1212
signJWT,
13+
signJWTWithJWK,
1314
verifyJWT,
15+
keyDescToJWK,
1416

1517
-- * Additional Helpers
1618
textToSignedJWT,
@@ -97,7 +99,8 @@ newtype KeyThumbprint = KeyThumbprint Text
9799

98100
data KeyMap = KeyMap
99101
{ byKeyId :: (Map KeyThumbprint JWT.JWK),
100-
-- | The key from before the introduction of key ids. This will be used to verify legacy tokens, but can eventually be removed.
102+
-- | The key from before the introduction of key ids.
103+
-- This will be used to verify legacy tokens, but is also used to sign HashJWTs on share.
101104
legacyKey :: Maybe JWT.JWK
102105
}
103106
deriving (Show) via (Censored KeyMap)
@@ -134,11 +137,11 @@ defaultJWTSettings ::
134137
-- E.g. https://api.unison-lang.org
135138
URI ->
136139
Either CryptoError JWTSettings
137-
defaultJWTSettings signingKey legacyKey oldValidKeys acceptedAudiences issuer = toEither do
138-
sjwk@(_, signingJWK) <- toJWK signingKey
139-
verificationJWKs <- (sjwk :) <$> traverse toJWK (Set.toList oldValidKeys)
140+
defaultJWTSettings signingKey legacyKey oldValidKeys acceptedAudiences issuer = do
141+
sjwk@(_, signingJWK) <- keyDescToJWK signingKey
142+
verificationJWKs <- (sjwk :) <$> traverse keyDescToJWK (Set.toList oldValidKeys)
140143
let byKeyId = Map.fromList verificationJWKs
141-
legacyKey <- traverse toJWK legacyKey <&> fmap snd
144+
legacyKey <- traverse keyDescToJWK legacyKey <&> fmap snd
142145
pure $
143146
JWTSettings
144147
{ signingJWK,
@@ -148,31 +151,33 @@ defaultJWTSettings signingKey legacyKey oldValidKeys acceptedAudiences issuer =
148151
acceptedAudiences,
149152
issuer
150153
}
154+
155+
-- | Converts a 'KeyDescription' to a 'JWK' and a 'KeyThumbprint'.
156+
keyDescToJWK :: KeyDescription -> Either CryptoError (KeyThumbprint, JWK.JWK)
157+
keyDescToJWK (KeyDescription {key, alg}) = cryptoFailableToEither $ do
158+
case alg of
159+
HS256 -> do
160+
let jwk =
161+
JWK.fromOctets key
162+
& JWK.jwkUse .~ Just JWK.Sig
163+
& JWK.jwkAlg .~ Just (JWK.JWSAlg JWS.HS256)
164+
let thumbprint = jwkThumbprint jwk
165+
pure (KeyThumbprint thumbprint, jwk & JWK.jwkKid .~ Just thumbprint)
166+
Ed25519 -> do
167+
privKey <- Ed25519.secretKey key
168+
let pubKey = Ed25519.toPublic privKey
169+
let jwk =
170+
(JWT.Ed25519Key pubKey (Just privKey))
171+
& JWT.OKPKeyMaterial
172+
& JWK.fromKeyMaterial
173+
& JWK.jwkUse .~ Just JWK.Sig
174+
& JWK.jwkAlg .~ Just (JWK.JWSAlg JWS.EdDSA)
175+
let thumbprint = jwkThumbprint jwk
176+
pure (KeyThumbprint thumbprint, jwk & JWK.jwkKid .~ Just thumbprint)
151177
where
152-
toEither :: CryptoFailable a -> Either CryptoError a
153-
toEither (CryptoFailed err) = Left err
154-
toEither (CryptoPassed a) = Right a
155-
toJWK :: KeyDescription -> CryptoFailable (KeyThumbprint, JWK.JWK)
156-
toJWK (KeyDescription {key, alg}) =
157-
case alg of
158-
HS256 -> do
159-
let jwk =
160-
JWK.fromOctets key
161-
& JWK.jwkUse .~ Just JWK.Sig
162-
& JWK.jwkAlg .~ Just (JWK.JWSAlg JWS.HS256)
163-
let thumbprint = jwkThumbprint jwk
164-
pure (KeyThumbprint thumbprint, jwk & JWK.jwkKid .~ Just thumbprint)
165-
Ed25519 -> do
166-
privKey <- Ed25519.secretKey key
167-
let pubKey = Ed25519.toPublic privKey
168-
let jwk =
169-
(JWT.Ed25519Key pubKey (Just privKey))
170-
& JWT.OKPKeyMaterial
171-
& JWK.fromKeyMaterial
172-
& JWK.jwkUse .~ Just JWK.Sig
173-
& JWK.jwkAlg .~ Just (JWK.JWSAlg JWS.EdDSA)
174-
let thumbprint = jwkThumbprint jwk
175-
pure (KeyThumbprint thumbprint, jwk & JWK.jwkKid .~ Just thumbprint)
178+
cryptoFailableToEither :: CryptoFailable a -> Either CryptoError a
179+
cryptoFailableToEither (CryptoFailed err) = Left err
180+
cryptoFailableToEither (CryptoPassed a) = Right a
176181

177182
jwkThumbprint :: JWK.JWK -> Text
178183
jwkThumbprint jwk =
@@ -216,10 +221,15 @@ textToSignedJWT jwtText = JWT.decodeCompact (TL.encodeUtf8 . TL.fromStrict $ jwt
216221

217222
-- | Signs and encodes a JWT using the given 'JWTSettings'.
218223
signJWT :: forall m v. (MonadIO m, ServantAuth.ToJWT v) => JWTSettings -> v -> m (Either JWT.JWTError JWT.SignedJWT)
219-
signJWT JWTSettings {signingJWK} v = runExceptT $ do
224+
signJWT JWTSettings {signingJWK} v = signJWTWithJWK signingJWK v
225+
226+
-- | Signs and encodes a JWT using the given JWK, you should typically use 'signJWT' instead
227+
-- unless you have a specific reason to use a different JWK.
228+
signJWTWithJWK :: forall m v. (MonadIO m, ServantAuth.ToJWT v) => JWK.JWK -> v -> m (Either JWT.JWTError JWT.SignedJWT)
229+
signJWTWithJWK jwk v = runExceptT $ do
220230
let claimsSet = ServantAuth.encodeJWT v
221-
jwtHeader <- mapExceptT liftIO (JWT.makeJWSHeader signingJWK)
222-
mapExceptT liftIO (JWT.signClaims signingJWK jwtHeader claimsSet)
231+
jwtHeader <- mapExceptT liftIO (JWT.makeJWSHeader jwk)
232+
mapExceptT liftIO (JWT.signClaims jwk jwtHeader claimsSet)
223233

224234
-- | Decodes a JWT and verifies the following:
225235
-- * algorithm (except for legacy tokens)

src/Share/Env.hs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ module Share.Env
33
)
44
where
55

6+
import Crypto.JOSE.JWK qualified as JWK
67
import Database.Redis qualified as R
78
import Hasql.Pool qualified as Hasql
89
import Network.URI (URI)
@@ -34,6 +35,7 @@ data Env ctx = Env
3435
githubClientID :: Text,
3536
githubClientSecret :: Text,
3637
jwtSettings :: JWT.JWTSettings,
38+
hashJWTJWK :: JWK.JWK,
3739
cookieSettings :: Cookies.CookieSettings,
3840
sessionCookieKey :: Text,
3941
sandboxedRuntime :: Runtime Symbol,

src/Share/Web/Authentication/HashJWT.hs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ import Unison.Share.API.Hash (HashJWTClaims)
1010

1111
signHashJWT :: HashJWTClaims -> WebApp SignedJWT
1212
signHashJWT claims = do
13-
jSettings <- asks Env.jwtSettings
14-
JWT.signJWT jSettings claims >>= \case
13+
hashJWTJWK <- asks Env.hashJWTJWK
14+
JWT.signJWTWithJWK hashJWTJWK claims >>= \case
1515
Left err -> respondError (InternalServerError "jwt:signing-error" err)
1616
Right a -> pure a

transcripts/share-apis/releases/project-release-ucm-create.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,12 @@
11
{
22
"body": {
33
"payload": {
4-
"branch-head": "eyJhbGciOiJIUzI1NiJ9.eyJoIjoic2c2MGJ2am85MWZzb283cGtoOWdlamJuMHFnYzk1dnJhODdhcDZsNWQzNXJpMGxrYXVkbDdiczEyZDcxc2YzZmg2cDIzdGVlbXVvcjdtazFpOW41NjdtNTBpYmFrY2doamVjNWFqZyIsInQiOiJoaiIsInUiOiJVLWQzMmY0ZGRmLTI0MjMtNGYxMC1hNGRlLTQ2NTkzOTk1MTM1NCJ9.Cy7vswarIBxyh-HzT7bckfZu6CVzTCm2F3kMLBiHf0s",
4+
"branch-head": "eyJhbGciOiJIUzI1NiIsImtpZCI6IkxCVkJwSlZBZ2hJVHctbll1VlMxZUw3RjFyaWp5VXNqcV9Xd2QzNWJkYXcifQ.eyJoIjoic2c2MGJ2am85MWZzb283cGtoOWdlamJuMHFnYzk1dnJhODdhcDZsNWQzNXJpMGxrYXVkbDdiczEyZDcxc2YzZmg2cDIzdGVlbXVvcjdtazFpOW41NjdtNTBpYmFrY2doamVjNWFqZyIsInQiOiJoaiIsInUiOiJVLWQzMmY0ZGRmLTI0MjMtNGYxMC1hNGRlLTQ2NTkzOTk1MTM1NCJ9.X-GqvqfKSn4im2wDpQq3Zj3Rli4ge4SmmPuNgtvZL0A",
55
"branch-id": "R-<UUID>",
66
"branch-name": "releases/4.5.6",
77
"project-id": "P-<UUID>",
88
"project-name": "@test/publictestproject",
9-
"squashed-branch-head": "eyJhbGciOiJIUzI1NiJ9.eyJoIjoic2c2MGJ2am85MWZzb283cGtoOWdlamJuMHFnYzk1dnJhODdhcDZsNWQzNXJpMGxrYXVkbDdiczEyZDcxc2YzZmg2cDIzdGVlbXVvcjdtazFpOW41NjdtNTBpYmFrY2doamVjNWFqZyIsInQiOiJoaiIsInUiOiJVLWQzMmY0ZGRmLTI0MjMtNGYxMC1hNGRlLTQ2NTkzOTk1MTM1NCJ9.Cy7vswarIBxyh-HzT7bckfZu6CVzTCm2F3kMLBiHf0s"
9+
"squashed-branch-head": "eyJhbGciOiJIUzI1NiIsImtpZCI6IkxCVkJwSlZBZ2hJVHctbll1VlMxZUw3RjFyaWp5VXNqcV9Xd2QzNWJkYXcifQ.eyJoIjoic2c2MGJ2am85MWZzb283cGtoOWdlamJuMHFnYzk1dnJhODdhcDZsNWQzNXJpMGxrYXVkbDdiczEyZDcxc2YzZmg2cDIzdGVlbXVvcjdtazFpOW41NjdtNTBpYmFrY2doamVjNWFqZyIsInQiOiJoaiIsInUiOiJVLWQzMmY0ZGRmLTI0MjMtNGYxMC1hNGRlLTQ2NTkzOTk1MTM1NCJ9.X-GqvqfKSn4im2wDpQq3Zj3Rli4ge4SmmPuNgtvZL0A"
1010
},
1111
"type": "success"
1212
},

0 commit comments

Comments
 (0)