OpenSSL
OpenSSL
OpenSSL은 SSL, TLS, 그리고 범용 암호화 기능을 제공하는 모듈이에요. OpenSSL 라이브러리를 감싸서 Ruby에서 쓸 수 있게 해 주죠.
출처: Ruby 4.0 API
본문
아래 예제들은 모두 require 'openssl'로 OpenSSL을 로드했다고 가정해요. 그리고 예제끼리 서로 위에 쌓아가는 구조예요. 예를 들어 다음에서 만든 키가 뒤이은 예제들 전체에서 쓰여요.
Keys
키 만들기
이 예제는 2048비트 RSA 키 쌍을 만들고 현재 디렉터리에 써요.
key = OpenSSL::PKey::RSA.new 2048
File.write 'private_key.pem', key.private_to_pem
File.write 'public_key.pem', key.public_to_pem
키 내보내기
암호화 없이 디스크에 저장한 키는 안전하지 않아요. 키를 손에 넣는 사람이라면 누구나(암호화 안 됐다면) 쓸 수 있으니까요. 키를 안전하게 내보내려면 비밀번호와 함께 내보내면 돼요.
cipher = OpenSSL::Cipher.new 'aes-256-cbc'
password = 'my secure password goes here'
key_secure = key.private_to_pem cipher, password
File.write 'private.secure.pem', key_secure
OpenSSL::Cipher.ciphers는 사용 가능한 cipher 목록을 돌려줘요.
키 로드하기
키는 파일에서도 불러올 수 있어요.
key2 = OpenSSL::PKey.read File.read 'private_key.pem'
key2.public? # => true
key2.private? # => true
또는 이렇게요.
key3 = OpenSSL::PKey.read File.read 'public_key.pem'
key3.public? # => true
key3.private? # => false
암호화된 키 로드하기
암호화된 키를 로드할 때 OpenSSL은 비밀번호 입력을 요구해요. 비밀번호를 직접 타이핑할 수 없는 상황이라면, 키를 로드할 때 비밀번호를 함께 넘겨주면 돼요.
key4_pem = File.read 'private.secure.pem'
password = 'my secure password goes here'
key4 = OpenSSL::PKey.read key4_pem, password
RSA 암호화
RSA는 공개 키와 개인 키를 이용해 암·복호화를 제공해요. 암호화된 데이터의 용도에 따라 다양한 패딩(padding) 방식을 쓸 수 있어요.
암호화와 복호화
비대칭 공개/개인 키 암호화는 느리고, 패딩 없이 쓰거나 큰 데이터 덩어리를 직접 암호화할 때 공격에 취약해요. RSA 암호화의 전형적인 용도는 대칭 키를 받는 사람의 공개 키로 "감싸는(wrap)" 것이고, 받는 사람이 자기 개인 키로 그 대칭 키를 다시 "푸는(unwrap)" 구조예요. 아래는 그런 키 전송 계획을 단순화한 예시예요. 실제로는 쓰면 안 되고, 표준화된 프로토콜을 항상 우선해야 해요.
wrapped_key = key.public_encrypt key
공개 키로 암호화된 대칭 키는 받는 사람의 대응하는 개인 키로만 복호화할 수 있어요.
original_key = key.private_decrypt wrapped_key
기본적으로 PKCS#1 패딩이 사용되지만, 다른 패딩 형태도 쓸 수 있어요. 자세한 내용은 PKey::RSA를 참고하세요.
서명(Signatures)
"private_encrypt"로 개인 키를 사용해 데이터를 암호화하는 것은 데이터에 디지털 서명을 적용하는 것과 동등해요. 검증하는 쪽은 서명을 "public_decrypt"로 복호화한 결과를 원본 데이터와 비교해서 서명을 검증할 수 있어요. 하지만 OpenSSL::PKey에는 디지털 서명을 표준화된 방식으로 처리하는 "sign"과 "verify" 메서드가 이미 있어요. "private_encrypt"와 "public_decrypt"는 실제로 쓰면 안 돼요.
문서에 서명하려면 먼저 문서의 암호학적으로 안전한 해시를 계산하고, 그 해시를 개인 키로 서명해요.
signature = key.sign 'SHA256', document
서명을 검증하려면 다시 문서의 해시를 계산하고, 서명을 공개 키로 복호화해요. 그 결과를 방금 계산한 해시와 비교해서 같으면 서명이 유효한 거예요.
if key.verify 'SHA256', signature, document
puts 'Valid'
else
puts 'Invalid'
end
PBKDF2 패스워드 기반 암호화
사용 중인 OpenSSL 버전이 지원한다면, 패스워드 기반 암호화는 PKCS5의 기능을 사용해야 해요. 지원되지 않거나 레거시 애플리케이션에서 요구한다면, RFC 2898에 명시된 더 오래되고 덜 안전한 방법도 지원돼요(아래 참고).
PKCS5는 PKCS#5 v2.0에 명시된 대로 PBKDF2를 지원해요. 여전히 패스워드와 솔트(salt)를 사용하고, 거기에 키 도출 과정을 느리게 만드는 반복 횟수(iterations)를 추가해요. 느릴수록 결과 키를 무차별 대입(brute-force)으로 뚫는 데 더 많은 작업이 필요해지죠.
암호화
전략은 먼저 암호화용 Cipher를 만들고, PBKDF2로 패스워드에서 도출한 키와 무작위 IV를 생성하는 거예요. PKCS#5 v2.0은 솔트에 최소 8바이트를 권장하고, 반복 횟수는 사용하는 하드웨어에 크게 의존해요.
cipher = OpenSSL::Cipher.new 'aes-256-cbc'
cipher.encrypt
iv = cipher.random_iv
pwd = 'some hopefully not to easily guessable password'
salt = OpenSSL::Random.random_bytes 16
iter = 20000
key_len = cipher.key_len
digest = OpenSSL::Digest.new('SHA256')
key = OpenSSL::PKCS5.pbkdf2_hmac(pwd, salt, iter, key_len, digest)
cipher.key = key
Now encrypt the data:
encrypted = cipher.update document
encrypted << cipher.final
복호화
앞서와 같은 단계로 대칭 AES 키를 도출하되, 이번에는 Cipher를 복호화용으로 설정해요.
cipher = OpenSSL::Cipher.new 'aes-256-cbc'
cipher.decrypt
cipher.iv = iv # the one generated with #random_iv
pwd = 'some hopefully not to easily guessable password'
salt = ... # the one generated above
iter = 20000
key_len = cipher.key_len
digest = OpenSSL::Digest.new('SHA256')
key = OpenSSL::PKCS5.pbkdf2_hmac(pwd, salt, iter, key_len, digest)
cipher.key = key
Now decrypt the data:
decrypted = cipher.update encrypted
decrypted << cipher.final
X509 인증서
인증서 만들기
이 예제는 RSA 키와 SHA1 서명을 사용해 자체 서명(self-signed) 인증서를 만들어요.
key = OpenSSL::PKey::RSA.new 2048
name = OpenSSL::X509::Name.parse '/CN=nobody/DC=example'
cert = OpenSSL::X509::Certificate.new
cert.version = 2
cert.serial = 0
cert.not_before = Time.now
cert.not_after = Time.now + 3600
cert.public_key = key.public_key
cert.subject = name
인증서 확장(Certificate Extensions)
OpenSSL::SSL::ExtensionFactory로 인증서에 확장을 추가해서 인증서의 용도를 나타낼 수 있어요.
extension_factory = OpenSSL::X509::ExtensionFactory.new nil, cert
cert.add_extension \
extension_factory.create_extension('basicConstraints', 'CA:FALSE', true)
cert.add_extension \
extension_factory.create_extension(
'keyUsage', 'keyEncipherment,dataEncipherment,digitalSignature')
cert.add_extension \
extension_factory.create_extension('subjectKeyIdentifier', 'hash')
지원되는 확장 목록(그리고 어떤 경우엔 가능한 값들)은 OpenSSL 소스 코드의 "objects.h" 파일에서 확인할 수 있어요.
인증서 서명하기
인증서에 서명하려면 issuer를 설정하고 OpenSSL::X509::Certificate#sign을 다이제스트 알고리즘과 함께 사용해요. 이렇게 하면 인증서를 만들 때 쓴 것과 같은 이름과 키로 서명하므로 자체 서명 인증서가 돼요.
cert.issuer = name
cert.sign key, OpenSSL::Digest.new('SHA1')
open 'certificate.pem', 'w' do |io| io.write cert.to_pem end
인증서 로드하기
키처럼 인증서도 파일에서 로드할 수 있어요.
cert2 = OpenSSL::X509::Certificate.new File.read 'certificate.pem'
인증서 검증하기
Certificate#verify는 인증서가 주어진 공개 키로 서명됐을 때 true를 돌려줘요.
raise 'certificate can not be verified' unless cert2.verify key
인증 기관(Certificate Authority)
인증 기관(CA)은 신뢰할 수 있는 제3자로, 알 수 없는 인증서의 소유권을 검증할 수 있게 해 줘요. CA는 그 키의 사용자를 신뢰한다는 의미의 키 서명을 발행해요. 키를 마주한 사용자는 CA의 공개 키로 서명을 검증할 수 있죠.
CA 키
CA 키는 소중하니까 암호화해서 디스크에 저장하고, 다른 사용자가 읽지 못하게 해요.
ca_key = OpenSSL::PKey::RSA.new 2048
password = 'my secure password goes here'
cipher = 'aes-256-cbc'
open 'ca_key.pem', 'w', 0400 do |io|
io.write ca_key.private_to_pem(cipher, password)
end
CA 인증서
CA 인증서는 위에서 인증서를 만든 것과 같은 방식으로 만들되, 확장만 다르게 해요.
ca_name = OpenSSL::X509::Name.parse '/CN=ca/DC=example'
ca_cert = OpenSSL::X509::Certificate.new
ca_cert.serial = 0
ca_cert.version = 2
ca_cert.not_before = Time.now
ca_cert.not_after = Time.now + 86400
ca_cert.public_key = ca_key.public_key
ca_cert.subject = ca_name
ca_cert.issuer = ca_name
extension_factory = OpenSSL::X509::ExtensionFactory.new
extension_factory.subject_certificate = ca_cert
extension_factory.issuer_certificate = ca_cert
ca_cert.add_extension \
extension_factory.create_extension('subjectKeyIdentifier', 'hash')
이 확장은 CA의 키가 CA로 쓰일 수 있음을 나타내요.
ca_cert.add_extension \
extension_factory.create_extension('basicConstraints', 'CA:TRUE', true)
이 확장은 CA의 키가 인증서와 인증서 폐기 둘 다의 서명을 검증하는 데 쓰일 수 있음을 나타내요.
ca_cert.add_extension \
extension_factory.create_extension(
'keyUsage', 'cRLSign,keyCertSign', true)
루트 CA 인증서는 자체 서명돼요.
ca_cert.sign ca_key, OpenSSL::Digest.new('SHA1')
CA 인증서는 이 CA가 서명할 키들의 모든 사용자에게 배포될 수 있도록 디스크에 저장해요.
open 'ca_cert.pem', 'w' do |io|
io.write ca_cert.to_pem
end
인증서 서명 요청(Certificate Signing Request)
CA는 인증서 서명 요청(CSR)을 통해 키에 서명해요. CSR에는 키를 식별하는 데 필요한 정보가 들어 있어요.
csr = OpenSSL::X509::Request.new
csr.version = 0
csr.subject = name
csr.public_key = key.public_key
csr.sign key, OpenSSL::Digest.new('SHA1')
CSR은 디스크에 저장했다가 서명을 위해 CA에 보내요.
open 'csr.pem', 'w' do |io|
io.write csr.to_pem
end
CSR에서 인증서 만들기
CSR을 받은 CA는 서명하기 전에 검증해요. 최소한의 검증은 CSR의 서명을 확인하는 거예요.
csr = OpenSSL::X509::Request.new File.read 'csr.pem'
raise 'CSR can not be verified' unless csr.verify csr.public_key
검증 후 인증서가 만들어지고, 다양한 용도로 표시되며, CA 키로 서명된 뒤 요청자에게 돌아가요.
csr_cert = OpenSSL::X509::Certificate.new
csr_cert.serial = 0
csr_cert.version = 2
csr_cert.not_before = Time.now
csr_cert.not_after = Time.now + 600
csr_cert.subject = csr.subject
csr_cert.public_key = csr.public_key
csr_cert.issuer = ca_cert.subject
extension_factory = OpenSSL::X509::ExtensionFactory.new
extension_factory.subject_certificate = csr_cert
extension_factory.issuer_certificate = ca_cert
csr_cert.add_extension \
extension_factory.create_extension('basicConstraints', 'CA:FALSE')
csr_cert.add_extension \
extension_factory.create_extension(
'keyUsage', 'keyEncipherment,dataEncipherment,digitalSignature')
csr_cert.add_extension \
extension_factory.create_extension('subjectKeyIdentifier', 'hash')
csr_cert.sign ca_key, OpenSSL::Digest.new('SHA1')
open 'csr_cert.pem', 'w' do |io|
io.write csr_cert.to_pem
end
SSL/TLS 연결
만든 키와 인증서를 사용해 SSL이나 TLS 연결을 만들 수 있어요. OpenSSL::SSL::SSLContext는 SSL 세션을 설정하는 데 사용돼요.
context = OpenSSL::SSL::SSLContext.new
SSL 서버
SSL 서버는 클라이언트와 안전하게 통신하려면 인증서와 개인 키가 필요해요.
context.cert = cert
context.key = key
그리고 TCP 서버 소켓과 컨텍스트로 OpenSSL::SSL::SSLServer를 만들어요. SSLServer를 평범한 TCP 서버처럼 사용하면 돼요.
require 'socket'
tcp_server = TCPServer.new 5000
ssl_server = OpenSSL::SSL::SSLServer.new tcp_server, context
loop do
ssl_connection = ssl_server.accept
data = ssl_connection.gets
response = "I got #{data.dump}"
puts response
ssl_connection.puts "I got #{data.dump}"
ssl_connection.close
end
SSL 클라이언트
SSL 클라이언트는 TCP 소켓과 컨텍스트로 만들어요. SSL 핸드셰이크를 시작하고 암호화를 시작하려면 OpenSSL::SSL::SSLSocket#connect를 호출해야 해요. 클라이언트 소켓에는 키와 인증서가 필요하지 않아요.
OpenSSL::SSL::SSLSocket#close는 기본적으로 밑의 소켓을 닫지 않는다는 점에 유의하세요. 원한다면 OpenSSL::SSL::SSLSocket#sync_close를 true로 설정하면 돼요.
require 'socket'
tcp_socket = TCPSocket.new 'localhost', 5000
ssl_client = OpenSSL::SSL::SSLSocket.new tcp_socket, context
ssl_client.sync_close = true
ssl_client.connect
ssl_client.puts "hello server!"
puts ssl_client.gets
ssl_client.close # shutdown the TLS connection and close tcp_socket
피어 검증(Peer Verification)
검증되지 않은 SSL 연결은 그다지 안전을 제공하지 못해요. 강화된 보안을 위해 클라이언트나 서버는 피어의 인증서를 검증할 수 있어요.
클라이언트를 수정해서 서버의 인증서를 인증 기관의 인증서와 대조해 검증할 수 있어요.
context.ca_file = 'ca_cert.pem'
context.verify_mode = OpenSSL::SSL::VERIFY_PEER
require 'socket'
tcp_socket = TCPSocket.new 'localhost', 5000
ssl_client = OpenSSL::SSL::SSLSocket.new tcp_socket, context
ssl_client.connect
ssl_client.puts "hello server!"
puts ssl_client.gets
서버 인증서가 유효하지 않거나 피어 검증 시 context.ca_file이 설정돼 있지 않으면 OpenSSL::SSL::SSLError가 발생해요.
Constants
LIBRESSL_VERSION_NUMBER
Ruby/OpenSSL 확장을 컴파일할 때 사용된 LibreSSL 라이브러리 버전 번호예요. 런타임에 사용되는 버전과 다를 수 있어요. 이 상수는 확장이 LibreSSL로 컴파일된 경우에만 정의돼요. 번호는 0xMNNFF00f(major minor fix 00 status) 형식을 따라요. 매뉴얼 페이지 LIBRESSL_VERSION_NUMBER(3)도 참고하세요.
OPENSSL_FIPS
OpenSSL 라이브러리가 FIPS 지원 여부를 나타내는 불리언이에요. OpenSSL 3.0 이후에서는 항상 true예요. 이 상수는 폐기 예정(obsolete)이고 미래에 제거될 거예요. OpenSSL.fips_mode도 참고하세요.
OPENSSL_LIBRARY_VERSION
현재 런타임에서 사용 중인 OpenSSL 라이브러리 버전 문자열이에요.
OPENSSL_VERSION
Ruby/OpenSSL 확장을 컴파일할 때 사용된 OpenSSL 라이브러리 버전 문자열이에요. 런타임 버전과 다를 수 있어요.
OPENSSL_VERSION_NUMBER
Ruby/OpenSSL 확장을 컴파일할 때 사용된 OpenSSL 라이브러리 버전 번호예요. 런타임 버전과 다를 수 있어요. 버전 번호는 단일 정수 값으로 인코딩돼요. 형식은 다음과 같아요.
- OpenSSL 3.0.0 이상: 0xMNN00PP0 (major minor 00 patch 0)
- OpenSSL 1.1.1 이하: 0xMNNFFPPS (major minor fix patch status)
- LibreSSL: 0x20000000 (고정 값)
매뉴얼 페이지 OPENSSL_VERSION_NUMBER(3)도 참고하세요.
VERSION
Ruby/OpenSSL의 버전 문자열이에요.
Public Class Methods
Digest(name)
이름으로 Digest 서브클래스를 돌려줘요.
require 'openssl'
OpenSSL::Digest("MD5")
# => OpenSSL::Digest::MD5
OpenSSL::Digest("Foo")
# => NameError: wrong constant name Foo
debug → true | false
Ruby/OpenSSL의 디버그 모드가 현재 켜져 있는지 돌려줘요.
debug = boolean
디버그 모드를 켜거나 꺼요. 디버그 모드에서는 OpenSSL 오류 큐에 추가된 모든 오류가 stderr로 출력돼요.
errors → [String...]
OpenSSL 스레드-로컬 오류 큐에 남아 있는 나머지 오류들을 돌려주고 큐를 비워요. 보통은 빈 배열을 돌려줘야 해요. Ruby/OpenSSL 디버깅을 위한 메서드예요. 여기서 오류를 보게 된다면 확장의 버그일 가능성이 높아요. github.com/ruby/openssl에 이슈를 올려 주세요. 프로그램을 디버깅할 때는 OpenSSL.debug=가 유용할 수 있어요.
fips_mode → true | false
FIPS 모드가 현재 켜져 있는지 돌려줘요.
fips_mode = boolean
FIPS 모드를 켜거나 꺼요. FIPS 모드를 켜는 것은 분명히 FIPS 지원 OpenSSL 설치에서만 효과가 있어요. 그 외의 경우 시도하면 오류가 나요.
OpenSSL.fips_mode = true # turn FIPS mode on
OpenSSL.fips_mode = false # and off again
fixed_length_secure_compare(string, string) → true or false
고정 길이 문자열(예: HMAC 계산 결과)을 위한 상수 시간(constant time) 메모리 비교예요. 문자열이 동일하면 true, 같은 길이지만 동일하지 않으면 false를 돌려줘요. 길이가 다르면 ArgumentError가 발생해요.
secure_compare(string, string) → true or false
상수 시간 메모리 비교예요. 입력을 SHA-256으로 해시해 비밀의 길이를 숨겨요. 문자열이 동일하면 true, 아니면 false를 돌려줘요. 이 메서드는 SHA-256 해싱 때문에 비싸요. 입력 길이가 같다고 알려졌거나 길이가 민감하지 않은 대부분의 경우에는 OpenSSL.fixed_length_secure_compare를 쓰는 게 낫습니다.
Private Instance Methods
Digest(name)
이름으로 Digest 서브클래스를 돌려줘요.
require 'openssl'
OpenSSL::Digest("MD5")
# => OpenSSL::Digest::MD5
OpenSSL::Digest("Foo")
# => NameError: wrong constant name Foo