• 포스트
  • 도구
  • 작업
  • 포스트
  • 도구
  • 작업
로그인회원가입

© 2005 kaisa.co.kr. All Rights Reserved.

소개개인정보처리방침7083620@hanmail.net
← 목록으로
2026-09-20

OCPP 보안 프로파일과 인증서 운영 — Basic 인증부터 mTLS, SignCertificate까지

OCPP 보안 프로파일과 인증서 운영 — Basic 인증부터 mTLS, SignCertificate까지

요약: OCPP 보안 프로파일 0~3의 의미와 실제 설정, 충전기·서버·CA 키스토어 세트를 한 번에 만드는 방법, SignCertificate로 충전기 인증서를 발급하는 흐름, 현장 IP 서버에서 루트 인증서를 자동 설치하게 만든 방법, 자주 하는 실수를 정리한다.


들어가며

충전기 보안은 오랫동안 "URL에 충전기 ID만 맞으면 붙는" 수준이었다. 보안 백서(1.6)와 2.0.1 본문이 보안 프로파일을 정의하면서 이제 입찰과 인증 시험 모두 최소 프로파일 2를 요구한다. 그리고 PnC로 가면 인증서가 충전 인증 자체가 된다.

이 글은 보안 프로파일을 실제 서버에 적용하고, 인증서를 만들고 배포하고 갱신하면서 겪은 것을 정리한다. PnC 특유의 PKI는 다음 글에서 다룬다.

1. 보안 프로파일 한 장 요약

프로파일 전송 충전기 인증 서버 인증 비고
0 ws 없음 없음 규격상 정의만. 운영 금지
1 ws HTTP Basic 없음 폐쇄망 전제. 비밀번호가 평문으로 흐른다
2 wss (TLS) HTTP Basic 서버 인증서 현실적인 최소선
3 wss (mTLS) 클라이언트 인증서 서버 인증서 Basic 없이 인증서로 충전기 식별

실무 포인트:

  • Basic 인증의 사용자명은 충전기 ID와 같아야 한다. 비밀번호는 충전기별로 달라야 하고, 서버는 상수 시간 비교를 한다. 비밀번호 변경은 1.6에서 ChangeConfiguration(AuthorizationKey), 2.0.1에서 SetVariables(SecurityCtrlr.BasicAuthPassword)로 서버가 내린다.
  • 프로파일 3의 충전기 인증서 CN은 충전기 ID여야 한다. 서버는 TLS 계층에서 인증서를 검증하고, 애플리케이션에서 CN과 URL의 ID가 같은지 확인한다.
  • 프로파일을 올리는 건 서버가 지시할 수 있지만(SecurityProfile 설정), 내리는 건 막아야 한다. 다운그레이드 공격이 된다.
  • 운영에서 TLS를 리버스 프록시에서 끝낸다면 프로파일 2까지만 가능하다. 프로파일 3은 클라이언트 인증서를 서버가 직접 봐야 하므로, 프록시가 인증서를 넘겨주는 구성을 하거나 서버가 직접 TLS를 받아야 한다.

2. 한 포트 = 한 TLS 설정

처음에 겪은 제약이다. 톰캣 커넥터 하나는 하나의 키스토어, 하나의 클라이언트 인증 정책을 가진다.

  • 프로파일 2와 3을 같이 받으려면 포트를 둘로 나누거나, client-auth: want로 받고 애플리케이션에서 프로파일을 판정해야 한다. 인증 시험(프로파일 3)은 need를 요구했다.
  • RSA 인증서와 ECDSA 인증서를 한 포트에서 섞지 못한다. 충전기 쪽 TLS 스택이 특정 알고리즘만 지원하는 경우가 있어, 알고리즘별 포트를 두기도 한다.
  • 프로파일 1(TLS 없음)은 아예 별도 서버 인스턴스로 분리했다(2편).

3. 키스토어 세트는 스크립트로 한 번에 만든다

필요한 파일이 생각보다 많다. 환경(로컬, 시험, 운영)마다, 알고리즘(RSA, ECDSA)마다 다음 세트를 만든다.

csms-root-ca          서버 측 루트 CA (충전기가 신뢰할 대상)
csms-server.p12       서버 TLS 인증서 + 키
csms-mtls-truststore  서버가 신뢰할 충전기 발급 CA (프로파일 3)
csms-ca.p12           SignCertificate의 CSR에 서명할 CA 키  ← 절대 외부 업로드 금지
cp-client.jks         충전기(또는 시험 도구) 클라이언트 인증서 + 키
cp-client-invalid.jks 일부러 잘못 만든 인증서 (거절 시험용)
cp-truststore.jks     충전기가 신뢰할 루트 (서버 루트 + 제조사 루트)
*.cer                 배포용 루트 인증서

