# frozen_string_literal: true
#
# Copyright (c) 2006-2023 Hal Brodigan (postmodern.mod3 at gmail.com)
#
# Ronin is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# Ronin is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with Ronin. If not, see .
#
require 'ronin/cli/command'
require 'ronin/support/crypto/cert'
require 'ronin/support/text/patterns'
require 'ronin/core/cli/logging'
module Ronin
class CLI
module Commands
#
# Generates a new X509 certificate.
#
# ## Usage
#
# ronin cert-gen [options]
#
# ## Options
#
# --version NUM The certificate version number (Default: 2)
# --serial NUM The certificate serial number (Default: 0)
# --not-before TIME When the certificate becomes valid. Defaults to the current time.
# --not-after TIME When the certificate becomes no longer valid. Defaults to one year from now.
# -c, --common-name DOMAIN The Common Name (CN) for the certificate
# -A, --subject-alt-name HOST|IP Adds HOST or IP to subjectAltName
# -O, --organization NAME The Organization (O) for the certificate
# -U, --organizational-unit NAME The Organizational Unit (OU)
# -L, --locality NAME The locality for the certificate
# -S, --state XX The two-letter State (ST) code for the certificate
# -C, --country XX The two-letter Country (C) code for the certificate
# -t, --key-type rsa|ec The signing key type
# --generate-key PATH Generates and saves a random key (Default: key.pem)
# -k, --key-file FILE Loads the signing key from the FILE
# -H sha256|sha1|md5, The hash algorithm to use for signing (Default: sha256)
# --signing-hash
# --ca-key FILE The Certificate Authority (CA) key
# --ca-cert FILE The Certificate Authority (CA) certificate
# --ca Generates a CA certificate
# -o, --output FILE The output file (Default: cert.crt)
# -h, --help Print help information
#
# ### Examples
#
# ronin cert_gen -c test.com -O "Test Co" -U "Test Dept" -L "Test City" -S NY -C US
# ronin cert_gen -c test.com -O "Test Co" -U "Test Dept" -L "Test City" -S NY -C US --key-file private.key
# ronin cert_gen -c test.com -A www.test.com -O "Test Co" -U "Test Dept" -L "Test City" -S NY -C US
# ronin cert_gen --ca -c "Test CA" -O "Test Co" -U "Test Dept" -L "Test City" -S NY -C US
# ronin cert_gen -c test.com -O "Test Co" -U "Test Dept" -L "Test City" -S NY -C US --ca-key ca.key --ca-cert ca.crt
#
class CertGen < Command
include Core::CLI::Logging
option :version, value: {
type: Integer,
usage: 'NUM',
default: 2
},
desc: 'The certificate version number'
option :serial, value: {
type: Integer,
usage: 'NUM',
default: 0
},
desc: 'The certificate serial number'
option :not_before, value: {
type: String,
usage: 'TIME'
},
desc: 'When the certificate becomes valid. Defaults to the current time.'
option :not_after, value: {
type: String,
usage: 'TIME'
},
desc: 'When the certificate becomes no longer valid. Defaults to one year from now.'
option :common_name, short: '-c',
value: {
type: String,
usage: 'DOMAIN'
},
desc: 'The Common Name (CN) for the certificate'
option :subject_alt_name, short: '-A',
value: {
type: /[a-z0-9:\._-]+/,
usage: 'HOST|IP'
},
desc: 'Adds HOST or IP to subjectAltName' do |value|
@subject_alt_names << value
end
option :organization, short: '-O',
value: {
type: String,
usage: 'NAME'
},
desc: 'The Organization (O) for the certificate'
option :organizational_unit, short: '-U',
value: {
type: String,
usage: 'NAME'
},
desc: 'The Organizational Unit (OU)'
option :locality, short: '-L',
value: {
type: String,
usage: 'NAME'
},
desc: 'The locality for the certificate'
option :state, short: '-S',
value: {
type: String,
usage: 'XX'
},
desc: 'The two-letter State (ST) code for the certificate'
option :country, short: '-C',
value: {
type: String,
usage: 'XX'
},
desc: 'The two-letter Country (C) code for the certificate'
option :key_type, short: '-t',
value: {
type: [:rsa, :ec]
},
desc: 'The signing key type'
option :generate_key, value: {
type: String,
usage: 'PATH',
default: 'key.pem'
},
desc: 'Generates and saves a random key'
option :key_file, short: '-k',
value: {
type: String,
usage: 'FILE'
},
desc: 'Loads the signing key from the FILE'
option :signing_hash, short: '-H',
value: {
type: [:sha256, :sha1, :md5],
default: :sha256
},
desc: 'The hash algorithm to use for signing'
option :ca_key, value: {
type: String,
usage: 'FILE'
},
desc: 'The Certificate Authority (CA) key'
option :ca_cert, value: {
type: String,
usage: 'FILE'
},
desc: 'The Certificate Authority (CA) certificate'
option :ca, desc: 'Generates a CA certificate'
option :output, short: '-o',
value: {
type: String,
usage: 'FILE',
default: 'cert.crt'
},
desc: 'The output file'
examples [
'-c test.com -O "Test Co" -U "Test Dept" -L "Test City" -S NY -C US',
'-c test.com -O "Test Co" -U "Test Dept" -L "Test City" -S NY -C US --key-file private.key',
'-c test.com -A www.test.com -O "Test Co" -U "Test Dept" -L "Test City" -S NY -C US',
'--ca -c "Test CA" -O "Test Co" -U "Test Dept" -L "Test City" -S NY -C US',
'-c test.com -O "Test Co" -U "Test Dept" -L "Test City" -S NY -C US --ca-key ca.key --ca-cert ca.crt'
]
description 'Generates a new X509 certificate'
man_page 'ronin-cert-gen.1'
#
# Initializes the `ronin cert-gen` command.
#
# @param [Hash{Symbol => Object}] kwargs
# Additional keyword arguments.
#
def initialize(**kwargs)
super(**kwargs)
@subject_alt_names = []
end
#
# Runs the `ronin cert-gen` command.
#
def run
if options[:generate_key]
log_info "Generating new #{options.fetch(:key_type,:rsa).upcase} key ..."
end
key = signing_key
cert = Ronin::Support::Crypto::Cert.generate(
version: options[:version],
serial: options[:serial],
not_before: not_before,
not_after: not_after,
key: key,
ca_key: ca_key,
ca_cert: ca_cert,
subject: {
common_name: options[:common_name],
organization: options[:organization],
organizational_unit: options[:organizational_unit],
locality: options[:locality],
state: options[:state],
country: options[:country]
},
extensions: extensions
)
if options[:generate_key]
log_info "Saving key to #{options[:generate_key]} ..."
key.save(options[:generate_key])
end
log_info "Saving certificate to #{options[:output]} ..."
cert.save(options[:output])
end
#
# The parsed `--not-before` time or now.
#
# @return [Time]
#
def not_before
@not_before ||= if options[:not_before]
Time.parse(options[:not_before])
else
Time.now
end
end
#
# The parsed `--not-after` time or one year from now.
#
# @return [Time]
#
def not_after
@not_after ||= if options[:not_after]
Time.parse(options[:not_after])
else
not_before+Support::Crypto::Cert::ONE_YEAR
end
end
#
# The `--key-type` key class.
#
# @return [Class,
# Class, nil]
#
def key_class
case options[:key_type]
when :rsa then Support::Crypto::Key::RSA
when :ec then Support::Crypto::Key::EC
end
end
#
# Loads the `--key-file` key file or generates a new signing key.
#
# @return [Ronin::Support::Key::RSA, Ronin::Support::Key::EC, nil]
#
def signing_key
if options[:key_file]
if options[:key_type]
key_class.load_file(options[:key_file])
else
begin
Support::Crypto::Key.load_file(options[:key_file])
rescue ArgumentError => error
print_error(error.message)
exit(-1)
end
end
else
(key_class || Support::Crypto::Key::RSA).random
end
end
#
# Loads the `--ca-key` key file.
#
# @return [Ronin::Support::Key::RSA, nil]
#
def ca_key
if options[:ca_key]
Support::Crypto::Key::RSA.load_file(options[:ca_key])
end
end
#
# Loads the `--ca-cert` certificate file.
#
# @return [Ronin::Support::Crypto::Cert, nil]
#
def ca_cert
if options[:ca_cert]
Support::Crypto::Cert.load_file(options[:ca_cert])
end
end
#
# Builds the extensions.
#
# @return [Hash{String => Object}, nil]
#
def extensions
exts = {}
if (ext = basic_constraints_ext)
exts['basicConstraints'] = ext
end
if (ext = subject_alt_name_ext)
exts['subjectAltName'] = ext
end
exts unless exts.empty?
end
#
# Builds the `basicConstraints` extension.
#
# @return [(String, Boolean), nil]
#
def basic_constraints_ext
if options[:ca]
['CA:TRUE', true]
elsif options[:ca_key] || options[:ca_cert]
['CA:FALSE', true]
end
end
IP_REGEXP = Support::Text::Patterns::IP
#
# Builds the `subjectAltName` extension.
#
# @return [String, nil]
#
def subject_alt_name_ext
if !@subject_alt_names.empty?
@subject_alt_names.map { |name|
if name =~ IP_REGEXP
"IP: #{name}"
else
"DNS: #{name}"
end
}.join(', ')
end
end
end
end
end
end