IoT(사물 인터넷) 프로젝트가 PoC(개념 증명) 단계를 넘어 대규모 양산(Mass Production) 단계로 진입할 때 가장 먼저 마주하는 병목 구간은 '디바이스 프로비저닝'입니다. 수만, 수백만 대의 디바이스에 동일한 자격 증명을 심는 것은 보안상 용납될 수 없으며, 그렇다고 수동으로 개별 인증서를 주입하는 것은 운영 비용 측면에서 불가능에 가깝습니다. 결국 디바이스가 스스로 신원을 증명하고 인증서를 발급받는 자동화된 워크플로우가 필수적입니다.
1. 프로비저닝 아키텍처와 CSR의 필요성
AWS IoT Core를 포함한 대부분의 엔터프라이즈 IoT 플랫폼은 mTLS(Mutual TLS) 방식을 표준으로 채택하고 있습니다. 이는 서버뿐만 아니라 클라이언트(디바이스)도 인증서를 통해 자신을 증명해야 함을 의미합니다. 여기서 핵심은 개인 키(Private Key)의 소유권 보장입니다.
중앙 서버에서 키 쌍(Key Pair)을 생성하여 디바이스에 배포하는 방식은 'Key Distribution Problem'을 야기합니다. 배포 과정에서 키가 탈취될 위험(Man-in-the-Middle)이 존재하기 때문입니다. 따라서 보안 모범 사례(Best Practice)는 다음과 같습니다.
- 디바이스 내부(가능하면 Secure Element/TPM)에서 키 쌍을 생성한다.
- 개인 키는 디바이스 외부로 절대 유출되지 않는다.
- 공개 키와 디바이스 정보를 담은 CSR(Certificate Signing Request)을 생성하여 CA(AWS IoT)에 전송한다.
- CA는 서명을 검증하고 X.509 인증서를 발급하여 디바이스에 반환한다.
2. Bouncy Castle 선정 이유 및 라이브러리 구성
Java의 표준 라이브러리인 JCA(Java Cryptography Architecture)와 JCE(Java Cryptography Extension)만으로도 이론상 CSR 생성이 가능합니다. 그러나 실제 현업 프로젝트에서 Bouncy Castle(BC)을 사실상의 표준으로 사용하는 이유는 명확합니다.
| 비교 항목 | 표준 Java (JCA/JCE) | Bouncy Castle |
|---|---|---|
| 구현 난이도 | ASN.1 구조를 직접 다루어야 하는 경우가 많아 복잡함 | PKCS10CertificationRequestBuilder 등 고수준 API 제공 |
| 알고리즘 지원 | JDK 버전에 따라 지원하는 ECC 커브가 제한적일 수 있음 | 최신 암호화 알고리즘 및 다양한 타원 곡선(NIST, Brainpool 등) 지원 |
| 이식성 | 특정 벤더(Oracle, IBM 등) JDK에 종속될 수 있음 | 경량화되어 있으며 안드로이드 및 임베디드 Java 환경 호환성 우수 |
특히 Bouncy Castle의 PKIX 모듈은 X.509 인증서와 CSR(PKCS#10) 처리에 있어 강력한 추상화 계층을 제공하여, 개발자가 ASN.1 인코딩의 세부 사항을 몰라도 안전한 코드를 작성할 수 있게 돕습니다.
Dependency 설정 (Gradle)
dependencies {
// Core Provider
implementation 'org.bouncycastle:bcprov-jdk18on:1.76'
// PKIX/CMS/CSR Support (필수)
implementation 'org.bouncycastle:bcpkix-jdk18on:1.76'
}
3. 구현: CSR 생성 프로세스 상세
다음은 Bouncy Castle을 사용하여 RSA 또는 ECDSA 키 쌍을 생성하고, 이를 기반으로 CSR을 생성하여 PEM 포맷으로 출력하는 전체 로직입니다. 이 코드는 스레드 안전(Thread-safe)하며 AWS IoT Fleet Provisioning에 즉시 사용 가능합니다.
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.StringWriter;
import java.security.*;
import java.security.spec.ECGenParameterSpec;
public class AwsIotCsrGenerator {
static {
// 런타임에 Bouncy Castle Provider 등록 (필수)
if (Security.getProvider(BouncyCastleProvider.PROVIDER_NAME) == null) {
Security.addProvider(new BouncyCastleProvider());
}
}
/**
* AWS IoT용 ECC(Elliptic Curve) 키 쌍 생성 (NIST P-256)
*/
public KeyPair generateEcKeyPair() throws Exception {
KeyPairGenerator keyPairGenerator = KeyPairGenerator.getInstance("ECDSA", "BC");
// AWS IoT는 prime256v1 (secp256r1)을 권장합니다.
ECGenParameterSpec ecSpec = new ECGenParameterSpec("prime256v1");
keyPairGenerator.initialize(ecSpec, new SecureRandom());
return keyPairGenerator.generateKeyPair();
}
/**
* CSR 생성 및 서명
* @param keyPair 생성된 키 쌍
* @param commonName 디바이스 식별자 (Thing Name)
*/
public String generateCsrPem(KeyPair keyPair, String commonName) throws Exception {
// 1. Subject Name 구성 (X.500)
X500NameBuilder nameBuilder = new X500NameBuilder(BCStyle.INSTANCE);
nameBuilder.addRDN(BCStyle.CN, commonName);
nameBuilder.addRDN(BCStyle.O, "My Organization");
nameBuilder.addRDN(BCStyle.OU, "IoT Unit");
nameBuilder.addRDN(BCStyle.C, "KR");
X500Name subject = nameBuilder.build();
// 2. CSR 빌더 초기화 (Public Key 포함)
PKCS10CertificationRequestBuilder p10Builder =
new JcaPKCS10CertificationRequestBuilder(subject, keyPair.getPublic());
// 3. 서명 알고리즘 지정 (ECC의 경우 SHA256withECDSA)
String signatureAlgorithm = "SHA256withECDSA";
// RSA의 경우 "SHA256withRSA" 사용
// 4. ContentSigner 생성 (Private Key로 서명 준비)
JcaContentSignerBuilder csBuilder = new JcaContentSignerBuilder(signatureAlgorithm);
csBuilder.setProvider("BC"); // 명시적 Provider 지정
ContentSigner signer = csBuilder.build(keyPair.getPrivate());
// 5. CSR 생성
PKCS10CertificationRequest csr = p10Builder.build(signer);
// 6. PEM 포맷 변환
return convertToPem(csr);
}
private String convertToPem(PKCS10CertificationRequest csr) throws Exception {
StringWriter stringWriter = new StringWriter();
try (PemWriter pemWriter = new PemWriter(stringWriter)) {
pemWriter.writeObject(new PemObject("CERTIFICATE REQUEST", csr.getEncoded()));
}
return stringWriter.toString();
}
}
4. 보안 아키텍처 및 트레이드오프 분석
코드 구현 이후에는 시스템 레벨의 보안 전략을 수립해야 합니다. 단순히 CSR을 생성하는 것을 넘어, 키의 생명주기를 어떻게 관리할 것인지가 중요합니다.
4.1. Hardware Security Module (HSM) 통합
위의 코드는 편의상 소프트웨어 레벨(Memory)에서 키를 생성합니다. 하지만 최상의 보안을 위해서는 키 생성 로직이 애플리케이션 프로세스가 아닌, Secure Element(SE), TPM, 또는 HSM 내부에서 수행되어야 합니다.
- Software Key Store (JKS/PKCS12): 구현이 쉽고 비용이 저렴하지만, OS 레벨의 취약점 공격(Rooting 등) 시 키가 유출될 수 있습니다.
- Hardware Backed Keystore: 안드로이드 Keystore 시스템이나 전용 암호화 칩을 사용합니다. Bouncy Castle은 `KeyStore` API를 통해 하드웨어 모듈과 연동할 수 있는 인터페이스를 제공하므로, 실제 프로덕션 코드에서는 `KeyPairGenerator` 대신 하드웨어 연동 Provider를 사용하도록 코드를 리팩토링해야 합니다.
4.2. Fleet Provisioning과 권한 분리
생성된 CSR은 AWS IoT의 CreateCertificateFromCsr API 또는 MQTT 토픽을 통해 제출됩니다. 이때 보안을 위해 초기 진입용 'Bootstrap 인증서'와 실제 운영용 'Operational 인증서'를 분리해야 합니다.
- Bootstrap 인증서: 매우 제한적인 권한(오직 프로비저닝 API만 호출 가능)을 가지며, 프로비저닝 완료 후 폐기하거나 사용을 중단합니다.
- Operational 인증서: CSR을 통해 발급받은 고유 인증서로, 디바이스의 실제 텔레메트리 데이터 전송 권한을 갖습니다.
결론
Bouncy Castle을 활용한 CSR 생성 자동화는 AWS IoT 대규모 배포를 위한 핵심 기술입니다. 이를 통해 개인 키의 이동을 원천적으로 차단하고, 제조 공정이나 초기 구동 시점에 디바이스 스스로 신원을 확립하는 'Zero Touch Provisioning'을 구현할 수 있습니다. 엔지니어는 구현의 편의성뿐만 아니라 키 저장소의 물리적 보안과 인증서 수명 주기 관리(Rotation)까지 고려한 전체적인 보안 아키텍처를 설계해야 합니다.
Post a Comment