Open brownoxford opened 9 months ago
Currently there is not direct support, although I could work on an implementation. Are you using 3.x or 4.x ?
I’m using 4.2. It appears that gdscript just doesn’t expose the necessary crypto though ☹️
In my use case, all I get from the jwks is modulo and exponent, and I think the asn1 related libs would be needed go convert that to PEM.
On Sat, Dec 2, 2023 at 06:01 Nicolò Santilio @.***> wrote:
Currently there is not direct support, although I could work on an implementation. Are you using 3.x or 4.x ?
— Reply to this email directly, view it on GitHub https://github.com/fenix-hub/godot-engine.jwt/issues/18#issuecomment-1837120477, or unsubscribe https://github.com/notifications/unsubscribe-auth/AAFYMCLAXV3EBFM6SHTX2A3YHMDARAVCNFSM6AAAAABADXPJZ6VHI2DSMVQWIX3LMV43OSLTON2WKQ3PNVWWK3TUHMYTQMZXGEZDANBXG4 . You are receiving this because you authored the thread.Message ID: @.***>
Ehi @brownoxford Yeah I was just attempting to implement the JWKS verification in the addon but we run up against the same issue. The best my addon can do is calculate the hex value of the modulus and exponent of the matching kid, but then a GDExtesion (or calling openssl from Godot engine maybe?) is required to actually calculate the RSA key.
I think I'm on to something - I'll post code when I have it.
Okay, here is what I came up with to generate an RSA public key in PEM (PKCS#1). In the end, all I had to do was properly do the ASN.1 encoding. I'm including an attached file with all of the code I generated, including the stuff I ended up not using.
This is very specific to my use case, not necessarily ready for general use, which is why I'm attaching it and not creating a PR. I believe this will be helpful if you do end up implementing this functionality.
extends RefCounted
## https://lapo.it/asn1js
## https://letsencrypt.org/docs/a-warm-welcome-to-asn1-and-der/
func der_encode_rsa_public_key(n: PackedByteArray, e: PackedByteArray) -> PackedByteArray:
# Build the DER-encoded RSA public key
var der_encoding: PackedByteArray = PackedByteArray()
# Both modulus and exponent are positive numbers, but must be represented
# in two's complement before encoding as integers.
if (n[0] & 0x80):
n.insert(0, 0x00)
if (e[0] & 0x80):
e.insert(0, 0x00)
# PKCS#1
der_encoding = encode_sequence([
encode_integer(n),
encode_integer(e),
])
return der_encoding
func der_to_pem(der_encoding: PackedByteArray, key_type: String = "RSA PUBLIC KEY") -> String:
var base64_encoded = Marshalls.raw_to_base64(der_encoding)
# Break the base64-encoded string into lines for better readability
var pem_lines = []
while base64_encoded.length() > 0:
var chunk = base64_encoded.left(64)
pem_lines.append(chunk)
base64_encoded = base64_encoded.substr(64, base64_encoded.length() - 64)
# Construct the PEM-encoded key
var pem_encoding = "-----BEGIN " + key_type + "-----\n"
pem_encoding += "\n".join(pem_lines)
pem_encoding += "\n-----END " + key_type + "-----\n"
return pem_encoding
func encode_integer(value: PackedByteArray) -> PackedByteArray:
# Encode an integer in DER format
var encoding: PackedByteArray = PackedByteArray()
encoding.append(0x02) # INTEGER tag
encoding += encode_length(value.size())
encoding += value
return encoding
func encode_sequence(values: Array[PackedByteArray]) -> PackedByteArray:
var concatenated: PackedByteArray = PackedByteArray()
for value in values:
concatenated += value
# Encode a sequence in DER format
var encoding: PackedByteArray = PackedByteArray()
encoding.append(0x30) # SEQUENCE tag
encoding += encode_length(concatenated.size())
encoding += concatenated
return encoding
func encode_length(length: int) -> PackedByteArray:
# Encode the length in DER format
var encoding: PackedByteArray = PackedByteArray()
if length < 128:
encoding.append(length)
else:
var length_bytes: PackedByteArray = PackedByteArray()
while length > 0:
length_bytes.insert(0, length % 256)
length /= 256
encoding.append(0x80 | length_bytes.size())
encoding += length_bytes
return encoding
var asn1 = load("res://oauth2/asn1.gd").new()
## Extract RS256 public key from JWKS
func _extract_public_key(jwks: Dictionary, kid: String) -> String:
# TODO: This should be more robust, in case fields are missing
var keys = jwks["keys"]
for key in keys:
if key["kid"] != kid or key["kty"] != "RSA":
continue
var modulus = JWTUtils.base64URL_decode(key["n"])
var exponent = JWTUtils.base64URL_decode(key["e"])
var key_der = asn1.der_encode_rsa_public_key(modulus, exponent)
var key_pem = asn1.der_to_pem(key_der)
return key_pem
return ""
var jwks = "<a json web keyset>"
var kid_to_extract = "JWT-Signature-Key"
var public_key_pem = _extract_public_key(jwks, kid_to_extract)
if public_key_pem.is_empty():
print("could not extract key")
var public_key: CryptoKey = CryptoKey.new()
var err = public_key.load_from_string(public_key_pem, true)
if err != OK:
print("unable to load cryptokey from pem string: %s" % error_string(err))
var jwt_algorithm: JWTAlgorithm = JWTAlgorithmBuilder.RSA256(public_key)
var jwt_verifier: JWTVerifier = JWT.require(jwt_algorithm) \
.accept_expire_at(Time.get_unix_time_from_system()) \
.accept_issued_at(Time.get_unix_time_from_system()) \
.build() # Reusable Verifier
Looks legit, I'll make sure to adapt it to the library in the hope that Crypto class will eventually expose some utility functions to generate certificates from modulus and exponent. Thank you very much for your time.
Feature request
I'm writing an app that uses OAuth 2.0 for user login, and I'd like to verify the JWT tokens returned by the authentication server, but there doesn't seem to be a way to validate signatures using JWKS... am I missing something?