Azure Confidential Computing: Verifying Microsoft Azure Attestation JWT tokens

📣 Dive into the code samples from this blog on GitHub!

A few weeks ago I got another excellent question from a community member, who had run into an issue while trying to validate a Confidential Virtual Machine’s (CVM) Guest attestation token.

“We are using AMD SEV confidential VMs and are trying to validate a guest attestation token, which was signed by Microsoft Azure Attestation. For debugging purposes, we’re using JWT.io to perform the validation, but it doesn’t seem to work. Any ideas?”

We managed to identify and fix the issue rather quickly! I figured that there may be other people who will bump into the same issue, so I decided to write down my reasoning in a couple of paragraphs.

Guest attestation 101

You might be wondering: “What is this guest attestation business and how does it relate to Azure Confidential Computing”?

I’m glad you asked. If you haven’t heard of this concept before, I will try to distil it for you in this section.

The concept of attestation is one of the foundational elements of Azure Confidential Computing, so it’s quite an important concept to learn about. Attestation helps us to cryptographically assess that a Trusted Execution Environment (the operation system running inside of an AMD SEV-SNP powered CVM) is running in the intended operating state. It is the process by which one party, the verifier, assesses the trustworthiness of a potentially untrusted peer, the attester.

Perhaps this sounds a little vague, so let me give you an example. By using the process of attestation, we can cryptographically assess that our sensitive software workload is running on a genuine Azure confidential virtual machine, running AMD SEV-SNP. To achieve this, you can use the shared Microsoft Azure Attestation (MAA) endpoint, or you can deploy your instance of MAA, and create a custom attestation policy that only returns an attestation token after the evidence has been processed according to your own rules.

Anyway, Microsoft has open sourced a Windows and Linux client binary that does the heavy lifting for you, so we get more easily integrate the guest attestation process into our existing workloads.

The output of both the Guest Attestation client and the Microsoft Azure Attestation service to which it sends requests is simply a string encoded in base64url format.

eyJ<something>.eyJ<something>.<signature>

There are a couple of sections tucked inside the result, delimited by a dot (.). Since these are base64url-encoded, we can base64url-decode these individual strings! These values are actually what make up a signed JSON Web Token (JWT), more specifically these three pieces:

  • A JSON Object Signing and Encryption (JOSE) header
  • A JSON Web Signature (JWS) payload, a.k.a. a set of claims.
  • A JWS signature

💡 If you’d like to learn a little more about Guest attestation in more detail, feel free to check out my post on ACC: Secure Key Release.

About those JSON Web Signatures

Let’s turn to digital signatures for a brief moment, as they are an important concept in the world of cybersecurity. According to the Cybersecurity and Infrastructure Security Agency (CISA), a digital signature is defined as:

"… a mathematical algorithm routinely used to validate the authenticity and integrity of a message."

It’s reasonable to assume that a digital signature provides us with enough confidence that a message was sent by the intended sender (authenticity) and that it has not been tampered with in transit (integrity).

🤔 Fair enough, but that still leaves us with a few questions. Is there a way for us to perform the validation ourselves? What is the algorithm that was used to generate the signature attached to the attestation token? It is a custom-made algorithm that is kept under lock and key by Microsoft or could it be publically available?

Before we dive into the details of how to validate a token attested by the Azure Attestation service, let’s first take a closer look at the algorithm used to generate the signature that is attached to the token. This algorithm is essential to understand, as it is what allows us to validate the token and ensure its authenticity.

To facilitate the exchange of tokens with the Microsoft Azure Attestation service, Microsoft leverages the built-in mechanisms that are already part of the JSON Web Token (JWT) specification, and its corresponding JSON Web Signatures (JWS) specification. The Cryptographic algorithms and identifiers that you can use with the JWS specification are described in a separate specification named the JSON Web Algorithms (JWA) specification.

💡 You might already be familiar with JSON Web tokens, as this isn’t unique to Microsoft or Azure Confidential Computing. Chances are that most of the access tokens to your favourite online services, that you have stored in your web browser, are kept in some form of JSON Web Token.

The details of all of these specifications are described in their respective RFC documents, here is a list should you wish to consult them.

💡 An RFC document is simply a “publication in a series from the principal technical development and standards-setting bodies for the Internet”.

Making sense of it all

Before we take a look at the attestation token, let’s try to gain some understanding of what we’re working with by taking a look at a simple JWT as an example.

eyJhbGciOiJSUzI1NiJ9.eyJpc3MiOiJqb2UiLCJleHAiOjEzMDA4MTkzODAsImh0dHA6Ly9leGFtcGxlLmNvbS9pc19yb290Ijp0cnVlfQ.lgd2LOxR1jOAHoKxEi8fU5qi6aoq_jSDWfyHS6w_py-uAe75ExZ2CkKnggWOcrp6DynQMoeNGPKGKC-j5HzQcot4w4lo-keU8g2alMXeqSHwmA-MKatPow7Cf09_A6CEbRu_VNBrrG0uGqYFpPMbzeqEd4oI4FU9S7cgomYp8UkCoIWZRzczbhDa6s_vuYhk0DTzX_sZmAYSovFXnaT2jE_USjk53yAsNIkdQ6pP0aUcj7UtssBGc9Wt815tbX7YffoKlKjbrxfq0GdM1zkLRWLv0KdZAfrb6WGDKY0P_xJ8ScTknH9zRUITt1eqKJFcvxcexdywaoSRYvua8g5YwJUbsoNdkF-Ck1zsLgDQpC0PAyyJlOt0mcwRue5WNCn-SIYuZ6hT70oGmhr-fImfRk6uk2aNamfAbrKRtqYp3FsNH3yJjnCULi32SItA4jFikDA_pBS3J2sWpJV4jV9G05jEY5bNuFR9x71BpG4a0ANP96svG9XcLYEPx1C6J04Fog5qkWbKfpBSdQcWqaQIC5ZUXUDE5Ha3XjI4ZQHl_iHP4vPGOVij6Fp0U92F_cASwl2-UJRQaBtINFe3kBG_ri-yALJeWbFXmhVH3dJBZvKbNDa5kQz-ttfX_KBPQcw4CCWZseAh-pFCnmRawofVB6PALyK30UoNybFF3EU43RY

Remember, there are a couple of sections tucked inside the token, delimited by a dot (.) . We should take a look and see what secrets each section holds, by base64-url decoding the string’s sections. Let’s begin by looking at the first section, the JOSE header; by decoding it, we can determine which algorithm was used and consult the list of defined alg values in the JSON Web Algorithms RFC to understand how it works.

{
    "alg": "RS256"
}

This tells us that the header contains metadata about the token, including the alg value, which identifies the cryptographic algorithm used to secure the JWS. It seems that for us to know which algorithm was used, we will need to consult that list of defined alg values, specifically the ones used for JSON Web Signatures, in the JSON Web Algorithms RFC. You can find the list under section 3, “Cryptographic Algorithms for Digital Signatures and MACs” of RFC 7518.

For convenience’s sake, however, I’ve gone ahead and also listed them below:

alg valueDigital Signature or MAC algorithmImplementation Requirements
HS256HMAC using SHA-256Required
HS384HMAC using SHA-384Optional
HS521HMAC using SHA-512Optional
RS256RSASSA-PKCS1-v1_5 using SHA-256Recommended
RS384RSASSA-PKCS1-v1_5 using SHA-384Optional
RS512RSASSA-PKCS1-v1_5 using SHA-512Optional
ES256ECDSA using P-256 using SHA-256Recommended+
ES384ECDSA using P-384 using SHA-384Optional
ES512ECDSA using P-512 using SHA-512Optional
PS256RSASSA-PSS using SHA-256 and MGF1 with SHA-256Optional
PS384RSASSA-PSS using SHA-384 and MGF1 with SHA-384Optional
PS512RSASSA-PSS using SHA-512 and MGF1 with SHA-512Optional
noneNo digital signature or MAC performedOptional

As you can tell, we’ve determined which specific algorithm was used to create the digital signature. The value for alg is RS256 and corresponds to “RSASSA-PKCS1-v1_5 using SHA-256”. In case we need information on how to create and verify the digital signature, we’re in luck. The RFC for JSON Web Signatures comes with a few examples of how to perform the encoding and validation process for all of the algorithms, including the one we’re after.

💡 Keep in mind: Digital signatures combine two cryptographic techniques; hashing and asymmetric cryptography. Asymmetric Encryption is typically a slow process and makes use of a public and private key. This is different from symmetric encryption, which has just one key but is typically considered a faster process.

Creating the digital signature using RSASSA-PKCS1-v1_5 SHA-256 is pretty straight-forward, you essentially concatenate two strings, like so:

function Base64UrlEncode([string]$text){
  [Convert]::ToBase64String([System.Text.UTF8Encoding]::UTF8.GetBytes($text)).Replace('+', '-').Replace('/', '_').Split("=")[0]
}

$jwsHeader = '{"alg":"RS256"}'
$jwsPayload = '{"iss":"joe","exp":1300819380,"http://example.com/is_root":true}'

$JwsResult = "{0}.{1}" -f (Base64UrlEncode($jwsHeader)), (Base64UrlEncode($jwsPayload))

The temporary result, for example, then becomes the following string:

$JwsResult
# eyJhbGciOiJSUzI1NiJ9.eyJpc3MiOiJqb2UiLCJleHAiOjEzMDA4MTkzODAsImh0dHA6Ly9leGFtcGxlLmNvbS9pc19yb290Ijp0cnVlfQ

At this point, the sender signs this string using “an RSA signing function”. Typically, that signing function will hash the string using Secure Hash Algorithm-256 (SHA-256), after which it is encrypted using a private RSA key. By encrypting the hash with the sender’s private RSA key, others will be able to decrypt it with the sender’s public key and verify that this hash came from this particular sender.

🔥 You might have heard that SHA-256 isn’t all that useful for keeping things secret and you’d be correct. SHA-256 is a deterministic function and will therefore return the same hash, for the same text message, so it’s not the greatest at keeping things a secret. However a SHA-256 hashing algorithm will return a hash of a fixed size (256 bits/32 bytes or more) each time. This does not exceed a typical RSA key’s size (2048 bits or more), so combining the two technologies is a good option for ensuring the authenticity and integrity of a message.

$JwsResultAsByteArr = [System.Text.UTF8Encoding]::UTF8.GetBytes($JwsResult)

$hash = [System.Security.Cryptography.SHA256]::Create().ComputeHash($JwsResultAsByteArr)

# 👇 SHA-256 returns a 256-bit/32-byte array.
$hash.Length
# 32

Now we have a hash, it’s time to get it signed using an RSA key pair. For demonstration purposes I’ll be generating a random key, if you follow along you will most likely get a different result. This is because we’re generating a random RSA key pair.

# Typically you'd get this key from a key store.
$rsa = [System.Security.Cryptography.RSA]::Create(4096)

# LegalKeySizes                           KeySize KeyExchangeAlgorithm SignatureAlgorithm
# -------------                           ------- -------------------- ------------------
# {System.Security.Cryptography.KeySizes}    4096 RSA                  RSA

$hashAlgorithm = [System.Security.Cryptography.HashAlgorithmName]::SHA256;
$padding = [System.Security.Cryptography.RSASignaturePadding]::Pkcs1

$signedResult = $rsa.SignHash($hash,$hashAlgorithm,$padding)

# 👇 4096-bit or 512 byte
$signedResult.Length
# 512

# base64url requires us to convert '+' and '/' characters and remove the '=' characters.
$signedResultb64url = [Convert]::ToBase64String($signedResult).Replace('+', '-').Replace('/', '_').Split("=")[0]
# lgd2LOxR1jOAHoKxEi8fU5qi6aoq_jSDWfyHS6w_py-uAe75ExZ2CkKnggWOcrp6DynQMoeNGPKGKC-j5HzQcot4w4lo-keU8g2alMXeqSHwmA-MKatPow7Cf09_A6CEbRu_VNBrrG0uGqYFpPMbzeqEd4oI4FU9S7cgomYp8UkCoIWZRzczbhDa6s_vuYhk0DTzX_sZmAYSovFXnaT2jE_USjk53yAsNIkdQ6pP0aUcj7UtssBGc9Wt815tbX7YffoKlKjbrxfq0GdM1zkLRWLv0KdZAfrb6WGDKY0P_xJ8ScTknH9zRUITt1eqKJFcvxcexdywaoSRYvua8g5YwJUbsoNdkF-Ck1zsLgDQpC0PAyyJlOt0mcwRue5WNCn-SIYuZ6hT70oGmhr-fImfRk6uk2aNamfAbrKRtqYp3FsNH3yJjnCULi32SItA4jFikDA_pBS3J2sWpJV4jV9G05jEY5bNuFR9x71BpG4a0ANP96svG9XcLYEPx1C6J04Fog5qkWbKfpBSdQcWqaQIC5ZUXUDE5Ha3XjI4ZQHl_iHP4vPGOVij6Fp0U92F_cASwl2-UJRQaBtINFe3kBG_ri-yALJeWbFXmhVH3dJBZvKbNDa5kQz-ttfX_KBPQcw4CCWZseAh-pFCnmRawofVB6PALyK30UoNybFF3EU43RY

Now let’s take that signature and verify it against the hash we computed earlier. We will do this using the same $rsa key pair that we generated, so both the public and private keys are in memory, but it speaks for itself that you will not always have access to both keys. Typically we will either have one or the other, a private or a public key, to work with.


function ConvertTo-ByteArray ([string]$Base64UrlEncodedData) {
    $Base64EncodedString = ConvertTo-Base64EncodedString -Base64UrlEncodedData $Base64UrlEncodedData
    return [Convert]::FromBase64String($Base64EncodedString)
}

function ConvertTo-Base64EncodedString ([string]$Base64UrlEncodedData) {
    $Base64EncodedString = $Base64UrlEncodedData.Replace('-', '+').Replace('_', '/')
    switch ($Base64EncodedString.Length % 4) {
        0 { break; }
        2 { $Base64EncodedString += '=='; break; }
        3 { $Base64EncodedString += '='; break; }
    }
    return $Base64EncodedString
}


$jwsSignatureBytes = ConvertTo-ByteArray -Base64UrlEncodedData $signedResultb64url

$rsa.VerifyHash($hash, $jwsSignatureBytes, $hashAlgorithm, $padding)
# True

The signature is then concatenated to $JwsResult.

