概要
IoTや組み込みデバイスとの通信において、サーバー側でデバイスから送信されるデータを盗聴・改ざんから守るための実践的なセキュリティ実装手法を紹介します。TLSによる安全な通信、HMAC-SHA256によるデータ整合性検証、RSAによるデジタル署名、およびリプレイ攻撃対策を統合した企業向けソリューションを提供します。
技術スタックとアーキテクチャ
- フレームワーク: Spring Boot 3.2+
- プロトコル: HTTPS(必須)、MQTT(オプション)
- 暗号化: TLS 1.3(転送時)、HMAC-SHA256(データ署名)、RSA(非対称署名)
- ストレージ: MySQL(デバイス情報・キー管理)、Redis(タイムスタンプ・ノンスのキャッシュ)
環境設定
HTTPS証明書の設定
# 秘密鍵生成
openssl genrsa -out server.key 2048
# 証明書署名要求生成
openssl req -new -key server.key -out server.csr
# 自己署名証明書生成(365日有効)
openssl x509 -req -days 365 -in server.csr -signkey server.key -out server.crt
# PKCS#12形式に変換(Java互換)
openssl pkcs12 -export -in server.crt -inkey server.key -out server.p12 -name "iot-server"
application.yml 設定例
server:
port: 8443
ssl:
enabled: true
key-store: classpath:server.p12
key-store-password: secure_password
key-store-type: PKCS12
protocol: TLSv1.3
ciphers: TLS_AES_256_GCM_SHA384,TLS_CHACHA20_POLY1305_SHA256
device:
security:
rsa-private-key: classpath:private.pem
rsa-public-key: classpath:public.pem
spring:
data:
redis:
host: localhost
port: 6379
datasource:
url: jdbc:mysql://localhost:3306/iot_sec?useSSL=true&serverTimezone=UTC
username: dbuser
password: dbpass
データベース設計
デバイス情報テーブル (devices)
CREATE TABLE devices (
id BIGINT AUTO_INCREMENT PRIMARY KEY,
device_uid VARCHAR(64) NOT NULL UNIQUE,
secret_key VARCHAR(128) NOT NULL,
public_key TEXT,
is_active BOOLEAN DEFAULT TRUE,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP
);
通信ログテーブル (comm_logs)
CREATE TABLE comm_logs (
id BIGINT AUTO_INCREMENT PRIMARY KEY,
device_uid VARCHAR(64) NOT NULL,
direction TINYINT NOT NULL, -- 1: デバイス→サーバー, 2: サーバー→デバイス
payload TEXT,
signature VARCHAR(256),
verified BOOLEAN,
error_reason VARCHAR(256),
logged_at DATETIME DEFAULT CURRENT_TIMESTAMP,
INDEX idx_device (device_uid),
INDEX idx_time (logged_at)
);
セキュリティユーティリティクラス
SecurityHelper.java
@Component
public class SecurityHelper {
private Sign serverSigner;
private Sign serverVerifier;
@PostConstruct
public void init() throws Exception {
String privKey = Files.readString(Path.of("classpath:private.pem".replace("classpath:", "")));
String pubKey = Files.readString(Path.of("classpath:public.pem".replace("classpath:", "")));
serverSigner = new Sign(SignAlgorithm.SHA256withRSA, privKey, null);
serverVerifier = new Sign(SignAlgorithm.SHA256withRSA, null, pubKey);
}
public String computeHmac(String message, String key) {
HMac hmac = new HMac(HmacAlgorithm.HmacSHA256, key.getBytes(StandardCharsets.UTF_8));
return hmac.digestHex(message);
}
public boolean validateHmac(String message, String key, String expected) {
return computeHmac(message, key).equalsIgnoreCase(expected);
}
public String signWithRsa(String data) {
return Base64.encode(serverSigner.sign(data.getBytes(StandardCharsets.UTF_8)));
}
public boolean verifyRsaSignature(String data, String sig, String publicKey) {
Sign verifier = (publicKey != null)
? new Sign(SignAlgorithm.SHA256withRSA, null, publicKey)
: serverVerifier;
return verifier.verify(data.getBytes(StandardCharsets.UTF_8), Base64.decode(sig));
}
}
ReplayProtector.java
@Component
public class ReplayProtector {
private static final long WINDOW_MS = 300_000; // 5分
private final StringRedisTemplate redis;
public ReplayProtector(StringRedisTemplate redis) {
this.redis = redis;
}
public boolean isFresh(String deviceId, long timestamp, String nonce) {
if (Math.abs(System.currentTimeMillis() - timestamp) > WINDOW_MS) {
return false;
}
String key = "replay:" + deviceId + ":" + timestamp + ":" + nonce;
return Boolean.TRUE.equals(redis.opsForValue().setIfAbsent(key, "1", Duration.ofMillis(WINDOW_MS)));
}
}
APIエンドポイント実装
DeviceReportDto.java
@Data
public class DeviceReportDto {
private String deviceUid;
private String payload;
private String hmac;
private Long timestamp;
private String nonce;
private String rsaSig;
}
DeviceService.java
@Service
@Transactional
public class DeviceService {
private final DeviceRepository deviceRepo;
private final LogRepository logRepo;
private final SecurityHelper crypto;
private final ReplayProtector replayGuard;
public String processReport(DeviceReportDto req) {
Device dev = deviceRepo.findByUidAndActive(req.getDeviceUid(), true);
if (dev == null) {
logRepo.save(new CommLog(req.getDeviceUid(), 1, req.getPayload(), req.getHmac(), false, "Invalid device"));
return "Unauthorized";
}
if (!replayGuard.isFresh(req.getDeviceUid(), req.getTimestamp(), req.getNonce())) {
logRepo.save(new CommLog(req.getDeviceUid(), 1, req.getPayload(), req.getHmac(), false, "Replay detected"));
return "Stale request";
}
String toSign = req.getDeviceUid() + req.getPayload() + req.getTimestamp() + req.getNonce();
boolean hmacOk = crypto.validateHmac(toSign, dev.getSecretKey(), req.getHmac());
boolean rsaOk = req.getRsaSig() == null ||
crypto.verifyRsaSignature(toSign, req.getRsaSig(), dev.getPublicKey());
if (!hmacOk || !rsaOk) {
logRepo.save(new CommLog(req.getDeviceUid(), 1, req.getPayload(), req.getHmac(), false, "Signature mismatch"));
return "Verification failed";
}
// 実際のビジネスロジック(例:センサーデータ保存)
logRepo.save(new CommLog(req.getDeviceUid(), 1, req.getPayload(), req.getHmac(), true, "OK"));
return "Accepted";
}
public String issueCommand(String deviceUid, String command, long sequence) {
Device dev = deviceRepo.findByUidAndActive(deviceUid, true);
if (dev == null) return "Device not found";
String cmdData = deviceUid + command + sequence;
String hmac = crypto.computeHmac(cmdData, dev.getSecretKey());
String rsaSig = crypto.signWithRsa(cmdData);
logRepo.save(new CommLog(deviceUid, 2, command, hmac, true, "Issued"));
return "{\"cmd\":" + command + ",\"seq\":" + sequence + ",\"sig\":\"" + hmac + "\",\"rsaSig\":\"" + rsaSig + "\"}";
}
}
MQTT統合(オプション)
MQTTを使用する場合、spring-integration-mqtt を利用し、受信メッセージを同様の検証フローに通すことで、HTTPと同等のセキュリティを確保できます。トピック階層からデバイスIDを抽出し、ペイロードをJSONとして解析後、上記のprocessReportメソッドを呼び出します。
本番環境への展開ガイド
- キー管理: 秘密鍵やHMACキーはハードウェアセキュリティモジュール(HSM)またはクラウドKMSで管理
- 証明書: Let's Encryptなどの信頼されたCAから取得
- パフォーマンス: 高頻度通信ではHMACを優先し、RSAは低頻度指令に限定
- 監視: 署名失敗率や異常アクセスをリアルタイムで監視
- レート制限: API GatewayまたはSpring Cloud Gatewayでリクエスト制限を適用