CHEF-3962: Adds ability to verify ssh keys (#6656)

* ssh_key resource file

Signed-off-by: Vasu1105 <vasundhara.jagdale@progress.com>

* Initial commit for unit test for ssh_key resource

Signed-off-by: Vasu1105 <vasundhara.jagdale@progress.com>

* Fix linting and added resource in resources file

Signed-off-by: Vasu1105 <vasundhara.jagdale@progress.com>

* extend the ssh key resource to use file resource properties

Signed-off-by: Vasu1105 <vasundhara.jagdale@progress.com>

* Updates the ssh_key resource to get length and type of key.

Signed-off-by: Vasu1105 <vasundhara.jagdale@progress.com>

* Updates unit test for ssh_key resource.

Signed-off-by: Vasu1105 <vasundhara.jagdale@progress.com>

* ADDS Docs for ssh_key resource

Signed-off-by: Vasu1105 <vasundhara.jagdale@progress.com>

* Fix Review: Empty file handling

Signed-off-by: Vasu1105 <vasundhara.jagdale@progress.com>

* Fixed review comments and few code refactoring for ssh_key resource

Signed-off-by: Vasu1105 <vasundhara.jagdale@progress.com>

* Doc edits

Signed-off-by: Ian Maddaus <ian.maddaus@progress.com>

* Updates test

Signed-off-by: Vasu1105 <vasundhara.jagdale@progress.com>

---------

Signed-off-by: Vasu1105 <vasundhara.jagdale@progress.com>
Signed-off-by: Ian Maddaus <ian.maddaus@progress.com>
Co-authored-by: Ian Maddaus <ian.maddaus@progress.com>
This commit is contained in:
Vasundhara Jagdale 2024-01-19 16:26:18 +00:00 committed by GitHub
parent 8af5834774
commit b946f5454d
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
5 changed files with 277 additions and 0 deletions

View file

@ -0,0 +1,114 @@
+++
title = "ssh_key resource"
draft = false
gh_repo = "inspec"
platform = "os"
[menu]
[menu.inspec]
title = "ssh_key"
identifier = "inspec/resources/os/ssh_key.md ssh_key resource"
parent = "inspec/resources/os"
+++
Use the `ssh_key` Chef InSpec audit resource to test ssh keys. Supported key types RSA, DSA(Limited support. Not verifies private key) , ECDSA, Ed25519
## Availability
### Install
{{% inspec/inspec_installation %}}
### Version
This resource first became available in v1.18.0 of Chef InSpec.
## Syntax
The `ssh_key` resource tests the properties of an SSH key file. Provide the path to a key file or a key filename. If you pass in a filename, this resource checks for keys on default path `~/.ssh/`.
```rb
describe ssh_key('~/.ssh/id_rsa') do
it { should be_private }
it { should be_public }
its('type') { should cmp /rsa/ }
its('key_length') { should eq 2048 }
its('mode') { should cmp '0400' }
end
```
You can use an optional passphrase with `ssh_key`:
```rb
describe ssh_key('~/.ssh/id_rsa', '<PASSPHRASE>') do
it { should be_private }
end
```
Replace `<PASSPHRASE>` with the private key passphrase.
## Properties
### key_length
The `key_length` property tests the number of bits in the key pair. This only works with RSA keys.
```rb
describe ssh_key('~/.ssh/id_rsa') do
its('key_length') { should eq 4096 }
end
```
### type
The `type` property verifies the key type.
```rb
describe ssh_key('~/.ssh/id_ecdsa') do
its('type') { should cmp /ecdsa/ }
end
```
Allowed values:
- `rsa`
- `ecdsa`
- `ed25519`
- `dsa`
### file properties
The ssh_key resource also tests the same properties that the [file resource](/inspec/resources/file#properties) tests.
For example, you can use the `mode` property to test if the mode assigned to the SSH key matches the specified value.
```rb
describe ssh_key('~/.ssh/id_rsa') do
its('mode') { should cmp '0400' }
end
```
## Matchers
For a full list of available matchers, see the [matchers page](/inspec/matchers/).
### be_public
Use `be_public` to verify that a key is public key:
```rb
describe ssh_key('~/.ssh/id_ed25519.pub') do
it { should be_public }
end
```
### be_private
Use `be_private` to verify that a key is a private key:
```rb
describe ssh_key('~/.ssh/id_ecdsa', '<PASSPHRASE>') do
it { should be_private }
end
```
Replace `<PASSPHRASE>` with the private key passphrase.

View file

@ -110,6 +110,7 @@ require "inspec/resources/selinux"
require "inspec/resources/service"
require "inspec/resources/shadow"
require "inspec/resources/ssh_config"
require "inspec/resources/ssh_key"
require "inspec/resources/ssl"
require "inspec/resources/sys_info"
require "inspec/resources/toml"

View file

@ -0,0 +1,124 @@
require "inspec/utils/file_reader"
require "net/ssh" unless defined?(Net::SSH)
# Change module if required
module Inspec::Resources
class SshKey < FileResource
# Every resource requires an internal name.
name "ssh_key"
# Restrict to only run on the below platforms (if none were given,
# all OS's and cloud API's supported)
supports platform: "unix"
supports platform: "windows"
desc "public/private SSH key pair test"
example <<~EXAMPLE
describe ssh_key('path: ~/.ssh/id_rsa') do
its('key_length') { should eq 4096 }
its('type') { should cmp /rsa/ }
it { should be_private }
end
EXAMPLE
include FileReader
def initialize(keypath, passphrase = nil)
skip_resource "The `ssh_key` resource is not yet available on your OS." unless inspec.os.unix? || inspec.os.windows?
@key_path = set_ssh_key_path(keypath)
@passphrase = passphrase
@key = read_ssh_key
super(@key_path)
end
def public?
return if @key.nil?
@key[:public]
end
def private?
return if @key.nil?
@key[:private]
end
def key_length
return if @key.nil?
@key[:key_length]
end
def type
return if @key.nil?
@key[:type]
end
# Define a resource ID. This is used in reporting engines to uniquely identify the individual resource.
# This might be a file path, or a process ID, or a cloud instance ID. Only meaningful to the implementation.
# Must be a string. Defaults to the empty string if not implemented.
def resource_id
@key_path || "SSH key"
end
def to_s
"ssh_key #{@key_path}"
end
private
def set_ssh_key_path(keypath)
if File.exist?(keypath)
@key_path = keypath
elsif File.exist?(File.join("#{Dir.home}/.ssh/", keypath))
@key_path = File.join("#{Dir.home}/.ssh/", keypath)
else
raise Inspec::Exceptions::ResourceSkipped, "Can't find file: #{keypath}"
end
end
def read_ssh_key
key_data = {}
key = nil
filecontent = read_file_content((@key_path), @passphrase)
raise Inspec::Exceptions::ResourceSkipped, "File is empty: #{@key_path}" if filecontent.split("\n").empty?
if filecontent.split("\n")[0].include?("PRIVATE")
# Net::SSH::KeyFactory does not have support to load private key for DSA
key = Net::SSH::KeyFactory.load_private_key(@key_path, @passphrase, false)
unless key.nil?
key_data[:private] = true
key_data[:public] = false
# The data send for ssh type is not in same format so it's good to match on the string
key_data[:type] = key.ssh_type
key_data[:key_length] = key_lengh(key)
end
else
key = Net::SSH::KeyFactory.load_public_key(@key_path)
unless key.nil?
key_data[:private] = false
key_data[:public] = true
# The data send for ssh type is not in same format so it's good to match on the string
key_data[:type] = key.ssh_type
key_data[:key_length] = key_lengh(key)
end
end
key_data
rescue OpenSSL::PKey::PKeyError => e
raise Inspec::Exceptions::ResourceFailed, "#{e.message}"
end
def key_lengh(key)
if key.class.to_s == "OpenSSL::PKey::RSA"
key.public_key.n.num_bits
else
# Unable to get the key lenght data for other types of keys
# TODO: Need to check if there is any method that will get this info.
nil
end
end
end
end

View file

@ -151,6 +151,7 @@ class MockLoader
"test_certificate.rsa.crt.pem" => mockfile.call("test_certificate.rsa.crt.pem"),
"test_certificate.rsa.key.pem" => mockfile.call("test_certificate.rsa.key.pem"),
"test_ca_public.key.pem" => mockfile.call("test_ca_public.key.pem"),
"test/fixtures/files/test_rsa_key" => mockfile.call("test_rsa_key"),
# Test DH parameters, 2048 bit long safe prime, generator 2 for dh_params in PEM format
"dh_params.dh_pem" => mockfile.call("dh_params.dh_pem"),
"default.toml" => mockfile.call("default.toml"),

View file

@ -0,0 +1,37 @@
require "inspec/globals"
require "#{Inspec.src_root}/test/helper"
require_relative "../../../lib/inspec/resources/ssh_key"
require "mixlib/shellout"
class TestSshKeyResource < Minitest::Test
def setup
# Generate an SSH RSA key for testing
@private_key_path = generate_ssh_key
end
def teardown
# Clean up: remove the generated ssh keys
FileUtils.rm_rf(@private_key_path)
FileUtils.rm_rf("#{@private_key_path}.pub")
end
def test_ssh_key_resoure
@ssh_key = MockLoader.new("ubuntu".to_sym).load_resource("ssh_key", @private_key_path)
assert_match("rsa", @ssh_key.type)
assert_equal(4096, @ssh_key.key_length)
assert_equal(true, @ssh_key.private?)
assert_equal(false, @ssh_key.public?)
end
private
def generate_ssh_key
file_path = "test/fixtures/files/test_rsa_key"
cmd = Mixlib::ShellOut.new("yes | ssh-keygen -t rsa -b 4096 -N '' -f #{file_path}")
cmd.run_command
cmd.error!
sleep 3
file_path
end
end