PowerShell + keytool(또는 openssl) 스크립트 하나로 전체를 생성하게 만들었다. 경험상 규칙은 이렇다.

  • 파일 하나만 손으로 다시 만들지 마라. 체인이 깨진다. 항상 스크립트 전체를 다시 돌린다.
  • truststore에 개인 키를 넣지 마라. 신뢰 저장소에는 인증서만 들어간다.
  • 시험 도구가 JKS를 원하면 JKS로. PKCS12만 받는 도구와 JKS만 받는 도구가 섞여 있다.
  • CN/SAN이 실제 접속 호스트와 맞아야 한다. IP로 붙는 현장이면 SAN에 IP를 넣는다.
  • CA 키(서명용)는 서버 키스토어와 다른 파일, 다른 비밀번호로. 저장소에 커밋하지 않는다. 비밀번호는 문서가 아니라 비밀 저장소에 둔다.

4. SignCertificate → CertificateSigned: 충전기 인증서 발급

프로파일 3과 PnC의 충전기(SECC) 인증서는 충전기가 스스로 키를 만들고 CSR을 보내는 방식으로 발급한다.

충전기 ── SignCertificate(csr, certificateType) ──▶ CSMS
       ◀── Accepted ─────────────────────────────── (즉시 응답)
                             CSMS ── CSR ──▶ CA(내부 CA 또는 PKI)
                             CSMS ◀── 인증서 체인 ──
충전기 ◀── CertificateSigned(certificateChain) ──── CSMS (비동기)
       ── Accepted / Rejected ─────────────────────▶
  • SignCertificate에는 즉시 Accepted로 답하고, 발급은 비동기로 한다. CA 호출이 느려도 WebSocket 스레드를 붙잡지 않는다.
  • 서버가 발급을 먼저 시작하게 하려면 TriggerMessage(SignChargePointCertificate)(1.6은 ExtendedTriggerMessage) / TriggerMessage(SignChargingStationCertificate)(2.0.1)를 보낸다.
  • 체인은 리프부터 위로(leaf → 중간 CA) PEM을 이어 붙인다. 루트를 포함할지는 용도별 규칙을 따른다(PnC는 다음 글).
  • 발급한 인증서의 시리얼과 만료일을 저장해 둔다. 만료 임박 배치가 이걸 보고 재발급 트리거를 보낸다.

CSR 정리

현장에서 받은 CSR은 생각보다 지저분하다.

  • 줄바꿈이 CRLF로 섞여 있거나(윈도우 기반 충전기 소프트웨어), base64 중간에 줄바꿈이 들어 있다.
  • -----BEGIN CERTIFICATE REQUEST-----의 공백이 빠진 헤더(BEGINCERTIFICATEREQUEST)도 봤다.

그래서 헤더·푸터·공백을 전부 제거하고 순수 base64 DER만 남기는 정규식(\s|-----BEGIN[^-]*-----|-----END[^-]*-----)을 쓴다. CA 쪽이 PEM을 원하든 DER을 원하든 여기서 다시 만든다.

재발급을 막지 마라

"DB에 이미 유효한 인증서 시리얼이 있으면 재발급 요청을 무시하자"는 아이디어가 나왔다가 기각됐다. 충전기가 리셋 후 인증서 파일을 잃어버렸을 수 있다. 그러면 그 충전기는 영원히 인증서를 못 받는다. 대신 CA가 충돌(409)을 돌려주면 로그를 남기고 이전 인증서를 폐기해, 다음 시도가 저절로 성공하게 했다. 서버 DB보다 충전기의 현재 상태를 믿는 쪽이 복구가 쉽다.

거절 시험을 위한 장치

"잘못된 인증서를 보내면 충전기가 거절하고 보안 이벤트를 보내는가"를 확인하는 시험이 있다. 충전기별 일회성 플래그를 세우고 TriggerMessage를 보낸 뒤, 다음 CertificateSigned에 별도 키스토어로 만든 잘못된 인증서를 싣게 했다. 충전기는 Rejected를 보내고 SecurityEventNotification(InvalidChargingStationCertificate)를 보내야 한다.

5. 루트 인증서 배포: 충전기를 다시 빌드하지 않기

프로파일 2 이상에서 충전기는 서버 인증서를 검증할 루트를 가지고 있어야 한다. 공인 인증서(도메인)라면 OS 기본 신뢰 저장소로 충분하지만, IP로 붙는 사설 서버라면 사설 루트를 충전기에 넣어야 한다. 처음에는 루트가 바뀔 때마다 충전기 소프트웨어를 다시 배포했다.

지금은 이렇게 한다(충전기 HMI를 직접 만드는 경우).

  1. 접속 대상이 IP이고 프로파일 2 이상이면, 접속 전에 API 허브의 GET /ca/root?ip=…&algorithm=ecdsa&format=pem을 호출한다.
  2. 받은 루트를 로컬에 캐시하고, OS 루트 저장소에 설치한다(지문으로 중복 설치 방지).
  3. 그다음 wss로 접속한다.

서버 인증서를 바꿀 때 API 허브의 키스토어만 바꾸면 된다. 물론 "이 엔드포인트를 누가 믿을 것인가"라는 부트스트랩 문제가 남는다. 우리는 폐쇄망·고정 IP라는 전제에서 썼고, 공용망이라면 공인 인증서를 쓰는 것이 맞다. 1.6/2.0.1 규격의 InstallCertificate(CentralSystemRootCertificate / CSMSRootCertificate)로 서버가 루트를 내려주는 것이 표준 방식이고, 위 방식은 그 전 단계(최초 접속)를 풀기 위한 보조 수단이다.

