mirror of https://github.com/grpc/grpc-java.git
s2a: Add gRPC S2A (#11113)
This commit is contained in:
parent
f3cf7c3c75
commit
b8c1aa517a
|
@ -41,6 +41,7 @@ IO_GRPC_GRPC_JAVA_ARTIFACTS = [
|
|||
"io.perfmark:perfmark-api:0.27.0",
|
||||
"junit:junit:4.13.2",
|
||||
"org.apache.tomcat:annotations-api:6.0.53",
|
||||
"org.checkerframework:checker-qual:3.12.0",
|
||||
"org.codehaus.mojo:animal-sniffer-annotations:1.24",
|
||||
]
|
||||
# GRPC_DEPS_END
|
||||
|
|
|
@ -8,7 +8,7 @@ curl -Ls https://github.com/grpc/grpc-proto/archive/master.tar.gz | tar xz -C "$
|
|||
base="$tmpdir/grpc-proto-master"
|
||||
|
||||
# Copy protos in 'src/main/proto' from grpc-proto for these projects
|
||||
for project in alts grpclb services rls interop-testing; do
|
||||
for project in alts grpclb services s2a rls interop-testing; do
|
||||
while read -r proto; do
|
||||
[ -f "$base/$proto" ] && cp "$base/$proto" "$project/src/main/proto/$proto"
|
||||
echo "$proto"
|
||||
|
|
|
@ -45,6 +45,7 @@ IO_GRPC_GRPC_JAVA_ARTIFACTS = [
|
|||
"io.perfmark:perfmark-api:0.27.0",
|
||||
"junit:junit:4.13.2",
|
||||
"org.apache.tomcat:annotations-api:6.0.53",
|
||||
"org.checkerframework:checker-qual:3.12.0",
|
||||
"org.codehaus.mojo:animal-sniffer-annotations:1.24",
|
||||
]
|
||||
# GRPC_DEPS_END
|
||||
|
@ -80,6 +81,7 @@ IO_GRPC_GRPC_JAVA_OVERRIDE_TARGETS = {
|
|||
"io.grpc:grpc-rls": "@io_grpc_grpc_java//rls",
|
||||
"io.grpc:grpc-services": "@io_grpc_grpc_java//services:services_maven",
|
||||
"io.grpc:grpc-stub": "@io_grpc_grpc_java//stub",
|
||||
"io.grpc:grpc-s2a": "@io_grpc_grpc_java//s2a",
|
||||
"io.grpc:grpc-testing": "@io_grpc_grpc_java//testing",
|
||||
"io.grpc:grpc-xds": "@io_grpc_grpc_java//xds:xds_maven",
|
||||
"io.grpc:grpc-util": "@io_grpc_grpc_java//util",
|
||||
|
|
|
@ -0,0 +1,194 @@
|
|||
load("@rules_proto//proto:defs.bzl", "proto_library")
|
||||
load("//:java_grpc_library.bzl", "java_grpc_library")
|
||||
load("@rules_jvm_external//:defs.bzl", "artifact")
|
||||
|
||||
java_library(
|
||||
name = "s2a_channel_pool",
|
||||
srcs = glob([
|
||||
"src/main/java/io/grpc/s2a/channel/*.java",
|
||||
]),
|
||||
deps = [
|
||||
"//api",
|
||||
"//core",
|
||||
"//core:internal",
|
||||
"//netty",
|
||||
artifact("com.google.code.findbugs:jsr305"),
|
||||
artifact("com.google.errorprone:error_prone_annotations"),
|
||||
artifact("com.google.guava:guava"),
|
||||
artifact("org.checkerframework:checker-qual"),
|
||||
artifact("io.netty:netty-common"),
|
||||
artifact("io.netty:netty-transport"),
|
||||
],
|
||||
)
|
||||
|
||||
java_library(
|
||||
name = "s2a_identity",
|
||||
srcs = ["src/main/java/io/grpc/s2a/handshaker/S2AIdentity.java"],
|
||||
deps = [
|
||||
":common_java_proto",
|
||||
artifact("com.google.errorprone:error_prone_annotations"),
|
||||
artifact("com.google.guava:guava"),
|
||||
],
|
||||
)
|
||||
|
||||
java_library(
|
||||
name = "token_fetcher",
|
||||
srcs = ["src/main/java/io/grpc/s2a/handshaker/tokenmanager/TokenFetcher.java"],
|
||||
deps = [
|
||||
":s2a_identity",
|
||||
],
|
||||
)
|
||||
|
||||
java_library(
|
||||
name = "access_token_manager",
|
||||
srcs = [
|
||||
"src/main/java/io/grpc/s2a/handshaker/tokenmanager/AccessTokenManager.java",
|
||||
],
|
||||
deps = [
|
||||
":s2a_identity",
|
||||
":token_fetcher",
|
||||
artifact("com.google.code.findbugs:jsr305"),
|
||||
],
|
||||
)
|
||||
|
||||
java_library(
|
||||
name = "single_token_fetcher",
|
||||
srcs = [
|
||||
"src/main/java/io/grpc/s2a/handshaker/tokenmanager/SingleTokenFetcher.java",
|
||||
],
|
||||
deps = [
|
||||
":s2a_identity",
|
||||
":token_fetcher",
|
||||
artifact("com.google.guava:guava"),
|
||||
],
|
||||
)
|
||||
|
||||
java_library(
|
||||
name = "s2a_handshaker",
|
||||
srcs = [
|
||||
"src/main/java/io/grpc/s2a/handshaker/ConnectionClosedException.java",
|
||||
"src/main/java/io/grpc/s2a/handshaker/GetAuthenticationMechanisms.java",
|
||||
"src/main/java/io/grpc/s2a/handshaker/ProtoUtil.java",
|
||||
"src/main/java/io/grpc/s2a/handshaker/S2AConnectionException.java",
|
||||
"src/main/java/io/grpc/s2a/handshaker/S2APrivateKeyMethod.java",
|
||||
"src/main/java/io/grpc/s2a/handshaker/S2AProtocolNegotiatorFactory.java",
|
||||
"src/main/java/io/grpc/s2a/handshaker/S2AStub.java",
|
||||
"src/main/java/io/grpc/s2a/handshaker/S2ATrustManager.java",
|
||||
"src/main/java/io/grpc/s2a/handshaker/SslContextFactory.java",
|
||||
],
|
||||
deps = [
|
||||
":access_token_manager",
|
||||
":common_java_proto",
|
||||
":s2a_channel_pool",
|
||||
":s2a_identity",
|
||||
":s2a_java_proto",
|
||||
":s2a_java_grpc_proto",
|
||||
":single_token_fetcher",
|
||||
"//api",
|
||||
"//core:internal",
|
||||
"//netty",
|
||||
"//stub",
|
||||
artifact("com.google.code.findbugs:jsr305"),
|
||||
artifact("com.google.errorprone:error_prone_annotations"),
|
||||
artifact("com.google.guava:guava"),
|
||||
artifact("org.checkerframework:checker-qual"),
|
||||
"@com_google_protobuf//:protobuf_java",
|
||||
artifact("io.netty:netty-common"),
|
||||
artifact("io.netty:netty-handler"),
|
||||
artifact("io.netty:netty-transport"),
|
||||
],
|
||||
)
|
||||
|
||||
java_library(
|
||||
name = "s2av2_credentials",
|
||||
srcs = ["src/main/java/io/grpc/s2a/S2AChannelCredentials.java"],
|
||||
visibility = ["//visibility:public"],
|
||||
deps = [
|
||||
":s2a_channel_pool",
|
||||
":s2a_handshaker",
|
||||
":s2a_identity",
|
||||
"//api",
|
||||
"//core:internal",
|
||||
"//netty",
|
||||
artifact("com.google.code.findbugs:jsr305"),
|
||||
artifact("com.google.errorprone:error_prone_annotations"),
|
||||
artifact("com.google.guava:guava"),
|
||||
artifact("org.checkerframework:checker-qual"),
|
||||
],
|
||||
)
|
||||
|
||||
java_library(
|
||||
name = "mtls_to_s2av2_credentials",
|
||||
srcs = ["src/main/java/io/grpc/s2a/MtlsToS2AChannelCredentials.java"],
|
||||
visibility = ["//visibility:public"],
|
||||
deps = [
|
||||
":s2a_channel_pool",
|
||||
":s2av2_credentials",
|
||||
"//api",
|
||||
"//util",
|
||||
artifact("com.google.guava:guava"),
|
||||
],
|
||||
)
|
||||
|
||||
# bazel only accepts proto import with absolute path.
|
||||
genrule(
|
||||
name = "protobuf_imports",
|
||||
srcs = glob(["src/main/proto/grpc/gcp/s2a/*.proto"]),
|
||||
outs = [
|
||||
"protobuf_out/grpc/gcp/s2a/s2a.proto",
|
||||
"protobuf_out/grpc/gcp/s2a/s2a_context.proto",
|
||||
"protobuf_out/grpc/gcp/s2a/common.proto",
|
||||
],
|
||||
cmd = "for fname in $(SRCS); do " +
|
||||
"sed 's,import \",import \"s2a/protobuf_out/,g' $$fname > " +
|
||||
"$(@D)/protobuf_out/grpc/gcp/s2a/$$(basename $$fname); done",
|
||||
)
|
||||
|
||||
proto_library(
|
||||
name = "common_proto",
|
||||
srcs = [
|
||||
"protobuf_out/grpc/gcp/s2a/common.proto",
|
||||
],
|
||||
)
|
||||
|
||||
proto_library(
|
||||
name = "s2a_context_proto",
|
||||
srcs = [
|
||||
"protobuf_out/grpc/gcp/s2a/s2a_context.proto",
|
||||
],
|
||||
deps = [
|
||||
":common_proto",
|
||||
],
|
||||
)
|
||||
|
||||
proto_library(
|
||||
name = "s2a_proto",
|
||||
srcs = [
|
||||
"protobuf_out/grpc/gcp/s2a/s2a.proto",
|
||||
],
|
||||
deps = [
|
||||
":common_proto",
|
||||
":s2a_context_proto",
|
||||
],
|
||||
)
|
||||
|
||||
java_proto_library(
|
||||
name = "s2a_java_proto",
|
||||
deps = [":s2a_proto"],
|
||||
)
|
||||
|
||||
java_proto_library(
|
||||
name = "s2a_context_java_proto",
|
||||
deps = [":s2a_context_proto"],
|
||||
)
|
||||
|
||||
java_proto_library(
|
||||
name = "common_java_proto",
|
||||
deps = [":common_proto"],
|
||||
)
|
||||
|
||||
java_grpc_library(
|
||||
name = "s2a_java_grpc_proto",
|
||||
srcs = [":s2a_proto"],
|
||||
deps = [":s2a_java_proto"],
|
||||
)
|
|
@ -0,0 +1,151 @@
|
|||
buildscript {
|
||||
dependencies {
|
||||
classpath 'com.google.gradle:osdetector-gradle-plugin:1.4.0'
|
||||
}
|
||||
}
|
||||
|
||||
plugins {
|
||||
id "java-library"
|
||||
id "maven-publish"
|
||||
|
||||
id "com.github.johnrengelman.shadow"
|
||||
id "com.google.protobuf"
|
||||
id "ru.vyarus.animalsniffer"
|
||||
}
|
||||
|
||||
description = "gRPC: S2A"
|
||||
|
||||
apply plugin: "com.google.osdetector"
|
||||
|
||||
dependencies {
|
||||
|
||||
api project(':grpc-api')
|
||||
implementation project(':grpc-stub'),
|
||||
project(':grpc-protobuf'),
|
||||
project(':grpc-core'),
|
||||
libraries.protobuf.java,
|
||||
libraries.conscrypt,
|
||||
libraries.guava.jre // JRE required by protobuf-java-util from grpclb
|
||||
def nettyDependency = implementation project(':grpc-netty')
|
||||
compileOnly libraries.javax.annotation
|
||||
|
||||
shadow configurations.implementation.getDependencies().minus(nettyDependency)
|
||||
shadow project(path: ':grpc-netty-shaded', configuration: 'shadow')
|
||||
|
||||
testImplementation project(':grpc-benchmarks'),
|
||||
project(':grpc-testing'),
|
||||
project(':grpc-testing-proto'),
|
||||
testFixtures(project(':grpc-core')),
|
||||
libraries.guava,
|
||||
libraries.junit,
|
||||
libraries.mockito.core,
|
||||
libraries.truth,
|
||||
libraries.conscrypt,
|
||||
libraries.netty.transport.epoll
|
||||
|
||||
testImplementation 'com.google.truth:truth:1.4.2'
|
||||
testImplementation 'com.google.truth.extensions:truth-proto-extension:1.4.2'
|
||||
testImplementation libraries.guava.testlib
|
||||
|
||||
testRuntimeOnly libraries.netty.tcnative,
|
||||
libraries.netty.tcnative.classes
|
||||
testRuntimeOnly (libraries.netty.tcnative) {
|
||||
artifact {
|
||||
classifier = "linux-x86_64"
|
||||
}
|
||||
}
|
||||
testRuntimeOnly (libraries.netty.tcnative) {
|
||||
artifact {
|
||||
classifier = "linux-aarch_64"
|
||||
}
|
||||
}
|
||||
testRuntimeOnly (libraries.netty.tcnative) {
|
||||
artifact {
|
||||
classifier = "osx-x86_64"
|
||||
}
|
||||
}
|
||||
testRuntimeOnly (libraries.netty.tcnative) {
|
||||
artifact {
|
||||
classifier = "osx-aarch_64"
|
||||
}
|
||||
}
|
||||
testRuntimeOnly (libraries.netty.tcnative) {
|
||||
artifact {
|
||||
classifier = "windows-x86_64"
|
||||
}
|
||||
}
|
||||
testRuntimeOnly (libraries.netty.transport.epoll) {
|
||||
artifact {
|
||||
classifier = "linux-x86_64"
|
||||
}
|
||||
}
|
||||
|
||||
signature libraries.signature.java
|
||||
}
|
||||
|
||||
tasks.named("compileJava") {
|
||||
dependsOn(tasks.named("generateProto"))
|
||||
//dependsOn(tasks.named("syncGeneratedSourcesmain"))
|
||||
}
|
||||
|
||||
|
||||
tasks.named("sourcesJar") {
|
||||
dependsOn(tasks.named("generateProto"))
|
||||
//dependsOn(tasks.named("syncGeneratedSourcesmain"))
|
||||
}
|
||||
|
||||
sourceSets {
|
||||
main {
|
||||
//java.srcDirs += "src/generated/main/java"
|
||||
//java.srcDirs += "src/generated/main/grpc"
|
||||
}
|
||||
}
|
||||
//println sourceSets.main.java.srcDirs
|
||||
//println sourceSets.test.resources.srcDirs
|
||||
|
||||
configureProtoCompilation()
|
||||
|
||||
tasks.named("javadoc").configure {
|
||||
exclude 'io/grpc/s2a/**'
|
||||
}
|
||||
|
||||
tasks.named("jar").configure {
|
||||
// Must use a different archiveClassifier to avoid conflicting with shadowJar
|
||||
archiveClassifier = 'original'
|
||||
manifest {
|
||||
attributes('Automatic-Module-Name': 'io.grpc.s2a')
|
||||
}
|
||||
}
|
||||
|
||||
// We want to use grpc-netty-shaded instead of grpc-netty. But we also want our
|
||||
// source to work with Bazel, so we rewrite the code as part of the build.
|
||||
tasks.named("shadowJar").configure {
|
||||
archiveClassifier = null
|
||||
dependencies {
|
||||
exclude(dependency {true})
|
||||
}
|
||||
relocate 'io.grpc.netty', 'io.grpc.netty.shaded.io.grpc.netty'
|
||||
relocate 'io.netty', 'io.grpc.netty.shaded.io.netty'
|
||||
}
|
||||
|
||||
publishing {
|
||||
publications {
|
||||
maven(MavenPublication) {
|
||||
// We want this to throw an exception if it isn't working
|
||||
def originalJar = artifacts.find { dep -> dep.classifier == 'original'}
|
||||
artifacts.remove(originalJar)
|
||||
|
||||
pom.withXml {
|
||||
def dependenciesNode = new Node(null, 'dependencies')
|
||||
project.configurations.shadow.allDependencies.each { dep ->
|
||||
def dependencyNode = dependenciesNode.appendNode('dependency')
|
||||
dependencyNode.appendNode('groupId', dep.group)
|
||||
dependencyNode.appendNode('artifactId', dep.name)
|
||||
dependencyNode.appendNode('version', dep.version)
|
||||
dependencyNode.appendNode('scope', 'compile')
|
||||
}
|
||||
asNode().dependencies[0].replaceNode(dependenciesNode)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,285 @@
|
|||
package io.grpc.s2a.handshaker;
|
||||
|
||||
import static io.grpc.MethodDescriptor.generateFullMethodName;
|
||||
|
||||
/**
|
||||
*/
|
||||
@javax.annotation.Generated(
|
||||
value = "by gRPC proto compiler",
|
||||
comments = "Source: grpc/gcp/s2a/s2a.proto")
|
||||
@io.grpc.stub.annotations.GrpcGenerated
|
||||
public final class S2AServiceGrpc {
|
||||
|
||||
private S2AServiceGrpc() {}
|
||||
|
||||
public static final java.lang.String SERVICE_NAME = "grpc.gcp.s2a.S2AService";
|
||||
|
||||
// Static method descriptors that strictly reflect the proto.
|
||||
private static volatile io.grpc.MethodDescriptor<io.grpc.s2a.handshaker.SessionReq,
|
||||
io.grpc.s2a.handshaker.SessionResp> getSetUpSessionMethod;
|
||||
|
||||
@io.grpc.stub.annotations.RpcMethod(
|
||||
fullMethodName = SERVICE_NAME + '/' + "SetUpSession",
|
||||
requestType = io.grpc.s2a.handshaker.SessionReq.class,
|
||||
responseType = io.grpc.s2a.handshaker.SessionResp.class,
|
||||
methodType = io.grpc.MethodDescriptor.MethodType.BIDI_STREAMING)
|
||||
public static io.grpc.MethodDescriptor<io.grpc.s2a.handshaker.SessionReq,
|
||||
io.grpc.s2a.handshaker.SessionResp> getSetUpSessionMethod() {
|
||||
io.grpc.MethodDescriptor<io.grpc.s2a.handshaker.SessionReq, io.grpc.s2a.handshaker.SessionResp> getSetUpSessionMethod;
|
||||
if ((getSetUpSessionMethod = S2AServiceGrpc.getSetUpSessionMethod) == null) {
|
||||
synchronized (S2AServiceGrpc.class) {
|
||||
if ((getSetUpSessionMethod = S2AServiceGrpc.getSetUpSessionMethod) == null) {
|
||||
S2AServiceGrpc.getSetUpSessionMethod = getSetUpSessionMethod =
|
||||
io.grpc.MethodDescriptor.<io.grpc.s2a.handshaker.SessionReq, io.grpc.s2a.handshaker.SessionResp>newBuilder()
|
||||
.setType(io.grpc.MethodDescriptor.MethodType.BIDI_STREAMING)
|
||||
.setFullMethodName(generateFullMethodName(SERVICE_NAME, "SetUpSession"))
|
||||
.setSampledToLocalTracing(true)
|
||||
.setRequestMarshaller(io.grpc.protobuf.ProtoUtils.marshaller(
|
||||
io.grpc.s2a.handshaker.SessionReq.getDefaultInstance()))
|
||||
.setResponseMarshaller(io.grpc.protobuf.ProtoUtils.marshaller(
|
||||
io.grpc.s2a.handshaker.SessionResp.getDefaultInstance()))
|
||||
.setSchemaDescriptor(new S2AServiceMethodDescriptorSupplier("SetUpSession"))
|
||||
.build();
|
||||
}
|
||||
}
|
||||
}
|
||||
return getSetUpSessionMethod;
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new async stub that supports all call types for the service
|
||||
*/
|
||||
public static S2AServiceStub newStub(io.grpc.Channel channel) {
|
||||
io.grpc.stub.AbstractStub.StubFactory<S2AServiceStub> factory =
|
||||
new io.grpc.stub.AbstractStub.StubFactory<S2AServiceStub>() {
|
||||
@java.lang.Override
|
||||
public S2AServiceStub newStub(io.grpc.Channel channel, io.grpc.CallOptions callOptions) {
|
||||
return new S2AServiceStub(channel, callOptions);
|
||||
}
|
||||
};
|
||||
return S2AServiceStub.newStub(factory, channel);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new blocking-style stub that supports unary and streaming output calls on the service
|
||||
*/
|
||||
public static S2AServiceBlockingStub newBlockingStub(
|
||||
io.grpc.Channel channel) {
|
||||
io.grpc.stub.AbstractStub.StubFactory<S2AServiceBlockingStub> factory =
|
||||
new io.grpc.stub.AbstractStub.StubFactory<S2AServiceBlockingStub>() {
|
||||
@java.lang.Override
|
||||
public S2AServiceBlockingStub newStub(io.grpc.Channel channel, io.grpc.CallOptions callOptions) {
|
||||
return new S2AServiceBlockingStub(channel, callOptions);
|
||||
}
|
||||
};
|
||||
return S2AServiceBlockingStub.newStub(factory, channel);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new ListenableFuture-style stub that supports unary calls on the service
|
||||
*/
|
||||
public static S2AServiceFutureStub newFutureStub(
|
||||
io.grpc.Channel channel) {
|
||||
io.grpc.stub.AbstractStub.StubFactory<S2AServiceFutureStub> factory =
|
||||
new io.grpc.stub.AbstractStub.StubFactory<S2AServiceFutureStub>() {
|
||||
@java.lang.Override
|
||||
public S2AServiceFutureStub newStub(io.grpc.Channel channel, io.grpc.CallOptions callOptions) {
|
||||
return new S2AServiceFutureStub(channel, callOptions);
|
||||
}
|
||||
};
|
||||
return S2AServiceFutureStub.newStub(factory, channel);
|
||||
}
|
||||
|
||||
/**
|
||||
*/
|
||||
public interface AsyncService {
|
||||
|
||||
/**
|
||||
* <pre>
|
||||
* SetUpSession is a bidirectional stream used by applications to offload
|
||||
* operations from the TLS handshake.
|
||||
* </pre>
|
||||
*/
|
||||
default io.grpc.stub.StreamObserver<io.grpc.s2a.handshaker.SessionReq> setUpSession(
|
||||
io.grpc.stub.StreamObserver<io.grpc.s2a.handshaker.SessionResp> responseObserver) {
|
||||
return io.grpc.stub.ServerCalls.asyncUnimplementedStreamingCall(getSetUpSessionMethod(), responseObserver);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Base class for the server implementation of the service S2AService.
|
||||
*/
|
||||
public static abstract class S2AServiceImplBase
|
||||
implements io.grpc.BindableService, AsyncService {
|
||||
|
||||
@java.lang.Override public final io.grpc.ServerServiceDefinition bindService() {
|
||||
return S2AServiceGrpc.bindService(this);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* A stub to allow clients to do asynchronous rpc calls to service S2AService.
|
||||
*/
|
||||
public static final class S2AServiceStub
|
||||
extends io.grpc.stub.AbstractAsyncStub<S2AServiceStub> {
|
||||
private S2AServiceStub(
|
||||
io.grpc.Channel channel, io.grpc.CallOptions callOptions) {
|
||||
super(channel, callOptions);
|
||||
}
|
||||
|
||||
@java.lang.Override
|
||||
protected S2AServiceStub build(
|
||||
io.grpc.Channel channel, io.grpc.CallOptions callOptions) {
|
||||
return new S2AServiceStub(channel, callOptions);
|
||||
}
|
||||
|
||||
/**
|
||||
* <pre>
|
||||
* SetUpSession is a bidirectional stream used by applications to offload
|
||||
* operations from the TLS handshake.
|
||||
* </pre>
|
||||
*/
|
||||
public io.grpc.stub.StreamObserver<io.grpc.s2a.handshaker.SessionReq> setUpSession(
|
||||
io.grpc.stub.StreamObserver<io.grpc.s2a.handshaker.SessionResp> responseObserver) {
|
||||
return io.grpc.stub.ClientCalls.asyncBidiStreamingCall(
|
||||
getChannel().newCall(getSetUpSessionMethod(), getCallOptions()), responseObserver);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* A stub to allow clients to do synchronous rpc calls to service S2AService.
|
||||
*/
|
||||
public static final class S2AServiceBlockingStub
|
||||
extends io.grpc.stub.AbstractBlockingStub<S2AServiceBlockingStub> {
|
||||
private S2AServiceBlockingStub(
|
||||
io.grpc.Channel channel, io.grpc.CallOptions callOptions) {
|
||||
super(channel, callOptions);
|
||||
}
|
||||
|
||||
@java.lang.Override
|
||||
protected S2AServiceBlockingStub build(
|
||||
io.grpc.Channel channel, io.grpc.CallOptions callOptions) {
|
||||
return new S2AServiceBlockingStub(channel, callOptions);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* A stub to allow clients to do ListenableFuture-style rpc calls to service S2AService.
|
||||
*/
|
||||
public static final class S2AServiceFutureStub
|
||||
extends io.grpc.stub.AbstractFutureStub<S2AServiceFutureStub> {
|
||||
private S2AServiceFutureStub(
|
||||
io.grpc.Channel channel, io.grpc.CallOptions callOptions) {
|
||||
super(channel, callOptions);
|
||||
}
|
||||
|
||||
@java.lang.Override
|
||||
protected S2AServiceFutureStub build(
|
||||
io.grpc.Channel channel, io.grpc.CallOptions callOptions) {
|
||||
return new S2AServiceFutureStub(channel, callOptions);
|
||||
}
|
||||
}
|
||||
|
||||
private static final int METHODID_SET_UP_SESSION = 0;
|
||||
|
||||
private static final class MethodHandlers<Req, Resp> implements
|
||||
io.grpc.stub.ServerCalls.UnaryMethod<Req, Resp>,
|
||||
io.grpc.stub.ServerCalls.ServerStreamingMethod<Req, Resp>,
|
||||
io.grpc.stub.ServerCalls.ClientStreamingMethod<Req, Resp>,
|
||||
io.grpc.stub.ServerCalls.BidiStreamingMethod<Req, Resp> {
|
||||
private final AsyncService serviceImpl;
|
||||
private final int methodId;
|
||||
|
||||
MethodHandlers(AsyncService serviceImpl, int methodId) {
|
||||
this.serviceImpl = serviceImpl;
|
||||
this.methodId = methodId;
|
||||
}
|
||||
|
||||
@java.lang.Override
|
||||
@java.lang.SuppressWarnings("unchecked")
|
||||
public void invoke(Req request, io.grpc.stub.StreamObserver<Resp> responseObserver) {
|
||||
switch (methodId) {
|
||||
default:
|
||||
throw new AssertionError();
|
||||
}
|
||||
}
|
||||
|
||||
@java.lang.Override
|
||||
@java.lang.SuppressWarnings("unchecked")
|
||||
public io.grpc.stub.StreamObserver<Req> invoke(
|
||||
io.grpc.stub.StreamObserver<Resp> responseObserver) {
|
||||
switch (methodId) {
|
||||
case METHODID_SET_UP_SESSION:
|
||||
return (io.grpc.stub.StreamObserver<Req>) serviceImpl.setUpSession(
|
||||
(io.grpc.stub.StreamObserver<io.grpc.s2a.handshaker.SessionResp>) responseObserver);
|
||||
default:
|
||||
throw new AssertionError();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public static final io.grpc.ServerServiceDefinition bindService(AsyncService service) {
|
||||
return io.grpc.ServerServiceDefinition.builder(getServiceDescriptor())
|
||||
.addMethod(
|
||||
getSetUpSessionMethod(),
|
||||
io.grpc.stub.ServerCalls.asyncBidiStreamingCall(
|
||||
new MethodHandlers<
|
||||
io.grpc.s2a.handshaker.SessionReq,
|
||||
io.grpc.s2a.handshaker.SessionResp>(
|
||||
service, METHODID_SET_UP_SESSION)))
|
||||
.build();
|
||||
}
|
||||
|
||||
private static abstract class S2AServiceBaseDescriptorSupplier
|
||||
implements io.grpc.protobuf.ProtoFileDescriptorSupplier, io.grpc.protobuf.ProtoServiceDescriptorSupplier {
|
||||
S2AServiceBaseDescriptorSupplier() {}
|
||||
|
||||
@java.lang.Override
|
||||
public com.google.protobuf.Descriptors.FileDescriptor getFileDescriptor() {
|
||||
return io.grpc.s2a.handshaker.S2AProto.getDescriptor();
|
||||
}
|
||||
|
||||
@java.lang.Override
|
||||
public com.google.protobuf.Descriptors.ServiceDescriptor getServiceDescriptor() {
|
||||
return getFileDescriptor().findServiceByName("S2AService");
|
||||
}
|
||||
}
|
||||
|
||||
private static final class S2AServiceFileDescriptorSupplier
|
||||
extends S2AServiceBaseDescriptorSupplier {
|
||||
S2AServiceFileDescriptorSupplier() {}
|
||||
}
|
||||
|
||||
private static final class S2AServiceMethodDescriptorSupplier
|
||||
extends S2AServiceBaseDescriptorSupplier
|
||||
implements io.grpc.protobuf.ProtoMethodDescriptorSupplier {
|
||||
private final java.lang.String methodName;
|
||||
|
||||
S2AServiceMethodDescriptorSupplier(java.lang.String methodName) {
|
||||
this.methodName = methodName;
|
||||
}
|
||||
|
||||
@java.lang.Override
|
||||
public com.google.protobuf.Descriptors.MethodDescriptor getMethodDescriptor() {
|
||||
return getServiceDescriptor().findMethodByName(methodName);
|
||||
}
|
||||
}
|
||||
|
||||
private static volatile io.grpc.ServiceDescriptor serviceDescriptor;
|
||||
|
||||
public static io.grpc.ServiceDescriptor getServiceDescriptor() {
|
||||
io.grpc.ServiceDescriptor result = serviceDescriptor;
|
||||
if (result == null) {
|
||||
synchronized (S2AServiceGrpc.class) {
|
||||
result = serviceDescriptor;
|
||||
if (result == null) {
|
||||
serviceDescriptor = result = io.grpc.ServiceDescriptor.newBuilder(SERVICE_NAME)
|
||||
.setSchemaDescriptor(new S2AServiceFileDescriptorSupplier())
|
||||
.addMethod(getSetUpSessionMethod())
|
||||
.build();
|
||||
}
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,96 @@
|
|||
/*
|
||||
* Copyright 2024 The gRPC Authors
|
||||
*
|
||||
* 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 io.grpc.s2a;
|
||||
|
||||
import static com.google.common.base.Preconditions.checkArgument;
|
||||
import static com.google.common.base.Preconditions.checkState;
|
||||
import static com.google.common.base.Strings.isNullOrEmpty;
|
||||
|
||||
import io.grpc.ChannelCredentials;
|
||||
import io.grpc.TlsChannelCredentials;
|
||||
import io.grpc.util.AdvancedTlsX509KeyManager;
|
||||
import io.grpc.util.AdvancedTlsX509TrustManager;
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.security.GeneralSecurityException;
|
||||
|
||||
/**
|
||||
* Configures an {@code S2AChannelCredentials.Builder} instance with credentials used to establish a
|
||||
* connection with the S2A to support talking to the S2A over mTLS.
|
||||
*/
|
||||
public final class MtlsToS2AChannelCredentials {
|
||||
/**
|
||||
* Creates a {@code S2AChannelCredentials.Builder} builder, that talks to the S2A over mTLS.
|
||||
*
|
||||
* @param s2aAddress the address of the S2A server used to secure the connection.
|
||||
* @param privateKeyPath the path to the private key PEM to use for authenticating to the S2A.
|
||||
* @param certChainPath the path to the cert chain PEM to use for authenticating to the S2A.
|
||||
* @param trustBundlePath the path to the trust bundle PEM.
|
||||
* @return a {@code MtlsToS2AChannelCredentials.Builder} instance.
|
||||
*/
|
||||
public static Builder createBuilder(
|
||||
String s2aAddress, String privateKeyPath, String certChainPath, String trustBundlePath) {
|
||||
checkArgument(!isNullOrEmpty(s2aAddress), "S2A address must not be null or empty.");
|
||||
checkArgument(!isNullOrEmpty(privateKeyPath), "privateKeyPath must not be null or empty.");
|
||||
checkArgument(!isNullOrEmpty(certChainPath), "certChainPath must not be null or empty.");
|
||||
checkArgument(!isNullOrEmpty(trustBundlePath), "trustBundlePath must not be null or empty.");
|
||||
return new Builder(s2aAddress, privateKeyPath, certChainPath, trustBundlePath);
|
||||
}
|
||||
|
||||
/** Builds an {@code MtlsToS2AChannelCredentials} instance. */
|
||||
public static final class Builder {
|
||||
private final String s2aAddress;
|
||||
private final String privateKeyPath;
|
||||
private final String certChainPath;
|
||||
private final String trustBundlePath;
|
||||
|
||||
Builder(
|
||||
String s2aAddress, String privateKeyPath, String certChainPath, String trustBundlePath) {
|
||||
this.s2aAddress = s2aAddress;
|
||||
this.privateKeyPath = privateKeyPath;
|
||||
this.certChainPath = certChainPath;
|
||||
this.trustBundlePath = trustBundlePath;
|
||||
}
|
||||
|
||||
public S2AChannelCredentials.Builder build() throws GeneralSecurityException, IOException {
|
||||
checkState(!isNullOrEmpty(s2aAddress), "S2A address must not be null or empty.");
|
||||
checkState(!isNullOrEmpty(privateKeyPath), "privateKeyPath must not be null or empty.");
|
||||
checkState(!isNullOrEmpty(certChainPath), "certChainPath must not be null or empty.");
|
||||
checkState(!isNullOrEmpty(trustBundlePath), "trustBundlePath must not be null or empty.");
|
||||
File privateKeyFile = new File(privateKeyPath);
|
||||
File certChainFile = new File(certChainPath);
|
||||
File trustBundleFile = new File(trustBundlePath);
|
||||
|
||||
AdvancedTlsX509KeyManager keyManager = new AdvancedTlsX509KeyManager();
|
||||
keyManager.updateIdentityCredentials(certChainFile, privateKeyFile);
|
||||
|
||||
AdvancedTlsX509TrustManager trustManager = AdvancedTlsX509TrustManager.newBuilder().build();
|
||||
trustManager.updateTrustCredentials(trustBundleFile);
|
||||
|
||||
ChannelCredentials channelToS2ACredentials =
|
||||
TlsChannelCredentials.newBuilder()
|
||||
.keyManager(keyManager)
|
||||
.trustManager(trustManager)
|
||||
.build();
|
||||
|
||||
return S2AChannelCredentials.createBuilder(s2aAddress)
|
||||
.setS2AChannelCredentials(channelToS2ACredentials);
|
||||
}
|
||||
}
|
||||
|
||||
private MtlsToS2AChannelCredentials() {}
|
||||
}
|
|
@ -0,0 +1,130 @@
|
|||
/*
|
||||
* Copyright 2024 The gRPC Authors
|
||||
*
|
||||
* 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 io.grpc.s2a;
|
||||
|
||||
import static com.google.common.base.Preconditions.checkArgument;
|
||||
import static com.google.common.base.Preconditions.checkNotNull;
|
||||
import static com.google.common.base.Preconditions.checkState;
|
||||
import static com.google.common.base.Strings.isNullOrEmpty;
|
||||
|
||||
import com.google.errorprone.annotations.CanIgnoreReturnValue;
|
||||
import io.grpc.Channel;
|
||||
import io.grpc.ChannelCredentials;
|
||||
import io.grpc.internal.ObjectPool;
|
||||
import io.grpc.internal.SharedResourcePool;
|
||||
import io.grpc.netty.InternalNettyChannelCredentials;
|
||||
import io.grpc.netty.InternalProtocolNegotiator;
|
||||
import io.grpc.s2a.channel.S2AHandshakerServiceChannel;
|
||||
import io.grpc.s2a.handshaker.S2AIdentity;
|
||||
import io.grpc.s2a.handshaker.S2AProtocolNegotiatorFactory;
|
||||
import java.util.Optional;
|
||||
import javax.annotation.concurrent.NotThreadSafe;
|
||||
import org.checkerframework.checker.nullness.qual.Nullable;
|
||||
|
||||
/**
|
||||
* Configures gRPC to use S2A for transport security when establishing a secure channel. Only for
|
||||
* use on the client side of a gRPC connection.
|
||||
*/
|
||||
public final class S2AChannelCredentials {
|
||||
/**
|
||||
* Creates a channel credentials builder for establishing an S2A-secured connection.
|
||||
*
|
||||
* @param s2aAddress the address of the S2A server used to secure the connection.
|
||||
* @return a {@code S2AChannelCredentials.Builder} instance.
|
||||
*/
|
||||
public static Builder createBuilder(String s2aAddress) {
|
||||
checkArgument(!isNullOrEmpty(s2aAddress), "S2A address must not be null or empty.");
|
||||
return new Builder(s2aAddress);
|
||||
}
|
||||
|
||||
/** Builds an {@code S2AChannelCredentials} instance. */
|
||||
@NotThreadSafe
|
||||
public static final class Builder {
|
||||
private final String s2aAddress;
|
||||
private ObjectPool<Channel> s2aChannelPool;
|
||||
private Optional<ChannelCredentials> s2aChannelCredentials;
|
||||
private @Nullable S2AIdentity localIdentity = null;
|
||||
|
||||
Builder(String s2aAddress) {
|
||||
this.s2aAddress = s2aAddress;
|
||||
this.s2aChannelPool = null;
|
||||
this.s2aChannelCredentials = Optional.empty();
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the local identity of the client in the form of a SPIFFE ID. The client may set at most
|
||||
* 1 local identity. If no local identity is specified, then the S2A chooses a default local
|
||||
* identity, if one exists.
|
||||
*/
|
||||
@CanIgnoreReturnValue
|
||||
public Builder setLocalSpiffeId(String localSpiffeId) {
|
||||
checkNotNull(localSpiffeId);
|
||||
checkArgument(localIdentity == null, "localIdentity is already set.");
|
||||
localIdentity = S2AIdentity.fromSpiffeId(localSpiffeId);
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the local identity of the client in the form of a hostname. The client may set at most 1
|
||||
* local identity. If no local identity is specified, then the S2A chooses a default local
|
||||
* identity, if one exists.
|
||||
*/
|
||||
@CanIgnoreReturnValue
|
||||
public Builder setLocalHostname(String localHostname) {
|
||||
checkNotNull(localHostname);
|
||||
checkArgument(localIdentity == null, "localIdentity is already set.");
|
||||
localIdentity = S2AIdentity.fromHostname(localHostname);
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the local identity of the client in the form of a UID. The client may set at most 1
|
||||
* local identity. If no local identity is specified, then the S2A chooses a default local
|
||||
* identity, if one exists.
|
||||
*/
|
||||
@CanIgnoreReturnValue
|
||||
public Builder setLocalUid(String localUid) {
|
||||
checkNotNull(localUid);
|
||||
checkArgument(localIdentity == null, "localIdentity is already set.");
|
||||
localIdentity = S2AIdentity.fromUid(localUid);
|
||||
return this;
|
||||
}
|
||||
|
||||
/** Sets the credentials to be used when connecting to the S2A. */
|
||||
@CanIgnoreReturnValue
|
||||
public Builder setS2AChannelCredentials(ChannelCredentials s2aChannelCredentials) {
|
||||
this.s2aChannelCredentials = Optional.of(s2aChannelCredentials);
|
||||
return this;
|
||||
}
|
||||
|
||||
public ChannelCredentials build() {
|
||||
checkState(!isNullOrEmpty(s2aAddress), "S2A address must not be null or empty.");
|
||||
ObjectPool<Channel> s2aChannelPool =
|
||||
SharedResourcePool.forResource(
|
||||
S2AHandshakerServiceChannel.getChannelResource(s2aAddress, s2aChannelCredentials));
|
||||
checkNotNull(s2aChannelPool, "s2aChannelPool");
|
||||
this.s2aChannelPool = s2aChannelPool;
|
||||
return InternalNettyChannelCredentials.create(buildProtocolNegotiatorFactory());
|
||||
}
|
||||
|
||||
InternalProtocolNegotiator.ClientFactory buildProtocolNegotiatorFactory() {
|
||||
return S2AProtocolNegotiatorFactory.createClientFactory(localIdentity, s2aChannelPool);
|
||||
}
|
||||
}
|
||||
|
||||
private S2AChannelCredentials() {}
|
||||
}
|
|
@ -0,0 +1,43 @@
|
|||
/*
|
||||
* Copyright 2024 The gRPC Authors
|
||||
*
|
||||
* 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 io.grpc.s2a.channel;
|
||||
|
||||
import com.google.errorprone.annotations.CanIgnoreReturnValue;
|
||||
import io.grpc.Channel;
|
||||
import javax.annotation.concurrent.ThreadSafe;
|
||||
|
||||
/** Manages a channel pool to be used for communication with the S2A. */
|
||||
@ThreadSafe
|
||||
public interface S2AChannelPool extends AutoCloseable {
|
||||
/**
|
||||
* Retrieves an open channel to the S2A from the channel pool.
|
||||
*
|
||||
* @throws IllegalStateException if no channel is available.
|
||||
*/
|
||||
@CanIgnoreReturnValue
|
||||
Channel getChannel();
|
||||
|
||||
/** Returns a channel to the channel pool. */
|
||||
void returnToPool(Channel channel);
|
||||
|
||||
/**
|
||||
* Returns all channels to the channel pool and closes the pool so that no new channels can be
|
||||
* retrieved from the pool.
|
||||
*/
|
||||
@Override
|
||||
void close();
|
||||
}
|
|
@ -0,0 +1,109 @@
|
|||
/*
|
||||
* Copyright 2024 The gRPC Authors
|
||||
*
|
||||
* 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 io.grpc.s2a.channel;
|
||||
|
||||
import static com.google.common.base.Preconditions.checkArgument;
|
||||
import static com.google.common.base.Preconditions.checkNotNull;
|
||||
import static com.google.common.base.Preconditions.checkState;
|
||||
|
||||
import com.google.errorprone.annotations.concurrent.GuardedBy;
|
||||
import io.grpc.Channel;
|
||||
import io.grpc.internal.ObjectPool;
|
||||
import javax.annotation.concurrent.ThreadSafe;
|
||||
import org.checkerframework.checker.nullness.qual.Nullable;
|
||||
|
||||
/**
|
||||
* Manages a gRPC channel pool and a cached gRPC channel to be used for communication with the S2A.
|
||||
*/
|
||||
@ThreadSafe
|
||||
public final class S2AGrpcChannelPool implements S2AChannelPool {
|
||||
private static final int MAX_NUMBER_USERS_OF_CACHED_CHANNEL = 100000;
|
||||
private final ObjectPool<Channel> channelPool;
|
||||
|
||||
@GuardedBy("this")
|
||||
private @Nullable Channel cachedChannel;
|
||||
|
||||
@GuardedBy("this")
|
||||
private int numberOfUsersOfCachedChannel = 0;
|
||||
|
||||
private enum State {
|
||||
OPEN,
|
||||
CLOSED,
|
||||
}
|
||||
|
||||
;
|
||||
|
||||
@GuardedBy("this")
|
||||
private State state = State.OPEN;
|
||||
|
||||
public static S2AChannelPool create(ObjectPool<Channel> channelPool) {
|
||||
checkNotNull(channelPool, "Channel pool should not be null.");
|
||||
return new S2AGrpcChannelPool(channelPool);
|
||||
}
|
||||
|
||||
private S2AGrpcChannelPool(ObjectPool<Channel> channelPool) {
|
||||
this.channelPool = channelPool;
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves a channel from {@code channelPool} if {@code channel} is null, and returns {@code
|
||||
* channel} otherwise.
|
||||
*
|
||||
* @return a {@link Channel} obtained from the channel pool.
|
||||
*/
|
||||
@Override
|
||||
public synchronized Channel getChannel() {
|
||||
checkState(state.equals(State.OPEN), "Channel pool is not open.");
|
||||
checkState(
|
||||
numberOfUsersOfCachedChannel < MAX_NUMBER_USERS_OF_CACHED_CHANNEL,
|
||||
"Max number of channels have been retrieved from the channel pool.");
|
||||
if (cachedChannel == null) {
|
||||
cachedChannel = channelPool.getObject();
|
||||
}
|
||||
numberOfUsersOfCachedChannel += 1;
|
||||
return cachedChannel;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns {@code channel} to {@code channelPool}.
|
||||
*
|
||||
* <p>The caller must ensure that {@code channel} was retrieved from this channel pool.
|
||||
*/
|
||||
@Override
|
||||
public synchronized void returnToPool(Channel channel) {
|
||||
checkState(state.equals(State.OPEN), "Channel pool is not open.");
|
||||
checkArgument(
|
||||
cachedChannel != null && numberOfUsersOfCachedChannel > 0 && cachedChannel.equals(channel),
|
||||
"Cannot return the channel to channel pool because the channel was not obtained from"
|
||||
+ " channel pool.");
|
||||
numberOfUsersOfCachedChannel -= 1;
|
||||
if (numberOfUsersOfCachedChannel == 0) {
|
||||
channelPool.returnObject(channel);
|
||||
cachedChannel = null;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public synchronized void close() {
|
||||
state = State.CLOSED;
|
||||
numberOfUsersOfCachedChannel = 0;
|
||||
if (cachedChannel != null) {
|
||||
channelPool.returnObject(cachedChannel);
|
||||
cachedChannel = null;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,195 @@
|
|||
/*
|
||||
* Copyright 2024 The gRPC Authors
|
||||
*
|
||||
* 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 io.grpc.s2a.channel;
|
||||
|
||||
import static com.google.common.base.Preconditions.checkNotNull;
|
||||
import static java.util.concurrent.TimeUnit.SECONDS;
|
||||
|
||||
import com.google.common.annotations.VisibleForTesting;
|
||||
import com.google.common.collect.Maps;
|
||||
import io.grpc.CallOptions;
|
||||
import io.grpc.Channel;
|
||||
import io.grpc.ChannelCredentials;
|
||||
import io.grpc.ClientCall;
|
||||
import io.grpc.ManagedChannel;
|
||||
import io.grpc.MethodDescriptor;
|
||||
import io.grpc.internal.SharedResourceHolder.Resource;
|
||||
import io.grpc.netty.NettyChannelBuilder;
|
||||
import io.netty.channel.EventLoopGroup;
|
||||
import io.netty.channel.nio.NioEventLoopGroup;
|
||||
import io.netty.channel.socket.nio.NioSocketChannel;
|
||||
import io.netty.util.concurrent.DefaultThreadFactory;
|
||||
import java.time.Duration;
|
||||
import java.util.Optional;
|
||||
import java.util.concurrent.ConcurrentMap;
|
||||
import javax.annotation.concurrent.ThreadSafe;
|
||||
|
||||
/**
|
||||
* Provides APIs for managing gRPC channels to S2A servers. Each channel is local and plaintext. If
|
||||
* credentials are provided, they are used to secure the channel.
|
||||
*
|
||||
* <p>This is done as follows: for each S2A server, provides an implementation of gRPC's {@link
|
||||
* SharedResourceHolder.Resource} interface called a {@code Resource<Channel>}. A {@code
|
||||
* Resource<Channel>} is a factory for creating gRPC channels to the S2A server at a given address,
|
||||
* and a channel must be returned to the {@code Resource<Channel>} when it is no longer needed.
|
||||
*
|
||||
* <p>Typical usage pattern is below:
|
||||
*
|
||||
* <pre>{@code
|
||||
* Resource<Channel> resource = S2AHandshakerServiceChannel.getChannelResource("localhost:1234",
|
||||
* creds);
|
||||
* Channel channel = resource.create();
|
||||
* // Send an RPC over the channel to the S2A server running at localhost:1234.
|
||||
* resource.close(channel);
|
||||
* }</pre>
|
||||
*/
|
||||
@ThreadSafe
|
||||
public final class S2AHandshakerServiceChannel {
|
||||
private static final ConcurrentMap<String, Resource<Channel>> SHARED_RESOURCE_CHANNELS =
|
||||
Maps.newConcurrentMap();
|
||||
private static final Duration DELEGATE_TERMINATION_TIMEOUT = Duration.ofSeconds(2);
|
||||
private static final Duration CHANNEL_SHUTDOWN_TIMEOUT = Duration.ofSeconds(10);
|
||||
|
||||
/**
|
||||
* Returns a {@link SharedResourceHolder.Resource} instance for managing channels to an S2A server
|
||||
* running at {@code s2aAddress}.
|
||||
*
|
||||
* @param s2aAddress the address of the S2A, typically in the format {@code host:port}.
|
||||
* @param s2aChannelCredentials the credentials to use when establishing a connection to the S2A.
|
||||
* @return a {@link ChannelResource} instance that manages a {@link Channel} to the S2A server
|
||||
* running at {@code s2aAddress}.
|
||||
*/
|
||||
public static Resource<Channel> getChannelResource(
|
||||
String s2aAddress, Optional<ChannelCredentials> s2aChannelCredentials) {
|
||||
checkNotNull(s2aAddress);
|
||||
return SHARED_RESOURCE_CHANNELS.computeIfAbsent(
|
||||
s2aAddress, channelResource -> new ChannelResource(s2aAddress, s2aChannelCredentials));
|
||||
}
|
||||
|
||||
/**
|
||||
* Defines how to create and destroy a {@link Channel} instance that uses shared resources. A
|
||||
* channel created by {@code ChannelResource} is a plaintext, local channel to the service running
|
||||
* at {@code targetAddress}.
|
||||
*/
|
||||
private static class ChannelResource implements Resource<Channel> {
|
||||
private final String targetAddress;
|
||||
private final Optional<ChannelCredentials> channelCredentials;
|
||||
|
||||
public ChannelResource(String targetAddress, Optional<ChannelCredentials> channelCredentials) {
|
||||
this.targetAddress = targetAddress;
|
||||
this.channelCredentials = channelCredentials;
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a {@code EventLoopHoldingChannel} instance to the service running at {@code
|
||||
* targetAddress}. This channel uses a dedicated thread pool for its {@code EventLoopGroup}
|
||||
* instance to avoid blocking.
|
||||
*/
|
||||
@Override
|
||||
public Channel create() {
|
||||
EventLoopGroup eventLoopGroup =
|
||||
new NioEventLoopGroup(1, new DefaultThreadFactory("S2A channel pool", true));
|
||||
ManagedChannel channel = null;
|
||||
if (channelCredentials.isPresent()) {
|
||||
// Create a secure channel.
|
||||
channel =
|
||||
NettyChannelBuilder.forTarget(targetAddress, channelCredentials.get())
|
||||
.channelType(NioSocketChannel.class)
|
||||
.directExecutor()
|
||||
.eventLoopGroup(eventLoopGroup)
|
||||
.build();
|
||||
} else {
|
||||
// Create a plaintext channel.
|
||||
channel =
|
||||
NettyChannelBuilder.forTarget(targetAddress)
|
||||
.channelType(NioSocketChannel.class)
|
||||
.directExecutor()
|
||||
.eventLoopGroup(eventLoopGroup)
|
||||
.usePlaintext()
|
||||
.build();
|
||||
}
|
||||
return EventLoopHoldingChannel.create(channel, eventLoopGroup);
|
||||
}
|
||||
|
||||
/** Destroys a {@code EventLoopHoldingChannel} instance. */
|
||||
@Override
|
||||
public void close(Channel instanceChannel) {
|
||||
checkNotNull(instanceChannel);
|
||||
EventLoopHoldingChannel channel = (EventLoopHoldingChannel) instanceChannel;
|
||||
channel.close();
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return "grpc-s2a-channel";
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Manages a channel using a {@link ManagedChannel} instance that belong to the {@code
|
||||
* EventLoopGroup} thread pool.
|
||||
*/
|
||||
@VisibleForTesting
|
||||
static class EventLoopHoldingChannel extends Channel {
|
||||
private final ManagedChannel delegate;
|
||||
private final EventLoopGroup eventLoopGroup;
|
||||
|
||||
static EventLoopHoldingChannel create(ManagedChannel delegate, EventLoopGroup eventLoopGroup) {
|
||||
checkNotNull(delegate);
|
||||
checkNotNull(eventLoopGroup);
|
||||
return new EventLoopHoldingChannel(delegate, eventLoopGroup);
|
||||
}
|
||||
|
||||
private EventLoopHoldingChannel(ManagedChannel delegate, EventLoopGroup eventLoopGroup) {
|
||||
this.delegate = delegate;
|
||||
this.eventLoopGroup = eventLoopGroup;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the address of the service to which the {@code delegate} channel connects, which is
|
||||
* typically of the form {@code host:port}.
|
||||
*/
|
||||
@Override
|
||||
public String authority() {
|
||||
return delegate.authority();
|
||||
}
|
||||
|
||||
/** Creates a {@link ClientCall} that invokes the operations in {@link MethodDescriptor}. */
|
||||
@Override
|
||||
public <ReqT, RespT> ClientCall<ReqT, RespT> newCall(
|
||||
MethodDescriptor<ReqT, RespT> methodDescriptor, CallOptions options) {
|
||||
return delegate.newCall(methodDescriptor, options);
|
||||
}
|
||||
|
||||
@SuppressWarnings("FutureReturnValueIgnored")
|
||||
public void close() {
|
||||
delegate.shutdownNow();
|
||||
boolean isDelegateTerminated;
|
||||
try {
|
||||
isDelegateTerminated =
|
||||
delegate.awaitTermination(DELEGATE_TERMINATION_TIMEOUT.getSeconds(), SECONDS);
|
||||
} catch (InterruptedException e) {
|
||||
isDelegateTerminated = false;
|
||||
}
|
||||
long quietPeriodSeconds = isDelegateTerminated ? 0 : 1;
|
||||
eventLoopGroup.shutdownGracefully(
|
||||
quietPeriodSeconds, CHANNEL_SHUTDOWN_TIMEOUT.getSeconds(), SECONDS);
|
||||
}
|
||||
}
|
||||
|
||||
private S2AHandshakerServiceChannel() {}
|
||||
}
|
|
@ -0,0 +1,27 @@
|
|||
/*
|
||||
* Copyright 2024 The gRPC Authors
|
||||
*
|
||||
* 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 io.grpc.s2a.handshaker;
|
||||
|
||||
import java.io.IOException;
|
||||
|
||||
/** Indicates that a connection has been closed. */
|
||||
@SuppressWarnings("serial") // This class is never serialized.
|
||||
final class ConnectionClosedException extends IOException {
|
||||
public ConnectionClosedException(String errorMessage) {
|
||||
super(errorMessage);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,56 @@
|
|||
/*
|
||||
* Copyright 2024 The gRPC Authors
|
||||
*
|
||||
* 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 io.grpc.s2a.handshaker;
|
||||
|
||||
import com.google.errorprone.annotations.Immutable;
|
||||
import io.grpc.s2a.handshaker.S2AIdentity;
|
||||
import io.grpc.s2a.handshaker.tokenmanager.AccessTokenManager;
|
||||
import java.util.Optional;
|
||||
|
||||
/** Retrieves the authentication mechanism for a given local identity. */
|
||||
@Immutable
|
||||
final class GetAuthenticationMechanisms {
|
||||
private static final Optional<AccessTokenManager> TOKEN_MANAGER = AccessTokenManager.create();
|
||||
|
||||
/**
|
||||
* Retrieves the authentication mechanism for a given local identity.
|
||||
*
|
||||
* @param localIdentity the identity for which to fetch a token.
|
||||
* @return an {@link AuthenticationMechanism} for the given local identity.
|
||||
*/
|
||||
static Optional<AuthenticationMechanism> getAuthMechanism(Optional<S2AIdentity> localIdentity) {
|
||||
if (!TOKEN_MANAGER.isPresent()) {
|
||||
return Optional.empty();
|
||||
}
|
||||
AccessTokenManager manager = TOKEN_MANAGER.get();
|
||||
// If no identity is provided, fetch the default access token and DO NOT attach an identity
|
||||
// to the request.
|
||||
if (!localIdentity.isPresent()) {
|
||||
return Optional.of(
|
||||
AuthenticationMechanism.newBuilder().setToken(manager.getDefaultToken()).build());
|
||||
} else {
|
||||
// Fetch an access token for the provided identity.
|
||||
return Optional.of(
|
||||
AuthenticationMechanism.newBuilder()
|
||||
.setIdentity(localIdentity.get().getIdentity())
|
||||
.setToken(manager.getToken(localIdentity.get()))
|
||||
.build());
|
||||
}
|
||||
}
|
||||
|
||||
private GetAuthenticationMechanisms() {}
|
||||
}
|
|
@ -0,0 +1,96 @@
|
|||
/*
|
||||
* Copyright 2024 The gRPC Authors
|
||||
*
|
||||
* 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 io.grpc.s2a.handshaker;
|
||||
|
||||
import com.google.common.collect.ImmutableSet;
|
||||
|
||||
/** Converts proto messages to Netty strings. */
|
||||
final class ProtoUtil {
|
||||
/**
|
||||
* Converts {@link Ciphersuite} to its {@link String} representation.
|
||||
*
|
||||
* @param ciphersuite the {@link Ciphersuite} to be converted.
|
||||
* @return a {@link String} representing the ciphersuite.
|
||||
* @throws AssertionError if the {@link Ciphersuite} is not one of the supported ciphersuites.
|
||||
*/
|
||||
static String convertCiphersuite(Ciphersuite ciphersuite) {
|
||||
switch (ciphersuite) {
|
||||
case CIPHERSUITE_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256:
|
||||
return "TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256";
|
||||
case CIPHERSUITE_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384:
|
||||
return "TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384";
|
||||
case CIPHERSUITE_ECDHE_ECDSA_WITH_CHACHA20_POLY1305_SHA256:
|
||||
return "TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305_SHA256";
|
||||
case CIPHERSUITE_ECDHE_RSA_WITH_AES_128_GCM_SHA256:
|
||||
return "TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256";
|
||||
case CIPHERSUITE_ECDHE_RSA_WITH_AES_256_GCM_SHA384:
|
||||
return "TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384";
|
||||
case CIPHERSUITE_ECDHE_RSA_WITH_CHACHA20_POLY1305_SHA256:
|
||||
return "TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305_SHA256";
|
||||
default:
|
||||
throw new AssertionError(
|
||||
String.format("Ciphersuite %d is not supported.", ciphersuite.getNumber()));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts a {@link TLSVersion} object to its {@link String} representation.
|
||||
*
|
||||
* @param tlsVersion the {@link TLSVersion} object to be converted.
|
||||
* @return a {@link String} representation of the TLS version.
|
||||
* @throws AssertionError if the {@code tlsVersion} is not one of the supported TLS versions.
|
||||
*/
|
||||
static String convertTlsProtocolVersion(TLSVersion tlsVersion) {
|
||||
switch (tlsVersion) {
|
||||
case TLS_VERSION_1_3:
|
||||
return "TLSv1.3";
|
||||
case TLS_VERSION_1_2:
|
||||
return "TLSv1.2";
|
||||
case TLS_VERSION_1_1:
|
||||
return "TLSv1.1";
|
||||
case TLS_VERSION_1_0:
|
||||
return "TLSv1";
|
||||
default:
|
||||
throw new AssertionError(
|
||||
String.format("TLS version %d is not supported.", tlsVersion.getNumber()));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Builds a set of strings representing all {@link TLSVersion}s between {@code minTlsVersion} and
|
||||
* {@code maxTlsVersion}.
|
||||
*/
|
||||
static ImmutableSet<String> buildTlsProtocolVersionSet(
|
||||
TLSVersion minTlsVersion, TLSVersion maxTlsVersion) {
|
||||
ImmutableSet.Builder<String> tlsVersions = ImmutableSet.<String>builder();
|
||||
for (TLSVersion tlsVersion : TLSVersion.values()) {
|
||||
int versionNumber;
|
||||
try {
|
||||
versionNumber = tlsVersion.getNumber();
|
||||
} catch (IllegalArgumentException e) {
|
||||
continue;
|
||||
}
|
||||
if (versionNumber >= minTlsVersion.getNumber()
|
||||
&& versionNumber <= maxTlsVersion.getNumber()) {
|
||||
tlsVersions.add(convertTlsProtocolVersion(tlsVersion));
|
||||
}
|
||||
}
|
||||
return tlsVersions.build();
|
||||
}
|
||||
|
||||
private ProtoUtil() {}
|
||||
}
|
|
@ -0,0 +1,25 @@
|
|||
/*
|
||||
* Copyright 2024 The gRPC Authors
|
||||
*
|
||||
* 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 io.grpc.s2a.handshaker;
|
||||
|
||||
/** Exception that denotes a runtime error that was encountered when talking to the S2A server. */
|
||||
@SuppressWarnings("serial") // This class is never serialized.
|
||||
public class S2AConnectionException extends RuntimeException {
|
||||
S2AConnectionException(String message) {
|
||||
super(message);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,62 @@
|
|||
/*
|
||||
* Copyright 2024 The gRPC Authors
|
||||
*
|
||||
* 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 io.grpc.s2a.handshaker;
|
||||
|
||||
import static com.google.common.base.Preconditions.checkNotNull;
|
||||
|
||||
import com.google.errorprone.annotations.ThreadSafe;
|
||||
|
||||
/**
|
||||
* Stores an identity in such a way that it can be sent to the S2A handshaker service. The identity
|
||||
* may be formatted as a SPIFFE ID or as a hostname.
|
||||
*/
|
||||
@ThreadSafe
|
||||
public final class S2AIdentity {
|
||||
private final Identity identity;
|
||||
|
||||
/** Returns an {@link S2AIdentity} instance with SPIFFE ID set to {@code spiffeId}. */
|
||||
public static S2AIdentity fromSpiffeId(String spiffeId) {
|
||||
checkNotNull(spiffeId);
|
||||
return new S2AIdentity(Identity.newBuilder().setSpiffeId(spiffeId).build());
|
||||
}
|
||||
|
||||
/** Returns an {@link S2AIdentity} instance with hostname set to {@code hostname}. */
|
||||
public static S2AIdentity fromHostname(String hostname) {
|
||||
checkNotNull(hostname);
|
||||
return new S2AIdentity(Identity.newBuilder().setHostname(hostname).build());
|
||||
}
|
||||
|
||||
/** Returns an {@link S2AIdentity} instance with UID set to {@code uid}. */
|
||||
public static S2AIdentity fromUid(String uid) {
|
||||
checkNotNull(uid);
|
||||
return new S2AIdentity(Identity.newBuilder().setUid(uid).build());
|
||||
}
|
||||
|
||||
/** Returns an {@link S2AIdentity} instance with {@code identity} set. */
|
||||
public static S2AIdentity fromIdentity(Identity identity) {
|
||||
return new S2AIdentity(identity == null ? Identity.getDefaultInstance() : identity);
|
||||
}
|
||||
|
||||
private S2AIdentity(Identity identity) {
|
||||
this.identity = identity;
|
||||
}
|
||||
|
||||
/** Returns the proto {@link Identity} representation of this identity instance. */
|
||||
public Identity getIdentity() {
|
||||
return identity;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,143 @@
|
|||
/*
|
||||
* Copyright 2024 The gRPC Authors
|
||||
*
|
||||
* 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 io.grpc.s2a.handshaker;
|
||||
|
||||
import static com.google.common.base.Preconditions.checkArgument;
|
||||
import static com.google.common.base.Preconditions.checkNotNull;
|
||||
|
||||
import com.google.common.annotations.VisibleForTesting;
|
||||
import com.google.common.collect.ImmutableMap;
|
||||
import com.google.protobuf.ByteString;
|
||||
import io.grpc.s2a.handshaker.S2AIdentity;
|
||||
import io.netty.handler.ssl.OpenSslPrivateKeyMethod;
|
||||
import java.io.IOException;
|
||||
import java.util.Optional;
|
||||
import javax.annotation.concurrent.NotThreadSafe;
|
||||
import javax.net.ssl.SSLEngine;
|
||||
|
||||
/**
|
||||
* Handles requests on signing bytes with a private key designated by {@code stub}.
|
||||
*
|
||||
* <p>This is done by sending the to-be-signed bytes to an S2A server (designated by {@code stub})
|
||||
* and read the signature from the server.
|
||||
*
|
||||
* <p>OpenSSL libraries must be appropriately initialized before using this class. One possible way
|
||||
* to initialize OpenSSL library is to call {@code
|
||||
* GrpcSslContexts.configure(SslContextBuilder.forClient());}.
|
||||
*/
|
||||
@NotThreadSafe
|
||||
final class S2APrivateKeyMethod implements OpenSslPrivateKeyMethod {
|
||||
private final S2AStub stub;
|
||||
private final Optional<S2AIdentity> localIdentity;
|
||||
private static final ImmutableMap<Integer, SignatureAlgorithm>
|
||||
OPENSSL_TO_S2A_SIGNATURE_ALGORITHM_MAP =
|
||||
ImmutableMap.of(
|
||||
OpenSslPrivateKeyMethod.SSL_SIGN_RSA_PKCS1_SHA256,
|
||||
SignatureAlgorithm.S2A_SSL_SIGN_RSA_PKCS1_SHA256,
|
||||
OpenSslPrivateKeyMethod.SSL_SIGN_RSA_PKCS1_SHA384,
|
||||
SignatureAlgorithm.S2A_SSL_SIGN_RSA_PKCS1_SHA384,
|
||||
OpenSslPrivateKeyMethod.SSL_SIGN_RSA_PKCS1_SHA512,
|
||||
SignatureAlgorithm.S2A_SSL_SIGN_RSA_PKCS1_SHA512,
|
||||
OpenSslPrivateKeyMethod.SSL_SIGN_ECDSA_SECP256R1_SHA256,
|
||||
SignatureAlgorithm.S2A_SSL_SIGN_ECDSA_SECP256R1_SHA256,
|
||||
OpenSslPrivateKeyMethod.SSL_SIGN_ECDSA_SECP384R1_SHA384,
|
||||
SignatureAlgorithm.S2A_SSL_SIGN_ECDSA_SECP384R1_SHA384,
|
||||
OpenSslPrivateKeyMethod.SSL_SIGN_ECDSA_SECP521R1_SHA512,
|
||||
SignatureAlgorithm.S2A_SSL_SIGN_ECDSA_SECP521R1_SHA512,
|
||||
OpenSslPrivateKeyMethod.SSL_SIGN_RSA_PSS_RSAE_SHA256,
|
||||
SignatureAlgorithm.S2A_SSL_SIGN_RSA_PSS_RSAE_SHA256,
|
||||
OpenSslPrivateKeyMethod.SSL_SIGN_RSA_PSS_RSAE_SHA384,
|
||||
SignatureAlgorithm.S2A_SSL_SIGN_RSA_PSS_RSAE_SHA384,
|
||||
OpenSslPrivateKeyMethod.SSL_SIGN_RSA_PSS_RSAE_SHA512,
|
||||
SignatureAlgorithm.S2A_SSL_SIGN_RSA_PSS_RSAE_SHA512);
|
||||
|
||||
public static S2APrivateKeyMethod create(S2AStub stub, Optional<S2AIdentity> localIdentity) {
|
||||
checkNotNull(stub);
|
||||
return new S2APrivateKeyMethod(stub, localIdentity);
|
||||
}
|
||||
|
||||
private S2APrivateKeyMethod(S2AStub stub, Optional<S2AIdentity> localIdentity) {
|
||||
this.stub = stub;
|
||||
this.localIdentity = localIdentity;
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts the signature algorithm to an enum understood by S2A.
|
||||
*
|
||||
* @param signatureAlgorithm the int representation of the signature algorithm define by {@code
|
||||
* OpenSslPrivateKeyMethod}.
|
||||
* @return the signature algorithm enum defined by S2A proto.
|
||||
* @throws UnsupportedOperationException if the algorithm is not supported by S2A.
|
||||
*/
|
||||
@VisibleForTesting
|
||||
static SignatureAlgorithm convertOpenSslSignAlgToS2ASignAlg(int signatureAlgorithm) {
|
||||
SignatureAlgorithm sig = OPENSSL_TO_S2A_SIGNATURE_ALGORITHM_MAP.get(signatureAlgorithm);
|
||||
if (sig == null) {
|
||||
throw new UnsupportedOperationException(
|
||||
String.format("Signature Algorithm %d is not supported.", signatureAlgorithm));
|
||||
}
|
||||
return sig;
|
||||
}
|
||||
|
||||
/**
|
||||
* Signs the input bytes by sending the request to the S2A srever.
|
||||
*
|
||||
* @param engine not used.
|
||||
* @param signatureAlgorithm the {@link OpenSslPrivateKeyMethod}'s signature algorithm
|
||||
* representation
|
||||
* @param input the bytes to be signed.
|
||||
* @return the signature of the {@code input}.
|
||||
* @throws IOException if the connection to the S2A server is corrupted.
|
||||
* @throws InterruptedException if the connection to the S2A server is interrupted.
|
||||
* @throws S2AConnectionException if the response from the S2A server does not contain valid data.
|
||||
*/
|
||||
@Override
|
||||
public byte[] sign(SSLEngine engine, int signatureAlgorithm, byte[] input)
|
||||
throws IOException, InterruptedException {
|
||||
checkArgument(input.length > 0, "No bytes to sign.");
|
||||
SignatureAlgorithm s2aSignatureAlgorithm =
|
||||
convertOpenSslSignAlgToS2ASignAlg(signatureAlgorithm);
|
||||
SessionReq.Builder reqBuilder =
|
||||
SessionReq.newBuilder()
|
||||
.setOffloadPrivateKeyOperationReq(
|
||||
OffloadPrivateKeyOperationReq.newBuilder()
|
||||
.setOperation(OffloadPrivateKeyOperationReq.PrivateKeyOperation.SIGN)
|
||||
.setSignatureAlgorithm(s2aSignatureAlgorithm)
|
||||
.setRawBytes(ByteString.copyFrom(input)));
|
||||
if (localIdentity.isPresent()) {
|
||||
reqBuilder.setLocalIdentity(localIdentity.get().getIdentity());
|
||||
}
|
||||
|
||||
SessionResp resp = stub.send(reqBuilder.build());
|
||||
|
||||
if (resp.hasStatus() && resp.getStatus().getCode() != 0) {
|
||||
throw new S2AConnectionException(
|
||||
String.format(
|
||||
"Error occurred in response from S2A, error code: %d, error message: \"%s\".",
|
||||
resp.getStatus().getCode(), resp.getStatus().getDetails()));
|
||||
}
|
||||
if (!resp.hasOffloadPrivateKeyOperationResp()) {
|
||||
throw new S2AConnectionException("No valid response received from S2A.");
|
||||
}
|
||||
return resp.getOffloadPrivateKeyOperationResp().getOutBytes().toByteArray();
|
||||
}
|
||||
|
||||
@Override
|
||||
public byte[] decrypt(SSLEngine engine, byte[] input) {
|
||||
throw new UnsupportedOperationException("decrypt is not supported.");
|
||||
}
|
||||
}
|
|
@ -0,0 +1,249 @@
|
|||
/*
|
||||
* Copyright 2024 The gRPC Authors
|
||||
*
|
||||
* 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 io.grpc.s2a.handshaker;
|
||||
|
||||
import static com.google.common.base.Preconditions.checkArgument;
|
||||
import static com.google.common.base.Preconditions.checkNotNull;
|
||||
import static com.google.common.base.Strings.isNullOrEmpty;
|
||||
|
||||
import com.google.common.annotations.VisibleForTesting;
|
||||
import com.google.common.net.HostAndPort;
|
||||
import com.google.common.util.concurrent.FutureCallback;
|
||||
import com.google.common.util.concurrent.Futures;
|
||||
import com.google.common.util.concurrent.ListenableFuture;
|
||||
import com.google.common.util.concurrent.ListeningExecutorService;
|
||||
import com.google.common.util.concurrent.MoreExecutors;
|
||||
import com.google.errorprone.annotations.ThreadSafe;
|
||||
import io.grpc.Channel;
|
||||
import io.grpc.internal.ObjectPool;
|
||||
import io.grpc.netty.GrpcHttp2ConnectionHandler;
|
||||
import io.grpc.netty.InternalProtocolNegotiator;
|
||||
import io.grpc.netty.InternalProtocolNegotiator.ProtocolNegotiator;
|
||||
import io.grpc.netty.InternalProtocolNegotiators;
|
||||
import io.grpc.netty.InternalProtocolNegotiators.ProtocolNegotiationHandler;
|
||||
import io.grpc.s2a.channel.S2AChannelPool;
|
||||
import io.grpc.s2a.channel.S2AGrpcChannelPool;
|
||||
import io.grpc.s2a.handshaker.S2AIdentity;
|
||||
import io.netty.channel.ChannelHandler;
|
||||
import io.netty.channel.ChannelHandlerAdapter;
|
||||
import io.netty.channel.ChannelHandlerContext;
|
||||
import io.netty.channel.ChannelInboundHandlerAdapter;
|
||||
import io.netty.handler.ssl.SslContext;
|
||||
import io.netty.util.AsciiString;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
import java.util.concurrent.Executors;
|
||||
import javax.annotation.Nullable;
|
||||
|
||||
/** Factory for performing negotiation of a secure channel using the S2A. */
|
||||
@ThreadSafe
|
||||
public final class S2AProtocolNegotiatorFactory {
|
||||
@VisibleForTesting static final int DEFAULT_PORT = 443;
|
||||
private static final AsciiString SCHEME = AsciiString.of("https");
|
||||
|
||||
/**
|
||||
* Creates a {@code S2AProtocolNegotiatorFactory} configured for a client to establish secure
|
||||
* connections using the S2A.
|
||||
*
|
||||
* @param localIdentity the identity of the client; if none is provided, the S2A will use the
|
||||
* client's default identity.
|
||||
* @param s2aChannelPool a pool of shared channels that can be used to connect to the S2A.
|
||||
* @return a factory for creating a client-side protocol negotiator.
|
||||
*/
|
||||
public static InternalProtocolNegotiator.ClientFactory createClientFactory(
|
||||
@Nullable S2AIdentity localIdentity, ObjectPool<Channel> s2aChannelPool) {
|
||||
checkNotNull(s2aChannelPool, "S2A channel pool should not be null.");
|
||||
S2AChannelPool channelPool = S2AGrpcChannelPool.create(s2aChannelPool);
|
||||
return new S2AClientProtocolNegotiatorFactory(localIdentity, channelPool);
|
||||
}
|
||||
|
||||
static final class S2AClientProtocolNegotiatorFactory
|
||||
implements InternalProtocolNegotiator.ClientFactory {
|
||||
private final @Nullable S2AIdentity localIdentity;
|
||||
private final S2AChannelPool channelPool;
|
||||
|
||||
S2AClientProtocolNegotiatorFactory(
|
||||
@Nullable S2AIdentity localIdentity, S2AChannelPool channelPool) {
|
||||
this.localIdentity = localIdentity;
|
||||
this.channelPool = channelPool;
|
||||
}
|
||||
|
||||
@Override
|
||||
public ProtocolNegotiator newNegotiator() {
|
||||
return S2AProtocolNegotiator.createForClient(channelPool, localIdentity);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getDefaultPort() {
|
||||
return DEFAULT_PORT;
|
||||
}
|
||||
}
|
||||
|
||||
/** Negotiates the TLS handshake using S2A. */
|
||||
@VisibleForTesting
|
||||
static final class S2AProtocolNegotiator implements ProtocolNegotiator {
|
||||
|
||||
private final S2AChannelPool channelPool;
|
||||
private final Optional<S2AIdentity> localIdentity;
|
||||
private final ListeningExecutorService service =
|
||||
MoreExecutors.listeningDecorator(Executors.newFixedThreadPool(1));
|
||||
|
||||
static S2AProtocolNegotiator createForClient(
|
||||
S2AChannelPool channelPool, @Nullable S2AIdentity localIdentity) {
|
||||
checkNotNull(channelPool, "Channel pool should not be null.");
|
||||
if (localIdentity == null) {
|
||||
return new S2AProtocolNegotiator(channelPool, Optional.empty());
|
||||
} else {
|
||||
return new S2AProtocolNegotiator(channelPool, Optional.of(localIdentity));
|
||||
}
|
||||
}
|
||||
|
||||
@VisibleForTesting
|
||||
static @Nullable String getHostNameFromAuthority(@Nullable String authority) {
|
||||
if (authority == null) {
|
||||
return null;
|
||||
}
|
||||
return HostAndPort.fromString(authority).getHost();
|
||||
}
|
||||
|
||||
private S2AProtocolNegotiator(S2AChannelPool channelPool, Optional<S2AIdentity> localIdentity) {
|
||||
this.channelPool = channelPool;
|
||||
this.localIdentity = localIdentity;
|
||||
}
|
||||
|
||||
@Override
|
||||
public AsciiString scheme() {
|
||||
return SCHEME;
|
||||
}
|
||||
|
||||
@Override
|
||||
public ChannelHandler newHandler(GrpcHttp2ConnectionHandler grpcHandler) {
|
||||
checkNotNull(grpcHandler, "grpcHandler should not be null.");
|
||||
String hostname = getHostNameFromAuthority(grpcHandler.getAuthority());
|
||||
checkArgument(!isNullOrEmpty(hostname), "hostname should not be null or empty.");
|
||||
return new S2AProtocolNegotiationHandler(
|
||||
grpcHandler, channelPool, localIdentity, hostname, service);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void close() {
|
||||
service.shutdown();
|
||||
channelPool.close();
|
||||
}
|
||||
}
|
||||
|
||||
@VisibleForTesting
|
||||
static class BufferReadsHandler extends ChannelInboundHandlerAdapter {
|
||||
private final List<Object> reads = new ArrayList<>();
|
||||
private boolean readComplete;
|
||||
|
||||
public List<Object> getReads() {
|
||||
return reads;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void channelRead(ChannelHandlerContext unused, Object msg) {
|
||||
reads.add(msg);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void channelReadComplete(ChannelHandlerContext unused) {
|
||||
readComplete = true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void handlerRemoved(ChannelHandlerContext ctx) throws Exception {
|
||||
for (Object msg : reads) {
|
||||
super.channelRead(ctx, msg);
|
||||
}
|
||||
if (readComplete) {
|
||||
super.channelReadComplete(ctx);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static final class S2AProtocolNegotiationHandler extends ProtocolNegotiationHandler {
|
||||
private final S2AChannelPool channelPool;
|
||||
private final Optional<S2AIdentity> localIdentity;
|
||||
private final String hostname;
|
||||
private final GrpcHttp2ConnectionHandler grpcHandler;
|
||||
private final ListeningExecutorService service;
|
||||
|
||||
private S2AProtocolNegotiationHandler(
|
||||
GrpcHttp2ConnectionHandler grpcHandler,
|
||||
S2AChannelPool channelPool,
|
||||
Optional<S2AIdentity> localIdentity,
|
||||
String hostname,
|
||||
ListeningExecutorService service) {
|
||||
super(
|
||||
// superclass (InternalProtocolNegotiators.ProtocolNegotiationHandler) expects 'next'
|
||||
// handler but we don't have a next handler _yet_. So we "disable" superclass's behavior
|
||||
// here and then manually add 'next' when we call fireProtocolNegotiationEvent()
|
||||
new ChannelHandlerAdapter() {
|
||||
@Override
|
||||
public void handlerAdded(ChannelHandlerContext ctx) {
|
||||
ctx.pipeline().remove(this);
|
||||
}
|
||||
},
|
||||
grpcHandler.getNegotiationLogger());
|
||||
this.grpcHandler = grpcHandler;
|
||||
this.channelPool = channelPool;
|
||||
this.localIdentity = localIdentity;
|
||||
this.hostname = hostname;
|
||||
checkNotNull(service, "service should not be null.");
|
||||
this.service = service;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void handlerAdded0(ChannelHandlerContext ctx) {
|
||||
// Buffer all reads until the TLS Handler is added.
|
||||
BufferReadsHandler bufferReads = new BufferReadsHandler();
|
||||
ctx.pipeline().addBefore(ctx.name(), /* name= */ null, bufferReads);
|
||||
|
||||
Channel ch = channelPool.getChannel();
|
||||
S2AServiceGrpc.S2AServiceStub stub = S2AServiceGrpc.newStub(ch);
|
||||
S2AStub s2aStub = S2AStub.newInstance(stub);
|
||||
|
||||
ListenableFuture<SslContext> sslContextFuture =
|
||||
service.submit(() -> SslContextFactory.createForClient(s2aStub, hostname, localIdentity));
|
||||
Futures.addCallback(
|
||||
sslContextFuture,
|
||||
new FutureCallback<SslContext>() {
|
||||
@Override
|
||||
public void onSuccess(SslContext sslContext) {
|
||||
ChannelHandler handler =
|
||||
InternalProtocolNegotiators.tls(sslContext).newHandler(grpcHandler);
|
||||
|
||||
// Remove the bufferReads handler and delegate the rest of the handshake to the TLS
|
||||
// handler.
|
||||
ctx.pipeline().addAfter(ctx.name(), /* name= */ null, handler);
|
||||
fireProtocolNegotiationEvent(ctx);
|
||||
ctx.pipeline().remove(bufferReads);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onFailure(Throwable t) {
|
||||
ctx.fireExceptionCaught(t);
|
||||
}
|
||||
},
|
||||
service);
|
||||
}
|
||||
}
|
||||
|
||||
private S2AProtocolNegotiatorFactory() {}
|
||||
}
|
|
@ -0,0 +1,221 @@
|
|||
/*
|
||||
* Copyright 2024 The gRPC Authors
|
||||
*
|
||||
* 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 io.grpc.s2a.handshaker;
|
||||
|
||||
import static com.google.common.base.Preconditions.checkArgument;
|
||||
import static com.google.common.base.Preconditions.checkNotNull;
|
||||
import static com.google.common.base.Verify.verify;
|
||||
import static java.util.concurrent.TimeUnit.SECONDS;
|
||||
|
||||
import com.google.common.annotations.VisibleForTesting;
|
||||
import io.grpc.stub.StreamObserver;
|
||||
import java.io.IOException;
|
||||
import java.util.Optional;
|
||||
import java.util.concurrent.ArrayBlockingQueue;
|
||||
import java.util.concurrent.BlockingQueue;
|
||||
import java.util.logging.Level;
|
||||
import java.util.logging.Logger;
|
||||
import javax.annotation.concurrent.NotThreadSafe;
|
||||
|
||||
/** Reads and writes messages to and from the S2A. */
|
||||
@NotThreadSafe
|
||||
class S2AStub implements AutoCloseable {
|
||||
private static final Logger logger = Logger.getLogger(S2AStub.class.getName());
|
||||
private static final long HANDSHAKE_RPC_DEADLINE_SECS = 20;
|
||||
private final StreamObserver<SessionResp> reader = new Reader();
|
||||
private final BlockingQueue<Result> responses = new ArrayBlockingQueue<>(10);
|
||||
private S2AServiceGrpc.S2AServiceStub serviceStub;
|
||||
private StreamObserver<SessionReq> writer;
|
||||
private boolean doneReading = false;
|
||||
private boolean doneWriting = false;
|
||||
|
||||
static S2AStub newInstance(S2AServiceGrpc.S2AServiceStub serviceStub) {
|
||||
checkNotNull(serviceStub);
|
||||
return new S2AStub(serviceStub);
|
||||
}
|
||||
|
||||
@VisibleForTesting
|
||||
static S2AStub newInstanceForTesting(StreamObserver<SessionReq> writer) {
|
||||
checkNotNull(writer);
|
||||
return new S2AStub(writer);
|
||||
}
|
||||
|
||||
private S2AStub(S2AServiceGrpc.S2AServiceStub serviceStub) {
|
||||
this.serviceStub = serviceStub;
|
||||
}
|
||||
|
||||
private S2AStub(StreamObserver<SessionReq> writer) {
|
||||
this.writer = writer;
|
||||
}
|
||||
|
||||
@VisibleForTesting
|
||||
StreamObserver<SessionResp> getReader() {
|
||||
return reader;
|
||||
}
|
||||
|
||||
@VisibleForTesting
|
||||
BlockingQueue<Result> getResponses() {
|
||||
return responses;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sends a request and returns the response. Caller must wait until this method executes prior to
|
||||
* calling it again. If this method throws {@code ConnectionClosedException}, then it should not
|
||||
* be called again, and both {@code reader} and {@code writer} are closed.
|
||||
*
|
||||
* @param req the {@code SessionReq} message to be sent to the S2A server.
|
||||
* @return the {@code SessionResp} message received from the S2A server.
|
||||
* @throws ConnectionClosedException if {@code reader} or {@code writer} calls their {@code
|
||||
* onCompleted} method.
|
||||
* @throws IOException if an unexpected response is received, or if the {@code reader} or {@code
|
||||
* writer} calls their {@code onError} method.
|
||||
*/
|
||||
public SessionResp send(SessionReq req) throws IOException, InterruptedException {
|
||||
if (doneWriting && doneReading) {
|
||||
logger.log(Level.INFO, "Stream to the S2A is closed.");
|
||||
throw new ConnectionClosedException("Stream to the S2A is closed.");
|
||||
}
|
||||
createWriterIfNull();
|
||||
if (!responses.isEmpty()) {
|
||||
IOException exception = null;
|
||||
SessionResp resp = null;
|
||||
try {
|
||||
resp = responses.take().getResultOrThrow();
|
||||
} catch (IOException e) {
|
||||
exception = e;
|
||||
}
|
||||
responses.clear();
|
||||
if (exception != null) {
|
||||
throw new IOException(
|
||||
"Received an unexpected response from a host at the S2A's address. The S2A might be"
|
||||
+ " unavailable."
|
||||
+ exception.getMessage());
|
||||
}
|
||||
return resp;
|
||||
}
|
||||
try {
|
||||
writer.onNext(req);
|
||||
} catch (RuntimeException e) {
|
||||
writer.onError(e);
|
||||
responses.offer(Result.createWithThrowable(e));
|
||||
}
|
||||
try {
|
||||
return responses.take().getResultOrThrow();
|
||||
} catch (ConnectionClosedException e) {
|
||||
// A ConnectionClosedException is thrown by getResultOrThrow when reader calls its
|
||||
// onCompleted method. The close method is called to also close the writer, and then the
|
||||
// ConnectionClosedException is re-thrown in order to indicate to the caller that send
|
||||
// should not be called again.
|
||||
close();
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void close() {
|
||||
if (doneWriting && doneReading) {
|
||||
return;
|
||||
}
|
||||
verify(!doneWriting);
|
||||
doneReading = true;
|
||||
doneWriting = true;
|
||||
if (writer != null) {
|
||||
writer.onCompleted();
|
||||
}
|
||||
}
|
||||
|
||||
/** Create a new writer if the writer is null. */
|
||||
private void createWriterIfNull() {
|
||||
if (writer == null) {
|
||||
writer =
|
||||
serviceStub
|
||||
.withWaitForReady()
|
||||
.withDeadlineAfter(HANDSHAKE_RPC_DEADLINE_SECS, SECONDS)
|
||||
.setUpSession(reader);
|
||||
}
|
||||
}
|
||||
|
||||
private class Reader implements StreamObserver<SessionResp> {
|
||||
/**
|
||||
* Places a {@code SessionResp} message in the {@code responses} queue, or an {@code
|
||||
* IOException} if reading is complete.
|
||||
*
|
||||
* @param resp the {@code SessionResp} message received from the S2A handshaker module.
|
||||
*/
|
||||
@Override
|
||||
public void onNext(SessionResp resp) {
|
||||
verify(!doneReading);
|
||||
responses.offer(Result.createWithResponse(resp));
|
||||
}
|
||||
|
||||
/**
|
||||
* Places a {@code Throwable} in the {@code responses} queue.
|
||||
*
|
||||
* @param t the {@code Throwable} caught when reading the stream to the S2A handshaker module.
|
||||
*/
|
||||
@Override
|
||||
public void onError(Throwable t) {
|
||||
responses.offer(Result.createWithThrowable(t));
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets {@code doneReading} to true, and places a {@code ConnectionClosedException} in the
|
||||
* {@code responses} queue.
|
||||
*/
|
||||
@Override
|
||||
public void onCompleted() {
|
||||
logger.log(Level.INFO, "Reading from the S2A is complete.");
|
||||
doneReading = true;
|
||||
responses.offer(
|
||||
Result.createWithThrowable(
|
||||
new ConnectionClosedException("Reading from the S2A is complete.")));
|
||||
}
|
||||
}
|
||||
|
||||
private static final class Result {
|
||||
private final Optional<SessionResp> response;
|
||||
private final Optional<Throwable> throwable;
|
||||
|
||||
static Result createWithResponse(SessionResp response) {
|
||||
return new Result(Optional.of(response), Optional.empty());
|
||||
}
|
||||
|
||||
static Result createWithThrowable(Throwable throwable) {
|
||||
return new Result(Optional.empty(), Optional.of(throwable));
|
||||
}
|
||||
|
||||
private Result(Optional<SessionResp> response, Optional<Throwable> throwable) {
|
||||
checkArgument(response.isPresent() != throwable.isPresent());
|
||||
this.response = response;
|
||||
this.throwable = throwable;
|
||||
}
|
||||
|
||||
/** Throws {@code throwable} if present, and returns {@code response} otherwise. */
|
||||
SessionResp getResultOrThrow() throws IOException {
|
||||
if (throwable.isPresent()) {
|
||||
if (throwable.get() instanceof ConnectionClosedException) {
|
||||
ConnectionClosedException exception = (ConnectionClosedException) throwable.get();
|
||||
throw exception;
|
||||
} else {
|
||||
throw new IOException(throwable.get());
|
||||
}
|
||||
}
|
||||
verify(response.isPresent());
|
||||
return response.get();
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,152 @@
|
|||
/*
|
||||
* Copyright 2024 The gRPC Authors
|
||||
*
|
||||
* 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 io.grpc.s2a.handshaker;
|
||||
|
||||
import static com.google.common.base.Preconditions.checkArgument;
|
||||
import static com.google.common.base.Preconditions.checkNotNull;
|
||||
|
||||
import com.google.common.collect.ImmutableList;
|
||||
import com.google.protobuf.ByteString;
|
||||
import io.grpc.s2a.handshaker.S2AIdentity;
|
||||
import io.grpc.s2a.handshaker.ValidatePeerCertificateChainReq.VerificationMode;
|
||||
import java.io.IOException;
|
||||
import java.security.cert.CertificateEncodingException;
|
||||
import java.security.cert.CertificateException;
|
||||
import java.security.cert.X509Certificate;
|
||||
import java.util.Optional;
|
||||
import javax.annotation.concurrent.NotThreadSafe;
|
||||
import javax.net.ssl.X509TrustManager;
|
||||
import org.checkerframework.checker.nullness.qual.Nullable;
|
||||
|
||||
/** Offloads verification of the peer certificate chain to S2A. */
|
||||
@NotThreadSafe
|
||||
final class S2ATrustManager implements X509TrustManager {
|
||||
private final Optional<S2AIdentity> localIdentity;
|
||||
private final S2AStub stub;
|
||||
private final String hostname;
|
||||
|
||||
static S2ATrustManager createForClient(
|
||||
S2AStub stub, String hostname, Optional<S2AIdentity> localIdentity) {
|
||||
checkNotNull(stub);
|
||||
checkNotNull(hostname);
|
||||
return new S2ATrustManager(stub, hostname, localIdentity);
|
||||
}
|
||||
|
||||
private S2ATrustManager(S2AStub stub, String hostname, Optional<S2AIdentity> localIdentity) {
|
||||
this.stub = stub;
|
||||
this.hostname = hostname;
|
||||
this.localIdentity = localIdentity;
|
||||
}
|
||||
|
||||
/**
|
||||
* Validates the given certificate chain provided by the peer.
|
||||
*
|
||||
* @param chain the peer certificate chain
|
||||
* @param authType the authentication type based on the client certificate
|
||||
* @throws IllegalArgumentException if null or zero-length chain is passed in for the chain
|
||||
* parameter.
|
||||
* @throws CertificateException if the certificate chain is not trusted by this TrustManager.
|
||||
*/
|
||||
@Override
|
||||
public void checkClientTrusted(X509Certificate[] chain, String authType)
|
||||
throws CertificateException {
|
||||
checkPeerTrusted(chain, /* isCheckingClientCertificateChain= */ true);
|
||||
}
|
||||
|
||||
/**
|
||||
* Validates the given certificate chain provided by the peer.
|
||||
*
|
||||
* @param chain the peer certificate chain
|
||||
* @param authType the authentication type based on the client certificate
|
||||
* @throws IllegalArgumentException if null or zero-length chain is passed in for the chain
|
||||
* parameter.
|
||||
* @throws CertificateException if the certificate chain is not trusted by this TrustManager.
|
||||
*/
|
||||
@Override
|
||||
public void checkServerTrusted(X509Certificate[] chain, String authType)
|
||||
throws CertificateException {
|
||||
checkPeerTrusted(chain, /* isCheckingClientCertificateChain= */ false);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns null because the accepted issuers are held in S2A and this class receives decision made
|
||||
* from S2A on the fly about which to use to verify a given chain.
|
||||
*
|
||||
* @return null.
|
||||
*/
|
||||
@Override
|
||||
public X509Certificate @Nullable [] getAcceptedIssuers() {
|
||||
return null;
|
||||
}
|
||||
|
||||
private void checkPeerTrusted(X509Certificate[] chain, boolean isCheckingClientCertificateChain)
|
||||
throws CertificateException {
|
||||
checkNotNull(chain);
|
||||
checkArgument(chain.length > 0, "Certificate chain has zero certificates.");
|
||||
|
||||
ValidatePeerCertificateChainReq.Builder validatePeerCertificateChainReq =
|
||||
ValidatePeerCertificateChainReq.newBuilder().setMode(VerificationMode.UNSPECIFIED);
|
||||
if (isCheckingClientCertificateChain) {
|
||||
validatePeerCertificateChainReq.setClientPeer(
|
||||
ValidatePeerCertificateChainReq.ClientPeer.newBuilder()
|
||||
.addAllCertificateChain(certificateChainToDerChain(chain)));
|
||||
} else {
|
||||
validatePeerCertificateChainReq.setServerPeer(
|
||||
ValidatePeerCertificateChainReq.ServerPeer.newBuilder()
|
||||
.addAllCertificateChain(certificateChainToDerChain(chain))
|
||||
.setServerHostname(hostname));
|
||||
}
|
||||
|
||||
SessionReq.Builder reqBuilder =
|
||||
SessionReq.newBuilder().setValidatePeerCertificateChainReq(validatePeerCertificateChainReq);
|
||||
if (localIdentity.isPresent()) {
|
||||
reqBuilder.setLocalIdentity(localIdentity.get().getIdentity());
|
||||
}
|
||||
|
||||
SessionResp resp;
|
||||
try {
|
||||
resp = stub.send(reqBuilder.build());
|
||||
} catch (IOException | InterruptedException e) {
|
||||
throw new CertificateException("Failed to send request to S2A.", e);
|
||||
}
|
||||
if (resp.hasStatus() && resp.getStatus().getCode() != 0) {
|
||||
throw new CertificateException(
|
||||
String.format(
|
||||
"Error occurred in response from S2A, error code: %d, error message: %s.",
|
||||
resp.getStatus().getCode(), resp.getStatus().getDetails()));
|
||||
}
|
||||
|
||||
if (!resp.hasValidatePeerCertificateChainResp()) {
|
||||
throw new CertificateException("No valid response received from S2A.");
|
||||
}
|
||||
|
||||
ValidatePeerCertificateChainResp validationResult = resp.getValidatePeerCertificateChainResp();
|
||||
if (validationResult.getValidationResult()
|
||||
!= ValidatePeerCertificateChainResp.ValidationResult.SUCCESS) {
|
||||
throw new CertificateException(validationResult.getValidationDetails());
|
||||
}
|
||||
}
|
||||
|
||||
private static ImmutableList<ByteString> certificateChainToDerChain(X509Certificate[] chain)
|
||||
throws CertificateEncodingException {
|
||||
ImmutableList.Builder<ByteString> derChain = ImmutableList.<ByteString>builder();
|
||||
for (X509Certificate certificate : chain) {
|
||||
derChain.add(ByteString.copyFrom(certificate.getEncoded()));
|
||||
}
|
||||
return derChain.build();
|
||||
}
|
||||
}
|
|
@ -0,0 +1,178 @@
|
|||
/*
|
||||
* Copyright 2024 The gRPC Authors
|
||||
*
|
||||
* 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 io.grpc.s2a.handshaker;
|
||||
|
||||
import static com.google.common.base.Preconditions.checkNotNull;
|
||||
import static java.nio.charset.StandardCharsets.UTF_8;
|
||||
|
||||
import com.google.common.collect.ImmutableSet;
|
||||
import io.grpc.netty.GrpcSslContexts;
|
||||
import io.grpc.s2a.handshaker.S2AIdentity;
|
||||
import io.netty.handler.ssl.OpenSslContextOption;
|
||||
import io.netty.handler.ssl.OpenSslSessionContext;
|
||||
import io.netty.handler.ssl.OpenSslX509KeyManagerFactory;
|
||||
import io.netty.handler.ssl.SslContext;
|
||||
import io.netty.handler.ssl.SslContextBuilder;
|
||||
import java.io.ByteArrayInputStream;
|
||||
import java.io.IOException;
|
||||
import java.security.GeneralSecurityException;
|
||||
import java.security.KeyStoreException;
|
||||
import java.security.NoSuchAlgorithmException;
|
||||
import java.security.UnrecoverableKeyException;
|
||||
import java.security.cert.CertificateException;
|
||||
import java.security.cert.CertificateFactory;
|
||||
import java.security.cert.X509Certificate;
|
||||
import java.util.Optional;
|
||||
import javax.net.ssl.KeyManager;
|
||||
import javax.net.ssl.SSLSessionContext;
|
||||
|
||||
/** Creates {@link SslContext} objects with TLS configurations from S2A server. */
|
||||
final class SslContextFactory {
|
||||
|
||||
/**
|
||||
* Creates {@link SslContext} objects for client with TLS configurations from S2A server.
|
||||
*
|
||||
* @param stub the {@link S2AStub} to talk to the S2A server.
|
||||
* @param targetName the {@link String} of the server that this client makes connection to.
|
||||
* @param localIdentity the {@link S2AIdentity} that should be used when talking to S2A server.
|
||||
* Will use default identity if empty.
|
||||
* @return a {@link SslContext} object.
|
||||
* @throws NullPointerException if either {@code stub} or {@code targetName} is null.
|
||||
* @throws IOException if an unexpected response from S2A server is received.
|
||||
* @throws InterruptedException if {@code stub} is closed.
|
||||
*/
|
||||
static SslContext createForClient(
|
||||
S2AStub stub, String targetName, Optional<S2AIdentity> localIdentity)
|
||||
throws IOException,
|
||||
InterruptedException,
|
||||
CertificateException,
|
||||
KeyStoreException,
|
||||
NoSuchAlgorithmException,
|
||||
UnrecoverableKeyException,
|
||||
GeneralSecurityException {
|
||||
checkNotNull(stub, "stub should not be null.");
|
||||
checkNotNull(targetName, "targetName should not be null on client side.");
|
||||
GetTlsConfigurationResp.ClientTlsConfiguration clientTlsConfiguration;
|
||||
try {
|
||||
clientTlsConfiguration = getClientTlsConfigurationFromS2A(stub, localIdentity);
|
||||
} catch (IOException | InterruptedException e) {
|
||||
throw new GeneralSecurityException("Failed to get client TLS configuration from S2A.", e);
|
||||
}
|
||||
|
||||
// Use the default value for timeout.
|
||||
// Use the smallest possible value for cache size.
|
||||
// The Provider is by default OPENSSL. No need to manually set it.
|
||||
SslContextBuilder sslContextBuilder =
|
||||
GrpcSslContexts.configure(SslContextBuilder.forClient())
|
||||
.sessionCacheSize(1)
|
||||
.sessionTimeout(0);
|
||||
|
||||
configureSslContextWithClientTlsConfiguration(clientTlsConfiguration, sslContextBuilder);
|
||||
sslContextBuilder.trustManager(
|
||||
S2ATrustManager.createForClient(stub, targetName, localIdentity));
|
||||
sslContextBuilder.option(
|
||||
OpenSslContextOption.PRIVATE_KEY_METHOD, S2APrivateKeyMethod.create(stub, localIdentity));
|
||||
|
||||
SslContext sslContext = sslContextBuilder.build();
|
||||
SSLSessionContext sslSessionContext = sslContext.sessionContext();
|
||||
if (sslSessionContext instanceof OpenSslSessionContext) {
|
||||
OpenSslSessionContext openSslSessionContext = (OpenSslSessionContext) sslSessionContext;
|
||||
openSslSessionContext.setSessionCacheEnabled(false);
|
||||
}
|
||||
|
||||
return sslContext;
|
||||
}
|
||||
|
||||
private static GetTlsConfigurationResp.ClientTlsConfiguration getClientTlsConfigurationFromS2A(
|
||||
S2AStub stub, Optional<S2AIdentity> localIdentity) throws IOException, InterruptedException {
|
||||
checkNotNull(stub, "stub should not be null.");
|
||||
SessionReq.Builder reqBuilder = SessionReq.newBuilder();
|
||||
if (localIdentity.isPresent()) {
|
||||
reqBuilder.setLocalIdentity(localIdentity.get().getIdentity());
|
||||
}
|
||||
Optional<AuthenticationMechanism> authMechanism =
|
||||
GetAuthenticationMechanisms.getAuthMechanism(localIdentity);
|
||||
if (authMechanism.isPresent()) {
|
||||
reqBuilder.addAuthenticationMechanisms(authMechanism.get());
|
||||
}
|
||||
SessionResp resp =
|
||||
stub.send(
|
||||
reqBuilder
|
||||
.setGetTlsConfigurationReq(
|
||||
GetTlsConfigurationReq.newBuilder()
|
||||
.setConnectionSide(ConnectionSide.CONNECTION_SIDE_CLIENT))
|
||||
.build());
|
||||
if (resp.hasStatus() && resp.getStatus().getCode() != 0) {
|
||||
throw new S2AConnectionException(
|
||||
String.format(
|
||||
"response from S2A server has ean error %d with error message %s.",
|
||||
resp.getStatus().getCode(), resp.getStatus().getDetails()));
|
||||
}
|
||||
if (!resp.getGetTlsConfigurationResp().hasClientTlsConfiguration()) {
|
||||
throw new S2AConnectionException(
|
||||
"Response from S2A server does NOT contain ClientTlsConfiguration.");
|
||||
}
|
||||
return resp.getGetTlsConfigurationResp().getClientTlsConfiguration();
|
||||
}
|
||||
|
||||
private static void configureSslContextWithClientTlsConfiguration(
|
||||
GetTlsConfigurationResp.ClientTlsConfiguration clientTlsConfiguration,
|
||||
SslContextBuilder sslContextBuilder)
|
||||
throws CertificateException,
|
||||
IOException,
|
||||
KeyStoreException,
|
||||
NoSuchAlgorithmException,
|
||||
UnrecoverableKeyException {
|
||||
sslContextBuilder.keyManager(createKeylessManager(clientTlsConfiguration));
|
||||
ImmutableSet<String> tlsVersions =
|
||||
ProtoUtil.buildTlsProtocolVersionSet(
|
||||
clientTlsConfiguration.getMinTlsVersion(), clientTlsConfiguration.getMaxTlsVersion());
|
||||
if (tlsVersions.isEmpty()) {
|
||||
throw new S2AConnectionException("Set of TLS versions received from S2A server is empty.");
|
||||
}
|
||||
sslContextBuilder.protocols(tlsVersions);
|
||||
}
|
||||
|
||||
private static KeyManager createKeylessManager(
|
||||
GetTlsConfigurationResp.ClientTlsConfiguration clientTlsConfiguration)
|
||||
throws CertificateException,
|
||||
IOException,
|
||||
KeyStoreException,
|
||||
NoSuchAlgorithmException,
|
||||
UnrecoverableKeyException {
|
||||
X509Certificate[] certificates =
|
||||
new X509Certificate[clientTlsConfiguration.getCertificateChainCount()];
|
||||
for (int i = 0; i < clientTlsConfiguration.getCertificateChainCount(); ++i) {
|
||||
certificates[i] = convertStringToX509Cert(clientTlsConfiguration.getCertificateChain(i));
|
||||
}
|
||||
KeyManager[] keyManagers =
|
||||
OpenSslX509KeyManagerFactory.newKeyless(certificates).getKeyManagers();
|
||||
if (keyManagers == null || keyManagers.length == 0) {
|
||||
throw new IllegalStateException("No key managers created.");
|
||||
}
|
||||
return keyManagers[0];
|
||||
}
|
||||
|
||||
private static X509Certificate convertStringToX509Cert(String certificate)
|
||||
throws CertificateException {
|
||||
return (X509Certificate)
|
||||
CertificateFactory.getInstance("X509")
|
||||
.generateCertificate(new ByteArrayInputStream(certificate.getBytes(UTF_8)));
|
||||
}
|
||||
|
||||
private SslContextFactory() {}
|
||||
}
|
|
@ -0,0 +1,61 @@
|
|||
/*
|
||||
* Copyright 2024 The gRPC Authors
|
||||
*
|
||||
* 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 io.grpc.s2a.handshaker.tokenmanager;
|
||||
|
||||
import io.grpc.s2a.handshaker.S2AIdentity;
|
||||
import java.lang.reflect.Method;
|
||||
import java.util.Optional;
|
||||
import javax.annotation.concurrent.ThreadSafe;
|
||||
|
||||
/** Manages access tokens for authenticating to the S2A. */
|
||||
@ThreadSafe
|
||||
public final class AccessTokenManager {
|
||||
private final TokenFetcher tokenFetcher;
|
||||
|
||||
/** Creates an {@code AccessTokenManager} based on the environment where the application runs. */
|
||||
@SuppressWarnings("RethrowReflectiveOperationExceptionAsLinkageError")
|
||||
public static Optional<AccessTokenManager> create() {
|
||||
Optional<?> tokenFetcher;
|
||||
try {
|
||||
Class<?> singleTokenFetcherClass =
|
||||
Class.forName("io.grpc.s2a.handshaker.tokenmanager.SingleTokenFetcher");
|
||||
Method createTokenFetcher = singleTokenFetcherClass.getMethod("create");
|
||||
tokenFetcher = (Optional) createTokenFetcher.invoke(null);
|
||||
} catch (ClassNotFoundException e) {
|
||||
tokenFetcher = Optional.empty();
|
||||
} catch (ReflectiveOperationException e) {
|
||||
throw new AssertionError(e);
|
||||
}
|
||||
return tokenFetcher.isPresent()
|
||||
? Optional.of(new AccessTokenManager((TokenFetcher) tokenFetcher.get()))
|
||||
: Optional.empty();
|
||||
}
|
||||
|
||||
private AccessTokenManager(TokenFetcher tokenFetcher) {
|
||||
this.tokenFetcher = tokenFetcher;
|
||||
}
|
||||
|
||||
/** Returns an access token when no identity is specified. */
|
||||
public String getDefaultToken() {
|
||||
return tokenFetcher.getDefaultToken();
|
||||
}
|
||||
|
||||
/** Returns an access token for the given identity. */
|
||||
public String getToken(S2AIdentity identity) {
|
||||
return tokenFetcher.getToken(identity);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,57 @@
|
|||
/*
|
||||
* Copyright 2024 The gRPC Authors
|
||||
*
|
||||
* 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 io.grpc.s2a.handshaker.tokenmanager;
|
||||
|
||||
import com.google.common.annotations.VisibleForTesting;
|
||||
import io.grpc.s2a.handshaker.S2AIdentity;
|
||||
import java.util.Optional;
|
||||
|
||||
/** Fetches a single access token via an environment variable. */
|
||||
@SuppressWarnings("NonFinalStaticField")
|
||||
public final class SingleTokenFetcher implements TokenFetcher {
|
||||
private static final String ENVIRONMENT_VARIABLE = "S2A_ACCESS_TOKEN";
|
||||
private static String accessToken = System.getenv(ENVIRONMENT_VARIABLE);
|
||||
|
||||
private final String token;
|
||||
|
||||
/**
|
||||
* Creates a {@code SingleTokenFetcher} from {@code ENVIRONMENT_VARIABLE}, and returns an empty
|
||||
* {@code Optional} instance if the token could not be fetched.
|
||||
*/
|
||||
public static Optional<TokenFetcher> create() {
|
||||
return Optional.ofNullable(accessToken).map(SingleTokenFetcher::new);
|
||||
}
|
||||
|
||||
@VisibleForTesting
|
||||
public static void setAccessToken(String token) {
|
||||
accessToken = token;
|
||||
}
|
||||
|
||||
private SingleTokenFetcher(String token) {
|
||||
this.token = token;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getDefaultToken() {
|
||||
return token;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getToken(S2AIdentity identity) {
|
||||
return token;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,28 @@
|
|||
/*
|
||||
* Copyright 2024 The gRPC Authors
|
||||
*
|
||||
* 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 io.grpc.s2a.handshaker.tokenmanager;
|
||||
|
||||
import io.grpc.s2a.handshaker.S2AIdentity;
|
||||
|
||||
/** Fetches tokens used to authenticate to S2A. */
|
||||
interface TokenFetcher {
|
||||
/** Returns an access token when no identity is specified. */
|
||||
String getDefaultToken();
|
||||
|
||||
/** Returns an access token for the given identity. */
|
||||
String getToken(S2AIdentity identity);
|
||||
}
|
|
@ -0,0 +1,82 @@
|
|||
// Copyright 2024 The gRPC Authors
|
||||
//
|
||||
// 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.
|
||||
|
||||
// The canonical version of this proto can be found at
|
||||
// https://github.com/grpc/grpc-proto/blob/master/grpc/gcp/s2a/common.proto
|
||||
|
||||
syntax = "proto3";
|
||||
|
||||
package grpc.gcp.s2a;
|
||||
|
||||
option java_multiple_files = true;
|
||||
option java_outer_classname = "CommonProto";
|
||||
option java_package = "io.grpc.s2a.handshaker";
|
||||
|
||||
// The TLS 1.0-1.2 ciphersuites that the application can negotiate when using
|
||||
// S2A.
|
||||
enum Ciphersuite {
|
||||
CIPHERSUITE_UNSPECIFIED = 0;
|
||||
CIPHERSUITE_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256 = 1;
|
||||
CIPHERSUITE_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384 = 2;
|
||||
CIPHERSUITE_ECDHE_ECDSA_WITH_CHACHA20_POLY1305_SHA256 = 3;
|
||||
CIPHERSUITE_ECDHE_RSA_WITH_AES_128_GCM_SHA256 = 4;
|
||||
CIPHERSUITE_ECDHE_RSA_WITH_AES_256_GCM_SHA384 = 5;
|
||||
CIPHERSUITE_ECDHE_RSA_WITH_CHACHA20_POLY1305_SHA256 = 6;
|
||||
}
|
||||
|
||||
// The TLS versions supported by S2A's handshaker module.
|
||||
enum TLSVersion {
|
||||
TLS_VERSION_UNSPECIFIED = 0;
|
||||
TLS_VERSION_1_0 = 1;
|
||||
TLS_VERSION_1_1 = 2;
|
||||
TLS_VERSION_1_2 = 3;
|
||||
TLS_VERSION_1_3 = 4;
|
||||
}
|
||||
|
||||
// The side in the TLS connection.
|
||||
enum ConnectionSide {
|
||||
CONNECTION_SIDE_UNSPECIFIED = 0;
|
||||
CONNECTION_SIDE_CLIENT = 1;
|
||||
CONNECTION_SIDE_SERVER = 2;
|
||||
}
|
||||
|
||||
// The ALPN protocols that the application can negotiate during a TLS handshake.
|
||||
enum AlpnProtocol {
|
||||
ALPN_PROTOCOL_UNSPECIFIED = 0;
|
||||
ALPN_PROTOCOL_GRPC = 1;
|
||||
ALPN_PROTOCOL_HTTP2 = 2;
|
||||
ALPN_PROTOCOL_HTTP1_1 = 3;
|
||||
}
|
||||
|
||||
message Identity {
|
||||
oneof identity_oneof {
|
||||
// The SPIFFE ID of a connection endpoint.
|
||||
string spiffe_id = 1;
|
||||
|
||||
// The hostname of a connection endpoint.
|
||||
string hostname = 2;
|
||||
|
||||
// The UID of a connection endpoint.
|
||||
string uid = 4;
|
||||
|
||||
// The username of a connection endpoint.
|
||||
string username = 5;
|
||||
|
||||
// The GCP ID of a connection endpoint.
|
||||
string gcp_id = 6;
|
||||
}
|
||||
|
||||
// Additional identity-specific attributes.
|
||||
map<string, string> attributes = 3;
|
||||
}
|
|
@ -0,0 +1,369 @@
|
|||
// Copyright 2024 The gRPC Authors
|
||||
//
|
||||
// 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.
|
||||
|
||||
// The canonical version of this proto can be found at
|
||||
// https://github.com/grpc/grpc-proto/blob/master/grpc/gcp/s2a/s2a.proto
|
||||
|
||||
syntax = "proto3";
|
||||
|
||||
package grpc.gcp.s2a;
|
||||
|
||||
import "grpc/gcp/s2a/common.proto";
|
||||
import "grpc/gcp/s2a/s2a_context.proto";
|
||||
|
||||
option java_multiple_files = true;
|
||||
option java_outer_classname = "S2AProto";
|
||||
option java_package = "io.grpc.s2a.handshaker";
|
||||
|
||||
enum SignatureAlgorithm {
|
||||
S2A_SSL_SIGN_UNSPECIFIED = 0;
|
||||
// RSA Public-Key Cryptography Standards #1.
|
||||
S2A_SSL_SIGN_RSA_PKCS1_SHA256 = 1;
|
||||
S2A_SSL_SIGN_RSA_PKCS1_SHA384 = 2;
|
||||
S2A_SSL_SIGN_RSA_PKCS1_SHA512 = 3;
|
||||
// ECDSA.
|
||||
S2A_SSL_SIGN_ECDSA_SECP256R1_SHA256 = 4;
|
||||
S2A_SSL_SIGN_ECDSA_SECP384R1_SHA384 = 5;
|
||||
S2A_SSL_SIGN_ECDSA_SECP521R1_SHA512 = 6;
|
||||
// RSA Probabilistic Signature Scheme.
|
||||
S2A_SSL_SIGN_RSA_PSS_RSAE_SHA256 = 7;
|
||||
S2A_SSL_SIGN_RSA_PSS_RSAE_SHA384 = 8;
|
||||
S2A_SSL_SIGN_RSA_PSS_RSAE_SHA512 = 9;
|
||||
// ED25519.
|
||||
S2A_SSL_SIGN_ED25519 = 10;
|
||||
}
|
||||
|
||||
message AlpnPolicy {
|
||||
// If true, the application MUST perform ALPN negotiation.
|
||||
bool enable_alpn_negotiation = 1;
|
||||
|
||||
// The ordered list of ALPN protocols that specify how the application SHOULD
|
||||
// negotiate ALPN during the TLS handshake.
|
||||
//
|
||||
// The application MAY ignore any ALPN protocols in this list that are not
|
||||
// supported by the application.
|
||||
repeated AlpnProtocol alpn_protocols = 2;
|
||||
}
|
||||
|
||||
message AuthenticationMechanism {
|
||||
// Applications may specify an identity associated to an authentication
|
||||
// mechanism. Otherwise, S2A assumes that the authentication mechanism is
|
||||
// associated with the default identity. If the default identity cannot be
|
||||
// determined, the request is rejected.
|
||||
Identity identity = 1;
|
||||
|
||||
oneof mechanism_oneof {
|
||||
// A token that the application uses to authenticate itself to S2A.
|
||||
string token = 2;
|
||||
}
|
||||
}
|
||||
|
||||
message Status {
|
||||
// The status code that is specific to the application and the implementation
|
||||
// of S2A, e.g., gRPC status code.
|
||||
uint32 code = 1;
|
||||
|
||||
// The status details.
|
||||
string details = 2;
|
||||
}
|
||||
|
||||
message GetTlsConfigurationReq {
|
||||
// The role of the application in the TLS connection.
|
||||
ConnectionSide connection_side = 1;
|
||||
|
||||
// The server name indication (SNI) extension, which MAY be populated when a
|
||||
// server is offloading to S2A. The SNI is used to determine the server
|
||||
// identity if the local identity in the request is empty.
|
||||
string sni = 2;
|
||||
}
|
||||
|
||||
message GetTlsConfigurationResp {
|
||||
// Next ID: 8
|
||||
message ClientTlsConfiguration {
|
||||
reserved 4, 5;
|
||||
|
||||
// The certificate chain that the client MUST use for the TLS handshake.
|
||||
// It's a list of PEM-encoded certificates, ordered from leaf to root,
|
||||
// excluding the root.
|
||||
repeated string certificate_chain = 1;
|
||||
|
||||
// The minimum TLS version number that the client MUST use for the TLS
|
||||
// handshake. If this field is not provided, the client MUST use the default
|
||||
// minimum version of the client's TLS library.
|
||||
TLSVersion min_tls_version = 2;
|
||||
|
||||
// The maximum TLS version number that the client MUST use for the TLS
|
||||
// handshake. If this field is not provided, the client MUST use the default
|
||||
// maximum version of the client's TLS library.
|
||||
TLSVersion max_tls_version = 3;
|
||||
|
||||
// The ordered list of TLS 1.0-1.2 ciphersuites that the client MAY offer to
|
||||
// negotiate in the TLS handshake.
|
||||
repeated Ciphersuite ciphersuites = 6;
|
||||
|
||||
// The policy that dictates how the client negotiates ALPN during the TLS
|
||||
// handshake.
|
||||
AlpnPolicy alpn_policy = 7;
|
||||
}
|
||||
|
||||
// Next ID: 12
|
||||
message ServerTlsConfiguration {
|
||||
reserved 4, 5;
|
||||
|
||||
enum RequestClientCertificate {
|
||||
UNSPECIFIED = 0;
|
||||
DONT_REQUEST_CLIENT_CERTIFICATE = 1;
|
||||
REQUEST_CLIENT_CERTIFICATE_BUT_DONT_VERIFY = 2;
|
||||
REQUEST_CLIENT_CERTIFICATE_AND_VERIFY = 3;
|
||||
REQUEST_AND_REQUIRE_CLIENT_CERTIFICATE_BUT_DONT_VERIFY = 4;
|
||||
REQUEST_AND_REQUIRE_CLIENT_CERTIFICATE_AND_VERIFY = 5;
|
||||
}
|
||||
|
||||
// The certificate chain that the server MUST use for the TLS handshake.
|
||||
// It's a list of PEM-encoded certificates, ordered from leaf to root,
|
||||
// excluding the root.
|
||||
repeated string certificate_chain = 1;
|
||||
|
||||
// The minimum TLS version number that the server MUST use for the TLS
|
||||
// handshake. If this field is not provided, the server MUST use the default
|
||||
// minimum version of the server's TLS library.
|
||||
TLSVersion min_tls_version = 2;
|
||||
|
||||
// The maximum TLS version number that the server MUST use for the TLS
|
||||
// handshake. If this field is not provided, the server MUST use the default
|
||||
// maximum version of the server's TLS library.
|
||||
TLSVersion max_tls_version = 3;
|
||||
|
||||
// The ordered list of TLS 1.0-1.2 ciphersuites that the server MAY offer to
|
||||
// negotiate in the TLS handshake.
|
||||
repeated Ciphersuite ciphersuites = 10;
|
||||
|
||||
// Whether to enable TLS resumption.
|
||||
bool tls_resumption_enabled = 6;
|
||||
|
||||
// Whether the server MUST request a client certificate (i.e. to negotiate
|
||||
// TLS vs. mTLS).
|
||||
RequestClientCertificate request_client_certificate = 7;
|
||||
|
||||
// Returns the maximum number of extra bytes that
|
||||
// |OffloadResumptionKeyOperation| can add to the number of unencrypted
|
||||
// bytes to form the encrypted bytes.
|
||||
uint32 max_overhead_of_ticket_aead = 9;
|
||||
|
||||
// The policy that dictates how the server negotiates ALPN during the TLS
|
||||
// handshake.
|
||||
AlpnPolicy alpn_policy = 11;
|
||||
}
|
||||
|
||||
oneof tls_configuration {
|
||||
ClientTlsConfiguration client_tls_configuration = 1;
|
||||
ServerTlsConfiguration server_tls_configuration = 2;
|
||||
}
|
||||
}
|
||||
|
||||
message OffloadPrivateKeyOperationReq {
|
||||
enum PrivateKeyOperation {
|
||||
UNSPECIFIED = 0;
|
||||
// When performing a TLS 1.2 or 1.3 handshake, the (partial) transcript of
|
||||
// the TLS handshake must be signed to prove possession of the private key.
|
||||
//
|
||||
// See https://www.rfc-editor.org/rfc/rfc8446.html#section-4.4.3.
|
||||
SIGN = 1;
|
||||
// When performing a TLS 1.2 handshake using an RSA algorithm, the key
|
||||
// exchange algorithm involves the client generating a premaster secret,
|
||||
// encrypting it using the server's public key, and sending this encrypted
|
||||
// blob to the server in a ClientKeyExchange message.
|
||||
//
|
||||
// See https://www.rfc-editor.org/rfc/rfc4346#section-7.4.7.1.
|
||||
DECRYPT = 2;
|
||||
}
|
||||
|
||||
// The operation the private key is used for.
|
||||
PrivateKeyOperation operation = 1;
|
||||
|
||||
// The signature algorithm to be used for signing operations.
|
||||
SignatureAlgorithm signature_algorithm = 2;
|
||||
|
||||
// The input bytes to be signed or decrypted.
|
||||
oneof in_bytes {
|
||||
// Raw bytes to be hashed and signed, or decrypted.
|
||||
bytes raw_bytes = 4;
|
||||
// A SHA256 hash to be signed. Must be 32 bytes.
|
||||
bytes sha256_digest = 5;
|
||||
// A SHA384 hash to be signed. Must be 48 bytes.
|
||||
bytes sha384_digest = 6;
|
||||
// A SHA512 hash to be signed. Must be 64 bytes.
|
||||
bytes sha512_digest = 7;
|
||||
}
|
||||
}
|
||||
|
||||
message OffloadPrivateKeyOperationResp {
|
||||
// The signed or decrypted output bytes.
|
||||
bytes out_bytes = 1;
|
||||
}
|
||||
|
||||
message OffloadResumptionKeyOperationReq {
|
||||
enum ResumptionKeyOperation {
|
||||
UNSPECIFIED = 0;
|
||||
ENCRYPT = 1;
|
||||
DECRYPT = 2;
|
||||
}
|
||||
|
||||
// The operation the resumption key is used for.
|
||||
ResumptionKeyOperation operation = 1;
|
||||
|
||||
// The bytes to be encrypted or decrypted.
|
||||
bytes in_bytes = 2;
|
||||
}
|
||||
|
||||
message OffloadResumptionKeyOperationResp {
|
||||
// The encrypted or decrypted bytes.
|
||||
bytes out_bytes = 1;
|
||||
}
|
||||
|
||||
message ValidatePeerCertificateChainReq {
|
||||
enum VerificationMode {
|
||||
// The default verification mode supported by S2A.
|
||||
UNSPECIFIED = 0;
|
||||
// The SPIFFE verification mode selects the set of trusted certificates to
|
||||
// use for path building based on the SPIFFE trust domain in the peer's leaf
|
||||
// certificate.
|
||||
SPIFFE = 1;
|
||||
// The connect-to-Google verification mode uses the trust bundle for
|
||||
// connecting to Google, e.g. *.mtls.googleapis.com endpoints.
|
||||
CONNECT_TO_GOOGLE = 2;
|
||||
}
|
||||
|
||||
message ClientPeer {
|
||||
// The certificate chain to be verified. The chain MUST be a list of
|
||||
// DER-encoded certificates, ordered from leaf to root, excluding the root.
|
||||
repeated bytes certificate_chain = 1;
|
||||
}
|
||||
|
||||
message ServerPeer {
|
||||
// The certificate chain to be verified. The chain MUST be a list of
|
||||
// DER-encoded certificates, ordered from leaf to root, excluding the root.
|
||||
repeated bytes certificate_chain = 1;
|
||||
|
||||
// The expected hostname of the server.
|
||||
string server_hostname = 2;
|
||||
|
||||
// The UnrestrictedClientPolicy specified by the user.
|
||||
bytes serialized_unrestricted_client_policy = 3;
|
||||
}
|
||||
|
||||
// The verification mode that S2A MUST use to validate the peer certificate
|
||||
// chain.
|
||||
VerificationMode mode = 1;
|
||||
|
||||
oneof peer_oneof {
|
||||
ClientPeer client_peer = 2;
|
||||
ServerPeer server_peer = 3;
|
||||
}
|
||||
}
|
||||
|
||||
message ValidatePeerCertificateChainResp {
|
||||
enum ValidationResult {
|
||||
UNSPECIFIED = 0;
|
||||
SUCCESS = 1;
|
||||
FAILURE = 2;
|
||||
}
|
||||
|
||||
// The result of validating the peer certificate chain.
|
||||
ValidationResult validation_result = 1;
|
||||
|
||||
// The validation details. This field is only populated when the validation
|
||||
// result is NOT SUCCESS.
|
||||
string validation_details = 2;
|
||||
|
||||
// The S2A context contains information from the peer certificate chain.
|
||||
//
|
||||
// The S2A context MAY be populated even if validation of the peer certificate
|
||||
// chain fails.
|
||||
S2AContext context = 3;
|
||||
}
|
||||
|
||||
message SessionReq {
|
||||
// The identity corresponding to the TLS configurations that MUST be used for
|
||||
// the TLS handshake.
|
||||
//
|
||||
// If a managed identity already exists, the local identity and authentication
|
||||
// mechanisms are ignored. If a managed identity doesn't exist and the local
|
||||
// identity is not populated, S2A will try to deduce the managed identity to
|
||||
// use from the SNI extension. If that also fails, S2A uses the default
|
||||
// identity (if one exists).
|
||||
Identity local_identity = 1;
|
||||
|
||||
// The authentication mechanisms that the application wishes to use to
|
||||
// authenticate to S2A, ordered by preference. S2A will always use the first
|
||||
// authentication mechanism that matches the managed identity.
|
||||
repeated AuthenticationMechanism authentication_mechanisms = 2;
|
||||
|
||||
oneof req_oneof {
|
||||
// Requests the certificate chain and TLS configuration corresponding to the
|
||||
// local identity, which the application MUST use to negotiate the TLS
|
||||
// handshake.
|
||||
GetTlsConfigurationReq get_tls_configuration_req = 3;
|
||||
|
||||
// Signs or decrypts the input bytes using a private key corresponding to
|
||||
// the local identity in the request.
|
||||
//
|
||||
// WARNING: More than one OffloadPrivateKeyOperationReq may be sent to the
|
||||
// S2Av2 by a server during a TLS 1.2 handshake.
|
||||
OffloadPrivateKeyOperationReq offload_private_key_operation_req = 4;
|
||||
|
||||
// Encrypts or decrypts the input bytes using a resumption key corresponding
|
||||
// to the local identity in the request.
|
||||
OffloadResumptionKeyOperationReq offload_resumption_key_operation_req = 5;
|
||||
|
||||
// Verifies the peer's certificate chain using
|
||||
// (a) trust bundles corresponding to the local identity in the request, and
|
||||
// (b) the verification mode in the request.
|
||||
ValidatePeerCertificateChainReq validate_peer_certificate_chain_req = 6;
|
||||
}
|
||||
}
|
||||
|
||||
message SessionResp {
|
||||
// Status of the session response.
|
||||
//
|
||||
// The status field is populated so that if an error occurs when making an
|
||||
// individual request, then communication with the S2A may continue. If an
|
||||
// error is returned directly (e.g. at the gRPC layer), then it may result
|
||||
// that the bidirectional stream being closed.
|
||||
Status status = 1;
|
||||
|
||||
oneof resp_oneof {
|
||||
// Contains the certificate chain and TLS configurations corresponding to
|
||||
// the local identity.
|
||||
GetTlsConfigurationResp get_tls_configuration_resp = 2;
|
||||
|
||||
// Contains the signed or encrypted output bytes using the private key
|
||||
// corresponding to the local identity.
|
||||
OffloadPrivateKeyOperationResp offload_private_key_operation_resp = 3;
|
||||
|
||||
// Contains the encrypted or decrypted output bytes using the resumption key
|
||||
// corresponding to the local identity.
|
||||
OffloadResumptionKeyOperationResp offload_resumption_key_operation_resp = 4;
|
||||
|
||||
// Contains the validation result, peer identity and fingerprints of peer
|
||||
// certificates.
|
||||
ValidatePeerCertificateChainResp validate_peer_certificate_chain_resp = 5;
|
||||
}
|
||||
}
|
||||
|
||||
service S2AService {
|
||||
// SetUpSession is a bidirectional stream used by applications to offload
|
||||
// operations from the TLS handshake.
|
||||
rpc SetUpSession(stream SessionReq) returns (stream SessionResp) {}
|
||||
}
|
|
@ -0,0 +1,62 @@
|
|||
// Copyright 2024 The gRPC Authors
|
||||
//
|
||||
// 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.
|
||||
|
||||
// The canonical version of this proto can be found at
|
||||
// https://github.com/grpc/grpc-proto/blob/master/grpc/gcp/s2a/s2a_context.proto
|
||||
|
||||
syntax = "proto3";
|
||||
|
||||
package grpc.gcp.s2a;
|
||||
|
||||
import "grpc/gcp/s2a/common.proto";
|
||||
|
||||
option java_multiple_files = true;
|
||||
option java_outer_classname = "S2AContextProto";
|
||||
option java_package = "io.grpc.s2a.handshaker";
|
||||
|
||||
message S2AContext {
|
||||
// The SPIFFE ID from the peer leaf certificate, if present.
|
||||
//
|
||||
// This field is only populated if the leaf certificate is a valid SPIFFE
|
||||
// SVID; in particular, there is a unique URI SAN and this URI SAN is a valid
|
||||
// SPIFFE ID.
|
||||
string leaf_cert_spiffe_id = 1;
|
||||
|
||||
// The URIs that are present in the SubjectAltName extension of the peer leaf
|
||||
// certificate.
|
||||
//
|
||||
// Note that the extracted URIs are not validated and may not be properly
|
||||
// formatted.
|
||||
repeated string leaf_cert_uris = 2;
|
||||
|
||||
// The DNSNames that are present in the SubjectAltName extension of the peer
|
||||
// leaf certificate.
|
||||
repeated string leaf_cert_dnsnames = 3;
|
||||
|
||||
// The (ordered) list of fingerprints in the certificate chain used to verify
|
||||
// the given leaf certificate. The order MUST be from leaf certificate
|
||||
// fingerprint to root certificate fingerprint.
|
||||
//
|
||||
// A fingerprint is the base-64 encoding of the SHA256 hash of the
|
||||
// DER-encoding of a certificate. The list MAY be populated even if the peer
|
||||
// certificate chain was NOT validated successfully.
|
||||
repeated string peer_certificate_chain_fingerprints = 4;
|
||||
|
||||
// The local identity used during session setup.
|
||||
Identity local_identity = 5;
|
||||
|
||||
// The SHA256 hash of the DER-encoding of the local leaf certificate used in
|
||||
// the handshake.
|
||||
bytes local_leaf_cert_fingerprint = 6;
|
||||
}
|
|
@ -0,0 +1,135 @@
|
|||
/*
|
||||
* Copyright 2024 The gRPC Authors
|
||||
*
|
||||
* 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 io.grpc.s2a;
|
||||
|
||||
import static com.google.common.truth.Truth.assertThat;
|
||||
import static org.junit.Assert.assertThrows;
|
||||
|
||||
import org.junit.Test;
|
||||
import org.junit.runner.RunWith;
|
||||
import org.junit.runners.JUnit4;
|
||||
|
||||
@RunWith(JUnit4.class)
|
||||
public final class MtlsToS2AChannelCredentialsTest {
|
||||
@Test
|
||||
public void createBuilder_nullAddress_throwsException() throws Exception {
|
||||
assertThrows(
|
||||
IllegalArgumentException.class,
|
||||
() ->
|
||||
MtlsToS2AChannelCredentials.createBuilder(
|
||||
/* s2aAddress= */ null,
|
||||
/* privateKeyPath= */ "src/test/resources/client_key.pem",
|
||||
/* certChainPath= */ "src/test/resources/client_cert.pem",
|
||||
/* trustBundlePath= */ "src/test/resources/root_cert.pem"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void createBuilder_nullPrivateKeyPath_throwsException() throws Exception {
|
||||
assertThrows(
|
||||
IllegalArgumentException.class,
|
||||
() ->
|
||||
MtlsToS2AChannelCredentials.createBuilder(
|
||||
/* s2aAddress= */ "s2a_address",
|
||||
/* privateKeyPath= */ null,
|
||||
/* certChainPath= */ "src/test/resources/client_cert.pem",
|
||||
/* trustBundlePath= */ "src/test/resources/root_cert.pem"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void createBuilder_nullCertChainPath_throwsException() throws Exception {
|
||||
assertThrows(
|
||||
IllegalArgumentException.class,
|
||||
() ->
|
||||
MtlsToS2AChannelCredentials.createBuilder(
|
||||
/* s2aAddress= */ "s2a_address",
|
||||
/* privateKeyPath= */ "src/test/resources/client_key.pem",
|
||||
/* certChainPath= */ null,
|
||||
/* trustBundlePath= */ "src/test/resources/root_cert.pem"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void createBuilder_nullTrustBundlePath_throwsException() throws Exception {
|
||||
assertThrows(
|
||||
IllegalArgumentException.class,
|
||||
() ->
|
||||
MtlsToS2AChannelCredentials.createBuilder(
|
||||
/* s2aAddress= */ "s2a_address",
|
||||
/* privateKeyPath= */ "src/test/resources/client_key.pem",
|
||||
/* certChainPath= */ "src/test/resources/client_cert.pem",
|
||||
/* trustBundlePath= */ null));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void createBuilder_emptyAddress_throwsException() throws Exception {
|
||||
assertThrows(
|
||||
IllegalArgumentException.class,
|
||||
() ->
|
||||
MtlsToS2AChannelCredentials.createBuilder(
|
||||
/* s2aAddress= */ "",
|
||||
/* privateKeyPath= */ "src/test/resources/client_key.pem",
|
||||
/* certChainPath= */ "src/test/resources/client_cert.pem",
|
||||
/* trustBundlePath= */ "src/test/resources/root_cert.pem"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void createBuilder_emptyPrivateKeyPath_throwsException() throws Exception {
|
||||
assertThrows(
|
||||
IllegalArgumentException.class,
|
||||
() ->
|
||||
MtlsToS2AChannelCredentials.createBuilder(
|
||||
/* s2aAddress= */ "s2a_address",
|
||||
/* privateKeyPath= */ "",
|
||||
/* certChainPath= */ "src/test/resources/client_cert.pem",
|
||||
/* trustBundlePath= */ "src/test/resources/root_cert.pem"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void createBuilder_emptyCertChainPath_throwsException() throws Exception {
|
||||
assertThrows(
|
||||
IllegalArgumentException.class,
|
||||
() ->
|
||||
MtlsToS2AChannelCredentials.createBuilder(
|
||||
/* s2aAddress= */ "s2a_address",
|
||||
/* privateKeyPath= */ "src/test/resources/client_key.pem",
|
||||
/* certChainPath= */ "",
|
||||
/* trustBundlePath= */ "src/test/resources/root_cert.pem"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void createBuilder_emptyTrustBundlePath_throwsException() throws Exception {
|
||||
assertThrows(
|
||||
IllegalArgumentException.class,
|
||||
() ->
|
||||
MtlsToS2AChannelCredentials.createBuilder(
|
||||
/* s2aAddress= */ "s2a_address",
|
||||
/* privateKeyPath= */ "src/test/resources/client_key.pem",
|
||||
/* certChainPath= */ "src/test/resources/client_cert.pem",
|
||||
/* trustBundlePath= */ ""));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void build_s2AChannelCredentials_success() throws Exception {
|
||||
assertThat(
|
||||
MtlsToS2AChannelCredentials.createBuilder(
|
||||
/* s2aAddress= */ "s2a_address",
|
||||
/* privateKeyPath= */ "src/test/resources/client_key.pem",
|
||||
/* certChainPath= */ "src/test/resources/client_cert.pem",
|
||||
/* trustBundlePath= */ "src/test/resources/root_cert.pem")
|
||||
.build())
|
||||
.isInstanceOf(S2AChannelCredentials.Builder.class);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,112 @@
|
|||
/*
|
||||
* Copyright 2024 The gRPC Authors
|
||||
*
|
||||
* 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 io.grpc.s2a;
|
||||
|
||||
import static com.google.common.truth.Truth.assertThat;
|
||||
import static org.junit.Assert.assertThrows;
|
||||
|
||||
import io.grpc.ChannelCredentials;
|
||||
import io.grpc.TlsChannelCredentials;
|
||||
import java.io.File;
|
||||
import org.junit.Test;
|
||||
import org.junit.runner.RunWith;
|
||||
import org.junit.runners.JUnit4;
|
||||
|
||||
/** Unit tests for {@code S2AChannelCredentials}. */
|
||||
@RunWith(JUnit4.class)
|
||||
public final class S2AChannelCredentialsTest {
|
||||
@Test
|
||||
public void createBuilder_nullArgument_throwsException() throws Exception {
|
||||
assertThrows(IllegalArgumentException.class, () -> S2AChannelCredentials.createBuilder(null));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void createBuilder_emptyAddress_throwsException() throws Exception {
|
||||
assertThrows(IllegalArgumentException.class, () -> S2AChannelCredentials.createBuilder(""));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void setLocalSpiffeId_nullArgument_throwsException() throws Exception {
|
||||
assertThrows(
|
||||
NullPointerException.class,
|
||||
() -> S2AChannelCredentials.createBuilder("s2a_address").setLocalSpiffeId(null));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void setLocalHostname_nullArgument_throwsException() throws Exception {
|
||||
assertThrows(
|
||||
NullPointerException.class,
|
||||
() -> S2AChannelCredentials.createBuilder("s2a_address").setLocalHostname(null));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void setLocalUid_nullArgument_throwsException() throws Exception {
|
||||
assertThrows(
|
||||
NullPointerException.class,
|
||||
() -> S2AChannelCredentials.createBuilder("s2a_address").setLocalUid(null));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void build_withLocalSpiffeId_succeeds() throws Exception {
|
||||
assertThat(
|
||||
S2AChannelCredentials.createBuilder("s2a_address")
|
||||
.setLocalSpiffeId("spiffe://test")
|
||||
.build())
|
||||
.isNotNull();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void build_withLocalHostname_succeeds() throws Exception {
|
||||
assertThat(
|
||||
S2AChannelCredentials.createBuilder("s2a_address")
|
||||
.setLocalHostname("local_hostname")
|
||||
.build())
|
||||
.isNotNull();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void build_withLocalUid_succeeds() throws Exception {
|
||||
assertThat(S2AChannelCredentials.createBuilder("s2a_address").setLocalUid("local_uid").build())
|
||||
.isNotNull();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void build_withNoLocalIdentity_succeeds() throws Exception {
|
||||
assertThat(S2AChannelCredentials.createBuilder("s2a_address").build())
|
||||
.isNotNull();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void build_withTlsChannelCredentials_succeeds() throws Exception {
|
||||
assertThat(
|
||||
S2AChannelCredentials.createBuilder("s2a_address")
|
||||
.setLocalSpiffeId("spiffe://test")
|
||||
.setS2AChannelCredentials(getTlsChannelCredentials())
|
||||
.build())
|
||||
.isNotNull();
|
||||
}
|
||||
|
||||
private static ChannelCredentials getTlsChannelCredentials() throws Exception {
|
||||
File clientCert = new File("src/test/resources/client_cert.pem");
|
||||
File clientKey = new File("src/test/resources/client_key.pem");
|
||||
File rootCert = new File("src/test/resources/root_cert.pem");
|
||||
return TlsChannelCredentials.newBuilder()
|
||||
.keyManager(clientCert, clientKey)
|
||||
.trustManager(rootCert)
|
||||
.build();
|
||||
}
|
||||
}
|
|
@ -0,0 +1,125 @@
|
|||
/*
|
||||
* Copyright 2024 The gRPC Authors
|
||||
*
|
||||
* 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 io.grpc.s2a.channel;
|
||||
|
||||
import static com.google.common.truth.Truth.assertThat;
|
||||
import static org.junit.Assert.assertThrows;
|
||||
import static org.mockito.Mockito.mock;
|
||||
|
||||
import io.grpc.Channel;
|
||||
import io.grpc.internal.ObjectPool;
|
||||
import org.checkerframework.checker.nullness.qual.Nullable;
|
||||
import org.junit.Test;
|
||||
import org.junit.runner.RunWith;
|
||||
import org.junit.runners.JUnit4;
|
||||
|
||||
/** Unit tests for {@link S2AGrpcChannelPool}. */
|
||||
@RunWith(JUnit4.class)
|
||||
public final class S2AGrpcChannelPoolTest {
|
||||
@Test
|
||||
public void getChannel_success() throws Exception {
|
||||
FakeChannelPool fakeChannelPool = new FakeChannelPool();
|
||||
S2AChannelPool s2aChannelPool = S2AGrpcChannelPool.create(fakeChannelPool);
|
||||
|
||||
Channel channel = s2aChannelPool.getChannel();
|
||||
|
||||
assertThat(channel).isNotNull();
|
||||
assertThat(fakeChannelPool.isChannelCached()).isTrue();
|
||||
assertThat(s2aChannelPool.getChannel()).isEqualTo(channel);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void returnToPool_success() throws Exception {
|
||||
FakeChannelPool fakeChannelPool = new FakeChannelPool();
|
||||
S2AChannelPool s2aChannelPool = S2AGrpcChannelPool.create(fakeChannelPool);
|
||||
|
||||
s2aChannelPool.returnToPool(s2aChannelPool.getChannel());
|
||||
|
||||
assertThat(fakeChannelPool.isChannelCached()).isFalse();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void returnToPool_channelStillCachedBecauseMultipleChannelsRetrieved() throws Exception {
|
||||
FakeChannelPool fakeChannelPool = new FakeChannelPool();
|
||||
S2AChannelPool s2aChannelPool = S2AGrpcChannelPool.create(fakeChannelPool);
|
||||
|
||||
s2aChannelPool.getChannel();
|
||||
s2aChannelPool.returnToPool(s2aChannelPool.getChannel());
|
||||
|
||||
assertThat(fakeChannelPool.isChannelCached()).isTrue();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void returnToPool_failureBecauseChannelWasNotFromPool() throws Exception {
|
||||
S2AChannelPool s2aChannelPool = S2AGrpcChannelPool.create(new FakeChannelPool());
|
||||
|
||||
IllegalArgumentException expected =
|
||||
assertThrows(
|
||||
IllegalArgumentException.class,
|
||||
() -> s2aChannelPool.returnToPool(mock(Channel.class)));
|
||||
assertThat(expected)
|
||||
.hasMessageThat()
|
||||
.isEqualTo(
|
||||
"Cannot return the channel to channel pool because the channel was not obtained from"
|
||||
+ " channel pool.");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void close_success() throws Exception {
|
||||
FakeChannelPool fakeChannelPool = new FakeChannelPool();
|
||||
try (S2AChannelPool s2aChannelPool = S2AGrpcChannelPool.create(fakeChannelPool)) {
|
||||
s2aChannelPool.getChannel();
|
||||
}
|
||||
|
||||
assertThat(fakeChannelPool.isChannelCached()).isFalse();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void close_poolIsUnusable() throws Exception {
|
||||
S2AChannelPool s2aChannelPool = S2AGrpcChannelPool.create(new FakeChannelPool());
|
||||
s2aChannelPool.close();
|
||||
|
||||
IllegalStateException expected =
|
||||
assertThrows(IllegalStateException.class, s2aChannelPool::getChannel);
|
||||
|
||||
assertThat(expected).hasMessageThat().isEqualTo("Channel pool is not open.");
|
||||
}
|
||||
|
||||
private static class FakeChannelPool implements ObjectPool<Channel> {
|
||||
private final Channel mockChannel = mock(Channel.class);
|
||||
private @Nullable Channel cachedChannel = null;
|
||||
|
||||
@Override
|
||||
public Channel getObject() {
|
||||
if (cachedChannel == null) {
|
||||
cachedChannel = mockChannel;
|
||||
}
|
||||
return cachedChannel;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Channel returnObject(Object object) {
|
||||
assertThat(object).isSameInstanceAs(mockChannel);
|
||||
cachedChannel = null;
|
||||
return null;
|
||||
}
|
||||
|
||||
public boolean isChannelCached() {
|
||||
return (cachedChannel != null);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,390 @@
|
|||
/*
|
||||
* Copyright 2024 The gRPC Authors
|
||||
*
|
||||
* 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 io.grpc.s2a.channel;
|
||||
|
||||
import static com.google.common.truth.Truth.assertThat;
|
||||
import static com.google.common.truth.extensions.proto.ProtoTruth.assertThat;
|
||||
import static java.util.concurrent.TimeUnit.SECONDS;
|
||||
import static org.junit.Assert.assertThrows;
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.times;
|
||||
import static org.mockito.Mockito.verify;
|
||||
|
||||
import io.grpc.CallOptions;
|
||||
import io.grpc.Channel;
|
||||
import io.grpc.ChannelCredentials;
|
||||
import io.grpc.ClientCall;
|
||||
import io.grpc.ManagedChannel;
|
||||
import io.grpc.MethodDescriptor;
|
||||
import io.grpc.Server;
|
||||
import io.grpc.ServerBuilder;
|
||||
import io.grpc.ServerCredentials;
|
||||
import io.grpc.StatusRuntimeException;
|
||||
import io.grpc.TlsChannelCredentials;
|
||||
import io.grpc.TlsServerCredentials;
|
||||
import io.grpc.benchmarks.Utils;
|
||||
import io.grpc.internal.SharedResourceHolder.Resource;
|
||||
import io.grpc.netty.NettyServerBuilder;
|
||||
import io.grpc.s2a.channel.S2AHandshakerServiceChannel.EventLoopHoldingChannel;
|
||||
import io.grpc.stub.StreamObserver;
|
||||
import io.grpc.testing.GrpcCleanupRule;
|
||||
import io.grpc.testing.protobuf.SimpleRequest;
|
||||
import io.grpc.testing.protobuf.SimpleResponse;
|
||||
import io.grpc.testing.protobuf.SimpleServiceGrpc;
|
||||
import io.netty.channel.EventLoopGroup;
|
||||
import java.io.File;
|
||||
import java.time.Duration;
|
||||
import java.util.Optional;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import org.junit.Before;
|
||||
import org.junit.ClassRule;
|
||||
import org.junit.Test;
|
||||
import org.junit.runner.RunWith;
|
||||
import org.junit.runners.JUnit4;
|
||||
|
||||
/** Unit tests for {@link S2AHandshakerServiceChannel}. */
|
||||
@RunWith(JUnit4.class)
|
||||
public final class S2AHandshakerServiceChannelTest {
|
||||
@ClassRule public static final GrpcCleanupRule grpcCleanup = new GrpcCleanupRule();
|
||||
private static final Duration CHANNEL_SHUTDOWN_TIMEOUT = Duration.ofSeconds(10);
|
||||
private final EventLoopGroup mockEventLoopGroup = mock(EventLoopGroup.class);
|
||||
private Server mtlsServer;
|
||||
private Server plaintextServer;
|
||||
|
||||
@Before
|
||||
public void setUp() throws Exception {
|
||||
mtlsServer = createMtlsServer();
|
||||
plaintextServer = createPlaintextServer();
|
||||
mtlsServer.start();
|
||||
plaintextServer.start();
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a {@code Resource<Channel>} and verifies that it produces a {@code ChannelResource}
|
||||
* instance by using its {@code toString()} method.
|
||||
*/
|
||||
@Test
|
||||
public void getChannelResource_success() {
|
||||
Resource<Channel> resource =
|
||||
S2AHandshakerServiceChannel.getChannelResource(
|
||||
"localhost:" + plaintextServer.getPort(),
|
||||
/* s2aChannelCredentials= */ Optional.empty());
|
||||
assertThat(resource.toString()).isEqualTo("grpc-s2a-channel");
|
||||
}
|
||||
|
||||
/** Same as getChannelResource_success, but use mTLS. */
|
||||
@Test
|
||||
public void getChannelResource_mtlsSuccess() throws Exception {
|
||||
Resource<Channel> resource =
|
||||
S2AHandshakerServiceChannel.getChannelResource(
|
||||
"localhost:" + mtlsServer.getPort(), getTlsChannelCredentials());
|
||||
assertThat(resource.toString()).isEqualTo("grpc-s2a-channel");
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates two {@code Resoure<Channel>}s for the same target address and verifies that they are
|
||||
* equal.
|
||||
*/
|
||||
@Test
|
||||
public void getChannelResource_twoEqualChannels() {
|
||||
Resource<Channel> resource =
|
||||
S2AHandshakerServiceChannel.getChannelResource(
|
||||
"localhost:" + plaintextServer.getPort(),
|
||||
/* s2aChannelCredentials= */ Optional.empty());
|
||||
Resource<Channel> resourceTwo =
|
||||
S2AHandshakerServiceChannel.getChannelResource(
|
||||
"localhost:" + plaintextServer.getPort(),
|
||||
/* s2aChannelCredentials= */ Optional.empty());
|
||||
assertThat(resource).isEqualTo(resourceTwo);
|
||||
}
|
||||
|
||||
/** Same as getChannelResource_twoEqualChannels, but use mTLS. */
|
||||
@Test
|
||||
public void getChannelResource_mtlsTwoEqualChannels() throws Exception {
|
||||
Resource<Channel> resource =
|
||||
S2AHandshakerServiceChannel.getChannelResource(
|
||||
"localhost:" + mtlsServer.getPort(), getTlsChannelCredentials());
|
||||
Resource<Channel> resourceTwo =
|
||||
S2AHandshakerServiceChannel.getChannelResource(
|
||||
"localhost:" + mtlsServer.getPort(), getTlsChannelCredentials());
|
||||
assertThat(resource).isEqualTo(resourceTwo);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates two {@code Resoure<Channel>}s for different target addresses and verifies that they are
|
||||
* distinct.
|
||||
*/
|
||||
@Test
|
||||
public void getChannelResource_twoDistinctChannels() {
|
||||
Resource<Channel> resource =
|
||||
S2AHandshakerServiceChannel.getChannelResource(
|
||||
"localhost:" + plaintextServer.getPort(),
|
||||
/* s2aChannelCredentials= */ Optional.empty());
|
||||
Resource<Channel> resourceTwo =
|
||||
S2AHandshakerServiceChannel.getChannelResource(
|
||||
"localhost:" + Utils.pickUnusedPort(), /* s2aChannelCredentials= */ Optional.empty());
|
||||
assertThat(resourceTwo).isNotEqualTo(resource);
|
||||
}
|
||||
|
||||
/** Same as getChannelResource_twoDistinctChannels, but use mTLS. */
|
||||
@Test
|
||||
public void getChannelResource_mtlsTwoDistinctChannels() throws Exception {
|
||||
Resource<Channel> resource =
|
||||
S2AHandshakerServiceChannel.getChannelResource(
|
||||
"localhost:" + mtlsServer.getPort(), getTlsChannelCredentials());
|
||||
Resource<Channel> resourceTwo =
|
||||
S2AHandshakerServiceChannel.getChannelResource(
|
||||
"localhost:" + Utils.pickUnusedPort(), getTlsChannelCredentials());
|
||||
assertThat(resourceTwo).isNotEqualTo(resource);
|
||||
}
|
||||
|
||||
/**
|
||||
* Uses a {@code Resource<Channel>} to create a channel, closes the channel, and verifies that the
|
||||
* channel is closed by attempting to make a simple RPC.
|
||||
*/
|
||||
@Test
|
||||
public void close_success() {
|
||||
Resource<Channel> resource =
|
||||
S2AHandshakerServiceChannel.getChannelResource(
|
||||
"localhost:" + plaintextServer.getPort(),
|
||||
/* s2aChannelCredentials= */ Optional.empty());
|
||||
Channel channel = resource.create();
|
||||
resource.close(channel);
|
||||
StatusRuntimeException expected =
|
||||
assertThrows(
|
||||
StatusRuntimeException.class,
|
||||
() ->
|
||||
SimpleServiceGrpc.newBlockingStub(channel)
|
||||
.unaryRpc(SimpleRequest.getDefaultInstance()));
|
||||
assertThat(expected).hasMessageThat().isEqualTo("UNAVAILABLE: Channel shutdown invoked");
|
||||
}
|
||||
|
||||
/** Same as close_success, but use mTLS. */
|
||||
@Test
|
||||
public void close_mtlsSuccess() throws Exception {
|
||||
Resource<Channel> resource =
|
||||
S2AHandshakerServiceChannel.getChannelResource(
|
||||
"localhost:" + mtlsServer.getPort(), getTlsChannelCredentials());
|
||||
Channel channel = resource.create();
|
||||
resource.close(channel);
|
||||
StatusRuntimeException expected =
|
||||
assertThrows(
|
||||
StatusRuntimeException.class,
|
||||
() ->
|
||||
SimpleServiceGrpc.newBlockingStub(channel)
|
||||
.unaryRpc(SimpleRequest.getDefaultInstance()));
|
||||
assertThat(expected).hasMessageThat().isEqualTo("UNAVAILABLE: Channel shutdown invoked");
|
||||
}
|
||||
|
||||
/**
|
||||
* Verifies that an {@code EventLoopHoldingChannel}'s {@code newCall} method can be used to
|
||||
* perform a simple RPC.
|
||||
*/
|
||||
@Test
|
||||
public void newCall_performSimpleRpcSuccess() {
|
||||
Resource<Channel> resource =
|
||||
S2AHandshakerServiceChannel.getChannelResource(
|
||||
"localhost:" + plaintextServer.getPort(),
|
||||
/* s2aChannelCredentials= */ Optional.empty());
|
||||
Channel channel = resource.create();
|
||||
assertThat(channel).isInstanceOf(EventLoopHoldingChannel.class);
|
||||
assertThat(
|
||||
SimpleServiceGrpc.newBlockingStub(channel).unaryRpc(SimpleRequest.getDefaultInstance()))
|
||||
.isEqualToDefaultInstance();
|
||||
}
|
||||
|
||||
/** Same as newCall_performSimpleRpcSuccess, but use mTLS. */
|
||||
@Test
|
||||
public void newCall_mtlsPerformSimpleRpcSuccess() throws Exception {
|
||||
Resource<Channel> resource =
|
||||
S2AHandshakerServiceChannel.getChannelResource(
|
||||
"localhost:" + mtlsServer.getPort(), getTlsChannelCredentials());
|
||||
Channel channel = resource.create();
|
||||
assertThat(channel).isInstanceOf(EventLoopHoldingChannel.class);
|
||||
assertThat(
|
||||
SimpleServiceGrpc.newBlockingStub(channel).unaryRpc(SimpleRequest.getDefaultInstance()))
|
||||
.isEqualToDefaultInstance();
|
||||
}
|
||||
|
||||
/** Creates a {@code EventLoopHoldingChannel} instance and verifies its authority. */
|
||||
@Test
|
||||
public void authority_success() throws Exception {
|
||||
ManagedChannel channel = new FakeManagedChannel(true);
|
||||
EventLoopHoldingChannel eventLoopHoldingChannel =
|
||||
EventLoopHoldingChannel.create(channel, mockEventLoopGroup);
|
||||
assertThat(eventLoopHoldingChannel.authority()).isEqualTo("FakeManagedChannel");
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates and closes a {@code EventLoopHoldingChannel} when its {@code ManagedChannel} terminates
|
||||
* successfully.
|
||||
*/
|
||||
@Test
|
||||
public void close_withDelegateTerminatedSuccess() throws Exception {
|
||||
ManagedChannel channel = new FakeManagedChannel(true);
|
||||
EventLoopHoldingChannel eventLoopHoldingChannel =
|
||||
EventLoopHoldingChannel.create(channel, mockEventLoopGroup);
|
||||
eventLoopHoldingChannel.close();
|
||||
assertThat(channel.isShutdown()).isTrue();
|
||||
verify(mockEventLoopGroup, times(1))
|
||||
.shutdownGracefully(0, CHANNEL_SHUTDOWN_TIMEOUT.getSeconds(), SECONDS);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates and closes a {@code EventLoopHoldingChannel} when its {@code ManagedChannel} does not
|
||||
* terminate successfully.
|
||||
*/
|
||||
@Test
|
||||
public void close_withDelegateTerminatedFailure() throws Exception {
|
||||
ManagedChannel channel = new FakeManagedChannel(false);
|
||||
EventLoopHoldingChannel eventLoopHoldingChannel =
|
||||
EventLoopHoldingChannel.create(channel, mockEventLoopGroup);
|
||||
eventLoopHoldingChannel.close();
|
||||
assertThat(channel.isShutdown()).isTrue();
|
||||
verify(mockEventLoopGroup, times(1))
|
||||
.shutdownGracefully(1, CHANNEL_SHUTDOWN_TIMEOUT.getSeconds(), SECONDS);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates and closes a {@code EventLoopHoldingChannel}, creates a new channel from the same
|
||||
* resource, and verifies that this second channel is useable.
|
||||
*/
|
||||
@Test
|
||||
public void create_succeedsAfterCloseIsCalledOnce() throws Exception {
|
||||
Resource<Channel> resource =
|
||||
S2AHandshakerServiceChannel.getChannelResource(
|
||||
"localhost:" + plaintextServer.getPort(),
|
||||
/* s2aChannelCredentials= */ Optional.empty());
|
||||
Channel channelOne = resource.create();
|
||||
resource.close(channelOne);
|
||||
|
||||
Channel channelTwo = resource.create();
|
||||
assertThat(channelTwo).isInstanceOf(EventLoopHoldingChannel.class);
|
||||
assertThat(
|
||||
SimpleServiceGrpc.newBlockingStub(channelTwo)
|
||||
.unaryRpc(SimpleRequest.getDefaultInstance()))
|
||||
.isEqualToDefaultInstance();
|
||||
resource.close(channelTwo);
|
||||
}
|
||||
|
||||
/** Same as create_succeedsAfterCloseIsCalledOnce, but use mTLS. */
|
||||
@Test
|
||||
public void create_mtlsSucceedsAfterCloseIsCalledOnce() throws Exception {
|
||||
Resource<Channel> resource =
|
||||
S2AHandshakerServiceChannel.getChannelResource(
|
||||
"localhost:" + mtlsServer.getPort(), getTlsChannelCredentials());
|
||||
Channel channelOne = resource.create();
|
||||
resource.close(channelOne);
|
||||
|
||||
Channel channelTwo = resource.create();
|
||||
assertThat(channelTwo).isInstanceOf(EventLoopHoldingChannel.class);
|
||||
assertThat(
|
||||
SimpleServiceGrpc.newBlockingStub(channelTwo)
|
||||
.unaryRpc(SimpleRequest.getDefaultInstance()))
|
||||
.isEqualToDefaultInstance();
|
||||
resource.close(channelTwo);
|
||||
}
|
||||
|
||||
private static Server createMtlsServer() throws Exception {
|
||||
SimpleServiceImpl service = new SimpleServiceImpl();
|
||||
File serverCert = new File("src/test/resources/server_cert.pem");
|
||||
File serverKey = new File("src/test/resources/server_key.pem");
|
||||
File rootCert = new File("src/test/resources/root_cert.pem");
|
||||
ServerCredentials creds =
|
||||
TlsServerCredentials.newBuilder()
|
||||
.keyManager(serverCert, serverKey)
|
||||
.trustManager(rootCert)
|
||||
.clientAuth(TlsServerCredentials.ClientAuth.REQUIRE)
|
||||
.build();
|
||||
return grpcCleanup.register(
|
||||
NettyServerBuilder.forPort(Utils.pickUnusedPort(), creds).addService(service).build());
|
||||
}
|
||||
|
||||
private static Server createPlaintextServer() {
|
||||
SimpleServiceImpl service = new SimpleServiceImpl();
|
||||
return grpcCleanup.register(
|
||||
ServerBuilder.forPort(Utils.pickUnusedPort()).addService(service).build());
|
||||
}
|
||||
|
||||
private static Optional<ChannelCredentials> getTlsChannelCredentials() throws Exception {
|
||||
File clientCert = new File("src/test/resources/client_cert.pem");
|
||||
File clientKey = new File("src/test/resources/client_key.pem");
|
||||
File rootCert = new File("src/test/resources/root_cert.pem");
|
||||
return Optional.of(
|
||||
TlsChannelCredentials.newBuilder()
|
||||
.keyManager(clientCert, clientKey)
|
||||
.trustManager(rootCert)
|
||||
.build());
|
||||
}
|
||||
|
||||
private static class SimpleServiceImpl extends SimpleServiceGrpc.SimpleServiceImplBase {
|
||||
@Override
|
||||
public void unaryRpc(SimpleRequest request, StreamObserver<SimpleResponse> streamObserver) {
|
||||
streamObserver.onNext(SimpleResponse.getDefaultInstance());
|
||||
streamObserver.onCompleted();
|
||||
}
|
||||
}
|
||||
|
||||
private static class FakeManagedChannel extends ManagedChannel {
|
||||
private final boolean isDelegateTerminatedSuccess;
|
||||
private boolean isShutdown = false;
|
||||
|
||||
FakeManagedChannel(boolean isDelegateTerminatedSuccess) {
|
||||
this.isDelegateTerminatedSuccess = isDelegateTerminatedSuccess;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String authority() {
|
||||
return "FakeManagedChannel";
|
||||
}
|
||||
|
||||
@Override
|
||||
public <ReqT, RespT> ClientCall<ReqT, RespT> newCall(
|
||||
MethodDescriptor<ReqT, RespT> methodDescriptor, CallOptions options) {
|
||||
throw new UnsupportedOperationException("This method should not be called.");
|
||||
}
|
||||
|
||||
@Override
|
||||
public ManagedChannel shutdown() {
|
||||
throw new UnsupportedOperationException("This method should not be called.");
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isShutdown() {
|
||||
return isShutdown;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isTerminated() {
|
||||
throw new UnsupportedOperationException("This method should not be called.");
|
||||
}
|
||||
|
||||
@Override
|
||||
public ManagedChannel shutdownNow() {
|
||||
isShutdown = true;
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean awaitTermination(long timeout, TimeUnit unit) throws InterruptedException {
|
||||
if (isDelegateTerminatedSuccess) {
|
||||
return true;
|
||||
}
|
||||
throw new InterruptedException("Await termination was interrupted.");
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,55 @@
|
|||
/*
|
||||
* Copyright 2024 The gRPC Authors
|
||||
*
|
||||
* 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 io.grpc.s2a.handshaker;
|
||||
|
||||
import io.grpc.stub.StreamObserver;
|
||||
import java.security.NoSuchAlgorithmException;
|
||||
import java.security.spec.InvalidKeySpecException;
|
||||
import java.util.logging.Logger;
|
||||
|
||||
/** A fake S2Av2 server that should be used for testing only. */
|
||||
public final class FakeS2AServer extends S2AServiceGrpc.S2AServiceImplBase {
|
||||
private static final Logger logger = Logger.getLogger(FakeS2AServer.class.getName());
|
||||
|
||||
private final FakeWriter writer;
|
||||
|
||||
public FakeS2AServer() throws InvalidKeySpecException, NoSuchAlgorithmException {
|
||||
this.writer = new FakeWriter();
|
||||
this.writer.setVerificationResult(FakeWriter.VerificationResult.SUCCESS).initializePrivateKey();
|
||||
}
|
||||
|
||||
@Override
|
||||
public StreamObserver<SessionReq> setUpSession(StreamObserver<SessionResp> responseObserver) {
|
||||
return new StreamObserver<SessionReq>() {
|
||||
@Override
|
||||
public void onNext(SessionReq req) {
|
||||
logger.info("Received a request from client.");
|
||||
responseObserver.onNext(writer.handleResponse(req));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onError(Throwable t) {
|
||||
responseObserver.onError(t);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onCompleted() {
|
||||
responseObserver.onCompleted();
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
|
@ -0,0 +1,265 @@
|
|||
/*
|
||||
* Copyright 2024 The gRPC Authors
|
||||
*
|
||||
* 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 io.grpc.s2a.handshaker;
|
||||
|
||||
import static com.google.common.truth.extensions.proto.ProtoTruth.assertThat;
|
||||
import static java.util.concurrent.TimeUnit.SECONDS;
|
||||
|
||||
import com.google.common.collect.ImmutableList;
|
||||
import com.google.protobuf.ByteString;
|
||||
import io.grpc.Grpc;
|
||||
import io.grpc.InsecureChannelCredentials;
|
||||
import io.grpc.ManagedChannel;
|
||||
import io.grpc.Server;
|
||||
import io.grpc.ServerBuilder;
|
||||
import io.grpc.benchmarks.Utils;
|
||||
import io.grpc.s2a.handshaker.ValidatePeerCertificateChainReq.VerificationMode;
|
||||
import io.grpc.stub.StreamObserver;
|
||||
import java.util.concurrent.CountDownLatch;
|
||||
import java.util.concurrent.ExecutorService;
|
||||
import java.util.concurrent.Executors;
|
||||
import java.util.logging.Logger;
|
||||
import org.junit.After;
|
||||
import org.junit.Before;
|
||||
import org.junit.Test;
|
||||
import org.junit.runner.RunWith;
|
||||
import org.junit.runners.JUnit4;
|
||||
|
||||
/** Unit tests for {@link FakeS2AServer}. */
|
||||
@RunWith(JUnit4.class)
|
||||
public final class FakeS2AServerTest {
|
||||
private static final Logger logger = Logger.getLogger(FakeS2AServerTest.class.getName());
|
||||
|
||||
private static final ImmutableList<ByteString> FAKE_CERT_DER_CHAIN =
|
||||
ImmutableList.of(
|
||||
ByteString.copyFrom(
|
||||
new byte[] {'f', 'a', 'k', 'e', '-', 'd', 'e', 'r', '-', 'c', 'h', 'a', 'i', 'n'}));
|
||||
private int port;
|
||||
private String serverAddress;
|
||||
private SessionResp response = null;
|
||||
private Server fakeS2AServer;
|
||||
|
||||
@Before
|
||||
public void setUp() throws Exception {
|
||||
port = Utils.pickUnusedPort();
|
||||
fakeS2AServer = ServerBuilder.forPort(port).addService(new FakeS2AServer()).build();
|
||||
fakeS2AServer.start();
|
||||
serverAddress = String.format("localhost:%d", port);
|
||||
}
|
||||
|
||||
@After
|
||||
public void tearDown() {
|
||||
fakeS2AServer.shutdown();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void callS2AServerOnce_getTlsConfiguration_returnsValidResult()
|
||||
throws InterruptedException {
|
||||
ExecutorService executor = Executors.newSingleThreadExecutor();
|
||||
logger.info("Client connecting to: " + serverAddress);
|
||||
ManagedChannel channel =
|
||||
Grpc.newChannelBuilder(serverAddress, InsecureChannelCredentials.create())
|
||||
.executor(executor)
|
||||
.build();
|
||||
|
||||
try {
|
||||
S2AServiceGrpc.S2AServiceStub asyncStub = S2AServiceGrpc.newStub(channel);
|
||||
StreamObserver<SessionReq> requestObserver =
|
||||
asyncStub.setUpSession(
|
||||
new StreamObserver<SessionResp>() {
|
||||
@Override
|
||||
public void onNext(SessionResp resp) {
|
||||
response = resp;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onError(Throwable t) {
|
||||
throw new RuntimeException(t);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onCompleted() {}
|
||||
});
|
||||
try {
|
||||
requestObserver.onNext(
|
||||
SessionReq.newBuilder()
|
||||
.setGetTlsConfigurationReq(
|
||||
GetTlsConfigurationReq.newBuilder()
|
||||
.setConnectionSide(ConnectionSide.CONNECTION_SIDE_CLIENT))
|
||||
.build());
|
||||
} catch (RuntimeException e) {
|
||||
// Cancel the RPC.
|
||||
requestObserver.onError(e);
|
||||
throw e;
|
||||
}
|
||||
// Mark the end of requests.
|
||||
requestObserver.onCompleted();
|
||||
// Wait for receiving to happen.
|
||||
} finally {
|
||||
channel.shutdown();
|
||||
channel.awaitTermination(1, SECONDS);
|
||||
executor.shutdown();
|
||||
executor.awaitTermination(1, SECONDS);
|
||||
}
|
||||
|
||||
SessionResp expected =
|
||||
SessionResp.newBuilder()
|
||||
.setGetTlsConfigurationResp(
|
||||
GetTlsConfigurationResp.newBuilder()
|
||||
.setClientTlsConfiguration(
|
||||
GetTlsConfigurationResp.ClientTlsConfiguration.newBuilder()
|
||||
.addCertificateChain(FakeWriter.LEAF_CERT)
|
||||
.addCertificateChain(FakeWriter.INTERMEDIATE_CERT_2)
|
||||
.addCertificateChain(FakeWriter.INTERMEDIATE_CERT_1)
|
||||
.setMinTlsVersion(TLSVersion.TLS_VERSION_1_3)
|
||||
.setMaxTlsVersion(TLSVersion.TLS_VERSION_1_3)
|
||||
.addCiphersuites(
|
||||
Ciphersuite.CIPHERSUITE_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256)
|
||||
.addCiphersuites(
|
||||
Ciphersuite.CIPHERSUITE_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384)
|
||||
.addCiphersuites(
|
||||
Ciphersuite.CIPHERSUITE_ECDHE_ECDSA_WITH_CHACHA20_POLY1305_SHA256)))
|
||||
.build();
|
||||
assertThat(response).ignoringRepeatedFieldOrder().isEqualTo(expected);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void callS2AServerOnce_validatePeerCertifiate_returnsValidResult()
|
||||
throws InterruptedException {
|
||||
ExecutorService executor = Executors.newSingleThreadExecutor();
|
||||
logger.info("Client connecting to: " + serverAddress);
|
||||
ManagedChannel channel =
|
||||
Grpc.newChannelBuilder(serverAddress, InsecureChannelCredentials.create())
|
||||
.executor(executor)
|
||||
.build();
|
||||
|
||||
try {
|
||||
S2AServiceGrpc.S2AServiceStub asyncStub = S2AServiceGrpc.newStub(channel);
|
||||
StreamObserver<SessionReq> requestObserver =
|
||||
asyncStub.setUpSession(
|
||||
new StreamObserver<SessionResp>() {
|
||||
@Override
|
||||
public void onNext(SessionResp resp) {
|
||||
response = resp;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onError(Throwable t) {
|
||||
throw new RuntimeException(t);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onCompleted() {}
|
||||
});
|
||||
try {
|
||||
requestObserver.onNext(
|
||||
SessionReq.newBuilder()
|
||||
.setValidatePeerCertificateChainReq(
|
||||
ValidatePeerCertificateChainReq.newBuilder()
|
||||
.setMode(VerificationMode.UNSPECIFIED)
|
||||
.setClientPeer(
|
||||
ValidatePeerCertificateChainReq.ClientPeer.newBuilder()
|
||||
.addAllCertificateChain(FAKE_CERT_DER_CHAIN)))
|
||||
.build());
|
||||
} catch (RuntimeException e) {
|
||||
// Cancel the RPC.
|
||||
requestObserver.onError(e);
|
||||
throw e;
|
||||
}
|
||||
// Mark the end of requests.
|
||||
requestObserver.onCompleted();
|
||||
// Wait for receiving to happen.
|
||||
} finally {
|
||||
channel.shutdown();
|
||||
channel.awaitTermination(1, SECONDS);
|
||||
executor.shutdown();
|
||||
executor.awaitTermination(1, SECONDS);
|
||||
}
|
||||
|
||||
SessionResp expected =
|
||||
SessionResp.newBuilder()
|
||||
.setValidatePeerCertificateChainResp(
|
||||
ValidatePeerCertificateChainResp.newBuilder()
|
||||
.setValidationResult(ValidatePeerCertificateChainResp.ValidationResult.SUCCESS))
|
||||
.build();
|
||||
assertThat(response).ignoringRepeatedFieldOrder().isEqualTo(expected);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void callS2AServerRepeatedly_returnsValidResult() throws InterruptedException {
|
||||
final int numberOfRequests = 10;
|
||||
ExecutorService executor = Executors.newSingleThreadExecutor();
|
||||
logger.info("Client connecting to: " + serverAddress);
|
||||
ManagedChannel channel =
|
||||
Grpc.newChannelBuilder(serverAddress, InsecureChannelCredentials.create())
|
||||
.executor(executor)
|
||||
.build();
|
||||
|
||||
try {
|
||||
S2AServiceGrpc.S2AServiceStub asyncStub = S2AServiceGrpc.newStub(channel);
|
||||
CountDownLatch finishLatch = new CountDownLatch(1);
|
||||
StreamObserver<SessionReq> requestObserver =
|
||||
asyncStub.setUpSession(
|
||||
new StreamObserver<SessionResp>() {
|
||||
private int expectedNumberOfReplies = numberOfRequests;
|
||||
|
||||
@Override
|
||||
public void onNext(SessionResp reply) {
|
||||
System.out.println("Received a message from the S2AService service.");
|
||||
expectedNumberOfReplies -= 1;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onError(Throwable t) {
|
||||
finishLatch.countDown();
|
||||
if (expectedNumberOfReplies != 0) {
|
||||
throw new RuntimeException(t);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onCompleted() {
|
||||
finishLatch.countDown();
|
||||
if (expectedNumberOfReplies != 0) {
|
||||
throw new RuntimeException();
|
||||
}
|
||||
}
|
||||
});
|
||||
try {
|
||||
for (int i = 0; i < numberOfRequests; i++) {
|
||||
requestObserver.onNext(SessionReq.getDefaultInstance());
|
||||
}
|
||||
} catch (RuntimeException e) {
|
||||
// Cancel the RPC.
|
||||
requestObserver.onError(e);
|
||||
throw e;
|
||||
}
|
||||
// Mark the end of requests.
|
||||
requestObserver.onCompleted();
|
||||
// Wait for receiving to happen.
|
||||
if (!finishLatch.await(10, SECONDS)) {
|
||||
throw new RuntimeException();
|
||||
}
|
||||
} finally {
|
||||
channel.shutdown();
|
||||
channel.awaitTermination(1, SECONDS);
|
||||
executor.shutdown();
|
||||
executor.awaitTermination(1, SECONDS);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,363 @@
|
|||
/*
|
||||
* Copyright 2024 The gRPC Authors
|
||||
*
|
||||
* 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 io.grpc.s2a.handshaker;
|
||||
|
||||
import static io.grpc.s2a.handshaker.TLSVersion.TLS_VERSION_1_2;
|
||||
import static io.grpc.s2a.handshaker.TLSVersion.TLS_VERSION_1_3;
|
||||
|
||||
import com.google.common.collect.ImmutableMap;
|
||||
import com.google.errorprone.annotations.CanIgnoreReturnValue;
|
||||
import com.google.protobuf.ByteString;
|
||||
import io.grpc.stub.StreamObserver;
|
||||
import java.io.IOException;
|
||||
import java.security.KeyFactory;
|
||||
import java.security.NoSuchAlgorithmException;
|
||||
import java.security.PrivateKey;
|
||||
import java.security.Signature;
|
||||
import java.security.spec.InvalidKeySpecException;
|
||||
import java.security.spec.PKCS8EncodedKeySpec;
|
||||
import java.util.Base64;
|
||||
|
||||
/** A fake Writer Class to mock the behavior of S2A server. */
|
||||
final class FakeWriter implements StreamObserver<SessionReq> {
|
||||
/** Fake behavior of S2A service. */
|
||||
enum Behavior {
|
||||
OK_STATUS,
|
||||
EMPTY_RESPONSE,
|
||||
ERROR_STATUS,
|
||||
ERROR_RESPONSE,
|
||||
COMPLETE_STATUS,
|
||||
BAD_TLS_VERSION_RESPONSE,
|
||||
}
|
||||
|
||||
enum VerificationResult {
|
||||
UNSPECIFIED,
|
||||
SUCCESS,
|
||||
FAILURE
|
||||
}
|
||||
|
||||
public static final String LEAF_CERT =
|
||||
"-----BEGIN CERTIFICATE-----\n"
|
||||
+ "MIICkDCCAjagAwIBAgIUSAtcrPhNNs1zxv51lIfGOVtkw6QwCgYIKoZIzj0EAwIw\n"
|
||||
+ "QTEXMBUGA1UECgwOc2VjdXJpdHktcmVhbG0xEDAOBgNVBAsMB2NvbnRleHQxFDAS\n"
|
||||
+ "BgorBgEEAdZ5AggBDAQyMDIyMCAXDTIzMDcxNDIyMzYwNFoYDzIwNTAxMTI5MjIz\n"
|
||||
+ "NjA0WjARMQ8wDQYDVQQDDAZ1bnVzZWQwWTATBgcqhkjOPQIBBggqhkjOPQMBBwNC\n"
|
||||
+ "AAQGFlJpLxJMh4HuUm0DKjnUF7larH3tJvroQ12xpk+pPKQepn4ILoq9lZ8Xd3jz\n"
|
||||
+ "U98eDRXG5f4VjnX98DDHE4Ido4IBODCCATQwDgYDVR0PAQH/BAQDAgeAMCAGA1Ud\n"
|
||||
+ "JQEB/wQWMBQGCCsGAQUFBwMCBggrBgEFBQcDATAMBgNVHRMBAf8EAjAAMIGxBgNV\n"
|
||||
+ "HREBAf8EgaYwgaOGSnNwaWZmZTovL3NpZ25lci1yb2xlLmNvbnRleHQuc2VjdXJp\n"
|
||||
+ "dHktcmVhbG0ucHJvZC5nb29nbGUuY29tL3JvbGUvbGVhZi1yb2xlgjNzaWduZXIt\n"
|
||||
+ "cm9sZS5jb250ZXh0LnNlY3VyaXR5LXJlYWxtLnByb2Quc3BpZmZlLmdvb2eCIGZx\n"
|
||||
+ "ZG4tb2YtdGhlLW5vZGUucHJvZC5nb29nbGUuY29tMB0GA1UdDgQWBBSWSd5Fw6dI\n"
|
||||
+ "TGpt0m1Uxwf0iKqebzAfBgNVHSMEGDAWgBRm5agVVdpWfRZKM7u6OMuzHhqPcDAK\n"
|
||||
+ "BggqhkjOPQQDAgNIADBFAiB0sjRPSYy2eFq8Y0vQ8QN4AZ2NMajskvxnlifu7O4U\n"
|
||||
+ "RwIhANTh5Fkyx2nMYFfyl+W45dY8ODTw3HnlZ4b51hTAdkWl\n"
|
||||
+ "-----END CERTIFICATE-----";
|
||||
public static final String INTERMEDIATE_CERT_2 =
|
||||
"-----BEGIN CERTIFICATE-----\n"
|
||||
+ "MIICQjCCAeigAwIBAgIUKxXRDlnWXefNV5lj5CwhDuXEq7MwCgYIKoZIzj0EAwIw\n"
|
||||
+ "OzEXMBUGA1UECgwOc2VjdXJpdHktcmVhbG0xEDAOBgNVBAsMB2NvbnRleHQxDjAM\n"
|
||||
+ "BgNVBAMMBTEyMzQ1MCAXDTIzMDcxNDIyMzYwNFoYDzIwNTAxMTI5MjIzNjA0WjBB\n"
|
||||
+ "MRcwFQYDVQQKDA5zZWN1cml0eS1yZWFsbTEQMA4GA1UECwwHY29udGV4dDEUMBIG\n"
|
||||
+ "CisGAQQB1nkCCAEMBDIwMjIwWTATBgcqhkjOPQIBBggqhkjOPQMBBwNCAAT/Zu7x\n"
|
||||
+ "UYVyg+T/vg2H+y4I6t36Kc4qxD0eqqZjRLYBVKkUQHxBqc14t0DpoROMYQCNd4DF\n"
|
||||
+ "pcxv/9m6DaJbRk6Ao4HBMIG+MA4GA1UdDwEB/wQEAwIBBjASBgNVHRMBAf8ECDAG\n"
|
||||
+ "AQH/AgEBMFgGA1UdHgEB/wROMEygSjA1gjNzaWduZXItcm9sZS5jb250ZXh0LnNl\n"
|
||||
+ "Y3VyaXR5LXJlYWxtLnByb2Quc3BpZmZlLmdvb2cwEYIPcHJvZC5nb29nbGUuY29t\n"
|
||||
+ "MB0GA1UdDgQWBBRm5agVVdpWfRZKM7u6OMuzHhqPcDAfBgNVHSMEGDAWgBQcjNAh\n"
|
||||
+ "SCHTj+BW8KrzSSLo2ASEgjAKBggqhkjOPQQDAgNIADBFAiEA6KyGd9VxXDZceMZG\n"
|
||||
+ "IsbC40rtunFjLYI0mjZw9RcRWx8CIHCIiIHxafnDaCi+VB99NZfzAdu37g6pJptB\n"
|
||||
+ "gjIY71MO\n"
|
||||
+ "-----END CERTIFICATE-----";
|
||||
public static final String INTERMEDIATE_CERT_1 =
|
||||
"-----BEGIN CERTIFICATE-----\n"
|
||||
+ "MIICODCCAd6gAwIBAgIUXtZECORWRSKnS9rRTJYkiALUXswwCgYIKoZIzj0EAwIw\n"
|
||||
+ "NzEXMBUGA1UECgwOc2VjdXJpdHktcmVhbG0xDTALBgNVBAsMBHJvb3QxDTALBgNV\n"
|
||||
+ "BAMMBDEyMzQwIBcNMjMwNzE0MjIzNjA0WhgPMjA1MDExMjkyMjM2MDRaMDsxFzAV\n"
|
||||
+ "BgNVBAoMDnNlY3VyaXR5LXJlYWxtMRAwDgYDVQQLDAdjb250ZXh0MQ4wDAYDVQQD\n"
|
||||
+ "DAUxMjM0NTBZMBMGByqGSM49AgEGCCqGSM49AwEHA0IABAycVTZrjockbpD59f1a\n"
|
||||
+ "4l1SNL7nSyXz66Guz4eDveQqLmaMBg7vpACfO4CtiAGnolHEffuRtSkdM434m5En\n"
|
||||
+ "bXCjgcEwgb4wDgYDVR0PAQH/BAQDAgEGMBIGA1UdEwEB/wQIMAYBAf8CAQIwWAYD\n"
|
||||
+ "VR0eAQH/BE4wTKBKMDWCM3NpZ25lci1yb2xlLmNvbnRleHQuc2VjdXJpdHktcmVh\n"
|
||||
+ "bG0ucHJvZC5zcGlmZmUuZ29vZzARgg9wcm9kLmdvb2dsZS5jb20wHQYDVR0OBBYE\n"
|
||||
+ "FByM0CFIIdOP4FbwqvNJIujYBISCMB8GA1UdIwQYMBaAFMX+vebuj/lYfYEC23IA\n"
|
||||
+ "8HoIW0HsMAoGCCqGSM49BAMCA0gAMEUCIQCfxeXEBd7UPmeImT16SseCRu/6cHxl\n"
|
||||
+ "kTDsq9sKZ+eXBAIgA+oViAVOUhUQO1/6Mjlczg8NmMy2vNtG4V/7g9dMMVU=\n"
|
||||
+ "-----END CERTIFICATE-----";
|
||||
|
||||
private static final String PRIVATE_KEY =
|
||||
"MIGHAgEAMBMGByqGSM49AgEGCCqGSM49AwEHBG0wawIBAQQgqA2U0ld1OOHLMXWf"
|
||||
+ "uyN4GSaqhhudEIaKkll3rdIq0M+hRANCAAQGFlJpLxJMh4HuUm0DKjnUF7larH3t"
|
||||
+ "JvroQ12xpk+pPKQepn4ILoq9lZ8Xd3jzU98eDRXG5f4VjnX98DDHE4Id";
|
||||
private static final ImmutableMap<SignatureAlgorithm, String>
|
||||
ALGORITHM_TO_SIGNATURE_INSTANCE_IDENTIFIER =
|
||||
ImmutableMap.of(
|
||||
SignatureAlgorithm.S2A_SSL_SIGN_ECDSA_SECP256R1_SHA256,
|
||||
"SHA256withECDSA",
|
||||
SignatureAlgorithm.S2A_SSL_SIGN_ECDSA_SECP384R1_SHA384,
|
||||
"SHA384withECDSA",
|
||||
SignatureAlgorithm.S2A_SSL_SIGN_ECDSA_SECP521R1_SHA512,
|
||||
"SHA512withECDSA");
|
||||
|
||||
private boolean fakeWriterClosed = false;
|
||||
private Behavior behavior = Behavior.OK_STATUS;
|
||||
private StreamObserver<SessionResp> reader;
|
||||
private VerificationResult verificationResult = VerificationResult.UNSPECIFIED;
|
||||
private String failureReason;
|
||||
private PrivateKey privateKey;
|
||||
|
||||
@CanIgnoreReturnValue
|
||||
FakeWriter setReader(StreamObserver<SessionResp> reader) {
|
||||
this.reader = reader;
|
||||
return this;
|
||||
}
|
||||
|
||||
@CanIgnoreReturnValue
|
||||
FakeWriter setBehavior(Behavior behavior) {
|
||||
this.behavior = behavior;
|
||||
return this;
|
||||
}
|
||||
|
||||
@CanIgnoreReturnValue
|
||||
FakeWriter setVerificationResult(VerificationResult verificationResult) {
|
||||
this.verificationResult = verificationResult;
|
||||
return this;
|
||||
}
|
||||
|
||||
@CanIgnoreReturnValue
|
||||
FakeWriter setFailureReason(String failureReason) {
|
||||
this.failureReason = failureReason;
|
||||
return this;
|
||||
}
|
||||
|
||||
@CanIgnoreReturnValue
|
||||
FakeWriter initializePrivateKey() throws InvalidKeySpecException, NoSuchAlgorithmException {
|
||||
privateKey =
|
||||
KeyFactory.getInstance("EC")
|
||||
.generatePrivate(new PKCS8EncodedKeySpec(Base64.getDecoder().decode(PRIVATE_KEY)));
|
||||
return this;
|
||||
}
|
||||
|
||||
@CanIgnoreReturnValue
|
||||
FakeWriter resetPrivateKey() {
|
||||
privateKey = null;
|
||||
return this;
|
||||
}
|
||||
|
||||
void sendUnexpectedResponse() {
|
||||
reader.onNext(SessionResp.getDefaultInstance());
|
||||
}
|
||||
|
||||
void sendIoError() {
|
||||
reader.onError(new IOException("Intended ERROR from FakeWriter."));
|
||||
}
|
||||
|
||||
void sendGetTlsConfigResp() {
|
||||
reader.onNext(
|
||||
SessionResp.newBuilder()
|
||||
.setGetTlsConfigurationResp(
|
||||
GetTlsConfigurationResp.newBuilder()
|
||||
.setClientTlsConfiguration(
|
||||
GetTlsConfigurationResp.ClientTlsConfiguration.newBuilder()
|
||||
.addCertificateChain(LEAF_CERT)
|
||||
.addCertificateChain(INTERMEDIATE_CERT_2)
|
||||
.addCertificateChain(INTERMEDIATE_CERT_1)
|
||||
.setMinTlsVersion(TLS_VERSION_1_3)
|
||||
.setMaxTlsVersion(TLS_VERSION_1_3)
|
||||
.addCiphersuites(
|
||||
Ciphersuite.CIPHERSUITE_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256)
|
||||
.addCiphersuites(
|
||||
Ciphersuite.CIPHERSUITE_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384)
|
||||
.addCiphersuites(
|
||||
Ciphersuite.CIPHERSUITE_ECDHE_ECDSA_WITH_CHACHA20_POLY1305_SHA256)))
|
||||
.build());
|
||||
}
|
||||
|
||||
boolean isFakeWriterClosed() {
|
||||
return fakeWriterClosed;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onNext(SessionReq sessionReq) {
|
||||
switch (behavior) {
|
||||
case OK_STATUS:
|
||||
reader.onNext(handleResponse(sessionReq));
|
||||
break;
|
||||
case EMPTY_RESPONSE:
|
||||
reader.onNext(SessionResp.getDefaultInstance());
|
||||
break;
|
||||
case ERROR_STATUS:
|
||||
reader.onNext(
|
||||
SessionResp.newBuilder()
|
||||
.setStatus(
|
||||
Status.newBuilder()
|
||||
.setCode(1)
|
||||
.setDetails("Intended ERROR Status from FakeWriter."))
|
||||
.build());
|
||||
break;
|
||||
case ERROR_RESPONSE:
|
||||
reader.onError(new S2AConnectionException("Intended ERROR from FakeWriter."));
|
||||
break;
|
||||
case COMPLETE_STATUS:
|
||||
reader.onCompleted();
|
||||
break;
|
||||
case BAD_TLS_VERSION_RESPONSE:
|
||||
reader.onNext(
|
||||
SessionResp.newBuilder()
|
||||
.setGetTlsConfigurationResp(
|
||||
GetTlsConfigurationResp.newBuilder()
|
||||
.setClientTlsConfiguration(
|
||||
GetTlsConfigurationResp.ClientTlsConfiguration.newBuilder()
|
||||
.addCertificateChain(LEAF_CERT)
|
||||
.addCertificateChain(INTERMEDIATE_CERT_2)
|
||||
.addCertificateChain(INTERMEDIATE_CERT_1)
|
||||
.setMinTlsVersion(TLS_VERSION_1_3)
|
||||
.setMaxTlsVersion(TLS_VERSION_1_2)))
|
||||
.build());
|
||||
break;
|
||||
default:
|
||||
reader.onNext(handleResponse(sessionReq));
|
||||
}
|
||||
}
|
||||
|
||||
SessionResp handleResponse(SessionReq sessionReq) {
|
||||
if (sessionReq.hasGetTlsConfigurationReq()) {
|
||||
return handleGetTlsConfigurationReq(sessionReq.getGetTlsConfigurationReq());
|
||||
}
|
||||
|
||||
if (sessionReq.hasValidatePeerCertificateChainReq()) {
|
||||
return handleValidatePeerCertificateChainReq(sessionReq.getValidatePeerCertificateChainReq());
|
||||
}
|
||||
|
||||
if (sessionReq.hasOffloadPrivateKeyOperationReq()) {
|
||||
return handleOffloadPrivateKeyOperationReq(sessionReq.getOffloadPrivateKeyOperationReq());
|
||||
}
|
||||
|
||||
return SessionResp.newBuilder()
|
||||
.setStatus(
|
||||
Status.newBuilder().setCode(255).setDetails("No supported operation designated."))
|
||||
.build();
|
||||
}
|
||||
|
||||
private SessionResp handleGetTlsConfigurationReq(GetTlsConfigurationReq req) {
|
||||
if (!req.getConnectionSide().equals(ConnectionSide.CONNECTION_SIDE_CLIENT)) {
|
||||
return SessionResp.newBuilder()
|
||||
.setStatus(
|
||||
Status.newBuilder()
|
||||
.setCode(255)
|
||||
.setDetails("No TLS configuration for the server side."))
|
||||
.build();
|
||||
}
|
||||
return SessionResp.newBuilder()
|
||||
.setGetTlsConfigurationResp(
|
||||
GetTlsConfigurationResp.newBuilder()
|
||||
.setClientTlsConfiguration(
|
||||
GetTlsConfigurationResp.ClientTlsConfiguration.newBuilder()
|
||||
.addCertificateChain(LEAF_CERT)
|
||||
.addCertificateChain(INTERMEDIATE_CERT_2)
|
||||
.addCertificateChain(INTERMEDIATE_CERT_1)
|
||||
.setMinTlsVersion(TLS_VERSION_1_3)
|
||||
.setMaxTlsVersion(TLS_VERSION_1_3)
|
||||
.addCiphersuites(
|
||||
Ciphersuite.CIPHERSUITE_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256)
|
||||
.addCiphersuites(
|
||||
Ciphersuite.CIPHERSUITE_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384)
|
||||
.addCiphersuites(
|
||||
Ciphersuite.CIPHERSUITE_ECDHE_ECDSA_WITH_CHACHA20_POLY1305_SHA256)))
|
||||
.build();
|
||||
}
|
||||
|
||||
private SessionResp handleValidatePeerCertificateChainReq(ValidatePeerCertificateChainReq req) {
|
||||
if (verifyValidatePeerCertificateChainReq(req)
|
||||
&& verificationResult == VerificationResult.SUCCESS) {
|
||||
return SessionResp.newBuilder()
|
||||
.setValidatePeerCertificateChainResp(
|
||||
ValidatePeerCertificateChainResp.newBuilder()
|
||||
.setValidationResult(ValidatePeerCertificateChainResp.ValidationResult.SUCCESS))
|
||||
.build();
|
||||
}
|
||||
return SessionResp.newBuilder()
|
||||
.setValidatePeerCertificateChainResp(
|
||||
ValidatePeerCertificateChainResp.newBuilder()
|
||||
.setValidationResult(
|
||||
verificationResult == VerificationResult.FAILURE
|
||||
? ValidatePeerCertificateChainResp.ValidationResult.FAILURE
|
||||
: ValidatePeerCertificateChainResp.ValidationResult.UNSPECIFIED)
|
||||
.setValidationDetails(failureReason))
|
||||
.build();
|
||||
}
|
||||
|
||||
private boolean verifyValidatePeerCertificateChainReq(ValidatePeerCertificateChainReq req) {
|
||||
if (req.getMode() != ValidatePeerCertificateChainReq.VerificationMode.UNSPECIFIED) {
|
||||
return false;
|
||||
}
|
||||
if (req.getClientPeer().getCertificateChainCount() > 0) {
|
||||
return true;
|
||||
}
|
||||
if (req.getServerPeer().getCertificateChainCount() > 0
|
||||
&& !req.getServerPeer().getServerHostname().isEmpty()) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
private SessionResp handleOffloadPrivateKeyOperationReq(OffloadPrivateKeyOperationReq req) {
|
||||
if (privateKey == null) {
|
||||
return SessionResp.newBuilder()
|
||||
.setStatus(Status.newBuilder().setCode(255).setDetails("No Private Key available."))
|
||||
.build();
|
||||
}
|
||||
String signatureIdentifier =
|
||||
ALGORITHM_TO_SIGNATURE_INSTANCE_IDENTIFIER.get(req.getSignatureAlgorithm());
|
||||
if (signatureIdentifier == null) {
|
||||
return SessionResp.newBuilder()
|
||||
.setStatus(
|
||||
Status.newBuilder()
|
||||
.setCode(255)
|
||||
.setDetails("Only ECDSA key algorithms are supported."))
|
||||
.build();
|
||||
}
|
||||
|
||||
byte[] signature;
|
||||
try {
|
||||
Signature sig = Signature.getInstance(signatureIdentifier);
|
||||
sig.initSign(privateKey);
|
||||
sig.update(req.getRawBytes().toByteArray());
|
||||
signature = sig.sign();
|
||||
} catch (Exception e) {
|
||||
return SessionResp.newBuilder()
|
||||
.setStatus(Status.newBuilder().setCode(255).setDetails(e.getMessage()))
|
||||
.build();
|
||||
}
|
||||
|
||||
return SessionResp.newBuilder()
|
||||
.setOffloadPrivateKeyOperationResp(
|
||||
OffloadPrivateKeyOperationResp.newBuilder().setOutBytes(ByteString.copyFrom(signature)))
|
||||
.build();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onError(Throwable t) {
|
||||
throw new UnsupportedOperationException("onError is not supported by FakeWriter.");
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onCompleted() {
|
||||
fakeWriterClosed = true;
|
||||
reader.onCompleted();
|
||||
}
|
||||
}
|
|
@ -0,0 +1,61 @@
|
|||
/*
|
||||
* Copyright 2024 The gRPC Authors
|
||||
*
|
||||
* 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 io.grpc.s2a.handshaker;
|
||||
|
||||
import com.google.common.truth.Expect;
|
||||
import io.grpc.s2a.handshaker.S2AIdentity;
|
||||
import io.grpc.s2a.handshaker.tokenmanager.SingleTokenFetcher;
|
||||
import java.util.Optional;
|
||||
import org.junit.BeforeClass;
|
||||
import org.junit.Rule;
|
||||
import org.junit.Test;
|
||||
import org.junit.runner.RunWith;
|
||||
import org.junit.runners.JUnit4;
|
||||
|
||||
/** Unit tests for {@link GetAuthenticationMechanisms}. */
|
||||
@RunWith(JUnit4.class)
|
||||
public final class GetAuthenticationMechanismsTest {
|
||||
@Rule public final Expect expect = Expect.create();
|
||||
private static final String TOKEN = "access_token";
|
||||
|
||||
@BeforeClass
|
||||
public static void setUpClass() {
|
||||
// Set the token that the client will use to authenticate to the S2A.
|
||||
SingleTokenFetcher.setAccessToken(TOKEN);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void getAuthMechanisms_emptyIdentity_success() {
|
||||
expect
|
||||
.that(GetAuthenticationMechanisms.getAuthMechanism(Optional.empty()))
|
||||
.isEqualTo(
|
||||
Optional.of(AuthenticationMechanism.newBuilder().setToken("access_token").build()));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void getAuthMechanisms_nonEmptyIdentity_success() {
|
||||
S2AIdentity fakeIdentity = S2AIdentity.fromSpiffeId("fake-spiffe-id");
|
||||
expect
|
||||
.that(GetAuthenticationMechanisms.getAuthMechanism(Optional.of(fakeIdentity)))
|
||||
.isEqualTo(
|
||||
Optional.of(
|
||||
AuthenticationMechanism.newBuilder()
|
||||
.setIdentity(fakeIdentity.getIdentity())
|
||||
.setToken("access_token")
|
||||
.build()));
|
||||
}
|
||||
}
|
|
@ -0,0 +1,320 @@
|
|||
/*
|
||||
* Copyright 2024 The gRPC Authors
|
||||
*
|
||||
* 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 io.grpc.s2a.handshaker;
|
||||
|
||||
import static com.google.common.truth.Truth.assertThat;
|
||||
import static java.nio.charset.StandardCharsets.UTF_8;
|
||||
import static java.util.concurrent.TimeUnit.SECONDS;
|
||||
|
||||
import io.grpc.ChannelCredentials;
|
||||
import io.grpc.Grpc;
|
||||
import io.grpc.ManagedChannel;
|
||||
import io.grpc.Server;
|
||||
import io.grpc.ServerBuilder;
|
||||
import io.grpc.ServerCredentials;
|
||||
import io.grpc.TlsServerCredentials;
|
||||
import io.grpc.benchmarks.Utils;
|
||||
import io.grpc.netty.GrpcSslContexts;
|
||||
import io.grpc.netty.NettyServerBuilder;
|
||||
import io.grpc.s2a.MtlsToS2AChannelCredentials;
|
||||
import io.grpc.s2a.S2AChannelCredentials;
|
||||
import io.grpc.s2a.handshaker.FakeS2AServer;
|
||||
import io.grpc.stub.StreamObserver;
|
||||
import io.grpc.testing.protobuf.SimpleRequest;
|
||||
import io.grpc.testing.protobuf.SimpleResponse;
|
||||
import io.grpc.testing.protobuf.SimpleServiceGrpc;
|
||||
import io.netty.handler.ssl.ClientAuth;
|
||||
import io.netty.handler.ssl.OpenSslSessionContext;
|
||||
import io.netty.handler.ssl.SslContext;
|
||||
import io.netty.handler.ssl.SslContextBuilder;
|
||||
import io.netty.handler.ssl.SslProvider;
|
||||
import java.io.ByteArrayInputStream;
|
||||
import java.io.File;
|
||||
import java.util.concurrent.ExecutorService;
|
||||
import java.util.concurrent.Executors;
|
||||
import java.util.logging.Level;
|
||||
import java.util.logging.Logger;
|
||||
import javax.net.ssl.SSLException;
|
||||
import javax.net.ssl.SSLSessionContext;
|
||||
import org.junit.After;
|
||||
import org.junit.Before;
|
||||
import org.junit.Test;
|
||||
import org.junit.runner.RunWith;
|
||||
import org.junit.runners.JUnit4;
|
||||
|
||||
@RunWith(JUnit4.class)
|
||||
public final class IntegrationTest {
|
||||
private static final Logger logger = Logger.getLogger(FakeS2AServer.class.getName());
|
||||
|
||||
private static final String CERT_CHAIN =
|
||||
"-----BEGIN CERTIFICATE-----\n"
|
||||
+ "MIICkDCCAjagAwIBAgIUSAtcrPhNNs1zxv51lIfGOVtkw6QwCgYIKoZIzj0EAwIw\n"
|
||||
+ "QTEXMBUGA1UECgwOc2VjdXJpdHktcmVhbG0xEDAOBgNVBAsMB2NvbnRleHQxFDAS\n"
|
||||
+ "BgorBgEEAdZ5AggBDAQyMDIyMCAXDTIzMDcxNDIyMzYwNFoYDzIwNTAxMTI5MjIz\n"
|
||||
+ "NjA0WjARMQ8wDQYDVQQDDAZ1bnVzZWQwWTATBgcqhkjOPQIBBggqhkjOPQMBBwNC\n"
|
||||
+ "AAQGFlJpLxJMh4HuUm0DKjnUF7larH3tJvroQ12xpk+pPKQepn4ILoq9lZ8Xd3jz\n"
|
||||
+ "U98eDRXG5f4VjnX98DDHE4Ido4IBODCCATQwDgYDVR0PAQH/BAQDAgeAMCAGA1Ud\n"
|
||||
+ "JQEB/wQWMBQGCCsGAQUFBwMCBggrBgEFBQcDATAMBgNVHRMBAf8EAjAAMIGxBgNV\n"
|
||||
+ "HREBAf8EgaYwgaOGSnNwaWZmZTovL3NpZ25lci1yb2xlLmNvbnRleHQuc2VjdXJp\n"
|
||||
+ "dHktcmVhbG0ucHJvZC5nb29nbGUuY29tL3JvbGUvbGVhZi1yb2xlgjNzaWduZXIt\n"
|
||||
+ "cm9sZS5jb250ZXh0LnNlY3VyaXR5LXJlYWxtLnByb2Quc3BpZmZlLmdvb2eCIGZx\n"
|
||||
+ "ZG4tb2YtdGhlLW5vZGUucHJvZC5nb29nbGUuY29tMB0GA1UdDgQWBBSWSd5Fw6dI\n"
|
||||
+ "TGpt0m1Uxwf0iKqebzAfBgNVHSMEGDAWgBRm5agVVdpWfRZKM7u6OMuzHhqPcDAK\n"
|
||||
+ "BggqhkjOPQQDAgNIADBFAiB0sjRPSYy2eFq8Y0vQ8QN4AZ2NMajskvxnlifu7O4U\n"
|
||||
+ "RwIhANTh5Fkyx2nMYFfyl+W45dY8ODTw3HnlZ4b51hTAdkWl\n"
|
||||
+ "-----END CERTIFICATE-----\n"
|
||||
+ "-----BEGIN CERTIFICATE-----\n"
|
||||
+ "MIICQjCCAeigAwIBAgIUKxXRDlnWXefNV5lj5CwhDuXEq7MwCgYIKoZIzj0EAwIw\n"
|
||||
+ "OzEXMBUGA1UECgwOc2VjdXJpdHktcmVhbG0xEDAOBgNVBAsMB2NvbnRleHQxDjAM\n"
|
||||
+ "BgNVBAMMBTEyMzQ1MCAXDTIzMDcxNDIyMzYwNFoYDzIwNTAxMTI5MjIzNjA0WjBB\n"
|
||||
+ "MRcwFQYDVQQKDA5zZWN1cml0eS1yZWFsbTEQMA4GA1UECwwHY29udGV4dDEUMBIG\n"
|
||||
+ "CisGAQQB1nkCCAEMBDIwMjIwWTATBgcqhkjOPQIBBggqhkjOPQMBBwNCAAT/Zu7x\n"
|
||||
+ "UYVyg+T/vg2H+y4I6t36Kc4qxD0eqqZjRLYBVKkUQHxBqc14t0DpoROMYQCNd4DF\n"
|
||||
+ "pcxv/9m6DaJbRk6Ao4HBMIG+MA4GA1UdDwEB/wQEAwIBBjASBgNVHRMBAf8ECDAG\n"
|
||||
+ "AQH/AgEBMFgGA1UdHgEB/wROMEygSjA1gjNzaWduZXItcm9sZS5jb250ZXh0LnNl\n"
|
||||
+ "Y3VyaXR5LXJlYWxtLnByb2Quc3BpZmZlLmdvb2cwEYIPcHJvZC5nb29nbGUuY29t\n"
|
||||
+ "MB0GA1UdDgQWBBRm5agVVdpWfRZKM7u6OMuzHhqPcDAfBgNVHSMEGDAWgBQcjNAh\n"
|
||||
+ "SCHTj+BW8KrzSSLo2ASEgjAKBggqhkjOPQQDAgNIADBFAiEA6KyGd9VxXDZceMZG\n"
|
||||
+ "IsbC40rtunFjLYI0mjZw9RcRWx8CIHCIiIHxafnDaCi+VB99NZfzAdu37g6pJptB\n"
|
||||
+ "gjIY71MO\n"
|
||||
+ "-----END CERTIFICATE-----\n"
|
||||
+ "-----BEGIN CERTIFICATE-----\n"
|
||||
+ "MIICODCCAd6gAwIBAgIUXtZECORWRSKnS9rRTJYkiALUXswwCgYIKoZIzj0EAwIw\n"
|
||||
+ "NzEXMBUGA1UECgwOc2VjdXJpdHktcmVhbG0xDTALBgNVBAsMBHJvb3QxDTALBgNV\n"
|
||||
+ "BAMMBDEyMzQwIBcNMjMwNzE0MjIzNjA0WhgPMjA1MDExMjkyMjM2MDRaMDsxFzAV\n"
|
||||
+ "BgNVBAoMDnNlY3VyaXR5LXJlYWxtMRAwDgYDVQQLDAdjb250ZXh0MQ4wDAYDVQQD\n"
|
||||
+ "DAUxMjM0NTBZMBMGByqGSM49AgEGCCqGSM49AwEHA0IABAycVTZrjockbpD59f1a\n"
|
||||
+ "4l1SNL7nSyXz66Guz4eDveQqLmaMBg7vpACfO4CtiAGnolHEffuRtSkdM434m5En\n"
|
||||
+ "bXCjgcEwgb4wDgYDVR0PAQH/BAQDAgEGMBIGA1UdEwEB/wQIMAYBAf8CAQIwWAYD\n"
|
||||
+ "VR0eAQH/BE4wTKBKMDWCM3NpZ25lci1yb2xlLmNvbnRleHQuc2VjdXJpdHktcmVh\n"
|
||||
+ "bG0ucHJvZC5zcGlmZmUuZ29vZzARgg9wcm9kLmdvb2dsZS5jb20wHQYDVR0OBBYE\n"
|
||||
+ "FByM0CFIIdOP4FbwqvNJIujYBISCMB8GA1UdIwQYMBaAFMX+vebuj/lYfYEC23IA\n"
|
||||
+ "8HoIW0HsMAoGCCqGSM49BAMCA0gAMEUCIQCfxeXEBd7UPmeImT16SseCRu/6cHxl\n"
|
||||
+ "kTDsq9sKZ+eXBAIgA+oViAVOUhUQO1/6Mjlczg8NmMy2vNtG4V/7g9dMMVU=\n"
|
||||
+ "-----END CERTIFICATE-----";
|
||||
private static final String ROOT_PEM =
|
||||
"-----BEGIN CERTIFICATE-----\n"
|
||||
+ "MIIBtTCCAVqgAwIBAgIUbAe+8OocndQXRBCElLBxBSdfdV8wCgYIKoZIzj0EAwIw\n"
|
||||
+ "NzEXMBUGA1UECgwOc2VjdXJpdHktcmVhbG0xDTALBgNVBAsMBHJvb3QxDTALBgNV\n"
|
||||
+ "BAMMBDEyMzQwIBcNMjMwNzE0MjIzNjA0WhgPMjA1MDExMjkyMjM2MDRaMDcxFzAV\n"
|
||||
+ "BgNVBAoMDnNlY3VyaXR5LXJlYWxtMQ0wCwYDVQQLDARyb290MQ0wCwYDVQQDDAQx\n"
|
||||
+ "MjM0MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEaMY2tBW5r1t0+vhayz0ZoGMF\n"
|
||||
+ "boX/ZmmCmIh0iTWg4madvwNOh74CMVVvDUlXZcuVqZ3vVIX/a7PTFVqUwQlKW6NC\n"
|
||||
+ "MEAwDgYDVR0PAQH/BAQDAgGGMA8GA1UdEwEB/wQFMAMBAf8wHQYDVR0OBBYEFMX+\n"
|
||||
+ "vebuj/lYfYEC23IA8HoIW0HsMAoGCCqGSM49BAMCA0kAMEYCIQDETd27nsUTXKWY\n"
|
||||
+ "CiOno78O09gK95NoTkPU5e2chJYMqAIhALYFAyh7PU5xgFQsN9hiqgsHUc5/pmBG\n"
|
||||
+ "BGjJ1iz8rWGJ\n"
|
||||
+ "-----END CERTIFICATE-----";
|
||||
private static final String PRIVATE_KEY =
|
||||
"-----BEGIN PRIVATE KEY-----\n"
|
||||
+ "MIGHAgEAMBMGByqGSM49AgEGCCqGSM49AwEHBG0wawIBAQQgqA2U0ld1OOHLMXWf\n"
|
||||
+ "uyN4GSaqhhudEIaKkll3rdIq0M+hRANCAAQGFlJpLxJMh4HuUm0DKjnUF7larH3t\n"
|
||||
+ "JvroQ12xpk+pPKQepn4ILoq9lZ8Xd3jzU98eDRXG5f4VjnX98DDHE4Id\n"
|
||||
+ "-----END PRIVATE KEY-----";
|
||||
|
||||
private String s2aAddress;
|
||||
private int s2aPort;
|
||||
private Server s2aServer;
|
||||
private String s2aDelayAddress;
|
||||
private int s2aDelayPort;
|
||||
private Server s2aDelayServer;
|
||||
private String mtlsS2AAddress;
|
||||
private int mtlsS2APort;
|
||||
private Server mtlsS2AServer;
|
||||
private int serverPort;
|
||||
private String serverAddress;
|
||||
private Server server;
|
||||
|
||||
@Before
|
||||
public void setUp() throws Exception {
|
||||
s2aPort = Utils.pickUnusedPort();
|
||||
s2aAddress = "localhost:" + s2aPort;
|
||||
s2aServer = ServerBuilder.forPort(s2aPort).addService(new FakeS2AServer()).build();
|
||||
logger.info("S2A service listening on localhost:" + s2aPort);
|
||||
s2aServer.start();
|
||||
|
||||
mtlsS2APort = Utils.pickUnusedPort();
|
||||
mtlsS2AAddress = "localhost:" + mtlsS2APort;
|
||||
File s2aCert = new File("src/test/resources/server_cert.pem");
|
||||
File s2aKey = new File("src/test/resources/server_key.pem");
|
||||
File rootCert = new File("src/test/resources/root_cert.pem");
|
||||
ServerCredentials s2aCreds =
|
||||
TlsServerCredentials.newBuilder()
|
||||
.keyManager(s2aCert, s2aKey)
|
||||
.trustManager(rootCert)
|
||||
.clientAuth(TlsServerCredentials.ClientAuth.REQUIRE)
|
||||
.build();
|
||||
mtlsS2AServer =
|
||||
NettyServerBuilder.forPort(mtlsS2APort, s2aCreds).addService(new FakeS2AServer()).build();
|
||||
logger.info("mTLS S2A service listening on localhost:" + mtlsS2APort);
|
||||
mtlsS2AServer.start();
|
||||
|
||||
s2aDelayPort = Utils.pickUnusedPort();
|
||||
s2aDelayAddress = "localhost:" + s2aDelayPort;
|
||||
s2aDelayServer = ServerBuilder.forPort(s2aDelayPort).addService(new FakeS2AServer()).build();
|
||||
|
||||
serverPort = Utils.pickUnusedPort();
|
||||
serverAddress = "localhost:" + serverPort;
|
||||
server =
|
||||
NettyServerBuilder.forPort(serverPort)
|
||||
.addService(new SimpleServiceImpl())
|
||||
.sslContext(buildSslContext())
|
||||
.build();
|
||||
logger.info("Simple Service listening on localhost:" + serverPort);
|
||||
server.start();
|
||||
}
|
||||
|
||||
@After
|
||||
public void tearDown() throws Exception {
|
||||
server.awaitTermination(10, SECONDS);
|
||||
server.shutdown();
|
||||
s2aServer.awaitTermination(10, SECONDS);
|
||||
s2aServer.shutdown();
|
||||
s2aDelayServer.awaitTermination(10, SECONDS);
|
||||
s2aDelayServer.shutdown();
|
||||
mtlsS2AServer.awaitTermination(10, SECONDS);
|
||||
mtlsS2AServer.shutdown();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void clientCommunicateUsingS2ACredentials_succeeds() throws Exception {
|
||||
ExecutorService executor = Executors.newSingleThreadExecutor();
|
||||
ChannelCredentials credentials =
|
||||
S2AChannelCredentials.createBuilder(s2aAddress).setLocalSpiffeId("test-spiffe-id").build();
|
||||
ManagedChannel channel =
|
||||
Grpc.newChannelBuilder(serverAddress, credentials).executor(executor).build();
|
||||
|
||||
assertThat(doUnaryRpc(executor, channel)).isTrue();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void clientCommunicateUsingS2ACredentialsNoLocalIdentity_succeeds() throws Exception {
|
||||
ExecutorService executor = Executors.newSingleThreadExecutor();
|
||||
ChannelCredentials credentials = S2AChannelCredentials.createBuilder(s2aAddress).build();
|
||||
ManagedChannel channel =
|
||||
Grpc.newChannelBuilder(serverAddress, credentials).executor(executor).build();
|
||||
|
||||
assertThat(doUnaryRpc(executor, channel)).isTrue();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void clientCommunicateUsingMtlsToS2ACredentials_succeeds() throws Exception {
|
||||
ExecutorService executor = Executors.newSingleThreadExecutor();
|
||||
ChannelCredentials credentials =
|
||||
MtlsToS2AChannelCredentials.createBuilder(
|
||||
/* s2aAddress= */ mtlsS2AAddress,
|
||||
/* privateKeyPath= */ "src/test/resources/client_key.pem",
|
||||
/* certChainPath= */ "src/test/resources/client_cert.pem",
|
||||
/* trustBundlePath= */ "src/test/resources/root_cert.pem")
|
||||
.build()
|
||||
.setLocalSpiffeId("test-spiffe-id")
|
||||
.build();
|
||||
ManagedChannel channel =
|
||||
Grpc.newChannelBuilder(serverAddress, credentials).executor(executor).build();
|
||||
|
||||
assertThat(doUnaryRpc(executor, channel)).isTrue();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void clientCommunicateUsingS2ACredentials_s2AdelayStart_succeeds() throws Exception {
|
||||
DoUnaryRpc doUnaryRpc = new DoUnaryRpc();
|
||||
doUnaryRpc.start();
|
||||
Thread.sleep(2000);
|
||||
s2aDelayServer.start();
|
||||
doUnaryRpc.join();
|
||||
}
|
||||
|
||||
private class DoUnaryRpc extends Thread {
|
||||
@Override
|
||||
public void run() {
|
||||
ExecutorService executor = Executors.newSingleThreadExecutor();
|
||||
ChannelCredentials credentials = S2AChannelCredentials.createBuilder(s2aDelayAddress).build();
|
||||
ManagedChannel channel =
|
||||
Grpc.newChannelBuilder(serverAddress, credentials).executor(executor).build();
|
||||
boolean result = false;
|
||||
try {
|
||||
result = doUnaryRpc(executor, channel);
|
||||
} catch (InterruptedException e) {
|
||||
logger.log(Level.SEVERE, "Failed to do unary rpc", e);
|
||||
result = false;
|
||||
}
|
||||
assertThat(result).isTrue();
|
||||
}
|
||||
}
|
||||
|
||||
public static boolean doUnaryRpc(ExecutorService executor, ManagedChannel channel)
|
||||
throws InterruptedException {
|
||||
try {
|
||||
SimpleServiceGrpc.SimpleServiceBlockingStub stub =
|
||||
SimpleServiceGrpc.newBlockingStub(channel);
|
||||
SimpleResponse resp = stub.unaryRpc(SimpleRequest.newBuilder()
|
||||
.setRequestMessage("S2A team")
|
||||
.build());
|
||||
if (!resp.getResponseMessage().equals("Hello, S2A team!")) {
|
||||
logger.info(
|
||||
"Received unexpected message from the Simple Service: " + resp.getResponseMessage());
|
||||
throw new RuntimeException();
|
||||
} else {
|
||||
System.out.println(
|
||||
"We received this message from the Simple Service: " + resp.getResponseMessage());
|
||||
return true;
|
||||
}
|
||||
} finally {
|
||||
channel.shutdown();
|
||||
channel.awaitTermination(1, SECONDS);
|
||||
executor.shutdown();
|
||||
executor.awaitTermination(1, SECONDS);
|
||||
}
|
||||
}
|
||||
|
||||
private static SslContext buildSslContext() throws SSLException {
|
||||
SslContextBuilder sslServerContextBuilder =
|
||||
SslContextBuilder.forServer(
|
||||
new ByteArrayInputStream(CERT_CHAIN.getBytes(UTF_8)),
|
||||
new ByteArrayInputStream(PRIVATE_KEY.getBytes(UTF_8)));
|
||||
SslContext sslServerContext =
|
||||
GrpcSslContexts.configure(sslServerContextBuilder, SslProvider.OPENSSL)
|
||||
.protocols("TLSv1.3", "TLSv1.2")
|
||||
.trustManager(new ByteArrayInputStream(ROOT_PEM.getBytes(UTF_8)))
|
||||
.clientAuth(ClientAuth.REQUIRE)
|
||||
.build();
|
||||
|
||||
// Enable TLS resumption. This requires using the OpenSSL provider, since the JDK provider does
|
||||
// not allow a server to send session tickets.
|
||||
SSLSessionContext sslSessionContext = sslServerContext.sessionContext();
|
||||
if (!(sslSessionContext instanceof OpenSslSessionContext)) {
|
||||
throw new SSLException("sslSessionContext does not use OpenSSL.");
|
||||
}
|
||||
OpenSslSessionContext openSslSessionContext = (OpenSslSessionContext) sslSessionContext;
|
||||
// Calling {@code setTicketKeys} without specifying any keys means that the SSL libraries will
|
||||
// handle the generation of the resumption master secret.
|
||||
openSslSessionContext.setTicketKeys();
|
||||
|
||||
return sslServerContext;
|
||||
}
|
||||
|
||||
public static class SimpleServiceImpl extends SimpleServiceGrpc.SimpleServiceImplBase {
|
||||
@Override
|
||||
public void unaryRpc(SimpleRequest request, StreamObserver<SimpleResponse> observer) {
|
||||
observer.onNext(
|
||||
SimpleResponse.newBuilder()
|
||||
.setResponseMessage("Hello, " + request.getRequestMessage() + "!")
|
||||
.build());
|
||||
observer.onCompleted();
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,131 @@
|
|||
/*
|
||||
* Copyright 2024 The gRPC Authors
|
||||
*
|
||||
* 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 io.grpc.s2a.handshaker;
|
||||
|
||||
import static org.junit.Assert.assertThrows;
|
||||
|
||||
import com.google.common.collect.ImmutableSet;
|
||||
import com.google.common.truth.Expect;
|
||||
import org.junit.Rule;
|
||||
import org.junit.Test;
|
||||
import org.junit.runner.RunWith;
|
||||
import org.junit.runners.JUnit4;
|
||||
|
||||
/** Unit tests for {@link ProtoUtil}. */
|
||||
@RunWith(JUnit4.class)
|
||||
public final class ProtoUtilTest {
|
||||
@Rule public final Expect expect = Expect.create();
|
||||
|
||||
@Test
|
||||
public void convertCiphersuite_success() {
|
||||
expect
|
||||
.that(
|
||||
ProtoUtil.convertCiphersuite(
|
||||
Ciphersuite.CIPHERSUITE_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256))
|
||||
.isEqualTo("TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256");
|
||||
expect
|
||||
.that(
|
||||
ProtoUtil.convertCiphersuite(
|
||||
Ciphersuite.CIPHERSUITE_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384))
|
||||
.isEqualTo("TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384");
|
||||
expect
|
||||
.that(
|
||||
ProtoUtil.convertCiphersuite(
|
||||
Ciphersuite.CIPHERSUITE_ECDHE_ECDSA_WITH_CHACHA20_POLY1305_SHA256))
|
||||
.isEqualTo("TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305_SHA256");
|
||||
expect
|
||||
.that(
|
||||
ProtoUtil.convertCiphersuite(Ciphersuite.CIPHERSUITE_ECDHE_RSA_WITH_AES_128_GCM_SHA256))
|
||||
.isEqualTo("TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256");
|
||||
expect
|
||||
.that(
|
||||
ProtoUtil.convertCiphersuite(Ciphersuite.CIPHERSUITE_ECDHE_RSA_WITH_AES_256_GCM_SHA384))
|
||||
.isEqualTo("TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384");
|
||||
expect
|
||||
.that(
|
||||
ProtoUtil.convertCiphersuite(
|
||||
Ciphersuite.CIPHERSUITE_ECDHE_RSA_WITH_CHACHA20_POLY1305_SHA256))
|
||||
.isEqualTo("TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305_SHA256");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void convertCiphersuite_withUnspecifiedCiphersuite_fails() {
|
||||
AssertionError expected =
|
||||
assertThrows(
|
||||
AssertionError.class,
|
||||
() -> ProtoUtil.convertCiphersuite(Ciphersuite.CIPHERSUITE_UNSPECIFIED));
|
||||
expect.that(expected).hasMessageThat().isEqualTo("Ciphersuite 0 is not supported.");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void convertTlsProtocolVersion_success() {
|
||||
expect
|
||||
.that(ProtoUtil.convertTlsProtocolVersion(TLSVersion.TLS_VERSION_1_3))
|
||||
.isEqualTo("TLSv1.3");
|
||||
expect
|
||||
.that(ProtoUtil.convertTlsProtocolVersion(TLSVersion.TLS_VERSION_1_2))
|
||||
.isEqualTo("TLSv1.2");
|
||||
expect
|
||||
.that(ProtoUtil.convertTlsProtocolVersion(TLSVersion.TLS_VERSION_1_1))
|
||||
.isEqualTo("TLSv1.1");
|
||||
expect.that(ProtoUtil.convertTlsProtocolVersion(TLSVersion.TLS_VERSION_1_0)).isEqualTo("TLSv1");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void convertTlsProtocolVersion_withUnknownTlsVersion_fails() {
|
||||
AssertionError expected =
|
||||
assertThrows(
|
||||
AssertionError.class,
|
||||
() -> ProtoUtil.convertTlsProtocolVersion(TLSVersion.TLS_VERSION_UNSPECIFIED));
|
||||
expect.that(expected).hasMessageThat().isEqualTo("TLS version 0 is not supported.");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void buildTlsProtocolVersionSet_success() {
|
||||
expect
|
||||
.that(
|
||||
ProtoUtil.buildTlsProtocolVersionSet(
|
||||
TLSVersion.TLS_VERSION_1_0, TLSVersion.TLS_VERSION_1_3))
|
||||
.isEqualTo(ImmutableSet.of("TLSv1", "TLSv1.1", "TLSv1.2", "TLSv1.3"));
|
||||
expect
|
||||
.that(
|
||||
ProtoUtil.buildTlsProtocolVersionSet(
|
||||
TLSVersion.TLS_VERSION_1_2, TLSVersion.TLS_VERSION_1_2))
|
||||
.isEqualTo(ImmutableSet.of("TLSv1.2"));
|
||||
expect
|
||||
.that(
|
||||
ProtoUtil.buildTlsProtocolVersionSet(
|
||||
TLSVersion.TLS_VERSION_1_3, TLSVersion.TLS_VERSION_1_3))
|
||||
.isEqualTo(ImmutableSet.of("TLSv1.3"));
|
||||
expect
|
||||
.that(
|
||||
ProtoUtil.buildTlsProtocolVersionSet(
|
||||
TLSVersion.TLS_VERSION_1_3, TLSVersion.TLS_VERSION_1_2))
|
||||
.isEmpty();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void buildTlsProtocolVersionSet_failure() {
|
||||
AssertionError expected =
|
||||
assertThrows(
|
||||
AssertionError.class,
|
||||
() ->
|
||||
ProtoUtil.buildTlsProtocolVersionSet(
|
||||
TLSVersion.TLS_VERSION_UNSPECIFIED, TLSVersion.TLS_VERSION_1_3));
|
||||
expect.that(expected).hasMessageThat().isEqualTo("TLS version 0 is not supported.");
|
||||
}
|
||||
}
|
|
@ -0,0 +1,308 @@
|
|||
/*
|
||||
* Copyright 2024 The gRPC Authors
|
||||
*
|
||||
* 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 io.grpc.s2a.handshaker;
|
||||
|
||||
import static com.google.common.truth.Truth.assertThat;
|
||||
import static java.nio.charset.StandardCharsets.UTF_8;
|
||||
import static org.junit.Assert.assertThrows;
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.verify;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
import com.google.common.truth.Expect;
|
||||
import com.google.protobuf.ByteString;
|
||||
import io.grpc.netty.GrpcSslContexts;
|
||||
import io.grpc.s2a.handshaker.S2AIdentity;
|
||||
import io.netty.handler.ssl.OpenSslPrivateKeyMethod;
|
||||
import io.netty.handler.ssl.SslContextBuilder;
|
||||
import java.io.ByteArrayInputStream;
|
||||
import java.security.PublicKey;
|
||||
import java.security.Signature;
|
||||
import java.security.cert.CertificateFactory;
|
||||
import java.security.cert.X509Certificate;
|
||||
import java.util.Optional;
|
||||
import org.junit.Before;
|
||||
import org.junit.Rule;
|
||||
import org.junit.Test;
|
||||
import org.junit.runner.RunWith;
|
||||
import org.junit.runners.JUnit4;
|
||||
|
||||
@RunWith(JUnit4.class)
|
||||
public final class S2APrivateKeyMethodTest {
|
||||
@Rule public final Expect expect = Expect.create();
|
||||
private static final byte[] DATA_TO_SIGN = "random bytes for signing.".getBytes(UTF_8);
|
||||
|
||||
private S2AStub stub;
|
||||
private FakeWriter writer;
|
||||
private S2APrivateKeyMethod keyMethod;
|
||||
|
||||
private static PublicKey extractPublicKeyFromPem(String pem) throws Exception {
|
||||
X509Certificate cert =
|
||||
(X509Certificate)
|
||||
CertificateFactory.getInstance("X.509")
|
||||
.generateCertificate(new ByteArrayInputStream(pem.getBytes(UTF_8)));
|
||||
return cert.getPublicKey();
|
||||
}
|
||||
|
||||
private static boolean verifySignature(
|
||||
byte[] dataToSign, byte[] signature, String signatureAlgorithm) throws Exception {
|
||||
Signature sig = Signature.getInstance(signatureAlgorithm);
|
||||
sig.initVerify(extractPublicKeyFromPem(FakeWriter.LEAF_CERT));
|
||||
sig.update(dataToSign);
|
||||
return sig.verify(signature);
|
||||
}
|
||||
|
||||
@Before
|
||||
public void setUp() {
|
||||
// This is line is to ensure that JNI correctly links the necessary objects. Without this, we
|
||||
// get `java.lang.UnsatisfiedLinkError` on
|
||||
// `io.netty.internal.tcnative.NativeStaticallyReferencedJniMethods.sslSignRsaPkcsSha1()`
|
||||
GrpcSslContexts.configure(SslContextBuilder.forClient());
|
||||
|
||||
writer = new FakeWriter();
|
||||
stub = S2AStub.newInstanceForTesting(writer);
|
||||
writer.setReader(stub.getReader());
|
||||
keyMethod = S2APrivateKeyMethod.create(stub, /* localIdentity= */ Optional.empty());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void signatureAlgorithmConversion_success() {
|
||||
expect
|
||||
.that(
|
||||
S2APrivateKeyMethod.convertOpenSslSignAlgToS2ASignAlg(
|
||||
OpenSslPrivateKeyMethod.SSL_SIGN_RSA_PKCS1_SHA256))
|
||||
.isEqualTo(SignatureAlgorithm.S2A_SSL_SIGN_RSA_PKCS1_SHA256);
|
||||
expect
|
||||
.that(
|
||||
S2APrivateKeyMethod.convertOpenSslSignAlgToS2ASignAlg(
|
||||
OpenSslPrivateKeyMethod.SSL_SIGN_RSA_PKCS1_SHA384))
|
||||
.isEqualTo(SignatureAlgorithm.S2A_SSL_SIGN_RSA_PKCS1_SHA384);
|
||||
expect
|
||||
.that(
|
||||
S2APrivateKeyMethod.convertOpenSslSignAlgToS2ASignAlg(
|
||||
OpenSslPrivateKeyMethod.SSL_SIGN_RSA_PKCS1_SHA512))
|
||||
.isEqualTo(SignatureAlgorithm.S2A_SSL_SIGN_RSA_PKCS1_SHA512);
|
||||
expect
|
||||
.that(
|
||||
S2APrivateKeyMethod.convertOpenSslSignAlgToS2ASignAlg(
|
||||
OpenSslPrivateKeyMethod.SSL_SIGN_ECDSA_SECP256R1_SHA256))
|
||||
.isEqualTo(SignatureAlgorithm.S2A_SSL_SIGN_ECDSA_SECP256R1_SHA256);
|
||||
expect
|
||||
.that(
|
||||
S2APrivateKeyMethod.convertOpenSslSignAlgToS2ASignAlg(
|
||||
OpenSslPrivateKeyMethod.SSL_SIGN_ECDSA_SECP384R1_SHA384))
|
||||
.isEqualTo(SignatureAlgorithm.S2A_SSL_SIGN_ECDSA_SECP384R1_SHA384);
|
||||
expect
|
||||
.that(
|
||||
S2APrivateKeyMethod.convertOpenSslSignAlgToS2ASignAlg(
|
||||
OpenSslPrivateKeyMethod.SSL_SIGN_ECDSA_SECP521R1_SHA512))
|
||||
.isEqualTo(SignatureAlgorithm.S2A_SSL_SIGN_ECDSA_SECP521R1_SHA512);
|
||||
expect
|
||||
.that(
|
||||
S2APrivateKeyMethod.convertOpenSslSignAlgToS2ASignAlg(
|
||||
OpenSslPrivateKeyMethod.SSL_SIGN_RSA_PSS_RSAE_SHA256))
|
||||
.isEqualTo(SignatureAlgorithm.S2A_SSL_SIGN_RSA_PSS_RSAE_SHA256);
|
||||
expect
|
||||
.that(
|
||||
S2APrivateKeyMethod.convertOpenSslSignAlgToS2ASignAlg(
|
||||
OpenSslPrivateKeyMethod.SSL_SIGN_RSA_PSS_RSAE_SHA384))
|
||||
.isEqualTo(SignatureAlgorithm.S2A_SSL_SIGN_RSA_PSS_RSAE_SHA384);
|
||||
expect
|
||||
.that(
|
||||
S2APrivateKeyMethod.convertOpenSslSignAlgToS2ASignAlg(
|
||||
OpenSslPrivateKeyMethod.SSL_SIGN_RSA_PSS_RSAE_SHA512))
|
||||
.isEqualTo(SignatureAlgorithm.S2A_SSL_SIGN_RSA_PSS_RSAE_SHA512);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void signatureAlgorithmConversion_unsupportedOperation() {
|
||||
UnsupportedOperationException e =
|
||||
assertThrows(
|
||||
UnsupportedOperationException.class,
|
||||
() -> S2APrivateKeyMethod.convertOpenSslSignAlgToS2ASignAlg(-1));
|
||||
|
||||
assertThat(e).hasMessageThat().contains("Signature Algorithm -1 is not supported.");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void createOnNullStub_returnsNullPointerException() {
|
||||
assertThrows(
|
||||
NullPointerException.class,
|
||||
() -> S2APrivateKeyMethod.create(/* stub= */ null, /* localIdentity= */ Optional.empty()));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void decrypt_unsupportedOperation() {
|
||||
UnsupportedOperationException e =
|
||||
assertThrows(
|
||||
UnsupportedOperationException.class,
|
||||
() -> keyMethod.decrypt(/* engine= */ null, DATA_TO_SIGN));
|
||||
|
||||
assertThat(e).hasMessageThat().contains("decrypt is not supported.");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void fakelocalIdentity_signWithSha256_success() throws Exception {
|
||||
S2AIdentity fakeIdentity = S2AIdentity.fromSpiffeId("fake-spiffe-id");
|
||||
S2AStub mockStub = mock(S2AStub.class);
|
||||
OpenSslPrivateKeyMethod keyMethodWithFakeIdentity =
|
||||
S2APrivateKeyMethod.create(mockStub, Optional.of(fakeIdentity));
|
||||
SessionReq req =
|
||||
SessionReq.newBuilder()
|
||||
.setLocalIdentity(fakeIdentity.getIdentity())
|
||||
.setOffloadPrivateKeyOperationReq(
|
||||
OffloadPrivateKeyOperationReq.newBuilder()
|
||||
.setOperation(OffloadPrivateKeyOperationReq.PrivateKeyOperation.SIGN)
|
||||
.setSignatureAlgorithm(SignatureAlgorithm.S2A_SSL_SIGN_ECDSA_SECP256R1_SHA256)
|
||||
.setRawBytes(ByteString.copyFrom(DATA_TO_SIGN)))
|
||||
.build();
|
||||
byte[] expectedOutbytes = "fake out bytes".getBytes(UTF_8);
|
||||
when(mockStub.send(req))
|
||||
.thenReturn(
|
||||
SessionResp.newBuilder()
|
||||
.setOffloadPrivateKeyOperationResp(
|
||||
OffloadPrivateKeyOperationResp.newBuilder()
|
||||
.setOutBytes(ByteString.copyFrom(expectedOutbytes)))
|
||||
.build());
|
||||
|
||||
byte[] signature =
|
||||
keyMethodWithFakeIdentity.sign(
|
||||
/* engine= */ null,
|
||||
OpenSslPrivateKeyMethod.SSL_SIGN_ECDSA_SECP256R1_SHA256,
|
||||
DATA_TO_SIGN);
|
||||
verify(mockStub).send(req);
|
||||
assertThat(signature).isEqualTo(expectedOutbytes);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void signWithSha256_success() throws Exception {
|
||||
writer.initializePrivateKey().setBehavior(FakeWriter.Behavior.OK_STATUS);
|
||||
|
||||
byte[] signature =
|
||||
keyMethod.sign(
|
||||
/* engine= */ null,
|
||||
OpenSslPrivateKeyMethod.SSL_SIGN_ECDSA_SECP256R1_SHA256,
|
||||
DATA_TO_SIGN);
|
||||
|
||||
assertThat(signature).isNotEmpty();
|
||||
assertThat(verifySignature(DATA_TO_SIGN, signature, "SHA256withECDSA")).isTrue();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void signWithSha384_success() throws Exception {
|
||||
writer.initializePrivateKey().setBehavior(FakeWriter.Behavior.OK_STATUS);
|
||||
|
||||
byte[] signature =
|
||||
keyMethod.sign(
|
||||
/* engine= */ null,
|
||||
OpenSslPrivateKeyMethod.SSL_SIGN_ECDSA_SECP384R1_SHA384,
|
||||
DATA_TO_SIGN);
|
||||
|
||||
assertThat(signature).isNotEmpty();
|
||||
assertThat(verifySignature(DATA_TO_SIGN, signature, "SHA384withECDSA")).isTrue();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void signWithSha512_success() throws Exception {
|
||||
writer.initializePrivateKey().setBehavior(FakeWriter.Behavior.OK_STATUS);
|
||||
|
||||
byte[] signature =
|
||||
keyMethod.sign(
|
||||
/* engine= */ null,
|
||||
OpenSslPrivateKeyMethod.SSL_SIGN_ECDSA_SECP521R1_SHA512,
|
||||
DATA_TO_SIGN);
|
||||
|
||||
assertThat(signature).isNotEmpty();
|
||||
assertThat(verifySignature(DATA_TO_SIGN, signature, "SHA512withECDSA")).isTrue();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void sign_noKeyAvailable() throws Exception {
|
||||
writer.resetPrivateKey().setBehavior(FakeWriter.Behavior.OK_STATUS);
|
||||
|
||||
S2AConnectionException e =
|
||||
assertThrows(
|
||||
S2AConnectionException.class,
|
||||
() ->
|
||||
keyMethod.sign(
|
||||
/* engine= */ null,
|
||||
OpenSslPrivateKeyMethod.SSL_SIGN_ECDSA_SECP256R1_SHA256,
|
||||
DATA_TO_SIGN));
|
||||
|
||||
assertThat(e)
|
||||
.hasMessageThat()
|
||||
.contains(
|
||||
"Error occurred in response from S2A, error code: 255, error message: \"No Private Key"
|
||||
+ " available.\".");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void sign_algorithmNotSupported() throws Exception {
|
||||
writer.initializePrivateKey().setBehavior(FakeWriter.Behavior.OK_STATUS);
|
||||
|
||||
S2AConnectionException e =
|
||||
assertThrows(
|
||||
S2AConnectionException.class,
|
||||
() ->
|
||||
keyMethod.sign(
|
||||
/* engine= */ null,
|
||||
OpenSslPrivateKeyMethod.SSL_SIGN_RSA_PKCS1_SHA256,
|
||||
DATA_TO_SIGN));
|
||||
|
||||
assertThat(e)
|
||||
.hasMessageThat()
|
||||
.contains(
|
||||
"Error occurred in response from S2A, error code: 255, error message: \"Only ECDSA key"
|
||||
+ " algorithms are supported.\".");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void sign_getsErrorResponse() throws Exception {
|
||||
writer.initializePrivateKey().setBehavior(FakeWriter.Behavior.ERROR_STATUS);
|
||||
|
||||
S2AConnectionException e =
|
||||
assertThrows(
|
||||
S2AConnectionException.class,
|
||||
() ->
|
||||
keyMethod.sign(
|
||||
/* engine= */ null,
|
||||
OpenSslPrivateKeyMethod.SSL_SIGN_ECDSA_SECP256R1_SHA256,
|
||||
DATA_TO_SIGN));
|
||||
|
||||
assertThat(e)
|
||||
.hasMessageThat()
|
||||
.contains(
|
||||
"Error occurred in response from S2A, error code: 1, error message: \"Intended ERROR"
|
||||
+ " Status from FakeWriter.\".");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void sign_getsEmptyResponse() throws Exception {
|
||||
writer.initializePrivateKey().setBehavior(FakeWriter.Behavior.EMPTY_RESPONSE);
|
||||
|
||||
S2AConnectionException e =
|
||||
assertThrows(
|
||||
S2AConnectionException.class,
|
||||
() ->
|
||||
keyMethod.sign(
|
||||
/* engine= */ null,
|
||||
OpenSslPrivateKeyMethod.SSL_SIGN_ECDSA_SECP256R1_SHA256,
|
||||
DATA_TO_SIGN));
|
||||
|
||||
assertThat(e).hasMessageThat().contains("No valid response received from S2A.");
|
||||
}
|
||||
}
|
|
@ -0,0 +1,284 @@
|
|||
/*
|
||||
* Copyright 2024 The gRPC Authors
|
||||
*
|
||||
* 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 io.grpc.s2a.handshaker;
|
||||
|
||||
import static com.google.common.truth.Truth.assertThat;
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.verify;
|
||||
|
||||
import com.google.common.testing.NullPointerTester;
|
||||
import com.google.common.testing.NullPointerTester.Visibility;
|
||||
import io.grpc.Channel;
|
||||
import io.grpc.Grpc;
|
||||
import io.grpc.InsecureChannelCredentials;
|
||||
import io.grpc.ManagedChannel;
|
||||
import io.grpc.Server;
|
||||
import io.grpc.ServerBuilder;
|
||||
import io.grpc.benchmarks.Utils;
|
||||
import io.grpc.internal.ObjectPool;
|
||||
import io.grpc.internal.SharedResourcePool;
|
||||
import io.grpc.internal.TestUtils.NoopChannelLogger;
|
||||
import io.grpc.netty.GrpcHttp2ConnectionHandler;
|
||||
import io.grpc.netty.InternalProtocolNegotiator;
|
||||
import io.grpc.netty.InternalProtocolNegotiator.ProtocolNegotiator;
|
||||
import io.grpc.s2a.channel.S2AChannelPool;
|
||||
import io.grpc.s2a.channel.S2AGrpcChannelPool;
|
||||
import io.grpc.s2a.channel.S2AHandshakerServiceChannel;
|
||||
import io.grpc.s2a.handshaker.S2AIdentity;
|
||||
import io.grpc.s2a.handshaker.S2AProtocolNegotiatorFactory.S2AProtocolNegotiator;
|
||||
import io.grpc.stub.StreamObserver;
|
||||
import io.netty.channel.ChannelDuplexHandler;
|
||||
import io.netty.channel.ChannelHandler;
|
||||
import io.netty.channel.ChannelHandlerContext;
|
||||
import io.netty.channel.ChannelPromise;
|
||||
import io.netty.channel.embedded.EmbeddedChannel;
|
||||
import io.netty.handler.codec.http2.Http2ConnectionDecoder;
|
||||
import io.netty.handler.codec.http2.Http2ConnectionEncoder;
|
||||
import io.netty.handler.codec.http2.Http2Settings;
|
||||
import io.netty.util.AsciiString;
|
||||
import java.util.Optional;
|
||||
import java.util.concurrent.ExecutorService;
|
||||
import java.util.concurrent.Executors;
|
||||
import org.checkerframework.checker.nullness.qual.Nullable;
|
||||
import org.junit.After;
|
||||
import org.junit.Before;
|
||||
import org.junit.Test;
|
||||
import org.junit.runner.RunWith;
|
||||
import org.junit.runners.JUnit4;
|
||||
|
||||
/** Unit tests for {@link S2AProtocolNegotiatorFactory}. */
|
||||
@RunWith(JUnit4.class)
|
||||
public class S2AProtocolNegotiatorFactoryTest {
|
||||
private static final S2AIdentity LOCAL_IDENTITY = S2AIdentity.fromSpiffeId("local identity");
|
||||
private final ChannelHandlerContext mockChannelHandlerContext = mock(ChannelHandlerContext.class);
|
||||
private GrpcHttp2ConnectionHandler fakeConnectionHandler;
|
||||
private String authority;
|
||||
private int port;
|
||||
private Server fakeS2AServer;
|
||||
private ObjectPool<Channel> channelPool;
|
||||
|
||||
@Before
|
||||
public void setUp() throws Exception {
|
||||
port = Utils.pickUnusedPort();
|
||||
fakeS2AServer = ServerBuilder.forPort(port).addService(new S2AServiceImpl()).build();
|
||||
fakeS2AServer.start();
|
||||
channelPool = new FakeChannelPool();
|
||||
authority = "localhost:" + port;
|
||||
fakeConnectionHandler = FakeConnectionHandler.create(authority);
|
||||
}
|
||||
|
||||
@After
|
||||
public void tearDown() {
|
||||
fakeS2AServer.shutdown();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void handlerRemoved_success() throws Exception {
|
||||
S2AProtocolNegotiatorFactory.BufferReadsHandler handler1 =
|
||||
new S2AProtocolNegotiatorFactory.BufferReadsHandler();
|
||||
S2AProtocolNegotiatorFactory.BufferReadsHandler handler2 =
|
||||
new S2AProtocolNegotiatorFactory.BufferReadsHandler();
|
||||
EmbeddedChannel channel = new EmbeddedChannel(handler1, handler2);
|
||||
channel.writeInbound("message1");
|
||||
channel.writeInbound("message2");
|
||||
channel.writeInbound("message3");
|
||||
assertThat(handler1.getReads()).hasSize(3);
|
||||
assertThat(handler2.getReads()).isEmpty();
|
||||
channel.pipeline().remove(handler1);
|
||||
assertThat(handler2.getReads()).hasSize(3);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void createProtocolNegotiatorFactory_nullArgument() throws Exception {
|
||||
NullPointerTester tester = new NullPointerTester().setDefault(Optional.class, Optional.empty());
|
||||
|
||||
tester.testStaticMethods(S2AProtocolNegotiatorFactory.class, Visibility.PUBLIC);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void createProtocolNegotiator_nullArgument() throws Exception {
|
||||
S2AChannelPool pool =
|
||||
S2AGrpcChannelPool.create(
|
||||
SharedResourcePool.forResource(
|
||||
S2AHandshakerServiceChannel.getChannelResource(
|
||||
"localhost:8080", /* s2aChannelCredentials= */ Optional.empty())));
|
||||
|
||||
NullPointerTester tester =
|
||||
new NullPointerTester()
|
||||
.setDefault(S2AChannelPool.class, pool)
|
||||
.setDefault(Optional.class, Optional.empty());
|
||||
|
||||
tester.testStaticMethods(S2AProtocolNegotiator.class, Visibility.PACKAGE);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void createProtocolNegotiatorFactory_getsDefaultPort_succeeds() throws Exception {
|
||||
InternalProtocolNegotiator.ClientFactory clientFactory =
|
||||
S2AProtocolNegotiatorFactory.createClientFactory(LOCAL_IDENTITY, channelPool);
|
||||
|
||||
assertThat(clientFactory.getDefaultPort()).isEqualTo(S2AProtocolNegotiatorFactory.DEFAULT_PORT);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void s2aProtocolNegotiator_getHostNameOnNull_returnsNull() throws Exception {
|
||||
assertThat(S2AProtocolNegotiatorFactory.S2AProtocolNegotiator.getHostNameFromAuthority(null))
|
||||
.isNull();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void s2aProtocolNegotiator_getHostNameOnValidAuthority_returnsValidHostname()
|
||||
throws Exception {
|
||||
assertThat(
|
||||
S2AProtocolNegotiatorFactory.S2AProtocolNegotiator.getHostNameFromAuthority(
|
||||
"hostname:80"))
|
||||
.isEqualTo("hostname");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void createProtocolNegotiatorFactory_buildsAnS2AProtocolNegotiatorOnClientSide_succeeds()
|
||||
throws Exception {
|
||||
InternalProtocolNegotiator.ClientFactory clientFactory =
|
||||
S2AProtocolNegotiatorFactory.createClientFactory(LOCAL_IDENTITY, channelPool);
|
||||
|
||||
ProtocolNegotiator clientNegotiator = clientFactory.newNegotiator();
|
||||
|
||||
assertThat(clientNegotiator).isInstanceOf(S2AProtocolNegotiator.class);
|
||||
assertThat(clientNegotiator.scheme()).isEqualTo(AsciiString.of("https"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void closeProtocolNegotiator_verifyProtocolNegotiatorIsClosedOnClientSide()
|
||||
throws Exception {
|
||||
InternalProtocolNegotiator.ClientFactory clientFactory =
|
||||
S2AProtocolNegotiatorFactory.createClientFactory(LOCAL_IDENTITY, channelPool);
|
||||
ProtocolNegotiator clientNegotiator = clientFactory.newNegotiator();
|
||||
|
||||
clientNegotiator.close();
|
||||
|
||||
assertThat(((FakeChannelPool) channelPool).isChannelCached()).isFalse();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void createChannelHandler_addHandlerToMockContext() throws Exception {
|
||||
ExecutorService executor = Executors.newSingleThreadExecutor();
|
||||
ManagedChannel channel =
|
||||
Grpc.newChannelBuilder(authority, InsecureChannelCredentials.create())
|
||||
.executor(executor)
|
||||
.build();
|
||||
FakeS2AChannelPool fakeChannelPool = new FakeS2AChannelPool(channel);
|
||||
ProtocolNegotiator clientNegotiator =
|
||||
S2AProtocolNegotiatorFactory.S2AProtocolNegotiator.createForClient(
|
||||
fakeChannelPool, LOCAL_IDENTITY);
|
||||
|
||||
ChannelHandler channelHandler = clientNegotiator.newHandler(fakeConnectionHandler);
|
||||
|
||||
((ChannelDuplexHandler) channelHandler).userEventTriggered(mockChannelHandlerContext, "event");
|
||||
verify(mockChannelHandlerContext).fireUserEventTriggered("event");
|
||||
}
|
||||
|
||||
/** A {@link S2AChannelPool} that returns the given channel. */
|
||||
private static class FakeS2AChannelPool implements S2AChannelPool {
|
||||
private final Channel channel;
|
||||
|
||||
FakeS2AChannelPool(Channel channel) {
|
||||
this.channel = channel;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Channel getChannel() {
|
||||
return channel;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void returnToPool(Channel channel) {}
|
||||
|
||||
@Override
|
||||
public void close() {}
|
||||
}
|
||||
|
||||
/** A {@code GrpcHttp2ConnectionHandler} that does nothing. */
|
||||
private static class FakeConnectionHandler extends GrpcHttp2ConnectionHandler {
|
||||
private static final Http2ConnectionDecoder DECODER = mock(Http2ConnectionDecoder.class);
|
||||
private static final Http2ConnectionEncoder ENCODER = mock(Http2ConnectionEncoder.class);
|
||||
private static final Http2Settings SETTINGS = new Http2Settings();
|
||||
private final String authority;
|
||||
|
||||
static FakeConnectionHandler create(String authority) {
|
||||
return new FakeConnectionHandler(null, DECODER, ENCODER, SETTINGS, authority);
|
||||
}
|
||||
|
||||
private FakeConnectionHandler(
|
||||
ChannelPromise channelUnused,
|
||||
Http2ConnectionDecoder decoder,
|
||||
Http2ConnectionEncoder encoder,
|
||||
Http2Settings initialSettings,
|
||||
String authority) {
|
||||
super(channelUnused, decoder, encoder, initialSettings, new NoopChannelLogger());
|
||||
this.authority = authority;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getAuthority() {
|
||||
return authority;
|
||||
}
|
||||
}
|
||||
|
||||
/** An S2A server that handles GetTlsConfiguration request. */
|
||||
private static class S2AServiceImpl extends S2AServiceGrpc.S2AServiceImplBase {
|
||||
static final FakeWriter writer = new FakeWriter();
|
||||
|
||||
@Override
|
||||
public StreamObserver<SessionReq> setUpSession(StreamObserver<SessionResp> responseObserver) {
|
||||
return new StreamObserver<SessionReq>() {
|
||||
@Override
|
||||
public void onNext(SessionReq req) {
|
||||
responseObserver.onNext(writer.handleResponse(req));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onError(Throwable t) {}
|
||||
|
||||
@Override
|
||||
public void onCompleted() {}
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
private static class FakeChannelPool implements ObjectPool<Channel> {
|
||||
private final Channel mockChannel = mock(Channel.class);
|
||||
private @Nullable Channel cachedChannel = null;
|
||||
|
||||
@Override
|
||||
public Channel getObject() {
|
||||
if (cachedChannel == null) {
|
||||
cachedChannel = mockChannel;
|
||||
}
|
||||
return cachedChannel;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Channel returnObject(Object object) {
|
||||
assertThat(object).isSameInstanceAs(mockChannel);
|
||||
cachedChannel = null;
|
||||
return null;
|
||||
}
|
||||
|
||||
public boolean isChannelCached() {
|
||||
return (cachedChannel != null);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,260 @@
|
|||
/*
|
||||
* Copyright 2024 The gRPC Authors
|
||||
*
|
||||
* 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 io.grpc.s2a.handshaker;
|
||||
|
||||
import static com.google.common.truth.Truth.assertThat;
|
||||
import static com.google.common.truth.extensions.proto.ProtoTruth.assertThat;
|
||||
import static org.junit.Assert.assertThrows;
|
||||
|
||||
import com.google.common.truth.Expect;
|
||||
import io.grpc.internal.SharedResourcePool;
|
||||
import io.grpc.s2a.channel.S2AChannelPool;
|
||||
import io.grpc.s2a.channel.S2AGrpcChannelPool;
|
||||
import io.grpc.s2a.channel.S2AHandshakerServiceChannel;
|
||||
import io.grpc.stub.StreamObserver;
|
||||
import java.io.IOException;
|
||||
import java.util.Optional;
|
||||
import org.junit.Before;
|
||||
import org.junit.Rule;
|
||||
import org.junit.Test;
|
||||
import org.junit.runner.RunWith;
|
||||
import org.junit.runners.JUnit4;
|
||||
|
||||
/** Unit tests for {@link S2AStub}. */
|
||||
@RunWith(JUnit4.class)
|
||||
public class S2AStubTest {
|
||||
@Rule public final Expect expect = Expect.create();
|
||||
private static final String S2A_ADDRESS = "localhost:8080";
|
||||
private S2AStub stub;
|
||||
private FakeWriter writer;
|
||||
|
||||
@Before
|
||||
public void setUp() {
|
||||
writer = new FakeWriter();
|
||||
stub = S2AStub.newInstanceForTesting(writer);
|
||||
writer.setReader(stub.getReader());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void send_receiveOkStatus() throws Exception {
|
||||
S2AChannelPool channelPool =
|
||||
S2AGrpcChannelPool.create(
|
||||
SharedResourcePool.forResource(
|
||||
S2AHandshakerServiceChannel.getChannelResource(
|
||||
S2A_ADDRESS, /* s2aChannelCredentials= */ Optional.empty())));
|
||||
S2AServiceGrpc.S2AServiceStub serviceStub = S2AServiceGrpc.newStub(channelPool.getChannel());
|
||||
S2AStub newStub = S2AStub.newInstance(serviceStub);
|
||||
|
||||
IOException expected =
|
||||
assertThrows(IOException.class, () -> newStub.send(SessionReq.getDefaultInstance()));
|
||||
|
||||
assertThat(expected).hasMessageThat().contains("DEADLINE_EXCEEDED");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void send_clientTlsConfiguration_receiveOkStatus() throws Exception {
|
||||
SessionReq req =
|
||||
SessionReq.newBuilder()
|
||||
.setGetTlsConfigurationReq(
|
||||
GetTlsConfigurationReq.newBuilder()
|
||||
.setConnectionSide(ConnectionSide.CONNECTION_SIDE_CLIENT))
|
||||
.build();
|
||||
|
||||
SessionResp resp = stub.send(req);
|
||||
|
||||
SessionResp expected =
|
||||
SessionResp.newBuilder()
|
||||
.setGetTlsConfigurationResp(
|
||||
GetTlsConfigurationResp.newBuilder()
|
||||
.setClientTlsConfiguration(
|
||||
GetTlsConfigurationResp.ClientTlsConfiguration.newBuilder()
|
||||
.addCertificateChain(FakeWriter.LEAF_CERT)
|
||||
.addCertificateChain(FakeWriter.INTERMEDIATE_CERT_2)
|
||||
.addCertificateChain(FakeWriter.INTERMEDIATE_CERT_1)
|
||||
.setMinTlsVersion(TLSVersion.TLS_VERSION_1_3)
|
||||
.setMaxTlsVersion(TLSVersion.TLS_VERSION_1_3)
|
||||
.addCiphersuites(
|
||||
Ciphersuite.CIPHERSUITE_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256)
|
||||
.addCiphersuites(
|
||||
Ciphersuite.CIPHERSUITE_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384)
|
||||
.addCiphersuites(
|
||||
Ciphersuite.CIPHERSUITE_ECDHE_ECDSA_WITH_CHACHA20_POLY1305_SHA256)))
|
||||
.build();
|
||||
assertThat(resp).ignoringRepeatedFieldOrder().isEqualTo(expected);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void send_serverTlsConfiguration_receiveErrorStatus() throws Exception {
|
||||
SessionReq req =
|
||||
SessionReq.newBuilder()
|
||||
.setGetTlsConfigurationReq(
|
||||
GetTlsConfigurationReq.newBuilder()
|
||||
.setConnectionSide(ConnectionSide.CONNECTION_SIDE_SERVER))
|
||||
.build();
|
||||
|
||||
SessionResp resp = stub.send(req);
|
||||
|
||||
SessionResp expected =
|
||||
SessionResp.newBuilder()
|
||||
.setStatus(
|
||||
Status.newBuilder()
|
||||
.setCode(255)
|
||||
.setDetails("No TLS configuration for the server side."))
|
||||
.build();
|
||||
assertThat(resp).isEqualTo(expected);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void send_receiveErrorStatus() throws Exception {
|
||||
writer.setBehavior(FakeWriter.Behavior.ERROR_STATUS);
|
||||
|
||||
SessionResp resp = stub.send(SessionReq.getDefaultInstance());
|
||||
|
||||
SessionResp expected =
|
||||
SessionResp.newBuilder()
|
||||
.setStatus(
|
||||
Status.newBuilder().setCode(1).setDetails("Intended ERROR Status from FakeWriter."))
|
||||
.build();
|
||||
assertThat(resp).isEqualTo(expected);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void send_receiveErrorResponse() throws InterruptedException {
|
||||
writer.setBehavior(FakeWriter.Behavior.ERROR_RESPONSE);
|
||||
|
||||
IOException expected =
|
||||
assertThrows(IOException.class, () -> stub.send(SessionReq.getDefaultInstance()));
|
||||
|
||||
expect.that(expected).hasCauseThat().isInstanceOf(RuntimeException.class);
|
||||
expect.that(expected).hasMessageThat().contains("Intended ERROR from FakeWriter.");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void send_receiveCompleteStatus() throws Exception {
|
||||
writer.setBehavior(FakeWriter.Behavior.COMPLETE_STATUS);
|
||||
|
||||
ConnectionClosedException expected =
|
||||
assertThrows(
|
||||
ConnectionClosedException.class, () -> stub.send(SessionReq.getDefaultInstance()));
|
||||
|
||||
assertThat(expected).hasMessageThat().contains("Reading from the S2A is complete.");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void send_receiveUnexpectedResponse() throws Exception {
|
||||
writer.sendIoError();
|
||||
|
||||
IOException expected =
|
||||
assertThrows(IOException.class, () -> stub.send(SessionReq.getDefaultInstance()));
|
||||
|
||||
assertThat(expected)
|
||||
.hasMessageThat()
|
||||
.contains(
|
||||
"Received an unexpected response from a host at the S2A's address. The S2A might be"
|
||||
+ " unavailable.");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void send_receiveManyUnexpectedResponse_expectResponsesEmpty() throws Exception {
|
||||
writer.sendIoError();
|
||||
writer.sendIoError();
|
||||
writer.sendIoError();
|
||||
|
||||
IOException expected =
|
||||
assertThrows(IOException.class, () -> stub.send(SessionReq.getDefaultInstance()));
|
||||
|
||||
assertThat(expected)
|
||||
.hasMessageThat()
|
||||
.contains(
|
||||
"Received an unexpected response from a host at the S2A's address. The S2A might be"
|
||||
+ " unavailable.");
|
||||
|
||||
assertThat(stub.getResponses()).isEmpty();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void send_receiveDelayedResponse() throws Exception {
|
||||
writer.sendGetTlsConfigResp();
|
||||
SessionResp resp = stub.send(SessionReq.getDefaultInstance());
|
||||
SessionResp expected =
|
||||
SessionResp.newBuilder()
|
||||
.setGetTlsConfigurationResp(
|
||||
GetTlsConfigurationResp.newBuilder()
|
||||
.setClientTlsConfiguration(
|
||||
GetTlsConfigurationResp.ClientTlsConfiguration.newBuilder()
|
||||
.addCertificateChain(FakeWriter.LEAF_CERT)
|
||||
.addCertificateChain(FakeWriter.INTERMEDIATE_CERT_2)
|
||||
.addCertificateChain(FakeWriter.INTERMEDIATE_CERT_1)
|
||||
.setMinTlsVersion(TLSVersion.TLS_VERSION_1_3)
|
||||
.setMaxTlsVersion(TLSVersion.TLS_VERSION_1_3)
|
||||
.addCiphersuites(
|
||||
Ciphersuite.CIPHERSUITE_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256)
|
||||
.addCiphersuites(
|
||||
Ciphersuite.CIPHERSUITE_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384)
|
||||
.addCiphersuites(
|
||||
Ciphersuite.CIPHERSUITE_ECDHE_ECDSA_WITH_CHACHA20_POLY1305_SHA256)))
|
||||
.build();
|
||||
assertThat(resp).ignoringRepeatedFieldOrder().isEqualTo(expected);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void send_afterEarlyClose_receivesClosedException() throws InterruptedException {
|
||||
stub.close();
|
||||
expect.that(writer.isFakeWriterClosed()).isTrue();
|
||||
|
||||
ConnectionClosedException expected =
|
||||
assertThrows(
|
||||
ConnectionClosedException.class, () -> stub.send(SessionReq.getDefaultInstance()));
|
||||
|
||||
assertThat(expected).hasMessageThat().contains("Stream to the S2A is closed.");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void send_failToWrite() throws Exception {
|
||||
FailWriter failWriter = new FailWriter();
|
||||
stub = S2AStub.newInstanceForTesting(failWriter);
|
||||
|
||||
IOException expected =
|
||||
assertThrows(IOException.class, () -> stub.send(SessionReq.getDefaultInstance()));
|
||||
|
||||
expect.that(expected).hasCauseThat().isInstanceOf(S2AConnectionException.class);
|
||||
expect
|
||||
.that(expected)
|
||||
.hasCauseThat()
|
||||
.hasMessageThat()
|
||||
.isEqualTo("Could not send request to S2A.");
|
||||
}
|
||||
|
||||
/** Fails whenever a write is attempted. */
|
||||
private static class FailWriter implements StreamObserver<SessionReq> {
|
||||
@Override
|
||||
public void onNext(SessionReq req) {
|
||||
assertThat(req).isNotNull();
|
||||
throw new S2AConnectionException("Could not send request to S2A.");
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onError(Throwable t) {
|
||||
assertThat(t).isInstanceOf(S2AConnectionException.class);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onCompleted() {
|
||||
throw new UnsupportedOperationException();
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,262 @@
|
|||
/*
|
||||
* Copyright 2024 The gRPC Authors
|
||||
*
|
||||
* 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 io.grpc.s2a.handshaker;
|
||||
|
||||
import static com.google.common.truth.Truth.assertThat;
|
||||
import static org.junit.Assert.assertThrows;
|
||||
|
||||
import io.grpc.s2a.handshaker.S2AIdentity;
|
||||
import java.io.ByteArrayInputStream;
|
||||
import java.security.cert.CertificateException;
|
||||
import java.security.cert.CertificateFactory;
|
||||
import java.security.cert.X509Certificate;
|
||||
import java.util.Base64;
|
||||
import java.util.Optional;
|
||||
import org.junit.Before;
|
||||
import org.junit.Test;
|
||||
import org.junit.runner.RunWith;
|
||||
import org.junit.runners.JUnit4;
|
||||
|
||||
@RunWith(JUnit4.class)
|
||||
public final class S2ATrustManagerTest {
|
||||
private S2AStub stub;
|
||||
private FakeWriter writer;
|
||||
private static final String FAKE_HOSTNAME = "Fake-Hostname";
|
||||
private static final String CLIENT_CERT_PEM =
|
||||
"MIICKjCCAc+gAwIBAgIUC2GShcVO+5Zkml+7VO3OQ+B2c7EwCgYIKoZIzj0EAwIw"
|
||||
+ "HzEdMBsGA1UEAwwUcm9vdGNlcnQuZXhhbXBsZS5jb20wIBcNMjMwMTI2MTk0OTUx"
|
||||
+ "WhgPMjA1MDA2MTMxOTQ5NTFaMB8xHTAbBgNVBAMMFGxlYWZjZXJ0LmV4YW1wbGUu"
|
||||
+ "Y29tMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEeciYZgFAZjxyzTrklCRIWpad"
|
||||
+ "8wkyCZQzJSf0IfNn9NKtfzL2V/blteULO0o9Da8e2Avaj+XCKfFTc7salMo/waOB"
|
||||
+ "5jCB4zAOBgNVHQ8BAf8EBAMCB4AwIAYDVR0lAQH/BBYwFAYIKwYBBQUHAwIGCCsG"
|
||||
+ "AQUFBwMBMAwGA1UdEwEB/wQCMAAwYQYDVR0RBFowWIYic3BpZmZlOi8vZm9vLnBy"
|
||||
+ "b2QuZ29vZ2xlLmNvbS9wMS9wMoIUZm9vLnByb2Quc3BpZmZlLmdvb2eCHG1hY2hp"
|
||||
+ "bmUtbmFtZS5wcm9kLmdvb2dsZS5jb20wHQYDVR0OBBYEFETY6Cu/aW924nfvUrOs"
|
||||
+ "yXCC1hrpMB8GA1UdIwQYMBaAFJLkXGlTYKISiGd+K/Ijh4IOEpHBMAoGCCqGSM49"
|
||||
+ "BAMCA0kAMEYCIQCZDW472c1/4jEOHES/88X7NTqsYnLtIpTjp5PZ62z3sAIhAN1J"
|
||||
+ "vxvbxt9ySdFO+cW7oLBEkCwUicBhxJi5VfQeQypT";
|
||||
|
||||
@Before
|
||||
public void setUp() {
|
||||
writer = new FakeWriter();
|
||||
stub = S2AStub.newInstanceForTesting(writer);
|
||||
writer.setReader(stub.getReader());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void createForClient_withNullStub_throwsError() {
|
||||
NullPointerException expected =
|
||||
assertThrows(
|
||||
NullPointerException.class,
|
||||
() ->
|
||||
S2ATrustManager.createForClient(
|
||||
/* stub= */ null, FAKE_HOSTNAME, /* localIdentity= */ Optional.empty()));
|
||||
|
||||
assertThat(expected).hasMessageThat().isNull();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void createForClient_withNullHostname_throwsError() {
|
||||
NullPointerException expected =
|
||||
assertThrows(
|
||||
NullPointerException.class,
|
||||
() ->
|
||||
S2ATrustManager.createForClient(
|
||||
stub, /* hostname= */ null, /* localIdentity= */ Optional.empty()));
|
||||
|
||||
assertThat(expected).hasMessageThat().isNull();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void getAcceptedIssuers_returnsExpectedNullResult() {
|
||||
S2ATrustManager trustManager =
|
||||
S2ATrustManager.createForClient(stub, FAKE_HOSTNAME, /* localIdentity= */ Optional.empty());
|
||||
|
||||
assertThat(trustManager.getAcceptedIssuers()).isNull();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void checkClientTrusted_withEmptyCertificateChain_throwsException()
|
||||
throws CertificateException {
|
||||
writer.setVerificationResult(FakeWriter.VerificationResult.SUCCESS);
|
||||
S2ATrustManager trustManager =
|
||||
S2ATrustManager.createForClient(stub, FAKE_HOSTNAME, /* localIdentity= */ Optional.empty());
|
||||
|
||||
IllegalArgumentException expected =
|
||||
assertThrows(
|
||||
IllegalArgumentException.class,
|
||||
() -> trustManager.checkClientTrusted(new X509Certificate[] {}, /* authType= */ ""));
|
||||
|
||||
assertThat(expected).hasMessageThat().contains("Certificate chain has zero certificates.");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void checkServerTrusted_withEmptyCertificateChain_throwsException()
|
||||
throws CertificateException {
|
||||
writer.setVerificationResult(FakeWriter.VerificationResult.SUCCESS);
|
||||
S2ATrustManager trustManager =
|
||||
S2ATrustManager.createForClient(stub, FAKE_HOSTNAME, /* localIdentity= */ Optional.empty());
|
||||
|
||||
IllegalArgumentException expected =
|
||||
assertThrows(
|
||||
IllegalArgumentException.class,
|
||||
() -> trustManager.checkServerTrusted(new X509Certificate[] {}, /* authType= */ ""));
|
||||
|
||||
assertThat(expected).hasMessageThat().contains("Certificate chain has zero certificates.");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void checkClientTrusted_getsSuccessResponse() throws CertificateException {
|
||||
writer.setVerificationResult(FakeWriter.VerificationResult.SUCCESS);
|
||||
S2ATrustManager trustManager =
|
||||
S2ATrustManager.createForClient(stub, FAKE_HOSTNAME, /* localIdentity= */ Optional.empty());
|
||||
|
||||
// Expect no exception.
|
||||
trustManager.checkClientTrusted(getCerts(), /* authType= */ "");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void checkClientTrusted_withLocalIdentity_getsSuccessResponse()
|
||||
throws CertificateException {
|
||||
writer.setVerificationResult(FakeWriter.VerificationResult.SUCCESS);
|
||||
S2ATrustManager trustManager =
|
||||
S2ATrustManager.createForClient(
|
||||
stub, FAKE_HOSTNAME, Optional.of(S2AIdentity.fromSpiffeId("fake-spiffe-id")));
|
||||
|
||||
// Expect no exception.
|
||||
trustManager.checkClientTrusted(getCerts(), /* authType= */ "");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void checkServerTrusted_getsSuccessResponse() throws CertificateException {
|
||||
writer.setVerificationResult(FakeWriter.VerificationResult.SUCCESS);
|
||||
S2ATrustManager trustManager =
|
||||
S2ATrustManager.createForClient(stub, FAKE_HOSTNAME, /* localIdentity= */ Optional.empty());
|
||||
|
||||
// Expect no exception.
|
||||
trustManager.checkServerTrusted(getCerts(), /* authType= */ "");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void checkServerTrusted_withLocalIdentity_getsSuccessResponse()
|
||||
throws CertificateException {
|
||||
writer.setVerificationResult(FakeWriter.VerificationResult.SUCCESS);
|
||||
S2ATrustManager trustManager =
|
||||
S2ATrustManager.createForClient(
|
||||
stub, FAKE_HOSTNAME, Optional.of(S2AIdentity.fromSpiffeId("fake-spiffe-id")));
|
||||
|
||||
// Expect no exception.
|
||||
trustManager.checkServerTrusted(getCerts(), /* authType= */ "");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void checkClientTrusted_getsIntendedFailureResponse() throws CertificateException {
|
||||
writer
|
||||
.setVerificationResult(FakeWriter.VerificationResult.FAILURE)
|
||||
.setFailureReason("Intended failure.");
|
||||
S2ATrustManager trustManager =
|
||||
S2ATrustManager.createForClient(stub, FAKE_HOSTNAME, /* localIdentity= */ Optional.empty());
|
||||
|
||||
CertificateException expected =
|
||||
assertThrows(
|
||||
CertificateException.class,
|
||||
() -> trustManager.checkClientTrusted(getCerts(), /* authType= */ ""));
|
||||
|
||||
assertThat(expected).hasMessageThat().contains("Intended failure.");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void checkClientTrusted_getsIntendedFailureStatusInResponse() throws CertificateException {
|
||||
writer.setBehavior(FakeWriter.Behavior.ERROR_STATUS);
|
||||
S2ATrustManager trustManager =
|
||||
S2ATrustManager.createForClient(stub, FAKE_HOSTNAME, /* localIdentity= */ Optional.empty());
|
||||
|
||||
CertificateException expected =
|
||||
assertThrows(
|
||||
CertificateException.class,
|
||||
() -> trustManager.checkClientTrusted(getCerts(), /* authType= */ ""));
|
||||
|
||||
assertThat(expected).hasMessageThat().contains("Error occurred in response from S2A");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void checkClientTrusted_getsIntendedFailureFromServer() throws CertificateException {
|
||||
writer.setBehavior(FakeWriter.Behavior.ERROR_RESPONSE);
|
||||
S2ATrustManager trustManager =
|
||||
S2ATrustManager.createForClient(stub, FAKE_HOSTNAME, /* localIdentity= */ Optional.empty());
|
||||
|
||||
CertificateException expected =
|
||||
assertThrows(
|
||||
CertificateException.class,
|
||||
() -> trustManager.checkClientTrusted(getCerts(), /* authType= */ ""));
|
||||
|
||||
assertThat(expected).hasMessageThat().isEqualTo("Failed to send request to S2A.");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void checkServerTrusted_getsIntendedFailureResponse() throws CertificateException {
|
||||
writer
|
||||
.setVerificationResult(FakeWriter.VerificationResult.FAILURE)
|
||||
.setFailureReason("Intended failure.");
|
||||
S2ATrustManager trustManager =
|
||||
S2ATrustManager.createForClient(stub, FAKE_HOSTNAME, /* localIdentity= */ Optional.empty());
|
||||
|
||||
CertificateException expected =
|
||||
assertThrows(
|
||||
CertificateException.class,
|
||||
() -> trustManager.checkServerTrusted(getCerts(), /* authType= */ ""));
|
||||
|
||||
assertThat(expected).hasMessageThat().contains("Intended failure.");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void checkServerTrusted_getsIntendedFailureStatusInResponse() throws CertificateException {
|
||||
writer.setBehavior(FakeWriter.Behavior.ERROR_STATUS);
|
||||
S2ATrustManager trustManager =
|
||||
S2ATrustManager.createForClient(stub, FAKE_HOSTNAME, /* localIdentity= */ Optional.empty());
|
||||
|
||||
CertificateException expected =
|
||||
assertThrows(
|
||||
CertificateException.class,
|
||||
() -> trustManager.checkServerTrusted(getCerts(), /* authType= */ ""));
|
||||
|
||||
assertThat(expected).hasMessageThat().contains("Error occurred in response from S2A");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void checkServerTrusted_getsIntendedFailureFromServer() throws CertificateException {
|
||||
writer.setBehavior(FakeWriter.Behavior.ERROR_RESPONSE);
|
||||
S2ATrustManager trustManager =
|
||||
S2ATrustManager.createForClient(stub, FAKE_HOSTNAME, /* localIdentity= */ Optional.empty());
|
||||
|
||||
CertificateException expected =
|
||||
assertThrows(
|
||||
CertificateException.class,
|
||||
() -> trustManager.checkServerTrusted(getCerts(), /* authType= */ ""));
|
||||
|
||||
assertThat(expected).hasMessageThat().isEqualTo("Failed to send request to S2A.");
|
||||
}
|
||||
|
||||
private X509Certificate[] getCerts() throws CertificateException {
|
||||
byte[] decoded = Base64.getDecoder().decode(CLIENT_CERT_PEM);
|
||||
return new X509Certificate[] {
|
||||
(X509Certificate)
|
||||
CertificateFactory.getInstance("X.509")
|
||||
.generateCertificate(new ByteArrayInputStream(decoded))
|
||||
};
|
||||
}
|
||||
}
|
|
@ -0,0 +1,177 @@
|
|||
/*
|
||||
* Copyright 2024 The gRPC Authors
|
||||
*
|
||||
* 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 io.grpc.s2a.handshaker;
|
||||
|
||||
import static com.google.common.truth.Truth.assertThat;
|
||||
import static org.junit.Assert.assertThrows;
|
||||
|
||||
import com.google.common.truth.Expect;
|
||||
import io.grpc.s2a.handshaker.S2AIdentity;
|
||||
import io.netty.handler.ssl.OpenSslSessionContext;
|
||||
import io.netty.handler.ssl.SslContext;
|
||||
import java.security.GeneralSecurityException;
|
||||
import java.util.Optional;
|
||||
import javax.net.ssl.SSLSessionContext;
|
||||
import org.junit.Before;
|
||||
import org.junit.Rule;
|
||||
import org.junit.Test;
|
||||
import org.junit.runner.RunWith;
|
||||
import org.junit.runners.JUnit4;
|
||||
|
||||
/** Unit tests for {@link SslContextFactory}. */
|
||||
@RunWith(JUnit4.class)
|
||||
public final class SslContextFactoryTest {
|
||||
@Rule public final Expect expect = Expect.create();
|
||||
private static final String FAKE_TARGET_NAME = "fake_target_name";
|
||||
private S2AStub stub;
|
||||
private FakeWriter writer;
|
||||
|
||||
@Before
|
||||
public void setUp() {
|
||||
writer = new FakeWriter();
|
||||
stub = S2AStub.newInstanceForTesting(writer);
|
||||
writer.setReader(stub.getReader());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void createForClient_returnsValidSslContext() throws Exception {
|
||||
SslContext sslContext =
|
||||
SslContextFactory.createForClient(
|
||||
stub, FAKE_TARGET_NAME, /* localIdentity= */ Optional.empty());
|
||||
|
||||
expect.that(sslContext).isNotNull();
|
||||
expect.that(sslContext.sessionCacheSize()).isEqualTo(1);
|
||||
expect.that(sslContext.sessionTimeout()).isEqualTo(300);
|
||||
expect.that(sslContext.isClient()).isTrue();
|
||||
expect.that(sslContext.applicationProtocolNegotiator().protocols()).containsExactly("h2");
|
||||
SSLSessionContext sslSessionContext = sslContext.sessionContext();
|
||||
if (sslSessionContext instanceof OpenSslSessionContext) {
|
||||
OpenSslSessionContext openSslSessionContext = (OpenSslSessionContext) sslSessionContext;
|
||||
expect.that(openSslSessionContext.isSessionCacheEnabled()).isFalse();
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void createForClient_withLocalIdentity_returnsValidSslContext() throws Exception {
|
||||
SslContext sslContext =
|
||||
SslContextFactory.createForClient(
|
||||
stub, FAKE_TARGET_NAME, Optional.of(S2AIdentity.fromSpiffeId("fake-spiffe-id")));
|
||||
|
||||
expect.that(sslContext).isNotNull();
|
||||
expect.that(sslContext.sessionCacheSize()).isEqualTo(1);
|
||||
expect.that(sslContext.sessionTimeout()).isEqualTo(300);
|
||||
expect.that(sslContext.isClient()).isTrue();
|
||||
expect.that(sslContext.applicationProtocolNegotiator().protocols()).containsExactly("h2");
|
||||
SSLSessionContext sslSessionContext = sslContext.sessionContext();
|
||||
if (sslSessionContext instanceof OpenSslSessionContext) {
|
||||
OpenSslSessionContext openSslSessionContext = (OpenSslSessionContext) sslSessionContext;
|
||||
expect.that(openSslSessionContext.isSessionCacheEnabled()).isFalse();
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void createForClient_returnsEmptyResponse_error() throws Exception {
|
||||
writer.setBehavior(FakeWriter.Behavior.EMPTY_RESPONSE);
|
||||
|
||||
S2AConnectionException expected =
|
||||
assertThrows(
|
||||
S2AConnectionException.class,
|
||||
() ->
|
||||
SslContextFactory.createForClient(
|
||||
stub, FAKE_TARGET_NAME, /* localIdentity= */ Optional.empty()));
|
||||
|
||||
assertThat(expected)
|
||||
.hasMessageThat()
|
||||
.contains("Response from S2A server does NOT contain ClientTlsConfiguration.");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void createForClient_returnsErrorStatus_error() throws Exception {
|
||||
writer.setBehavior(FakeWriter.Behavior.ERROR_STATUS);
|
||||
|
||||
S2AConnectionException expected =
|
||||
assertThrows(
|
||||
S2AConnectionException.class,
|
||||
() ->
|
||||
SslContextFactory.createForClient(
|
||||
stub, FAKE_TARGET_NAME, /* localIdentity= */ Optional.empty()));
|
||||
|
||||
assertThat(expected).hasMessageThat().contains("Intended ERROR Status from FakeWriter.");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void createForClient_getsErrorFromServer_throwsError() throws Exception {
|
||||
writer.sendIoError();
|
||||
|
||||
GeneralSecurityException expected =
|
||||
assertThrows(
|
||||
GeneralSecurityException.class,
|
||||
() ->
|
||||
SslContextFactory.createForClient(
|
||||
stub, FAKE_TARGET_NAME, /* localIdentity= */ Optional.empty()));
|
||||
|
||||
assertThat(expected)
|
||||
.hasMessageThat()
|
||||
.contains("Failed to get client TLS configuration from S2A.");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void createForClient_getsBadTlsVersionsFromServer_throwsError() throws Exception {
|
||||
writer.setBehavior(FakeWriter.Behavior.BAD_TLS_VERSION_RESPONSE);
|
||||
|
||||
S2AConnectionException expected =
|
||||
assertThrows(
|
||||
S2AConnectionException.class,
|
||||
() ->
|
||||
SslContextFactory.createForClient(
|
||||
stub, FAKE_TARGET_NAME, /* localIdentity= */ Optional.empty()));
|
||||
|
||||
assertThat(expected)
|
||||
.hasMessageThat()
|
||||
.contains("Set of TLS versions received from S2A server is empty.");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void createForClient_nullStub_throwsError() throws Exception {
|
||||
writer.sendUnexpectedResponse();
|
||||
|
||||
NullPointerException expected =
|
||||
assertThrows(
|
||||
NullPointerException.class,
|
||||
() ->
|
||||
SslContextFactory.createForClient(
|
||||
/* stub= */ null, FAKE_TARGET_NAME, /* localIdentity= */ Optional.empty()));
|
||||
|
||||
assertThat(expected).hasMessageThat().isEqualTo("stub should not be null.");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void createForClient_nullTargetName_throwsError() throws Exception {
|
||||
writer.sendUnexpectedResponse();
|
||||
|
||||
NullPointerException expected =
|
||||
assertThrows(
|
||||
NullPointerException.class,
|
||||
() ->
|
||||
SslContextFactory.createForClient(
|
||||
stub, /* targetName= */ null, /* localIdentity= */ Optional.empty()));
|
||||
|
||||
assertThat(expected)
|
||||
.hasMessageThat()
|
||||
.isEqualTo("targetName should not be null on client side.");
|
||||
}
|
||||
}
|
|
@ -0,0 +1,71 @@
|
|||
/*
|
||||
* Copyright 2024 The gRPC Authors
|
||||
*
|
||||
* 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 io.grpc.s2a.handshaker.tokenmanager;
|
||||
|
||||
import static com.google.common.truth.Truth.assertThat;
|
||||
|
||||
import io.grpc.s2a.handshaker.S2AIdentity;
|
||||
import java.util.Optional;
|
||||
import org.junit.Before;
|
||||
import org.junit.Test;
|
||||
import org.junit.runner.RunWith;
|
||||
import org.junit.runners.JUnit4;
|
||||
|
||||
@RunWith(JUnit4.class)
|
||||
public final class SingleTokenAccessTokenManagerTest {
|
||||
private static final S2AIdentity IDENTITY = S2AIdentity.fromSpiffeId("spiffe_id");
|
||||
private static final String TOKEN = "token";
|
||||
|
||||
@Before
|
||||
public void setUp() {
|
||||
SingleTokenFetcher.setAccessToken(null);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void getDefaultToken_success() throws Exception {
|
||||
SingleTokenFetcher.setAccessToken(TOKEN);
|
||||
Optional<AccessTokenManager> manager = AccessTokenManager.create();
|
||||
assertThat(manager).isPresent();
|
||||
assertThat(manager.get().getDefaultToken()).isEqualTo(TOKEN);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void getToken_success() throws Exception {
|
||||
SingleTokenFetcher.setAccessToken(TOKEN);
|
||||
Optional<AccessTokenManager> manager = AccessTokenManager.create();
|
||||
assertThat(manager).isPresent();
|
||||
assertThat(manager.get().getToken(IDENTITY)).isEqualTo(TOKEN);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void getToken_noEnvironmentVariable() throws Exception {
|
||||
assertThat(SingleTokenFetcher.create()).isEmpty();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void create_success() throws Exception {
|
||||
SingleTokenFetcher.setAccessToken(TOKEN);
|
||||
Optional<AccessTokenManager> manager = AccessTokenManager.create();
|
||||
assertThat(manager).isPresent();
|
||||
assertThat(manager.get().getToken(IDENTITY)).isEqualTo(TOKEN);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void create_noEnvironmentVariable() throws Exception {
|
||||
assertThat(AccessTokenManager.create()).isEmpty();
|
||||
}
|
||||
}
|
|
@ -0,0 +1,32 @@
|
|||
# Generating certificates and keys for testing mTLS-S2A
|
||||
|
||||
Content from: https://github.com/google/s2a-go/blob/main/testdata/README.md
|
||||
|
||||
Create root CA
|
||||
|
||||
```
|
||||
openssl req -x509 -sha256 -days 7305 -newkey rsa:2048 -keyout root_key.pem -out
|
||||
root_cert.pem
|
||||
```
|
||||
|
||||
Generate private keys for server and client
|
||||
|
||||
```
|
||||
openssl genrsa -out server_key.pem 2048
|
||||
openssl genrsa -out client_key.pem 2048
|
||||
```
|
||||
|
||||
Generate CSRs for server and client (set Common Name to localhost, leave all
|
||||
other fields blank)
|
||||
|
||||
```
|
||||
openssl req -key server_key.pem -new -out server.csr -config config.cnf
|
||||
openssl req -key client_key.pem -new -out client.csr -config config.cnf
|
||||
```
|
||||
|
||||
Sign CSRs for server and client
|
||||
|
||||
```
|
||||
openssl x509 -req -CA root_cert.pem -CAkey root_key.pem -in server.csr -out server_cert.pem -days 7305 -extfile config.cnf -extensions req_ext
|
||||
openssl x509 -req -CA root_cert.pem -CAkey root_key.pem -in client.csr -out client_cert.pem -days 7305
|
||||
```
|
|
@ -0,0 +1,16 @@
|
|||
-----BEGIN CERTIFICATE REQUEST-----
|
||||
MIIChzCCAW8CAQAwFDESMBAGA1UEAwwJbG9jYWxob3N0MIIBIjANBgkqhkiG9w0B
|
||||
AQEFAAOCAQ8AMIIBCgKCAQEAoSS3KtFgiXX4vAUNscFGIB/r2OOMgiZMKHz72dN0
|
||||
5kSxwdpQxpMIhwEoe0lhHNfOiuE7/r6VbGG9RGGIcQcoSonc3InPRfpnzfj9KohJ
|
||||
i8pYkLL9EwElAEl9sWnvVKTza8jTApDP2Z/fntBEsWAMsLPpuRZT6tgN1sXe4vNG
|
||||
4wufJSxuImyCVAx1fkZjRkYEKOtm1osnEDng4R0WXZ6S+q5lYzYPk1wxgbjdZu2U
|
||||
fWxP6V63SphV0NFXTx0E401j2h258cIqTVj8lRX6dfl9gO0d43Rd+hSU7R4iXGEw
|
||||
arixuH9g5H745AFf9H52twHPcNP9cEKBljBpSV5z3MvTkQIDAQABoC4wLAYJKoZI
|
||||
hvcNAQkOMR8wHTAbBgNVHREEFDAShxAAAAAAAAAAAAAAAAAAAAAAMA0GCSqGSIb3
|
||||
DQEBCwUAA4IBAQCQHim3aIpGJs5u6JhEA07Rwm8YKyVALDEklhsHILlFhdNr2uV7
|
||||
S+3bHV79mDGjxNWvFcgK5h5ENkT60tXbhbie1gYmFT0RMCYHDsL09NGTh8G9Bbdl
|
||||
UKeA9DMhRSYzE7Ks3Lo1dJvX7OAEI0qV77dGpQknufYpmHiBXuqtB9I0SpYi1c4O
|
||||
9IUn/NY0yiYFPsIEsVRz/1dK97wazusLnijaMwNNhUc9bJwTyujhlr+b8ioPyADG
|
||||
e+GDF97d0nQ8806DOJF4GTRKwaXD+R5zN5t4ULhZ7ERqLNeE9EnWRe4CvSGvBoNA
|
||||
hIVeYaLd761Z9ZKvOnsgCr8qvMDilDFY6OfB
|
||||
-----END CERTIFICATE REQUEST-----
|
|
@ -0,0 +1,18 @@
|
|||
-----BEGIN CERTIFICATE-----
|
||||
MIIC9DCCAdwCFB+cDXee2sIHjdlBhdNpTo+G2XAjMA0GCSqGSIb3DQEBCwUAMFkx
|
||||
CzAJBgNVBAYTAkFVMRMwEQYDVQQIDApTb21lLVN0YXRlMSEwHwYDVQQKDBhJbnRl
|
||||
cm5ldCBXaWRnaXRzIFB0eSBMdGQxEjAQBgNVBAMMCWxvY2FsaG9zdDAeFw0yMzEw
|
||||
MTcyMzA5MDNaFw00MzEwMTcyMzA5MDNaMBQxEjAQBgNVBAMMCWxvY2FsaG9zdDCC
|
||||
ASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAKEktyrRYIl1+LwFDbHBRiAf
|
||||
69jjjIImTCh8+9nTdOZEscHaUMaTCIcBKHtJYRzXzorhO/6+lWxhvURhiHEHKEqJ
|
||||
3NyJz0X6Z834/SqISYvKWJCy/RMBJQBJfbFp71Sk82vI0wKQz9mf357QRLFgDLCz
|
||||
6bkWU+rYDdbF3uLzRuMLnyUsbiJsglQMdX5GY0ZGBCjrZtaLJxA54OEdFl2ekvqu
|
||||
ZWM2D5NcMYG43WbtlH1sT+let0qYVdDRV08dBONNY9odufHCKk1Y/JUV+nX5fYDt
|
||||
HeN0XfoUlO0eIlxhMGq4sbh/YOR++OQBX/R+drcBz3DT/XBCgZYwaUlec9zL05EC
|
||||
AwEAATANBgkqhkiG9w0BAQsFAAOCAQEARorc1t2OJnwm1lxhf2KpTpNvNOI9FJak
|
||||
iSHz/MxhMdu4BG/dQHkKkWoVC6W2Kaimx4OImBwRlGEmGf4P0bXOLSTOumk2k1np
|
||||
ZUbw7Z2cJzvBmT2BLoHRXcBvbFIBW5DJUSHR37eXEKP57BeD+Og4/3XhNzehSpTX
|
||||
DRd2Ix/D39JjYA462nqPHQP8HDMf6+0BFmvf9ZRYmFucccYQRCUCKDqb8+wGf9W6
|
||||
tKNRE6qPG2jpAQ9qkgO7XuucbLvpywt5xj+yDRbOIq43l40mHaz4lRp697oaxjP8
|
||||
HSVcMydW3cluoW3AVInNIaqbM1dr6931MllK62DKipFtmCycq/56XA==
|
||||
-----END CERTIFICATE-----
|
|
@ -0,0 +1,28 @@
|
|||
-----BEGIN PRIVATE KEY-----
|
||||
MIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQChJLcq0WCJdfi8
|
||||
BQ2xwUYgH+vY44yCJkwofPvZ03TmRLHB2lDGkwiHASh7SWEc186K4Tv+vpVsYb1E
|
||||
YYhxByhKidzcic9F+mfN+P0qiEmLyliQsv0TASUASX2xae9UpPNryNMCkM/Zn9+e
|
||||
0ESxYAyws+m5FlPq2A3Wxd7i80bjC58lLG4ibIJUDHV+RmNGRgQo62bWiycQOeDh
|
||||
HRZdnpL6rmVjNg+TXDGBuN1m7ZR9bE/pXrdKmFXQ0VdPHQTjTWPaHbnxwipNWPyV
|
||||
Ffp1+X2A7R3jdF36FJTtHiJcYTBquLG4f2DkfvjkAV/0fna3Ac9w0/1wQoGWMGlJ
|
||||
XnPcy9ORAgMBAAECggEALAUqoGDIHWUDyOEch5WDwZzWwc4PgTJTFbBm4G96fLkB
|
||||
UjKAZG6gIrk3RM6b39Q4UQoMaJ/Jk+zzVi3Kpw3MfOhCVGC1JamtF8BP8IGAjdZ9
|
||||
8TFkHv/uCrEIzCFjRt00vhoDQq0qiom4/dppGYdikBbl3zDxRbM1vJkbNSY+FCGW
|
||||
dA0uJ5XdMLR6lPeB5odqjUggnfUgPCOLdV/F+HkSM9NP1bzmHLiKznzwFsfat139
|
||||
7LdzJwNN5IX4Io6cxsxNlrX/NNvPkKdGv07Z6FYxWROyKCunjh48xFcQg0ltoRuq
|
||||
R9P8/LwS8GYrcc1uC/uBc0e6VgM9D9fsvh+8SQtf3QKBgQDXX+z2GnsFoEs7xv9U
|
||||
qN0HEX4jOkihZvFu43layUmeCeE8wlEctJ0TsM5Bd7FMoUG6e5/btwhsAIYW89Xn
|
||||
l/R8OzxR6Kh952Dce4DAULuIeopiw7ASJwTZtO9lWhxw0hjM1hxXTG+xxOqQvsRX
|
||||
c+d+vtvdIqyJ4ELfzg9kUtkdpwKBgQC/ig3cmej7dQdRAMn0YAwgwhuLkCqVFh4y
|
||||
WIlqyPPejKf8RXubqgtaSYx/T7apP87SMMSfSLaUdrYAGjST6k+tG5cmwutPIbw/
|
||||
osL7U3hcIhjX3hfHgI69Ojcpplbd5yqTxZHpxIs6iAQCEqNuasLXIDMouqNhGF1D
|
||||
YssD6qxcBwKBgQCdZqWvVrsB6ZwSG+UO4jpmqAofhMD/9FQOToCqMOF0dpP966WL
|
||||
7RO/CEA06FzTPCblOuQhlyq4g8l7jMiPcSZkhIYY9oftO+Q2Pqxh4J6tp6DrfUh4
|
||||
e7u3v9wVnj2a1nD5gqFDy8D1kow7LLAhmbtdje7xNh4SxasaFWZ6U3IJkQKBgGS1
|
||||
F5i3q9IatCAZBBZjMb0/kfANevYsTPA3sPjec6q91c1EUzuDarisFx0RMn9Gt124
|
||||
mokNWEIzMHpZTO/AsOfZq92LeuF+YVYsI8y1FIGMw/csJOCWbXZ812gkt2OxGafc
|
||||
p118I6BAx6q3VgrGQ2+M1JlDmIeCofa+SPPkPX+dAoGBAJrOgEJ+oyEaX/YR1g+f
|
||||
33pWoPQbRCG7T4+Y0oetCCWIcMg1/IUvGUCGmRDxj5dMqB+a0vJtviQN9rjpSuNS
|
||||
0EVw79AJkIjHhi6KDOfAuyBvzGhxpqxGufnQ2GU0QL65NxQfd290xkxikN0ZGtuB
|
||||
SDgZoJxMOGYwf8EX5i9h27Db
|
||||
-----END PRIVATE KEY-----
|
|
@ -0,0 +1,17 @@
|
|||
[req]
|
||||
distinguished_name = req_distinguished_name
|
||||
req_extensions = req_ext
|
||||
|
||||
[req_distinguished_name]
|
||||
countryName = Country Name (2 letter code)
|
||||
stateOrProvinceName = State or Province Name (full name)
|
||||
localityName = Locality Name (eg, city)
|
||||
organizationalUnitName = Organizational Unit Name (eg, section)
|
||||
commonName = Common Name (eg, your name or your server\'s hostname)
|
||||
emailAddress = Email Address
|
||||
|
||||
[req_ext]
|
||||
subjectAltName = @alt_names
|
||||
|
||||
[alt_names]
|
||||
IP.1 = ::
|
|
@ -0,0 +1,22 @@
|
|||
-----BEGIN CERTIFICATE-----
|
||||
MIIDkzCCAnugAwIBAgIUb7RsINwsFgKf0Q0RuzfOgp48j6UwDQYJKoZIhvcNAQEL
|
||||
BQAwWTELMAkGA1UEBhMCQVUxEzARBgNVBAgMClNvbWUtU3RhdGUxITAfBgNVBAoM
|
||||
GEludGVybmV0IFdpZGdpdHMgUHR5IEx0ZDESMBAGA1UEAwwJbG9jYWxob3N0MB4X
|
||||
DTIzMTAxNzIzMDczOFoXDTQzMTAxNzIzMDczOFowWTELMAkGA1UEBhMCQVUxEzAR
|
||||
BgNVBAgMClNvbWUtU3RhdGUxITAfBgNVBAoMGEludGVybmV0IFdpZGdpdHMgUHR5
|
||||
IEx0ZDESMBAGA1UEAwwJbG9jYWxob3N0MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8A
|
||||
MIIBCgKCAQEAkIFnQLuhzYnm3rvmi/U7zMgEP2Tqgb3VC00frSXEV6olZcLgyC9g
|
||||
0DAGdt9l9lP90DQTG5KCOtoW2BTqM/aaVpR0OaDFOCy90FIj6YyZLZ9w2PQxQcxS
|
||||
GQHyEvWszTkNxeDyG1mPTj+Go8JLKqdvLg/9GUgPg6stxyAZwYhyUTGuEM4bv0sn
|
||||
b3vmHRmIGJ/w6aLtd7nK8LkNHa3WVrbvRGHrzdMHfpzF/M/5fAk8GfRYugo39knf
|
||||
VLKGyQCXNI8Y1iHGEmPqQZIFPTjBL6caIlbEV0VHlxoSOGB6JVxcllxAEvd6abqX
|
||||
RJVJPQzzGfEnMNYp9SiZQ9bvDRUsUkWyYwIDAQABo1MwUTAdBgNVHQ4EFgQUAZMN
|
||||
F9JAGHbA3jGOeu6bWFvSdWkwHwYDVR0jBBgwFoAUAZMNF9JAGHbA3jGOeu6bWFvS
|
||||
dWkwDwYDVR0TAQH/BAUwAwEB/zANBgkqhkiG9w0BAQsFAAOCAQEAicBli36ISJFu
|
||||
lrJqOHVqTeNP6go0I35VGnP44nEEP5cBvRD3XntBFEk5D3mSNNOGt+2ncxom8VR9
|
||||
FsLuTfHAipXePJI6MSxFuBPea8V/YPBs3npk5f1FRvJ5vEgtzFvBjsKmp1dS9hH0
|
||||
KUWtWcsAkO2Anc/LVc0xxSidL8NjzYoEFqiki0TNNwCJjmd9XwnBLHW38sEb/pgy
|
||||
KTyRpOyG3Zg2UDjBHiXPBrmIvVFLB6+LrPNvfr1k4HjIgVY539ZXUvVMDKytMrDY
|
||||
h63EMDn4kkPpxXlufgWGybjN5D51OylyWBZLe+L1DQyWEg0Vd7GwPzb6p7bmI7MP
|
||||
pooqbgbDpQ==
|
||||
-----END CERTIFICATE-----
|
|
@ -0,0 +1,30 @@
|
|||
-----BEGIN ENCRYPTED PRIVATE KEY-----
|
||||
MIIFHDBOBgkqhkiG9w0BBQ0wQTApBgkqhkiG9w0BBQwwHAQInmQVkXP3TFcCAggA
|
||||
MAwGCCqGSIb3DQIJBQAwFAYIKoZIhvcNAwcECGeCAVH1pefxBIIEyD3Nj1Dy19oy
|
||||
fogU+z8YBLXuSCx8s3zncYPF9nYlegGSSo0ace/WxfPu8AEPus1P2MxlxfcCQ1A+
|
||||
5+vMihtEpgpTg9R4RlLAWs45jz4AduGiwqW05+W5zgDn6g7p7HIL0+M5FxKRkAW0
|
||||
KEH4Jy8Vc1XQxkhOm1Q4NLI8PT94rcBDE9Od03sdrW/hQgaOFz5AWOlT5jF1uUOz
|
||||
glF1RQQxfJygTB6qlPTC3BAaiAnWij3NOg5L5vvUhjLa7iOZOhRQBRkf4YtHsM+2
|
||||
rFy8Z7MeHOvrqFf8LXosNy3JreQW036rLGR0Xh5myATkNrEwA8df37AgLUmwqyfz
|
||||
hjZefPW77LgMAXlaN8s345AGikOX8yQKEFzPV/Nag32p6t4oiRRcUUfdB4wzKi6T
|
||||
mzZ6lKcGR3qqL4V6lJSV3I2fmgkYZnUwymolyu+1+CVYDLuE53TBi5dRXwgOghi7
|
||||
npw7PqqQCian8yxHF9c1rYukD0ov0/y8ratjOu9XoJG2/wWQJNvDkAyc3mSJf+3y
|
||||
6Wtu1qhLszU8pZOGW0fK6bGyHSp+wkoah/vRzB0+yFjvuMIG6py2ZDQeqhqS3ZV2
|
||||
nZHHjj0tZ45Wbdf4k17ujEK34pFXluPH//zADnd6ym2W0t6x+jtqR5tYu3poORQg
|
||||
jFgpudkn2RUSq8N/gIiHDwblYBxU2dmyzEVudv1zNgVSHyetGLxsFoNB7Prn89rJ
|
||||
u24a/xtuCyC2pshWo3KiL74hkkCsC8rLbEAAbADheb35b+Ca3JnMwgyUHbHL6Hqf
|
||||
EiVIgm14lB/1uz651X58Boo6tDFkgrxEtGDUIZm8yk2n0tGflp7BtYbMCw+7gqhb
|
||||
XN4hlhFDcCJm8peXcyCtGajOnBuNO9JJDNYor6QjptaIpQBFb7/0rc7kyO12BIUv
|
||||
F9mrCHF18Hd/9AtUO93+tyDAnL64Jqq9tUv8dOVtIfbcHXZSYHf24l0XAiKByb8y
|
||||
9NQLUZkIuF4aUZVHV8ZBDdHNqjzqVglKQlGHdw1XBexSal5pC9HvknOmWBgl0aza
|
||||
flzeTRPX7TPrMJDE5lgSy58czGpvZzhFYwOp6cwpfjNsiqdzD78Zs0xsRbNg519s
|
||||
d+cLmbiU3plWCoYCuDb68eZRRzT+o41+QJG2PoMCpzPw5wMLl6HuW7HXMRFpZKJc
|
||||
tPKpeTIzb8hjhA+TwVIVpTPHvvQehtTUQD2mRujdvNM6PF8tnuC3F3sB3PTjeeJg
|
||||
uzfEfs3BynRTIj/gX6y87gzwsrwWIEN6U0cCbQ6J1EcgdQCiH8vbhIgfd4DkLgLN
|
||||
Kkif+fI/HgBOqaiwSw3sHmWgB6PllVQOKH6qAiejTHR/UUvJTPvgKJFLunmBiF12
|
||||
N1bRge1sSXE1eLKVdi+dP1j0o6RxhaRrbX7ie3y/wYHwCJnb8h08DEprgCqoswFs
|
||||
SuNKmvlibBHAsnOdhyCTOd9I5n8XzAUUp6mT+C5WDfl7qfYvh6IHFlSrhZ9aS9b6
|
||||
RY873cnphKbqU5d7Cr8Ufx4b4SgS+hEnuP8y5IToLQ3BONGQH2lu7nmd89wjW0uo
|
||||
IMRXybwf/5FnKhEy8Aw+pD6AxiXC3DZVTKl3SHmjkYBDvNElsJVgygVTKgbOa1Z+
|
||||
ovIK/D7QV7Nv3uVortH8XA==
|
||||
-----END ENCRYPTED PRIVATE KEY-----
|
|
@ -0,0 +1,16 @@
|
|||
-----BEGIN CERTIFICATE REQUEST-----
|
||||
MIIChzCCAW8CAQAwFDESMBAGA1UEAwwJbG9jYWxob3N0MIIBIjANBgkqhkiG9w0B
|
||||
AQEFAAOCAQ8AMIIBCgKCAQEAlPThqu8tfJ4hQKRiUw/vNPfo2L2LQU8NlrRL7rvV
|
||||
71E345LGK1h/hM3MHp5VgEvaaIibb0hSNv/TYz3HVCQyNuPlcmkHZTJ9mB0icilU
|
||||
rYWdM0LPIg46iThmIQVhMiNfpMKQLDLQ7o3Jktjm32OxnQdtYSV+7NFnw8/0pB4j
|
||||
iaiBYfZIMeGzEJIOFG8GSNJG0pfCI71DyLRonIcb2XzfeDPHeWSF7lbIoMGAuKIE
|
||||
2mXpwHmAjTMJzIShSgLqCvmbz7wR3ZeVMknXcgcqMmagGphy8SjizIWC5KRbrnRq
|
||||
F22Ouxdat6scIevRXGp5nYawFYdpK9qo+82gEouVX3dtSQIDAQABoC4wLAYJKoZI
|
||||
hvcNAQkOMR8wHTAbBgNVHREEFDAShxAAAAAAAAAAAAAAAAAAAAAAMA0GCSqGSIb3
|
||||
DQEBCwUAA4IBAQB2qU354OlNVunhZhiOFNwabovxLcgKoQz+GtJ2EzsMEza+NPvV
|
||||
dttPxXzqL/U+gDghvGzSYGuh2yMfTTPO+XtZKpvMUmIWonN5jItbFwSTaWcoE8Qs
|
||||
zFZokRuFJ9dy017u642mpdf6neUzjbfCjWs8+3jyFzWlkrMF3RlSTxPuksWjhXsX
|
||||
dxxLNu8YWcsYRB3fODHqrlBNuDn+9kb9z8to+yq76MA0HtdDkjd/dfgghiTDJhqm
|
||||
IcwhBXufwQUrOP4YiuiwM0mo7Xlhw65gnSmRcwR9ha98SV2zG5kiRYE+m+94bDbd
|
||||
kGBRfhpQSzh1w09cVzmLgzkfxRShEB+bb9Ss
|
||||
-----END CERTIFICATE REQUEST-----
|
|
@ -0,0 +1,20 @@
|
|||
-----BEGIN CERTIFICATE-----
|
||||
MIIDWjCCAkKgAwIBAgIUMZkgD5gtoa39H9jdI/ijVkyxC/swDQYJKoZIhvcNAQEL
|
||||
BQAwWTELMAkGA1UEBhMCQVUxEzARBgNVBAgMClNvbWUtU3RhdGUxITAfBgNVBAoM
|
||||
GEludGVybmV0IFdpZGdpdHMgUHR5IEx0ZDESMBAGA1UEAwwJbG9jYWxob3N0MB4X
|
||||
DTIzMTAxNzIzMDg1M1oXDTQzMTAxNzIzMDg1M1owFDESMBAGA1UEAwwJbG9jYWxo
|
||||
b3N0MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAlPThqu8tfJ4hQKRi
|
||||
Uw/vNPfo2L2LQU8NlrRL7rvV71E345LGK1h/hM3MHp5VgEvaaIibb0hSNv/TYz3H
|
||||
VCQyNuPlcmkHZTJ9mB0icilUrYWdM0LPIg46iThmIQVhMiNfpMKQLDLQ7o3Jktjm
|
||||
32OxnQdtYSV+7NFnw8/0pB4jiaiBYfZIMeGzEJIOFG8GSNJG0pfCI71DyLRonIcb
|
||||
2XzfeDPHeWSF7lbIoMGAuKIE2mXpwHmAjTMJzIShSgLqCvmbz7wR3ZeVMknXcgcq
|
||||
MmagGphy8SjizIWC5KRbrnRqF22Ouxdat6scIevRXGp5nYawFYdpK9qo+82gEouV
|
||||
X3dtSQIDAQABo18wXTAbBgNVHREEFDAShxAAAAAAAAAAAAAAAAAAAAAAMB0GA1Ud
|
||||
DgQWBBTKJU+NK7Q6ZPccSigRCMBCBgjkaDAfBgNVHSMEGDAWgBQBkw0X0kAYdsDe
|
||||
MY567ptYW9J1aTANBgkqhkiG9w0BAQsFAAOCAQEAXuCs6MGVoND8TaJ6qaDmqtpy
|
||||
wKEW2hsGclI9yv5cMS0XCVTkmKYnIoijtqv6Pdh8PfhIx5oJqJC8Ml16w4Iou4+6
|
||||
kKF0DdzdQyiM0OlNCgLYPiR4rh0ZCAFFCvOsDum1g+b9JTFZGooK4TMd9thwms4D
|
||||
SqpP5v1NWf/ZLH5TYnp2CkPzBxDlnMJZphuWtPHL+78TbgQuQaKu2nMLBGBJqtFi
|
||||
HDOGxckgZuwBsy0c+aC/ZwaV7FdMP42kxUZduCEx8+BDSGwPoEpz6pwVIkjiyYAm
|
||||
3O8FUeEPzYzwpkANIbbEIDWV6FVH9IahKRRkE+bL3BqoQkv8SMciEA5zWsPrbA==
|
||||
-----END CERTIFICATE-----
|
|
@ -0,0 +1,28 @@
|
|||
-----BEGIN PRIVATE KEY-----
|
||||
MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQCU9OGq7y18niFA
|
||||
pGJTD+809+jYvYtBTw2WtEvuu9XvUTfjksYrWH+EzcwenlWAS9poiJtvSFI2/9Nj
|
||||
PcdUJDI24+VyaQdlMn2YHSJyKVSthZ0zQs8iDjqJOGYhBWEyI1+kwpAsMtDujcmS
|
||||
2ObfY7GdB21hJX7s0WfDz/SkHiOJqIFh9kgx4bMQkg4UbwZI0kbSl8IjvUPItGic
|
||||
hxvZfN94M8d5ZIXuVsigwYC4ogTaZenAeYCNMwnMhKFKAuoK+ZvPvBHdl5UySddy
|
||||
ByoyZqAamHLxKOLMhYLkpFuudGoXbY67F1q3qxwh69FcanmdhrAVh2kr2qj7zaAS
|
||||
i5Vfd21JAgMBAAECggEACTBuN4hXywdKT92UP0GNZTwh/jT7QUUqNnDa+lhWI1Rk
|
||||
WUK1vPjRrRSxEfZ8mdSUHbzHsf7JK6FungGyqUsuWdqHTh6SmTibLOYnONm54paK
|
||||
kx38/0HXdJ2pF0Jos5ohDV3/XOqpnv3aQJfm7kMNMv3BTqvsf5mPiDHtCq7dTGGj
|
||||
rGiLc0zirKZq79C6YSB1UMB01BsDl2ScflK8b3osT18uYx/BOdjLT4yZWQsU/nbB
|
||||
OeF+ziWTTUAVjodGeTf+NYG7cFN/9N9PdSnAwuw8Nche3xZKbHTh2I578Zd4bsDX
|
||||
H+hoMN862nzOXEvD6KyLB8xDdnEZ+p+njeDROJVmgQKBgQDQhzQEl/co1LYc5IDO
|
||||
mynhCOtKJeRWBLhYEPIuaSY3qF+lrOWzqyOUNppWDx+HeKOq70X1Q+ETeSXtbaL1
|
||||
qHBkNcApQ2lStcpkR9whcVbr9NIWC8y8UQxyerEK3x3l0bZ99dfJ/z6lbxdS7prc
|
||||
Hhxy6pUj8Q8AgpTZA8HfQUF1EQKBgQC23ek24kTVvWeWX2C/82H1Yfia6ITL7WHz
|
||||
3aEJaZaO5JD3KmOSZgY88Ob3pkDTRYjFZND5zSB7PnM68gpo/OEDla6ZYtfwBWCX
|
||||
q4QhFtv2obehobmDk+URVfvlOcBikoEP1i8oy7WdZ5CgC4gNKkkD15l68W+g5IIG
|
||||
2ZOA97yUuQKBgDAzoI2TRxmUGciR9UhMy6Bt/F12ZtKPYsFQoXqi6aeh7wIP9kTS
|
||||
wXWoLYLJGiOpekOv7X7lQujKbz7zweCBIAG5/wJKx9TLms4VYkgEt+/w9oMMFTZO
|
||||
kc8Al14I9xNBp6p0In5Z1vRMupp79yX8e90AZpsZRLt8c8W6PZ1Kq0PRAoGBAKmD
|
||||
7LzD46t/eJccs0M9CoG94Ac5pGCmHTdDLBTdnIO5vehhkwwTJ5U2e+T2aQFwY+kY
|
||||
G+B1FrconQj3dk78nFoGV2Q5DJOjaHcwt7s0xZNLNj7O/HnMj3wSiP9lGcJGrP1R
|
||||
P0ZCEIlph9fU2LnbiPPW2J/vT9uF+EMBTosvG9GBAoGAEVaDLLXOHj+oh1i6YY7s
|
||||
0qokN2CdeKY4gG7iKjuDFb0r/l6R9uFvpUwJMhLEkF5SPQMyrzKFdnTpw3n/jnRa
|
||||
AWG6GoV+D7LES+lHP5TXKKijbnHJdFjW8PtfDXHCJ6uGG91vH0TMMp1LqhcvGfTv
|
||||
lcNGXkk6gUNSecxBC1uJfKE=
|
||||
-----END PRIVATE KEY-----
|
|
@ -65,6 +65,7 @@ include ":grpc-benchmarks"
|
|||
include ":grpc-services"
|
||||
include ":grpc-servlet"
|
||||
include ":grpc-servlet-jakarta"
|
||||
include ":grpc-s2a"
|
||||
include ":grpc-xds"
|
||||
include ":grpc-bom"
|
||||
include ":grpc-rls"
|
||||
|
@ -100,6 +101,7 @@ project(':grpc-benchmarks').projectDir = "$rootDir/benchmarks" as File
|
|||
project(':grpc-services').projectDir = "$rootDir/services" as File
|
||||
project(':grpc-servlet').projectDir = "$rootDir/servlet" as File
|
||||
project(':grpc-servlet-jakarta').projectDir = "$rootDir/servlet/jakarta" as File
|
||||
project(':grpc-s2a').projectDir = "$rootDir/s2a" as File
|
||||
project(':grpc-xds').projectDir = "$rootDir/xds" as File
|
||||
project(':grpc-bom').projectDir = "$rootDir/bom" as File
|
||||
project(':grpc-rls').projectDir = "$rootDir/rls" as File
|
||||
|
|
Loading…
Reference in New Issue