Skip to content

Commit 5573169

Browse files
authored
feat: Implement Client-Side CAB token generation. (#1598)
* feat: Implement Client-Side CAB token generation. Change-Id: I2c217656584cf5805297f02340cbbabca471f609 * Use IllegalStateException(String, Throwable) to capture upstream exception during Tink initialization Change-Id: I12af5b84eae4dcec5865adfdad1f9396d54c0200 * Rethrow exceptions from tink and CEL Change-Id: If8c94c786ee39201029d9c27856fd2eafb61e51c * Add tests for invalid keys from upstream, and rename test cases. Change-Id: Ib41cb81c779534fc6efd74d66bf4728efd743906 * Add additional throws comment for generatToken method. Change-Id: I9cfc589ade8a91040fc9c447740493fd49e392af * Refactor tests for better readability. Change-Id: Icfd0bc24c1694f220bcbffc6cde41462c59119c4 * Catch and rethrow the exception of session key not being base64 encoded. Change-Id: I5fa0c25fe020e9612735e4ac5df2b85a2a5aab11 * Format the code using mvn com.coveo:fmt-maven-plugin:format. Change-Id: I46572488dcd28de450a6b1b2f732bee5baa86910 * Fix a typo in the javadoc comment. Change-Id: Icef9ef5f7c3567224ec507303543b78e61f43ec1
1 parent 0d96dcf commit 5573169

File tree

5 files changed

+498
-24
lines changed

5 files changed

+498
-24
lines changed

cab-token-generator/java/com/google/auth/credentialaccessboundary/ClientSideCredentialAccessBoundaryFactory.java

Lines changed: 121 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -37,9 +37,12 @@
3737

3838
import com.google.api.client.util.Clock;
3939
import com.google.auth.Credentials;
40+
import com.google.auth.credentialaccessboundary.protobuf.ClientSideAccessBoundaryProto.ClientSideAccessBoundary;
41+
import com.google.auth.credentialaccessboundary.protobuf.ClientSideAccessBoundaryProto.ClientSideAccessBoundaryRule;
4042
import com.google.auth.http.HttpTransportFactory;
4143
import com.google.auth.oauth2.AccessToken;
4244
import com.google.auth.oauth2.CredentialAccessBoundary;
45+
import com.google.auth.oauth2.CredentialAccessBoundary.AccessBoundaryRule;
4346
import com.google.auth.oauth2.GoogleCredentials;
4447
import com.google.auth.oauth2.OAuth2Utils;
4548
import com.google.auth.oauth2.StsRequestHandler;
@@ -53,10 +56,26 @@
5356
import com.google.common.util.concurrent.ListenableFuture;
5457
import com.google.common.util.concurrent.ListenableFutureTask;
5558
import com.google.common.util.concurrent.MoreExecutors;
59+
import com.google.crypto.tink.Aead;
60+
import com.google.crypto.tink.InsecureSecretKeyAccess;
61+
import com.google.crypto.tink.KeysetHandle;
62+
import com.google.crypto.tink.RegistryConfiguration;
63+
import com.google.crypto.tink.TinkProtoKeysetFormat;
64+
import com.google.crypto.tink.aead.AeadConfig;
5665
import com.google.errorprone.annotations.CanIgnoreReturnValue;
66+
import dev.cel.common.CelAbstractSyntaxTree;
67+
import dev.cel.common.CelOptions;
68+
import dev.cel.common.CelProtoAbstractSyntaxTree;
69+
import dev.cel.common.CelValidationException;
70+
import dev.cel.compiler.CelCompiler;
71+
import dev.cel.compiler.CelCompilerFactory;
72+
import dev.cel.expr.Expr;
5773
import java.io.IOException;
74+
import java.security.GeneralSecurityException;
5875
import java.time.Duration;
76+
import java.util.Base64;
5977
import java.util.Date;
78+
import java.util.List;
6079
import java.util.concurrent.ExecutionException;
6180
import javax.annotation.Nullable;
6281

@@ -72,6 +91,7 @@ public class ClientSideCredentialAccessBoundaryFactory {
7291
private final Object refreshLock = new byte[0];
7392
private volatile IntermediateCredentials intermediateCredentials = null;
7493
private final Clock clock;
94+
private final CelCompiler celCompiler;
7595

7696
enum RefreshType {
7797
NONE,
@@ -83,6 +103,18 @@ private ClientSideCredentialAccessBoundaryFactory(Builder builder) {
83103
this.transportFactory = builder.transportFactory;
84104
this.sourceCredential = builder.sourceCredential;
85105
this.tokenExchangeEndpoint = builder.tokenExchangeEndpoint;
106+
107+
// Initializes the Tink AEAD registry for encrypting the client-side
108+
// restrictions.
109+
try {
110+
AeadConfig.register();
111+
} catch (GeneralSecurityException e) {
112+
throw new IllegalStateException("Error occurred when registering Tink", e);
113+
}
114+
115+
CelOptions options = CelOptions.current().build();
116+
this.celCompiler = CelCompilerFactory.standardCelCompilerBuilder().setOptions(options).build();
117+
86118
this.refreshMargin =
87119
builder.refreshMargin != null ? builder.refreshMargin : DEFAULT_REFRESH_MARGIN;
88120
this.minimumTokenLifetime =
@@ -92,10 +124,37 @@ private ClientSideCredentialAccessBoundaryFactory(Builder builder) {
92124
this.clock = builder.clock;
93125
}
94126

95-
public AccessToken generateToken(CredentialAccessBoundary accessBoundary) {
96-
// TODO(negarb/jiahuah): Implement generateToken
97-
// Note: This method will call refreshCredentialsIfRequired().
98-
throw new UnsupportedOperationException("generateToken is not yet implemented.");
127+
/**
128+
* Generates a Client-Side CAB token given the {@link CredentialAccessBoundary}.
129+
*
130+
* @param accessBoundary
131+
* @return The Client-Side CAB token in an {@link AccessToken} object
132+
* @throws IOException If an I/O error occurs while refreshing the source credentials
133+
* @throws CelValidationException If the availability condition is an invalid CEL expression
134+
* @throws GeneralSecurityException If an error occurs during encryption
135+
*/
136+
public AccessToken generateToken(CredentialAccessBoundary accessBoundary)
137+
throws IOException, CelValidationException, GeneralSecurityException {
138+
this.refreshCredentialsIfRequired();
139+
140+
String intermediateToken, sessionKey;
141+
Date intermediateTokenExpirationTime;
142+
143+
synchronized (refreshLock) {
144+
intermediateToken = this.intermediateCredentials.intermediateAccessToken.getTokenValue();
145+
intermediateTokenExpirationTime =
146+
this.intermediateCredentials.intermediateAccessToken.getExpirationTime();
147+
sessionKey = this.intermediateCredentials.accessBoundarySessionKey;
148+
}
149+
150+
byte[] rawRestrictions = this.serializeCredentialAccessBoundary(accessBoundary);
151+
152+
byte[] encryptedRestrictions = this.encryptRestrictions(rawRestrictions, sessionKey);
153+
154+
String tokenValue =
155+
intermediateToken + "." + Base64.getUrlEncoder().encodeToString(encryptedRestrictions);
156+
157+
return new AccessToken(tokenValue, intermediateTokenExpirationTime);
99158
}
100159

101160
/**
@@ -403,6 +462,64 @@ public void run() {
403462
}
404463
}
405464

465+
/** Serializes a {@link CredentialAccessBoundary} object into Protobuf wire format. */
466+
@VisibleForTesting
467+
byte[] serializeCredentialAccessBoundary(CredentialAccessBoundary credentialAccessBoundary)
468+
throws CelValidationException {
469+
List<AccessBoundaryRule> rules = credentialAccessBoundary.getAccessBoundaryRules();
470+
ClientSideAccessBoundary.Builder accessBoundaryBuilder = ClientSideAccessBoundary.newBuilder();
471+
472+
for (AccessBoundaryRule rule : rules) {
473+
ClientSideAccessBoundaryRule.Builder ruleBuilder =
474+
accessBoundaryBuilder
475+
.addAccessBoundaryRulesBuilder()
476+
.addAllAvailablePermissions(rule.getAvailablePermissions())
477+
.setAvailableResource(rule.getAvailableResource());
478+
479+
// Availability condition is an optional field from the CredentialAccessBoundary
480+
// CEL compliation is only performed if there is a non-empty availablity condition.
481+
if (rule.getAvailabilityCondition() != null) {
482+
String availabilityCondition = rule.getAvailabilityCondition().getExpression();
483+
484+
Expr availabilityConditionExpr = this.compileCel(availabilityCondition);
485+
ruleBuilder.setCompiledAvailabilityCondition(availabilityConditionExpr);
486+
}
487+
}
488+
489+
return accessBoundaryBuilder.build().toByteArray();
490+
}
491+
492+
/** Compiles CEL expression from String to an {@link Expr} proto object. */
493+
private Expr compileCel(String expr) throws CelValidationException {
494+
CelAbstractSyntaxTree ast = celCompiler.parse(expr).getAst();
495+
496+
CelProtoAbstractSyntaxTree astProto = CelProtoAbstractSyntaxTree.fromCelAst(ast);
497+
498+
return astProto.getExpr();
499+
}
500+
501+
/** Encrypts the given bytes using a sessionKey using Tink Aead. */
502+
private byte[] encryptRestrictions(byte[] restriction, String sessionKey)
503+
throws GeneralSecurityException {
504+
byte[] rawKey;
505+
506+
try {
507+
rawKey = Base64.getDecoder().decode(sessionKey);
508+
} catch (IllegalArgumentException e) {
509+
// Session key from the server is expected to be Base64 encoded
510+
throw new IllegalStateException("Session key is not Base64 encoded", e);
511+
}
512+
513+
KeysetHandle keysetHandle =
514+
TinkProtoKeysetFormat.parseKeyset(rawKey, InsecureSecretKeyAccess.get());
515+
516+
Aead aead = keysetHandle.getPrimitive(RegistryConfiguration.get(), Aead.class);
517+
518+
// For Client-Side CAB token encryption, empty associated data is expected.
519+
// Tink requires a byte[0] to be passed for this case.
520+
return aead.encrypt(restriction, /*associatedData=*/ new byte[0]);
521+
}
522+
406523
public static Builder newBuilder() {
407524
return new Builder();
408525
}

0 commit comments

Comments
 (0)