summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorNick Thomas <nick@gitlab.com>2017-08-21 11:30:03 +0100
committerNick Thomas <nick@gitlab.com>2017-08-30 20:50:44 +0100
commitb0f982fbdf69c292ab4530c0aaaf1ab42f4e7a01 (patch)
tree0d76c74fb6260de1e3c9694a8501491b2eb486ef
parent81f08d30e641dc1a6666022ab1f5d36dbcdced7e (diff)
downloadgitlab-ce-b0f982fbdf69c292ab4530c0aaaf1ab42f4e7a01.tar.gz
Add settings for minimum key strength and allowed key type
This is an amalgamation of: * Cory Hinshaw: Initial implementation !5552 * Rémy Coutable: Updates !9350 * Nick Thomas: Resolve conflicts and add ED25519 support !13712
-rw-r--r--app/controllers/admin/application_settings_controller.rb3
-rw-r--r--app/helpers/application_settings_helper.rb18
-rw-r--r--app/models/application_setting.rb33
-rw-r--r--app/models/key.rb45
-rw-r--r--app/views/admin/application_settings/_form.html.haml49
-rw-r--r--changelogs/unreleased/17849-allow-admin-to-restrict-min-key-length-and-techno.yml5
-rw-r--r--db/migrate/20161020180657_add_minimum_key_length_to_application_settings.rb24
-rw-r--r--db/schema.rb5
-rw-r--r--doc/api/settings.md19
-rw-r--r--doc/security/README.md1
-rw-r--r--doc/security/img/ssh_keys_restrictions_settings.pngbin0 -> 41803 bytes
-rw-r--r--doc/security/ssh_keys_restrictions.md18
-rw-r--r--lib/api/entities.rb1
-rw-r--r--lib/api/settings.rb6
-rw-r--r--lib/gitlab/git_access.rb9
-rw-r--r--lib/gitlab/key_fingerprint.rb48
-rw-r--r--lib/gitlab/ssh_public_key.rb84
-rw-r--r--spec/factories/keys.rb49
-rw-r--r--spec/features/admin/admin_settings_spec.rb20
-rw-r--r--spec/features/profiles/keys_spec.rb16
-rw-r--r--spec/lib/gitlab/git_access_spec.rb42
-rw-r--r--spec/lib/gitlab/key_fingerprint_spec.rb82
-rw-r--r--spec/lib/gitlab/ssh_public_key_spec.rb132
-rw-r--r--spec/models/application_setting_spec.rb33
-rw-r--r--spec/models/key_spec.rb85
-rw-r--r--spec/requests/api/settings_spec.rb17
26 files changed, 703 insertions, 141 deletions
diff --git a/app/controllers/admin/application_settings_controller.rb b/app/controllers/admin/application_settings_controller.rb
index 8367c22d1ca..8cc7111033f 100644
--- a/app/controllers/admin/application_settings_controller.rb
+++ b/app/controllers/admin/application_settings_controller.rb
@@ -66,6 +66,8 @@ class Admin::ApplicationSettingsController < Admin::ApplicationController
end
end
+ params[:application_setting][:allowed_key_types]&.delete('')
+
enabled_oauth_sign_in_sources = params[:application_setting].delete(:enabled_oauth_sign_in_sources)
params[:application_setting][:disabled_oauth_sign_in_sources] =
@@ -83,6 +85,7 @@ class Admin::ApplicationSettingsController < Admin::ApplicationController
def visible_application_setting_attributes
ApplicationSettingsHelper.visible_attributes + [
:domain_blacklist_file,
+ allowed_key_types: [],
disabled_oauth_sign_in_sources: [],
import_sources: [],
repository_storages: [],
diff --git a/app/helpers/application_settings_helper.rb b/app/helpers/application_settings_helper.rb
index 3b76da238e0..75d090359d0 100644
--- a/app/helpers/application_settings_helper.rb
+++ b/app/helpers/application_settings_helper.rb
@@ -81,6 +81,20 @@ module ApplicationSettingsHelper
end
end
+ def allowed_key_types_checkboxes(help_block_id)
+ Gitlab::SSHPublicKey.technology_names.map do |type|
+ checked = current_application_settings.allowed_key_types.include?(type)
+ checkbox_id = "allowed_key_types-#{type}"
+
+ label_tag(checkbox_id, class: checked ? 'active' : nil) do
+ check_box_tag('application_setting[allowed_key_types][]', type, checked,
+ autocomplete: 'off',
+ 'aria-describedby' => help_block_id,
+ id: checkbox_id) + type.upcase
+ end
+ end
+ end
+
def repository_storages_options_for_select
options = Gitlab.config.repositories.storages.map do |name, storage|
["#{name} - #{storage['path']}", name]
@@ -141,6 +155,10 @@ module ApplicationSettingsHelper
:metrics_port,
:metrics_sample_interval,
:metrics_timeout,
+ :minimum_dsa_bits,
+ :minimum_ecdsa_bits,
+ :minimum_ed25519_bits,
+ :minimum_rsa_bits,
:password_authentication_enabled,
:performance_bar_allowed_group_id,
:performance_bar_enabled,
diff --git a/app/models/application_setting.rb b/app/models/application_setting.rb
index 8e446ff6dd8..988ee4802b9 100644
--- a/app/models/application_setting.rb
+++ b/app/models/application_setting.rb
@@ -20,6 +20,7 @@ class ApplicationSetting < ActiveRecord::Base
serialize :domain_blacklist, Array # rubocop:disable Cop/ActiveRecordSerialize
serialize :repository_storages # rubocop:disable Cop/ActiveRecordSerialize
serialize :sidekiq_throttling_queues, Array # rubocop:disable Cop/ActiveRecordSerialize
+ serialize :allowed_key_types, Array # rubocop:disable Cop/ActiveRecordSerialize
cache_markdown_field :sign_in_text
cache_markdown_field :help_page_text
@@ -146,6 +147,24 @@ class ApplicationSetting < ActiveRecord::Base
presence: true,
numericality: { greater_than_or_equal_to: 0 }
+ validates :allowed_key_types, presence: true
+
+ validates :minimum_rsa_bits,
+ presence: true,
+ inclusion: { in: Gitlab::SSHPublicKey.allowed_sizes('rsa') }
+
+ validates :minimum_dsa_bits,
+ presence: true,
+ inclusion: { in: Gitlab::SSHPublicKey.allowed_sizes('dsa') }
+
+ validates :minimum_ecdsa_bits,
+ presence: true,
+ inclusion: { in: Gitlab::SSHPublicKey.allowed_sizes('ecdsa') }
+
+ validates :minimum_ed25519_bits,
+ presence: true,
+ inclusion: { in: Gitlab::SSHPublicKey.allowed_sizes('ed25519') }
+
validates_each :restricted_visibility_levels do |record, attr, value|
value&.each do |level|
unless Gitlab::VisibilityLevel.options.value?(level)
@@ -170,7 +189,16 @@ class ApplicationSetting < ActiveRecord::Base
end
end
+ validates_each :allowed_key_types do |record, attr, value|
+ value&.each do |type|
+ unless Gitlab::SSHPublicKey.allowed_type?(type)
+ record.errors.add(attr, "'#{type}' is not a valid SSH key type")
+ end
+ end
+ end
+
before_validation :ensure_uuid!
+
before_save :ensure_runners_registration_token
before_save :ensure_health_check_access_token
@@ -212,6 +240,7 @@ class ApplicationSetting < ActiveRecord::Base
{
after_sign_up_text: nil,
akismet_enabled: false,
+ allowed_key_types: Gitlab::SSHPublicKey.technology_names,
container_registry_token_expire_delay: 5,
default_artifacts_expire_in: '30 days',
default_branch_protection: Settings.gitlab['default_branch_protection'],
@@ -239,6 +268,10 @@ class ApplicationSetting < ActiveRecord::Base
max_attachment_size: Settings.gitlab['max_attachment_size'],
password_authentication_enabled: Settings.gitlab['password_authentication_enabled'],
performance_bar_allowed_group_id: nil,
+ minimum_rsa_bits: 1024,
+ minimum_dsa_bits: 1024,
+ minimum_ecdsa_bits: 256,
+ minimum_ed25519_bits: 256,
plantuml_enabled: false,
plantuml_url: nil,
project_export_enabled: true,
diff --git a/app/models/key.rb b/app/models/key.rb
index 49bc26122fa..27c91679ec9 100644
--- a/app/models/key.rb
+++ b/app/models/key.rb
@@ -1,6 +1,8 @@
require 'digest/md5'
class Key < ActiveRecord::Base
+ include AfterCommitQueue
+ include Gitlab::CurrentSettings
include Sortable
LAST_USED_AT_REFRESH_TIME = 1.day.to_i
@@ -12,14 +14,18 @@ class Key < ActiveRecord::Base
validates :title,
presence: true,
length: { maximum: 255 }
+
validates :key,
presence: true,
length: { maximum: 5000 },
format: { with: /\A(ssh|ecdsa)-.*\Z/ }
+
validates :fingerprint,
uniqueness: true,
presence: { message: 'cannot be generated' }
+ validate :key_meets_minimum_bit_length, :key_type_is_allowed
+
delegate :name, :email, to: :user, prefix: true
after_commit :add_to_shell, on: :create
@@ -80,6 +86,10 @@ class Key < ActiveRecord::Base
SystemHooksService.new.execute_hooks_for(self, :destroy)
end
+ def public_key
+ @public_key ||= Gitlab::SSHPublicKey.new(key)
+ end
+
private
def generate_fingerprint
@@ -87,7 +97,40 @@ class Key < ActiveRecord::Base
return unless self.key.present?
- self.fingerprint = Gitlab::KeyFingerprint.new(self.key).fingerprint
+ self.fingerprint = public_key.fingerprint
+ end
+
+ def key_meets_minimum_bit_length
+ case public_key.type
+ when :rsa
+ if public_key.bits < current_application_settings.minimum_rsa_bits
+ errors.add(:key, "length must be at least #{current_application_settings.minimum_rsa_bits} bits")
+ end
+ when :dsa
+ if public_key.bits < current_application_settings.minimum_dsa_bits
+ errors.add(:key, "length must be at least #{current_application_settings.minimum_dsa_bits} bits")
+ end
+ when :ecdsa
+ if public_key.bits < current_application_settings.minimum_ecdsa_bits
+ errors.add(:key, "elliptic curve size must be at least #{current_application_settings.minimum_ecdsa_bits} bits")
+ end
+ when :ed25519
+ if public_key.bits < current_application_settings.minimum_ed25519_bits
+ errors.add(:key, "length must be at least #{current_application_settings.minimum_ed25519_bits} bits")
+ end
+ end
+ end
+
+ def key_type_is_allowed
+ unless current_application_settings.allowed_key_types.include?(public_key.type.to_s)
+ allowed_types =
+ current_application_settings
+ .allowed_key_types
+ .map(&:upcase)
+ .to_sentence(last_word_connector: ', or ', two_words_connector: ' or ')
+
+ errors.add(:key, "type is not allowed. Must be #{allowed_types}")
+ end
end
def notify_user
diff --git a/app/views/admin/application_settings/_form.html.haml b/app/views/admin/application_settings/_form.html.haml
index 959af5c0d13..1cda98ffea8 100644
--- a/app/views/admin/application_settings/_form.html.haml
+++ b/app/views/admin/application_settings/_form.html.haml
@@ -42,18 +42,57 @@
= link_to "(?)", help_page_path("integration/bitbucket")
and GitLab.com
= link_to "(?)", help_page_path("integration/gitlab")
+
+ .form-group
+ .col-sm-offset-2.col-sm-10
+ .checkbox
+ = f.label :project_export_enabled do
+ = f.check_box :project_export_enabled
+ Project export enabled
+
.form-group
%label.control-label.col-sm-2 Enabled Git access protocols
.col-sm-10
= select(:application_setting, :enabled_git_access_protocol, [['Both SSH and HTTP(S)', nil], ['Only SSH', 'ssh'], ['Only HTTP(S)', 'http']], {}, class: 'form-control')
%span.help-block#clone-protocol-help
Allow only the selected protocols to be used for Git access.
+
.form-group
- .col-sm-offset-2.col-sm-10
- .checkbox
- = f.label :project_export_enabled do
- = f.check_box :project_export_enabled
- Project export enabled
+ = f.label :allowed_key_types, 'Allowed SSH keys', class: 'control-label col-sm-2'
+ .col-sm-10
+ = hidden_field_tag 'application_setting[allowed_key_types][]', nil, id: 'allowed_key_types-none'
+ - allowed_key_types_checkboxes('allowed-key-types-help').each do |key_type_checkbox|
+ .checkbox= key_type_checkbox
+ %span.help-block#allowed-key-types-help
+ Only SSH keys with allowed algorithms can be uploaded.
+
+ .form-group
+ = f.label :minimum_rsa_bits, 'Minimum RSA key length', class: 'control-label col-sm-2'
+ .col-sm-10
+ = f.select :minimum_rsa_bits, Gitlab::SSHPublicKey.allowed_sizes('rsa'), {}, class: 'form-control'
+ .help-block
+ The minimum length for user RSA SSH keys (in bits)
+
+ .form-group
+ = f.label :minimum_dsa_bits, 'Minimum DSA key length', class: 'control-label col-sm-2'
+ .col-sm-10
+ = f.select :minimum_dsa_bits, Gitlab::SSHPublicKey.allowed_sizes('dsa'), {}, class: 'form-control'
+ .help-block
+ The minimum length for user DSA SSH keys (in bits)
+
+ .form-group
+ = f.label :minimum_ecdsa_bits, 'Minimum ECDSA key length', class: 'control-label col-sm-2'
+ .col-sm-10
+ = f.select :minimum_ecdsa_bits, Gitlab::SSHPublicKey.allowed_sizes('ecdsa'), {}, class: 'form-control'
+ .help-block
+ The minimum elliptic curve size for user ECDSA SSH keys (in bits)
+
+ .form-group
+ = f.label :minimum_ed25519_bits, 'Minimum ED25519 key length', class: 'control-label col-sm-2'
+ .col-sm-10
+ = f.select :minimum_ed25519_bits, Gitlab::SSHPublicKey.allowed_sizes('ed25519'), {}, class: 'form-control'
+ .help-block
+ The minimum length for user ED25519 SSH keys (in bits)
%fieldset
%legend Account and Limit Settings
diff --git a/changelogs/unreleased/17849-allow-admin-to-restrict-min-key-length-and-techno.yml b/changelogs/unreleased/17849-allow-admin-to-restrict-min-key-length-and-techno.yml
new file mode 100644
index 00000000000..2e0f509b17c
--- /dev/null
+++ b/changelogs/unreleased/17849-allow-admin-to-restrict-min-key-length-and-techno.yml
@@ -0,0 +1,5 @@
+---
+title: Add settings for minimum key strength and allowed key type
+merge_request: 13712
+author: Cory Hinshaw
+type: added
diff --git a/db/migrate/20161020180657_add_minimum_key_length_to_application_settings.rb b/db/migrate/20161020180657_add_minimum_key_length_to_application_settings.rb
new file mode 100644
index 00000000000..ce87d8a26b6
--- /dev/null
+++ b/db/migrate/20161020180657_add_minimum_key_length_to_application_settings.rb
@@ -0,0 +1,24 @@
+class AddMinimumKeyLengthToApplicationSettings < ActiveRecord::Migration
+ include Gitlab::Database::MigrationHelpers
+
+ # Set this constant to true if this migration requires downtime.
+ DOWNTIME = false
+
+ disable_ddl_transaction!
+
+ def up
+ add_column_with_default :application_settings, :minimum_rsa_bits, :integer, default: 1024
+ add_column_with_default :application_settings, :minimum_dsa_bits, :integer, default: 1024
+ add_column_with_default :application_settings, :minimum_ecdsa_bits, :integer, default: 256
+ add_column_with_default :application_settings, :minimum_ed25519_bits, :integer, default: 256
+ add_column_with_default :application_settings, :allowed_key_types, :string, default: %w[rsa dsa ecdsa ed25519].to_yaml
+ end
+
+ def down
+ remove_column :application_settings, :minimum_rsa_bits
+ remove_column :application_settings, :minimum_dsa_bits
+ remove_column :application_settings, :minimum_ecdsa_bits
+ remove_column :application_settings, :minimum_ed25519_bits
+ remove_column :application_settings, :allowed_key_types
+ end
+end
diff --git a/db/schema.rb b/db/schema.rb
index 0f4b0c0c3b3..49ae4b48627 100644
--- a/db/schema.rb
+++ b/db/schema.rb
@@ -129,6 +129,11 @@ ActiveRecord::Schema.define(version: 20170824162758) do
t.boolean "password_authentication_enabled"
t.boolean "project_export_enabled", default: true, null: false
t.boolean "hashed_storage_enabled", default: false, null: false
+ t.integer "minimum_rsa_bits", default: 1024, null: false
+ t.integer "minimum_dsa_bits", default: 1024, null: false
+ t.integer "minimum_ecdsa_bits", default: 256, null: false
+ t.integer "minimum_ed25519_bits", default: 256, null: false
+ t.string "allowed_key_types", default: "---\n- rsa\n- dsa\n- ecdsa\n- ed25519\n", null: false
end
create_table "audit_events", force: :cascade do |t|
diff --git a/doc/api/settings.md b/doc/api/settings.md
index 94a9f8265fb..a43e13e6217 100644
--- a/doc/api/settings.md
+++ b/doc/api/settings.md
@@ -48,7 +48,12 @@ Example response:
"plantuml_enabled": false,
"plantuml_url": null,
"terminal_max_session_time": 0,
- "polling_interval_multiplier": 1.0
+ "polling_interval_multiplier": 1.0,
+ "minimum_rsa_bits": 1024,
+ "minimum_dsa_bits": 1024,
+ "minimum_ecdsa_bits": 256,
+ "minimum_ed25519_bits": 256,
+ "allowed_key_types": ["rsa", "dsa", "ecdsa", "ed25519"]
}
```
@@ -88,6 +93,11 @@ PUT /application/settings
| `plantuml_url` | string | yes (if `plantuml_enabled` is `true`) | The PlantUML instance URL for integration. |
| `terminal_max_session_time` | integer | no | Maximum time for web terminal websocket connection (in seconds). Set to 0 for unlimited time. |
| `polling_interval_multiplier` | decimal | no | Interval multiplier used by endpoints that perform polling. Set to 0 to disable polling. |
+| `minimum_rsa_bits` | integer | no | The minimum allowed bit length of an uploaded RSA key. Default is `1024`.
+| `minimum_dsa_bits` | integer | no | The minimum allowed bit length of an uploaded DSA key. Default is `1024`.
+| `minimum_ecdsa_bits` | integer | no | The minimum allowed curve size (in bits) of an uploaded ECDSA key. Default is `256`.
+| `minimum_ed25519_bits` | integer | no | The minimum allowed curve size (in bits) of an uploaded ED25519 key. Default is `256`.
+| `allowed_key_types` | array of strings | no | Array of SSH key types accepted by the application. Allowed values are: `rsa`, `dsa`, `ecdsa`, and `ed25519`. Default is `["rsa", "dsa", "ecdsa", "ed25519"]`.
```bash
curl --request PUT --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v4/application/settings?signup_enabled=false&default_project_visibility=internal
@@ -125,6 +135,11 @@ Example response:
"plantuml_enabled": false,
"plantuml_url": null,
"terminal_max_session_time": 0,
- "polling_interval_multiplier": 1.0
+ "polling_interval_multiplier": 1.0,
+ "minimum_rsa_bits": 1024,
+ "minimum_dsa_bits": 1024,
+ "minimum_ecdsa_bits": 256,
+ "minimum_ed25519_bits": 256,
+ "allowed_key_types": ["rsa", "dsa", "ecdsa", "ed25519"]
}
```
diff --git a/doc/security/README.md b/doc/security/README.md
index 38706e48ec5..1f54948d113 100644
--- a/doc/security/README.md
+++ b/doc/security/README.md
@@ -1,6 +1,7 @@
# Security
- [Password length limits](password_length_limits.md)
+- [Restrict allowed SSH key technologies and minimum length](ssh_keys_restrictions.md)
- [Rack attack](rack_attack.md)
- [Webhooks and insecure internal web services](webhooks.md)
- [Information exclusivity](information_exclusivity.md)
diff --git a/doc/security/img/ssh_keys_restrictions_settings.png b/doc/security/img/ssh_keys_restrictions_settings.png
new file mode 100644
index 00000000000..b62bfc2f7e0
--- /dev/null
+++ b/doc/security/img/ssh_keys_restrictions_settings.png
Binary files differ
diff --git a/doc/security/ssh_keys_restrictions.md b/doc/security/ssh_keys_restrictions.md
new file mode 100644
index 00000000000..32ca7dacab3
--- /dev/null
+++ b/doc/security/ssh_keys_restrictions.md
@@ -0,0 +1,18 @@
+# Restrict allowed SSH key technologies and minimum length
+
+`ssh-keygen` allows users to create RSA keys with as few as 768 bits, which
+falls well below recommendations from certain standards groups (such as the US
+NIST). Some organizations deploying Gitlab will need to enforce minimum key
+strength, either to satisfy internal security policy or for regulatory
+compliance.
+
+Similarly, certain standards groups recommend using RSA or ECDSA over the older
+DSA and administrators may need to limit the allowed SSH key algorithms.
+
+GitLab allows you to restrict the allowed SSH key technology as well as specify
+the minimum key length for each technology.
+
+In the Admin area under **Settings** (`/admin/application_settings`), look for
+the "Visibility and Access Controls" area:
+
+![SSH keys restriction admin settings](img/ssh_keys_restrictions_settings.png)
diff --git a/lib/api/entities.rb b/lib/api/entities.rb
index 803b48dd88a..8f766ba4f8d 100644
--- a/lib/api/entities.rb
+++ b/lib/api/entities.rb
@@ -744,6 +744,7 @@ module API
expose(:default_snippet_visibility) { |setting, _options| Gitlab::VisibilityLevel.string_level(setting.default_snippet_visibility) }
expose(:default_group_visibility) { |setting, _options| Gitlab::VisibilityLevel.string_level(setting.default_group_visibility) }
expose :password_authentication_enabled, as: :signin_enabled
+ expose :allowed_key_types
end
class Release < Grape::Entity
diff --git a/lib/api/settings.rb b/lib/api/settings.rb
index 667ba468ce6..6ace0e1e390 100644
--- a/lib/api/settings.rb
+++ b/lib/api/settings.rb
@@ -122,6 +122,12 @@ module API
optional :terminal_max_session_time, type: Integer, desc: 'Maximum time for web terminal websocket connection (in seconds). Set to 0 for unlimited time.'
optional :polling_interval_multiplier, type: BigDecimal, desc: 'Interval multiplier used by endpoints that perform polling. Set to 0 to disable polling.'
+ optional :minimum_rsa_bits, type: Integer, values: Gitlab::SSHPublicKey.allowed_sizes('rsa'), desc: 'The minimum allowed bit length of an uploaded RSA key.'
+ optional :minimum_dsa_bits, type: Integer, values: Gitlab::SSHPublicKey.allowed_sizes('dsa'), desc: 'The minimum allowed bit length of an uploaded DSA key.'
+ optional :minimum_ecdsa_bits, type: Integer, values: Gitlab::SSHPublicKey.allowed_sizes('ecdsa'), desc: 'The minimum allowed curve size (in bits) of an uploaded ECDSA key.'
+ optional :minimum_ed25519_bits, type: Integer, values: Gitlab::SSHPublicKey.allowed_sizes('ed25519'), desc: 'The minimum allowed curve size (in bits) of an uploaded ED25519 key.'
+ optional :allowed_key_types, type: Array[String], values: Gitlab::SSHPublicKey.technology_names, desc: 'The SSH key types accepted by the application (`rsa`, `dsa`, `ecdsa` or `ed25519`).'
+
optional(*::ApplicationSettingsHelper.visible_attributes)
at_least_one_of(*::ApplicationSettingsHelper.visible_attributes)
end
diff --git a/lib/gitlab/git_access.rb b/lib/gitlab/git_access.rb
index 3e8b83c0f90..9eab26d111e 100644
--- a/lib/gitlab/git_access.rb
+++ b/lib/gitlab/git_access.rb
@@ -34,6 +34,7 @@ module Gitlab
end
def check(cmd, changes)
+ check_valid_actor!
check_protocol!
check_active_user!
check_project_accessibility!
@@ -70,6 +71,14 @@ module Gitlab
private
+ def check_valid_actor!
+ return unless actor.is_a?(Key)
+
+ unless actor.valid?
+ raise UnauthorizedError, "Your SSH key #{actor.errors[:key].first}."
+ end
+ end
+
def check_protocol!
unless protocol_allowed?
raise UnauthorizedError, "Git access over #{protocol.upcase} is not allowed"
diff --git a/lib/gitlab/key_fingerprint.rb b/lib/gitlab/key_fingerprint.rb
deleted file mode 100644
index d9a79f7c291..00000000000
--- a/lib/gitlab/key_fingerprint.rb
+++ /dev/null
@@ -1,48 +0,0 @@
-module Gitlab
- class KeyFingerprint
- attr_reader :key, :ssh_key
-
- # Unqualified MD5 fingerprint for compatibility
- delegate :fingerprint, to: :ssh_key, allow_nil: true
-
- def initialize(key)
- @key = key
-
- @ssh_key =
- begin
- Net::SSH::KeyFactory.load_data_public_key(key)
- rescue Net::SSH::Exception, NotImplementedError
- end
- end
-
- def valid?
- ssh_key.present?
- end
-
- def type
- return unless valid?
-
- parts = ssh_key.ssh_type.split('-')
- parts.shift if parts[0] == 'ssh'
-
- parts[0].upcase
- end
-
- def bits
- return unless valid?
-
- case type
- when 'RSA'
- ssh_key.n.num_bits
- when 'DSS', 'DSA'
- ssh_key.p.num_bits
- when 'ECDSA'
- ssh_key.group.order.num_bits
- when 'ED25519'
- 256
- else
- raise "Unsupported key type: #{type}"
- end
- end
- end
-end
diff --git a/lib/gitlab/ssh_public_key.rb b/lib/gitlab/ssh_public_key.rb
new file mode 100644
index 00000000000..2df31bcc246
--- /dev/null
+++ b/lib/gitlab/ssh_public_key.rb
@@ -0,0 +1,84 @@
+module Gitlab
+ class SSHPublicKey
+ TYPES = %w[rsa dsa ecdsa ed25519].freeze
+
+ Technology = Struct.new(:name, :allowed_sizes)
+
+ Technologies = [
+ Technology.new('rsa', [1024, 2048, 3072, 4096]),
+ Technology.new('dsa', [1024, 2048, 3072]),
+ Technology.new('ecdsa', [256, 384, 521]),
+ Technology.new('ed25519', [256])
+ ].freeze
+
+ def self.technology_names
+ Technologies.map(&:name)
+ end
+
+ def self.technology(name)
+ Technologies.find { |ssh_key_technology| ssh_key_technology.name == name }
+ end
+ private_class_method :technology
+
+ def self.allowed_sizes(name)
+ technology(name).allowed_sizes
+ end
+
+ def self.allowed_type?(type)
+ technology_names.include?(type.to_s)
+ end
+
+ attr_reader :key_text, :key
+
+ # Unqualified MD5 fingerprint for compatibility
+ delegate :fingerprint, to: :key, allow_nil: true
+
+ def initialize(key_text)
+ @key_text = key_text
+
+ @key =
+ begin
+ Net::SSH::KeyFactory.load_data_public_key(key_text)
+ rescue StandardError, NotImplementedError
+ end
+ end
+
+ def valid?
+ key.present?
+ end
+
+ def type
+ return unless valid?
+
+ case key
+ when OpenSSL::PKey::EC
+ :ecdsa
+ when OpenSSL::PKey::RSA
+ :rsa
+ when OpenSSL::PKey::DSA
+ :dsa
+ when Net::SSH::Authentication::ED25519::PubKey
+ :ed25519
+ else
+ raise "Unsupported key type: #{key.class}"
+ end
+ end
+
+ def bits
+ return unless valid?
+
+ case type
+ when :rsa
+ key.n.num_bits
+ when :dsa
+ key.p.num_bits
+ when :ecdsa
+ key.group.order.num_bits
+ when :ed25519
+ 256
+ else
+ raise "Unsupported key type: #{type}"
+ end
+ end
+ end
+end
diff --git a/spec/factories/keys.rb b/spec/factories/keys.rb
index a13b6e3596e..3f7c794b14a 100644
--- a/spec/factories/keys.rb
+++ b/spec/factories/keys.rb
@@ -18,5 +18,54 @@ FactoryGirl.define do
factory :write_access_key, class: 'DeployKey' do
can_push true
end
+
+ factory :rsa_key_2048 do
+ key do
+ 'ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQDFf6RYK3qu/RKF/3ndJmL5xgMLp3O9' \
+ '6x8lTay+QGZ0+9FnnAXMdUqBq/ZU6d/gyMB4IaW3nHzM1w049++yAB6UPCzMB8Uo27K5' \
+ '/jyZCtj7Vm9PFNjF/8am1kp46c/SeYicQgQaSBdzIW3UDEa1Ef68qroOlvpi9PYZ/tA7' \
+ 'M0YP0K5PXX+E36zaIRnJVMPT3f2k+GnrxtjafZrwFdpOP/Fol5BQLBgcsyiU+LM1SuaC' \
+ 'rzd8c9vyaTA1CxrkxaZh+buAi0PmdDtaDrHd42gqZkXCKavyvgM5o2CkQ5LJHCgzpXy0' \
+ '5qNFzmThBSkb+XtoxbyagBiGbVZtSVow6Xa7qewz= dummy@gitlab.com'
+ end
+
+ factory :rsa_deploy_key_2048, class: 'DeployKey'
+ end
+
+ factory :dsa_key_2048 do
+ key do
+ 'ssh-dss AAAAB3NzaC1kc3MAAAEBAO/3/NPLA/zSFkMOCaTtGo+uos1flfQ5f038Uk+G' \
+ 'Y9AeLGzX+Srhw59GdVXmOQLYBrOt5HdGwqYcmLnE2VurUGmhtfeO5H+3p5pGJbkS0Gxp' \
+ 'YH1HRO9lWsncF3Hh1w4lYsDjkclDiSTdfTuN8F4Kb3DXNnVSCieeonp+B25F/CXagyTQ' \
+ '/pvNmHFeYgGCVdnBtFdi+xfxaZ8NKdPrGggzokbKHElDZQ4Xo5EpdcyLajgM7nB2r2Rz' \
+ 'OrmeaevKi5lV68ehRa9Yyrb7vxvwiwBwOgqR/mnN7Gnaq1jUdmJY+ct04Qwx37f5jvhv' \
+ '5gA4U40SGMoiHM8RFIN7Ksz0jsyX73MAAAAVALRWOfjfzHpK7KLz4iqDvvTUAevJAAAB' \
+ 'AEa9NZ+6y9iQ5erGsdfLTXFrhSefTG0NhghoO/5IFkSGfd8V7kzTvCHaFrcfpEA5kP8t' \
+ 'poeOG0TASB6tgGOxm1Bq4Wncry5RORBPJlAVpDGRcvZ931ddH7IgltEInS6za2uH6F/1' \
+ 'M1QfKePSLr6xJ1ZLYfP0Og5KTp1x6yMQvfwV0a+XdA+EPgaJWLWp/pWwKWa0oLUgjsIH' \
+ 'MYzuOGh5c708uZrmkzqvgtW2NgXhcIroRgynT3IfI2lP2rqqb3uuuE/qH5UCUFO+Dc3H' \
+ 'nAFNeQDT/M25AERdPYBAY5a+iPjIgO+jT7BfmfByT+AZTqZySrCyc7nNZL3YgGLK0l6A' \
+ '1GgAAAEBAN9FpFOdIXE+YEZhKl1vPmbcn+b1y5zOl6N4x1B7Q8pD/pLMziWROIS8uLzb' \
+ 'aZ0sMIWezHIkxuo1iROMeT+jtCubn7ragaN6AX7nMpxYUH9+mYZZs/fyElt6wCviVhTI' \
+ 'zM+u7VdQsnZttOOlQfogHdL+SpeAft0DsfJjlcgQnsLlHQKv6aPqCPYUST2nE7RyW/Ex' \
+ 'PrMxLtOWt0/j8RYHbwwqvyeZqBz3ESBgrS9c5tBdBfauwYUV/E7gPLOU3OZFw9ue7o+z' \
+ 'wzoTZqW6Xouy5wtWvSLQSLT5XwOslmQz8QMBxD0AQyDfEFGsBCWzmbTgKv9uqrBjubsS' \
+ 'Taja+Cf9kMo== dummy@gitlab.com'
+ end
+ end
+
+ factory :ecdsa_key_256 do
+ key do
+ 'ecdsa-sha2-nistp256 AAAAE2VjZHNhLXNoYTItbmlzdHAyNTYAAAAIbmlzdHAyNTYA' \
+ 'AABBBJZmkzTgY0fiCQ+DVReyH/fFwTFz0XoR3RUO0u+199H19KFw7mNPxRSMOVS7tEtO' \
+ 'Nj3Q7FcZXfqthHvgAzDiHsc= dummy@gitlab.com'
+ end
+ end
+
+ factory :ed25519_key_256 do
+ key do
+ 'ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIETnVTgzqC1gatgSlC4zH6aYt2CAQzgJOhDRvf59ohL6 dummy@gitlab.com'
+ end
+ end
end
end
diff --git a/spec/features/admin/admin_settings_spec.rb b/spec/features/admin/admin_settings_spec.rb
index dbb0ae9c86e..9698dead67a 100644
--- a/spec/features/admin/admin_settings_spec.rb
+++ b/spec/features/admin/admin_settings_spec.rb
@@ -79,6 +79,26 @@ feature 'Admin updates settings' do
end
end
+ scenario 'Change Keys settings' do
+ uncheck 'RSA'
+ uncheck 'DSA'
+ uncheck 'ED25519'
+ select '384', from: 'Minimum ECDSA key length'
+ click_on 'Save'
+
+ expect(page).to have_content 'Application settings saved successfully'
+
+ expect(find_field('RSA', checked: false)).not_to be_checked
+ expect(find_field('DSA', checked: false)).not_to be_checked
+ expect(find_field('ED25519', checked: false)).not_to be_checked
+ expect(find_field('Minimum ECDSA key length').value).to eq '384'
+
+ uncheck 'ECDSA'
+ click_on 'Save'
+
+ expect(page).to have_content "Allowed key types can't be blank"
+ end
+
def check_all_events
page.check('Active')
page.check('Push')
diff --git a/spec/features/profiles/keys_spec.rb b/spec/features/profiles/keys_spec.rb
index 6541ea6bf57..a4ccf222b50 100644
--- a/spec/features/profiles/keys_spec.rb
+++ b/spec/features/profiles/keys_spec.rb
@@ -28,6 +28,22 @@ feature 'Profile > SSH Keys' do
expect(page).to have_content("Title: #{attrs[:title]}")
expect(page).to have_content(attrs[:key])
end
+
+ context 'when only DSA and ECDSA keys are allowed' do
+ before do
+ stub_application_setting(allowed_key_types: %w[dsa ecdsa])
+ end
+
+ scenario 'shows a validation error' do
+ attrs = attributes_for(:key)
+
+ fill_in('Key', with: attrs[:key])
+ fill_in('Title', with: attrs[:title])
+ click_button('Add key')
+
+ expect(page).to have_content('Key type is not allowed. Must be DSA or ECDSA')
+ end
+ end
end
scenario 'User sees their keys' do
diff --git a/spec/lib/gitlab/git_access_spec.rb b/spec/lib/gitlab/git_access_spec.rb
index 295a979da76..a67902c7209 100644
--- a/spec/lib/gitlab/git_access_spec.rb
+++ b/spec/lib/gitlab/git_access_spec.rb
@@ -155,6 +155,48 @@ describe Gitlab::GitAccess do
end
end
+ shared_examples '#check with a key that is not valid' do
+ before do
+ project.add_master(user)
+ end
+
+ context 'key is too small' do
+ before do
+ stub_application_setting(minimum_rsa_bits: 4096)
+ end
+
+ it 'does not allow keys which are too small' do
+ aggregate_failures do
+ expect(actor).not_to be_valid
+ expect { pull_access_check }.to raise_unauthorized('Your SSH key length must be at least 4096 bits.')
+ expect { push_access_check }.to raise_unauthorized('Your SSH key length must be at least 4096 bits.')
+ end
+ end
+ end
+
+ context 'key type is not allowed' do
+ before do
+ stub_application_setting(allowed_key_types: ['ecdsa'])
+ end
+
+ it 'does not allow keys which are too small' do
+ aggregate_failures do
+ expect(actor).not_to be_valid
+ expect { pull_access_check }.to raise_unauthorized('Your SSH key type is not allowed. Must be ECDSA.')
+ expect { push_access_check }.to raise_unauthorized('Your SSH key type is not allowed. Must be ECDSA.')
+ end
+ end
+ end
+ end
+
+ it_behaves_like '#check with a key that is not valid' do
+ let(:actor) { build(:rsa_key_2048, user: user) }
+ end
+
+ it_behaves_like '#check with a key that is not valid' do
+ let(:actor) { build(:rsa_deploy_key_2048, user: user) }
+ end
+
describe '#check_project_moved!' do
before do
project.add_master(user)
diff --git a/spec/lib/gitlab/key_fingerprint_spec.rb b/spec/lib/gitlab/key_fingerprint_spec.rb
deleted file mode 100644
index d643dc5342d..00000000000
--- a/spec/lib/gitlab/key_fingerprint_spec.rb
+++ /dev/null
@@ -1,82 +0,0 @@
-require 'spec_helper'
-
-describe Gitlab::KeyFingerprint, lib: true do
- KEYS = {
- rsa:
- 'example.com ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQC5z65PwQ1GE6foJgwk' \
- '9rmQi/glaXbUeVa5uvQpnZ3Z5+forcI7aTngh3aZ/H2UDP2L70TGy7kKNyp0J3a8/OdG' \
- 'Z08y5yi3JlbjFARO1NyoFEjw2H1SJxeJ43L6zmvTlu+hlK1jSAlidl7enS0ufTlzEEj4' \
- 'iJcuTPKdVzKRgZuTRVm9woWNVKqIrdRC0rJiTinERnfSAp/vNYERMuaoN4oJt8p/NEek' \
- 'rmFoDsQOsyDW5RAnCnjWUU+jFBKDpfkJQ1U2n6BjJewC9dl6ODK639l3yN4WOLZEk4tN' \
- 'UysfbGeF3rmMeflaD6O1Jplpv3YhwVGFNKa7fMq6k3Z0tszTJPYh',
- ecdsa:
- 'example.com ecdsa-sha2-nistp256 AAAAE2VjZHNhLXNoYTItbmlzdHAyNTYAAAAI' \
- 'bmlzdHAyNTYAAABBBKTJy43NZzJSfNxpv/e2E6Zy3qoHoTQbmOsU5FEfpWfWa1MdTeXQ' \
- 'YvKOi+qz/1AaNx6BK421jGu74JCDJtiZWT8=',
- ed25519:
- '@revoked example.com ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIAfuCHKVTjq' \
- 'uxvt6CM6tdG4SLp1Btn/nOeHHE5UOzRdf',
- dss:
- 'example.com ssh-dss AAAAB3NzaC1kc3MAAACBAP1/U4EddRIpUt9KnC7s5Of2EbdS' \
- 'PO9EAMMeP4C2USZpRV1AIlH7WT2NWPq/xfW6MPbLm1Vs14E7gB00b/JmYLdrmVClpJ+f' \
- '6AR7ECLCT7up1/63xhv4O1fnxqimFQ8E+4P208UewwI1VBNaFpEy9nXzrith1yrv8iID' \
- 'GZ3RSAHHAAAAFQCXYFCPFSMLzLKSuYKi64QL8Fgc9QAAAIEA9+GghdabPd7LvKtcNrhX' \
- 'uXmUr7v6OuqC+VdMCz0HgmdRWVeOutRZT+ZxBxCBgLRJFnEj6EwoFhO3zwkyjMim4TwW' \
- 'eotUfI0o4KOuHiuzpnWRbqN/C/ohNWLx+2J6ASQ7zKTxvqhRkImog9/hWuWfBpKLZl6A' \
- 'e1UlZAFMO/7PSSoAAACBAJcQ4JODqhuGbXIEpqxetm7PWbdbCcr3y/GzIZ066pRovpL6' \
- 'qm3qCVIym4cyChxWwb8qlyCIi+YRUUWm1z/wiBYT2Vf3S4FXBnyymCkKEaV/EY7+jd4X' \
- '1bXI58OD2u+bLCB/sInM4fGB8CZUIWT9nJH0Ve9jJUge2ms348/QOJ1+'
- }.freeze
-
- MD5_FINGERPRINTS = {
- rsa: '06:b2:8a:92:df:0e:11:2c:ca:7b:8f:a4:ba:6e:4b:fd',
- ecdsa: '45:ff:5b:98:9a:b6:8a:41:13:c1:30:8b:09:5e:7b:4e',
- ed25519: '2e:65:6a:c8:cf:bf:b2:8b:9a:bd:6d:9f:11:5c:12:16',
- dss: '57:98:86:02:5f:9c:f4:9b:ad:5a:1e:51:92:0e:fd:2b'
- }.freeze
-
- BIT_COUNTS = {
- rsa: 2048,
- ecdsa: 256,
- ed25519: 256,
- dss: 1024
- }.freeze
-
- describe '#type' do
- KEYS.each do |type, key|
- it "calculates the type of #{type} keys" do
- calculated_type = described_class.new(key).type
-
- expect(calculated_type).to eq(type.to_s.upcase)
- end
- end
- end
-
- describe '#fingerprint' do
- KEYS.each do |type, key|
- it "calculates the MD5 fingerprint for #{type} keys" do
- fp = described_class.new(key).fingerprint
-
- expect(fp).to eq(MD5_FINGERPRINTS[type])
- end
- end
- end
-
- describe '#bits' do
- KEYS.each do |type, key|
- it "calculates the number of bits in #{type} keys" do
- bits = described_class.new(key).bits
-
- expect(bits).to eq(BIT_COUNTS[type])
- end
- end
- end
-
- describe '#key' do
- it 'carries the unmodified key data' do
- key = described_class.new(KEYS[:rsa]).key
-
- expect(key).to eq(KEYS[:rsa])
- end
- end
-end
diff --git a/spec/lib/gitlab/ssh_public_key_spec.rb b/spec/lib/gitlab/ssh_public_key_spec.rb
new file mode 100644
index 00000000000..d3314552d31
--- /dev/null
+++ b/spec/lib/gitlab/ssh_public_key_spec.rb
@@ -0,0 +1,132 @@
+require 'spec_helper'
+
+describe Gitlab::SSHPublicKey, lib: true do
+ let(:key) { attributes_for(:rsa_key_2048)[:key] }
+ let(:public_key) { described_class.new(key) }
+
+ describe '.technology_names' do
+ it 'returns the available technology names' do
+ expect(described_class.technology_names).to eq(%w[rsa dsa ecdsa ed25519])
+ end
+ end
+
+ describe '.allowed_sizes(name)' do
+ where(:name, :sizes) do
+ [
+ ['rsa', [1024, 2048, 3072, 4096]],
+ ['dsa', [1024, 2048, 3072]],
+ ['ecdsa', [256, 384, 521]],
+ ['ed25519', [256]]
+ ]
+ end
+
+ subject { described_class.allowed_sizes(name) }
+
+ with_them do
+ it { is_expected.to eq(sizes) }
+ end
+ end
+
+ describe '.allowed_type?' do
+ it 'determines the key type' do
+ expect(described_class.allowed_type?('foo')).to be(false)
+ end
+ end
+
+ describe '#valid?' do
+ subject { public_key }
+
+ context 'with a valid SSH key' do
+ it { is_expected.to be_valid }
+ end
+
+ context 'with an invalid SSH key' do
+ let(:key) { 'this is not a key' }
+
+ it { is_expected.not_to be_valid }
+ end
+ end
+
+ describe '#type' do
+ subject { public_key.type }
+
+ where(:factory, :type) do
+ [
+ [:rsa_key_2048, :rsa],
+ [:dsa_key_2048, :dsa],
+ [:ecdsa_key_256, :ecdsa],
+ [:ed25519_key_256, :ed25519]
+ ]
+ end
+
+ with_them do
+ let(:key) { attributes_for(factory)[:key] }
+
+ it { is_expected.to eq(type) }
+ end
+
+ context 'with an invalid SSH key' do
+ let(:key) { 'this is not a key' }
+
+ it { is_expected.to be_nil }
+ end
+ end
+
+ describe '#bits' do
+ subject { public_key.bits }
+
+ where(:factory, :bits) do
+ [
+ [:rsa_key_2048, 2048],
+ [:dsa_key_2048, 2048],
+ [:ecdsa_key_256, 256],
+ [:ed25519_key_256, 256]
+ ]
+ end
+
+ with_them do
+ let(:key) { attributes_for(factory)[:key] }
+
+ it { is_expected.to eq(bits) }
+ end
+
+ context 'with an invalid SSH key' do
+ let(:key) { 'this is not a key' }
+
+ it { is_expected.to be_nil }
+ end
+ end
+
+ describe '#fingerprint' do
+ subject { public_key.fingerprint }
+
+ where(:factory, :fingerprint) do
+ [
+ [:rsa_key_2048, '2e:ca:dc:e0:37:29:ed:fc:f0:1d:bf:66:d4:cd:51:b1'],
+ [:dsa_key_2048, 'bc:c1:a4:be:7e:8c:84:56:b3:58:93:53:c6:80:78:8c'],
+ [:ecdsa_key_256, '67:a3:a9:7d:b8:e1:15:d4:80:40:21:34:bb:ed:97:38'],
+ [:ed25519_key_256, 'e6:eb:45:8a:3c:59:35:5f:e9:5b:80:12:be:7e:22:73']
+ ]
+ end
+
+ with_them do
+ let(:key) { attributes_for(factory)[:key] }
+
+ it { is_expected.to eq(fingerprint) }
+ end
+
+ context 'with an invalid SSH key' do
+ let(:key) { 'this is not a key' }
+
+ it { is_expected.to be_nil }
+ end
+ end
+
+ describe '#key_text' do
+ let(:key) { 'this is not a key' }
+
+ it 'carries the unmodified key data' do
+ expect(public_key.key_text).to eq(key)
+ end
+ end
+end
diff --git a/spec/models/application_setting_spec.rb b/spec/models/application_setting_spec.rb
index 359753b600e..44d473db07d 100644
--- a/spec/models/application_setting_spec.rb
+++ b/spec/models/application_setting_spec.rb
@@ -72,6 +72,27 @@ describe ApplicationSetting do
.is_greater_than(0)
end
+ it { is_expected.to validate_presence_of(:minimum_rsa_bits) }
+ it { is_expected.to allow_value(*Gitlab::SSHPublicKey.allowed_sizes('rsa')).for(:minimum_rsa_bits) }
+ it { is_expected.not_to allow_value(128).for(:minimum_rsa_bits) }
+
+ it { is_expected.to validate_presence_of(:minimum_dsa_bits) }
+ it { is_expected.to allow_value(*Gitlab::SSHPublicKey.allowed_sizes('dsa')).for(:minimum_dsa_bits) }
+ it { is_expected.not_to allow_value(128).for(:minimum_dsa_bits) }
+
+ it { is_expected.to validate_presence_of(:minimum_ecdsa_bits) }
+ it { is_expected.to allow_value(*Gitlab::SSHPublicKey.allowed_sizes('ecdsa')).for(:minimum_ecdsa_bits) }
+ it { is_expected.not_to allow_value(128).for(:minimum_ecdsa_bits) }
+
+ it { is_expected.to validate_presence_of(:minimum_ed25519_bits) }
+ it { is_expected.to allow_value(*Gitlab::SSHPublicKey.allowed_sizes('ed25519')).for(:minimum_ed25519_bits) }
+ it { is_expected.not_to allow_value(128).for(:minimum_ed25519_bits) }
+
+ describe 'allowed_key_types validations' do
+ it { is_expected.to allow_value(Gitlab::SSHPublicKey.technology_names).for(:allowed_key_types) }
+ it { is_expected.not_to allow_value(['foo']).for(:allowed_key_types) }
+ end
+
it_behaves_like 'an object with email-formated attributes', :admin_notification_email do
subject { setting }
end
@@ -441,4 +462,16 @@ describe ApplicationSetting do
end
end
end
+
+ context 'allowed key types attribute' do
+ it 'set value with array of symbols' do
+ setting.allowed_key_types = [:rsa]
+ expect(setting.allowed_key_types).to contain_exactly(:rsa)
+ end
+
+ it 'get value as array of symbols' do
+ setting.allowed_key_types = ['rsa']
+ expect(setting.allowed_key_types).to eq(['rsa'])
+ end
+ end
end
diff --git a/spec/models/key_spec.rb b/spec/models/key_spec.rb
index 3508391c721..83b11baa371 100644
--- a/spec/models/key_spec.rb
+++ b/spec/models/key_spec.rb
@@ -1,6 +1,13 @@
require 'spec_helper'
describe Key, :mailer do
+ include Gitlab::CurrentSettings
+
+ describe 'modules' do
+ subject { described_class }
+ it { is_expected.to include_module(Gitlab::CurrentSettings) }
+ end
+
describe "Associations" do
it { is_expected.to belong_to(:user) }
end
@@ -11,8 +18,10 @@ describe Key, :mailer do
it { is_expected.to validate_presence_of(:key) }
it { is_expected.to validate_length_of(:key).is_at_most(5000) }
- it { is_expected.to allow_value('ssh-foo').for(:key) }
- it { is_expected.to allow_value('ecdsa-foo').for(:key) }
+ it { is_expected.to allow_value(attributes_for(:rsa_key_2048)[:key]).for(:key) }
+ it { is_expected.to allow_value(attributes_for(:dsa_key_2048)[:key]).for(:key) }
+ it { is_expected.to allow_value(attributes_for(:ecdsa_key_256)[:key]).for(:key) }
+ it { is_expected.to allow_value(attributes_for(:ed25519_key_256)[:key]).for(:key) }
it { is_expected.not_to allow_value('foo-bar').for(:key) }
end
@@ -95,6 +104,78 @@ describe Key, :mailer do
end
end
+ context 'validate it meets minimum bit length' do
+ where(:factory, :minimum, :result) do
+ [
+ [:rsa_key_2048, 1024, true],
+ [:rsa_key_2048, 2048, true],
+ [:rsa_key_2048, 4096, false],
+ [:dsa_key_2048, 1024, true],
+ [:dsa_key_2048, 2048, true],
+ [:dsa_key_2048, 4096, false],
+ [:ecdsa_key_256, 256, true],
+ [:ecdsa_key_256, 384, false],
+ [:ed25519_key_256, 256, true],
+ [:ed25519_key_256, 384, false]
+ ]
+ end
+
+ with_them do
+ subject(:key) { build(factory) }
+
+ before do
+ stub_application_setting("minimum_#{key.public_key.type}_bits" => minimum)
+ end
+
+ it { expect(key.valid?).to eq(result) }
+ end
+ end
+
+ context 'validate the key type is allowed' do
+ it 'accepts RSA, DSA, ECDSA and ED25519 keys by default' do
+ expect(build(:rsa_key_2048)).to be_valid
+ expect(build(:dsa_key_2048)).to be_valid
+ expect(build(:ecdsa_key_256)).to be_valid
+ expect(build(:ed25519_key_256)).to be_valid
+ end
+
+ it 'rejects RSA, ECDSA and ED25519 keys if DSA is the only allowed type' do
+ stub_application_setting(allowed_key_types: ['dsa'])
+
+ expect(build(:rsa_key_2048)).not_to be_valid
+ expect(build(:dsa_key_2048)).to be_valid
+ expect(build(:ecdsa_key_256)).not_to be_valid
+ expect(build(:ed25519_key_256)).not_to be_valid
+ end
+
+ it 'rejects RSA, DSA and ED25519 keys if ECDSA is the only allowed type' do
+ stub_application_setting(allowed_key_types: ['ecdsa'])
+
+ expect(build(:rsa_key_2048)).not_to be_valid
+ expect(build(:dsa_key_2048)).not_to be_valid
+ expect(build(:ecdsa_key_256)).to be_valid
+ expect(build(:ed25519_key_256)).not_to be_valid
+ end
+
+ it 'rejects DSA, ECDSA and ED25519 keys if RSA is the only allowed type' do
+ stub_application_setting(allowed_key_types: ['rsa'])
+
+ expect(build(:rsa_key_2048)).to be_valid
+ expect(build(:dsa_key_2048)).not_to be_valid
+ expect(build(:ecdsa_key_256)).not_to be_valid
+ expect(build(:ed25519_key_256)).not_to be_valid
+ end
+
+ it 'rejects RSA, DSA and ECDSA keys if ED25519 is the only allowed type' do
+ stub_application_setting(allowed_key_types: ['ed25519'])
+
+ expect(build(:rsa_key_2048)).not_to be_valid
+ expect(build(:dsa_key_2048)).not_to be_valid
+ expect(build(:ecdsa_key_256)).not_to be_valid
+ expect(build(:ed25519_key_256)).to be_valid
+ end
+ end
+
context 'callbacks' do
it 'adds new key to authorized_file' do
key = build(:personal_key, id: 7)
diff --git a/spec/requests/api/settings_spec.rb b/spec/requests/api/settings_spec.rb
index 737c028ad53..60e7c2d0da3 100644
--- a/spec/requests/api/settings_spec.rb
+++ b/spec/requests/api/settings_spec.rb
@@ -19,6 +19,11 @@ describe API::Settings, 'Settings' do
expect(json_response['default_project_visibility']).to be_a String
expect(json_response['default_snippet_visibility']).to be_a String
expect(json_response['default_group_visibility']).to be_a String
+ expect(json_response['minimum_rsa_bits']).to eq(1024)
+ expect(json_response['minimum_dsa_bits']).to eq(1024)
+ expect(json_response['minimum_ecdsa_bits']).to eq(256)
+ expect(json_response['minimum_ed25519_bits']).to eq(256)
+ expect(json_response['allowed_key_types']).to contain_exactly('rsa', 'dsa', 'ecdsa', 'ed25519')
end
end
@@ -44,7 +49,12 @@ describe API::Settings, 'Settings' do
help_page_text: 'custom help text',
help_page_hide_commercial_content: true,
help_page_support_url: 'http://example.com/help',
- project_export_enabled: false
+ project_export_enabled: false,
+ minimum_rsa_bits: 2048,
+ minimum_dsa_bits: 2048,
+ minimum_ecdsa_bits: 384,
+ minimum_ed25519_bits: 256,
+ allowed_key_types: ['rsa']
expect(response).to have_http_status(200)
expect(json_response['default_projects_limit']).to eq(3)
@@ -61,6 +71,11 @@ describe API::Settings, 'Settings' do
expect(json_response['help_page_hide_commercial_content']).to be_truthy
expect(json_response['help_page_support_url']).to eq('http://example.com/help')
expect(json_response['project_export_enabled']).to be_falsey
+ expect(json_response['minimum_rsa_bits']).to eq(2048)
+ expect(json_response['minimum_dsa_bits']).to eq(2048)
+ expect(json_response['minimum_ecdsa_bits']).to eq(384)
+ expect(json_response['minimum_ed25519_bits']).to eq(256)
+ expect(json_response['allowed_key_types']).to eq(['rsa'])
end
end