$JwsResult += ".{0}" -f $signedResultb64url
# eyJhbGciOiJSUzI1NiJ9.eyJpc3MiOiJqb2UiLCJleHAiOjEzMDA4MTkzODAsImh0dHA6Ly9leGFtcGxlLmNvbS9pc19yb290Ijp0cnVlfQ.lgd2LOxR1jOAHoKxEi8fU5qi6aoq_jSDWfyHS6w_py-uAe75ExZ2CkKnggWOcrp6DynQMoeNGPKGKC-j5HzQcot4w4lo-keU8g2alMXeqSHwmA-MKatPow7Cf09_A6CEbRu_VNBrrG0uGqYFpPMbzeqEd4oI4FU9S7cgomYp8UkCoIWZRzczbhDa6s_vuYhk0DTzX_sZmAYSovFXnaT2jE_USjk53yAsNIkdQ6pP0aUcj7UtssBGc9Wt815tbX7YffoKlKjbrxfq0GdM1zkLRWLv0KdZAfrb6WGDKY0P_xJ8ScTknH9zRUITt1eqKJFcvxcexdywaoSRYvua8g5YwJUbsoNdkF-Ck1zsLgDQpC0PAyyJlOt0mcwRue5WNCn-SIYuZ6hT70oGmhr-fImfRk6uk2aNamfAbrKRtqYp3FsNH3yJjnCULi32SItA4jFikDA_pBS3J2sWpJV4jV9G05jEY5bNuFR9x71BpG4a0ANP96svG9XcLYEPx1C6J04Fog5qkWbKfpBSdQcWqaQIC5ZUXUDE5Ha3XjI4ZQHl_iHP4vPGOVij6Fp0U92F_cASwl2-UJRQaBtINFe3kBG_ri-yALJeWbFXmhVH3dJBZvKbNDa5kQz-ttfX_KBPQcw4CCWZseAh-pFCnmRawofVB6PALyK30UoNybFF3EU43RY

As a bonus, let’s verify this with JWT.io. Let’s grab the public key that’s associated with the $rsa variable. We can quite easily do this using $rsa.ExportRSAPublicKeyPem().

-----BEGIN RSA PUBLIC KEY-----
MIICCgKCAgEAwDO2P0f9gTtky+BZms5QE4XvfJYzmzKNLaxzqsS9xWFQKirJIZmh
+ssc7Q0PfdnNKRA0ksMIdqZ/mINGPA1qTLZAcVTG6lsedfJsZiYPsq9XMhZ+NVxD
ffSJyEXfx9cdMWZEGlkPHLqDh5raGiTrm/cgrhey6SN1x3Z4k3ug8UxKGe3kRhcd
KJE/gw63IJz5l9pFt3r8Wz7eTkypDskDukOHTJAv4EbN68sJUyJAcom1nSSkMEed
mCBamx6sP24m3xmJJ9dfeuuhj+mbB1qH3Nn3ID2U6mO0uBHeKQZMj9nTyrFLJ2Er
9gIhDBMNh9OvyP5xFqbu+GcfU7cUX6ga1OnM0FXPz3S+mrJT3vpE/2e1j/98RNId
4MLF3pL3zwoaIkNx+xkSV8NyHBX7pfCcMkXdL3QkLrk4XswBvUdq39NRTqvjpdTV
q/7URYxYsOFf163W/pXSfM4RQWGkbimdW51pWj7BBQFucPW3mfssvFILvmgRZ25T
QUE0I6Fjw8nvJHKZ1RZ0P/9At/Qh+7Grq2jTXk3AGAX7MP5xf87KaFY1l3i1qN7I
A2UW9/etlhVzsfS7Zb4eTiwRKf1gUXfjWMWLddP2JbzVdAxNJlmcZUrAkDdyyZBd
beAh5w1xuPlHMVill6tx+a/+x8rq2NRcvwxnAw2BssxSNEls/iamXlcCAwEAAQ==
-----END RSA PUBLIC KEY-----

Image of JWT.io

I wondered why I’d even bother going through the step of hashing the string in the first place. I thought: “Why not just use the private key to encrypt the message in its entirety? If we’re unable to decrypt the message, then we know there’s something wrong with the data that we’ve received, right?” It seems I was partially correct. However, there is a limit set on the amount of data that can be encrypted, which is determined by the RSA key size. Since SHA-256 will always return a 256-bit value when we’re using a 4096-bit long RSA key we can be confident that we will not encrypt a value that is larger than the RSA key’s size. Hashing the data and subsequently encrypting it with the private key, tells us that it came from a trusted sender since we can use the public key to determine this. A receiver can reconstitute the message and sign it with the public key, which allows us to verify that the message has not been tampered with.

The attestation token

Next, let’s examine an attestation token, which is a more complex example of a JWT. Despite its intimidating appearance, it’s still just a standard JWT consisting of three base64-url encoded strings concatenated with dots (.). As with the previous example, it’s important to keep in mind the structure of the token and how to decode its different parts.

eyJhbGciOiJSUzI1NiIsImprdSI6Imh0dHBzOi8vc2hhcmVkd2V1LndldS5hdHRlc3QuYXp1cmUubmV0L2NlcnRzIiwia2lkIjoiZFJLaCtoQmNXVWZRaW1TbDNJdjZaaFN0VzNUU090MFRod2lUZ1VVcVpBbz0iLCJ0eXAiOiJKV1QifQ.eyJleHAiOjE2NzE4NjUyMTgsImlhdCI6MTY3MTgzNjQxOCwiaXNzIjoiaHR0cHM6Ly9zaGFyZWR3ZXUud2V1LmF0dGVzdC5henVyZS5uZXQiLCJqdGkiOiJjZTM5NWU1ZGU5YzYzOGQzODRjZDNiZDA2MDQxZTY3NGVkZWU4MjAzMDU1OTZiYmEzMDI5MTc1YWYyMDE4ZGEwIiwibmJmIjoxNjcxODM2NDE4LCJzZWN1cmVib290Ijp0cnVlLCJ4LW1zLWF0dGVzdGF0aW9uLXR5cGUiOiJhenVyZXZtIiwieC1tcy1henVyZXZtLWF0dGVzdGF0aW9uLXByb3RvY29sLXZlciI6IjIuMCIsIngtbXMtYXp1cmV2bS1hdHRlc3RlZC1wY3JzIjpbMCwxLDIsMyw0LDUsNiw3XSwieC1tcy1henVyZXZtLWJvb3RkZWJ1Zy1lbmFibGVkIjpmYWxzZSwieC1tcy1henVyZXZtLWRidmFsaWRhdGVkIjp0cnVlLCJ4LW1zLWF6dXJldm0tZGJ4dmFsaWRhdGVkIjp0cnVlLCJ4LW1zLWF6dXJldm0tZGVidWdnZXJzZGlzYWJsZWQiOnRydWUsIngtbXMtYXp1cmV2bS1kZWZhdWx0LXNlY3VyZWJvb3RrZXlzdmFsaWRhdGVkIjp0cnVlLCJ4LW1zLWF6dXJldm0tZWxhbS1lbmFibGVkIjpmYWxzZSwieC1tcy1henVyZXZtLWZsaWdodHNpZ25pbmctZW5hYmxlZCI6ZmFsc2UsIngtbXMtYXp1cmV2bS1odmNpLXBvbGljeSI6MCwieC1tcy1henVyZXZtLWh5cGVydmlzb3JkZWJ1Zy1lbmFibGVkIjpmYWxzZSwieC1tcy1henVyZXZtLWlzLXdpbmRvd3MiOmZhbHNlLCJ4LW1zLWF6dXJldm0ta2VybmVsZGVidWctZW5hYmxlZCI6ZmFsc2UsIngtbXMtYXp1cmV2bS1vc2J1aWxkIjoiTm90QXBwbGljYXRpb24iLCJ4LW1zLWF6dXJldm0tb3NkaXN0cm8iOiJVYnVudHUiLCJ4LW1zLWF6dXJldm0tb3N0eXBlIjoiTGludXgiLCJ4LW1zLWF6dXJldm0tb3N2ZXJzaW9uLW1ham9yIjoyMCwieC1tcy1henVyZXZtLW9zdmVyc2lvbi1taW5vciI6NCwieC1tcy1henVyZXZtLXNpZ25pbmdkaXNhYmxlZCI6dHJ1ZSwieC1tcy1henVyZXZtLXRlc3RzaWduaW5nLWVuYWJsZWQiOmZhbHNlLCJ4LW1zLWF6dXJldm0tdm1pZCI6IjY1MDZCNTMxLTE2MzQtNDMxRS05OUQyLTQyQjdEMzQxNEFEMCIsIngtbXMtaXNvbGF0aW9uLXRlZSI6eyJ4LW1zLWF0dGVzdGF0aW9uLXR5cGUiOiJzZXZzbnB2bSIsIngtbXMtY29tcGxpYW5jZS1zdGF0dXMiOiJhenVyZS1jb21wbGlhbnQtY3ZtIiwieC1tcy1ydW50aW1lIjp7ImtleXMiOlt7ImUiOiJBUUFCIiwia2V5X29wcyI6WyJlbmNyeXB0Il0sImtpZCI6IkhDTEFrUHViIiwia3R5IjoiUlNBIiwibiI6InRYa1JMQUFCUTd2Z1g5NjQySjJqUzJsMW03MFlNcDl3Nnd4U2dPWVdzZmhpZkNub0Z6SC1pd2llLXUwNmhxZnVQa0hQQ29GZjBoUzN6R0VvbFJmLVNwc1daWTRvQ0s3bjNBR0tHZmRKNFJ4eVhwaHhDVTRKNlU0SDdpUGQ1MWRQTTFGalBySkVyMXRXRTlnQ00teTF5MFZpbTN2Y0FwOG43MElGWHRIdi1LdlpkczlYMFdWZUdPY0tNSk04SlQ2ZzcxazFFY1E0bWQ2Zk02NEpaVDF6VGtwNk41OG5rcUYweENtZkEzcmJYbFBValNKOEEtR1BYUTYxdFRnd1FFTURheFkxamRyWUNWQ1BSZ0pacnliTEVBc2pKWk5RNlVIeHlYMHNFNW5iaGtsb0loQlgzWE5YajVRbGxxZkZGSlhfZlk5SnJmWFF6VzAxYnNWSGswZTFPUSJ9XSwidm0tY29uZmlndXJhdGlvbiI6eyJjb25zb2xlLWVuYWJsZWQiOnRydWUsImN1cnJlbnQtdGltZSI6MTY3MTgzNTU0OCwic2VjdXJlLWJvb3QiOnRydWUsInRwbS1lbmFibGVkIjp0cnVlLCJ2bVVuaXF1ZUlkIjoiNjUwNkI1MzEtMTYzNC00MzFFLTk5RDItNDJCN0QzNDE0QUQwIn19LCJ4LW1zLXNldnNucHZtLWF1dGhvcmtleWRpZ2VzdCI6IjAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMCIsIngtbXMtc2V2c25wdm0tYm9vdGxvYWRlci1zdm4iOjMsIngtbXMtc2V2c25wdm0tZmFtaWx5SWQiOiIwMTAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMCIsIngtbXMtc2V2c25wdm0tZ3Vlc3Rzdm4iOjIsIngtbXMtc2V2c25wdm0taG9zdGRhdGEiOiIwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwIiwieC1tcy1zZXZzbnB2bS1pZGtleWRpZ2VzdCI6IjU3NDg2YTQ0N2VjMGYxOTU4MDAyYTIyYTA2Yjc2NzNiOWZkMjdkMTFlMWM2NTI3NDk4MDU2MDU0YzVmYTkyZDIzYzUwZjlkZTQ0MDcyNzYwZmUyYjZmYjg5NzQwYjY5NiIsIngtbXMtc2V2c25wdm0taW1hZ2VJZCI6IjAyMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwIiwieC1tcy1zZXZzbnB2bS1pcy1kZWJ1Z2dhYmxlIjpmYWxzZSwieC1tcy1zZXZzbnB2bS1sYXVuY2htZWFzdXJlbWVudCI6ImFkNmRlMTZhYzU5ZWU1MjM1MWM2MDM4ZGY1OGQxYmU1YWVhZjQxY2QwZjdjODFiMjI3OWVjY2EwZGY2ZWY0M2EyYjY5ZDY2M2FkNjk3M2Q2ZGJiOWRiMGZmZDdhOTAyMyIsIngtbXMtc2V2c25wdm0tbWljcm9jb2RlLXN2biI6MTE1LCJ4LW1zLXNldnNucHZtLW1pZ3JhdGlvbi1hbGxvd2VkIjpmYWxzZSwieC1tcy1zZXZzbnB2bS1yZXBvcnRkYXRhIjoiYzY1MDA4NTlhZjk1NDQwMjA2YWFjNWU5M2ViNTBhMGYyY2ZkNGZhMmM1NDg1ZTA1YTVjNzdhNWQ4MWMzZGVlMzAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAiLCJ4LW1zLXNldnNucHZtLXJlcG9ydGlkIjoiY2Y1ZWE3NDJmMDhjYjQ1MjQwZThhZDQ3MTliNjExNTAyOGYzZTFkOWQ4ODE3NWEyNDdlYjdjNmM4NmRhNjQ5MyIsIngtbXMtc2V2c25wdm0tc210LWFsbG93ZWQiOnRydWUsIngtbXMtc2V2c25wdm0tc25wZnctc3ZuIjo4LCJ4LW1zLXNldnNucHZtLXRlZS1zdm4iOjAsIngtbXMtc2V2c25wdm0tdm1wbCI6MH0sIngtbXMtcG9saWN5LWhhc2giOiJ3bTltSGx2VFU4MmU4VXFvT3kxWWoxRkJSU05rZmU5OS02OUlZRHE5ZVdzIiwieC1tcy1ydW50aW1lIjp7ImNsaWVudC1wYXlsb2FkIjp7Im5vbmNlIjoiIn0sImtleXMiOlt7ImUiOiJBUUFCIiwia2V5X29wcyI6WyJlbmNyeXB0Il0sImtpZCI6IlRwbUVwaGVtZXJhbEVuY3J5cHRpb25LZXkiLCJrdHkiOiJSU0EiLCJuIjoia1ZUTFN3QUFRcGd0bHFrd1JyRFhoRGdfYzFNZmhSWEkzeE5QbENWMWVWbEVoNWVybE1jS1oxcl9GVV9yMXFmamZiWGd3cmFMYldSQTBpUGlkdnN2ZXJHMDhVRmlBazc2bjlIclNHcVFzendTWDNNRzhUblNtTEU4bEc3N0t2OGx5TXhDN0N5LTlnN05fMXpiMGxHX3doOW1DSG1IVGdJSXAxTHU2WFNOb2tza3F4QUJVV1VxQjcxekZORWV0THNfNktNV0dCd2o3d1lQR0J0Y21ZV0VDeGYwUUprNDdxR0Z0UEZiSU40SEg4MVFKakJBSjA1OEo5Nk15b3ZFNlZOZkdEWEZRSEZ5XzJ3S0JJTzcwTzBLTm11RFVrUWdwaklWRVcxbGt1c2JOUnR4VU91VVJmUWlOaWpKaXRoeFdud1d5ZVdRc0xGaFNoeU8wVDljWDVPMHBRIn1dfSwieC1tcy12ZXIiOiIxLjAifQ.DTCCvMi2bZrK1BWBu1FTDxKoFnE9iQdbti_zvJlyHATjoc9rrCcCP8it_tfMwY1ZquILPDSWqQmXW9O0Dva3x06KhMUuX6begrpN8LROKM0_9n1Zy2Rxg3bnlxhzNV0c6neMeC2bvcGAf2Ikej6EaKX7KnZ4Y4cME_iLrDNbLIyq7sZCrUrZNJtjVuzvWQ03n4dFyZTgco1LIdlgzVZB50HFopTA67asW8SvWkl3RHYmXF1wYaujqTxXDvzhFZbyrLQF1S7da74XVj65mpFcSOkqXb28NYkBndxvfjVsyI-b8UiLYU9WhscNCZBZCfXszqB69ySxvWQGOuoQfHBTAQ

