"Fossies" - the Fresh Open Source Software Archive

Member "jitsi-meet-7315/resources/prosody-plugins/luajwtjitsi.lib.lua" (2 Jun 2023, 8278 Bytes) of package /linux/misc/jitsi-meet-7315.tar.gz:


As a special service "Fossies" has tried to format the requested source page into HTML format using (guessed) Lua source code syntax highlighting (style: standard) with prefixed line numbers and code folding option. Alternatively you can here view or download the uninterpreted source code file.

    1 local cjson_safe  = require 'cjson.safe'
    2 local basexx = require 'basexx'
    3 local digest = require 'openssl.digest'
    4 local hmac   = require 'openssl.hmac'
    5 local pkey   = require 'openssl.pkey'
    6 
    7 -- Generates an RSA signature of the data.
    8 -- @param data The data to be signed.
    9 -- @param key The private signing key in PEM format.
   10 -- @param algo The digest algorithm to user when generating the signature: sha256, sha384, or sha512.
   11 -- @return The signature or nil and an error message.
   12 local function signRS (data, key, algo)
   13     local privkey = pkey.new(key)
   14     if privkey == nil then
   15         return nil, 'Not a private PEM key'
   16     else
   17         local datadigest = digest.new(algo):update(data)
   18         return privkey:sign(datadigest)
   19     end
   20 end
   21 
   22 -- Verifies an RSA signature on the data.
   23 -- @param data The signed data.
   24 -- @param signature The signature to be verified.
   25 -- @param key The public key of the signer.
   26 -- @param algo The digest algorithm to user when generating the signature: sha256, sha384, or sha512.
   27 -- @return True if the signature is valid, false otherwise. Also returns false if the key is invalid.
   28 local function verifyRS (data, signature, key, algo)
   29     local pubkey = pkey.new(key)
   30     if pubkey == nil then
   31         return false
   32     end
   33 
   34     local datadigest = digest.new(algo):update(data)
   35     return pubkey:verify(signature, datadigest)
   36 end
   37 
   38 local alg_sign = {
   39     ['HS256'] = function(data, key) return hmac.new(key, 'sha256'):final(data) end,
   40     ['HS384'] = function(data, key) return hmac.new(key, 'sha384'):final(data) end,
   41     ['HS512'] = function(data, key) return hmac.new(key, 'sha512'):final(data) end,
   42     ['RS256'] = function(data, key) return signRS(data, key, 'sha256') end,
   43     ['RS384'] = function(data, key) return signRS(data, key, 'sha384') end,
   44     ['RS512'] = function(data, key) return signRS(data, key, 'sha512') end
   45 }
   46 
   47 local alg_verify = {
   48     ['HS256'] = function(data, signature, key) return signature == alg_sign['HS256'](data, key) end,
   49     ['HS384'] = function(data, signature, key) return signature == alg_sign['HS384'](data, key) end,
   50     ['HS512'] = function(data, signature, key) return signature == alg_sign['HS512'](data, key) end,
   51     ['RS256'] = function(data, signature, key) return verifyRS(data, signature, key, 'sha256') end,
   52     ['RS384'] = function(data, signature, key) return verifyRS(data, signature, key, 'sha384') end,
   53     ['RS512'] = function(data, signature, key) return verifyRS(data, signature, key, 'sha512') end
   54 }
   55 
   56 -- Splits a token into segments, separated by '.'.
   57 -- @param token The full token to be split.
   58 -- @return A table of segments.
   59 local function split_token(token)
   60     local segments={}
   61   for str in string.gmatch(token, "([^\\.]+)") do
   62     table.insert(segments, str)
   63   end
   64   return segments
   65 end
   66 
   67 -- Parses a JWT token into it's header, body, and signature.
   68 -- @param token The JWT token to be parsed.
   69 -- @return A JSON header and body represented as a table, and a signature.
   70 local function parse_token(token)
   71     local segments=split_token(token)
   72   if #segments ~= 3 then
   73         return nil, nil, nil, "Invalid token"
   74     end
   75 
   76     local header, err = cjson_safe.decode(basexx.from_url64(segments[1]))
   77     if err then
   78         return nil, nil, nil, "Invalid header"
   79     end
   80 
   81     local body, err = cjson_safe.decode(basexx.from_url64(segments[2]))
   82     if err then
   83         return nil, nil, nil, "Invalid body"
   84     end
   85 
   86     local sig, err = basexx.from_url64(segments[3])
   87     if err then
   88         return nil, nil, nil, "Invalid signature"
   89     end
   90 
   91     return header, body, sig
   92 end
   93 
   94 -- Removes the signature from a JWT token.
   95 -- @param token A JWT token.
   96 -- @return The token without its signature.
   97 local function strip_signature(token)
   98     local segments=split_token(token)
   99   if #segments ~= 3 then
  100         return nil, nil, nil, "Invalid token"
  101     end
  102 
  103     table.remove(segments)
  104     return table.concat(segments, ".")
  105 end
  106 
  107 -- Verifies that a claim is in a list of allowed claims. Allowed claims can be exact values, or the
  108 -- catch all wildcard '*'.
  109 -- @param claim The claim to be verified.
  110 -- @param acceptedClaims A table of accepted claims.
  111 -- @return True if the claim was allowed, false otherwise.
  112 local function verify_claim(claim, acceptedClaims)
  113   for i, accepted in ipairs(acceptedClaims) do
  114     if accepted == '*' then
  115       return true;
  116     end
  117     if claim == accepted then
  118       return true;
  119     end
  120   end
  121 
  122   return false;
  123 end
  124 
  125 local M = {}
  126 
  127 -- Encodes the data into a signed JWT token.
  128 -- @param data The data the put in the body of the JWT token.
  129 -- @param key The key to use for signing the JWT token.
  130 -- @param alg The signature algorithm to use: HS256, HS384, HS512, RS256, RS384, or RS512.
  131 -- @param header Additional values to put in the JWT header.
  132 -- @param The resulting JWT token, or nil and an error message.
  133 function M.encode(data, key, alg, header)
  134     if type(data) ~= 'table' then return nil, "Argument #1 must be table" end
  135     if type(key) ~= 'string' then return nil, "Argument #2 must be string" end
  136 
  137     alg = alg or "HS256"
  138 
  139     if not alg_sign[alg] then
  140         return nil, "Algorithm not supported"
  141     end
  142 
  143     header = header or {}
  144 
  145     header['typ'] = 'JWT'
  146     header['alg'] = alg
  147 
  148     local headerEncoded, err = cjson_safe.encode(header)
  149     if headerEncoded == nil then
  150         return nil, err
  151     end
  152 
  153     local dataEncoded, err = cjson_safe.encode(data)
  154     if dataEncoded == nil then
  155         return nil, err
  156     end
  157 
  158     local segments = {
  159         basexx.to_url64(headerEncoded),
  160         basexx.to_url64(dataEncoded)
  161     }
  162 
  163     local signing_input = table.concat(segments, ".")
  164     local signature, error = alg_sign[alg](signing_input, key)
  165     if signature == nil then
  166         return nil, error
  167     end
  168 
  169     segments[#segments+1] = basexx.to_url64(signature)
  170 
  171     return table.concat(segments, ".")
  172 end
  173 
  174 -- Verify that the token is valid, and if it is return the decoded JSON payload data.
  175 -- @param token The token to verify.
  176 -- @param expectedAlgo The signature algorithm the caller expects the token to be signed with:
  177 --     HS256, HS384, HS512, RS256, RS384, or RS512.
  178 -- @param key The verification key used for the signature.
  179 -- @param acceptedIssuers Optional table of accepted issuers. If not nil, the 'iss' claim will be
  180 --     checked against this list.
  181 -- @param acceptedAudiences Optional table of accepted audiences. If not nil, the 'aud' claim will
  182 --     be checked against this list.
  183 -- @return A table representing the JSON body of the token, or nil and an error message.
  184 function M.verify(token, expectedAlgo, key, acceptedIssuers, acceptedAudiences)
  185     if type(token) ~= 'string' then return nil, "token argument must be string" end
  186     if type(expectedAlgo) ~= 'string' then return nil, "algorithm argument must be string" end
  187     if type(key) ~= 'string' then return nil, "key argument must be string" end
  188     if acceptedIssuers ~= nil and type(acceptedIssuers) ~= 'table' then
  189         return nil, "acceptedIssuers argument must be table"
  190     end
  191     if acceptedAudiences ~= nil and type(acceptedAudiences) ~= 'table' then
  192         return nil, "acceptedAudiences argument must be table"
  193     end
  194 
  195     if not alg_verify[expectedAlgo] then
  196         return nil, "Algorithm not supported"
  197     end
  198 
  199     local header, body, sig, err = parse_token(token)
  200     if err ~= nil then
  201         return nil, err
  202     end
  203 
  204     -- Validate header
  205     if not header.typ or header.typ ~= "JWT" then
  206         return nil, "Invalid typ"
  207     end
  208 
  209     if not header.alg or header.alg ~= expectedAlgo then
  210         return nil, "Invalid or incorrect alg"
  211     end
  212 
  213     -- Validate signature
  214     if not alg_verify[expectedAlgo](strip_signature(token), sig, key) then
  215         return nil, 'Invalid signature'
  216     end
  217 
  218     -- Validate body
  219     if body.exp and type(body.exp) ~= "number" then
  220         return nil, "exp must be number"
  221     end
  222 
  223     if body.nbf and type(body.nbf) ~= "number" then
  224         return nil, "nbf must be number"
  225     end
  226 
  227 
  228     if body.exp and os.time() >= body.exp then
  229         return nil, "Not acceptable by exp"
  230     end
  231 
  232     if body.nbf and os.time() < body.nbf then
  233         return nil, "Not acceptable by nbf"
  234     end
  235 
  236     if acceptedIssuers ~= nil then
  237         local issClaim = body.iss;
  238         if issClaim == nil then
  239         return nil, "'iss' claim is missing";
  240     end
  241     if not verify_claim(issClaim, acceptedIssuers) then
  242         return nil, "invalid 'iss' claim";
  243     end
  244     end
  245 
  246     if acceptedAudiences ~= nil then
  247         local audClaim = body.aud;
  248         if audClaim == nil then
  249         return nil, "'aud' claim is missing";
  250     end
  251     if not verify_claim(audClaim, acceptedAudiences) then
  252         return nil, "invalid 'aud' claim";
  253     end
  254     end
  255 
  256     return body
  257 end
  258 
  259 return M