Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[Master] Add support for configuring server name to be used in the SSL SNI extension #2234

Merged
merged 4 commits into from
Dec 4, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
102 changes: 102 additions & 0 deletions ballerina-tests/http-security-tests/tests/ssl_sni_host_name_test.bal
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
// Copyright (c) 2024 WSO2 Inc. (http://www.wso2.org).
//
// WSO2 Inc. licenses this file to you 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.

import ballerina/http;
import ballerina/test;
import ballerina/http_test_common as common;

listener http:Listener http2SniListener = new (http2SniListenerPort, http2SslServiceConf);

http:ListenerConfiguration http1SniServiceConf = {
httpVersion: http:HTTP_1_1,
secureSocket: {
key: {
path: common:KEYSTORE_PATH,
password: "ballerina"
}
}
};

listener http:Listener http1SniListener = new (http1SniListenerPort, http1SniServiceConf);

service /http2SniService on http2SniListener {
resource function get .() returns string {
return "Sni works with HTTP_2!";
}
}

service /http1SniService on http1SniListener {
resource function get .() returns string {
return "Sni works with HTTP_1.1!";
}
}

http:ClientConfiguration http2SniClientConf = {
secureSocket: {
cert: {
path: common:TRUSTSTORE_PATH,
password: "ballerina"
},
serverName: "localhost"
TharmiganK marked this conversation as resolved.
Show resolved Hide resolved
}
};

http:ClientConfiguration http1SniClientConf = {
httpVersion: http:HTTP_1_1,
secureSocket: {
cert: {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Shall we have a test without the cert i.e. with JVM default certs?

Copy link
Member Author

@Bhashinee Bhashinee Dec 4, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We cannot configure only the serverName in the secureSocket config. It gives a runtime error.
Need to configure cert with client SSL certificates file
I think this is a bug. :D

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think that is the usecase for the internal issue. I think we need to fix it as well. Lets address that in a separate issue

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Then I'll fix that as well.

path: common:TRUSTSTORE_PATH,
password: "ballerina"
},
serverName: "localhost"
TharmiganK marked this conversation as resolved.
Show resolved Hide resolved
}
};

http:ClientConfiguration http1SniClientConf2 = {
httpVersion: http:HTTP_1_1,
secureSocket: {
cert: {
path: common:TRUSTSTORE_PATH,
password: "ballerina"
},
serverName: "xxxx"
}
};

@test:Config {}
public function testHttp2WithSni() returns error? {
http:Client clientEP = check new ("https://127.0.0.1:9207", http2SniClientConf);
string resp = check clientEP->get("/http2SniService/");
common:assertTextPayload(resp, "Sni works with HTTP_2!");
}

@test:Config {}
public function testHttp1WithSni() returns error? {
http:Client clientEP = check new ("https://127.0.0.1:9208", http1SniClientConf);
string resp = check clientEP->get("/http1SniService/");
common:assertTextPayload(resp, "Sni works with HTTP_1.1!");
}