Looking at the JOSE header, we can see that there are a few additional properties in the header.

{
    "alg": "RS256",
    "jku": "https://sharedweu.weu.attest.azure.net/certs",
    "kid": "dRKh+hBcWUfQimSl3Iv6ZhStW3TSOt0ThwiTgUUqZAo=",
    "typ": "JWT"
}

Even if you’ve never seen these JSON properties before, you might be able to surmise that the properties typ and alg are pretty self-explanatory, they relate to the algorithm that was used to “do something with the message” and that “the type of message is a JSON web token”. There’s also a URL in the jku property and a seemingly random value that was assigned to kid.

I initially had no clue what all of these properties meant or how they relate to one another. Fortunately, though, the RFC documents describe in great detail how these properties are meant to be interpreted. I’ve extracted a few snippets and oversimplified the definitions from RFC 7515, the one on JSON Web Signatures.

  • alg
    • “Identifies the cryptographic algorithm used to secure the JWS.”
    • “A list of defined “alg” values for this use can be found in the IANA “JSON Web Signature and Encryption Algorithms” registry established by JWA
  • jku
    • “A URI that refers to a resource for a set of JSON-encoded public keys, one of which corresponds to the key used to digitally sign the JWS.”
  • kid
    • “Indicates which key was used to secure the JWS.”
  • typ
    • “Used by JWS applications to declare the media type of this complete JWS.”

Upon closer inspection of the RFC, you may notice that jku and kid are optional fields in the JOSE header. If an x5c field is included, the structure of the JOSE header will be slightly different compared to when it is not included. When building validation, it’s important to be aware of these variations and handle them accordingly. In order to verify the attestation token, it is necessary to identify which key was used in the signing process. This information can be found in the kid property of the token’s header. In my case, key ID “dRKh+hBcWUfQimSl3Iv6ZhStW3TSOt0ThwiTgUUqZAo=” was used.

Go ahead and browse to the JKU endpoint, you should see a JSON result similar to this one.

{
  "keys": [
    {
      "x5c": [
        "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RU52Y25CdmNtRjBhVzl1TVJRd0VnWURWUVFIREF0VFlXNTBZU0JEYkdGeVlURUxNQWtHQTFVRQpDQXdDUTBFeEN6QUpCZ05WQkFZVEFsVlRNQjRYRFRJek1ESXhOVEF3TWpFeE5Gb1hEVE13TURJeE5UQXdNakV4Ck5Gb3djREVpTUNBR0ExVUVBd3daU1c1MFpXd2dVMGRZSUZCRFN5QkRaWEowYVdacFkyRjBaVEVhTUJnR0ExVUUKQ2d3UlNXNTBaV3dnUTI5eWNHOXlZWFJwYjI0eEZEQVNCZ05WQkFjTUMxTmhiblJoSUVOc1lYSmhNUXN3Q1FZRApWUVFJREFKRFFURUxNQWtHQTFVRUJoTUNWVk13V1RBVEJnY3Foa2pPUFFJQkJnZ3Foa2pPUFFNQkJ3TkNBQVFRCmtNekdPcmJBRTBsZmljREVZQ2pRWjQzdmtzWnFRM1cxMnBQME9jd2RaenU3Z3VkVE55MHcyMjBIQXJHc211dEQKYUNGUHQra2hyVUlRaHlsSkpzV3dvNElDcURDQ0FxUXdId1lEVlIwakJCZ3dGb0FVME9pcTJuWFgrUzVKRjVnOApleFJsME5YeVdVMHdiQVlEVlIwZkJHVXdZekJob0YrZ1hZWmJhSFIwY0hNNkx5OWhjR2t1ZEhKMWMzUmxaSE5sCmNuWnBZMlZ6TG1sdWRHVnNMbU52YlM5elozZ3ZZMlZ5ZEdsbWFXTmhkR2x2Ymk5Mk15OXdZMnRqY213L1kyRTkKY0hKdlkyVnpjMjl5Sm1WdVkyOWthVzVuUFdSbGNqQWRCZ05WSFE0RUZnUVVEQkRtK0Iwd0cvQldTT1lpQlZKdgpNbW5LTGRvd0RnWURWUjBQQVFIL0JBUURBZ2JBTUF3R0ExVWRFd0VCL3dRQ01BQXdnZ0hVQmdrcWhraUcrRTBCCkRRRUVnZ0hGTUlJQndUQWVCZ29xaGtpRytFMEJEUUVCQkJBSzE1SHJwYVMxM2FXY2twblVKOXF2TUlJQlpBWUsKS29aSWh2aE5BUTBCQWpDQ0FWUXdFQVlMS29aSWh2aE5BUTBCQWdFQ0FSUXdFQVlMS29aSWh2aE5BUTBCQWdJQwpBUlF3RUFZTEtvWklodmhOQVEwQkFnTUNBUUl3RUFZTEtvWklodmhOQVEwQkFnUUNBUVF3RUFZTEtvWklodmhOCkFRMEJBZ1VDQVFFd0VRWUxLb1pJaHZoTkFRMEJBZ1lDQWdDQU1CQUdDeXFHU0liNFRRRU5BUUlIQWdFT01CQUcKQ3lxR1NJYjRUUUVOQVFJSUFnRUFNQkFHQ3lxR1NJYjRUUUVOQVFJSkFnRUFNQkFHQ3lxR1NJYjRUUUVOQVFJSwpBZ0VBTUJBR0N5cUdTSWI0VFFFTkFRSUxBZ0VBTUJBR0N5cUdTSWI0VFFFTkFRSU1BZ0VBTUJBR0N5cUdTSWI0ClRRRU5BUUlOQWdFQU1CQUdDeXFHU0liNFRRRU5BUUlPQWdFQU1CQUdDeXFHU0liNFRRRU5BUUlQQWdFQU1CQUcKQ3lxR1NJYjRUUUVOQVFJUUFnRUFNQkFHQ3lxR1NJYjRUUUVOQVFJUkFnRU5NQjhHQ3lxR1NJYjRUUUVOQVFJUwpCQkFVRkFJRUFZQU9BQUFBQUFBQUFBQUFNQkFHQ2lxR1NJYjRUUUVOQVFNRUFnQUFNQlFHQ2lxR1NJYjRUUUVOCkFRUUVCZ0NRYnRVQUFEQVBCZ29xaGtpRytFMEJEUUVGQ2dFQU1Bb0dDQ3FHU000OUJBTUNBMGdBTUVVQ0lBaGIKWXdDVEtickk4dUYybUlYVGt5WXlhWVExNlZ4d09xbXg5R3JpTG55dEFpRUFrWS83YUNicnA5NURBTjNUN3lWdgpDdnN6NUF6RXhoNlJocE53aUkzYW5xST0KLS0tLS1FTkQgQ0VSVElGSUNBVEUtLS0tLQotLS0tLUJFR0lOIENFUlRJRklDQVRFLS0tLS0KTUlJQ21EQ0NBajZnQXdJQkFnSVZBTkRvcXRwMTEva3VTUmVZUEhzVVpkRFY4bGxOTUFvR0NDcUdTTTQ5QkFNQwpNR2d4R2pBWUJnTlZCQU1NRVVsdWRHVnNJRk5IV0NCU2IyOTBJRU5CTVJvd0dBWURWUVFLREJGSmJuUmxiQ0JECmIzSndiM0poZEdsdmJqRVVNQklHQTFVRUJ3d0xVMkZ1ZEdFZ1EyeGhjbUV4Q3pBSkJnTlZCQWdNQWtOQk1Rc3cKQ1FZRFZRUUdFd0pWVXpBZUZ3MHhPREExTWpFeE1EVXdNVEJhRncwek16QTFNakV4TURVd01UQmFNSEV4SXpBaApCZ05WQkFNTUdrbHVkR1ZzSUZOSFdDQlFRMHNnVUhKdlkyVnpjMjl5SUVOQk1Sb3dHQVlEVlFRS0RCRkpiblJsCmJDQkRiM0p3YjNKaGRHbHZiakVVTUJJR0ExVUVCd3dMVTJGdWRHRWdRMnhoY21FeEN6QUpCZ05WQkFnTUFrTkIKTVFzd0NRWURWUVFHRXdKVlV6QlpNQk1HQnlxR1NNNDlBZ0VHQ0NxR1NNNDlBd0VIQTBJQUJMOXErTk1wMklPZwp0ZGwxYmsvdVdaNStUR1FtOGFDaTh6NzhmcytmS0NRM2QrdUR6WG5WVEFUMlpoRENpZnlJdUp3dk4zd05CcDlpCkhCU1NNSk1KckJPamdic3dnYmd3SHdZRFZSMGpCQmd3Rm9BVUltVU0xbHFkTkluemc3U1ZVcjlRR3prbkJxd3cKVWdZRFZSMGZCRXN3U1RCSG9FV2dRNFpCYUhSMGNITTZMeTlqWlhKMGFXWnBZMkYwWlhNdWRISjFjM1JsWkhObApjblpwWTJWekxtbHVkR1ZzTG1OdmJTOUpiblJsYkZOSFdGSnZiM1JEUVM1a1pYSXdIUVlEVlIwT0JCWUVGTkRvCnF0cDExL2t1U1JlWVBIc1VaZERWOGxsTk1BNEdBMVVkRHdFQi93UUVBd0lCQmpBU0JnTlZIUk1CQWY4RUNEQUcKQVFIL0FnRUFNQW9HQ0NxR1NNNDlCQU1DQTBnQU1FVUNJUUNKZ1RidFZxT3laMW0zanFpQVhNNlFZYTZyNXNXUwo0eS9HN3k4dUlKR3hkd0lnUnFQdkJTS3p6UWFnQkxRcTVzNUE3MHBkb2lhUko4ei8wdUR6NE5nVjkxaz0KLS0tLS1FTkQgQ0VSVElGSUNBVEUtLS0tLQotLS0tLUJFR0lOIENFUlRJRklDQVRFLS0tLS0KTUlJQ2p6Q0NBalNnQXdJQkFnSVVJbVVNMWxxZE5JbnpnN1NWVXI5UUd6a25CcXd3Q2dZSUtvWkl6ajBFQXdJdwphREVhTUJnR0ExVUVBd3dSU1c1MFpXd2dVMGRZSUZKdmIzUWdRMEV4R2pBWUJnTlZCQW9NRVVsdWRHVnNJRU52CmNuQnZjbUYwYVc5dU1SUXdFZ1lEVlFRSERBdFRZVzUwWVNCRGJHRnlZVEVMTUFrR0ExVUVDQXdDUTBFeEN6QUoKQmdOVkJBWVRBbFZUTUI0WERURTRNRFV5TVRFd05EVXhNRm9YRFRRNU1USXpNVEl6TlRrMU9Wb3dhREVhTUJnRwpBMVVFQXd3UlNXNTBaV3dnVTBkWUlGSnZiM1FnUTBFeEdqQVlCZ05WQkFvTUVVbHVkR1ZzSUVOdmNuQnZjbUYwCmFXOXVNUlF3RWdZRFZRUUhEQXRUWVc1MFlTQkRiR0Z5WVRFTE1Ba0dBMVVFQ0F3Q1EwRXhDekFKQmdOVkJBWVQKQWxWVE1Ga3dFd1lIS29aSXpqMENBUVlJS29aSXpqMERBUWNEUWdBRUM2bkV3TURJWVpPai9pUFdzQ3phRUtpNwoxT2lPU0xSRmhXR2pibkJWSmZWbmtZNHUzSWprRFlZTDBNeE80bXFzeVlqbEJhbFRWWXhGUDJzSkJLNXpsS09CCnV6Q0J1REFmQmdOVkhTTUVHREFXZ0JRaVpReldXcDAwaWZPRHRKVlN2MUFiT1NjR3JEQlNCZ05WSFI4RVN6QkoKTUVlZ1JhQkRoa0ZvZEhSd2N6b3ZMMk5sY25ScFptbGpZWFJsY3k1MGNuVnpkR1ZrYzJWeWRtbGpaWE11YVc1MApaV3d1WTI5dEwwbHVkR1ZzVTBkWVVtOXZkRU5CTG1SbGNqQWRCZ05WSFE0RUZnUVVJbVVNMWxxZE5JbnpnN1NWClVyOVFHemtuQnF3d0RnWURWUjBQQVFIL0JBUURBZ0VHTUJJR0ExVWRFd0VCL3dRSU1BWUJBZjhDQVFFd0NnWUkKS29aSXpqMEVBd0lEU1FBd1JnSWhBT1cvNVFrUitTOUNpU0RjTm9vd0x1UFJMc1dHZi9ZaTdHU1g5NEJnd1R3ZwpBaUVBNEowbHJIb01zK1hvNW8vc1g2TzlRV3hIUkF2WlVHT2RSUTdjdnFSWGFxST0KLS0tLS1FTkQgQ0VSVElGSUNBVEUtLS0tLQoAMA0GCSqGSIb3DQEBCwUAA4IBAQB8SUZeINgbnluXqwNaOkvtUiAdx0eDGs1ij3Taq2lNAVv09d/CpjvHVSxw74Sl76LCuIpqIRs6ss6X1XUcJk1G4DB4SZIsq7AMot8ERoWqQpqikP9kWhWWnT52ej1Xt4zLEzeGerEuM1/WOtfmB5r9u5HA3qlc6PzZq+Dy7G3NzKTVb3XgbrNC2qje5wWsbOiXLsmUd36nimYa+kLPgx0pggBrh1crNy+Hhmiql7CqQUHTlEeRNt/AZwlFhZyO53dv5xg+YhqsJqqeNY487+aXve0+8vSpu0Dmp2FwsXi2QXRJaVxrCFkKTMac+v01vpqCRbyxh03huuSgQVNlBYCv"
      ],
      "kid": "dRKh+hBcWUfQimSl3Iv6ZhStW3TSOt0ThwiTgUUqZAo=",
      "kty": "RSA"
    },
    {
      "x5c": [
        "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R0ExVUVBd3daU1c1MFpXd2dVMGRZSUZCRFN5QkRaWEowYVdacFkyRjBaVEVhTUJnR0ExVUUKQ2d3UlNXNTBaV3dnUTI5eWNHOXlZWFJwYjI0eEZEQVNCZ05WQkFjTUMxTmhiblJoSUVOc1lYSmhNUXN3Q1FZRApWUVFJREFKRFFURUxNQWtHQTFVRUJoTUNWVk13V1RBVEJnY3Foa2pPUFFJQkJnZ3Foa2pPUFFNQkJ3TkNBQVFRCmtNekdPcmJBRTBsZmljREVZQ2pRWjQzdmtzWnFRM1cxMnBQME9jd2RaenU3Z3VkVE55MHcyMjBIQXJHc211dEQKYUNGUHQra2hyVUlRaHlsSkpzV3dvNElDcURDQ0FxUXdId1lEVlIwakJCZ3dGb0FVME9pcTJuWFgrUzVKRjVnOApleFJsME5YeVdVMHdiQVlEVlIwZkJHVXdZekJob0YrZ1hZWmJhSFIwY0hNNkx5OWhjR2t1ZEhKMWMzUmxaSE5sCmNuWnBZMlZ6TG1sdWRHVnNMbU52YlM5elozZ3ZZMlZ5ZEdsbWFXTmhkR2x2Ymk5Mk15OXdZMnRqY213L1kyRTkKY0hKdlkyVnpjMjl5Sm1WdVkyOWthVzVuUFdSbGNqQWRCZ05WSFE0RUZnUVVEQkRtK0Iwd0cvQldTT1lpQlZKdgpNbW5LTGRvd0RnWURWUjBQQVFIL0JBUURBZ2JBTUF3R0ExVWRFd0VCL3dRQ01BQXdnZ0hVQmdrcWhraUcrRTBCCkRRRUVnZ0hGTUlJQndUQWVCZ29xaGtpRytFMEJEUUVCQkJBSzE1SHJwYVMxM2FXY2twblVKOXF2TUlJQlpBWUsKS29aSWh2aE5BUTBCQWpDQ0FWUXdFQVlMS29aSWh2aE5BUTBCQWdFQ0FSUXdFQVlMS29aSWh2aE5BUTBCQWdJQwpBUlF3RUFZTEtvWklodmhOQVEwQkFnTUNBUUl3RUFZTEtvWklodmhOQVEwQkFnUUNBUVF3RUFZTEtvWklodmhOCkFRMEJBZ1VDQVFFd0VRWUxLb1pJaHZoTkFRMEJBZ1lDQWdDQU1CQUdDeXFHU0liNFRRRU5BUUlIQWdFT01CQUcKQ3lxR1NJYjRUUUVOQVFJSUFnRUFNQkFHQ3lxR1NJYjRUUUVOQVFJSkFnRUFNQkFHQ3lxR1NJYjRUUUVOQVFJSwpBZ0VBTUJBR0N5cUdTSWI0VFFFTkFRSUxBZ0VBTUJBR0N5cUdTSWI0VFFFTkFRSU1BZ0VBTUJBR0N5cUdTSWI0ClRRRU5BUUlOQWdFQU1CQUdDeXFHU0liNFRRRU5BUUlPQWdFQU1CQUdDeXFHU0liNFRRRU5BUUlQQWdFQU1CQUcKQ3lxR1NJYjRUUUVOQVFJUUFnRUFNQkFHQ3lxR1NJYjRUUUVOQVFJUkFnRU5NQjhHQ3lxR1NJYjRUUUVOQVFJUwpCQkFVRkFJRUFZQU9BQUFBQUFBQUFBQUFNQkFHQ2lxR1NJYjRUUUVOQVFNRUFnQUFNQlFHQ2lxR1NJYjRUUUVOCkFRUUVCZ0NRYnRVQUFEQVBCZ29xaGtpRytFMEJEUUVGQ2dFQU1Bb0dDQ3FHU000OUJBTUNBMGdBTUVVQ0lBaGIKWXdDVEtickk4dUYybUlYVGt5WXlhWVExNlZ4d09xbXg5R3JpTG55dEFpRUFrWS83YUNicnA5NURBTjNUN3lWdgpDdnN6NUF6RXhoNlJocE53aUkzYW5xST0KLS0tLS1FTkQgQ0VSVElGSUNBVEUtLS0tLQotLS0tLUJFR0lOIENFUlRJRklDQVRFLS0tLS0KTUlJQ21EQ0NBajZnQXdJQkFnSVZBTkRvcXRwMTEva3VTUmVZUEhzVVpkRFY4bGxOTUFvR0NDcUdTTTQ5QkFNQwpNR2d4R2pBWUJnTlZCQU1NRVVsdWRHVnNJRk5IV0NCU2IyOTBJRU5CTVJvd0dBWURWUVFLREJGSmJuUmxiQ0JECmIzSndiM0poZEdsdmJqRVVNQklHQTFVRUJ3d0xVMkZ1ZEdFZ1EyeGhjbUV4Q3pBSkJnTlZCQWdNQWtOQk1Rc3cKQ1FZRFZRUUdFd0pWVXpBZUZ3MHhPREExTWpFeE1EVXdNVEJhRncwek16QTFNakV4TURVd01UQmFNSEV4SXpBaApCZ05WQkFNTUdrbHVkR1ZzSUZOSFdDQlFRMHNnVUhKdlkyVnpjMjl5SUVOQk1Sb3dHQVlEVlFRS0RCRkpiblJsCmJDQkRiM0p3YjNKaGRHbHZiakVVTUJJR0ExVUVCd3dMVTJGdWRHRWdRMnhoY21FeEN6QUpCZ05WQkFnTUFrTkIKTVFzd0NRWURWUVFHRXdKVlV6QlpNQk1HQnlxR1NNNDlBZ0VHQ0NxR1NNNDlBd0VIQTBJQUJMOXErTk1wMklPZwp0ZGwxYmsvdVdaNStUR1FtOGFDaTh6NzhmcytmS0NRM2QrdUR6WG5WVEFUMlpoRENpZnlJdUp3dk4zd05CcDlpCkhCU1NNSk1KckJPamdic3dnYmd3SHdZRFZSMGpCQmd3Rm9BVUltVU0xbHFkTkluemc3U1ZVcjlRR3prbkJxd3cKVWdZRFZSMGZCRXN3U1RCSG9FV2dRNFpCYUhSMGNITTZMeTlqWlhKMGFXWnBZMkYwWlhNdWRISjFjM1JsWkhObApjblpwWTJWekxtbHVkR1ZzTG1OdmJTOUpiblJsYkZOSFdGSnZiM1JEUVM1a1pYSXdIUVlEVlIwT0JCWUVGTkRvCnF0cDExL2t1U1JlWVBIc1VaZERWOGxsTk1BNEdBMVVkRHdFQi93UUVBd0lCQmpBU0JnTlZIUk1CQWY4RUNEQUcKQVFIL0FnRUFNQW9HQ0NxR1NNNDlCQU1DQTBnQU1FVUNJUUNKZ1RidFZxT3laMW0zanFpQVhNNlFZYTZyNXNXUwo0eS9HN3k4dUlKR3hkd0lnUnFQdkJTS3p6UWFnQkxRcTVzNUE3MHBkb2lhUko4ei8wdUR6NE5nVjkxaz0KLS0tLS1FTkQgQ0VSVElGSUNBVEUtLS0tLQotLS0tLUJFR0lOIENFUlRJRklDQVRFLS0tLS0KTUlJQ2p6Q0NBalNnQXdJQkFnSVVJbVVNMWxxZE5JbnpnN1NWVXI5UUd6a25CcXd3Q2dZSUtvWkl6ajBFQXdJdwphREVhTUJnR0ExVUVBd3dSU1c1MFpXd2dVMGRZSUZKdmIzUWdRMEV4R2pBWUJnTlZCQW9NRVVsdWRHVnNJRU52CmNuQnZjbUYwYVc5dU1SUXdFZ1lEVlFRSERBdFRZVzUwWVNCRGJHRnlZVEVMTUFrR0ExVUVDQXdDUTBFeEN6QUoKQmdOVkJBWVRBbFZUTUI0WERURTRNRFV5TVRFd05EVXhNRm9YRFRRNU1USXpNVEl6TlRrMU9Wb3dhREVhTUJnRwpBMVVFQXd3UlNXNTBaV3dnVTBkWUlGSnZiM1FnUTBFeEdqQVlCZ05WQkFvTUVVbHVkR1ZzSUVOdmNuQnZjbUYwCmFXOXVNUlF3RWdZRFZRUUhEQXRUWVc1MFlTQkRiR0Z5WVRFTE1Ba0dBMVVFQ0F3Q1EwRXhDekFKQmdOVkJBWVQKQWxWVE1Ga3dFd1lIS29aSXpqMENBUVlJS29aSXpqMERBUWNEUWdBRUM2bkV3TURJWVpPai9pUFdzQ3phRUtpNwoxT2lPU0xSRmhXR2pibkJWSmZWbmtZNHUzSWprRFlZTDBNeE80bXFzeVlqbEJhbFRWWXhGUDJzSkJLNXpsS09CCnV6Q0J1REFmQmdOVkhTTUVHREFXZ0JRaVpReldXcDAwaWZPRHRKVlN2MUFiT1NjR3JEQlNCZ05WSFI4RVN6QkoKTUVlZ1JhQkRoa0ZvZEhSd2N6b3ZMMk5sY25ScFptbGpZWFJsY3k1MGNuVnpkR1ZrYzJWeWRtbGpaWE11YVc1MApaV3d1WTI5dEwwbHVkR1ZzVTBkWVVtOXZkRU5CTG1SbGNqQWRCZ05WSFE0RUZnUVVJbVVNMWxxZE5JbnpnN1NWClVyOVFHemtuQnF3d0RnWURWUjBQQVFIL0JBUURBZ0VHTUJJR0ExVWRFd0VCL3dRSU1BWUJBZjhDQVFFd0NnWUkKS29aSXpqMEVBd0lEU1FBd1JnSWhBT1cvNVFrUitTOUNpU0RjTm9vd0x1UFJMc1dHZi9ZaTdHU1g5NEJnd1R3ZwpBaUVBNEowbHJIb01zK1hvNW8vc1g2TzlRV3hIUkF2WlVHT2RSUTdjdnFSWGFxST0KLS0tLS1FTkQgQ0VSVElGSUNBVEUtLS0tLQoAMA0GCSqGSIb3DQEBCwUAA4GBAJ/tqLl+GgzcXWLjfvlGe53ncG0PyR5niIc8yC7Tr5M36QoG62TfkWP3J5kGjfKC5F4wYgUlHOYBGBoExX43nWHiM4HgNMFlbIvUl0UXMJTiqLmK4xyN5YCeLxdeTjBTAIA1XZ4oBV3WJh+OIlcf5B3PR5A+3xuhUfLtYsT9R5Wy"
      ],
      "kid": "wiFDMG11GDLqd14z3T/DUYh/ZmGaVfY0s29siGgYOJs=",
      "kty": "RSA"
    }
  ]
}

