blob: 3d204add3411ab2408440959a2bccc6d7a343f91 [file] [log] [blame]
// Copyright 2021 Google LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//
////////////////////////////////////////////////////////////////////////////////
package com.google.crypto.tink.testing;
import com.google.crypto.tink.BinaryKeysetReader;
import com.google.crypto.tink.BinaryKeysetWriter;
import com.google.crypto.tink.CleartextKeysetHandle;
import com.google.crypto.tink.KeysetHandle;
import com.google.crypto.tink.jwt.JwkSetConverter;
import com.google.crypto.tink.jwt.JwtInvalidException;
import com.google.crypto.tink.jwt.JwtMac;
import com.google.crypto.tink.jwt.JwtMacConfig;
import com.google.crypto.tink.jwt.JwtPublicKeySign;
import com.google.crypto.tink.jwt.JwtPublicKeyVerify;
import com.google.crypto.tink.jwt.JwtSignatureConfig;
import com.google.crypto.tink.jwt.JwtValidator;
import com.google.crypto.tink.jwt.RawJwt;
import com.google.crypto.tink.jwt.VerifiedJwt;
import com.google.crypto.tink.proto.Keyset;
import com.google.crypto.tink.testing.proto.JwtClaimValue;
import com.google.crypto.tink.testing.proto.JwtFromJwkSetRequest;
import com.google.crypto.tink.testing.proto.JwtFromJwkSetResponse;
import com.google.crypto.tink.testing.proto.JwtGrpc.JwtImplBase;
import com.google.crypto.tink.testing.proto.JwtSignRequest;
import com.google.crypto.tink.testing.proto.JwtSignResponse;
import com.google.crypto.tink.testing.proto.JwtToJwkSetRequest;
import com.google.crypto.tink.testing.proto.JwtToJwkSetResponse;
import com.google.crypto.tink.testing.proto.JwtToken;
import com.google.crypto.tink.testing.proto.JwtVerifyRequest;
import com.google.crypto.tink.testing.proto.JwtVerifyResponse;
import com.google.crypto.tink.testing.proto.NullValue;
import com.google.protobuf.ByteString;
import com.google.protobuf.InvalidProtocolBufferException;
import com.google.protobuf.StringValue;
import com.google.protobuf.Timestamp;
import io.grpc.Status;
import io.grpc.stub.StreamObserver;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.security.GeneralSecurityException;
import java.time.Clock;
import java.time.Duration;
import java.time.Instant;
import java.time.ZoneOffset;
import java.util.Map;
/** Implements a gRPC JWT Testing service. */
public final class JwtServiceImpl extends JwtImplBase {
public JwtServiceImpl() throws GeneralSecurityException {
JwtMacConfig.register();
JwtSignatureConfig.register();
}
private Instant timestampToInstant(Timestamp t) {
return Instant.ofEpochMilli(t.getSeconds() * 1000 + t.getNanos() / 1000000);
}
private Timestamp instantToTimestamp(Instant i) {
long millis = i.toEpochMilli();
long seconds = millis / 1000;
int nanos = (int) ((millis - seconds * 1000) * 1000000);
return Timestamp.newBuilder().setSeconds(seconds).setNanos(nanos).build();
}
private RawJwt convertJwtTokenToRawJwt(JwtToken token) throws JwtInvalidException {
RawJwt.Builder rawJwtBuilder = RawJwt.newBuilder();
if (token.hasTypeHeader()) {
rawJwtBuilder.setTypeHeader(token.getTypeHeader().getValue());
}
if (token.hasIssuer()) {
rawJwtBuilder.setIssuer(token.getIssuer().getValue());
}
if (token.hasSubject()) {
rawJwtBuilder.setSubject(token.getSubject().getValue());
}
for (String audience : token.getAudiencesList()) {
rawJwtBuilder.addAudience(audience);
}
if (token.hasJwtId()) {
rawJwtBuilder.setJwtId(token.getJwtId().getValue());
}
if (token.hasExpiration()) {
rawJwtBuilder.setExpiration(timestampToInstant(token.getExpiration()));
} else {
rawJwtBuilder.withoutExpiration();
}
if (token.hasNotBefore()) {
rawJwtBuilder.setNotBefore(timestampToInstant(token.getNotBefore()));
}
if (token.hasIssuedAt()) {
rawJwtBuilder.setIssuedAt(timestampToInstant(token.getIssuedAt()));
}
for (Map.Entry<String, JwtClaimValue> entry : token.getCustomClaimsMap().entrySet()) {
String name = entry.getKey();
JwtClaimValue value = entry.getValue();
switch (value.getKindCase().getNumber()) {
case JwtClaimValue.NULL_VALUE_FIELD_NUMBER:
rawJwtBuilder.addNullClaim(name);
break;
case JwtClaimValue.BOOL_VALUE_FIELD_NUMBER:
rawJwtBuilder.addBooleanClaim(name, value.getBoolValue());
break;
case JwtClaimValue.NUMBER_VALUE_FIELD_NUMBER:
rawJwtBuilder.addNumberClaim(name, value.getNumberValue());
break;
case JwtClaimValue.STRING_VALUE_FIELD_NUMBER:
rawJwtBuilder.addStringClaim(name, value.getStringValue());
break;
case JwtClaimValue.JSON_ARRAY_VALUE_FIELD_NUMBER:
rawJwtBuilder.addJsonArrayClaim(name, value.getJsonArrayValue());
break;
case JwtClaimValue.JSON_OBJECT_VALUE_FIELD_NUMBER:
rawJwtBuilder.addJsonObjectClaim(name, value.getJsonObjectValue());
break;
default:
throw new RuntimeException("Unknown JwtClaimValue kind: " + value.getKindCase());
}
}
return rawJwtBuilder.build();
}
/** Creates a signed compact JWT. */
@Override
public void computeMacAndEncode(
JwtSignRequest request, StreamObserver<JwtSignResponse> responseObserver) {
JwtSignResponse response;
try {
KeysetHandle keysetHandle =
CleartextKeysetHandle.read(
BinaryKeysetReader.withBytes(request.getKeyset().toByteArray()));
RawJwt rawJwt = convertJwtTokenToRawJwt(request.getRawJwt());
JwtMac jwtMac = keysetHandle.getPrimitive(JwtMac.class);
String signedCompactJwt = jwtMac.computeMacAndEncode(rawJwt);
response = JwtSignResponse.newBuilder().setSignedCompactJwt(signedCompactJwt).build();
} catch (GeneralSecurityException | InvalidProtocolBufferException e) {
response = JwtSignResponse.newBuilder().setErr(e.toString()).build();
} catch (IOException e) {
responseObserver.onError(Status.UNKNOWN.withDescription(e.getMessage()).asException());
return;
}
responseObserver.onNext(response);
responseObserver.onCompleted();
}
/** Creates a signed compact JWT. */
@Override
public void publicKeySignAndEncode(
JwtSignRequest request, StreamObserver<JwtSignResponse> responseObserver) {
JwtSignResponse response;
try {
KeysetHandle keysetHandle =
CleartextKeysetHandle.read(
BinaryKeysetReader.withBytes(request.getKeyset().toByteArray()));
RawJwt rawJwt = convertJwtTokenToRawJwt(request.getRawJwt());
JwtPublicKeySign signer = keysetHandle.getPrimitive(JwtPublicKeySign.class);
String signedCompactJwt = signer.signAndEncode(rawJwt);
response = JwtSignResponse.newBuilder().setSignedCompactJwt(signedCompactJwt).build();
} catch (GeneralSecurityException | InvalidProtocolBufferException e) {
response = JwtSignResponse.newBuilder().setErr(e.toString()).build();
} catch (IOException e) {
responseObserver.onError(Status.UNKNOWN.withDescription(e.getMessage()).asException());
return;
}
responseObserver.onNext(response);
responseObserver.onCompleted();
}
private void addCustomClaimToBuilder(VerifiedJwt token, String name, JwtToken.Builder builder)
throws JwtInvalidException {
// We do not know the type, so we just try them one by one.
if (token.isNullClaim(name)) {
builder.putCustomClaims(
name, JwtClaimValue.newBuilder().setNullValue(NullValue.NULL_VALUE).build());
return;
}
if (token.hasStringClaim(name)) {
String value = token.getStringClaim(name);
builder.putCustomClaims(name, JwtClaimValue.newBuilder().setStringValue(value).build());
return;
}
if (token.hasNumberClaim(name)) {
Double value = token.getNumberClaim(name);
builder.putCustomClaims(name, JwtClaimValue.newBuilder().setNumberValue(value).build());
return;
}
if (token.hasBooleanClaim(name)) {
Boolean value = token.getBooleanClaim(name);
builder.putCustomClaims(name, JwtClaimValue.newBuilder().setBoolValue(value).build());
return;
}
if (token.hasJsonArrayClaim(name)) {
String value = token.getJsonArrayClaim(name);
builder.putCustomClaims(name, JwtClaimValue.newBuilder().setJsonArrayValue(value).build());
return;
}
if (token.hasJsonObjectClaim(name)) {
String value = token.getJsonObjectClaim(name);
builder.putCustomClaims(name, JwtClaimValue.newBuilder().setJsonObjectValue(value).build());
return;
}
throw new RuntimeException("unable to add claim " + name);
}
private JwtToken convertVerifiedJwtToJwtToken(VerifiedJwt verifiedJwt)
throws JwtInvalidException {
JwtToken.Builder builder = JwtToken.newBuilder();
if (verifiedJwt.hasTypeHeader()) {
builder.setTypeHeader(StringValue.newBuilder().setValue(verifiedJwt.getTypeHeader()));
}
if (verifiedJwt.hasIssuer()) {
builder.setIssuer(StringValue.newBuilder().setValue(verifiedJwt.getIssuer()));
}
if (verifiedJwt.hasSubject()) {
builder.setSubject(StringValue.newBuilder().setValue(verifiedJwt.getSubject()));
}
if (verifiedJwt.hasAudiences()) {
for (String audience : verifiedJwt.getAudiences()) {
builder.addAudiences(audience);
}
}
if (verifiedJwt.hasJwtId()) {
builder.setJwtId(StringValue.newBuilder().setValue(verifiedJwt.getJwtId()));
}
if (verifiedJwt.hasExpiration()) {
builder.setExpiration(instantToTimestamp(verifiedJwt.getExpiration()));
}
if (verifiedJwt.hasNotBefore()) {
builder.setNotBefore(instantToTimestamp(verifiedJwt.getNotBefore()));
}
if (verifiedJwt.hasIssuedAt()) {
builder.setIssuedAt(instantToTimestamp(verifiedJwt.getIssuedAt()));
}
for (String claimName : verifiedJwt.customClaimNames()) {
addCustomClaimToBuilder(verifiedJwt, claimName, builder);
}
return builder.build();
}
private JwtValidator convertProtoValidatorToValidator(
com.google.crypto.tink.testing.proto.JwtValidator validator) throws JwtInvalidException {
JwtValidator.Builder validatorBuilder = JwtValidator.newBuilder();
if (validator.hasExpectedTypeHeader()) {
validatorBuilder.expectTypeHeader(validator.getExpectedTypeHeader().getValue());
}
if (validator.hasExpectedIssuer()) {
validatorBuilder.expectIssuer(validator.getExpectedIssuer().getValue());
}
if (validator.hasExpectedAudience()) {
validatorBuilder.expectAudience(validator.getExpectedAudience().getValue());
}
if (validator.getIgnoreTypeHeader()) {
validatorBuilder.ignoreTypeHeader();
}
if (validator.getIgnoreIssuer()) {
validatorBuilder.ignoreIssuer();
}
if (validator.getIgnoreAudience()) {
validatorBuilder.ignoreAudiences();
}
if (validator.getAllowMissingExpiration()) {
validatorBuilder.allowMissingExpiration();
}
if (validator.getExpectIssuedInThePast()) {
validatorBuilder.expectIssuedInThePast();
}
if (validator.hasNow()) {
Instant now = timestampToInstant(validator.getNow());
validatorBuilder.setClock(Clock.fixed(now, ZoneOffset.UTC));
}
if (validator.hasClockSkew()) {
validatorBuilder.setClockSkew(Duration.ofSeconds(validator.getClockSkew().getSeconds()));
}
return validatorBuilder.build();
}
/** Decodes and verifies a signed, compact JWT. */
@Override
public void verifyMacAndDecode(
JwtVerifyRequest request,
StreamObserver<JwtVerifyResponse> responseObserver) {
JwtVerifyResponse response;
try {
KeysetHandle keysetHandle =
CleartextKeysetHandle.read(
BinaryKeysetReader.withBytes(request.getKeyset().toByteArray()));
JwtValidator validator = convertProtoValidatorToValidator(request.getValidator());
JwtMac jwtMac = keysetHandle.getPrimitive(JwtMac.class);
VerifiedJwt verifiedJwt = jwtMac.verifyMacAndDecode(request.getSignedCompactJwt(), validator);
JwtToken token = convertVerifiedJwtToJwtToken(verifiedJwt);
response = JwtVerifyResponse.newBuilder().setVerifiedJwt(token).build();
} catch (GeneralSecurityException | InvalidProtocolBufferException e) {
response = JwtVerifyResponse.newBuilder().setErr(e.toString()).build();
} catch (IOException e) {
responseObserver.onError(Status.UNKNOWN.withDescription(e.getMessage()).asException());
return;
}
responseObserver.onNext(response);
responseObserver.onCompleted();
}
/** Decodes and verifies a signed, compact JWT. */
@Override
public void publicKeyVerifyAndDecode(
JwtVerifyRequest request,
StreamObserver<JwtVerifyResponse> responseObserver) {
JwtVerifyResponse response;
try {
KeysetHandle keysetHandle =
CleartextKeysetHandle.read(
BinaryKeysetReader.withBytes(request.getKeyset().toByteArray()));
JwtValidator validator = convertProtoValidatorToValidator(request.getValidator());
JwtPublicKeyVerify verifier = keysetHandle.getPrimitive(JwtPublicKeyVerify.class);
VerifiedJwt verifiedJwt = verifier.verifyAndDecode(request.getSignedCompactJwt(), validator);
JwtToken token = convertVerifiedJwtToJwtToken(verifiedJwt);
response = JwtVerifyResponse.newBuilder().setVerifiedJwt(token).build();
} catch (GeneralSecurityException | InvalidProtocolBufferException e) {
response = JwtVerifyResponse.newBuilder().setErr(e.toString()).build();
} catch (IOException e) {
responseObserver.onError(Status.UNKNOWN.withDescription(e.getMessage()).asException());
return;
}
responseObserver.onNext(response);
responseObserver.onCompleted();
}
/** Converts a Tink JWT Keyset to a JWK set. */
@Override
public void toJwkSet(
JwtToJwkSetRequest request, StreamObserver<JwtToJwkSetResponse> responseObserver) {
JwtToJwkSetResponse response;
try {
KeysetHandle keysetHandle =
CleartextKeysetHandle.read(
BinaryKeysetReader.withBytes(request.getKeyset().toByteArray()));
String jwkSet = JwkSetConverter.fromPublicKeysetHandle(keysetHandle);
response = JwtToJwkSetResponse.newBuilder().setJwkSet(jwkSet).build();
} catch (GeneralSecurityException | InvalidProtocolBufferException e) {
response = JwtToJwkSetResponse.newBuilder().setErr(e.toString()).build();
} catch (IOException e) {
responseObserver.onError(Status.UNKNOWN.withDescription(e.getMessage()).asException());
return;
}
responseObserver.onNext(response);
responseObserver.onCompleted();
}
/** Converts a JWK set to a Tink JWT Keyset. */
@Override
public void fromJwkSet(
JwtFromJwkSetRequest request, StreamObserver<JwtFromJwkSetResponse> responseObserver) {
JwtFromJwkSetResponse response;
try {
KeysetHandle keysetHandle = JwkSetConverter.toPublicKeysetHandle(request.getJwkSet());
Keyset keyset = CleartextKeysetHandle.getKeyset(keysetHandle);
ByteArrayOutputStream keysetStream = new ByteArrayOutputStream();
BinaryKeysetWriter.withOutputStream(keysetStream).write(keyset);
keysetStream.close();
response =
JwtFromJwkSetResponse.newBuilder()
.setKeyset(ByteString.copyFrom(keysetStream.toByteArray()))
.build();
} catch (GeneralSecurityException | InvalidProtocolBufferException e) {
response = JwtFromJwkSetResponse.newBuilder().setErr(e.toString()).build();
} catch (IOException e) {
responseObserver.onError(Status.UNKNOWN.withDescription(e.getMessage()).asException());
return;
}
responseObserver.onNext(response);
responseObserver.onCompleted();
}
}