Module: JWT::Multisig

Defined in:
lib/jwt-multisig.rb,
lib/jwt-multisig/version.rb

Overview

The module provides tools for encoding/decoding JWT with multiple signatures.

Constant Summary collapse

VERSION =
"1.0.5"

Class Method Summary collapse

Class Method Details

.add_jws(jwt, key_id, key_value, algorithm) ⇒ Hash

Generates and adds new JWS to existing JWT.

Parameters:

  • jwt (Hash)

    The existing JWT.

  • key_id (String)

    The JWS key ID.

  • key_value (String, OpenSSL::PKey::PKey)

    The private key in PEM format or as instance of OpenSSL::PKey::PKey.

  • algorithm (String)

    The signature algorithm.

Returns:

  • (Hash)

    The JWT with added JWS.

Raises:

  • (JWT::EncodeError)

65
66
67
68
69
70
71
72
# File 'lib/jwt-multisig.rb', line 65

def add_jws(jwt, key_id, key_value, algorithm)
  proxy_exception JWT::EncodeError do
    remove_jws(jwt, key_id).tap do |new_jwt|
      payload = JSON.parse(base64_decode(new_jwt.fetch(:payload)))
      new_jwt.fetch(:signatures) << generate_jws(payload, key_id, key_value, algorithm)
    end
  end
end

.generate_jws(payload, key_id, key_value, algorithm) ⇒ Hash

Generates new JWS based on payload, key, and algorithm.

Parameters:

  • payload (Hash)
  • key_id (String)

    The value which is used as «kid» in JWS header.

  • key_value (String, OpenSSL::PKey::PKey)

    The private key.

  • algorithm (String)

    The signature algorithm.

Returns:

  • (Hash)

    The JWS in the format as defined in RFC 7515. Example:

    { protected: "eyJhbGciOiJFUzI1NiJ9",
      header: {
        kid: "e9bc097a-ce51-4036-9562-d2ade882db0d"
      },
      signature: "DtEhU3ljbEg8L38VWAfUAqOyKAM6-Xx-F4GawxaepmXFCgfTjDxw5djxLa8ISlSApmWQxfKTUJqPP3-Kg6NU1Q"
    }
    

Raises:

  • (JWT::EncodeError)

167
168
169
170
171
172
173
174
# File 'lib/jwt-multisig.rb', line 167

def generate_jws(payload, key_id, key_value, algorithm)
  proxy_exception JWT::EncodeError do
    protected, _, signature = JWT.encode(payload, to_pem_or_key(key_value, algorithm), algorithm).split(".")
    { protected: protected,
      header:    { kid: key_id },
      signature: signature }
  end
end

.generate_jwt(payload, private_keychain, algorithms) ⇒ Hash

Generates new JWT based on payload, keys, and algorithms.

Parameters:

  • payload (Hash)
  • private_keychain (Hash)

    The hash which consists of pairs: key ID => private key. The key may be presented as string in PEM format or as instance of OpenSSL::PKey::PKey.

  • algorithms

    The hash which consists of pairs: key ID => signature algorithm.

Returns:

  • (Hash)

    The JWT in the format as defined in RFC 7515. Example:

    { payload: "eyJpc3MiOiJqb2UiLA0KICJleHAiOjEzMDA4MTkzODAsDQogImh0dHA6Ly9leGFtcGxlLmNvbS9pc19yb290Ijp0cnVlfQ",
      signatures: [
        { protected: "eyJhbGciOiJSUzI1NiJ9",
          header: { kid: "2010-12-29" },
          signature: "cC4hiUPoj9Eetdgtv3hF80EGrhuB__dzERat0XF9g2VtQgr9PJbu3XOiZj5RZmh7AAuHIm4Bh-0Qc_lF5YKt_O8W2Fp5jujGbds9uJdbF9CUAr7t1dnZcAcQjbKBYNX4BAynRFdiuB--f_nZLgrnbyTyWzO75vRK5h6xBArLIARNPvkSjtQBMHlb1L07Qe7K0GarZRmB_eSN9383LcOLn6_dO--xi12jzDwusC-eOkHWEsqtFZESc6BfI7noOPqvhJ1phCnvWh6IeYI2w9QOYEUipUTI8np6LbgGY9Fs98rqVt5AXLIhWkWywlVmtVrBp0igcN_IoypGlUPQGe77Rw"
        },
        { protected: "eyJhbGciOiJFUzI1NiJ9",
          header: { kid: "e9bc097a-ce51-4036-9562-d2ade882db0d" },
          signature: "DtEhU3ljbEg8L38VWAfUAqOyKAM6-Xx-F4GawxaepmXFCgfTjDxw5djxLa8ISlSApmWQxfKTUJqPP3-Kg6NU1Q"
        }
      ]
    }
    

Raises:

  • (JWT::EncodeError)

41
42
43
44
45
46
47
48
49
# File 'lib/jwt-multisig.rb', line 41

def generate_jwt(payload, private_keychain, algorithms)
  proxy_exception JWT::EncodeError do
    algorithms_mapping = algorithms.with_indifferent_access
    { payload:    base64_encode(::JSON.dump(payload)),
      signatures: private_keychain.map do |id, value|
        generate_jws(payload, id, value, algorithms_mapping.fetch(id))
      end }
  end
end

.remove_jws(jwt, key_id) ⇒ Hash

Removes all JWS associated with given key ID.

Parameters:

  • jwt (Hash)

    The existing JWT.

  • key_id (String)

    The key ID to match JWS by.

Returns:

  • (Hash)

    The JWT with all matched JWS removed.


83
84
85
86
87
88
89
# File 'lib/jwt-multisig.rb', line 83

