목차
서론: 대규모 IoT 시대의 보안 과제
사물 인터넷(Internet of Things, IoT) 기술이 산업 현장에서부터 우리 가정에 이르기까지 깊숙이 스며들면서, 수십억 개의 디바이스가 네트워크에 연결되고 있습니다. 이러한 초연결 시대는 전례 없는 편리함과 효율성을 제공하지만, 동시에 거대한 보안 과제를 제기합니다. 수많은 디바이스 각각의 신원을 어떻게 확인하고, 이들이 주고받는 데이터의 무결성과 기밀성을 어떻게 보장할 수 있을까요? 특히, 공장에서 막 출고된 수백만 개의 디바이스를 안전하고 자동화된 방식으로 클라우드 플랫폼에 등록(프로비저닝)하는 것은 IoT 프로젝트의 성패를 가르는 중요한 문제입니다.
이러한 문제에 대한 가장 강력한 해결책 중 하나는 공개 키 기반 구조(PKI, Public Key Infrastructure)를 활용한 인증 방식입니다. 각 디바이스에 고유한 디지털 신원(X.509 인증서)을 부여하고, 이를 통해 클라우드 서비스와 안전하게 통신하도록 하는 것입니다. Amazon Web Services(AWS)는 AWS IoT Core 서비스를 통해 이러한 대규모 디바이스 프로비저닝 및 관리를 위한 포괄적인 솔루션을 제공합니다.
이 글에서는 AWS IoT 환경에서 디바이스를 안전하게 프로비저닝하는 핵심 과정, 즉 인증서 서명 요청(CSR, Certificate Signing Request)을 생성하는 방법에 대해 심도 있게 다룹니다. 특히, Java 및 C# 환경에서 가장 널리 사용되는 경량 암호화 라이브러리인 Bouncy Castle을 활용하여 이 과정을 어떻게 구현하는지 상세한 코드 예제와 함께 단계별로 분석할 것입니다. 단순히 코드를 나열하는 것을 넘어, 각 단계의 암호학적 의미와 AWS IoT 보안 모델과의 연관성을 깊이 있게 탐구하여, 개발자들이 더욱 안전하고 확장 가능한 IoT 솔루션을 구축하는 데 필요한 지식을 제공하고자 합니다.
1. IoT 디바이스 프로비저닝의 핵심과 AWS IoT의 접근 방식
1.1. 프로비저닝이란 무엇이며 왜 중요한가?
IoT에서 '프로비저닝(Provisioning)'은 디바이스가 클라우드 서비스(예: AWS IoT Core)와 통신할 수 있도록 초기 설정을 구성하고, 보안 자격 증명을 설치하여 신뢰 관계를 수립하는 전체 과정을 의미합니다. 이는 단순히 디바이스를 네트워크에 연결하는 것을 넘어, 디바이스에 고유하고 검증 가능한 '신원'을 부여하는 과정입니다. 안전한 프로비저닝은 IoT 시스템 전체 보안의 출발점입니다.
프로비저닝이 부실할 경우 다음과 같은 심각한 보안 위협에 노출될 수 있습니다.
- 디바이스 스푸핑(Device Spoofing): 악의적인 공격자가 합법적인 디바이스인 것처럼 위장하여 시스템에 침투하고, 허위 데이터를 전송하거나 민감한 정보를 탈취할 수 있습니다.
- 중간자 공격(Man-in-the-Middle Attack): 디바이스와 클라우드 간의 통신이 암호화되지 않거나 취약한 인증을 사용할 경우, 공격자가 통신 내용을 가로채거나 조작할 수 있습니다.
- 대규모 봇넷 형성: 보안에 취약한 수많은 IoT 디바이스가 악성코드에 감염되어 분산 서비스 거부(DDoS) 공격과 같은 대규모 사이버 공격의 좀비로 악용될 수 있습니다. (예: 미라이 봇넷)
따라서, 각 디바이스가 제조 단계부터 고유한 비밀(개인 키)을 가지며, 이 비밀을 기반으로 검증 가능한 자격 증명(인증서)을 발급받아 클라우드에 자신을 증명하는 강력한 프로비저닝 절차가 필수적입니다.
1.2. AWS IoT의 다양한 프로비저닝 전략
AWS IoT는 다양한 규모와 요구사항에 맞춰 여러 가지 프로비저닝 방법을 제공합니다. 각 방법은 보안 수준, 자동화 정도, 운영 복잡성 측면에서 장단점을 가집니다.
- 1. 개별 등록 (Manual Provisioning)
- 가장 기본적인 방법으로, AWS 관리 콘솔이나 CLI를 통해 각 디바이스의 인증서와 정책을 수동으로 생성하고 등록합니다. 소수의 디바이스를 테스트하거나 개발할 때 유용하지만, 대규모 배포에는 적합하지 않습니다.
- 2. 적시 프로비저닝 (Just-in-Time Provisioning, JITP)
- 디바이스가 처음으로 AWS IoT Core에 연결을 시도할 때 자동으로 프로비저닝을 완료하는 방식입니다. 이를 위해 제조 단계에서 디바이스에 신뢰할 수 있는 인증 기관(CA)에서 서명한 '디바이스 인증서'를 미리 설치해야 합니다. AWS IoT는 이 CA를 사전에 등록해두고, 해당 CA로부터 서명된 인증서를 가진 디바이스의 첫 연결 시 자동으로 사물(Thing)을 생성하고 정책을 연결합니다. 대규모 생산 라인에 통합하기 용이합니다.
- 3. 플릿 프로비저닝 (Fleet Provisioning)
- 디바이스가 범용으로 사용 가능한 '부트스트랩 인증서'를 사용하여 AWS IoT에 처음 연결한 뒤, 프로비저닝 템플릿에 따라 고유한 장기 인증서와 자격 증명을 발급받는 방식입니다. JITP보다 유연성이 높으며, 디바이스별로 다른 속성이나 정책을 동적으로 적용할 수 있습니다. 디바이스에 대한 정보가 제조 시점에 확정되지 않은 경우에 특히 유용합니다.
이 글에서 다루는 CSR 생성은 위 방법들, 특히 '플릿 프로비저닝'이나 커스텀 프로비저닝 워크플로우에서 핵심적인 역할을 합니다. 디바이스는 자신의 개인 키를 외부에 노출하지 않은 상태에서, 자신의 신원 정보와 공개 키를 담은 CSR을 생성하여 AWS IoT에 제출하고, AWS IoT는 이를 검증한 후 정식 디바이스 인증서를 발급해주는 흐름입니다.
2. 신뢰의 기반: 공개 키 기반 구조(PKI)와 X.509 인증서
AWS IoT의 보안 모델을 이해하려면 그 근간을 이루는 PKI(공개 키 기반 구조)에 대한 이해가 선행되어야 합니다. PKI는 디지털 인증서를 사용하여 네트워크상의 사용자, 디바이스, 서비스의 신원을 확인하고 데이터 암호화를 가능하게 하는 기술, 정책, 절차의 집합입니다.
2.1. 비대칭 암호화: 디지털 신원의 초석
PKI의 핵심은 비대칭 암호화(또는 공개 키 암호화)입니다. 이 방식에서는 수학적으로 연결된 한 쌍의 키, 즉 개인 키(Private Key)와 공개 키(Public Key)를 사용합니다.
- 개인 키: 이름 그대로 소유자만이 비밀리에 보관해야 하는 키입니다. 데이터를 암호화하거나 디지털 서명을 생성하는 데 사용됩니다.
- 공개 키: 누구에게나 공개될 수 있는 키입니다. 공개 키로는 개인 키로 암호화된 데이터를 복호화하거나, 디지털 서명의 유효성을 검증할 수 있습니다.
이러한 속성을 이용해 IoT 디바이스는 다음과 같은 보안 기능을 구현합니다.
- 인증(Authentication): 디바이스는 자신의 개인 키로 특정 데이터에 서명하고, AWS IoT는 해당 디바이스의 공개 키로 이 서명을 검증합니다. 서명이 유효하다면, AWS IoT는 메시지를 보낸 주체가 해당 개인 키의 소유자, 즉 합법적인 디바이스임을 신뢰할 수 있습니다. 이를 '개인 키 소유 증명(Proof-of-Possession)'이라고 합니다.
- 기밀성(Confidentiality): AWS IoT가 디바이스의 공개 키로 메시지를 암호화하면, 오직 해당 디바이스의 개인 키를 가진 장치만이 그 메시지를 복호화할 수 있습니다.
2.2. 인증서 서명 요청(CSR)의 역할과 구조
공개 키만으로는 그 키가 정말로 특정 디바이스의 것인지 신뢰할 수 없습니다. 이 신뢰 문제를 해결하기 위해 '디지털 인증서'가 사용됩니다. X.509 인증서는 신뢰할 수 있는 제3자, 즉 인증 기관(CA, Certificate Authority)이 특정 공개 키가 특정 주체(Subject)의 소유임을 보증하는 디지털 문서입니다.
이때 디바이스가 CA(이 경우 AWS IoT)에게 "내 정보는 이것이고, 내 공개 키는 이것이니, 이 정보가 유효함을 보증하는 인증서를 발급해주세요"라고 공식적으로 요청하는 문서가 바로 인증서 서명 요청(CSR)입니다. CSR은 PKCS#10 표준에 따라 정의되며, 주로 다음 세 가지 정보로 구성됩니다.
- 주체 식별 정보 (Subject Distinguished Name): 인증서를 발급받을 대상의 정보입니다. 일반적으로 다음과 같은 필드를 포함합니다.
CN (Common Name)
: 디바이스의 고유 이름 (예: `my-iot-device-12345`). AWS IoT에서는 종종 이 값을 사물 이름(Thing Name)으로 사용합니다.O (Organization)
: 소유 조직의 이름 (예: `MyCompany`)OU (Organizational Unit)
: 조직 내 부서 이름 (예: `Manufacturing`)L (Locality)
: 도시 이름 (예: `Seoul`)C (Country)
: 국가 코드 (예: `KR`)
- 공개 키 (Public Key): 디바이스가 생성한 키 쌍 중 공개 키 부분입니다. 이 공개 키가 발급될 인증서에 포함됩니다.
- 디지털 서명 (Digital Signature): 디바이스가 자신의 개인 키로 위 1번과 2번 정보를 서명한 값입니다. 이 서명은 CA가 CSR의 유효성을 검증하는 데 사용됩니다. CA는 CSR에 포함된 공개 키를 사용하여 서명을 검증함으로써, 이 요청이 해당 개인 키를 실제로 소유한 주체로부터 온 것임을 확인할 수 있습니다. 이는 개인 키가 외부에 노출되지 않으면서도 소유권을 증명하는 매우 중요한 과정입니다.
결론적으로, CSR 생성은 디바이스가 자신의 신원을 안전하게 클라우드에 증명하기 위한 첫걸음이며, 이 과정의 핵심은 '개인 키는 디바이스 내부에 안전하게 유지하면서, 공개 키와 신원 정보만을 외부에 전달하는 것'입니다.
3. Bouncy Castle: 강력하고 유연한 암호화 라이브러리
Java 환경에서 암호화 기능을 구현할 때 가장 먼저 떠오르는 것은 Java Cryptography Architecture (JCA)와 Java Cryptography Extension (JCE)입니다. 이는 JDK에 내장된 표준 API입니다. 하지만 더 넓은 범위의 알고리즘, 더 높은 유연성, 또는 특정 플랫폼에서의 호환성이 필요할 때, Bouncy Castle은 최고의 선택지 중 하나입니다.
3.1. 왜 표준 JCA/JCE 대신 Bouncy Castle을 선택하는가?
Bouncy Castle은 호주의 자선 단체에서 개발 및 유지 관리하는 오픈 소스 암호화 라이브러리로, Java와 C# 버전을 제공합니다. Bouncy Castle이 널리 사용되는 이유는 다음과 같습니다.
- 광범위한 알고리즘 지원: Bouncy Castle은 표준 JCE에서 지원하지 않는 최신 암호화 알고리즘, 해시 함수, 타원 곡선(Elliptic Curves) 등을 포함한 방대한 알고리즘을 지원합니다. 이는 최신 보안 요구사항을 충족해야 하는 IoT 환경에서 큰 장점입니다.
- 경량성 및 플랫폼 독립성: Bouncy Castle은 상대적으로 가볍고 의존성이 적어 리소스가 제한적인 임베디드 시스템이나 안드로이드 환경에서도 효과적으로 사용할 수 있습니다.
- JCA/JCE 프로바이더 모델: Bouncy Castle은 JCA/JCE의 표준 '프로바이더(Provider)' 아키텍처를 따릅니다. 이는 코드에서 `Security.addProvider(new BouncyCastleProvider())` 한 줄만 추가하면, 기존 JCA/JCE API(`KeyPairGenerator`, `Cipher` 등)를 그대로 사용하면서 Bouncy Castle이 제공하는 구현체를 활용할 수 있게 해줍니다. 이는 코드의 이식성과 유연성을 높여줍니다.
- PKIX/CMS/OpenPGP 등 고급 API 제공: 단순 암호화/복호화를 넘어 X.509 인증서 처리, CSR 생성(PKCS#10), 암호화 메시지 구문(CMS) 등 PKI 관련 작업을 위한 고수준의 편리한 API를 제공합니다. 이 글에서 사용할 `JcaPKCS10CertificationRequestBuilder`가 대표적인 예입니다.
- FIPS 인증 버전: 미국 연방 정보 처리 표준(FIPS) 140-2 인증을 받은 버전을 별도로 제공하여, 높은 수준의 보안 규정을 준수해야 하는 프로젝트에도 사용할 수 있습니다.
3.2. 프로젝트에 Bouncy Castle 통합하기
Bouncy Castle을 Java 프로젝트에서 사용하려면 관련 라이브러리를 의존성에 추가해야 합니다. 일반적으로 두 가지 주요 아티팩트가 필요합니다.
bcprov-jdk**
: 기본적인 암호화 알고리즘과 JCA/JCE 프로바이더 구현을 포함합니다. (예:bcprov-jdk18on
for Java 1.8+)bcpkix-jdk**
: X.509 인증서, CSR 등 PKIX(공개 키 인프라) 관련 고수준 API를 포함합니다. CSR 생성을 위해서는 이 라이브러리가 반드시 필요합니다.
Maven (pom.xml)
Maven 프로젝트의 경우, pom.xml
파일의 <dependencies>
섹션에 다음을 추가합니다.
<dependencies>
<!-- Bouncy Castle Provider -->
<dependency>
<groupId>org.bouncycastle</groupId>
<artifactId>bcprov-jdk18on</artifactId>
<version>1.70</version> <!-- 최신 버전 확인 권장 -->
</dependency>
<!-- Bouncy Castle PKIX, CMS, EAC, TSP, PKCS, OCSP, CMP, and CRMF APIs -->
<dependency>
<groupId>org.bouncycastle</groupId>
<artifactId>bcpkix-jdk18on</artifactId>
<version>1.70</version> <!-- 최신 버전 확인 권장 -->
</dependency>
</dependencies>
Gradle (build.gradle)
Gradle 프로젝트의 경우, build.gradle
파일의 dependencies
블록에 다음을 추가합니다.
dependencies {
// Bouncy Castle Provider
implementation 'org.bouncycastle:bcprov-jdk18on:1.70' // 최신 버전 확인 권장
// Bouncy Castle PKIX, CMS, etc.
implementation 'org.bouncycastle:bcpkix-jdk18on:1.70' // 최신 버전 확인 권장
}
의존성 추가가 완료되면, 이제 Bouncy Castle이 제공하는 강력한 기능들을 활용하여 CSR을 생성할 준비가 된 것입니다.
4. Bouncy Castle을 활용한 CSR 생성 실전 가이드
이제 이론을 바탕으로 실제 Java 코드를 통해 Bouncy Castle을 사용하여 CSR을 생성하는 전 과정을 단계별로 살펴보겠습니다. 각 단계는 독립적인 코드 조각이 아닌, 하나의 완성된 로직으로 이어집니다.
4.1. 1단계: Bouncy Castle 보안 프로바이더 등록
가장 먼저 해야 할 일은 JVM에 Bouncy Castle을 보안 프로바이더로 등록하는 것입니다. 이 과정을 통해 JCA/JCE 프레임워크가 "BC"라는 이름의 프로바이더가 제공하는 암호화 알고리즘 구현을 찾고 사용할 수 있게 됩니다. 애플리케이션 시작 시 한 번만 실행하면 됩니다.
import org.bouncycastle.jce.provider.BouncyCastleProvider;
import java.security.Security;
public class BouncyCastleSetup {
public static void setup() {
// 기존에 등록된 프로바이더가 있다면 제거 (중복 등록 방지)
Security.removeProvider(BouncyCastleProvider.PROVIDER_NAME);
// Bouncy Castle 프로바이더를 보안 프로바이더 목록에 추가
Security.addProvider(new BouncyCastleProvider());
System.out.println("Bouncy Castle provider added successfully.");
}
}
이 코드를 메인 로직 실행 전에 호출하여 Bouncy Castle을 활성화합니다.
4.2. 2단계: RSA 및 ECDSA 키 쌍 생성
CSR의 핵심 요소인 공개 키와 서명에 필요한 개인 키를 생성합니다. AWS IoT는 RSA와 ECDSA(Elliptic Curve Digital Signature Algorithm) 키를 모두 지원합니다. ECDSA는 더 짧은 키 길이로도 RSA와 동등한 수준의 보안을 제공하여, 리소스가 제한적인 IoT 디바이스에 더 적합할 수 있습니다.
RSA 키 쌍 생성 (2048 비트)
import java.security.*;
public KeyPair generateRsaKeyPair() throws NoSuchProviderException, NoSuchAlgorithmException {
KeyPairGenerator keyPairGenerator = KeyPairGenerator.getInstance("RSA", "BC");
// RSA 키 크기를 2048비트로 초기화합니다. AWS IoT에서 권장하는 최소 길이입니다.
keyPairGenerator.initialize(2048, new SecureRandom());
KeyPair keyPair = keyPairGenerator.generateKeyPair();
System.out.println("RSA key pair generated.");
return keyPair;
}
ECDSA 키 쌍 생성 (NIST P-256 커브)
import java.security.*;
import java.security.spec.ECGenParameterSpec;
public KeyPair generateEcdsaKeyPair() throws NoSuchProviderException, NoSuchAlgorithmException, InvalidAlgorithmParameterException {
KeyPairGenerator keyPairGenerator = KeyPairGenerator.getInstance("ECDSA", "BC");
// prime256v1 (NIST P-256) 커브를 사용하도록 초기화합니다. AWS IoT에서 널리 지원됩니다.
keyPairGenerator.initialize(new ECGenParameterSpec("prime256v1"), new SecureRandom());
KeyPair keyPair = keyPairGenerator.generateKeyPair();
System.out.println("ECDSA key pair generated.");
return keyPair;
}
getInstance
메소드의 두 번째 인자로 "BC"를 명시하여 Bouncy Castle의 구현체를 사용하도록 강제합니다. 이렇게 생성된 KeyPair
객체는 keyPair.getPublic()
과 keyPair.getPrivate()
메소드를 통해 각각 공개 키와 개인 키를 포함합니다.
4.3. 3단계: X.500 주체 이름(Subject Name) 구성
CSR에 포함될 디바이스의 신원 정보를 정의합니다. 문자열을 직접 조합하는 것보다 X500NameBuilder
를 사용하면 표준을 준수하며 실수를 줄일 수 있습니다.
import org.bouncycastle.asn1.x500.X500Name;
import org.bouncycastle.asn1.x500.X500NameBuilder;
import org.bouncycastle.asn1.x500.style.BCStyle;
public X500Name createSubject(String commonName, String organization, String country) {
X500NameBuilder nameBuilder = new X500NameBuilder(BCStyle.INSTANCE);
// Common Name: AWS IoT Thing Name으로 사용될 수 있는 디바이스의 고유 식별자
nameBuilder.addRDN(BCStyle.CN, commonName);
nameBuilder.addRDN(BCStyle.O, organization);
nameBuilder.addRDN(BCStyle.C, country);
// 필요에 따라 다른 필드(L, OU, ST 등) 추가 가능
X500Name subject = nameBuilder.build();
System.out.println("Subject X.500 Name created: " + subject.toString());
return subject;
}
4.4. 4단계: CSR 요청 빌드 및 개인 키 서명
이제 앞서 준비한 주체 이름, 공개 키, 그리고 개인 키를 사용하여 CSR을 생성합니다. 이 과정이 바로 '개인 키 소유 증명'을 수행하는 단계입니다.
import org.bouncycastle.operator.ContentSigner;
import org.bouncycastle.operator.jcajce.JcaContentSignerBuilder;
import org.bouncycastle.pkcs.PKCS10CertificationRequest;
import org.bouncycastle.pkcs.PKCS10CertificationRequestBuilder;
import org.bouncycastle.pkcs.jcajce.JcaPKCS10CertificationRequestBuilder;
public PKCS10CertificationRequest createCsr(KeyPair keyPair, X500Name subject) throws Exception {
PublicKey publicKey = keyPair.getPublic();
PrivateKey privateKey = keyPair.getPrivate();
// 1. CSR 빌더 초기화: 주체 이름과 공개 키를 전달
PKCS10CertificationRequestBuilder p10Builder = new JcaPKCS10CertificationRequestBuilder(subject, publicKey);
// 2. 서명자(Signer) 준비: 서명 알고리즘을 지정
// 키 타입에 맞는 서명 알고리즘을 선택해야 합니다.
String signatureAlgorithm;
if (privateKey.getAlgorithm().equals("RSA")) {
signatureAlgorithm = "SHA256withRSA";
} else if (privateKey.getAlgorithm().equals("ECDSA")) {
signatureAlgorithm = "SHA256withECDSA";
} else {
throw new IllegalArgumentException("Unsupported key type: " + privateKey.getAlgorithm());
}
JcaContentSignerBuilder csBuilder = new JcaContentSignerBuilder(signatureAlgorithm);
// 3. 서명자 빌드: 서명에 사용할 개인 키를 전달
ContentSigner signer = csBuilder.build(privateKey);
// 4. 최종 CSR 객체 생성: CSR 빌더에 서명자를 적용
PKCS10CertificationRequest csr = p10Builder.build(signer);
System.out.println("CSR object created and signed.");
return csr;
}
4.5. 5단계: 생성된 CSR을 PEM 형식으로 인코딩
PKCS10CertificationRequest
객체는 메모리상의 표현일 뿐입니다. 이를 AWS IoT에 업로드하거나 파일로 저장하려면, 일반적으로 사용되는 텍스트 기반의 PEM(Privacy-Enhanced Mail) 형식으로 변환해야 합니다. PEM 형식은 Base64로 인코딩된 데이터와 `-----BEGIN CERTIFICATE REQUEST-----`, `-----END CERTIFICATE REQUEST-----` 헤더/푸터로 구성됩니다.
import org.bouncycastle.util.io.pem.PemObject;
import org.bouncycastle.util.io.pem.PemWriter;
import java.io.StringWriter;
import java.io.IOException;
public String convertCsrToPem(PKCS10CertificationRequest csr) throws IOException {
StringWriter stringWriter = new StringWriter();
try (PemWriter pemWriter = new PemWriter(stringWriter)) {
PemObject pemObject = new PemObject("CERTIFICATE REQUEST", csr.getEncoded());
pemWriter.writeObject(pemObject);
}
String pemString = stringWriter.toString();
System.out.println("CSR converted to PEM format.");
return pemString;
}
이제 `pemString` 변수에 담긴 문자열을 파일에 저장하거나, AWS SDK를 통해 `CreateCertificateFromCsr` API 호출에 사용하면 됩니다.
4.6. 전체 실행 코드 예제
위의 모든 단계를 통합한 전체 실행 가능한 예제 코드입니다.
import org.bouncycastle.asn1.x500.X500Name;
import org.bouncycastle.asn1.x500.X500NameBuilder;
import org.bouncycastle.asn1.x500.style.BCStyle;
import org.bouncycastle.jce.provider.BouncyCastleProvider;
import org.bouncycastle.operator.ContentSigner;
import org.bouncycastle.operator.jcajce.JcaContentSignerBuilder;
import org.bouncycastle.pkcs.PKCS10CertificationRequest;
import org.bouncycastle.pkcs.PKCS10CertificationRequestBuilder;
import org.bouncycastle.pkcs.jcajce.JcaPKCS10CertificationRequestBuilder;
import org.bouncycastle.util.io.pem.PemObject;
import org.bouncycastle.util.io.pem.PemWriter;
import java.io.IOException;
import java.io.StringWriter;
import java.security.*;
public class CsrGenerator {
static {
// 애플리케이션 로드 시 Bouncy Castle 프로바이더 등록
Security.addProvider(new BouncyCastleProvider());
}
public static void main(String[] args) {
try {
// --- 디바이스 정보 정의 ---
String commonName = "iot-device-SN123456";
String organization = "My Awesome IoT Company";
String country = "KR";
// --- RSA 기반 CSR 생성 ---
System.out.println("--- Generating CSR with RSA Key ---");
KeyPair rsaKeyPair = generateKeyPair("RSA", 2048);
X500Name subject = createSubject(commonName, organization, country);
PKCS10CertificationRequest rsaCsr = createCsr(rsaKeyPair, subject);
String rsaCsrPem = convertToPem(rsaCsr);
System.out.println("\n[Generated RSA CSR in PEM Format]");
System.out.println(rsaCsrPem);
// --- ECDSA 기반 CSR 생성 ---
System.out.println("\n--- Generating CSR with ECDSA Key ---");
KeyPair ecdsaKeyPair = generateKeyPair("ECDSA", 256); // 256은 ECC 커브 비트 수
PKCS10CertificationRequest ecdsaCsr = createCsr(ecdsaKeyPair, subject);
String ecdsaCsrPem = convertToPem(ecdsaCsr);
System.out.println("\n[Generated ECDSA CSR in PEM Format]");
System.out.println(ecdsaCsrPem);
} catch (Exception e) {
e.printStackTrace();
}
}
public static KeyPair generateKeyPair(String algorithm, int keySizeOrCurveBits) throws Exception {
KeyPairGenerator keyGen = KeyPairGenerator.getInstance(algorithm, "BC");
if ("RSA".equalsIgnoreCase(algorithm)) {
keyGen.initialize(keySizeOrCurveBits, new SecureRandom());
} else if ("ECDSA".equalsIgnoreCase(algorithm)) {
// P-256 (prime256v1) is a commonly used curve
keyGen.initialize(new java.security.spec.ECGenParameterSpec("prime256v1"), new SecureRandom());
} else {
throw new IllegalArgumentException("Unsupported algorithm: " + algorithm);
}
System.out.println("Generating " + algorithm + " key pair...");
return keyGen.generateKeyPair();
}
public static X500Name createSubject(String commonName, String organization, String country) {
X500NameBuilder nameBuilder = new X500NameBuilder(BCStyle.INSTANCE);
nameBuilder.addRDN(BCStyle.CN, commonName);
nameBuilder.addRDN(BCStyle.O, organization);
nameBuilder.addRDN(BCStyle.C, country);
return nameBuilder.build();
}
public static PKCS10CertificationRequest createCsr(KeyPair keyPair, X500Name subject) throws Exception {
PublicKey publicKey = keyPair.getPublic();
PrivateKey privateKey = keyPair.getPrivate();
PKCS10CertificationRequestBuilder p10Builder = new JcaPKCS10CertificationRequestBuilder(subject, publicKey);
String signatureAlgorithm = "SHA256with" + privateKey.getAlgorithm();
JcaContentSignerBuilder csBuilder = new JcaContentSignerBuilder(signatureAlgorithm);
ContentSigner signer = csBuilder.build(privateKey);
System.out.println("Creating and signing CSR with algorithm: " + signatureAlgorithm);
return p10Builder.build(signer);
}
public static String convertToPem(PKCS10CertificationRequest csr) throws IOException {
StringWriter stringWriter = new StringWriter();
try (PemWriter pemWriter = new PemWriter(stringWriter)) {
pemWriter.writeObject(new PemObject("CERTIFICATE REQUEST", csr.getEncoded()));
}
return stringWriter.toString();
}
}
5. 보안 강화: 키 관리 및 프로비저닝 모범 사례
CSR을 성공적으로 생성하는 것은 기술적인 과정일 뿐, IoT 보안의 전체 그림을 완성하려면 이를 둘러싼 정책과 절차가 뒷받침되어야 합니다.
5.1. 개인 키의 안전한 저장과 관리
가장 중요한 원칙은 개인 키가 디바이스 외부로 절대 유출되어서는 안 된다는 것입니다. 디바이스의 개인 키는 그 디바이스의 신원 그 자체입니다. 개인 키가 탈취되면 공격자는 해당 디바이스를 완벽하게 위장할 수 있습니다.
- 하드웨어 보안 모듈 (HSM) / 보안 요소 (SE): 가장 이상적인 방법은 개인 키를 생성하고 저장, 사용하는 모든 과정을 변조 방지(Tamper-resistant) 하드웨어 칩 내부에서 수행하는 것입니다. TPM(Trusted Platform Module), HSM, SE 등이 이러한 역할을 합니다. 키가 칩 외부로 절대 나오지 않으므로 소프트웨어적인 공격으로부터 안전합니다.
- 안전한 파일 시스템 저장: 하드웨어 보안 모듈 사용이 어렵다면, 최소한 암호화된 파일 시스템에 키를 저장하고, 운영체제의 강력한 접근 제어(루트 권한만 접근 가능 등)를 통해 보호해야 합니다.
- 제조 단계에서의 키 주입: 대량 생산되는 디바이스의 경우, 보안이 통제된 제조 시설에서 각 디바이스의 하드웨어 보안 칩에 고유한 개인 키와 초기 자격 증명을 주입하는 것이 일반적입니다.
5.2. CSR 제출 이후의 과정과 권한 관리
디바이스에서 생성된 CSR은 프로비저닝 워크플로우를 통해 AWS IoT Core로 전달됩니다. 예를 들어, 플릿 프로비저닝에서는 디바이스가 부트스트랩 인증서를 사용하여 AWS IoT의 MQTT 토픽으로 CSR을 발행할 수 있습니다. 백엔드의 Lambda 함수가 이 CSR을 받아 `CreateCertificateFromCsr` API를 호출합니다.
API 호출이 성공하면 AWS IoT는 CSR을 검증하고 서명하여 새로운 디바이스 인증서를 발급합니다. 이 인증서는 다시 디바이스로 전달되어 안전하게 저장되어야 합니다.
이후의 보안은 IAM 정책과 유사한 AWS IoT 정책을 통해 관리됩니다. 인증서가 발급되었다고 해서 모든 권한이 생기는 것이 아닙니다. 관리자는 각 디바이스(또는 디바이스 그룹)가 수행할 수 있는 작업을 최소한으로 제한하는 '최소 권한의 원칙'에 따라 정책을 작성해야 합니다.
예를 들어, 특정 온도 센서는 `telemetry/temp-sensor-123` 토픽에만 메시지를 발행(publish)할 수 있고, 다른 토픽은 구독(subscribe)하거나 발행할 수 없도록 제한해야 합니다. 이러한 세분화된 권한 제어는 시스템 일부가 침해되더라도 피해를 최소화하는 중요한 방어선이 됩니다.
결론: 안전한 IoT 생태계 구축을 향하여
지금까지 AWS IoT 환경에서 디바이스의 신뢰할 수 있는 신원을 확립하는 핵심 과정인 CSR 생성에 대해 Bouncy Castle 라이브러리를 중심으로 깊이 있게 살펴보았습니다. 우리는 대규모 IoT 환경에서 프로비저닝이 왜 중요한지부터 시작하여, 그 기반이 되는 PKI 이론, 그리고 실제 코드를 통한 CSR 생성의 전 과정을 단계별로 분석했습니다.
Bouncy Castle은 표준 Java 암호화 API의 한계를 넘어, 개발자에게 광범위한 알고리즘과 유연한 고수준 API를 제공함으로써 복잡한 PKI 작업을 손쉽게 구현할 수 있도록 돕는 강력한 도구입니다. 이를 통해 우리는 개인 키를 디바이스 내부에 안전하게 유지하면서도, 클라우드가 요구하는 표준화된 방식으로 인증서를 요청하는 과정을 자동화할 수 있습니다.
그러나 기술적인 구현만으로는 충분하지 않다는 점을 기억해야 합니다. 개인 키의 물리적, 논리적 보안을 보장하고, 발급된 인증서에 최소 권한의 원칙을 적용하며, 전체 인증서 수명 주기를 관리하는 체계적인 정책이 함께할 때 비로소 우리는 수십억 개의 디바이스가 안전하게 상호작용하는 견고한 IoT 생태계를 구축할 수 있을 것입니다. 이 글이 그 여정을 시작하는 개발자들에게 신뢰할 수 있는 나침반이 되기를 바랍니다.
0 개의 댓글:
Post a Comment