From c522506bb06ae36492dee4be50b565b25c430c72 Mon Sep 17 00:00:00 2001 From: mo khan Date: Tue, 27 May 2025 09:51:57 -0600 Subject: docs: add an example of public key crypto --- share/man/ENVOY.md | 36 ++++++++++++++++++++++++++++++++++++ 1 file changed, 36 insertions(+) diff --git a/share/man/ENVOY.md b/share/man/ENVOY.md index 907d53e..7db50cd 100644 --- a/share/man/ENVOY.md +++ b/share/man/ENVOY.md @@ -180,6 +180,42 @@ and send that message to me. Only I can decrypt that message using my private key. This ensures confidentiality so that the ciphertext produced can be snooped by anyone but only the recipient can convert the ciphertext back into plaintext. +The following example shows an exchange between two parties. Each party +encrypts a plaintext message with the other party's public key. When that party +receives the ciphertext message they are able to decrypt the message using their +own private key. + +```ruby +#!/bin/env ruby +require 'openssl' + +class Player + attr_reader :name, :public_key + + def initialize(name, private_key = OpenSSL::PKey::RSA.new(2048)) + @name = name + @private_key = private_key + @public_key = private_key.public_key + end + + def send_to(player, plaintext) + ciphertext = player.public_key.public_encrypt(plaintext) + player.receive_from(self, ciphertext) + end + + def receive_from(player, ciphertext) + plaintext = @private_key.private_decrypt(ciphertext) + puts "#{player.name}: #{plaintext}\n" + end +end + +clifford = Player.new("clifford") +reginald = Player.new("reginald") + +clifford.send_to(reginald, "What time is it?") +reginald.send_to(clifford, "Time to go live!") +``` + #### Authenticity To ensure that a message originated from the entity that claims to have sent the -- cgit v1.2.3 From 527ca756cbe44016596fbfa27a090b2f330316bb Mon Sep 17 00:00:00 2001 From: mo khan Date: Tue, 27 May 2025 12:30:45 -0600 Subject: docs: add an example of sending/receiving messages with signatures --- share/man/ENVOY.md | 87 ++++++++++++++++++++++++++++++++++++++++++++++++------ 1 file changed, 78 insertions(+), 9 deletions(-) diff --git a/share/man/ENVOY.md b/share/man/ENVOY.md index 7db50cd..9fb0701 100644 --- a/share/man/ENVOY.md +++ b/share/man/ENVOY.md @@ -189,7 +189,7 @@ own private key. #!/bin/env ruby require 'openssl' -class Player +class Person attr_reader :name, :public_key def initialize(name, private_key = OpenSSL::PKey::RSA.new(2048)) @@ -198,22 +198,22 @@ class Player @public_key = private_key.public_key end - def send_to(player, plaintext) - ciphertext = player.public_key.public_encrypt(plaintext) - player.receive_from(self, ciphertext) + def send_to(person, plaintext) + ciphertext = person.public_key.public_encrypt(plaintext) + person.receive_from(self, ciphertext) end - def receive_from(player, ciphertext) + def receive_from(person, ciphertext) plaintext = @private_key.private_decrypt(ciphertext) - puts "#{player.name}: #{plaintext}\n" + puts "#{person.name}: #{plaintext}\n" end end -clifford = Player.new("clifford") -reginald = Player.new("reginald") +clifford = Person.new("clifford") +reginald = Person.new("reginald") clifford.send_to(reginald, "What time is it?") -reginald.send_to(clifford, "Time to go live!") +reginald.send_to(clifford, "Time to go live! Who sent this?") ``` #### Authenticity @@ -227,6 +227,14 @@ then they can decrypt the signature using the public key of the sender. If signature can be decrypted without an error then we can trust that the message did in fact originate from the sender. This authenticates the message. +In the previous code example each party was able to ensure that the message that +they delivered to the intended recipient could only be read by that recipient. +However, the recipient could not guarantee that they message that they received +actually came from the party that claims to have sent it. If an attacker could +eavesdrop on the conversation, they could intercept the message and rewrite it +before delivering it. This might cause confusion between the two parties and an +attacker could then coerce one of the parties into a specific action. + #### Integrity When a recipient receives a message from a sender the recipient also needs to @@ -235,6 +243,67 @@ an encrypted hash then the recipient can compute a hash of the plaintext message and compare it with the hash in the encrypted signature. This ensures that the message hasn't been tampered with. +In the following code example the two actors perform a public key exchange with +each other before they start to communicate with each other. This allows them to +verify that the message that they receive did in fact originate from the person +that they think it originated from. It also allows them to ensure that the +message hasn't been altered in transit by appending a signature. + +```ruby +#!/bin/env ruby +require 'openssl' + +class Person + attr_reader :name, :public_key + + def initialize(name, private_key = OpenSSL::PKey::RSA.new(2048)) + @name = name + @private_key = private_key + @public_key = private_key.public_key + @friends = {} + end + + def add_friend(friend) + @friends[friend.name] = friend.public_key + end + + def send_to(person, plaintext) + signature = @private_key.private_encrypt(Digest::SHA1.hexdigest(plaintext)) + person.receive([self.name, plaintext, signature]) + end + + def receive(message) + raise "This message cannot be trusted" unless valid?(message) + + name, plaintext, _ = message + puts "#{name}: #{plaintext}\n" + end + + private + + def valid?(message) + header, body, signature = message + public_key = @friends[header] + + # verify that we know the sender + return false if public_key.nil? + + # verify that the message hasn't been altered in transit + Digest::SHA1.hexdigest(body) == public_key.public_decrypt(signature) + end +end + +clifford = Person.new("clifford") +reginald = Person.new("reginald") + +# public key exchange +clifford.add_friend(reginald) +reginald.add_friend(clifford) + +clifford.send_to(reginald, "What time is it?") +reginald.send_to(clifford, "It's still time to go live!") +``` + In order for us to be able to trust JSON Web Tokens we need public/private key pairs that we can use to validate the authenticity and integrity of the token. It is also possible to encrypt the JWT body but this isn't necessary and this is -- cgit v1.2.3 From efcbdfc1349d2333337dac671f9785e0e93cd078 Mon Sep 17 00:00:00 2001 From: mo khan Date: Tue, 27 May 2025 12:38:57 -0600 Subject: docs: describe JWT signature verification --- share/man/ENVOY.md | 66 +++++++++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 65 insertions(+), 1 deletion(-) diff --git a/share/man/ENVOY.md b/share/man/ENVOY.md index 9fb0701..29fbe64 100644 --- a/share/man/ENVOY.md +++ b/share/man/ENVOY.md @@ -247,7 +247,9 @@ In the following code example the two actors perform a public key exchange with each other before they start to communicate with each other. This allows them to verify that the message that they receive did in fact originate from the person that they think it originated from. It also allows them to ensure that the -message hasn't been altered in transit by appending a signature. +message hasn't been altered in transit by appending a signature. The choice of +SHA1 is meant for demonstration purposes only and is not considered a strong +enough hashing algorithm due to the opportunity for collisions to occur. ```ruby #!/bin/env ruby @@ -310,6 +312,68 @@ It is also possible to encrypt the JWT body but this isn't necessary and this is why storing sensitive information like personally identifiable information in a JWT claim is not recommended. +In the Ruby code example above the message that was sent from one person to +another took the form of: + +```plaintext + [name, plaintext, signature] +``` + +This shape is similar to how a JSON Web Token is structured. A JWT takes the +form of: + +```plaintext + header.body.signature +``` + +Where each segment is a base64 encoded JSON. The header provides information +such as the type of signature algorithm that was used and the key id of the +public key that can be used to verify the signature. This key id typically +corresponds to one of the keys that are published through the JSON Web Key Set +(JWKS) URI. For example, the GitLab JWKS can be discovered through the OIDC +Discovery Endpoint. + +```bash +$ curl https://gitlab.com/.well-known/openid-configuration | jq '.' | grep jwks_uri + "jwks_uri": "https://gitlab.com/oauth/discovery/keys", +``` + +The following keys imply that GitLab uses RSA for public key cryptography and +SHA256 as the hash algorithm for verifying digital signatures. + +```bash +$ curl https://gitlab.com/oauth/discovery/keys | jq '.' +{ + "keys": [ + { + "kty": "RSA", + "kid": "kewiQq9jiC84CvSsJYOB-N6A8WFLSV20Mb-y7IlWDSQ", + "e": "AQAB", + "n": "5RyvCSgBoOGNE03CMcJ9Bzo1JDvsU8XgddvRuJtdJAIq5zJ8fiUEGCnMfAZI4of36YXBuBalIycqkgxrRkSOENRUCWN45bf8xsQCcQ8zZxozu0St4w5S-aC7N7UTTarPZTp4BZH8ttUm-VnK4aEdMx9L3Izo0hxaJ135undTuA6gQpK-0nVsm6tRVq4akDe3OhC-7b2h6z7GWJX1SD4sAD3iaq4LZa8y1mvBBz6AIM9co8R-vU1_CduxKQc3KxCnqKALbEKXm0mTGsXha9aNv3pLNRNs_J-cCjBpb1EXAe_7qOURTiIHdv8_sdjcFTJ0OTeLWywuSf7mD0Wpx2LKcD6ImENbyq5IBuR1e2ghnh5Y9H33cuQ0FRni8ikq5W3xP3HSMfwlayhIAJN_WnmbhENRU-m2_hDPiD9JYF2CrQneLkE3kcazSdtarPbg9ZDiydHbKWCV-X7HxxIKEr9N7P1V5HKatF4ZUrG60e3eBnRyccPwmT66i9NYyrcy1_ZNN8D1DY8xh9kflUDy4dSYu4R7AEWxNJWQQov525v0MjD5FNAS03rpk4SuW3Mt7IP73m-_BpmIhW3LZsnmfd8xHRjf0M9veyJD0--ETGmh8t3_CXh3I3R9IbcSEntUl_2lCvc_6B-m8W-t2nZr4wvOq9-iaTQXAn1Au6EaOYWvDRE", + "use": "sig", + "alg": "RS256" + }, + { + "kty": "RSA", + "kid": "4i3sFE7sxqNPOT7FdvcGA1ZVGGI_r-tsDXnEuYT4ZqE", + "e": "AQAB", + "n": "4cxDjTcJRJFID6UCgepPV45T1XDz_cLXSPgMur00WXB4jJrR9bfnZDx6dWqwps2dCw-lD3Fccj2oItwdRQ99In61l48MgiJaITf5JK2c63halNYiNo22_cyBG__nCkDZTZwEfGdfPRXSOWMg1E0pgGc1PoqwOdHZrQVqTcP3vWJt8bDQSOuoZBHSwVzDSjHPY6LmJMEO42H27t3ZkcYtS5crU8j2Yf-UH5U6rrSEyMdrCpc9IXe9WCmWjz5yOQa0r3U7M5OPEKD1-8wuP6_dPw0DyNO_Ei7UerVtsx5XSTd-Z5ujeB3PFVeAdtGxJ23oRNCq2MCOZBa58EGeRDLR7Q", + "use": "sig", + "alg": "RS256" + }, + { + "kty": "RSA", + "kid": "UEtnUohTq58JiJzxHhBLSU0yTpsmW-9EY1Wykha6VIg", + "e": "AQAB", + "n": "9UAG0U59NZ3MBMQkjCVuA8c0ZHEL8SEljnXYYxAuuvy4P79XxTYNodmBAioe1CBsdOmFjjdtXzPIxYv_zEHwkI5WoL1U0r83Q8RSbcl_YSCjfq32TW1hj1KQe0bjzx1TohtnOZSIq-0_8QLbdJrwN7LnBHkalAdMYFk9qUEFlTP-jwUIxztmjpok_-d6W1621iDQwUzYqKiTYc7ZQdC3Bf5jv-8yTm7pMQrR0W6XvPNnRwJmGZdkDH1ZC6okTzLsaMBgMV5awtXJeSZqrR3Qy3ATX6hiitmld9K3FyKFyyIOpaygjltKVzNy5giwnDfTHaGs24Y51Jy1SZ51vqfEFQ", + "use": "sig", + "alg": "RS256" + } + ] +} +``` + + The problem of sharing and distributing public keys is solved using Public Key Infrastructure (PKI). PKI provides a mechanism for distributing X.509 certificates that include metadata and the public keys for different entities. -- cgit v1.2.3 From 6185741dd87541aad997672e2cdecfb6bbdebe7c Mon Sep 17 00:00:00 2001 From: mo khan Date: Tue, 27 May 2025 12:48:51 -0600 Subject: docs: describe the oauth2 filter in detail --- share/man/ENVOY.md | 25 +++++++++++++++++++++++++ 1 file changed, 25 insertions(+) diff --git a/share/man/ENVOY.md b/share/man/ENVOY.md index 29fbe64..5391aa3 100644 --- a/share/man/ENVOY.md +++ b/share/man/ENVOY.md @@ -536,6 +536,26 @@ does not support the OIDC Discovery endpoint but an Envoy Gateway [plugin](https://gateway.envoyproxy.io/docs/tasks/security/oidc/) does. Envoy Gateway is a control plane that is outside the scope of this document. +In the configuration below, the `envoy.filters.http.oauth2` HTTP filter is used +to manage an OAuth handshake with an OAuth Authorization server. By adding the +`openid` scope to the handshake we have implicitly upgraded the transaction from +a generic OAuth2 handshake to an OpenID Connect transaction. This upgrade allows +the OAuth handshake to receive an additional `id_token` from the Security Token +Service (STS) described by the `token_endpoint` configuration. + +The `authorization_endpoint` is the location of the Identity Provider (OIDC +Provider IdP) that this filter will redirect the user-agent to in order to begin a +transaction. The `token_endpoint` is the location that the OAuth client will +forward an OAuth Grant to in order to retrieve an `access_token`, `id_token`, +and `refresh_token`. This HTTP filter takes care of generating a nonce to handle +abusive clients that may wish to try to hit the callback endpoint. It also takes +care of calling the `token_endpoint` with a Refresh Token Grant when it needs to +generate a new `access_token`, `id_token`. This entire exchange is complex and +error prone and by using this filter we reduce the amount of errors that can be +introduced by incorrectly negotiating a new session with the IdP. This ensures +that we use a standards based approach for interoperating with our IdP in the +same manner as any external integration. + ```yaml # ... http_filters: @@ -566,6 +586,11 @@ Envoy Gateway is a control plane that is outside the scope of this document. # ... ``` +The `signout_path` is a virtual path that is managed by Envoy to take care of +clearing session cookies and terminating a session. The `token_endpoint` +configuration will be something that we can utilize to extract the STS code from +the `gitlab-org/gitlab` codebase into a separate isolated service. + ### Authorization Flow TODO:: model these examples from https://gitlab.com/gitlab-org/architecture/auth-architecture/design-doc/-/merge_requests/12#note_2516950269 -- cgit v1.2.3 From 6566ad4cab572685fa01ca3e22fa9ce3ea1663e8 Mon Sep 17 00:00:00 2001 From: mo khan Date: Tue, 27 May 2025 14:11:44 -0600 Subject: docs: add example of verifying a JWT signature --- share/man/ENVOY.md | 121 +++++++++++++++++++++++++++++++++++++++++++++++++---- 1 file changed, 113 insertions(+), 8 deletions(-) diff --git a/share/man/ENVOY.md b/share/man/ENVOY.md index 5391aa3..0ea852c 100644 --- a/share/man/ENVOY.md +++ b/share/man/ENVOY.md @@ -333,6 +333,43 @@ corresponds to one of the keys that are published through the JSON Web Key Set (JWKS) URI. For example, the GitLab JWKS can be discovered through the OIDC Discovery Endpoint. +Here's an example of JWT: + +```plaintext +eyJ0eXAiOiJKV1QiLCJraWQiOiJ0ZDBTbWRKUTRxUGg1cU5Lek0yNjBDWHgyVWgtd2hHLU1Eam9PS1dmdDhFIiwiYWxnIjoiUlMyNTYifQ.eyJpc3MiOiJodHRwOi8vZ2RrLnRlc3Q6MzAwMCIsInN1YiI6IjEiLCJhdWQiOiJlMzFlMWRhMGI4ZjZiNmUzNWNhNzBjNzkwYjEzYzA0MDZlNDRhY2E2YjJiZjY3ZjU1ZGU3MzU1YTk3OWEyMjRmIiwiZXhwIjoxNzQ3OTM3OTgzLCJpYXQiOjE3NDc5Mzc4NjMsImF1dGhfdGltZSI6MTc0Nzc3NDA2Nywic3ViX2xlZ2FjeSI6IjI0NzRjZjBiMjIxMTY4OGE1NzI5N2FjZTBlMjYwYTE1OTQ0NzU0ZDE2YjFiZDQyYzlkNjc3OWM5MDAzNjc4MDciLCJuYW1lIjoiQWRtaW5pc3RyYXRvciIsIm5pY2tuYW1lIjoicm9vdCIsInByZWZlcnJlZF91c2VybmFtZSI6InJvb3QiLCJlbWFpbCI6ImFkbWluQGV4YW1wbGUuY29tIiwiZW1haWxfdmVyaWZpZWQiOnRydWUsInByb2ZpbGUiOiJodHRwOi8vZ2RrLnRlc3Q6MzAwMC9yb290IiwicGljdHVyZSI6Imh0dHBzOi8vd3d3LmdyYXZhdGFyLmNvbS9hdmF0YXIvMjU4ZDhkYzkxNmRiOGNlYTJjYWZiNmMzY2QwY2IwMjQ2ZWZlMDYxNDIxZGJkODNlYzNhMzUwNDI4Y2FiZGE0Zj9zPTgwJmQ9aWRlbnRpY29uIiwiZ3JvdXBzX2RpcmVjdCI6WyJnaXRsYWItb3JnIiwidG9vbGJveCIsIm1hc3NfaW5zZXJ0X2dyb3VwX18wXzEwMCIsImN1c3RvbS1yb2xlcy1yb290LWdyb3VwL2FhIiwiY3VzdG9tLXJvbGVzLXJvb3QtZ3JvdXAvYWEvYWFhIiwiZ251d2dldCIsIkNvbW1pdDQ1MSIsImphc2hrZW5hcyIsImZsaWdodGpzIiwidHdpdHRlciIsImdpdGxhYi1leGFtcGxlcyIsImdpdGxhYi1leGFtcGxlcy9zZWN1cml0eSIsIjQxMjcwOCIsImdpdGxhYi1leGFtcGxlcy9kZW1vLWdyb3VwIiwiY3VzdG9tLXJvbGVzLXJvb3QtZ3JvdXAiLCI0MzQwNDQtZ3JvdXAtMSIsIjQzNDA0NC1ncm91cC0yIiwiZ2l0bGFiLW9yZzEiLCJnaXRsYWItb3JnL3NlY3VyZSIsImdpdGxhYi1vcmcvc2VjdXJlL21hbmFnZXJzIiwiZ2l0bGFiLW9yZy9zZWN1cml0eS1wcm9kdWN0cyIsImdpdGxhYi1vcmcvc2VjdXJpdHktcHJvZHVjdHMvYW5hbHl6ZXJzIl19.TjTrGS5FjfPoY0HWkSLvgjogBxB27jX2beosOZAkwXi_gO3q9DTnL0csOgxjoF1UR8baPNfMFBqL1ipLxBdY9vvDxZve-sOhoSptjzLGkCi7uQKeu7r8wNyFWNWhcLwmbinZyENGSZqIDSkHy0lGdo9oj7qqnH6sYqU46jtWACDGSHTFjNNuo1s_P2SZgkaq4c4v4jdlVV_C_Qlvtl7-eaWV1LzTpB4Mz0VWGsRx1pk3-KnS24crhBjxSE383z4Nar4ZhrsrTK-bOj33l6U32gRKNb4g6GxrPXaRQ268n37spQmbQn0aDwmUOABv-aBRy203bCCZca8BJ0XBur8t6w +``` + +If we break the JWT apart using `.` delimeter it will look like the following: + +```plaintext +header: + + eyJ0eXAiOiJKV1QiLCJraWQiOiJ0ZDBTbWRKUTRxUGg1cU5Lek0yNjBDWHgyVWgtd2hHLU1Eam9PS1dmdDhFIiwiYWxnIjoiUlMyNTYifQ + +body: + + eyJpc3MiOiJodHRwOi8vZ2RrLnRlc3Q6MzAwMCIsInN1YiI6IjEiLCJhdWQiOiJlMzFlMWRhMGI4ZjZiNmUzNWNhNzBjNzkwYjEzYzA0MDZlNDRhY2E2YjJiZjY3ZjU1ZGU3MzU1YTk3OWEyMjRmIiwiZXhwIjoxNzQ3OTM3OTgzLCJpYXQiOjE3NDc5Mzc4NjMsImF1dGhfdGltZSI6MTc0Nzc3NDA2Nywic3ViX2xlZ2FjeSI6IjI0NzRjZjBiMjIxMTY4OGE1NzI5N2FjZTBlMjYwYTE1OTQ0NzU0ZDE2YjFiZDQyYzlkNjc3OWM5MDAzNjc4MDciLCJuYW1lIjoiQWRtaW5pc3RyYXRvciIsIm5pY2tuYW1lIjoicm9vdCIsInByZWZlcnJlZF91c2VybmFtZSI6InJvb3QiLCJlbWFpbCI6ImFkbWluQGV4YW1wbGUuY29tIiwiZW1haWxfdmVyaWZpZWQiOnRydWUsInByb2ZpbGUiOiJodHRwOi8vZ2RrLnRlc3Q6MzAwMC9yb290IiwicGljdHVyZSI6Imh0dHBzOi8vd3d3LmdyYXZhdGFyLmNvbS9hdmF0YXIvMjU4ZDhkYzkxNmRiOGNlYTJjYWZiNmMzY2QwY2IwMjQ2ZWZlMDYxNDIxZGJkODNlYzNhMzUwNDI4Y2FiZGE0Zj9zPTgwJmQ9aWRlbnRpY29uIiwiZ3JvdXBzX2RpcmVjdCI6WyJnaXRsYWItb3JnIiwidG9vbGJveCIsIm1hc3NfaW5zZXJ0X2dyb3VwX18wXzEwMCIsImN1c3RvbS1yb2xlcy1yb290LWdyb3VwL2FhIiwiY3VzdG9tLXJvbGVzLXJvb3QtZ3JvdXAvYWEvYWFhIiwiZ251d2dldCIsIkNvbW1pdDQ1MSIsImphc2hrZW5hcyIsImZsaWdodGpzIiwidHdpdHRlciIsImdpdGxhYi1leGFtcGxlcyIsImdpdGxhYi1leGFtcGxlcy9zZWN1cml0eSIsIjQxMjcwOCIsImdpdGxhYi1leGFtcGxlcy9kZW1vLWdyb3VwIiwiY3VzdG9tLXJvbGVzLXJvb3QtZ3JvdXAiLCI0MzQwNDQtZ3JvdXAtMSIsIjQzNDA0NC1ncm91cC0yIiwiZ2l0bGFiLW9yZzEiLCJnaXRsYWItb3JnL3NlY3VyZSIsImdpdGxhYi1vcmcvc2VjdXJlL21hbmFnZXJzIiwiZ2l0bGFiLW9yZy9zZWN1cml0eS1wcm9kdWN0cyIsImdpdGxhYi1vcmcvc2VjdXJpdHktcHJvZHVjdHMvYW5hbHl6ZXJzIl19 + +signature: + + TjTrGS5FjfPoY0HWkSLvgjogBxB27jX2beosOZAkwXi_gO3q9DTnL0csOgxjoF1UR8baPNfMFBqL1ipLxBdY9vvDxZve-sOhoSptjzLGkCi7uQKeu7r8wNyFWNWhcLwmbinZyENGSZqIDSkHy0lGdo9oj7qqnH6sYqU46jtWACDGSHTFjNNuo1s_P2SZgkaq4c4v4jdlVV_C_Qlvtl7-eaWV1LzTpB4Mz0VWGsRx1pk3-KnS24crhBjxSE383z4Nar4ZhrsrTK-bOj33l6U32gRKNb4g6GxrPXaRQ268n37spQmbQn0aDwmUOABv-aBRy203bCCZca8BJ0XBur8t6w +``` + +When we Base64 decode the header it takes the following form. This tells us that +the signature was produced using an "RS256" algorithm which is a short hand for +RSA public key cryptography with a SHA256 hash. The identifier for the public +key that can be used to decrypt the signature is marked by the `kid` name. This +`kid` will correspond to an identifer that can be discovered at the JWKS +metadata endpoint. + +```plaintext +{ + "typ": "JWT", + "kid": "td0SmdJQ4qPh5qNKzM260CXx2Uh-whG-MDjoOKWft8E", + "alg": "RS256" +} +``` + ```bash $ curl https://gitlab.com/.well-known/openid-configuration | jq '.' | grep jwks_uri "jwks_uri": "https://gitlab.com/oauth/discovery/keys", @@ -342,30 +379,25 @@ The following keys imply that GitLab uses RSA for public key cryptography and SHA256 as the hash algorithm for verifying digital signatures. ```bash +# note that I have ommitted some data to keep the example brief $ curl https://gitlab.com/oauth/discovery/keys | jq '.' { "keys": [ { "kty": "RSA", "kid": "kewiQq9jiC84CvSsJYOB-N6A8WFLSV20Mb-y7IlWDSQ", - "e": "AQAB", - "n": "5RyvCSgBoOGNE03CMcJ9Bzo1JDvsU8XgddvRuJtdJAIq5zJ8fiUEGCnMfAZI4of36YXBuBalIycqkgxrRkSOENRUCWN45bf8xsQCcQ8zZxozu0St4w5S-aC7N7UTTarPZTp4BZH8ttUm-VnK4aEdMx9L3Izo0hxaJ135undTuA6gQpK-0nVsm6tRVq4akDe3OhC-7b2h6z7GWJX1SD4sAD3iaq4LZa8y1mvBBz6AIM9co8R-vU1_CduxKQc3KxCnqKALbEKXm0mTGsXha9aNv3pLNRNs_J-cCjBpb1EXAe_7qOURTiIHdv8_sdjcFTJ0OTeLWywuSf7mD0Wpx2LKcD6ImENbyq5IBuR1e2ghnh5Y9H33cuQ0FRni8ikq5W3xP3HSMfwlayhIAJN_WnmbhENRU-m2_hDPiD9JYF2CrQneLkE3kcazSdtarPbg9ZDiydHbKWCV-X7HxxIKEr9N7P1V5HKatF4ZUrG60e3eBnRyccPwmT66i9NYyrcy1_ZNN8D1DY8xh9kflUDy4dSYu4R7AEWxNJWQQov525v0MjD5FNAS03rpk4SuW3Mt7IP73m-_BpmIhW3LZsnmfd8xHRjf0M9veyJD0--ETGmh8t3_CXh3I3R9IbcSEntUl_2lCvc_6B-m8W-t2nZr4wvOq9-iaTQXAn1Au6EaOYWvDRE", "use": "sig", "alg": "RS256" }, { "kty": "RSA", "kid": "4i3sFE7sxqNPOT7FdvcGA1ZVGGI_r-tsDXnEuYT4ZqE", - "e": "AQAB", - "n": "4cxDjTcJRJFID6UCgepPV45T1XDz_cLXSPgMur00WXB4jJrR9bfnZDx6dWqwps2dCw-lD3Fccj2oItwdRQ99In61l48MgiJaITf5JK2c63halNYiNo22_cyBG__nCkDZTZwEfGdfPRXSOWMg1E0pgGc1PoqwOdHZrQVqTcP3vWJt8bDQSOuoZBHSwVzDSjHPY6LmJMEO42H27t3ZkcYtS5crU8j2Yf-UH5U6rrSEyMdrCpc9IXe9WCmWjz5yOQa0r3U7M5OPEKD1-8wuP6_dPw0DyNO_Ei7UerVtsx5XSTd-Z5ujeB3PFVeAdtGxJ23oRNCq2MCOZBa58EGeRDLR7Q", "use": "sig", "alg": "RS256" }, { "kty": "RSA", "kid": "UEtnUohTq58JiJzxHhBLSU0yTpsmW-9EY1Wykha6VIg", - "e": "AQAB", - "n": "9UAG0U59NZ3MBMQkjCVuA8c0ZHEL8SEljnXYYxAuuvy4P79XxTYNodmBAioe1CBsdOmFjjdtXzPIxYv_zEHwkI5WoL1U0r83Q8RSbcl_YSCjfq32TW1hj1KQe0bjzx1TohtnOZSIq-0_8QLbdJrwN7LnBHkalAdMYFk9qUEFlTP-jwUIxztmjpok_-d6W1621iDQwUzYqKiTYc7ZQdC3Bf5jv-8yTm7pMQrR0W6XvPNnRwJmGZdkDH1ZC6okTzLsaMBgMV5awtXJeSZqrR3Qy3ATX6hiitmld9K3FyKFyyIOpaygjltKVzNy5giwnDfTHaGs24Y51Jy1SZ51vqfEFQ", "use": "sig", "alg": "RS256" } @@ -373,6 +405,72 @@ $ curl https://gitlab.com/oauth/discovery/keys | jq '.' } ``` +When the body of the JWT is decoded it takes the following form: + +```bash +{ + "iss": "http://gdk.test:3000", + "sub": "1", + "aud": "e31e1da0b8f6b6e35ca70c790b13c0406e44aca6b2bf67f55de7355a979a224f", + "exp": 1747937983, + "iat": 1747937863, + "auth_time": 1747774067, + "sub_legacy": "2474cf0b2211688a57297ace0e260a15944754d16b1bd42c9d6779c900367807", + "name": "Administrator", + "nickname": "root", + "preferred_username": "root", + "email": "admin@example.com", + "email_verified": true, + "profile": "http://gdk.test:3000/root", + "picture": "https://www.gravatar.com/avatar/258d8dc916db8cea2cafb6c3cd0cb0246efe061421dbd83ec3a350428cabda4f?s=80&d=identicon", + "groups_direct": [ + "gitlab-org" + ] +} +``` + +There are several non-standard claims in this token such as the `auth_time`, +`sub_legacy`, `name`, `nickname`, `preferred_username`, `email`, +`email_verified`, `profile`, `picture`, `groups_direct`. I think that `email` is +problematic because this is considered personally identifiable information and +this is something that I would like us to consider removing. + +Finally, we can validate the integrity of the JWT by decrypting the signature +and recomputing a hash of the header + "." + body. The following ruby code +demonstrates this: + +```ruby +#!/usr/bin/env ruby +require 'openssl' +require 'base64' + +# This key is fetched from the `jwks_uri` +metadata = { + kty: "RSA", + kid: "td0SmdJQ4qPh5qNKzM260CXx2Uh-whG-MDjoOKWft8E", + e: "AQAB", + n: "z4JrfdkUjeCPcMQEB1ai9OJbZ8xMrtdNI9K80XUYTcyfkQDlFnZNgRvwnkLkZJ0XjtLbc6Y0RMEyo32DivIfWb31US_1FRRJm0oS2mSFV4iHsfTXjVnlmExYW0ke2_BZ4Vu_rRIVxD1eJYNLjn8Uqb7ZllnUJFZDzTk5qQCVX9F5idQgWFh9DxtY3pGutz1-BxaQmTDts_p4cDu8HPnmJEiTCsx7opIfvqpaumfuiLlPZvozERnsnC8BDS1EQja3nJhOnaBFV6vrk57VH_IwmybVACk2w3uW8n0o63roDHfnpo5hQuSm2M-5mEcyXH0PA5YsDuYRi1uxF58Vob6NSw", + use: "sig", + alg: "RS256" +} + +jwt = "eyJ0eXAiOiJKV1QiLCJraWQiOiJ0ZDBTbWRKUTRxUGg1cU5Lek0yNjBDWHgyVWgtd2hHLU1Eam9PS1dmdDhFIiwiYWxnIjoiUlMyNTYifQ.eyJpc3MiOiJodHRwOi8vZ2RrLnRlc3Q6MzAwMCIsInN1YiI6IjEiLCJhdWQiOiJlMzFlMWRhMGI4ZjZiNmUzNWNhNzBjNzkwYjEzYzA0MDZlNDRhY2E2YjJiZjY3ZjU1ZGU3MzU1YTk3OWEyMjRmIiwiZXhwIjoxNzQ3OTM3OTgzLCJpYXQiOjE3NDc5Mzc4NjMsImF1dGhfdGltZSI6MTc0Nzc3NDA2Nywic3ViX2xlZ2FjeSI6IjI0NzRjZjBiMjIxMTY4OGE1NzI5N2FjZTBlMjYwYTE1OTQ0NzU0ZDE2YjFiZDQyYzlkNjc3OWM5MDAzNjc4MDciLCJuYW1lIjoiQWRtaW5pc3RyYXRvciIsIm5pY2tuYW1lIjoicm9vdCIsInByZWZlcnJlZF91c2VybmFtZSI6InJvb3QiLCJlbWFpbCI6ImFkbWluQGV4YW1wbGUuY29tIiwiZW1haWxfdmVyaWZpZWQiOnRydWUsInByb2ZpbGUiOiJodHRwOi8vZ2RrLnRlc3Q6MzAwMC9yb290IiwicGljdHVyZSI6Imh0dHBzOi8vd3d3LmdyYXZhdGFyLmNvbS9hdmF0YXIvMjU4ZDhkYzkxNmRiOGNlYTJjYWZiNmMzY2QwY2IwMjQ2ZWZlMDYxNDIxZGJkODNlYzNhMzUwNDI4Y2FiZGE0Zj9zPTgwJmQ9aWRlbnRpY29uIiwiZ3JvdXBzX2RpcmVjdCI6WyJnaXRsYWItb3JnIiwidG9vbGJveCIsIm1hc3NfaW5zZXJ0X2dyb3VwX18wXzEwMCIsImN1c3RvbS1yb2xlcy1yb290LWdyb3VwL2FhIiwiY3VzdG9tLXJvbGVzLXJvb3QtZ3JvdXAvYWEvYWFhIiwiZ251d2dldCIsIkNvbW1pdDQ1MSIsImphc2hrZW5hcyIsImZsaWdodGpzIiwidHdpdHRlciIsImdpdGxhYi1leGFtcGxlcyIsImdpdGxhYi1leGFtcGxlcy9zZWN1cml0eSIsIjQxMjcwOCIsImdpdGxhYi1leGFtcGxlcy9kZW1vLWdyb3VwIiwiY3VzdG9tLXJvbGVzLXJvb3QtZ3JvdXAiLCI0MzQwNDQtZ3JvdXAtMSIsIjQzNDA0NC1ncm91cC0yIiwiZ2l0bGFiLW9yZzEiLCJnaXRsYWItb3JnL3NlY3VyZSIsImdpdGxhYi1vcmcvc2VjdXJlL21hbmFnZXJzIiwiZ2l0bGFiLW9yZy9zZWN1cml0eS1wcm9kdWN0cyIsImdpdGxhYi1vcmcvc2VjdXJpdHktcHJvZHVjdHMvYW5hbHl6ZXJzIl19.TjTrGS5FjfPoY0HWkSLvgjogBxB27jX2beosOZAkwXi_gO3q9DTnL0csOgxjoF1UR8baPNfMFBqL1ipLxBdY9vvDxZve-sOhoSptjzLGkCi7uQKeu7r8wNyFWNWhcLwmbinZyENGSZqIDSkHy0lGdo9oj7qqnH6sYqU46jtWACDGSHTFjNNuo1s_P2SZgkaq4c4v4jdlVV_C_Qlvtl7-eaWV1LzTpB4Mz0VWGsRx1pk3-KnS24crhBjxSE383z4Nar4ZhrsrTK-bOj33l6U32gRKNb4g6GxrPXaRQ268n37spQmbQn0aDwmUOABv-aBRy203bCCZca8BJ0XBur8t6w" +header, body, signature = jwt.split(".") + +n = OpenSSL::BN.new(Base64.urlsafe_decode64(metadata[:n]), 2) +e = OpenSSL::BN.new(Base64.urlsafe_decode64(metadata[:e]), 2) +public_key = OpenSSL::PKey::RSA.new(OpenSSL::ASN1::Sequence.new([ + OpenSSL::ASN1::Integer.new(n), + OpenSSL::ASN1::Integer.new(e) +]).to_der) + +puts public_key.verify( + OpenSSL::Digest::SHA256.new, + Base64.urlsafe_decode64(signature), + [header, body].join(".") +) +``` + The problem of sharing and distributing public keys is solved using Public Key Infrastructure (PKI). PKI provides a mechanism for distributing X.509 @@ -400,7 +498,9 @@ assumptions about user authentication is valid. The `id_token` in the OpenID Connect (OIDC) workflow represents the authentication context. This _DOES NOT_ represent an authorization context. -TODO:: Describe the sections of a JWT and the schema of the `id_token`. +OpenID Core specification describes the `id_token` as a JWT and the JWT +specification describes a set of standard claims that are found in the +JWT body. ### Authorization @@ -427,7 +527,10 @@ used to make authorization decisions. Authorization decisions should be made using the `access_token` because this represents the delegated authorization access granted to the service that the token was minted for. In general, most API's that receive a request should make authorization decisions based on the -privileges granted to the `access_token`. +privileges granted to the `access_token`. OAuth does not specify the format of +the `access_token` so this is up to the OAuth Authorization Server to decide the +schema. It is possible for us to adopt the JWT standard for `access_token` +representation. The authorization server that generates the `access_token` should only grant just enough privileges to this token that is required by the service (OAuth @@ -860,6 +963,8 @@ authentication/authorization strategy that GitLab as a whole supports. ## References * [Envoy Proxy](https://www.envoyproxy.io/) +* [OpenID Core Specificatioin](https://openid.net/specs/openid-connect-core-1_0-final.html#IDToken) +* [RFC-7519: JSON Web Token (JWT)](https://datatracker.ietf.org/doc/html/rfc7519) * [`envoy.filters.http.oauth2`](https://www.envoyproxy.io/docs/envoy/latest/configuration/http/http_filters/oauth2_filter.html) * [`envoy.filters.http.jwt_authn`](https://www.envoyproxy.io/docs/envoy/latest/api-v3/extensions/filters/http/jwt_authn/v3/config.proto) * [`envoy.filters.http.ext_authz`](https://www.envoyproxy.io/docs/envoy/latest/api-v3/extensions/filters/http/ext_authz/v3/ext_authz.proto) -- cgit v1.2.3 From a93660bc07e9534733b87fd58cafe853421e0f5f Mon Sep 17 00:00:00 2001 From: mo khan Date: Tue, 27 May 2025 14:15:16 -0600 Subject: docs: re-organize to place emphasis on the final sentence of the section --- share/man/ENVOY.md | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/share/man/ENVOY.md b/share/man/ENVOY.md index 0ea852c..7ad8b64 100644 --- a/share/man/ENVOY.md +++ b/share/man/ENVOY.md @@ -495,12 +495,10 @@ Provider, OIDC Relaying Party) depends on an exchange of public key information ahead of time (AoT). Without this pre-prequisite, none of the downstream assumptions about user authentication is valid. -The `id_token` in the OpenID Connect (OIDC) workflow represents the authentication context. -This _DOES NOT_ represent an authorization context. - -OpenID Core specification describes the `id_token` as a JWT and the JWT +The OpenID Core specification describes the `id_token` as a JWT and the JWT specification describes a set of standard claims that are found in the -JWT body. +JWT body. The `id_token` in the OpenID Connect (OIDC) workflow represents the authentication context. +This _DOES NOT_ represent an authorization context. ### Authorization -- cgit v1.2.3 From c228cdf135762b11ee486d8f2801930a97a0f828 Mon Sep 17 00:00:00 2001 From: mo khan Date: Tue, 27 May 2025 14:30:58 -0600 Subject: chore: add retry policy for oauth2 http filter --- etc/envoy/envoy.yaml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/etc/envoy/envoy.yaml b/etc/envoy/envoy.yaml index 39a49e9..b18a0ac 100644 --- a/etc/envoy/envoy.yaml +++ b/etc/envoy/envoy.yaml @@ -158,6 +158,8 @@ static_resources: path: exact: /callback redirect_uri: "%REQ(x-forwarded-proto)%://%REQ(:authority)%/callback" + retry_policy: + num_retries: 3 signout_path: path: exact: /signout -- cgit v1.2.3