1 // Copyright 2021 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.testing; 18 19 import com.google.crypto.tink.InsecureSecretKeyAccess; 20 import com.google.crypto.tink.KeysetHandle; 21 import com.google.crypto.tink.TinkProtoKeysetFormat; 22 import com.google.crypto.tink.jwt.JwkSetConverter; 23 import com.google.crypto.tink.jwt.JwtInvalidException; 24 import com.google.crypto.tink.jwt.JwtMac; 25 import com.google.crypto.tink.jwt.JwtMacConfig; 26 import com.google.crypto.tink.jwt.JwtPublicKeySign; 27 import com.google.crypto.tink.jwt.JwtPublicKeyVerify; 28 import com.google.crypto.tink.jwt.JwtSignatureConfig; 29 import com.google.crypto.tink.jwt.JwtValidator; 30 import com.google.crypto.tink.jwt.RawJwt; 31 import com.google.crypto.tink.jwt.VerifiedJwt; 32 import com.google.crypto.tink.testing.proto.CreationRequest; 33 import com.google.crypto.tink.testing.proto.CreationResponse; 34 import com.google.crypto.tink.testing.proto.JwtClaimValue; 35 import com.google.crypto.tink.testing.proto.JwtFromJwkSetRequest; 36 import com.google.crypto.tink.testing.proto.JwtFromJwkSetResponse; 37 import com.google.crypto.tink.testing.proto.JwtGrpc.JwtImplBase; 38 import com.google.crypto.tink.testing.proto.JwtSignRequest; 39 import com.google.crypto.tink.testing.proto.JwtSignResponse; 40 import com.google.crypto.tink.testing.proto.JwtToJwkSetRequest; 41 import com.google.crypto.tink.testing.proto.JwtToJwkSetResponse; 42 import com.google.crypto.tink.testing.proto.JwtToken; 43 import com.google.crypto.tink.testing.proto.JwtVerifyRequest; 44 import com.google.crypto.tink.testing.proto.JwtVerifyResponse; 45 import com.google.crypto.tink.testing.proto.NullValue; 46 import com.google.protobuf.ByteString; 47 import com.google.protobuf.StringValue; 48 import com.google.protobuf.Timestamp; 49 import io.grpc.stub.StreamObserver; 50 import java.io.IOException; 51 import java.security.GeneralSecurityException; 52 import java.time.Clock; 53 import java.time.Duration; 54 import java.time.Instant; 55 import java.time.ZoneOffset; 56 import java.util.Map; 57 58 /** Implements a gRPC JWT Testing service. */ 59 public final class JwtServiceImpl extends JwtImplBase { 60 JwtServiceImpl()61 public JwtServiceImpl() throws GeneralSecurityException { 62 JwtMacConfig.register(); 63 JwtSignatureConfig.register(); 64 } 65 66 @Override createJwtMac( CreationRequest request, StreamObserver<CreationResponse> responseObserver)67 public void createJwtMac( 68 CreationRequest request, StreamObserver<CreationResponse> responseObserver) { 69 Util.createPrimitiveForRpc(request, responseObserver, JwtMac.class); 70 } 71 72 @Override createJwtPublicKeySign( CreationRequest request, StreamObserver<CreationResponse> responseObserver)73 public void createJwtPublicKeySign( 74 CreationRequest request, StreamObserver<CreationResponse> responseObserver) { 75 Util.createPrimitiveForRpc(request, responseObserver, JwtPublicKeySign.class); 76 } 77 78 @Override createJwtPublicKeyVerify( CreationRequest request, StreamObserver<CreationResponse> responseObserver)79 public void createJwtPublicKeyVerify( 80 CreationRequest request, StreamObserver<CreationResponse> responseObserver) { 81 Util.createPrimitiveForRpc(request, responseObserver, JwtPublicKeyVerify.class); 82 } 83 timestampToInstant(Timestamp t)84 private Instant timestampToInstant(Timestamp t) { 85 return Instant.ofEpochMilli(t.getSeconds() * 1000 + t.getNanos() / 1000000); 86 } 87 instantToTimestamp(Instant i)88 private Timestamp instantToTimestamp(Instant i) { 89 long millis = i.toEpochMilli(); 90 long seconds = millis / 1000; 91 int nanos = (int) ((millis - seconds * 1000) * 1000000); 92 return Timestamp.newBuilder().setSeconds(seconds).setNanos(nanos).build(); 93 } 94 convertJwtTokenToRawJwt(JwtToken token)95 private RawJwt convertJwtTokenToRawJwt(JwtToken token) throws JwtInvalidException { 96 RawJwt.Builder rawJwtBuilder = RawJwt.newBuilder(); 97 if (token.hasTypeHeader()) { 98 rawJwtBuilder.setTypeHeader(token.getTypeHeader().getValue()); 99 } 100 if (token.hasIssuer()) { 101 rawJwtBuilder.setIssuer(token.getIssuer().getValue()); 102 } 103 if (token.hasSubject()) { 104 rawJwtBuilder.setSubject(token.getSubject().getValue()); 105 } 106 for (String audience : token.getAudiencesList()) { 107 rawJwtBuilder.addAudience(audience); 108 } 109 if (token.hasJwtId()) { 110 rawJwtBuilder.setJwtId(token.getJwtId().getValue()); 111 } 112 if (token.hasExpiration()) { 113 rawJwtBuilder.setExpiration(timestampToInstant(token.getExpiration())); 114 } else { 115 rawJwtBuilder.withoutExpiration(); 116 } 117 if (token.hasNotBefore()) { 118 rawJwtBuilder.setNotBefore(timestampToInstant(token.getNotBefore())); 119 } 120 if (token.hasIssuedAt()) { 121 rawJwtBuilder.setIssuedAt(timestampToInstant(token.getIssuedAt())); 122 } 123 for (Map.Entry<String, JwtClaimValue> entry : token.getCustomClaimsMap().entrySet()) { 124 String name = entry.getKey(); 125 JwtClaimValue value = entry.getValue(); 126 switch (value.getKindCase().getNumber()) { 127 case JwtClaimValue.NULL_VALUE_FIELD_NUMBER: 128 rawJwtBuilder.addNullClaim(name); 129 break; 130 case JwtClaimValue.BOOL_VALUE_FIELD_NUMBER: 131 rawJwtBuilder.addBooleanClaim(name, value.getBoolValue()); 132 break; 133 case JwtClaimValue.NUMBER_VALUE_FIELD_NUMBER: 134 rawJwtBuilder.addNumberClaim(name, value.getNumberValue()); 135 break; 136 case JwtClaimValue.STRING_VALUE_FIELD_NUMBER: 137 rawJwtBuilder.addStringClaim(name, value.getStringValue()); 138 break; 139 case JwtClaimValue.JSON_ARRAY_VALUE_FIELD_NUMBER: 140 rawJwtBuilder.addJsonArrayClaim(name, value.getJsonArrayValue()); 141 break; 142 case JwtClaimValue.JSON_OBJECT_VALUE_FIELD_NUMBER: 143 rawJwtBuilder.addJsonObjectClaim(name, value.getJsonObjectValue()); 144 break; 145 default: 146 throw new RuntimeException("Unknown JwtClaimValue kind: " + value.getKindCase()); 147 } 148 } 149 return rawJwtBuilder.build(); 150 } 151 computeMacAndEncode(JwtSignRequest request)152 private JwtSignResponse computeMacAndEncode(JwtSignRequest request) 153 throws GeneralSecurityException { 154 JwtMac jwtMac = 155 Util.parseBinaryProtoKeyset(request.getAnnotatedKeyset()) 156 .getPrimitive(JwtMac.class); 157 try { 158 RawJwt rawJwt = convertJwtTokenToRawJwt(request.getRawJwt()); 159 String signedCompactJwt = jwtMac.computeMacAndEncode(rawJwt); 160 return JwtSignResponse.newBuilder().setSignedCompactJwt(signedCompactJwt).build(); 161 } catch (GeneralSecurityException e) { 162 return JwtSignResponse.newBuilder().setErr(e.toString()).build(); 163 } 164 } 165 166 @Override computeMacAndEncode( JwtSignRequest request, StreamObserver<JwtSignResponse> responseObserver)167 public void computeMacAndEncode( 168 JwtSignRequest request, StreamObserver<JwtSignResponse> responseObserver) { 169 try { 170 JwtSignResponse response = computeMacAndEncode(request); 171 responseObserver.onNext(response); 172 responseObserver.onCompleted(); 173 } catch (Exception e) { 174 responseObserver.onError(e); 175 } 176 } 177 publicKeySignAndEncode(JwtSignRequest request)178 private JwtSignResponse publicKeySignAndEncode(JwtSignRequest request) 179 throws GeneralSecurityException { 180 JwtPublicKeySign signer = 181 Util.parseBinaryProtoKeyset(request.getAnnotatedKeyset()) 182 .getPrimitive(JwtPublicKeySign.class); 183 try { 184 RawJwt rawJwt = convertJwtTokenToRawJwt(request.getRawJwt()); 185 String signedCompactJwt = signer.signAndEncode(rawJwt); 186 return JwtSignResponse.newBuilder().setSignedCompactJwt(signedCompactJwt).build(); 187 } catch (GeneralSecurityException e) { 188 return JwtSignResponse.newBuilder().setErr(e.toString()).build(); 189 } 190 } 191 192 @Override publicKeySignAndEncode( JwtSignRequest request, StreamObserver<JwtSignResponse> responseObserver)193 public void publicKeySignAndEncode( 194 JwtSignRequest request, StreamObserver<JwtSignResponse> responseObserver) { 195 try { 196 JwtSignResponse response = publicKeySignAndEncode(request); 197 responseObserver.onNext(response); 198 responseObserver.onCompleted(); 199 } catch (Exception e) { 200 responseObserver.onError(e); 201 } 202 } 203 addCustomClaimToBuilder(VerifiedJwt token, String name, JwtToken.Builder builder)204 private void addCustomClaimToBuilder(VerifiedJwt token, String name, JwtToken.Builder builder) 205 throws JwtInvalidException { 206 // We do not know the type, so we just try them one by one. 207 if (token.isNullClaim(name)) { 208 builder.putCustomClaims( 209 name, JwtClaimValue.newBuilder().setNullValue(NullValue.NULL_VALUE).build()); 210 return; 211 } 212 if (token.hasStringClaim(name)) { 213 String value = token.getStringClaim(name); 214 builder.putCustomClaims(name, JwtClaimValue.newBuilder().setStringValue(value).build()); 215 return; 216 } 217 if (token.hasNumberClaim(name)) { 218 Double value = token.getNumberClaim(name); 219 builder.putCustomClaims(name, JwtClaimValue.newBuilder().setNumberValue(value).build()); 220 return; 221 } 222 if (token.hasBooleanClaim(name)) { 223 Boolean value = token.getBooleanClaim(name); 224 builder.putCustomClaims(name, JwtClaimValue.newBuilder().setBoolValue(value).build()); 225 return; 226 } 227 if (token.hasJsonArrayClaim(name)) { 228 String value = token.getJsonArrayClaim(name); 229 builder.putCustomClaims(name, JwtClaimValue.newBuilder().setJsonArrayValue(value).build()); 230 return; 231 } 232 if (token.hasJsonObjectClaim(name)) { 233 String value = token.getJsonObjectClaim(name); 234 builder.putCustomClaims(name, JwtClaimValue.newBuilder().setJsonObjectValue(value).build()); 235 return; 236 } 237 throw new RuntimeException("unable to add claim " + name); 238 } 239 convertVerifiedJwtToJwtToken(VerifiedJwt verifiedJwt)240 private JwtToken convertVerifiedJwtToJwtToken(VerifiedJwt verifiedJwt) 241 throws JwtInvalidException { 242 JwtToken.Builder builder = JwtToken.newBuilder(); 243 if (verifiedJwt.hasTypeHeader()) { 244 builder.setTypeHeader(StringValue.newBuilder().setValue(verifiedJwt.getTypeHeader())); 245 } 246 if (verifiedJwt.hasIssuer()) { 247 builder.setIssuer(StringValue.newBuilder().setValue(verifiedJwt.getIssuer())); 248 } 249 if (verifiedJwt.hasSubject()) { 250 builder.setSubject(StringValue.newBuilder().setValue(verifiedJwt.getSubject())); 251 } 252 if (verifiedJwt.hasAudiences()) { 253 for (String audience : verifiedJwt.getAudiences()) { 254 builder.addAudiences(audience); 255 } 256 } 257 if (verifiedJwt.hasJwtId()) { 258 builder.setJwtId(StringValue.newBuilder().setValue(verifiedJwt.getJwtId())); 259 } 260 if (verifiedJwt.hasExpiration()) { 261 builder.setExpiration(instantToTimestamp(verifiedJwt.getExpiration())); 262 } 263 if (verifiedJwt.hasNotBefore()) { 264 builder.setNotBefore(instantToTimestamp(verifiedJwt.getNotBefore())); 265 } 266 if (verifiedJwt.hasIssuedAt()) { 267 builder.setIssuedAt(instantToTimestamp(verifiedJwt.getIssuedAt())); 268 } 269 for (String claimName : verifiedJwt.customClaimNames()) { 270 addCustomClaimToBuilder(verifiedJwt, claimName, builder); 271 } 272 return builder.build(); 273 } 274 convertProtoValidatorToValidator( com.google.crypto.tink.testing.proto.JwtValidator validator)275 private JwtValidator convertProtoValidatorToValidator( 276 com.google.crypto.tink.testing.proto.JwtValidator validator) throws JwtInvalidException { 277 JwtValidator.Builder validatorBuilder = JwtValidator.newBuilder(); 278 if (validator.hasExpectedTypeHeader()) { 279 validatorBuilder.expectTypeHeader(validator.getExpectedTypeHeader().getValue()); 280 } 281 if (validator.hasExpectedIssuer()) { 282 validatorBuilder.expectIssuer(validator.getExpectedIssuer().getValue()); 283 } 284 if (validator.hasExpectedAudience()) { 285 validatorBuilder.expectAudience(validator.getExpectedAudience().getValue()); 286 } 287 if (validator.getIgnoreTypeHeader()) { 288 validatorBuilder.ignoreTypeHeader(); 289 } 290 if (validator.getIgnoreIssuer()) { 291 validatorBuilder.ignoreIssuer(); 292 } 293 if (validator.getIgnoreAudience()) { 294 validatorBuilder.ignoreAudiences(); 295 } 296 if (validator.getAllowMissingExpiration()) { 297 validatorBuilder.allowMissingExpiration(); 298 } 299 if (validator.getExpectIssuedInThePast()) { 300 validatorBuilder.expectIssuedInThePast(); 301 } 302 if (validator.hasNow()) { 303 Instant now = timestampToInstant(validator.getNow()); 304 validatorBuilder.setClock(Clock.fixed(now, ZoneOffset.UTC)); 305 } 306 if (validator.hasClockSkew()) { 307 validatorBuilder.setClockSkew(Duration.ofSeconds(validator.getClockSkew().getSeconds())); 308 } 309 return validatorBuilder.build(); 310 } 311 verifyMacAndDecode(JwtVerifyRequest request)312 private JwtVerifyResponse verifyMacAndDecode(JwtVerifyRequest request) 313 throws GeneralSecurityException { 314 JwtMac jwtMac = 315 Util.parseBinaryProtoKeyset(request.getAnnotatedKeyset()) 316 .getPrimitive(JwtMac.class); 317 try { 318 JwtValidator validator = convertProtoValidatorToValidator(request.getValidator()); 319 VerifiedJwt verifiedJwt = jwtMac.verifyMacAndDecode(request.getSignedCompactJwt(), validator); 320 JwtToken token = convertVerifiedJwtToJwtToken(verifiedJwt); 321 return JwtVerifyResponse.newBuilder().setVerifiedJwt(token).build(); 322 } catch (GeneralSecurityException e) { 323 return JwtVerifyResponse.newBuilder().setErr(e.toString()).build(); 324 } 325 } 326 327 @Override verifyMacAndDecode( JwtVerifyRequest request, StreamObserver<JwtVerifyResponse> responseObserver)328 public void verifyMacAndDecode( 329 JwtVerifyRequest request, 330 StreamObserver<JwtVerifyResponse> responseObserver) { 331 try { 332 JwtVerifyResponse response = verifyMacAndDecode(request); 333 responseObserver.onNext(response); 334 responseObserver.onCompleted(); 335 } catch (Exception e) { 336 responseObserver.onError(e); 337 } 338 } 339 publicKeyVerifyAndDecode(JwtVerifyRequest request)340 private JwtVerifyResponse publicKeyVerifyAndDecode(JwtVerifyRequest request) 341 throws GeneralSecurityException { 342 JwtPublicKeyVerify verifier = 343 Util.parseBinaryProtoKeyset(request.getAnnotatedKeyset()) 344 .getPrimitive(JwtPublicKeyVerify.class); 345 try { 346 JwtValidator validator = convertProtoValidatorToValidator(request.getValidator()); 347 VerifiedJwt verifiedJwt = verifier.verifyAndDecode(request.getSignedCompactJwt(), validator); 348 JwtToken token = convertVerifiedJwtToJwtToken(verifiedJwt); 349 return JwtVerifyResponse.newBuilder().setVerifiedJwt(token).build(); 350 } catch (GeneralSecurityException e) { 351 return JwtVerifyResponse.newBuilder().setErr(e.toString()).build(); 352 } 353 } 354 355 @Override publicKeyVerifyAndDecode( JwtVerifyRequest request, StreamObserver<JwtVerifyResponse> responseObserver)356 public void publicKeyVerifyAndDecode( 357 JwtVerifyRequest request, 358 StreamObserver<JwtVerifyResponse> responseObserver) { 359 try { 360 JwtVerifyResponse response = publicKeyVerifyAndDecode(request); 361 responseObserver.onNext(response); 362 responseObserver.onCompleted(); 363 } catch (GeneralSecurityException e) { 364 responseObserver.onError(e); 365 } 366 } 367 368 /** Converts a Tink JWT Keyset to a JWK set. */ 369 @Override toJwkSet( JwtToJwkSetRequest request, StreamObserver<JwtToJwkSetResponse> responseObserver)370 public void toJwkSet( 371 JwtToJwkSetRequest request, StreamObserver<JwtToJwkSetResponse> responseObserver) { 372 JwtToJwkSetResponse response; 373 try { 374 KeysetHandle keysetHandle = 375 TinkProtoKeysetFormat.parseKeyset( 376 request.getKeyset().toByteArray(), InsecureSecretKeyAccess.get()); 377 String jwkSet = JwkSetConverter.fromPublicKeysetHandle(keysetHandle); 378 response = JwtToJwkSetResponse.newBuilder().setJwkSet(jwkSet).build(); 379 } catch (GeneralSecurityException | IOException e) { 380 response = JwtToJwkSetResponse.newBuilder().setErr(e.toString()).build(); 381 } 382 responseObserver.onNext(response); 383 responseObserver.onCompleted(); 384 } 385 386 /** Converts a JWK set to a Tink JWT Keyset. */ 387 @Override fromJwkSet( JwtFromJwkSetRequest request, StreamObserver<JwtFromJwkSetResponse> responseObserver)388 public void fromJwkSet( 389 JwtFromJwkSetRequest request, StreamObserver<JwtFromJwkSetResponse> responseObserver) { 390 JwtFromJwkSetResponse response; 391 try { 392 KeysetHandle keysetHandle = JwkSetConverter.toPublicKeysetHandle(request.getJwkSet()); 393 byte[] serializedKeyset = 394 TinkProtoKeysetFormat.serializeKeyset(keysetHandle, InsecureSecretKeyAccess.get()); 395 response = 396 JwtFromJwkSetResponse.newBuilder() 397 .setKeyset(ByteString.copyFrom(serializedKeyset)) 398 .build(); 399 } catch (GeneralSecurityException | IOException e) { 400 response = JwtFromJwkSetResponse.newBuilder().setErr(e.toString()).build(); 401 return; 402 } 403 responseObserver.onNext(response); 404 responseObserver.onCompleted(); 405 } 406 } 407