⚠️ Depending on when you’re reading this, the JSON-encoded public keys and kid may have changed!

Just as before, it’s worth taking the time to read up on what we’re dealing with.

  • x5c
    • “X.509 certificate chain parameter contains a chain of one or more PKIX certificates.”
    • “The certificate chain is represented as a JSON array of certificate value strings”
    • “Each string in the array is a base64-encoded (not base64url-encoded) DER PKIX certificate value.”
    • “The certificate containing the public key corresponding to the key used to digitally sign the JWS MUST be the first certificate.”
  • kid
    • “Key ID parameter is used to match a specific key.”
    • “Key ID value is a case-sensitive string.”
  • kty
    • “Key type identifies the cryptographic algorithm family used with the key, such as “RSA” or “EC”.”

In asymmetric cryptography, a private key is used to sign the data and a corresponding public key is used to verify the signature. The public key can be freely shared and used to verify that the signature was indeed generated by the owner of the private key. To verify this particular JWS signature, we need the public key that corresponds to the private key used to sign the attestation token. Let’s grab the x5c public certificate that matches the kid from the JOSE header, dRKh+hBcWUfQimSl3Iv6ZhStW3TSOt0ThwiTgUUqZAo=.

💡 DER (Distinguished Encoding Rules) is a binary encoding format used for X.509 certificates and private keys, while PEM (originally “Privacy Enhanced Mail”) is a popular format used for X.509 certificates, CSRs, and cryptographic keys. A PEM file is a text file that includes one or more items in Base64 ASCII encoding, each with plain-text headers and footers such as -----BEGIN CERTIFICATE----- and -----END CERTIFICATE-----. The extensions commonly associated with PEM files are .crt, .pem, .cer, and .key for private keys, but they may also have different extensions.

In PowerShell, we can grab this base64-encoded (not base64URL-encoded) value and parse it.

