OpenSSL 모듈

OpenSSL 모듈

OpenSSL은 SSL, TLS와 일반 목적의 암호화를 제공해요. OpenSSL 라이브러리를 감싸고 있죠.

출처: Ruby 3.3 API

본문

예시 (Examples)

모든 예시는 다음으로 OpenSSL을 로드했을 때를 전제로 해요.

require 'openssl'

이 예시들은 서로 위에 쌓여 만들어져요. 예를 들어 다음 섹션에서 만든 키가 이후 예시 전반에서 사용돼요.

키 (Keys)

키 만들기 (Creating a Key)

이 예시는 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

키 내보내기 (Exporting a Key)

암호화 없이 디스크에 저장된 키는 보안에 취약해요. 키를 가로챈 사람이면 누구든 쓸 수 있으니까요. 키를 안전하게 내보내려면 패스워드와 함께 내보낼 수 있어요.

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 목록을 반환해요.

키 불러오기 (Loading a Key)

키는 파일에서도 불러올 수 있어요.

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

암호화된 키 불러오기 (Loading an Encrypted Key)

OpenSSL은 암호화된 키를 불러올 때 패스워드를 물어봐요. 패스워드를 직접 타이핑할 수 없는 상황이면, 키를 불러올 때 패스워드를 함께 줄 수 있어요.

key4_pem = File.read 'private.secure.pem'
password = 'my secure password goes here'
key4 = OpenSSL::PKey.read key4_pem, password

RSA 암호화 (RSA Encryption)

RSA는 공개 키와 개인 키를 써서 암호화와 복호화를 제공해요. 암호화된 데이터의 용도에 따라 다양한 패딩 방법을 쓸 수 있어요.

암호화와 복호화 (Encryption & Decryption)

비대칭 공개/개인 키 암호화는 느리고, 패딩 없이 쓰거나 큰 데이터 덩어리를 직접 암호화하는 경우 공격에 취약해요. 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), 추가로 키 유도 과정을 느리게 하는 반복 횟수를 사용해요. 키 유도가 느릴수록, 결과 키를 무차별 대입으로 뚫으려면 그만큼 더 많은 작업이 필요해요.

암호화 (Encryption)

전략은 먼저 암호화용 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

복호화 (Decryption)

대칭 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 인증서 (X509 Certificates)

인증서 만들기 (Creating a Certificate)

이 예시는 RSA 키와 SHA1 서명으로 자체 서명된 인증서를 만들어요.

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" 파일에서 확인할 수 있어요.

인증서 서명 (Signing a Certificate)

인증서에 서명하려면 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

인증서 불러오기 (Loading a Certificate)

키처럼 인증서도 파일에서 불러올 수 있어요.

cert2 = OpenSSL::X509::Certificate.new File.read 'certificate.pem'

인증서 검증 (Verifying a Certificate)

Certificate#verify는 인증서가 주어진 공개 키로 서명됐을 때 true를 반환해요.

raise 'certificate can not be verified' unless cert2.verify key

인증 기관 (Certificate Authority)

인증 기관(CA)은 신뢰할 수 있는 제3자로, 알 수 없는 인증서의 소유를 검증할 수 있게 해 줘요. CA는 그 키의 사용자를 신뢰한다는 뜻의 키 서명을 발행해요. 키를 만난 사용자는 CA의 공개 키로 서명을 검증할 수 있어요.

CA 키 (CA Key)

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 Certificate)

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에서 인증서 만들기 (Creating a Certificate from a 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 and TLS Connections)

만들어 둔 키와 인증서로 SSL이나 TLS 연결을 만들 수 있어요. SSLContext로 SSL 세션을 구성해요.

context = OpenSSL::SSL::SSLContext.new

SSL 서버 (SSL Server)

SSL 서버는 클라이언트와 안전하게 통신하려면 인증서와 개인 키가 필요해요.

context.cert = cert
context.key = key

그다음 TCP 서버 소켓과 컨텍스트로 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 client)

SSL 클라이언트는 TCP 소켓과 컨텍스트로 만들어져요. SSL 핸드셰이크를 시작하고 암호화를 켜려면 SSLSocket#connect를 호출해야 해요. 클라이언트 소켓에는 키와 인증서가 필요 없어요.

참고로 SSLSocket#close는 기본적으로 밑에 있는 소켓을 닫지 않아요. 원하면 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

Public Class Methods

Digest (name)

이름으로 Digest 하위클래스를 반환해요.

require 'openssl'

OpenSSL::Digest("MD5")
# => OpenSSL::Digest::MD5

Digest("Foo")
# => NameError: wrong constant name Foo

debug → true | false

디버그 모드 여부를 반환해요.

debug = boolean → boolean

디버그 모드를 켜거나 꺼요. 디버그 모드에서는 OpenSSL 에러 큐에 추가된 모든 에러가 stderr로 출력돼요.

errors → [String...]

큐에 남아 있는 나머지 에러를 확인해요.

여기 보이는 에러는 대부분 Ruby의 OpenSSL 구현의 버그 때문일 가능성이 커요.

fips_mode → true | false

FIPS 모드 여부를 반환해요.

fips_mode = boolean → boolean

FIPS 모드를 켜거나 꺼요. FIPS 모드를 켜는 것은 당연히 OpenSSL 라이브러리의 FIPS 지원 설치에서만 효과가 있어요. 그렇지 않은 경우 시도하면 에러가 날 거예요.

OpenSSL.fips_mode = true   # turn FIPS mode on
OpenSSL.fips_mode = false  # and off again

fixed_length_secure_compare(string, string) → boolean

HMAC 계산 결과 같은 고정 길이 문자열에 대해 일정 시간(constant time) 메모리 비교를 해요.

문자열이 동일하면 true, 길이가 같지만 동일하지 않으면 false를 반환해요. 길이가 다르면 ArgumentError가 던져져요.

secure_compare(string, string) → boolean

일정 시간 메모리 비교를 해요. 입력은 비밀의 길이를 가리기 위해 SHA-256으로 해시돼요. 문자열이 동일하면 true, 그렇지 않으면 false를 반환해요.

Private Instance Methods

Digest (name)

이름으로 Digest 하위클래스를 반환해요.

require 'openssl'

OpenSSL::Digest("MD5")
# => OpenSSL::Digest::MD5

Digest("Foo")
# => NameError: wrong constant name Foo