6. 충전기 쪽에서 본 TLS 검증

충전기 소프트웨어(HMI)를 직접 만들면서 서버 인증서 검증 콜백을 구현했다. 기억할 점:

  • 체인 검증 결과를 자세히 로그로 남긴다(어느 단계에서 왜 실패했는지). 현장 원격 지원의 절반이 이 로그로 끝난다.
  • 설치된 CA로 검증되지 않는 체인은 거절한다. "개발 중이라 일단 모두 허용"을 운영에 남기지 않는다.
  • 와일드카드 CN 정책을 명확히 한다.
  • 인증 정보를 로그에 찍지 마라. 디버깅하다 보면 Basic 헤더를 디코딩해 Info 로그로 남기는 코드가 생긴다. 실제로 그런 코드를 찾아서 지웠다. 클라이언트 인증서의 PFX 비밀번호를 소스에 하드코딩하는 것도 마찬가지로 피해야 한다.

7. 인증서 관리 메시지 운영

  • GetInstalledCertificateIds로 충전기에 설치된 루트 목록을 주기적으로 받아 두면, 루트 교체 때 누락을 찾기 쉽다.
  • DeleteCertificate는 해시(hashAlgorithm, issuerNameHash, issuerKeyHash, serialNumber)로 지정한다. 충전기가 보고한 해시 값을 그대로 저장해 두었다가 쓰는 것이 가장 안전하다. 서버가 인증서에서 계산한 값과 충전기가 계산한 값의 표기(대소문자, 선행 0)가 다를 수 있다.
  • 해시 필드의 길이 제한(시리얼 40자, 해시 128자)을 넘는 값은 저장하지 않는다. 나중 삭제 요청이 제약 위반으로 실패한다. 반대로 40자 시리얼이 36자로 잘려 오는 구현도 봤다. 길이는 받는 쪽과 보내는 쪽 모두 로그로 확인하자.
  • 여러 줄 PEM을 JSON 문자열에 넣을 때 제어 문자 이스케이프를 빠뜨리면 JSON이 깨진다. 관리 화면에서 PEM을 입력받는다면 전송 전에 JSON.parse로 검증하자.

정리

  • 운영 최소선은 프로파일 2, 충전기 인증서로 식별하는 프로파일 3이 목표다. 다운그레이드는 막는다.
  • 한 포트는 한 TLS 설정이다. 프로파일·알고리즘별로 리스너를 설계하자.
  • 키스토어 세트는 스크립트로 전체를 한 번에 만든다. CA 서명 키는 따로, 절대 외부에 올리지 않는다.
  • SignCertificate는 즉시 Accepted, 발급은 비동기. CSR은 정규화하고, 재발급을 막지 말고, 만료를 추적한다.
  • 사설 루트 배포는 표준(InstallCertificate)을 기본으로 하되 최초 접속 문제를 풀 보조 경로를 설계한다.
  • 인증 정보는 로그와 소스에 남기지 않는다.

다음 글에서는 이 인증서 위에 세워지는 Plug & Charge, 국내 1차 PnC 테스트를 진행하며 터득한 것을 정리한다.


OCPP 시리즈

  1. OCPP 한눈에 보기 — 1.6J, 2.0.1, 2.1은 무엇이 다른가
  2. CSMS 아키텍처 — 버전별 OCPP 서버, API 허브, 레거시 공존
  3. OCPP-J 메시지 파이프라인 구현 — 핸드셰이크부터 응답 매칭까지
  4. 충전기 수천 대의 WebSocket 세션 운영기 — 중복 접속, 배포 끊김, NAT, 좀비 세션
  5. 트랜잭션과 계량값의 함정 — 멱등 처리, 늦게 온 Stop, 충전기 편차 정규화
  6. OCTT 인증 통과기 — OCPP 1.6과 2.0.1 CSMS 적합성 시험에서 배운 것
  7. OCPP 보안 프로파일과 인증서 운영 — Basic 인증부터 mTLS, SignCertificate까지 (현재 글)
  8. Plug & Charge 1차 필드 테스트 — ISO 15118 인증서 체계와 CSMS가 해야 할 일
  9. OCPP 버전별 세션 관리 화면이 필요한 이유 — 관제·원격 명령·시험을 한 콘솔에
  10. 충전기 HMI를 WPF로 만들기 — 계층화, 정규화, 키오스크 UI, 무중단 업데이트

댓글

0

아직 댓글이 없습니다. 첫 댓글을 남겨보세요.

이전 글OCTT 인증 통과기 — OCPP 1.6과 2.0.1 CSMS 적합성 시험에서 배운 것다음 글Plug & Charge 1차 필드 테스트 — ISO 15118 인증서 체계와 CSMS가 해야 할 일
← 목록으로