$certBase64 = "MIIVTTCCFDWgAwIBAgIBATANBgkqhkiG9w0BAQsFADAxMS8wLQYDVQQDDCZodHRwczovL3NoYXJlZHdldS53ZXUuYXR0ZXN0LmF6dXJlLm5ldDAiGA8yMDE5MDUwMTAwMDAwMFoYDzIwNTAxMjMxMjM1OTU5WjAxMS8wLQYDVQQDDCZodHRwczovL3NoYXJlZHdldS53ZXUuYXR0ZXN0LmF6dXJlLm5ldDCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAL7sbE2a1aDvcyZz2hzmwMi7m5KrHQH+vbyB/JmpPI/ovOGRimk0VzNm9Oa2ykfroyKipWiDE7+/1IzvmyQlwLBDuKw94AKSc1tn/RM8qGq+BnWdLzTafh1OovJoFE4UWK6Pnn6IbIBKuAGejLEL31DtnTDsAm95ztcRCSUHhK9GzMQWnR4suZW8Vo6V0KzaSSOUft05gwCXPSzg1Ab5/b4n0lmJMAa/fgb5rDRN7+jymNPDP+8x0Y0b+Q7yzR/vJl6Ona5yShOELe2/lqEyzAQwAX3q8dR/4D235F7cBVsUkMD/NIAvJlAehbNretIXvp6ktZlX3YSI+As1jVMyMyUCAwEAAaOCEmowghJmMAkGA1UdEwQCMAAwHQYDVR0OBBYEFMdctenbRjWK3g+M5sAvZxZnFeENMB8GA1UdIwQYMBaAFMdctenbRjWK3g+M5sAvZxZnFeENMIISFwYJKwYBBAGCN2kBBIISCAEAAAACAAAA+BEAAAAAAAADAAIAAAAAAAkADQCTmnIz95xMqZQKDbOVfwYH0cgnA1fMcqovsmZqTUzlqAAAAAAUFAsH/4AOAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFAAAAAAAAAAcAAAAAAAAADUpYQFulvK4F7SLzv+cdgKXIa+929amEDcwX9h0KfjkAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAHS6X7oghXHPmeH3FY5lNqBbu2zngH7vj2qX9kp69kuDAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAQABAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAHUSofoQXFlH0IpkpdyL+mYUrVt00jrdE4cIk4FFKmQKAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABEEAAAAWUuvMnWsvl9zrZJJDbGQ+EDrpHvvFP/e/zQOviyfzb73Eq6JvxfrcDmtM8ico8YQzI59Yc0koFQu9LiF8EzL+/Q09uLLttvNL348q57IQiX1qReDvw85Yej0opwLRoZjFiACcQKo5xzXJbwar9XbW6dD1tiqwEPwPSJGvjCnlAUFAsH/4AOAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAVAAAAAAAAAAcAAAAAAAAAqoCrqI/UMTIOBySnGcUKvBBmWG5Xflh09V1/6F3bP+UAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAIxPV3XXllA+lhN/d8aKgpoAVqyN7XAUCwgbCUSQxXv/AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAQAJAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAEVTKbLgw4wNxwL5XL7SYdZLCiozKHh64HZh+kIS0HCwAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD52NfnqC3uIwSbui1xQaIG9L+F08OYq/jREdm+4uYwHLVQ5aHOZTS5alnUL77hfpPnyMS4XTd75ijt34Oj64phIAAAAQIDBAUGBwgJCgsMDQ4PEBESExQVFhcYGRobHB0eHwUA3A0AAC0tLS0tQkVHSU4gQ0VSVElGSUNBVEUtLS0tLQpNSUlFalRDQ0JET2dBd0lCQWdJVVVQYXd5bjE0dmM3SHFJZWxPTVhCNHd0eVJaZ3dDZ1lJS29aSXpqMEVBd0l3CmNURWpNQ0VHQTFVRUF3d2FTVzUwWld3Z1UwZFlJRkJEU3lCUWNtOWpaWE56YjNJZ1EwRXhHakFZQmdOVkJBb00KRVVsdWRHVnNJRU52Y25CdmNtRjBhVzl1TVJRd0VnWURWUVFIREF0VFlXNTBZU0JEYkdGeVlURUxNQWtHQTFVRQpDQXdDUTBFeEN6QUpCZ05WQkFZVEFsVlRNQjRYRFRJek1ESXhOVEF3TWpFeE5Gb1hEVE13TURJeE5UQXdNakV4Ck5Gb3djREVpTUNBR0ExVUVBd3daU1c1MFpXd2dVMGRZSUZCRFN5QkRaWEowYVdacFkyRjBaVEVhTUJnR0ExVUUKQ2d3UlNXNTBaV3dnUTI5eWNHOXlZWFJwYjI0eEZEQVNCZ05WQkFjTUMxTmhiblJoSUVOc1lYSmhNUXN3Q1FZRApWUVFJREFKRFFURUxNQWtHQTFVRUJoTUNWVk13V1RBVEJnY3Foa2pPUFFJQkJnZ3Foa2pPUFFNQkJ3TkNBQVFRCmtNekdPcmJBRTBsZmljREVZQ2pRWjQzdmtzWnFRM1cxMnBQME9jd2RaenU3Z3VkVE55MHcyMjBIQXJHc211dEQKYUNGUHQra2hyVUlRaHlsSkpzV3dvNElDcURDQ0FxUXdId1lEVlIwakJCZ3dGb0FVME9pcTJuWFgrUzVKRjVnOApleFJsME5YeVdVMHdiQVlEVlIwZkJHVXdZekJob0YrZ1hZWmJhSFIwY0hNNkx5OWhjR2t1ZEhKMWMzUmxaSE5sCmNuWnBZMlZ6TG1sdWRHVnNMbU52YlM5elozZ3ZZMlZ5ZEdsbWFXTmhkR2x2Ymk5Mk15OXdZMnRqY213L1kyRTkKY0hKdlkyVnpjMjl5Sm1WdVkyOWthVzVuUFdSbGNqQWRCZ05WSFE0RUZnUVVEQkRtK0Iwd0cvQldTT1lpQlZKdgpNbW5LTGRvd0RnWURWUjBQQVFIL0JBUURBZ2JBTUF3R0ExVWRFd0VCL3dRQ01BQXdnZ0hVQmdrcWhraUcrRTBCCkRRRUVnZ0hGTUlJQndUQWVCZ29xaGtpRytFMEJEUUVCQkJBSzE1SHJwYVMxM2FXY2twblVKOXF2TUlJQlpBWUsKS29aSWh2aE5BUTBCQWpDQ0FWUXdFQVlMS29aSWh2aE5BUTBCQWdFQ0FSUXdFQVlMS29aSWh2aE5BUTBCQWdJQwpBUlF3RUFZTEtvWklodmhOQVEwQkFnTUNBUUl3RUFZTEtvWklodmhOQVEwQkFnUUNBUVF3RUFZTEtvWklodmhOCkFRMEJBZ1VDQVFFd0VRWUxLb1pJaHZoTkFRMEJBZ1lDQWdDQU1CQUdDeXFHU0liNFRRRU5BUUlIQWdFT01CQUcKQ3lxR1NJYjRUUUVOQVFJSUFnRUFNQkFHQ3lxR1NJYjRUUUVOQVFJSkFnRUFNQkFHQ3lxR1NJYjRUUUVOQVFJSwpBZ0VBTUJBR0N5cUdTSWI0VFFFTkFRSUxBZ0VBTUJBR0N5cUdTSWI0VFFFTkFRSU1BZ0VBTUJBR0N5cUdTSWI0ClRRRU5BUUlOQWdFQU1CQUdDeXFHU0liNFRRRU5BUUlPQWdFQU1CQUdDeXFHU0liNFRRRU5BUUlQQWdFQU1CQUcKQ3lxR1NJYjRUUUVOQVFJUUFnRUFNQkFHQ3lxR1NJYjRUUUVOQVFJUkFnRU5NQjhHQ3lxR1NJYjRUUUVOQVFJUwpCQkFVRkFJRUFZQU9BQUFBQUFBQUFBQUFNQkFHQ2lxR1NJYjRUUUVOQVFNRUFnQUFNQlFHQ2lxR1NJYjRUUUVOCkFRUUVCZ0NRYnRVQUFEQVBCZ29xaGtpRytFMEJEUUVGQ2dFQU1Bb0dDQ3FHU000OUJBTUNBMGdBTUVVQ0lBaGIKWXdDVEtickk4dUYybUlYVGt5WXlhWVExNlZ4d09xbXg5R3JpTG55dEFpRUFrWS83YUNicnA5NURBTjNUN3lWdgpDdnN6NUF6RXhoNlJocE53aUkzYW5xST0KLS0tLS1FTkQgQ0VSVElGSUNBVEUtLS0tLQotLS0tLUJFR0lOIENFUlRJRklDQVRFLS0tLS0KTUlJQ21EQ0NBajZnQXdJQkFnSVZBTkRvcXRwMTEva3VTUmVZUEhzVVpkRFY4bGxOTUFvR0NDcUdTTTQ5QkFNQwpNR2d4R2pBWUJnTlZCQU1NRVVsdWRHVnNJRk5IV0NCU2IyOTBJRU5CTVJvd0dBWURWUVFLREJGSmJuUmxiQ0JECmIzSndiM0poZEdsdmJqRVVNQklHQTFVRUJ3d0xVMkZ1ZEdFZ1EyeGhjbUV4Q3pBSkJnTlZCQWdNQWtOQk1Rc3cKQ1FZRFZRUUdFd0pWVXpBZUZ3MHhPREExTWpFeE1EVXdNVEJhRncwek16QTFNakV4TURVd01UQmFNSEV4SXpBaApCZ05WQkFNTUdrbHVkR1ZzSUZOSFdDQlFRMHNnVUhKdlkyVnpjMjl5SUVOQk1Sb3dHQVlEVlFRS0RCRkpiblJsCmJDQkRiM0p3YjNKaGRHbHZiakVVTUJJR0ExVUVCd3dMVTJGdWRHRWdRMnhoY21FeEN6QUpCZ05WQkFnTUFrTkIKTVFzd0NRWURWUVFHRXdKVlV6QlpNQk1HQnlxR1NNNDlBZ0VHQ0NxR1NNNDlBd0VIQTBJQUJMOXErTk1wMklPZwp0ZGwxYmsvdVdaNStUR1FtOGFDaTh6NzhmcytmS0NRM2QrdUR6WG5WVEFUMlpoRENpZnlJdUp3dk4zd05CcDlpCkhCU1NNSk1KckJPamdic3dnYmd3SHdZRFZSMGpCQmd3Rm9BVUltVU0xbHFkTkluemc3U1ZVcjlRR3prbkJxd3cKVWdZRFZSMGZCRXN3U1RCSG9FV2dRNFpCYUhSMGNITTZMeTlqWlhKMGFXWnBZMkYwWlhNdWRISjFjM1JsWkhObApjblpwWTJWekxtbHVkR1ZzTG1OdmJTOUpiblJsYkZOSFdGSnZiM1JEUVM1a1pYSXdIUVlEVlIwT0JCWUVGTkRvCnF0cDExL2t1U1JlWVBIc1VaZERWOGxsTk1BNEdBMVVkRHdFQi93UUVBd0lCQmpBU0JnTlZIUk1CQWY4RUNEQUcKQVFIL0FnRUFNQW9HQ0NxR1NNNDlCQU1DQTBnQU1FVUNJUUNKZ1RidFZxT3laMW0zanFpQVhNNlFZYTZyNXNXUwo0eS9HN3k4dUlKR3hkd0lnUnFQdkJTS3p6UWFnQkxRcTVzNUE3MHBkb2lhUko4ei8wdUR6NE5nVjkxaz0KLS0tLS1FTkQgQ0VSVElGSUNBVEUtLS0tLQotLS0tLUJFR0lOIENFUlRJRklDQVRFLS0tLS0KTUlJQ2p6Q0NBalNnQXdJQkFnSVVJbVVNMWxxZE5JbnpnN1NWVXI5UUd6a25CcXd3Q2dZSUtvWkl6ajBFQXdJdwphREVhTUJnR0ExVUVBd3dSU1c1MFpXd2dVMGRZSUZKdmIzUWdRMEV4R2pBWUJnTlZCQW9NRVVsdWRHVnNJRU52CmNuQnZjbUYwYVc5dU1SUXdFZ1lEVlFRSERBdFRZVzUwWVNCRGJHRnlZVEVMTUFrR0ExVUVDQXdDUTBFeEN6QUoKQmdOVkJBWVRBbFZUTUI0WERURTRNRFV5TVRFd05EVXhNRm9YRFRRNU1USXpNVEl6TlRrMU9Wb3dhREVhTUJnRwpBMVVFQXd3UlNXNTBaV3dnVTBkWUlGSnZiM1FnUTBFeEdqQVlCZ05WQkFvTUVVbHVkR1ZzSUVOdmNuQnZjbUYwCmFXOXVNUlF3RWdZRFZRUUhEQXRUWVc1MFlTQkRiR0Z5WVRFTE1Ba0dBMVVFQ0F3Q1EwRXhDekFKQmdOVkJBWVQKQWxWVE1Ga3dFd1lIS29aSXpqMENBUVlJS29aSXpqMERBUWNEUWdBRUM2bkV3TURJWVpPai9pUFdzQ3phRUtpNwoxT2lPU0xSRmhXR2pibkJWSmZWbmtZNHUzSWprRFlZTDBNeE80bXFzeVlqbEJhbFRWWXhGUDJzSkJLNXpsS09CCnV6Q0J1REFmQmdOVkhTTUVHREFXZ0JRaVpReldXcDAwaWZPRHRKVlN2MUFiT1NjR3JEQlNCZ05WSFI4RVN6QkoKTUVlZ1JhQkRoa0ZvZEhSd2N6b3ZMMk5sY25ScFptbGpZWFJsY3k1MGNuVnpkR1ZrYzJWeWRtbGpaWE11YVc1MApaV3d1WTI5dEwwbHVkR1ZzVTBkWVVtOXZkRU5CTG1SbGNqQWRCZ05WSFE0RUZnUVVJbVVNMWxxZE5JbnpnN1NWClVyOVFHemtuQnF3d0RnWURWUjBQQVFIL0JBUURBZ0VHTUJJR0ExVWRFd0VCL3dRSU1BWUJBZjhDQVFFd0NnWUkKS29aSXpqMEVBd0lEU1FBd1JnSWhBT1cvNVFrUitTOUNpU0RjTm9vd0x1UFJMc1dHZi9ZaTdHU1g5NEJnd1R3ZwpBaUVBNEowbHJIb01zK1hvNW8vc1g2TzlRV3hIUkF2WlVHT2RSUTdjdnFSWGFxST0KLS0tLS1FTkQgQ0VSVElGSUNBVEUtLS0tLQoAMA0GCSqGSIb3DQEBCwUAA4IBAQB8SUZeINgbnluXqwNaOkvtUiAdx0eDGs1ij3Taq2lNAVv09d/CpjvHVSxw74Sl76LCuIpqIRs6ss6X1XUcJk1G4DB4SZIsq7AMot8ERoWqQpqikP9kWhWWnT52ej1Xt4zLEzeGerEuM1/WOtfmB5r9u5HA3qlc6PzZq+Dy7G3NzKTVb3XgbrNC2qje5wWsbOiXLsmUd36nimYa+kLPgx0pggBrh1crNy+Hhmiql7CqQUHTlEeRNt/AZwlFhZyO53dv5xg+YhqsJqqeNY487+aXve0+8vSpu0Dmp2FwsXi2QXRJaVxrCFkKTMac+v01vpqCRbyxh03huuSgQVNlBYCv"
$cert = [System.Security.Cryptography.X509Certificates.X509Certificate2]([System.Convert]::FromBase64String($certBase64))
$cert | Format-List *

# EnhancedKeyUsageList :
# DnsNameList          :
# SendAsTrustedIssuer  :
# Archived             : False
# Extensions           : {System.Security.Cryptography.Oid, System.Security.Cryptography.Oid, System.Security.Cryptography.Oid, System.Security.Cryptography.Oid}
# FriendlyName         :
# HasPrivateKey        : False 👈
# PrivateKey           :
# IssuerName           : System.Security.Cryptography.X509Certificates.X500DistinguishedName
# NotAfter             : 1/1/2051 12:59:59 AM
# NotBefore            : 5/1/2019 2:00:00 AM
# PublicKey            : System.Security.Cryptography.X509Certificates.PublicKey
# RawData              : {48, 130, 21, 77…}
# RawDataMemory        : System.ReadOnlyMemory<Byte>[5457]
# SerialNumber         : 01
# SignatureAlgorithm   : System.Security.Cryptography.Oid
# SubjectName          : System.Security.Cryptography.X509Certificates.X500DistinguishedName
# Thumbprint           : 198C95D3A6E2BD8E21E50743EECC23C7EC6130D5
# Version              : 3
# Handle               : 140218996030576
# Issuer               : CN=https://sharedweu.weu.attest.azure.net 👈 Look at that!
# Subject              : CN=https://sharedweu.weu.attest.azure.net
# SerialNumberBytes    : System.ReadOnlyMemory<Byte>[1]

You can simply copy the base64 representation of the certificate and turn it into a PEM file, after which we can paste it into the “verify signature” section of JWT.io. This “PEM file” contains the public key that is used to verify the digital signature of the JWT. We can achieve this simply by wrapping the base64 value with -----BEGIN CERTIFICATE----- and -----END CERTIFICATE-----. Here is an example of how that would look with our x5c public certificate.

