This is an automated email from the ASF dual-hosted git repository.

wusheng pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/skywalking-java.git


The following commit(s) were added to refs/heads/main by this push:
     new dd0d8af  Support mTLS for gRPC channel (#15)
dd0d8af is described below

commit dd0d8afe8849d6e86a1e0d61933a13172f782309
Author: Daming <[email protected]>
AuthorDate: Tue Aug 31 15:56:30 2021 +0800

    Support mTLS for gRPC channel (#15)
---
 CHANGES.md                                         |  1 +
 .../skywalking/apm/agent/core/conf/Config.java     | 15 ++++
 .../apm/agent/core/remote/TLSChannelBuilder.java   | 39 ++++++++---
 .../apm/agent/core/util/PrivateKeyUtil.java        | 80 ++++++++++++++++++++++
 apm-sniffer/config/agent.config                    |  8 +++
 docs/en/setup/service-agent/java-agent/TLS.md      | 43 +++++++++---
 .../service-agent/java-agent/configurations.md     |  3 +
 7 files changed, 172 insertions(+), 17 deletions(-)

diff --git a/CHANGES.md b/CHANGES.md
index 04cb623..f97699e 100644
--- a/CHANGES.md
+++ b/CHANGES.md
@@ -14,6 +14,7 @@ Release Notes.
 * Fix kafka-reporter-plugin shade package conflict
 * Add all config items to `agent.conf` file for convenient containerization 
use cases.
 * Advanced Kafka Producer configuration enhancement.
+* Suport mTLS for gRPC channel.
 
 #### Documentation
 
diff --git 
a/apm-sniffer/apm-agent-core/src/main/java/org/apache/skywalking/apm/agent/core/conf/Config.java
 
b/apm-sniffer/apm-agent-core/src/main/java/org/apache/skywalking/apm/agent/core/conf/Config.java
index 85c2b9c..b646416 100755
--- 
a/apm-sniffer/apm-agent-core/src/main/java/org/apache/skywalking/apm/agent/core/conf/Config.java
+++ 
b/apm-sniffer/apm-agent-core/src/main/java/org/apache/skywalking/apm/agent/core/conf/Config.java
@@ -135,6 +135,21 @@ public class Config {
          * Force open TLS for gRPC channel if true.
          */
         public static boolean FORCE_TLS = false;
+
+        /**
+         * SSL trusted ca file. If it exists, will enable TLS for gRPC channel.
+         */
+        public static String SSL_TRUSTED_CA_PATH = "ca" + 
Constants.PATH_SEPARATOR + "ca.crt";
+
+        /**
+         * Key cert chain file. If ssl_cert_chain and ssl_key exist, will 
enable mTLS for gRPC channel.
+         */
+        public static String SSL_CERT_CHAIN_PATH;
+
+        /**
+         * Private key file. If ssl_cert_chain and ssl_key exist, will enable 
mTLS for gRPC channel.
+         */
+        public static String SSL_KEY_PATH;
     }
 
     public static class OsInfo {
diff --git 
a/apm-sniffer/apm-agent-core/src/main/java/org/apache/skywalking/apm/agent/core/remote/TLSChannelBuilder.java
 
b/apm-sniffer/apm-agent-core/src/main/java/org/apache/skywalking/apm/agent/core/remote/TLSChannelBuilder.java
index 5a5e769..6732467 100644
--- 
a/apm-sniffer/apm-agent-core/src/main/java/org/apache/skywalking/apm/agent/core/remote/TLSChannelBuilder.java
+++ 
b/apm-sniffer/apm-agent-core/src/main/java/org/apache/skywalking/apm/agent/core/remote/TLSChannelBuilder.java
@@ -23,31 +23,54 @@ import io.grpc.netty.NegotiationType;
 import io.grpc.netty.NettyChannelBuilder;
 import io.netty.handler.ssl.SslContextBuilder;
 import java.io.File;
-import javax.net.ssl.SSLException;
+import java.io.FileInputStream;
+import java.io.IOException;
+import java.io.InputStream;
 import org.apache.skywalking.apm.agent.core.boot.AgentPackageNotFoundException;
 import org.apache.skywalking.apm.agent.core.boot.AgentPackagePath;
 import org.apache.skywalking.apm.agent.core.conf.Config;
-import org.apache.skywalking.apm.agent.core.conf.Constants;
+import org.apache.skywalking.apm.agent.core.logging.api.ILog;
+import org.apache.skywalking.apm.agent.core.logging.api.LogManager;
+import org.apache.skywalking.apm.agent.core.util.PrivateKeyUtil;
+import org.apache.skywalking.apm.util.StringUtil;
 
 /**
- * Detect the `/ca` folder in agent package, if `ca.crt` exists, start TLS (no 
mutual auth).
+ * If only ca.crt exists, start TLS. If cert, key and ca files exist, enable 
mTLS.
  */
 public class TLSChannelBuilder implements ChannelBuilder<NettyChannelBuilder> {
-    private static String CA_FILE_NAME = "ca" + Constants.PATH_SEPARATOR + 
"ca.crt";
+    private static final ILog LOGGER = 
LogManager.getLogger(TLSChannelBuilder.class);
 
     @Override
     public NettyChannelBuilder build(
-        NettyChannelBuilder managedChannelBuilder) throws 
AgentPackageNotFoundException, SSLException {
-        File caFile = new File(AgentPackagePath.getPath(), CA_FILE_NAME);
+        NettyChannelBuilder managedChannelBuilder) throws 
AgentPackageNotFoundException, IOException {
+
+        File caFile = new File(AgentPackagePath.getPath(), 
Config.Agent.SSL_TRUSTED_CA_PATH);
         boolean isCAFileExist = caFile.exists() && caFile.isFile();
         if (Config.Agent.FORCE_TLS || isCAFileExist) {
             SslContextBuilder builder = GrpcSslContexts.forClient();
+
             if (isCAFileExist) {
+                String certPath = Config.Agent.SSL_CERT_CHAIN_PATH;
+                String keyPath = Config.Agent.SSL_KEY_PATH;
+                if (StringUtil.isNotBlank(certPath) && 
StringUtil.isNotBlank(keyPath)) {
+                    File keyFile = new File(AgentPackagePath.getPath(), 
keyPath);
+                    File certFile = new File(AgentPackagePath.getPath(), 
certPath);
+
+                    if (certFile.isFile() && keyFile.isFile()) {
+                        try (InputStream cert = new FileInputStream(certFile);
+                             InputStream key = 
PrivateKeyUtil.loadDecryptionKey(keyPath)) {
+                            builder.keyManager(cert, key);
+                        }
+                    } else if (!certFile.isFile() || !keyFile.isFile()) {
+                        LOGGER.warn("Failed to enable mTLS caused by cert or 
key cannot be found.");
+                    }
+                }
+
                 builder.trustManager(caFile);
             }
-            managedChannelBuilder = 
managedChannelBuilder.negotiationType(NegotiationType.TLS)
-                                                         
.sslContext(builder.build());
+            
managedChannelBuilder.negotiationType(NegotiationType.TLS).sslContext(builder.build());
         }
         return managedChannelBuilder;
     }
+
 }
diff --git 
a/apm-sniffer/apm-agent-core/src/main/java/org/apache/skywalking/apm/agent/core/util/PrivateKeyUtil.java
 
b/apm-sniffer/apm-agent-core/src/main/java/org/apache/skywalking/apm/agent/core/util/PrivateKeyUtil.java
new file mode 100644
index 0000000..c0c0625
--- /dev/null
+++ 
b/apm-sniffer/apm-agent-core/src/main/java/org/apache/skywalking/apm/agent/core/util/PrivateKeyUtil.java
@@ -0,0 +1,80 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF 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.
+ */
+
+package org.apache.skywalking.apm.agent.core.util;
+
+import java.io.ByteArrayInputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.nio.charset.StandardCharsets;
+import java.nio.file.Files;
+import java.nio.file.Paths;
+import java.util.Base64;
+
+/**
+ * Util intends to parse PKCS#1 and PKCS#8 at same time.
+ */
+public class PrivateKeyUtil {
+    private static final String PKCS_1_PEM_HEADER = "-----BEGIN RSA PRIVATE 
KEY-----";
+    private static final String PKCS_1_PEM_FOOTER = "-----END RSA PRIVATE 
KEY-----";
+    private static final String PKCS_8_PEM_HEADER = "-----BEGIN PRIVATE 
KEY-----";
+    private static final String PKCS_8_PEM_FOOTER = "-----END PRIVATE 
KEY-----";
+
+    /**
+     * Load a RSA decryption key from a file (PEM or DER).
+     */
+    public static InputStream loadDecryptionKey(String keyFilePath) throws 
IOException {
+        byte[] keyDataBytes = Files.readAllBytes(Paths.get(keyFilePath));
+        String keyDataString = new String(keyDataBytes, 
StandardCharsets.UTF_8);
+
+        if (keyDataString.contains(PKCS_1_PEM_HEADER)) {
+            // OpenSSL / PKCS#1 Base64 PEM encoded file
+            keyDataString = keyDataString.replace(PKCS_1_PEM_HEADER, "");
+            keyDataString = keyDataString.replace(PKCS_1_PEM_FOOTER, "");
+            keyDataString = keyDataString.replace("\n", "");
+            return 
readPkcs1PrivateKey(Base64.getDecoder().decode(keyDataString));
+        }
+
+        return new ByteArrayInputStream(keyDataString.getBytes());
+    }
+
+    /**
+     * Create a InputStream instance from raw PKCS#1 bytes. Raw Java API can't 
recognize ASN.1 format, so we should
+     * convert it into a pkcs#8 format Java can understand.
+     */
+    private static InputStream readPkcs1PrivateKey(byte[] pkcs1Bytes) {
+        int pkcs1Length = pkcs1Bytes.length;
+        int totalLength = pkcs1Length + 22;
+        byte[] pkcs8Header = new byte[] {
+            0x30, (byte) 0x82, (byte) ((totalLength >> 8) & 0xff), (byte) 
(totalLength & 0xff), // Sequence + total length
+            0x2, 0x1, 0x0, // Integer (0)
+            0x30, 0xD, 0x6, 0x9, 0x2A, (byte) 0x86, 0x48, (byte) 0x86, (byte) 
0xF7, 0xD, 0x1, 0x1, 0x1, 0x5, 0x0, // Sequence: 1.2.840.113549.1.1.1, NULL
+            0x4, (byte) 0x82, (byte) ((pkcs1Length >> 8) & 0xff), (byte) 
(pkcs1Length & 0xff) // Octet string + length
+        };
+        StringBuilder pkcs8 = new StringBuilder(PKCS_8_PEM_HEADER);
+        pkcs8.append("\n").append(new 
String(Base64.getEncoder().encode(join(pkcs8Header, pkcs1Bytes))));
+        pkcs8.append("\n").append(PKCS_8_PEM_FOOTER);
+        return new ByteArrayInputStream(pkcs8.toString().getBytes());
+    }
+
+    private static byte[] join(byte[] byteArray1, byte[] byteArray2) {
+        byte[] bytes = new byte[byteArray1.length + byteArray2.length];
+        System.arraycopy(byteArray1, 0, bytes, 0, byteArray1.length);
+        System.arraycopy(byteArray2, 0, bytes, byteArray1.length, 
byteArray2.length);
+        return bytes;
+    }
+}
diff --git a/apm-sniffer/config/agent.config b/apm-sniffer/config/agent.config
index 5f95696..dd3b654 100755
--- a/apm-sniffer/config/agent.config
+++ b/apm-sniffer/config/agent.config
@@ -68,6 +68,14 @@ agent.keep_tracing=${SW_AGENT_KEEP_TRACING:false}
 # If true, SkyWalking agent uses TLS even no CA file detected.
 agent.force_tls=${SW_AGENT_FORCE_TLS:false}
 
+# gRPC SSL trusted ca file.
+agent.ssl_trusted_ca_path=${SW_AGENT_SSL_TRUSTED_CA_PATH:/ca/ca.crt}
+
+# enable mTLS when ssl_key_path and ssl_cert_chain_path exist.
+agent.ssl_key_path=${SW_AGENT_SSL_KEY_PATH:}
+
+agent.ssl_cert_chain_path=${SW_AGENT_SSL_CERT_CHAIN_PATH:}
+
 # Limit the length of the ipv4 list size.
 osinfo.ipv4_list_size=${SW_AGENT_OSINFO_IPV4_LIST_SIZE:10}
 
diff --git a/docs/en/setup/service-agent/java-agent/TLS.md 
b/docs/en/setup/service-agent/java-agent/TLS.md
index 05cc1a2..5ae9793 100644
--- a/docs/en/setup/service-agent/java-agent/TLS.md
+++ b/docs/en/setup/service-agent/java-agent/TLS.md
@@ -7,20 +7,45 @@ at the same time, the SkyWalking backend is in another region 
(VPC).
 > 
 > Because of that, security requirement is very obvious.
 
+## Creating SSL/TLS Certificates
+
+The first step is to generate certificates and key files for encrypting 
communication. This is
+fairly straightforward: use `openssl` from the command line.
+
+Use this [script](../../../../../tools/TLS/tls_key_generate.sh) if you are not 
familiar with how to generate key files.
+
+We need the following files:
+ - `client.pem`: A private RSA key to sign and authenticate the public key. 
It's either a PKCS#8(PEM) or PKCS#1(DER).
+ - `client.crt`: Self-signed X.509 public keys for distribution.
+ - `ca.crt`: A certificate authority public key for a client to validate the 
server's certificate.
+ 
 ## Authentication Mode
-Only support **no mutual auth**.
-- Use this [script](../../../../../tools/TLS/tls_key_generate.sh) if you are 
not familiar with how to generate key files.
-- Find `ca.crt`, and use it at client side
-- Find `server.crt` ,`server.pem` and `ca.crt`. Use them at server side. 
Please refer to `gRPC SSL` of the OAP server doc.
-  for more details.
+- Find `ca.crt`, and use it at client side. In `mTLS` mode, `client.crt` and 
`client.pem` are required at client side.
+- Find `server.crt`, `server.pem` and `ca.crt`. Use them at server side. 
Please refer to `gRPC Security` of the OAP server doc for more details.
 
 ## Open and config TLS
 
 ### Agent config
-- Place `ca.crt` into `/ca` folder in agent package. Notice, `/ca` is not 
created in distribution, please create it by yourself.
-
-- Agent open TLS automatically after the `/ca/ca.crt` file detected.
+- Agent enables TLS automatically after the `ca.crt`(by default `/ca` folder 
in agent package) file is detected.
 - TLS with no CA mode could be activated by this setting.
 ```
-agent.force_tls=${SW_AGENT_FORCE_TLS:false}
+agent.force_tls=${SW_AGENT_FORCE_TLS:true}
+```
+
+## Enable mutual TLS
+- Sharing gRPC server must be started with mTLS enabled. More details can be 
found in `receiver-sharing-server` section in `application.yaml`. Please refer 
to `gRPC Security` and `gRPC/HTTP server for receiver`.
+- Copy CA certificate, certificate and private key of client into `agent/ca`.
+- Configure client-side SSL/TLS in `agent.conf`.
+- Change `SW_AGENT_COLLECTOR_BACKEND_SERVICES` targeting to host and port of 
`receiver-sharing-server`.
+
+For example:
 ```
+agent.force_tls=${SW_AGENT_FORCE_TLS:true}
+agent.ssl_trusted_ca_path=${SW_AGENT_SSL_TRUSTED_CA_PATH:/ca/ca.crt}
+agent.ssl_key_path=${SW_AGENT_SSL_KEY_PATH:/ca/client.pem}
+agent.ssl_cert_chain_path=${SW_AGENT_SSL_CERT_CHAIN_PATH:/ca/client.crt}
+
+collector.backend_service=${SW_AGENT_COLLECTOR_BACKEND_SERVICES:skywalking-oap:11801}
+```
+
+Notice, the client-side's certificate and the private key are from the same CA 
certificate with server-side.
diff --git a/docs/en/setup/service-agent/java-agent/configurations.md 
b/docs/en/setup/service-agent/java-agent/configurations.md
index 5c14407..324dbd9 100644
--- a/docs/en/setup/service-agent/java-agent/configurations.md
+++ b/docs/en/setup/service-agent/java-agent/configurations.md
@@ -20,6 +20,9 @@ property key | Description | Default |
 `agent.operation_name_threshold `|The operationName max length, setting this 
value > 190 is not recommended.|`150`|
 `agent.keep_tracing`|Keep tracing even the backend is not available if this 
value is `true`.|`false`|
 `agent.force_tls`|Force open TLS for gRPC channel if this value is 
`true`.|`false`|
+`agent.ssl_trusted_ca_path` | gRPC SSL trusted ca file. | `/ca/ca.crt` |
+`agent.ssl_key_path`| The private key file. Enable mTLS when ssl_key_path and 
ssl_cert_chain_path exist. | `""` |
+`agent.ssl_cert_chain_path`| The certificate file. Enable mTLS when 
ssl_key_path and ssl_cert_chain_path exist. | `""` |
 `osinfo.ipv4_list_size`| Limit the length of the ipv4 list size. |`10`|
 `collector.grpc_channel_check_interval`|grpc channel status check 
interval.|`30`|
 `collector.heartbeat_period`|agent heartbeat report period. Unit, second.|`30`|

Reply via email to