summaryrefslogtreecommitdiff
path: root/lib
diff options
context:
space:
mode:
Diffstat (limited to 'lib')
-rw-r--r--lib/gitlab/email/hook/smime_signature_interceptor.rb50
-rw-r--r--lib/gitlab/email/smime/certificate.rb36
-rw-r--r--lib/gitlab/email/smime/signer.rb29
3 files changed, 115 insertions, 0 deletions
diff --git a/lib/gitlab/email/hook/smime_signature_interceptor.rb b/lib/gitlab/email/hook/smime_signature_interceptor.rb
new file mode 100644
index 00000000000..e48041d9218
--- /dev/null
+++ b/lib/gitlab/email/hook/smime_signature_interceptor.rb
@@ -0,0 +1,50 @@
+# frozen_string_literal: true
+
+module Gitlab
+ module Email
+ module Hook
+ class SmimeSignatureInterceptor
+ # Sign emails with SMIME if enabled
+ class << self
+ def delivering_email(message)
+ signed_message = Gitlab::Email::Smime::Signer.sign(
+ cert: certificate.cert,
+ key: certificate.key,
+ data: message.encoded)
+ signed_email = Mail.new(signed_message)
+
+ overwrite_body(message, signed_email)
+ overwrite_headers(message, signed_email)
+ end
+
+ private
+
+ def certificate
+ @certificate ||= Gitlab::Email::Smime::Certificate.from_files(key_path, cert_path)
+ end
+
+ def key_path
+ Gitlab.config.gitlab.email_smime.key_file
+ end
+
+ def cert_path
+ Gitlab.config.gitlab.email_smime.cert_file
+ end
+
+ def overwrite_body(message, signed_email)
+ # since this is a multipart email, assignment to nil is important,
+ # otherwise Message#body will add a new mail part
+ message.body = nil
+ message.body = signed_email.body.encoded
+ end
+
+ def overwrite_headers(message, signed_email)
+ message.content_disposition = signed_email.content_disposition
+ message.content_transfer_encoding = signed_email.content_transfer_encoding
+ message.content_type = signed_email.content_type
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/email/smime/certificate.rb b/lib/gitlab/email/smime/certificate.rb
new file mode 100644
index 00000000000..b331c4ca19c
--- /dev/null
+++ b/lib/gitlab/email/smime/certificate.rb
@@ -0,0 +1,36 @@
+# frozen_string_literal: true
+
+module Gitlab
+ module Email
+ module Smime
+ class Certificate
+ include OpenSSL
+
+ attr_reader :key, :cert
+
+ def key_string
+ @key.to_s
+ end
+
+ def cert_string
+ @cert.to_pem
+ end
+
+ def self.from_strings(key_string, cert_string)
+ key = PKey::RSA.new(key_string)
+ cert = X509::Certificate.new(cert_string)
+ new(key, cert)
+ end
+
+ def self.from_files(key_path, cert_path)
+ from_strings(File.read(key_path), File.read(cert_path))
+ end
+
+ def initialize(key, cert)
+ @key = key
+ @cert = cert
+ end
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/email/smime/signer.rb b/lib/gitlab/email/smime/signer.rb
new file mode 100644
index 00000000000..2fa83014003
--- /dev/null
+++ b/lib/gitlab/email/smime/signer.rb
@@ -0,0 +1,29 @@
+# frozen_string_literal: true
+
+require 'openssl'
+
+module Gitlab
+ module Email
+ module Smime
+ # Tooling for signing and verifying data with SMIME
+ class Signer
+ include OpenSSL
+
+ def self.sign(cert:, key:, data:)
+ signed_data = PKCS7.sign(cert, key, data, nil, PKCS7::DETACHED)
+ PKCS7.write_smime(signed_data)
+ end
+
+ # return nil if data cannot be verified, otherwise the signed content data
+ def self.verify_signature(cert:, ca_cert: nil, signed_data:)
+ store = X509::Store.new
+ store.set_default_paths
+ store.add_cert(ca_cert) if ca_cert
+
+ signed_smime = PKCS7.read_smime(signed_data)
+ signed_smime if signed_smime.verify([cert], store)
+ end
+ end
+ end
+ end
+end