-----BEGIN CERTIFICATE-----
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RU52Y25CdmNtRjBhVzl1TVJRd0VnWURWUVFIREF0VFlXNTBZU0JEYkdGeVlURUxNQWtHQTFVRQpDQXdDUTBFeEN6QUpCZ05WQkFZVEFsVlRNQjRYRFRJek1ESXhOVEF3TWpFeE5Gb1hEVE13TURJeE5UQXdNakV4Ck5Gb3djREVpTUNBR0ExVUVBd3daU1c1MFpXd2dVMGRZSUZCRFN5QkRaWEowYVdacFkyRjBaVEVhTUJnR0ExVUUKQ2d3UlNXNTBaV3dnUTI5eWNHOXlZWFJwYjI0eEZEQVNCZ05WQkFjTUMxTmhiblJoSUVOc1lYSmhNUXN3Q1FZRApWUVFJREFKRFFURUxNQWtHQTFVRUJoTUNWVk13V1RBVEJnY3Foa2pPUFFJQkJnZ3Foa2pPUFFNQkJ3TkNBQVFRCmtNekdPcmJBRTBsZmljREVZQ2pRWjQzdmtzWnFRM1cxMnBQME9jd2RaenU3Z3VkVE55MHcyMjBIQXJHc211dEQKYUNGUHQra2hyVUlRaHlsSkpzV3dvNElDcURDQ0FxUXdId1lEVlIwakJCZ3dGb0FVME9pcTJuWFgrUzVKRjVnOApleFJsME5YeVdVMHdiQVlEVlIwZkJHVXdZekJob0YrZ1hZWmJhSFIwY0hNNkx5OWhjR2t1ZEhKMWMzUmxaSE5sCmNuWnBZMlZ6TG1sdWRHVnNMbU52YlM5elozZ3ZZMlZ5ZEdsbWFXTmhkR2x2Ymk5Mk15OXdZMnRqY213L1kyRTkKY0hKdlkyVnpjMjl5Sm1WdVkyOWthVzVuUFdSbGNqQWRCZ05WSFE0RUZnUVVEQkRtK0Iwd0cvQldTT1lpQlZKdgpNbW5LTGRvd0RnWURWUjBQQVFIL0JBUURBZ2JBTUF3R0ExVWRFd0VCL3dRQ01BQXdnZ0hVQmdrcWhraUcrRTBCCkRRRUVnZ0hGTUlJQndUQWVCZ29xaGtpRytFMEJEUUVCQkJBSzE1SHJwYVMxM2FXY2twblVKOXF2TUlJQlpBWUsKS29aSWh2aE5BUTBCQWpDQ0FWUXdFQVlMS29aSWh2aE5BUTBCQWdFQ0FSUXdFQVlMS29aSWh2aE5BUTBCQWdJQwpBUlF3RUFZTEtvWklodmhOQVEwQkFnTUNBUUl3RUFZTEtvWklodmhOQVEwQkFnUUNBUVF3RUFZTEtvWklodmhOCkFRMEJBZ1VDQVFFd0VRWUxLb1pJaHZoTkFRMEJBZ1lDQWdDQU1CQUdDeXFHU0liNFRRRU5BUUlIQWdFT01CQUcKQ3lxR1NJYjRUUUVOQVFJSUFnRUFNQkFHQ3lxR1NJYjRUUUVOQVFJSkFnRUFNQkFHQ3lxR1NJYjRUUUVOQVFJSwpBZ0VBTUJBR0N5cUdTSWI0VFFFTkFRSUxBZ0VBTUJBR0N5cUdTSWI0VFFFTkFRSU1BZ0VBTUJBR0N5cUdTSWI0ClRRRU5BUUlOQWdFQU1CQUdDeXFHU0liNFRRRU5BUUlPQWdFQU1CQUdDeXFHU0liNFRRRU5BUUlQQWdFQU1CQUcKQ3lxR1NJYjRUUUVOQVFJUUFnRUFNQkFHQ3lxR1NJYjRUUUVOQVFJUkFnRU5NQjhHQ3lxR1NJYjRUUUVOQVFJUwpCQkFVRkFJRUFZQU9BQUFBQUFBQUFBQUFNQkFHQ2lxR1NJYjRUUUVOQVFNRUFnQUFNQlFHQ2lxR1NJYjRUUUVOCkFRUUVCZ0NRYnRVQUFEQVBCZ29xaGtpRytFMEJEUUVGQ2dFQU1Bb0dDQ3FHU000OUJBTUNBMGdBTUVVQ0lBaGIKWXdDVEtickk4dUYybUlYVGt5WXlhWVExNlZ4d09xbXg5R3JpTG55dEFpRUFrWS83YUNicnA5NURBTjNUN3lWdgpDdnN6NUF6RXhoNlJocE53aUkzYW5xST0KLS0tLS1FTkQgQ0VSVElGSUNBVEUtLS0tLQotLS0tLUJFR0lOIENFUlRJRklDQVRFLS0tLS0KTUlJQ21EQ0NBajZnQXdJQkFnSVZBTkRvcXRwMTEva3VTUmVZUEhzVVpkRFY4bGxOTUFvR0NDcUdTTTQ5QkFNQwpNR2d4R2pBWUJnTlZCQU1NRVVsdWRHVnNJRk5IV0NCU2IyOTBJRU5CTVJvd0dBWURWUVFLREJGSmJuUmxiQ0JECmIzSndiM0poZEdsdmJqRVVNQklHQTFVRUJ3d0xVMkZ1ZEdFZ1EyeGhjbUV4Q3pBSkJnTlZCQWdNQWtOQk1Rc3cKQ1FZRFZRUUdFd0pWVXpBZUZ3MHhPREExTWpFeE1EVXdNVEJhRncwek16QTFNakV4TURVd01UQmFNSEV4SXpBaApCZ05WQkFNTUdrbHVkR1ZzSUZOSFdDQlFRMHNnVUhKdlkyVnpjMjl5SUVOQk1Sb3dHQVlEVlFRS0RCRkpiblJsCmJDQkRiM0p3YjNKaGRHbHZiakVVTUJJR0ExVUVCd3dMVTJGdWRHRWdRMnhoY21FeEN6QUpCZ05WQkFnTUFrTkIKTVFzd0NRWURWUVFHRXdKVlV6QlpNQk1HQnlxR1NNNDlBZ0VHQ0NxR1NNNDlBd0VIQTBJQUJMOXErTk1wMklPZwp0ZGwxYmsvdVdaNStUR1FtOGFDaTh6NzhmcytmS0NRM2QrdUR6WG5WVEFUMlpoRENpZnlJdUp3dk4zd05CcDlpCkhCU1NNSk1KckJPamdic3dnYmd3SHdZRFZSMGpCQmd3Rm9BVUltVU0xbHFkTkluemc3U1ZVcjlRR3prbkJxd3cKVWdZRFZSMGZCRXN3U1RCSG9FV2dRNFpCYUhSMGNITTZMeTlqWlhKMGFXWnBZMkYwWlhNdWRISjFjM1JsWkhObApjblpwWTJWekxtbHVkR1ZzTG1OdmJTOUpiblJsYkZOSFdGSnZiM1JEUVM1a1pYSXdIUVlEVlIwT0JCWUVGTkRvCnF0cDExL2t1U1JlWVBIc1VaZERWOGxsTk1BNEdBMVVkRHdFQi93UUVBd0lCQmpBU0JnTlZIUk1CQWY4RUNEQUcKQVFIL0FnRUFNQW9HQ0NxR1NNNDlCQU1DQTBnQU1FVUNJUUNKZ1RidFZxT3laMW0zanFpQVhNNlFZYTZyNXNXUwo0eS9HN3k4dUlKR3hkd0lnUnFQdkJTS3p6UWFnQkxRcTVzNUE3MHBkb2lhUko4ei8wdUR6NE5nVjkxaz0KLS0tLS1FTkQgQ0VSVElGSUNBVEUtLS0tLQotLS0tLUJFR0lOIENFUlRJRklDQVRFLS0tLS0KTUlJQ2p6Q0NBalNnQXdJQkFnSVVJbVVNMWxxZE5JbnpnN1NWVXI5UUd6a25CcXd3Q2dZSUtvWkl6ajBFQXdJdwphREVhTUJnR0ExVUVBd3dSU1c1MFpXd2dVMGRZSUZKdmIzUWdRMEV4R2pBWUJnTlZCQW9NRVVsdWRHVnNJRU52CmNuQnZjbUYwYVc5dU1SUXdFZ1lEVlFRSERBdFRZVzUwWVNCRGJHRnlZVEVMTUFrR0ExVUVDQXdDUTBFeEN6QUoKQmdOVkJBWVRBbFZUTUI0WERURTRNRFV5TVRFd05EVXhNRm9YRFRRNU1USXpNVEl6TlRrMU9Wb3dhREVhTUJnRwpBMVVFQXd3UlNXNTBaV3dnVTBkWUlGSnZiM1FnUTBFeEdqQVlCZ05WQkFvTUVVbHVkR1ZzSUVOdmNuQnZjbUYwCmFXOXVNUlF3RWdZRFZRUUhEQXRUWVc1MFlTQkRiR0Z5WVRFTE1Ba0dBMVVFQ0F3Q1EwRXhDekFKQmdOVkJBWVQKQWxWVE1Ga3dFd1lIS29aSXpqMENBUVlJS29aSXpqMERBUWNEUWdBRUM2bkV3TURJWVpPai9pUFdzQ3phRUtpNwoxT2lPU0xSRmhXR2pibkJWSmZWbmtZNHUzSWprRFlZTDBNeE80bXFzeVlqbEJhbFRWWXhGUDJzSkJLNXpsS09CCnV6Q0J1REFmQmdOVkhTTUVHREFXZ0JRaVpReldXcDAwaWZPRHRKVlN2MUFiT1NjR3JEQlNCZ05WSFI4RVN6QkoKTUVlZ1JhQkRoa0ZvZEhSd2N6b3ZMMk5sY25ScFptbGpZWFJsY3k1MGNuVnpkR1ZrYzJWeWRtbGpaWE11YVc1MApaV3d1WTI5dEwwbHVkR1ZzVTBkWVVtOXZkRU5CTG1SbGNqQWRCZ05WSFE0RUZnUVVJbVVNMWxxZE5JbnpnN1NWClVyOVFHemtuQnF3d0RnWURWUjBQQVFIL0JBUURBZ0VHTUJJR0ExVWRFd0VCL3dRSU1BWUJBZjhDQVFFd0NnWUkKS29aSXpqMEVBd0lEU1FBd1JnSWhBT1cvNVFrUitTOUNpU0RjTm9vd0x1UFJMc1dHZi9ZaTdHU1g5NEJnd1R3ZwpBaUVBNEowbHJIb01zK1hvNW8vc1g2TzlRV3hIUkF2WlVHT2RSUTdjdnFSWGFxST0KLS0tLS1FTkQgQ0VSVElGSUNBVEUtLS0tLQoAMA0GCSqGSIb3DQEBCwUAA4IBAQB8SUZeINgbnluXqwNaOkvtUiAdx0eDGs1ij3Taq2lNAVv09d/CpjvHVSxw74Sl76LCuIpqIRs6ss6X1XUcJk1G4DB4SZIsq7AMot8ERoWqQpqikP9kWhWWnT52ej1Xt4zLEzeGerEuM1/WOtfmB5r9u5HA3qlc6PzZq+Dy7G3NzKTVb3XgbrNC2qje5wWsbOiXLsmUd36nimYa+kLPgx0pggBrh1crNy+Hhmiql7CqQUHTlEeRNt/AZwlFhZyO53dv5xg+YhqsJqqeNY487+aXve0+8vSpu0Dmp2FwsXi2QXRJaVxrCFkKTMac+v01vpqCRbyxh03huuSgQVNlBYCv
-----END CERTIFICATE-----

And after pasting both the entire attestation token and the public certificate in JWT.io we should get a message saying that the token’s signature is valid.

Image of JWT.io

Bringing it locally

After completing the steps, I started to consider whether it was possible to create a script that could run the verification process locally. So, I decided to give it a try. The script should be able to handle JWTs that were signed using RS256, RS384, RS512, ES256, ES384 and ES512.

💡 Please note that the following script is intended for educational purposes only and should not be used in a production environment without proper testing and modification. Use at your own risk.

#Requires -Version 7
#Requires -PSEdition Core

<#
.SYNOPSIS
    Validates a JSON web signature, supports RSA and ECDsa.
.DESCRIPTION
    Validates a JSON web signature, takes one argument, "Jwt," and validates a JSON web signature using RSA or ECDsa algorithms.
.PARAMETER -Jwt
    JWT consist of three parts: a header, a payload, and a signature.
.INPUTS
    None.
.OUTPUTS
    System.Management.Automation.PSObject