@test:Config {}
public function testSniFailure() returns error? {
http:Client clientEP = check new ("https://127.0.0.1:9208", http1SniClientConf2);
string|error resp = clientEP->get("/http1SniService/");
if resp is error {
test:assertEquals(resp.message(), "SSL connection failed:No subject alternative names present /127.0.0.1:9208");
} else {
test:assertFail("Test `testSniFailure` is expecting an error. But received a success response");
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -19,3 +19,6 @@ const int stsPort = 9445;

const int http2GeneralPort = 9100;
const int http2SslGeneralPort = 9107;

const int http2SniListenerPort = 9207;
const int http1SniListenerPort = 9208;
2 changes: 2 additions & 0 deletions ballerina/http_client_config.bal
Original file line number Diff line number Diff line change
Expand Up @@ -96,6 +96,7 @@ public type RetryConfig record {|
# + shareSession - Enable/disable new SSL session creation
# + handshakeTimeout - SSL handshake time out
# + sessionTimeout - SSL session time out
# + serverName - Server name indication(SNI) to be used. If this is not present, hostname from the target URL will be used
public type ClientSecureSocket record {|
boolean enable = true;
crypto:TrustStore|string cert?;
Expand All @@ -114,6 +115,7 @@ public type ClientSecureSocket record {|
boolean shareSession = true;
decimal handshakeTimeout?;
decimal sessionTimeout?;
string serverName?;
|};

# Provides configurations for controlling the endpoint's behaviour in response to HTTP redirect related responses.
Expand Down
1 change: 1 addition & 0 deletions changelog.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
[Semantic Versioning](https://semver.org/spec/v2.0.0.html).

## [Unreleased]
- [Add support for configuring server name to be used in the SSL SNI extension](https://github.com/ballerina-platform/ballerina-library/issues/7435)

### Added

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -486,6 +486,7 @@ public final class HttpConstants {
public static final BString SECURESOCKET_CONFIG_VERIFY_CLIENT = StringUtils.fromString("verifyClient");
public static final BString SECURESOCKET_CONFIG_CERT_VALIDATION_TYPE_OCSP_STAPLING =
StringUtils.fromString("OCSP_STAPLING");
public static final BString SECURESOCKET_CONFIG_SNI_HOST_NAME = StringUtils.fromString("serverName");

//Socket Config
public static final BString SOCKET_CONFIG = StringUtils.fromString("socketConfig");
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1900,6 +1900,11 @@ private static void evaluateCommonFields(BMap<BString, Object> secureSocket, Ssl
Parameter enableSessionCreationParam = new Parameter(HttpConstants.SECURESOCKET_CONFIG_SHARE_SESSION.getValue(),
enableSessionCreation);
paramList.add(enableSessionCreationParam);
if (secureSocket.containsKey(HttpConstants.SECURESOCKET_CONFIG_SNI_HOST_NAME)) {
Parameter sniHostNameParam = new Parameter(HttpConstants.SECURESOCKET_CONFIG_SNI_HOST_NAME.getValue(),
String.valueOf(secureSocket.getStringValue(HttpConstants.SECURESOCKET_CONFIG_SNI_HOST_NAME)));
paramList.add(sniHostNameParam);
}
}

private static BMap<BString, Object> getBMapValueIfPresent(BMap<BString, Object> map, BString key) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ public final class Constants {
public static final String CLIENT_SUPPORT_CIPHERS = "ciphers";
public static final String CLIENT_SUPPORT_SSL_PROTOCOLS = "sslEnabledProtocols";
public static final String CLIENT_ENABLE_SESSION_CREATION = "shareSession";
public static final String SNI_SERVER_NAME = "serverName";
public static final String MUTUAL_SSL_PASSED = "passed";
public static final String MUTUAL_SSL_FAILED = "failed";
public static final String MUTUAL_SSL_DISABLED = "disabled";
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@
import static io.ballerina.stdlib.http.transport.contract.Constants.SERVER_SUPPORTED_SNIMATCHERS;
import static io.ballerina.stdlib.http.transport.contract.Constants.SERVER_SUPPORT_CIPHERS;
import static io.ballerina.stdlib.http.transport.contract.Constants.SERVER_SUPPORT_SSL_PROTOCOLS;
import static io.ballerina.stdlib.http.transport.contract.Constants.SNI_SERVER_NAME;
import static io.ballerina.stdlib.http.transport.contract.Constants.TLS_PROTOCOL;
/**
* SSL configuration for HTTP connection.
Expand Down Expand Up @@ -277,6 +278,9 @@ private SSLConfig getSSLConfigForSender() {
case CLIENT_ENABLE_SESSION_CREATION:
sslConfig.setEnableSessionCreation(Boolean.parseBoolean(parameter.getValue()));
break;
case SNI_SERVER_NAME:
sslConfig.setSniHostName(parameter.getValue());
break;
default:
//do nothing
break;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -419,7 +419,7 @@ private static SSLEngine getSslEngineForCerts(SocketChannel socketChannel, Strin
SslHandler sslHandler = sslContext.newHandler(socketChannel.alloc(), host, port);
SSLEngine sslEngine = sslHandler.engine();
sslHandlerFactory.addCommonConfigs(sslEngine);
sslHandlerFactory.setSNIServerNames(sslEngine, host);
configureSniServerName(sslConfig, host, sslHandlerFactory, sslEngine);
if (sslConfig.isHostNameVerificationEnabled()) {
setHostNameVerfication(sslEngine);
}
Expand Down Expand Up @@ -514,14 +514,20 @@ private static SSLEngine instantiateAndConfigSSL(SSLConfig sslConfig, String hos
if (sslConfig != null) {
sslEngine = sslHandlerFactory.buildClientSSLEngine(host, port);
sslEngine.setUseClientMode(true);
sslHandlerFactory.setSNIServerNames(sslEngine, host);
configureSniServerName(sslConfig, host, sslHandlerFactory, sslEngine);
if (hostNameVerificationEnabled) {
sslHandlerFactory.setHostNameVerfication(sslEngine);
}
}
return sslEngine;
}

private static void configureSniServerName(SSLConfig sslConfig, String host, SSLHandlerFactory sslHandlerFactory,
SSLEngine sslEngine) {
sslHandlerFactory.setSNIServerNames(sslEngine,
sslConfig.getSniHostName() != null ? sslConfig.getSniHostName() : host);
}

/**
* Get integer type property value from a property map.
* <p>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,7 @@ public class SSLConfig {
private long handshakeTimeOut;
private boolean disableSsl = false;
private boolean useJavaDefaults = false;
private String sniHostName;

public SSLConfig() {}

Expand Down Expand Up @@ -179,6 +180,14 @@ public void setEnableSessionCreation(boolean enableSessionCreation) {
this.enableSessionCreation = enableSessionCreation;
}

public void setSniHostName(String sniHostName) {
this.sniHostName = sniHostName;
}

public String getSniHostName() {
return sniHostName;
}

public String[] getEnableProtocols() {
return enableProtocols == null ? null : enableProtocols.clone();
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -213,7 +213,8 @@ private void configureSslForHttp2(SocketChannel ch, ChannelPipeline clientPipeli
SslContext sslCtx = sslHandlerFactory.createHttp2TLSContextForClient(false);
SslHandler sslHandler = sslCtx.newHandler(ch.alloc(), httpRoute.getHost(), httpRoute.getPort());
SSLEngine sslEngine = sslHandler.engine();
sslHandlerFactory.setSNIServerNames(sslEngine, httpRoute.getHost());
sslHandlerFactory.setSNIServerNames(sslEngine,
sslConfig.getSniHostName() != null ? sslConfig.getSniHostName() : httpRoute.getHost());
if (sslConfig.isHostNameVerificationEnabled()) {
setHostNameVerfication(sslEngine);
}
Expand Down
Loading