Skip to content

Commit 855a2cc

Browse files
committed
feat: Implement Client-Side CAB token generation.
Change-Id: I2c217656584cf5805297f02340cbbabca471f609
1 parent 0d96dcf commit 855a2cc

File tree

5 files changed

+345
-23
lines changed

5 files changed

+345
-23
lines changed

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

Lines changed: 130 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,12 +56,28 @@
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;
5874
import java.time.Duration;
5975
import java.util.Date;
6076
import java.util.concurrent.ExecutionException;
6177
import javax.annotation.Nullable;
78+
import java.util.Base64;
79+
import java.util.List;
80+
import java.security.GeneralSecurityException;
6281

6382
public class ClientSideCredentialAccessBoundaryFactory {
6483
static final Duration DEFAULT_REFRESH_MARGIN = Duration.ofMinutes(30);
@@ -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,19 @@ private ClientSideCredentialAccessBoundaryFactory(Builder builder) {
83103
this.transportFactory = builder.transportFactory;
84104
this.sourceCredential = builder.sourceCredential;
85105
this.tokenExchangeEndpoint = builder.tokenExchangeEndpoint;
106+
107+
try {
108+
AeadConfig.register();
109+
} catch (GeneralSecurityException e) {
110+
throw new IllegalStateException("Error occurred when registering Tink");
111+
}
112+
113+
CelOptions options = CelOptions.current().build();
114+
this.celCompiler = CelCompilerFactory
115+
.standardCelCompilerBuilder()
116+
.setOptions(options)
117+
.build();
118+
86119
this.refreshMargin =
87120
builder.refreshMargin != null ? builder.refreshMargin : DEFAULT_REFRESH_MARGIN;
88121
this.minimumTokenLifetime =
@@ -92,10 +125,39 @@ private ClientSideCredentialAccessBoundaryFactory(Builder builder) {
92125
this.clock = builder.clock;
93126
}
94127

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.");
128+
/**
129+
* Generates a Client-Side CAB token given the {@link CredentialAccessBoundary}.
130+
*
131+
* @param accessBoundary
132+
* @return The Client-Side CAB token in an {@link AccessToken} object
133+
* @throws IOException
134+
*/
135+
public AccessToken generateToken(CredentialAccessBoundary accessBoundary) throws IOException {
136+
this.refreshCredentialsIfRequired();
137+
138+
String intermediaryToken, sessionKey;
139+
Date intermediaryTokenExpirationTime;
140+
141+
synchronized (refreshLock) {
142+
intermediaryToken =
143+
this.intermediateCredentials.intermediateAccessToken.getTokenValue();
144+
intermediaryTokenExpirationTime =
145+
this.intermediateCredentials.intermediateAccessToken
146+
.getExpirationTime();
147+
sessionKey = this.intermediateCredentials.accessBoundarySessionKey;
148+
}
149+
150+
byte[] rawRestrictions =
151+
this.serializeCredentialAccessBoundary(accessBoundary);
152+
153+
byte[] encryptedRestrictions =
154+
this.encryptRestrictions(rawRestrictions, sessionKey);
155+
156+
String tokenValue =
157+
intermediaryToken + "." +
158+
Base64.getUrlEncoder().encodeToString(encryptedRestrictions);
159+
160+
return new AccessToken(tokenValue, intermediaryTokenExpirationTime);
99161
}
100162

101163
/**
@@ -403,6 +465,70 @@ public void run() {
403465
}
404466
}
405467

468+
/**
469+
* Serializes a {@link CredentialAccessBoundary} object into Protobuf wire format.
470+
*/
471+
private byte[] serializeCredentialAccessBoundary(
472+
CredentialAccessBoundary credentialAccessBoundary) throws IOException {
473+
List<AccessBoundaryRule> rules =
474+
credentialAccessBoundary.getAccessBoundaryRules();
475+
ClientSideAccessBoundary.Builder accessBoundaryBuilder =
476+
ClientSideAccessBoundary.newBuilder();
477+
478+
for (AccessBoundaryRule rule : rules) {
479+
ClientSideAccessBoundaryRule.Builder ruleBuilder =
480+
accessBoundaryBuilder.addAccessBoundaryRulesBuilder()
481+
.addAllAvailablePermissions(rule.getAvailablePermissions())
482+
.setAvailableResource(rule.getAvailableResource());
483+
484+
if (rule.getAvailabilityCondition() != null) {
485+
String availabilityCondition =
486+
rule.getAvailabilityCondition().getExpression();
487+
488+
Expr availabilityConditionExpr = this.compileCel(availabilityCondition);
489+
ruleBuilder.setCompiledAvailabilityCondition(availabilityConditionExpr);
490+
}
491+
}
492+
493+
return accessBoundaryBuilder.build().toByteArray();
494+
}
495+
496+
/**
497+
* Compiles CEL expression from String to an {@link Expr} proto object.
498+
*/
499+
private Expr compileCel(String expr) throws IOException {
500+
try {
501+
CelAbstractSyntaxTree ast = celCompiler.parse(expr).getAst();
502+
503+
CelProtoAbstractSyntaxTree astProto =
504+
CelProtoAbstractSyntaxTree.fromCelAst(ast);
505+
506+
return astProto.getExpr();
507+
} catch (CelValidationException exception) {
508+
throw new IOException("Failed to parse CEL expression: " +
509+
exception.getMessage());
510+
}
511+
}
512+
513+
/**
514+
* Encrypts the given bytes using a sessionKey using Tink Aead.
515+
*/
516+
private byte[] encryptRestrictions(byte[] restriction, String sessionKey) throws InternalError {
517+
try {
518+
byte[] rawKey = Base64.getDecoder().decode(sessionKey);
519+
520+
KeysetHandle keysetHandle = TinkProtoKeysetFormat.parseKeyset(
521+
rawKey, InsecureSecretKeyAccess.get());
522+
523+
Aead aead =
524+
keysetHandle.getPrimitive(RegistryConfiguration.get(), Aead.class);
525+
526+
return aead.encrypt(restriction, /*associatedData=*/new byte[0]);
527+
} catch (GeneralSecurityException exception) {
528+
throw new InternalError("Failed to parse keyset: " + exception.getMessage());
529+
}
530+
}
531+
406532
public static Builder newBuilder() {
407533
return new Builder();
408534
}

cab-token-generator/javatests/com/google/auth/credentialaccessboundary/ClientSideCredentialAccessBoundaryFactoryTest.java

Lines changed: 183 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@
3535
import static org.junit.Assert.assertEquals;
3636
import static org.junit.Assert.assertNotNull;
3737
import static org.junit.Assert.assertThrows;
38+
import static org.junit.Assert.assertTrue;
3839
import static org.mockito.Mockito.mock;
3940
import static org.mockito.Mockito.when;
4041

@@ -44,15 +45,28 @@
4445
import com.google.auth.TestUtils;
4546
import com.google.auth.credentialaccessboundary.ClientSideCredentialAccessBoundaryFactory.IntermediateCredentials;
4647
import com.google.auth.credentialaccessboundary.ClientSideCredentialAccessBoundaryFactory.RefreshType;
48+
import com.google.auth.credentialaccessboundary.protobuf.ClientSideAccessBoundaryProto.ClientSideAccessBoundary;
49+
import com.google.auth.credentialaccessboundary.protobuf.ClientSideAccessBoundaryProto.ClientSideAccessBoundaryRule;
4750
import com.google.auth.http.HttpTransportFactory;
4851
import com.google.auth.oauth2.AccessToken;
52+
import com.google.auth.oauth2.CredentialAccessBoundary;
4953
import com.google.auth.oauth2.GoogleCredentials;
5054
import com.google.auth.oauth2.MockStsTransport;
5155
import com.google.auth.oauth2.MockTokenServerTransportFactory;
5256
import com.google.auth.oauth2.OAuth2Utils;
5357
import com.google.auth.oauth2.ServiceAccountCredentials;
58+
import com.google.common.collect.ImmutableList;
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+
65+
import dev.cel.expr.Expr;
66+
5467
import java.io.IOException;
5568
import java.time.Duration;
69+
import java.util.Base64;
5670
import java.util.Map;
5771
import java.util.concurrent.CountDownLatch;
5872
import org.junit.Before;
@@ -572,4 +586,173 @@ private static void triggerConcurrentRefresh(
572586
}
573587
}
574588
}
589+
590+
@Test
591+
public void generateToken() throws Exception {
592+
MockStsTransportFactory transportFactory = new MockStsTransportFactory();
593+
transportFactory.transport.setReturnAccessBoundarySessionKey(true);
594+
595+
ClientSideCredentialAccessBoundaryFactory.Builder builder =
596+
ClientSideCredentialAccessBoundaryFactory.newBuilder();
597+
598+
ClientSideCredentialAccessBoundaryFactory factory =
599+
builder
600+
.setSourceCredential(getServiceAccountSourceCredentials(
601+
mockTokenServerTransportFactory))
602+
.setHttpTransportFactory(transportFactory)
603+
.build();
604+
605+
CredentialAccessBoundary.Builder cabBuilder =
606+
CredentialAccessBoundary.newBuilder();
607+
CredentialAccessBoundary accessBoundary =
608+
cabBuilder
609+
.addRule(
610+
CredentialAccessBoundary.AccessBoundaryRule.newBuilder()
611+
.setAvailableResource("//storage.googleapis.com/projects/"
612+
+ "_/buckets/example-bucket")
613+
.setAvailablePermissions(
614+
ImmutableList.of("inRole:roles/storage.objectViewer"))
615+
.setAvailabilityCondition(
616+
CredentialAccessBoundary.AccessBoundaryRule
617+
.AvailabilityCondition.newBuilder()
618+
.setExpression(
619+
"resource.name.startsWith('projects/_/"
620+
+ "buckets/example-bucket/objects/customer-a')")
621+
.build())
622+
.build())
623+
.build();
624+
625+
AccessToken token = factory.generateToken(accessBoundary);
626+
627+
String[] parts = token.getTokenValue().split("\\.");
628+
assertEquals(parts.length, 2);
629+
assertEquals(parts[0], "accessToken");
630+
631+
byte[] rawKey = Base64.getDecoder().decode(
632+
transportFactory.transport.getAccessBoundarySessionKey());
633+
634+
KeysetHandle keysetHandle = TinkProtoKeysetFormat.parseKeyset(
635+
rawKey, InsecureSecretKeyAccess.get());
636+
637+
Aead aead =
638+
keysetHandle.getPrimitive(RegistryConfiguration.get(), Aead.class);
639+
byte[] rawRestrictions =
640+
aead.decrypt(Base64.getUrlDecoder().decode(parts[1]), new byte[0]);
641+
ClientSideAccessBoundary clientSideAccessBoundary =
642+
ClientSideAccessBoundary.parseFrom(rawRestrictions);
643+
assertEquals(clientSideAccessBoundary.getAccessBoundaryRulesCount(), 1);
644+
ClientSideAccessBoundaryRule rule =
645+
clientSideAccessBoundary.getAccessBoundaryRules(0);
646+
assertEquals(rule.getAvailableResource(),
647+
"//storage.googleapis.com/projects/_/buckets/example-bucket");
648+
assertEquals(rule.getAvailablePermissions(0),
649+
"inRole:roles/storage.objectViewer");
650+
Expr expr = rule.getCompiledAvailabilityCondition();
651+
assertEquals(expr.getCallExpr()
652+
.getTarget()
653+
.getSelectExpr()
654+
.getOperand()
655+
.getIdentExpr()
656+
.getName(),
657+
"resource");
658+
assertEquals(expr.getCallExpr().getFunction(), "startsWith");
659+
assertEquals(expr.getCallExpr().getArgs(0).getConstExpr().getStringValue(),
660+
"projects/_/buckets/example-bucket/objects/customer-a");
661+
}
662+
663+
@Test
664+
public void generateToken_withoutAvailabilityCondition() throws Exception {
665+
MockStsTransportFactory transportFactory = new MockStsTransportFactory();
666+
transportFactory.transport.setReturnAccessBoundarySessionKey(true);
667+
668+
ClientSideCredentialAccessBoundaryFactory.Builder builder =
669+
ClientSideCredentialAccessBoundaryFactory.newBuilder();
670+
671+
ClientSideCredentialAccessBoundaryFactory factory =
672+
builder
673+
.setSourceCredential(getServiceAccountSourceCredentials(
674+
mockTokenServerTransportFactory))
675+
.setHttpTransportFactory(transportFactory)
676+
.build();
677+
678+
CredentialAccessBoundary.Builder cabBuilder =
679+
CredentialAccessBoundary.newBuilder();
680+
CredentialAccessBoundary accessBoundary =
681+
cabBuilder
682+
.addRule(
683+
CredentialAccessBoundary.AccessBoundaryRule.newBuilder()
684+
.setAvailableResource("//storage.googleapis.com/projects/"
685+
+ "_/buckets/example-bucket")
686+
.setAvailablePermissions(
687+
ImmutableList.of("inRole:roles/storage.objectViewer"))
688+
.build())
689+
.build();
690+
691+
AccessToken token = factory.generateToken(accessBoundary);
692+
693+
String[] parts = token.getTokenValue().split("\\.");
694+
assertEquals(parts.length, 2);
695+
assertEquals(parts[0], "accessToken");
696+
697+
byte[] rawKey = Base64.getDecoder().decode(
698+
transportFactory.transport.getAccessBoundarySessionKey());
699+
700+
KeysetHandle keysetHandle = TinkProtoKeysetFormat.parseKeyset(
701+
rawKey, InsecureSecretKeyAccess.get());
702+
703+
Aead aead =
704+
keysetHandle.getPrimitive(RegistryConfiguration.get(), Aead.class);
705+
byte[] rawRestrictions =
706+
aead.decrypt(Base64.getUrlDecoder().decode(parts[1]), new byte[0]);
707+
ClientSideAccessBoundary clientSideAccessBoundary =
708+
ClientSideAccessBoundary.parseFrom(rawRestrictions);
709+
assertEquals(clientSideAccessBoundary.getAccessBoundaryRulesCount(), 1);
710+
ClientSideAccessBoundaryRule rule =
711+
clientSideAccessBoundary.getAccessBoundaryRules(0);
712+
assertEquals(rule.getAvailableResource(),
713+
"//storage.googleapis.com/projects/_/buckets/example-bucket");
714+
assertEquals(rule.getAvailablePermissions(0),
715+
"inRole:roles/storage.objectViewer");
716+
assertTrue(rule.getCompiledAvailabilityCondition().equals(
717+
Expr.getDefaultInstance()));
718+
}
719+
720+
@Test
721+
public void generateToken_withInvalidCelExpression() throws Exception {
722+
MockStsTransportFactory transportFactory = new MockStsTransportFactory();
723+
transportFactory.transport.setReturnAccessBoundarySessionKey(true);
724+
725+
ClientSideCredentialAccessBoundaryFactory.Builder builder =
726+
ClientSideCredentialAccessBoundaryFactory.newBuilder();
727+
728+
ClientSideCredentialAccessBoundaryFactory factory =
729+
builder
730+
.setSourceCredential(getServiceAccountSourceCredentials(
731+
mockTokenServerTransportFactory))
732+
.setHttpTransportFactory(transportFactory)
733+
.build();
734+
735+
CredentialAccessBoundary.Builder cabBuilder =
736+
CredentialAccessBoundary.newBuilder();
737+
CredentialAccessBoundary accessBoundary =
738+
cabBuilder
739+
.addRule(
740+
CredentialAccessBoundary.AccessBoundaryRule.newBuilder()
741+
.setAvailableResource("//storage.googleapis.com/projects/"
742+
+ "_/buckets/example-bucket")
743+
.setAvailablePermissions(
744+
ImmutableList.of("inRole:roles/storage.objectViewer"))
745+
.setAvailabilityCondition(
746+
CredentialAccessBoundary.AccessBoundaryRule
747+
.AvailabilityCondition.newBuilder()
748+
.setExpression(
749+
"resource.name.startsWith('projects/_/"
750+
+ "buckets/example-bucket/objects/customer-a'")
751+
.build())
752+
.build())
753+
.build();
754+
755+
assertThrows(IOException.class,
756+
() -> { factory.generateToken(accessBoundary); });
757+
}
575758
}

0 commit comments

Comments
 (0)