.EXAMPLE
    PS C:\> .\Confirm-AttestationTokenSignature.ps1 -Jwt "eyJhbGciOiJSUzI1NiIsImprdSI6Imh0dHBzOi8vc2hhcmVkd2V1LndldS5hdHRlc3QuYXp1cmUubmV0L2NlcnRzIiwia2lkIjoiZFJLaCtoQmNXVWZRaW1TbDNJdjZaaFN0VzNUU090MFRod2lUZ1VVcVpBbz0iLCJ0eXAiOiJKV1QifQ.eyJleHAiOjE2NzE4NjUyMTgsImlhdCI6MTY3MTgzNjQxOCwiaXNzIjoiaHR0cHM6Ly9zaGFyZWR3ZXUud2V1LmF0dGVzdC5henVyZS5uZXQiLCJqdGkiOiJjZTM5NWU1ZGU5YzYzOGQzODRjZDNiZDA2MDQxZTY3NGVkZWU4MjAzMDU1OTZiYmEzMDI5MTc1YWYyMDE4ZGEwIiwibmJmIjoxNjcxODM2NDE4LCJzZWN1cmVib290Ijp0cnVlLCJ4LW1zLWF0dGVzdGF0aW9uLXR5cGUiOiJhenVyZXZtIiwieC1tcy1henVyZXZtLWF0dGVzdGF0aW9uLXByb3RvY29sLXZlciI6IjIuMCIsIngtbXMtYXp1cmV2bS1hdHRlc3RlZC1wY3JzIjpbMCwxLDIsMyw0LDUsNiw3XSwieC1tcy1henVyZXZtLWJvb3RkZWJ1Zy1lbmFibGVkIjpmYWxzZSwieC1tcy1henVyZXZtLWRidmFsaWRhdGVkIjp0cnVlLCJ4LW1zLWF6dXJldm0tZGJ4dmFsaWRhdGVkIjp0cnVlLCJ4LW1zLWF6dXJldm0tZGVidWdnZXJzZGlzYWJsZWQiOnRydWUsIngtbXMtYXp1cmV2bS1kZWZhdWx0LXNlY3VyZWJvb3RrZXlzdmFsaWRhdGVkIjp0cnVlLCJ4LW1zLWF6dXJldm0tZWxhbS1lbmFibGVkIjpmYWxzZSwieC1tcy1henVyZXZtLWZsaWdodHNpZ25pbmctZW5hYmxlZCI6ZmFsc2UsIngtbXMtYXp1cmV2bS1odmNpLXBvbGljeSI6MCwieC1tcy1henVyZXZtLWh5cGVydmlzb3JkZWJ1Zy1lbmFibGVkIjpmYWxzZSwieC1tcy1henVyZXZtLWlzLXdpbmRvd3MiOmZhbHNlLCJ4LW1zLWF6dXJldm0ta2VybmVsZGVidWctZW5hYmxlZCI6ZmFsc2UsIngtbXMtYXp1cmV2bS1vc2J1aWxkIjoiTm90QXBwbGljYXRpb24iLCJ4LW1zLWF6dXJldm0tb3NkaXN0cm8iOiJVYnVudHUiLCJ4LW1zLWF6dXJldm0tb3N0eXBlIjoiTGludXgiLCJ4LW1zLWF6dXJldm0tb3N2ZXJzaW9uLW1ham9yIjoyMCwieC1tcy1henVyZXZtLW9zdmVyc2lvbi1taW5vciI6NCwieC1tcy1henVyZXZtLXNpZ25pbmdkaXNhYmxlZCI6dHJ1ZSwieC1tcy1henVyZXZtLXRlc3RzaWduaW5nLWVuYWJsZWQiOmZhbHNlLCJ4LW1zLWF6dXJldm0tdm1pZCI6IjY1MDZCNTMxLTE2MzQtNDMxRS05OUQyLTQyQjdEMzQxNEFEMCIsIngtbXMtaXNvbGF0aW9uLXRlZSI6eyJ4LW1zLWF0dGVzdGF0aW9uLXR5cGUiOiJzZXZzbnB2bSIsIngtbXMtY29tcGxpYW5jZS1zdGF0dXMiOiJhenVyZS1jb21wbGlhbnQtY3ZtIiwieC1tcy1ydW50aW1lIjp7ImtleXMiOlt7ImUiOiJBUUFCIiwia2V5X29wcyI6WyJlbmNyeXB0Il0sImtpZCI6IkhDTEFrUHViIiwia3R5IjoiUlNBIiwibiI6InRYa1JMQUFCUTd2Z1g5NjQySjJqUzJsMW03MFlNcDl3Nnd4U2dPWVdzZmhpZkNub0Z6SC1pd2llLXUwNmhxZnVQa0hQQ29GZjBoUzN6R0VvbFJmLVNwc1daWTRvQ0s3bjNBR0tHZmRKNFJ4eVhwaHhDVTRKNlU0SDdpUGQ1MWRQTTFGalBySkVyMXRXRTlnQ00teTF5MFZpbTN2Y0FwOG43MElGWHRIdi1LdlpkczlYMFdWZUdPY0tNSk04SlQ2ZzcxazFFY1E0bWQ2Zk02NEpaVDF6VGtwNk41OG5rcUYweENtZkEzcmJYbFBValNKOEEtR1BYUTYxdFRnd1FFTURheFkxamRyWUNWQ1BSZ0pacnliTEVBc2pKWk5RNlVIeHlYMHNFNW5iaGtsb0loQlgzWE5YajVRbGxxZkZGSlhfZlk5SnJmWFF6VzAxYnNWSGswZTFPUSJ9XSwidm0tY29uZmlndXJhdGlvbiI6eyJjb25zb2xlLWVuYWJsZWQiOnRydWUsImN1cnJlbnQtdGltZSI6MTY3MTgzNTU0OCwic2VjdXJlLWJvb3QiOnRydWUsInRwbS1lbmFibGVkIjp0cnVlLCJ2bVVuaXF1ZUlkIjoiNjUwNkI1MzEtMTYzNC00MzFFLTk5RDItNDJCN0QzNDE0QUQwIn19LCJ4LW1zLXNldnNucHZtLWF1dGhvcmtleWRpZ2VzdCI6IjAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMCIsIngtbXMtc2V2c25wdm0tYm9vdGxvYWRlci1zdm4iOjMsIngtbXMtc2V2c25wdm0tZmFtaWx5SWQiOiIwMTAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMCIsIngtbXMtc2V2c25wdm0tZ3Vlc3Rzdm4iOjIsIngtbXMtc2V2c25wdm0taG9zdGRhdGEiOiIwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwIiwieC1tcy1zZXZzbnB2bS1pZGtleWRpZ2VzdCI6IjU3NDg2YTQ0N2VjMGYxOTU4MDAyYTIyYTA2Yjc2NzNiOWZkMjdkMTFlMWM2NTI3NDk4MDU2MDU0YzVmYTkyZDIzYzUwZjlkZTQ0MDcyNzYwZmUyYjZmYjg5NzQwYjY5NiIsIngtbXMtc2V2c25wdm0taW1hZ2VJZCI6IjAyMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwIiwieC1tcy1zZXZzbnB2bS1pcy1kZWJ1Z2dhYmxlIjpmYWxzZSwieC1tcy1zZXZzbnB2bS1sYXVuY2htZWFzdXJlbWVudCI6ImFkNmRlMTZhYzU5ZWU1MjM1MWM2MDM4ZGY1OGQxYmU1YWVhZjQxY2QwZjdjODFiMjI3OWVjY2EwZGY2ZWY0M2EyYjY5ZDY2M2FkNjk3M2Q2ZGJiOWRiMGZmZDdhOTAyMyIsIngtbXMtc2V2c25wdm0tbWljcm9jb2RlLXN2biI6MTE1LCJ4LW1zLXNldnNucHZtLW1pZ3JhdGlvbi1hbGxvd2VkIjpmYWxzZSwieC1tcy1zZXZzbnB2bS1yZXBvcnRkYXRhIjoiYzY1MDA4NTlhZjk1NDQwMjA2YWFjNWU5M2ViNTBhMGYyY2ZkNGZhMmM1NDg1ZTA1YTVjNzdhNWQ4MWMzZGVlMzAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAiLCJ4LW1zLXNldnNucHZtLXJlcG9ydGlkIjoiY2Y1ZWE3NDJmMDhjYjQ1MjQwZThhZDQ3MTliNjExNTAyOGYzZTFkOWQ4ODE3NWEyNDdlYjdjNmM4NmRhNjQ5MyIsIngtbXMtc2V2c25wdm0tc210LWFsbG93ZWQiOnRydWUsIngtbXMtc2V2c25wdm0tc25wZnctc3ZuIjo4LCJ4LW1zLXNldnNucHZtLXRlZS1zdm4iOjAsIngtbXMtc2V2c25wdm0tdm1wbCI6MH0sIngtbXMtcG9saWN5LWhhc2giOiJ3bTltSGx2VFU4MmU4VXFvT3kxWWoxRkJSU05rZmU5OS02OUlZRHE5ZVdzIiwieC1tcy1ydW50aW1lIjp7ImNsaWVudC1wYXlsb2FkIjp7Im5vbmNlIjoiIn0sImtleXMiOlt7ImUiOiJBUUFCIiwia2V5X29wcyI6WyJlbmNyeXB0Il0sImtpZCI6IlRwbUVwaGVtZXJhbEVuY3J5cHRpb25LZXkiLCJrdHkiOiJSU0EiLCJuIjoia1ZUTFN3QUFRcGd0bHFrd1JyRFhoRGdfYzFNZmhSWEkzeE5QbENWMWVWbEVoNWVybE1jS1oxcl9GVV9yMXFmamZiWGd3cmFMYldSQTBpUGlkdnN2ZXJHMDhVRmlBazc2bjlIclNHcVFzendTWDNNRzhUblNtTEU4bEc3N0t2OGx5TXhDN0N5LTlnN05fMXpiMGxHX3doOW1DSG1IVGdJSXAxTHU2WFNOb2tza3F4QUJVV1VxQjcxekZORWV0THNfNktNV0dCd2o3d1lQR0J0Y21ZV0VDeGYwUUprNDdxR0Z0UEZiSU40SEg4MVFKakJBSjA1OEo5Nk15b3ZFNlZOZkdEWEZRSEZ5XzJ3S0JJTzcwTzBLTm11RFVrUWdwaklWRVcxbGt1c2JOUnR4VU91VVJmUWlOaWpKaXRoeFdud1d5ZVdRc0xGaFNoeU8wVDljWDVPMHBRIn1dfSwieC1tcy12ZXIiOiIxLjAifQ.DTCCvMi2bZrK1BWBu1FTDxKoFnE9iQdbti_zvJlyHATjoc9rrCcCP8it_tfMwY1ZquILPDSWqQmXW9O0Dva3x06KhMUuX6begrpN8LROKM0_9n1Zy2Rxg3bnlxhzNV0c6neMeC2bvcGAf2Ikej6EaKX7KnZ4Y4cME_iLrDNbLIyq7sZCrUrZNJtjVuzvWQ03n4dFyZTgco1LIdlgzVZB50HFopTA67asW8SvWkl3RHYmXF1wYaujqTxXDvzhFZbyrLQF1S7da74XVj65mpFcSOkqXb28NYkBndxvfjVsyI-b8UiLYU9WhscNCZBZCfXszqB69ySxvWQGOuoQfHBTAQ"
#>

param (
    [Parameter(Mandatory = $false)]
    [string]
    $Jwt
)

$ErrorActionPreference = 'Stop'

function ConvertTo-ByteArray ([string]$Base64UrlEncodedData) {
    $Base64EncodedString = ConvertTo-Base64EncodedString -Base64UrlEncodedData $Base64UrlEncodedData
    return [Convert]::FromBase64String($Base64EncodedString)
}

function ConvertTo-Base64EncodedString ([string]$Base64UrlEncodedData) {
    $Base64EncodedString = $Base64UrlEncodedData.Replace('-', '+').Replace('_', '/')
    switch ($Base64EncodedString.Length % 4) {
        0 { break; }
        2 { $Base64EncodedString += '=='; break; }
        3 { $Base64EncodedString += '='; break; }
    }
    return $Base64EncodedString
}

function Test-Jws ([string]$jwsValue, [string] $jwsType, [Switch] $TestValueNullOrEmpty, [Switch] $TestBase64JsonMalformed ) {
    if ($TestValueNullOrEmpty) {
        if ([string]::IsNullOrEmpty($jwsValue)) {
            Write-Warning -Message "Null or empty $jwsType."
        }
    }
    elseif ($TestBase64JsonMalformed) {
        if ( !$jwsValue.StartsWith("eyJ")) {
            Write-Warning -Message "Possible malformed base64 value for $jwsType."
        }
    }
}


$JwtSplit = $Jwt.Split('.')

# JWS JSON Serialization
[string]$jwsHeader = $JwtSplit[0]
[string]$jwPayload = $JwtSplit[1]
[string]$jwsSignature = $JwtSplit[2]

Test-Jws -jwsValue $jwsHeader -jwsType "JWS Header" -TestValueNullOrEmpty -TestBase64JsonMalformed
Test-Jws -jwsValue $jwPayload -jwsType "JWS Payload" -TestValueNullOrEmpty -TestBase64JsonMalformed
Test-Jws -jwsValue $jwsSignature -jwsType "JWS Signature" -TestValueNullOrEmpty

$data = "{0}.{1}" -f $jwsHeader, $jwPayload
$dataBytes = [System.Text.UTF8Encoding]::UTF8.GetBytes($data)
$hashedDataBytes = [System.Security.Cryptography.SHA256]::Create().ComputeHash($dataBytes)

$jwsSignatureBytes = ConvertTo-ByteArray -Base64UrlEncodedData $jwsSignature

$hashResult = $null

[PSCustomObject]$jwsHeaderObject = [System.Text.Encoding]::UTF8.GetString((ConvertTo-ByteArray -Base64UrlEncodedData $jwsHeader)) | ConvertFrom-Json

if ($null -eq $jwsHeaderObject) {
    throw "Unable to deserialize the JWS Header JSON."
}

Write-Host "Using algorithm '$($jwsHeaderObject.alg)'.."  -ForegroundColor Green
switch ($jwsHeaderObject.alg) {
    "RS256" {
        $hashAlgorithm = [System.Security.Cryptography.HashAlgorithmName]::SHA256;
        $padding = [System.Security.Cryptography.RSASignaturePadding]::Pkcs1
        break;
    }
    "RS384" {
        $hashAlgorithm = [System.Security.Cryptography.HashAlgorithmName]::SHA384;
        $padding = [System.Security.Cryptography.RSASignaturePadding]::Pkcs1
        break;
    }
    "RS512" {
        $hashAlgorithm = [System.Security.Cryptography.HashAlgorithmName]::SHA512;
        $padding = [System.Security.Cryptography.RSASignaturePadding]::Pkcs1
        break;
    }
    "ES256" {
        $hashAlgorithm = [System.Security.Cryptography.HashAlgorithmName]::SHA256;
        break;
    }
    "ES384" {
        $hashAlgorithm = [System.Security.Cryptography.HashAlgorithmName]::SHA384;
        break;
    }
    "ES512" {
        $hashAlgorithm = [System.Security.Cryptography.HashAlgorithmName]::SHA512;
        break;
    }
    Default {
        throw "Unknown/unimplemented JSON Web Algorithm"
    }
}

if ($jwsHeaderObject.jku) {
    Write-Host "Getting certificates from '$($jwsHeaderObject.jku)'.."  -ForegroundColor Green
    $getJkuResponse = Invoke-RestMethod -Uri $jwsHeaderObject.jku -Method Get

    $matchingCertificate = $getJkuResponse.keys | Where-Object { $_.kid -eq $jwsHeaderObject.kid }
    if ($matchingCertificate) {
        Write-Host "Found matching certificate for kid '$($jwsHeaderObject.kid)'! "  -ForegroundColor Green
    }
    else {
        Write-Error -Message "No matching certificate found for kid '$($jwsHeaderObject.kid)'.."
    }

    $publicCertificate = [System.Security.Cryptography.X509Certificates.X509Certificate2]([System.Convert]::FromBase64String($matchingCertificate.x5c[-1]))
    switch ($jwsHeaderObject.alg[0]) {
        "R" {
            $rsa = [System.Security.Cryptography.X509Certificates.RSACertificateExtensions]::GetRSAPublicKey($publicCertificate)
            $hashResult = $rsa.VerifyHash($hashedDataBytes, $jwsSignatureBytes, $hashAlgorithm, $padding)
        }
        "E" {
            $ecdsa = [System.Security.Cryptography.X509Certificates.ECDsaCertificateExtensions]::GetECDsaPublicKey($publicCertificate)
            $hashResult = $ecdsa.VerifyHash($hashedDataBytes, $jwsSignatureBytes, $hashAlgorithm, $padding)
        }
        Default {}
    }

    $fgc = if($hashResult) {"green"} else {"red"}
    Write-Host "Hash result: $hashResult" -ForegroundColor $fgc
}
elseif ($jwsHeaderObject.x5c) {
    $publicCertificate = $null

    foreach ($x5cItem in $jwsHeaderObject.x5c) {
        $tempCert = [System.Security.Cryptography.X509Certificates.X509Certificate2]([System.Convert]::FromBase64String($x5cItem))
        $chainTrustValidator = [System.IdentityModel.Selectors.X509CertificateValidator]::ChainTrust;
        $chainTrustValidator.Validate($tempCert);

        if ($tempCert.Thumbprint -ieq $jwsHeaderObject.kid) {
            $publicCertificate = $tempCert
        }
        $tempCert = $null
    }

    if ($publicCertificate) {
        Write-Host "Found matching certificate for kid '$($jwsHeaderObject.kid)'! " -ForegroundColor Green
    }
    else {
        Write-Error -Message "No matching certificate found for kid '$($jwsHeaderObject.kid)'.."
    }

    switch ($jwsHeaderObject.alg[0]) {
        "R" {
            $rsa = [System.Security.Cryptography.X509Certificates.RSACertificateExtensions]::GetRSAPublicKey($publicCertificate)
            $hashResult = $rsa.VerifyHash($hashedDataBytes, $jwsSignatureBytes, $hashAlgorithm, $padding)
        }
        "E" {
            $ecdsa = [System.Security.Cryptography.X509Certificates.ECDsaCertificateExtensions]::GetECDsaPublicKey($publicCertificate)
            $hashResult = $ecdsa.VerifyHash($hashedDataBytes, $jwsSignatureBytes, [System.Security.Cryptography.DSASignatureFormat]::Rfc3279DerSequence)

        }
    }

    $fgc = if ($hashResult) { "green" } else { "red" }
    Write-Host "Hash result: $hashResult" -ForegroundColor $fgc
} else {
    Write-Warning -Message "No jku or x5c found."
}

