obshellがHTTPリクエストを受信すると、そのリクエストに対してセキュリティ検証を行います。obshellの検証内容は、現在のノードのアイデンティティによって異なり、パスワード、URI、リクエストボディ(Request Body)の検証などが含まれますが、これに限定されません。
使用方法
リクエストの送信
obshellにHTTPリクエストを送信する場合は、次の手順に従って操作します。
AES暗号アルゴリズムを使用して、リクエストボディ(Request Body)の内容を暗号化します。
16ビットのランダムなAES鍵(key)と初期化ベクトル(Initialization Vector、IV)を生成します。
シリアライズされたリクエストボディの内容をCBC暗号モードで暗号化します。
安全性検証用のデータ構造HttpHeaderを構築します。構造体は以下のとおりです。
type HttpHeader struct { Auth string Ts string // Authの有効なタイムスタンプ Uri string // リクエストのURI Keys []byte // HTTPリクエストのBodyを暗号化する際に使用するAES暗号アルゴリズムのkeyとIV }そのうち、代入されるパスワードの種類(
Auth)によって、HTTPリクエストの検証の可否が異なります。Authがobshellノードのパスワードに設定されている場合、すべてのコンポーネント関連のHTTPリクエスト検証に合格します。一方、Authがroot@sysユーザーのパスワードに設定されている場合、OBProxy以外の他のコンポーネントのHTTPリクエスト検証(例えば、OceanBaseデータベースやAgentのデプロイメント、クラスタ運用保守関連リクエストなど)にのみ合格します。obshellノードのパスワードを設定する/api/v1/agent/passwordAPIのリクエスト検証方法も特別であり、詳細についてはノードパスワードの設定を参照してください。RSAアルゴリズムを使用してHttpHeaderを暗号化し、HTTPリクエストヘッダー(Request Headers)に設定します。
/api/v1/secretAPIを呼び出して、ターゲットノードの公開鍵を取得します(このAPIはセキュリティ検証を必要としません)。公開鍵を組み合わせて、シリアライズされたHttpHeader文字列をRSAアルゴリズムで暗号化し、暗号化後にBase64エンコードを行い、さらにUTF-8エンコードの文字列に復号して、暗号化後の文字列Sを得ます。
HTTPリクエストヘッダーを設定します。
Authがobshellノードのパスワードに設定されている場合、リクエストヘッダーを次のように設定します:{"X-OCS-Agent-Header": S}Authがroot@sysパスワードに設定されている場合、リクエストヘッダーを次のように設定します:{"X-OCS-Header": S}
obshell検証プロセス
obshell Serverは、HTTPリクエストを受信した後、次のプロセスを順番に実行して安全性検証を行います。
リクエストヘッダーからキー値ペア「
X-OCS-Header": S」または「X-OCS-Agent-Header": S」を取得します。自身の秘密鍵を組み合わせて、Sを復号し、復号後のHttpHeader構造体を取得します。
HttpHeaderに記録されているAuthが正しいかどうかを確認します。もし
AuthとリクエストAPIが属するコンポーネントがマッチしない場合、該当するエラーメッセージを直接返します。HttpHeaderに記録されているUriが実際のリクエストのUriと一致するかどうかを確認します。
HttpHeaderに記録されているKeysを組み合わせて、リクエストボディの内容を復号し、リクエストボディの平文を得ます。復号に失敗した場合、検証に失敗します。
上記のセキュリティ検証ステップをすべて通過した場合にのみ、obshell Serverはリクエストの処理を続けます。そうでない場合は、リクエスト元に対してセキュリティ検証に失敗したことを応答します。
Pythonによるリクエスト送信コード
以下は、Pythonを使用してリクエストを送信するコード例です。コード内で定義されている ip、port、pwd、uri、body(意味はコードのコメントを参照)を変更するだけで済みます。変更後、このコードをコンパイルして実行すると、対応するHTTPリクエストヘッダーとHTTPリクエストボディが得られます。
説明
以下のPythonスクリプトを実行するには、Python 3.5以降のバージョンが必要です。
import requests as req
import json
from Crypto.PublicKey import RSA
from Crypto.Cipher import PKCS1_v1_5 as PKCS1_cipher
import base64
from datetime import datetime
from Crypto.Cipher import AES
from Crypto.Random import get_random_bytes
from Crypto.util.Padding import pad
aes_key = get_random_bytes(16)
aes_iv = get_random_bytes(16)
max_chunk_size = 53
def encrypt(s, pk):
key = RSA.import_key(base64.b64decode(pk))
cipher = PKCS1_cipher.new(key)
data_to_encrypt = bytes(s.encode('utf8'))
chunks = [data_to_encrypt[i:i + max_chunk_size] for i in range(0, len(data_to_encrypt), max_chunk_size)]
encrypted_chunks = [cipher.encrypt(chunk) for chunk in chunks]
encrypted = b''.join(encrypted_chunks)
encoded_encrypted_chunks = base64.b64encode(encrypted).decode('utf-8')
return encoded_encrypted_chunks
def encrypt_body(body, pk):
cipher = AES.new(aes_key, AES.MODE_CBC, aes_iv)
return base64.b64encode(cipher.encrypt(pad(bytes(body.encode('utf8')), AES.block_size))).decode('utf8')
class Header:
auth: str
ts: str
uri: str
keys: bytes
def __init__(self, auth, ts, uri, keys):
self.auth = auth
self.ts = ts
self.uri = uri
self.keys = keys
def serialize_struct(struct):
return json.dumps({
'auth': struct.auth,
'ts': struct.ts,
'uri': struct.uri,
'keys': base64.b64encode(struct.keys).decode('utf-8')
})
ip = '10.10.10.1' # リクエストノードのIPアドレス
port = 2886 # リクエストノードのサービスポート
pwd = '******' # OceanBaseクラスタsysテナントのrootパスワード
agent_pwd = '******' # obshellノードのパスワード
uri = '/api/v1/ob/init' # リクエストのURI
body = {} # Request Bodyはここで更新
# 公開鍵を取得
resp = req.get(f'<url-placeholder:8f82431b20864404bd54da9f34ba276d').text
resp = json.loads(resp)
pk = resp['data']['public_key']
header = Header(pwd, str(int(datetime.now().timestamp()) + 100000), uri, aes_key + aes_iv)
# X-OCS-Agent-Header
# header = Header(agent_pwd, str(int(datetime.now().timestamp()) + 100000), uri, aes_key + aes_iv)
print("request headers: ", 'X-OCS-Header: ' + encrypt(serialize_struct(header), pk))
# X-OCS-Agent-Header
# print("request headers: ", 'X-OCS-Agent-Header: ' + encrypt(serialize_struct(header), pk))
print("\n")
print("request body: ", encrypt_body(json.dumps(body), pk))
print("\n")
print("encrypt password: ", encrypt(pwd, pk))
Goによるリクエスト送信コード
以下はGoを使用してリクエストを送信するコード例です。コード内で定義されているip、port、pwd、uri、body(意味はコードのコメントを参照)を変更するだけで済みます。変更後にコンパイルして実行すると、対応するHTTPリクエストヘッダーとHTTPリクエストボディが得られます。
package main
import (
"bytes"
"crypto/aes"
"crypto/cipher"
"crypto/rand"
"crypto/rsa"
"crypto/x509"
"encoding/base64"
"encoding/json"
"fmt"
"io"
"net/http"
"time"
)
var (
ip = "10.10.10.1" // リクエストノードのIPアドレス
port = 2886 // リクエストノードのサービスポート
uri = "/api/v1/ob/init" // リクエストのURI
pwd = "******" // OceanBaseクラスタsysテナントのrootパスワード
// agentPwd = "********" // obshellノードのパスワード
body = map[string]interface{}{} // Request Bodyはここで更新
)
var pk string
func main() {
var err error
pk, err = getPublicKey(fmt.Sprintf("http://%s:%d/api/v1/secret", ip, port))
if err != nil {
fmt.Printf("get public key failed: %s", err.Error())
return
}
fmt.Printf("target agent's public key: %s\n\n", pk)
encryptedBody, header, err := BuildBodyAndHeader(uri, body)
if err != nil {
return
}
fmt.Printf("request header: %v\n\nrequest body: %v\n", header, encryptedBody)
}
// getPublicKey関数はAPIから公開鍵を取得します
func getPublicKey(url string) (string, error) {
resp, err := http.Get(url)
if err != nil {
return "", err
}
defer resp.Body.Close()
body, err := io.ReadAll(resp.Body)
if err != nil {
return "", err
}
var response struct {
Data struct {
PublicKey string `json:"public_key"`
} `json:"data"`
}
if err = json.Unmarshal(body, &response); err != nil {
return "", err
}
return response.Data.PublicKey, nil
}
func BuildBodyAndHeader(uri string, param interface{}) (encryptedBody interface{}, header map[string]string, err error) {
encryptedBody, Key, Iv, err := EncryptBodyWithAes(param)
if err != nil {
return nil, nil, err
}
header = BuildHeader(Key, Iv)
return encryptedBody, header, nil
}
type HttpHeader struct {
Auth string
Ts string
Uri string
Keys []byte
}
func BuildHeader(keys ...[]byte) map[string]string {
headers := make(map[string]string)
var aesKeys []byte
if len(keys) != 2 {
aesKeys = nil
} else {
aesKeys = append(keys[0], keys[1]...)
}
header := HttpHeader{
Auth: pwd,
// X-OCS-Agent-Header
// Auth: agentPwd,
Ts: fmt.Sprintf("%d", time.Now().Add(100*time.Second).Unix()),
Uri: uri,
Keys: aesKeys,
}
mAuth, err := json.Marshal(header)
if err != nil {
return nil
}
encrypt, err := RSAEncrypt(mAuth, pk)
if err != nil {
return nil
}
headers["X-OCS-Header"] = encrypt
// X-OCS-Agent-Header
// headers["X-OCS-Agent-Header"] = encrypt
return headers
}
func RSAEncrypt(raw []byte, pk string) (string, error) {
pkix, err := base64.StdEncoding.DecodeString(pk)
if err != nil {
return "", err
}
pub, err := x509.ParsePKCS1PublicKey(pkix)
if err != nil {
return "", err
}
if len(raw) == 0 {
b, err := rsa.EncryptPKCS1v15(rand.Reader, pub, raw)
return base64.StdEncoding.EncodeToString(b), err
}
// 段階的な暗号化
blockSize := 512/8 - 11
numBlocks := (len(raw) + blockSize - 1) / blockSize
ciphertext := make([]byte, 0)
for i := 0; i < numBlocks; i++ {
start := i * blockSize
end := (i + 1) * blockSize
if end > len(raw) {
end = len(raw)
}
encrypted, err := rsa.EncryptPKCS1v15(rand.Reader, pub, raw[start:end])
if err != nil {
return "", err
}
ciphertext = append(ciphertext, encrypted...)
}
return base64.StdEncoding.EncodeToString(ciphertext), err
}
func EncryptBodyWithAes(body interface{}) (encryptedBody interface{}, key []byte, iv []byte, err error) {
if body == nil {
return
}
mBody, err := json.Marshal(body)
if err != nil {
return
}
key = make([]byte, 16)
iv = make([]byte, 16) // block_sizeと同じ、16バイト
_, err = rand.Read(key)
if err != nil {
return
}
_, err = rand.Read(iv)
if err != nil {
return
}
encryptedBody, err = AESEncrypt(mBody, key, iv)
return
}
func AESEncrypt(raw []byte, key []byte, iv []byte) (string, error) {
// AES暗号化エンジンの作成
block, err := aes.NewCipher(key)
if err != nil {
return "", err
}
raw = pkcs5Padding(raw, block.BlockSize())
// AESのCBCモードの暗号化エンジンの作成
mode := cipher.NewCBCEncrypter(block, iv)
// データの暗号化
ciphertext := make([]byte, len(raw))
mode.CryptBlocks(ciphertext, raw)
return base64.StdEncoding.EncodeToString(ciphertext), err
}
func pkcs5Padding(ciphertext []byte, blockSize int) []byte {
padding := blockSize - len(ciphertext)%blockSize
padtext := bytes.Repeat([]byte{byte(padding)}, padding)
return append(ciphertext, padtext...)
}
Javaサンプルコード
以下は、Javaを使用してリクエストを送信するコードのサンプルです。コード内で定義されているagent、pwd、uri、body(意味はコードのコメントを参照)を変更するだけで済みます。変更後にコンパイルして実行すると、対応するHTTPリクエストヘッダーとHTTPリクエストボディが得られます。
package com.oceanbase;
import javax.crypto.Cipher;
import javax.crypto.KeyGenerator;
import javax.crypto.SecretKey;
import javax.crypto.spec.IvParameterSpec;
import java.io.BufferedReader;
import java.io.InputStreamReader;
import java.io.Serializable;
import java.math.BigInteger;
import java.net.HttpURLConnection;
import java.nio.charset.StandardCharsets;
import java.security.*;
import java.security.spec.RSAPublicKeySpec;
import java.util.ArrayList;
import java.util.Base64;
import java.util.List;
import java.net.URL;
import org.bouncycastle.asn1.ASN1InputStream;
import org.bouncycastle.asn1.ASN1Integer;
import org.bouncycastle.asn1.ASN1Sequence;
import com.alibaba.fastjson2.JSON;
import com.alibaba.fastjson2.JSONObject;
import com.google.gson.Gson;
public class Main {
/* ここでパスワード、uri、ターゲットagentを変更します */
private static final String pwd = "******"; //OceanBaseクラスタsysテナントのrootパスワード
// X-OCS-Agent-Header
// private static final String agentPwd = "******"; // obshellノードのパスワード
private static final String uri = "/api/v1/obcluster/config"; // リクエストのURI
private static final String agent = "10.10.10.1:2886"; // リクエストノードのIPアドレスとポート
private static final String RSA_ALGORITHM = "RSA";
private static final Integer RSA_KEY_SIZE = 512;
private static final String RSA_TRANSFORMATION = "RSA/ECB/PKCS1Padding";
private static final String AES_ALGORITHM = "AES";
private static final Integer AES_KEY_SIZE = 128;
private static final String AES_TRANSFORMATION = "AES/CBC/PKCS5Padding";
public static class HTTPHeader implements Serializable {
private static final long serialVersionUID = 1L;
public String auth;
public String ts;
public String uri;
private String keys;
public HTTPHeader(String pwd, int timestamp, String uri, byte[] keys) {
this.auth = pwd;
// X-OCS-Agent-Header
// this.auth = agentPwd;
this.ts = String.valueOf(timestamp);
this.uri = uri;
this.keys = Base64.getEncoder().encodeToString(keys);
}
}
public static String serializeStruct(HTTPHeader header) {
Gson gson = new Gson();
// 構造をJSONに変換し、keysフィールドをBase64エンコード
return gson.toJson(header);
}
public static String encryptRSAWithSegment(Cipher cipher, byte[] data) throws Exception {
int inputLen = data.length;
int maxSize = RSA_KEY_SIZE / 8 - 11; // データ長から暗号化パディングを差し引く
int offSet = 0;
byte[] cache;
int i = 0;
List<byte[]> encryptedSegments = new ArrayList<>();
// データをセグメントごとに暗号化
while (inputLen - offSet > 0) {
if (inputLen - offSet > maxSize) {
cache = cipher.doFinal(data, offSet, maxSize);
} else {
cache = cipher.doFinal(data, offSet, inputLen - offSet);
}
encryptedSegments.add(cache);
i++;
offSet = i * maxSize;
}
// 暗号化されたセグメントを統合
int totalLen = encryptedSegments.stream().mapToInt(segment -> segment.length).sum();
byte[] encryptedData = new byte[totalLen];
int currentIndex = 0;
for (byte[] segment : encryptedSegments) {
System.arraycopy(segment, 0, encryptedData, currentIndex, segment.length);
currentIndex += segment.length;
}
return Base64.getEncoder().encodeToString(encryptedData);
}
private static String getPublicKey(String urlStr) throws Exception {
URL url = new URL(urlStr);
HttpURLConnection conn = (HttpURLConnection) url.openConnection();
conn.setRequestMethod("GET");
int responseCode = conn.getResponseCode();
if (responseCode == HttpURLConnection.HTTP_OK) {
try (BufferedReader in = new BufferedReader(new InputStreamReader(conn.getInputStream))) {
String inputLine;
StringBuilder response = new StringBuilder();
while ((inputLine = in.readLine()) != null) {
response.append(inputLine);
}
System.out.println("Response: " + response.toString());
JSONObject jsonResponse = JSON.parseObject(response.toString());
System.out.println("Public Key: " + jsonResponse.getJSONObject("data").getString("public_key"));
return jsonResponse.getJSONObject("data").getString("public_key");
} catch (Exception e){
System.out.println("Failed to read response: " + e.getMessage());
throw new Exception("Failed to read response", e);
}
} else {
throw new Exception("HTTP request failed with code " + responseCode);
}
}
private static PublicKey convertPKCS1ToPublicKey(String pkcs1PublicKeyStr) throws Exception {
pkcs1PublicKeyStr = pkcs1PublicKeyStr.replaceAll("\\n", "")
.replace("-----BEGIN RSA PUBLIC KEY-----", "")
.replace("-----END RSA PUBLIC KEY-----", "");
byte[] pkcs1PublicKey = Base64.getDecoder().decode(pkcs1PublicKeyStr);
ASN1InputStream asn1InputStream = new ASN1InputStream(pkcs1PublicKey);
ASN1Sequence sequence = (ASN1Sequence) asn1InputStream.readObject();
BigInteger modulus = ((ASN1Integer) sequence.getObjectAt(0)).getValue();
BigInteger publicExponent = ((ASN1Integer) sequence.getObjectAt(1)).getValue();
asn1InputStream.close();
RSAPublicKeySpec publicKeySpec = new RSAPublicKeySpec(modulus, publicExponent);
KeyFactory keyFactory = KeyFactory.getInstance("RSA");
return keyFactory.generatePublic(publicKeySpec);
}
public static void main(String[] args) throws Exception {
// 公開鍵を取得
String publicKeyStr = getPublicKey("http://" +agent+ "/api/v1/secret");
PublicKey publicKey = convertPKCS1ToPublicKey(publicKeyStr);
// RSAとAESキーを初期化
KeyPairGenerator keyGen = KeyPairGenerator.getInstance(RSA_ALGORITHM);
keyGen.initialize(RSA_KEY_SIZE); // RSA 2048ビットキー
KeyGenerator aesKeyGen = KeyGenerator.getInstance(AES_ALGORITHM);
aesKeyGen.init(AES_KEY_SIZE); // AES 128ビットキー
SecretKey aesKey = aesKeyGen.generateKey();
byte[] aesKeyBytes = aesKey.getEncoded();
byte[] iv = new byte[16]; // イニシャライズベクトルを16バイトに設定
SecureRandom random = new SecureRandom();
random.nextBytes(iv);
// RSAによるデータのセグメント暗号化
Cipher rsaCipher = Cipher.getInstance(RSA_TRANSFORMATION);
rsaCipher.init(Cipher.ENCRYPT_MODE, publicKey);
// Headerに格納するデータ(URI、タイムスタンプ、AESキー、IV)の準備
// データをBase64形式でエンコードし、文字列に連結する
long timestamp = System.currentTimeMillis() / 1000 + 100000;
HTTPHeader header = new HTTPHeader(pwd, (int) timestamp, uri, concatenate(aesKeyBytes, iv));
System.out.println("Header: " + serializeStruct(header));
String encryptedHeader = encryptRSAWithSegment(rsaCipher, serializeStruct(header).getBytes(StandardCharsets.UTF_8));
// AESによるHTTPデータボディの暗号化
Cipher aesCipher = Cipher.getInstance(AES_TRANSFORMATION);
aesCipher.init(Cipher.ENCRYPT_MODE, aesKey, new IvParameterSpec(iv));
/* ここでRequest Bodyを構築します */
JSONObject databody = new JSONObject();
data.body.put("clusterId", 1);
data.body.put("clusterName", "21421");
data.body.put("rootPwd", "2145325");
System.out.println(data.body.toString());
byte[] encryptedBody = aesCipher.doFinal(data.body.toString().getBytes(StandardCharsets.UTF_8));
String encryptedBodyBase64 = Base64.getEncoder().encodeToString(encryptedBody);
// 結果を出力
System.out.println("Encrypted Header (Base64, RSA-Encrypted): " + encryptedHeader);
System.out.println("Encrypted Body (Base64, AES-Encrypted): " + encryptedBodyBase64);
}
private static byte[] concatenate(byte[]... arrays) {
int totalLength = 0;
for (byte[] array : arrays) {
totalLength += array.length;
}
byte[] result = new byte[totalLength];
int currentIndex = 0;
for (byte[] array : arrays) {
System.arraycopy(array, 0, result, currentIndex, array.length);
currentIndex += array.length;
}
return result;
}
}