|
| 1 | +# JWT Token Validation |
| 2 | + |
| 3 | +JSON Web Token (JWT) is a term standing for two implementations: JSON Web Signature (JWS) and JSON Web Encryption (JWE). |
| 4 | +JWE is not very common, and most of the time you will see JWS. |
| 5 | + |
| 6 | +## JWS Basics |
| 7 | + |
| 8 | +A token consists of three parts: |
| 9 | + |
| 10 | +1. Header |
| 11 | +2. Payload |
| 12 | +3. Signature |
| 13 | + |
| 14 | +Header and Payload are base64 encoded JSON documents whose contents are based on [RFC7519](https://tools.ietf.org/html/rfc7519). |
| 15 | +The signature is created by hashing header and payload and then signing the result with a private key and base64 encoding the result. |
| 16 | +The JWS presentation is built by concatenating all three parts with dots in between, so in short the result looks like: |
| 17 | +Concat(Base64(Header) + '.' + Base64(Payload) + '.' + Base64(Signature). |
| 18 | + |
| 19 | +Note: To restrict parsing effort the CA only allows EdDSA as the signature algorithm with an Ed25519 key pair. |
| 20 | + |
| 21 | +## JWS in a regular (web-) service context |
| 22 | + |
| 23 | +To verify the token, you need the public key of the signing party. |
| 24 | +In a web service context this key is usually statically configured and only changes when the key is rotated or expired. |
| 25 | +As both header and payload are part of the signature any manipulation of the header or the payload becomes evident immediately. |
| 26 | + |
| 27 | +## JWS in the context of business-partner-agent:web |
| 28 | + |
| 29 | +Here we have the issue that the public key is delivered separately from the token, this opens possibilities for manipulation. |
| 30 | +In this context we have the following token retrieval and validation process: |
| 31 | + |
| 32 | +* Step 1. Resolve the public profile service endpoint via the did document |
| 33 | + |
| 34 | +This depends on the did method, so the did document can be written on a ledger or made available by a web service. |
| 35 | +As did document presentations are not always the same, resolution happens with the help of the universal resolver, |
| 36 | +that (hopefully) returns a normalised presentation of the did document. |
| 37 | + |
| 38 | +Example did document with a profile type endpoint within the service section: |
| 39 | +```json |
| 40 | +{ |
| 41 | + "@context": [ |
| 42 | + "https://www.w3.org/ns/did/v1" |
| 43 | + ], |
| 44 | + "id": "did:sov:F6dB7dMVHUQSC64qemnBi7", |
| 45 | + "verificationMethod": [ |
| 46 | + { |
| 47 | + "id": "did:sov:F6dB7dMVHUQSC64qemnBi7#key-1", |
| 48 | + "type": "Ed25519VerificationKey2018", |
| 49 | + "controller": "did:sov:F6dB7dMVHUQSC64qemnBi7", |
| 50 | + "publicKeyBase58": "8gdhRLtvJHzKoJGyuEqgdN1QZGYfai4wMHFGgtfDXg3D" |
| 51 | + } |
| 52 | + ], |
| 53 | + "service": [ |
| 54 | + { |
| 55 | + "id": "did:sov:F6dB7dMVHUQSC64qemnBi7#did-communication", |
| 56 | + "type": "did-communication", |
| 57 | + "serviceEndpoint": "http://localhost:8080", |
| 58 | + "recipientKeys": [ |
| 59 | + "did:sov:F6dB7dMVHUQSC64qemnBi7#key-1" |
| 60 | + ], |
| 61 | + "routingKeys": [], |
| 62 | + "priority": 1 |
| 63 | + }, |
| 64 | + { |
| 65 | + "id": "did:sov:F6dB7dMVHUQSC64qemnBi7#profile", |
| 66 | + "type": "profile", |
| 67 | + "serviceEndpoint": "http://localhost:8080/profile.jsonld" |
| 68 | + } |
| 69 | + ] |
| 70 | +} |
| 71 | +``` |
| 72 | + |
| 73 | +* Step 2. Resolve the public profile |
| 74 | + |
| 75 | +In this case the profile service endpoint returns a JSON structure that contains the JWT. |
| 76 | + |
| 77 | +``` |
| 78 | +{ |
| 79 | + "decoded": {}, |
| 80 | + "jwt": "" |
| 81 | +} |
| 82 | +``` |
| 83 | + |
| 84 | +* Step 3. Decode token and verify signature |
| 85 | + |
| 86 | +Decoding just means breaking the token down into the three parts mentioned above and verifying its integrity. |
| 87 | +Verification is the tricky part though. |
| 88 | + |
| 89 | +* Step 4. Resolve the public key |
| 90 | + |
| 91 | +If the token is self-signed the key is already part of the did document. |
| 92 | +If the token was signed by a third party the public key needs to be resolved by pulling the did document of the third party. |
| 93 | + |
| 94 | +### Token verification scenarios |
| 95 | + |
| 96 | +A token can be either self-signed or signed by a third party. |
| 97 | +Self-signed in the context of verifiable credentials means subject (sub) == issuer (iss). |
| 98 | +In JWT terms iss === sub. In general, we have to decide if we want to trust an issuer to make specific claim about a subject. |
| 99 | +In the self-signed public profile data case we have (human) trust if issuer == subject. |
| 100 | +But in some cases we would probably not trust did:indy:1 making claims about did:indy:2. |
| 101 | + |
| 102 | +The second step is cryptographic trust, to check if the issuer authenticated the credential. |
| 103 | +This is the part where we have to check that the public key which verifies the JWT is authorized by the issuer. |
| 104 | +Therefore, we have to check if the verifying public key is part of the did document of the issuer |
| 105 | + |
| 106 | +Therefore, we can state that: |
| 107 | + |
| 108 | +1. If there is no kid and iss == sub then the token is considered self-signed |
| 109 | +2. If kid && iss == sub then the token is also considered self-signed, and kid must be part of the issuers did document |
| 110 | +3. If sub != iss the token is signed by a third party |
| 111 | +4. If kid && sub != iss, the token is signed by a third party, and kid must be part of the issuers did document |
| 112 | + |
| 113 | + |
| 114 | +#### Self-signed - no key id in the header |
| 115 | + |
| 116 | +If the JWT is in the following form: |
| 117 | + |
| 118 | +```jsom |
| 119 | +{ |
| 120 | + "typ": "JWT", |
| 121 | + "alg": "EdDSA" |
| 122 | +}, |
| 123 | +{ |
| 124 | + "sub": "did:web:faber.iil.network", |
| 125 | + "iss": "did:web:faber.iil.network" |
| 126 | +} |
| 127 | +``` |
| 128 | + |
| 129 | +The did document must only contain a single key. If there is more than one key element the token is considered invalid. |
| 130 | +If the structure is valid the token signature is validated with the value of "publicKeyBase58". |
| 131 | + |
| 132 | +```json |
| 133 | +{ |
| 134 | + "id": "did:web:faber.iil.network", |
| 135 | + "publicKey": [ |
| 136 | + { |
| 137 | + "id": "did:web:faber.iil.network#key-1", |
| 138 | + "type": "Ed25519VerificationKey2018", |
| 139 | + "publicKeyBase58": "24j9iYZTPRE3L4W5kRixBAEQLHJzfzuHsqiQyCnVEbKZ" |
| 140 | + } |
| 141 | + ] |
| 142 | +} |
| 143 | +``` |
| 144 | + |
| 145 | +#### Self-signed - key id in the header |
| 146 | + |
| 147 | +If the header contains a kid... |
| 148 | + |
| 149 | +``` |
| 150 | +{ |
| 151 | + "kid": "did:web:faber.iil.network#key-1", |
| 152 | + "typ": "JWT", |
| 153 | + "alg": "EdDSA" |
| 154 | +}, |
| 155 | +{ |
| 156 | + "sub": "did:web:faber.iil.network", |
| 157 | + "iss": "did:web:faber.iil.network" |
| 158 | +} |
| 159 | +``` |
| 160 | + |
| 161 | +...the did document is allowed to have multiple keys. If the did document is missing a matching publicKey:id the token is considered invalid. |
| 162 | + |
| 163 | +```json |
| 164 | +{ |
| 165 | + "id": "did:web:faber.iil.network", |
| 166 | + "publicKey": [ |
| 167 | + { |
| 168 | + "id": "did:web:faber.iil.network#key-1", |
| 169 | + "type": "Ed25519VerificationKey2018", |
| 170 | + "publicKeyBase58": "24j9iYZTPRE3L4W5kRixBAEQLHJzfzuHsqiQyCnVEbKZ" |
| 171 | + } |
| 172 | + ] |
| 173 | +} |
| 174 | +``` |
| 175 | + |
| 176 | +#### Signed by third party |
| 177 | + |
| 178 | +Like mentioned above, the public key needs to be resolved by yet another call to the universal resolver to retrieve |
| 179 | +the did document and public key of the third party. The `did` used for resolution is taken from the iss field of the Payload. |
| 180 | +If there is a kid then the same mechanism as above is applied. |
| 181 | + |
| 182 | +This use-case is currently out of scope and validation will fail. |
0 commit comments