def remove_jws(jwt, key_id)
  jwt.deep_symbolize_keys.tap do |new_jwt|
    new_jwt[:signatures] = new_jwt.fetch(:signatures, []).reject do |jws|
      jws.fetch(:header).fetch(:kid) == key_id
    end
  end
end

.verify_jws(jws, encoded_payload, public_keychain, options = {}) ⇒ Hash

Verifies JWS.

Parameters:

  • jws (Hash)

    The JWS in the format as defined in RFC 7515. Example:

    { "protected" => "eyJhbGciOiJFUzI1NiJ9",
      "header" => {
        "kid" => "e9bc097a-ce51-4036-9562-d2ade882db0d"
      },
      "signature" => "DtEhU3ljbEg8L38VWAfUAqOyKAM6-Xx-F4GawxaepmXFCgfTjDxw5djxLa8ISlSApmWQxfKTUJqPP3-Kg6NU1Q"
    }
    
  • payload (Hash)
  • public_keychain (Hash)

    The hash which consists of pairs: key ID => public key. The key may be presented as string in PEM format or as instance of OpenSSL::PKey::PKey.

  • options (Hash) (defaults to: {})

    The rules for verifying JWT. The variable «algorithms» is always overwritten by the value from JWS header.

Returns:

  • (Hash)

    Returns payload if signature is valid.

Raises:

  • (JWT::DecodeError)

197
198
199
200
201
202
203
204
205
206
207
# File 'lib/jwt-multisig.rb', line 197

def verify_jws(jws, encoded_payload, public_keychain, options = {})
  proxy_exception JWT::DecodeError do
    encoded_header     = jws.fetch("protected")
    serialized_header  = base64_decode(encoded_header)
    signature          = jws.fetch("signature")
    public_key         = public_keychain.with_indifferent_access.fetch(jws.fetch("header").fetch("kid"))
    jwt                = [encoded_header, encoded_payload, signature].join(".")
    algorithm          = JSON.parse(serialized_header).fetch("alg")
    JWT.decode(jwt, to_pem_or_key(public_key, algorithm), true, options.merge(algorithms: [algorithm])).first
  end
end

.verify_jwt(jwt, public_keychain, options = {}) ⇒ Hash

Verifies JWT.

Parameters:

  • jwt (Hash)

    The JWT in the format as defined in RFC 7515. Example:

    { "payload" => "eyJpc3MiOiJqb2UiLA0KICJleHAiOjEzMDA4MTkzODAsDQogImh0dHA6Ly9leGFtcGxlLmNvbS9pc19yb290Ijp0cnVlfQ",
      "signatures" => [
        { "protected" => "eyJhbGciOiJSUzI1NiJ9",
          "header" => { "kid" => "2010-12-29" },
          "signature" => "cC4hiUPoj9Eetdgtv3hF80EGrhuB__dzERat0XF9g2VtQgr9PJbu3XOiZj5RZmh7AAuHIm4Bh-0Qc_lF5YKt_O8W2Fp5jujGbds9uJdbF9CUAr7t1dnZcAcQjbKBYNX4BAynRFdiuB--f_nZLgrnbyTyWzO75vRK5h6xBArLIARNPvkSjtQBMHlb1L07Qe7K0GarZRmB_eSN9383LcOLn6_dO--xi12jzDwusC-eOkHWEsqtFZESc6BfI7noOPqvhJ1phCnvWh6IeYI2w9QOYEUipUTI8np6LbgGY9Fs98rqVt5AXLIhWkWywlVmtVrBp0igcN_IoypGlUPQGe77Rw"
        },
        { "protected" => "eyJhbGciOiJFUzI1NiJ9",
          "header" => { "kid" => "e9bc097a-ce51-4036-9562-d2ade882db0d" },
          "signature" => "DtEhU3ljbEg8L38VWAfUAqOyKAM6-Xx-F4GawxaepmXFCgfTjDxw5djxLa8ISlSApmWQxfKTUJqPP3-Kg6NU1Q"
        }
      ]
    }
    
  • public_keychain (Hash)

    The hash which consists of pairs: key ID => public key. The key may be presented as string in PEM format or as instance of OpenSSL::PKey::PKey. The implementation only verifies signatures for which public key exists in keychain.

  • options (Hash) (defaults to: {})

    The rules for verifying JWT. The variable «algorithms» is always overwritten by the value from JWS header.

Returns:

  • (Hash)

    The returning value contains payload, list of verified, and unverified signatures (key ID). Example:

    { payload:    { sub: "session", profile: { email: "[email protected]" },
      verified:   [:"backend-1.mycompany.example", :"backend-3.mycompany.example"],
      unverified: [:"backend-2.mycompany.example"] }
    }
    

Raises:

  • (JWT::DecodeError)

123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
# File 'lib/jwt-multisig.rb', line 123

def verify_jwt(jwt, public_keychain, options = {})
  proxy_exception JWT::DecodeError do
    keychain           = public_keychain.with_indifferent_access
    encoded_payload    = jwt.fetch("payload")
    serialized_payload = base64_decode(jwt.fetch("payload"))
    payload            = JSON.parse(serialized_payload)
    verified           = []
    unverified         = []

    jwt.fetch("signatures").each do |jws|
      key_id = jws.fetch("header").fetch("kid")
      if keychain.key?(key_id)
        verify_jws(jws, encoded_payload, public_keychain, options)
        verified << key_id
      else
        unverified << key_id
      end
    end
    { payload:    payload.deep_symbolize_keys,
      verified:   verified.uniq.map(&:to_sym),
      unverified: unverified.uniq.map(&:to_sym) }
  end
end