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

[2201.7.x] Add support for configuring server name to be used in the SSL SNI extension #2240

Merged
merged 4 commits into from
Dec 6, 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
Original file line number Diff line number Diff line change
Expand Up @@ -61,19 +61,3 @@ public function testSslDisabledClient1() returns error? {
test:assertFail(msg = "Found unexpected output: " + resp.message());
}
}

http:ClientConfiguration disableSslClientConf2 = {
secureSocket: {
}
};

@test:Config {}
public function testSslDisabledClient2() {
http:Client|error httpClient = new ("https://localhost:9238", disableSslClientConf2);
string expectedErrMsg = "Need to configure cert with client SSL certificates file";
if (httpClient is error) {
test:assertEquals(httpClient.message(), expectedErrMsg);
} else {
test:assertFail(msg = "Expected mutual SSL error not found");
}
}
130 changes: 130 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,130 @@
// 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"
}
};

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

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

http:ClientConfiguration http2SniClientConf3 = {
secureSocket: {
serverName: "localhost"
}
};

@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");
}
}

@test:Config {}
public function testSniWhenUsingDefaultCerts() returns error? {
http:Client httpClient = check new("https://www.google.com", http2SniClientConf3);
string|error resp = httpClient->get("/");
// This response is success because even though we send a wrong server name, google.com sends the default cert which
// is valid and trusted by the client.
if resp is error {
test:assertFail("Found unexpected output: " + resp.message());
}
}

@test:Config {}
public function testSniFailureWhenUsingDefaultCerts() returns error? {
http:Client clientEP = check new ("https://127.0.0.1:9208", http2SniClientConf3);
string|error resp = clientEP->get("/http1SniService/");
if resp is error {
common:assertTrueTextPayload(resp.message(), "SSL connection failed:javax.net.ssl.SSLHandshakeException:");
} else {
test:assertFail("Test `testSniFailureWhenUsingDefaultCerts` 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
12 changes: 10 additions & 2 deletions changelog.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,17 @@
# Change Log
This file contains all the notable changes done to the Ballerina HTTP package through the releases.

The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to
[Semantic Versioning](https://semver.org/spec/v2.0.0.html).

## [Unreleased]

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

### Fixed
- [Fix the issue of not being able to configure only server name in the secureSocket config](https://github.com/ballerina-platform/ballerina-library/issues/7443)

## [2.9.10] - 2024-10-21

### Fixed
Expand Down Expand Up @@ -308,7 +316,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
- [Fix missing error of invalid inbound request parameter for forward() method](https://github.com/ballerina-platform/ballerina-standard-library/issues/311)
- [Fix HTTP Circuit Breaker failure when status codes are not provided in the configuration](https://github.com/ballerina-platform/ballerina-standard-library/issues/339)
- [Fix HTTP FailOver client failure when status codes are overridden by an empty array](https://github.com/ballerina-platform/ballerina-standard-library/issues/1598)
- [Fix already built incompatible payload thrown error](https://github.com/ballerina-platform/ballerina-standard-library/issues/1600)
- [Fix already built incompatible payload thrown error](https://github.com/ballerina-platform/ballerina-standard-library/issues/1600)
- [Optional Types Not Supported in HTTP Client Request Operation Target Type](https://github.com/ballerina-platform/ballerina-standard-library/issues/1433)

### Changed
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -458,6 +458,7 @@ public 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 @@ -1388,13 +1388,7 @@ public static void populateSSLConfiguration(SslConfiguration senderConfiguration
}
Object cert = secureSocket.get(HttpConstants.SECURESOCKET_CONFIG_CERT);
if (cert == null) {
BMap<BString, Object> key = getBMapValueIfPresent(secureSocket, HttpConstants.SECURESOCKET_CONFIG_KEY);
if (key != null) {
senderConfiguration.useJavaDefaults();
} else {
throw createHttpError("Need to configure cert with client SSL certificates file",
HttpErrorType.SSL_ERROR);
}
senderConfiguration.useJavaDefaults();
} else {
evaluateCertField(cert, senderConfiguration);
}
Expand Down Expand Up @@ -1865,6 +1859,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 @@ -244,6 +245,7 @@ private SSLConfig getSSLConfigForListener() {
}

private SSLConfig getSSLConfigForSender() {
setSslParameters();
if (sslConfig.isDisableSsl() || sslConfig.useJavaDefaults()) {
return sslConfig;
}
Expand All @@ -264,7 +266,10 @@ private SSLConfig getSSLConfigForSender() {
sslConfig.setSSLProtocol(sslProtocol);
String tlsStoreType = sslConfig.getTLSStoreType() != null ? sslConfig.getTLSStoreType() : JKS;
sslConfig.setTLSStoreType(tlsStoreType);
return sslConfig;
}

private void setSslParameters() {
if (parameters != null) {
for (Parameter parameter : parameters) {
switch (parameter.getName()) {
Expand All @@ -277,12 +282,14 @@ 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;
}
}
}
return sslConfig;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -411,7 +411,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 @@ -506,14 +506,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 @@ -207,7 +207,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