When you execute the script and pass in the attestation token, it will give you the following output:

.\Confirm-AttestationTokenSignature.ps1 -Jwt "eyJhbGciOiJSUzI1NiIsImprdSI6Imh0dHBzOi8vc2hhcmVkd2V1LndldS5hdHRlc3QuYXp1cmUubmV0L2NlcnRzIiwia2lkIjoiZFJLaCtoQmNXVWZRaW1TbDNJdjZaaFN0VzNUU090MFRod2lUZ1VVcVpBbz0iLCJ0eXAiOiJKV1QifQ.eyJleHAiOjE2NzE4NjUyMTgsImlhdCI6MTY3MTgzNjQxOCwiaXNzIjoiaHR0cHM6Ly9zaGFyZWR3ZXUud2V1LmF0dGVzdC5henVyZS5uZXQiLCJqdGkiOiJjZTM5NWU1ZGU5YzYzOGQzODRjZDNiZDA2MDQxZTY3NGVkZWU4MjAzMDU1OTZiYmEzMDI5MTc1YWYyMDE4ZGEwIiwibmJmIjoxNjcxODM2NDE4LCJzZWN1cmVib290Ijp0cnVlLCJ4LW1zLWF0dGVzdGF0aW9uLXR5cGUiOiJhenVyZXZtIiwieC1tcy1henVyZXZtLWF0dGVzdGF0aW9uLXByb3RvY29sLXZlciI6IjIuMCIsIngtbXMtYXp1cmV2bS1hdHRlc3RlZC1wY3JzIjpbMCwxLDIsMyw0LDUsNiw3XSwieC1tcy1henVyZXZtLWJvb3RkZWJ1Zy1lbmFibGVkIjpmYWxzZSwieC1tcy1henVyZXZtLWRidmFsaWRhdGVkIjp0cnVlLCJ4LW1zLWF6dXJldm0tZGJ4dmFsaWRhdGVkIjp0cnVlLCJ4LW1zLWF6dXJldm0tZGVidWdnZXJzZGlzYWJsZWQiOnRydWUsIngtbXMtYXp1cmV2bS1kZWZhdWx0LXNlY3VyZWJvb3RrZXlzdmFsaWRhdGVkIjp0cnVlLCJ4LW1zLWF6dXJldm0tZWxhbS1lbmFibGVkIjpmYWxzZSwieC1tcy1henVyZXZtLWZsaWdodHNpZ25pbmctZW5hYmxlZCI6ZmFsc2UsIngtbXMtYXp1cmV2bS1odmNpLXBvbGljeSI6MCwieC1tcy1henVyZXZtLWh5cGVydmlzb3JkZWJ1Zy1lbmFibGVkIjpmYWxzZSwieC1tcy1henVyZXZtLWlzLXdpbmRvd3MiOmZhbHNlLCJ4LW1zLWF6dXJldm0ta2VybmVsZGVidWctZW5hYmxlZCI6ZmFsc2UsIngtbXMtYXp1cmV2bS1vc2J1aWxkIjoiTm90QXBwbGljYXRpb24iLCJ4LW1zLWF6dXJldm0tb3NkaXN0cm8iOiJVYnVudHUiLCJ4LW1zLWF6dXJldm0tb3N0eXBlIjoiTGludXgiLCJ4LW1zLWF6dXJldm0tb3N2ZXJzaW9uLW1ham9yIjoyMCwieC1tcy1henVyZXZtLW9zdmVyc2lvbi1taW5vciI6NCwieC1tcy1henVyZXZtLXNpZ25pbmdkaXNhYmxlZCI6dHJ1ZSwieC1tcy1henVyZXZtLXRlc3RzaWduaW5nLWVuYWJsZWQiOmZhbHNlLCJ4LW1zLWF6dXJldm0tdm1pZCI6IjY1MDZCNTMxLTE2MzQtNDMxRS05OUQyLTQyQjdEMzQxNEFEMCIsIngtbXMtaXNvbGF0aW9uLXRlZSI6eyJ4LW1zLWF0dGVzdGF0aW9uLXR5cGUiOiJzZXZzbnB2bSIsIngtbXMtY29tcGxpYW5jZS1zdGF0dXMiOiJhenVyZS1jb21wbGlhbnQtY3ZtIiwieC1tcy1ydW50aW1lIjp7ImtleXMiOlt7ImUiOiJBUUFCIiwia2V5X29wcyI6WyJlbmNyeXB0Il0sImtpZCI6IkhDTEFrUHViIiwia3R5IjoiUlNBIiwibiI6InRYa1JMQUFCUTd2Z1g5NjQySjJqUzJsMW03MFlNcDl3Nnd4U2dPWVdzZmhpZkNub0Z6SC1pd2llLXUwNmhxZnVQa0hQQ29GZjBoUzN6R0VvbFJmLVNwc1daWTRvQ0s3bjNBR0tHZmRKNFJ4eVhwaHhDVTRKNlU0SDdpUGQ1MWRQTTFGalBySkVyMXRXRTlnQ00teTF5MFZpbTN2Y0FwOG43MElGWHRIdi1LdlpkczlYMFdWZUdPY0tNSk04SlQ2ZzcxazFFY1E0bWQ2Zk02NEpaVDF6VGtwNk41OG5rcUYweENtZkEzcmJYbFBValNKOEEtR1BYUTYxdFRnd1FFTURheFkxamRyWUNWQ1BSZ0pacnliTEVBc2pKWk5RNlVIeHlYMHNFNW5iaGtsb0loQlgzWE5YajVRbGxxZkZGSlhfZlk5SnJmWFF6VzAxYnNWSGswZTFPUSJ9XSwidm0tY29uZmlndXJhdGlvbiI6eyJjb25zb2xlLWVuYWJsZWQiOnRydWUsImN1cnJlbnQtdGltZSI6MTY3MTgzNTU0OCwic2VjdXJlLWJvb3QiOnRydWUsInRwbS1lbmFibGVkIjp0cnVlLCJ2bVVuaXF1ZUlkIjoiNjUwNkI1MzEtMTYzNC00MzFFLTk5RDItNDJCN0QzNDE0QUQwIn19LCJ4LW1zLXNldnNucHZtLWF1dGhvcmtleWRpZ2VzdCI6IjAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMCIsIngtbXMtc2V2c25wdm0tYm9vdGxvYWRlci1zdm4iOjMsIngtbXMtc2V2c25wdm0tZmFtaWx5SWQiOiIwMTAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMCIsIngtbXMtc2V2c25wdm0tZ3Vlc3Rzdm4iOjIsIngtbXMtc2V2c25wdm0taG9zdGRhdGEiOiIwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwIiwieC1tcy1zZXZzbnB2bS1pZGtleWRpZ2VzdCI6IjU3NDg2YTQ0N2VjMGYxOTU4MDAyYTIyYTA2Yjc2NzNiOWZkMjdkMTFlMWM2NTI3NDk4MDU2MDU0YzVmYTkyZDIzYzUwZjlkZTQ0MDcyNzYwZmUyYjZmYjg5NzQwYjY5NiIsIngtbXMtc2V2c25wdm0taW1hZ2VJZCI6IjAyMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwIiwieC1tcy1zZXZzbnB2bS1pcy1kZWJ1Z2dhYmxlIjpmYWxzZSwieC1tcy1zZXZzbnB2bS1sYXVuY2htZWFzdXJlbWVudCI6ImFkNmRlMTZhYzU5ZWU1MjM1MWM2MDM4ZGY1OGQxYmU1YWVhZjQxY2QwZjdjODFiMjI3OWVjY2EwZGY2ZWY0M2EyYjY5ZDY2M2FkNjk3M2Q2ZGJiOWRiMGZmZDdhOTAyMyIsIngtbXMtc2V2c25wdm0tbWljcm9jb2RlLXN2biI6MTE1LCJ4LW1zLXNldnNucHZtLW1pZ3JhdGlvbi1hbGxvd2VkIjpmYWxzZSwieC1tcy1zZXZzbnB2bS1yZXBvcnRkYXRhIjoiYzY1MDA4NTlhZjk1NDQwMjA2YWFjNWU5M2ViNTBhMGYyY2ZkNGZhMmM1NDg1ZTA1YTVjNzdhNWQ4MWMzZGVlMzAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAiLCJ4LW1zLXNldnNucHZtLXJlcG9ydGlkIjoiY2Y1ZWE3NDJmMDhjYjQ1MjQwZThhZDQ3MTliNjExNTAyOGYzZTFkOWQ4ODE3NWEyNDdlYjdjNmM4NmRhNjQ5MyIsIngtbXMtc2V2c25wdm0tc210LWFsbG93ZWQiOnRydWUsIngtbXMtc2V2c25wdm0tc25wZnctc3ZuIjo4LCJ4LW1zLXNldnNucHZtLXRlZS1zdm4iOjAsIngtbXMtc2V2c25wdm0tdm1wbCI6MH0sIngtbXMtcG9saWN5LWhhc2giOiJ3bTltSGx2VFU4MmU4VXFvT3kxWWoxRkJSU05rZmU5OS02OUlZRHE5ZVdzIiwieC1tcy1ydW50aW1lIjp7ImNsaWVudC1wYXlsb2FkIjp7Im5vbmNlIjoiIn0sImtleXMiOlt7ImUiOiJBUUFCIiwia2V5X29wcyI6WyJlbmNyeXB0Il0sImtpZCI6IlRwbUVwaGVtZXJhbEVuY3J5cHRpb25LZXkiLCJrdHkiOiJSU0EiLCJuIjoia1ZUTFN3QUFRcGd0bHFrd1JyRFhoRGdfYzFNZmhSWEkzeE5QbENWMWVWbEVoNWVybE1jS1oxcl9GVV9yMXFmamZiWGd3cmFMYldSQTBpUGlkdnN2ZXJHMDhVRmlBazc2bjlIclNHcVFzendTWDNNRzhUblNtTEU4bEc3N0t2OGx5TXhDN0N5LTlnN05fMXpiMGxHX3doOW1DSG1IVGdJSXAxTHU2WFNOb2tza3F4QUJVV1VxQjcxekZORWV0THNfNktNV0dCd2o3d1lQR0J0Y21ZV0VDeGYwUUprNDdxR0Z0UEZiSU40SEg4MVFKakJBSjA1OEo5Nk15b3ZFNlZOZkdEWEZRSEZ5XzJ3S0JJTzcwTzBLTm11RFVrUWdwaklWRVcxbGt1c2JOUnR4VU91VVJmUWlOaWpKaXRoeFdud1d5ZVdRc0xGaFNoeU8wVDljWDVPMHBRIn1dfSwieC1tcy12ZXIiOiIxLjAifQ.DTCCvMi2bZrK1BWBu1FTDxKoFnE9iQdbti_zvJlyHATjoc9rrCcCP8it_tfMwY1ZquILPDSWqQmXW9O0Dva3x06KhMUuX6begrpN8LROKM0_9n1Zy2Rxg3bnlxhzNV0c6neMeC2bvcGAf2Ikej6EaKX7KnZ4Y4cME_iLrDNbLIyq7sZCrUrZNJtjVuzvWQ03n4dFyZTgco1LIdlgzVZB50HFopTA67asW8SvWkl3RHYmXF1wYaujqTxXDvzhFZbyrLQF1S7da74XVj65mpFcSOkqXb28NYkBndxvfjVsyI-b8UiLYU9WhscNCZBZCfXszqB69ySxvWQGOuoQfHBTAQ"

# Using algorithm 'RS256'..
# Getting certificates from 'https://sharedweu.weu.attest.azure.net/certs'..
# Found matching certificate for kid 'dRKh+hBcWUfQimSl3Iv6ZhStW3TSOt0ThwiTgUUqZAo='!
# Hash result: True

And should we tamper with the message, the validation will fail.

.\Confirm-AttestationTokenSignature.ps1 -Jwt "eyJhbGciOiJSUzI1NiIsImprdSI6Imh0dHBzOi8vc2hhcmVkd2V1LndldS5hdHRlc3QuYXp1cmUubmV0L2NlcnRzIiwia2lkIjoiZFJLaCtoQmNXVWZRaW1TbDNJdjZaaFN0VzNUU090MFRod2lUZ1VVcVpBbz0iLCJ0eXAiOiJKV1QifQ.eyJHELLOTHERE.DTCCvMi2bZrK1BWBu1FTDxKoFnE9iQdbti_zvJlyHATjoc9rrCcCP8it_tfMwY1ZquILPDSWqQmXW9O0Dva3x06KhMUuX6begrpN8LROKM0_9n1Zy2Rxg3bnlxhzNV0c6neMeC2bvcGAf2Ikej6EaKX7KnZ4Y4cME_iLrDNbLIyq7sZCrUrZNJtjVuzvWQ03n4dFyZTgco1LIdlgzVZB50HFopTA67asW8SvWkl3RHYmXF1wYaujqTxXDvzhFZbyrLQF1S7da74XVj65mpFcSOkqXb28NYkBndxvfjVsyI-b8UiLYU9WhscNCZBZCfXszqB69ySxvWQGOuoQfHBTAQ"

# Using algorithm 'RS256'..
# Getting certificates from 'https://sharedweu.weu.attest.azure.net/certs'..
# Found matching certificate for kid 'dRKh+hBcWUfQimSl3Iv6ZhStW3TSOt0ThwiTgUUqZAo='!
# Hash result: False

In closing

The idea of writing a script to validate a JWT seemed exciting at first, but as I delved deeper into the process, I realized that it could be a daunting task. The RFC itself revealed that multiple algorithms can be utilized to sign a JWT, such as RSA, ECDSA, and HMAC. It left me wondering if I needed to go through the entire JWA specification to make this one sample work. While building a script to validate a JWT seemed like a good idea, I quickly realized that it would be a significant undertaking. The JWA specification defines many algorithms that can be used to sign a JWT, and I would need to learn them all. Nevertheless, I felt that it would be beneficial to try building the script myself as a learning exercise.

However, if you’re looking to build something similar, I would recommend checking out the curated list of libraries available on JWT.io’s website for token signing and verification. These libraries have been developed with careful consideration of the many issues that come with building your implementation. Instead of building from scratch, you could explore these libraries and find one that fits your needs. If you’re using .NET, you could also consider using the System.IdentityModel.Tokens.Jwt library provided by the IdentityModel extensions for .NET. While it may be possible to import this library into PowerShell with some tweaking, it’s always a good idea to carefully consider whether building your implementation is the best use of your time and resources.

Unless I’m mistaken, I don’t think the Azure documentation for Confidential Computing covers this particular topic, possibly because it relies on the JSON Web Token standard. Initially, I thought that reading through the IEEE RFC documents for JSON Web Tokens, JSON Web Signatures, and JSON Web Algorithms would be a daunting task. However, it turned out to be less challenging than I anticipated because it was excellently written.

Needless to say, going through this process gave me a deeper appreciation for the individuals who maintain the libraries that enable us to work with JWTs more efficiently. 😁