1 // Copyright 2023 Google LLC 2 // 3 // Licensed under the Apache License, Version 2.0 (the "License"); 4 // you may not use this file except in compliance with the License. 5 // You may obtain a copy of the License at 6 // 7 // http://www.apache.org/licenses/LICENSE-2.0 8 // 9 // Unless required by applicable law or agreed to in writing, software 10 // distributed under the License is distributed on an "AS IS" BASIS, 11 // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 // See the License for the specific language governing permissions and 13 // limitations under the License. 14 // 15 //////////////////////////////////////////////////////////////////////////////// 16 17 package com.google.crypto.tink.jwt; 18 19 import static com.google.common.truth.Truth.assertThat; 20 import static org.junit.Assert.assertThrows; 21 import static org.junit.Assume.assumeTrue; 22 23 import com.google.crypto.tink.DeterministicAead; 24 import com.google.crypto.tink.InsecureSecretKeyAccess; 25 import com.google.crypto.tink.KeyTemplates; 26 import com.google.crypto.tink.KeysetHandle; 27 import com.google.crypto.tink.RegistryConfiguration; 28 import com.google.crypto.tink.TinkJsonProtoKeysetFormat; 29 import com.google.crypto.tink.daead.DeterministicAeadConfig; 30 import com.google.crypto.tink.testing.TestUtil; 31 import java.security.GeneralSecurityException; 32 import java.time.Clock; 33 import java.time.Instant; 34 import org.junit.BeforeClass; 35 import org.junit.experimental.theories.DataPoints; 36 import org.junit.experimental.theories.FromDataPoints; 37 import org.junit.experimental.theories.Theories; 38 import org.junit.experimental.theories.Theory; 39 import org.junit.runner.RunWith; 40 41 /** Tests the JWT signature primitives. Uses only the public API. */ 42 @RunWith(Theories.class) 43 public final class JwtSignatureTest { 44 45 @BeforeClass setUp()46 public static void setUp() throws Exception { 47 JwtSignatureConfig.register(); 48 DeterministicAeadConfig.register(); // Needed for getPrimitiveFromIncompatbileKeyset_throws. 49 } 50 51 @DataPoints("jwt_signature_templates") 52 public static final String[] TEMPLATES = 53 new String[] { 54 "JWT_ES256", "JWT_ES512_RAW", "JWT_RS256_2048_F4", "JWT_PS256_3072_F4_RAW", 55 }; 56 57 @Theory createSignVerifyJwt(@romDataPoints"jwt_signature_templates") String templateName)58 public void createSignVerifyJwt(@FromDataPoints("jwt_signature_templates") String templateName) 59 throws Exception { 60 if (TestUtil.isTsan()) { 61 // Only run for JWT_E256 under TSAN -- too slow otherwise. 62 assumeTrue(templateName.equals("JWT_ES256")); 63 } 64 KeysetHandle handle = KeysetHandle.generateNew(KeyTemplates.get(templateName)); 65 JwtPublicKeySign jwtPublicKeySign = 66 handle.getPrimitive(RegistryConfiguration.get(), JwtPublicKeySign.class); 67 Instant now = Clock.systemUTC().instant(); 68 RawJwt rawJwt = 69 RawJwt.newBuilder() 70 .setIssuer("issuer") 71 .addAudience("audience") 72 .setSubject("subject") 73 .addStringClaim("claimName", "claimValue") 74 .setExpiration(now.plusSeconds(100)) 75 .build(); 76 String token = jwtPublicKeySign.signAndEncode(rawJwt); 77 78 JwtPublicKeyVerify jwtPublicKeyVerify = 79 handle 80 .getPublicKeysetHandle() 81 .getPrimitive(RegistryConfiguration.get(), JwtPublicKeyVerify.class); 82 83 JwtValidator validator = 84 JwtValidator.newBuilder().expectIssuer("issuer").expectAudience("audience").build(); 85 VerifiedJwt verifiedJwt = jwtPublicKeyVerify.verifyAndDecode(token, validator); 86 assertThat(verifiedJwt.getSubject()).isEqualTo("subject"); 87 assertThat(verifiedJwt.getStringClaim("claimName")).isEqualTo("claimValue"); 88 89 String expiredToken = 90 jwtPublicKeySign.signAndEncode( 91 RawJwt.newBuilder() 92 .setIssuer("issuer") 93 .addAudience("audience") 94 .setExpiration(now.minusSeconds(100)) 95 .build()); 96 assertThrows( 97 GeneralSecurityException.class, 98 () -> jwtPublicKeyVerify.verifyAndDecode(expiredToken, validator)); 99 100 String tokenWithInvalidIssuer = 101 jwtPublicKeySign.signAndEncode( 102 RawJwt.newBuilder() 103 .setIssuer("invalid") 104 .addAudience("audience") 105 .setSubject("subject") 106 .addStringClaim("claimName", "claimValue") 107 .setExpiration(now.minusSeconds(100)) 108 .build()); 109 assertThrows( 110 GeneralSecurityException.class, 111 () -> jwtPublicKeyVerify.verifyAndDecode(tokenWithInvalidIssuer, validator)); 112 113 String tokenWithInvalidAudience = 114 jwtPublicKeySign.signAndEncode( 115 RawJwt.newBuilder() 116 .setIssuer("issuer") 117 .addAudience("invalid") 118 .setSubject("subject") 119 .addStringClaim("claimName", "claimValue") 120 .setExpiration(now.minusSeconds(100)) 121 .build()); 122 assertThrows( 123 GeneralSecurityException.class, 124 () -> jwtPublicKeyVerify.verifyAndDecode(tokenWithInvalidAudience, validator)); 125 126 KeysetHandle otherHandle = KeysetHandle.generateNew(KeyTemplates.get(templateName)); 127 JwtPublicKeyVerify otherJwtPublicKeyVerify = 128 otherHandle 129 .getPublicKeysetHandle() 130 .getPrimitive(RegistryConfiguration.get(), JwtPublicKeyVerify.class); 131 assertThrows( 132 GeneralSecurityException.class, 133 () -> otherJwtPublicKeyVerify.verifyAndDecode(token, validator)); 134 135 assertThrows( 136 GeneralSecurityException.class, 137 () -> jwtPublicKeyVerify.verifyAndDecode("invalid", validator)); 138 assertThrows( 139 GeneralSecurityException.class, () -> jwtPublicKeyVerify.verifyAndDecode("", validator)); 140 } 141 142 // A keyset with one JWT public key sign keyset, serialized in Tink's JSON format. 143 private static final String JSON_JWT_PUBLIC_KEY_SIGN_KEYSET = 144 "{ \"primaryKeyId\": 1742360595, \"key\": [ { \"keyData\": { \"typeUrl\":" 145 + " \"type.googleapis.com/google.crypto.tink.JwtEcdsaPrivateKey\", \"value\":" 146 + " \"GiBgVYdAPg3Fa2FVFymGDYrI1trHMzVjhVNEMpIxG7t0HRJGIiBeoDMF9LS5BDCh6YgqE3DjHwWwnEKEI3WpPf8izEx1rRogbjQTXrTcw/1HKiiZm2Hqv41w7Vd44M9koyY/+VsP+SAQAQ==\"," 147 + " \"keyMaterialType\": \"ASYMMETRIC_PRIVATE\" }, \"status\":" 148 + " \"ENABLED\", \"keyId\": 1742360595, \"outputPrefixType\": \"TINK\" } ]" 149 + "}"; 150 151 // A keyset with one JWT public key verify keyset, serialized in Tink's JSON format. 152 private static final String JSON_JWT_PUBLIC_KEY_VERIFY_KEYSET = 153 "{ \"primaryKeyId\": 1742360595, \"key\": [ { \"keyData\": { \"typeUrl\":" 154 + " \"type.googleapis.com/google.crypto.tink.JwtEcdsaPublicKey\", \"value\":" 155 + " \"EAEaIG40E1603MP9RyoomZth6r+NcO1XeODPZKMmP/lbD/kgIiBeoDMF9LS5BDCh6YgqE3DjHwWwnEKEI3WpPf8izEx1rQ==\"," 156 + " \"keyMaterialType\": \"ASYMMETRIC_PUBLIC\" }, \"status\":" 157 + " \"ENABLED\", \"keyId\": 1742360595, \"outputPrefixType\": \"TINK\" } ]" 158 + "}"; 159 160 @Theory readKeysetSignVerifyJwt_success()161 public void readKeysetSignVerifyJwt_success() throws Exception { 162 KeysetHandle privateHandle = 163 TinkJsonProtoKeysetFormat.parseKeyset( 164 JSON_JWT_PUBLIC_KEY_SIGN_KEYSET, InsecureSecretKeyAccess.get()); 165 Instant now = Clock.systemUTC().instant(); 166 JwtPublicKeySign jwtPublicKeySign = 167 privateHandle.getPrimitive(RegistryConfiguration.get(), JwtPublicKeySign.class); 168 RawJwt rawJwt = 169 RawJwt.newBuilder() 170 .setIssuer("issuer") 171 .addAudience("audience") 172 .setSubject("subject") 173 .setExpiration(now.plusSeconds(100)) 174 .build(); 175 String token = jwtPublicKeySign.signAndEncode(rawJwt); 176 177 KeysetHandle publicHandle = 178 TinkJsonProtoKeysetFormat.parseKeyset( 179 JSON_JWT_PUBLIC_KEY_VERIFY_KEYSET, InsecureSecretKeyAccess.get()); 180 JwtPublicKeyVerify jwtPublicKeyVerify = 181 publicHandle.getPrimitive(RegistryConfiguration.get(), JwtPublicKeyVerify.class); 182 JwtValidator validator = 183 JwtValidator.newBuilder().expectIssuer("issuer").expectAudience("audience").build(); 184 VerifiedJwt verifiedJwt = jwtPublicKeyVerify.verifyAndDecode(token, validator); 185 assertThat(verifiedJwt.getSubject()).isEqualTo("subject"); 186 } 187 188 // A keyset with a valid DeterministicAead key. This keyset can't be used with the 189 // JwtPublicKeySign or JwtPublicKeyVerify primitive. 190 private static final String JSON_DAEAD_KEYSET = 191 "" 192 + "{" 193 + " \"primaryKeyId\": 961932622," 194 + " \"key\": [" 195 + " {" 196 + " \"keyData\": {" 197 + " \"typeUrl\": \"type.googleapis.com/google.crypto.tink.AesSivKey\"," 198 + " \"keyMaterialType\": \"SYMMETRIC\"," 199 + " \"value\": \"EkCJ9r5iwc5uxq5ugFyrHXh5dijTa7qalWUgZ8Gf08RxNd545FjtLMYL7ObcaFtCS" 200 + "kvV2+7u6F2DN+kqUjAfkf2W\"" 201 + " }," 202 + " \"outputPrefixType\": \"TINK\"," 203 + " \"keyId\": 961932622," 204 + " \"status\": \"ENABLED\"" 205 + " }" 206 + " ]" 207 + "}"; 208 209 @Theory getPrimitiveFromIncompatbileKeyset_throws()210 public void getPrimitiveFromIncompatbileKeyset_throws() throws Exception { 211 KeysetHandle handle = 212 TinkJsonProtoKeysetFormat.parseKeyset(JSON_DAEAD_KEYSET, InsecureSecretKeyAccess.get()); 213 Object unused = handle.getPrimitive(RegistryConfiguration.get(), DeterministicAead.class); 214 assertThrows( 215 GeneralSecurityException.class, 216 () -> handle.getPrimitive(RegistryConfiguration.get(), JwtPublicKeySign.class)); 217 assertThrows( 218 GeneralSecurityException.class, 219 () -> handle.getPrimitive(RegistryConfiguration.get(), JwtPublicKeyVerify.class)); 220 } 221 } 222