diff --git a/lib/puppet/provider/sshkey/parsed.rb b/lib/puppet/provider/sshkey/parsed.rb index 985fa83..14498d7 100644 --- a/lib/puppet/provider/sshkey/parsed.rb +++ b/lib/puppet/provider/sshkey/parsed.rb @@ -12,15 +12,42 @@ record_line :parsed, fields: ['name', 'type', 'key'], post_parse: proc { |hash| - names = hash[:name].split(',', -1) - hash[:name] = names.shift - hash[:host_aliases] = names + # Check if this is a cert-authority line by looking at the name field + if hash[:name] && hash[:name] == '@cert-authority' + # Re-parse the cert-authority format: @cert-authority hostname keytype key + # The original fields were: name='@cert-authority', type='hostname', key='keytype actualkey' + hostname = hash[:type] + keytype_and_key = hash[:key].split(/\s+/, 2) + if keytype_and_key.length >= 2 + keytype = keytype_and_key[0] + actual_key = keytype_and_key[1] + hash[:name] = hostname + hash[:type] = "@cert-authority #{keytype}" + hash[:key] = actual_key + end + end + + # Handle host aliases for all entries + if hash[:name] + names = hash[:name].split(',', -1) + hash[:name] = names.shift + hash[:host_aliases] = names + end }, pre_gen: proc { |hash| if hash[:host_aliases] hash[:name] = [hash[:name], hash[:host_aliases]].flatten.join(',') hash.delete(:host_aliases) end + # Handle cert-authority format + if hash[:type] && hash[:type].to_s.start_with?('@cert-authority ') + # Extract the key type from '@cert-authority ssh-rsa' format + key_type = hash[:type].to_s.sub(/^@cert-authority /, '') + # Reorder fields for cert-authority format: @cert-authority name key_type key + # We need to restructure to have three fields: name="@cert-authority hostname", type="keytype", key=key + hash[:name] = "@cert-authority #{hash[:name]}" + hash[:type] = key_type + end } # Make sure to use mode 644 if ssh_known_hosts is newly created diff --git a/lib/puppet/type/sshkey.rb b/lib/puppet/type/sshkey.rb index e57005a..50880ce 100644 --- a/lib/puppet/type/sshkey.rb +++ b/lib/puppet/type/sshkey.rb @@ -37,18 +37,30 @@ def self.title_patterns end newparam(:type) do - desc 'The encryption type used. Probably ssh-dss or ssh-rsa.' + desc 'The encryption type used. Probably ssh-dss or ssh-rsa. + For certificate authorities, use @cert-authority followed by the key type, + e.g., @cert-authority ssh-rsa. This will create an entry in the known_hosts + file formatted as "@cert-authority hostname keytype key" which allows SSH + to validate host certificates signed by the specified certificate authority.' isnamevar newvalues :'ssh-dss', :'ssh-ed25519', :'ssh-rsa', :'ecdsa-sha2-nistp256', :'ecdsa-sha2-nistp384', :'ecdsa-sha2-nistp521', - :'sk-ecdsa-sha2-nistp256@openssh.com', :'sk-ssh-ed25519@openssh.com' + :'sk-ecdsa-sha2-nistp256@openssh.com', :'sk-ssh-ed25519@openssh.com', + :'@cert-authority ssh-dss', :'@cert-authority ssh-ed25519', :'@cert-authority ssh-rsa', + :'@cert-authority ecdsa-sha2-nistp256', :'@cert-authority ecdsa-sha2-nistp384', :'@cert-authority ecdsa-sha2-nistp521', + :'@cert-authority sk-ecdsa-sha2-nistp256@openssh.com', :'@cert-authority sk-ssh-ed25519@openssh.com' aliasvalue(:dsa, :'ssh-dss') aliasvalue(:ed25519, :'ssh-ed25519') aliasvalue(:rsa, :'ssh-rsa') aliasvalue(:'ecdsa-sk', :'sk-ecdsa-sha2-nistp256@openssh.com') aliasvalue(:'ed25519-sk', :'sk-ssh-ed25519@openssh.com') + aliasvalue(:'@cert-authority dsa', :'@cert-authority ssh-dss') + aliasvalue(:'@cert-authority ed25519', :'@cert-authority ssh-ed25519') + aliasvalue(:'@cert-authority rsa', :'@cert-authority ssh-rsa') + aliasvalue(:'@cert-authority ecdsa-sk', :'@cert-authority sk-ecdsa-sha2-nistp256@openssh.com') + aliasvalue(:'@cert-authority ed25519-sk', :'@cert-authority sk-ssh-ed25519@openssh.com') end newproperty(:key) do diff --git a/spec/integration/provider/sshkey_spec.rb b/spec/integration/provider/sshkey_spec.rb index 11e98b5..5d253c1 100644 --- a/spec/integration/provider/sshkey_spec.rb +++ b/spec/integration/provider/sshkey_spec.rb @@ -195,4 +195,70 @@ resource_app.main end end + + describe 'cert-authority functionality' do + let(:provider_class) { described_class } + let(:type) { Puppet::Type.type(:sshkey) } + + describe 'round-trip conversion' do + it 'correctly parses and generates cert-authority entries' do + # Test parsing a cert-authority line + line = '@cert-authority *.example.com ssh-rsa AAAAB3NzaC1yc2EAAAABIwAAAQEAzwHhxXvIrtfIwrudFqc8yQcIfMudrgpnuh1F3AV6d2BrLgu/yQE7W5UyJMUjfj427sQudRwKW45O0Jsnr33F4mUw+GIMlAAmp9g24/OcrTiB8ZUKIjoPy/cO4coxGi8/NECtRzpD/ZUPFh6OEpyOwJPMb7/EC2Az6Otw4StHdXUYw22zHazBcPFnv6zCgPx1hA7QlQDWTu4YcL0WmTYQCtMUb3FUqrcFtzGDD0ytosgwSd+JyN5vj5UwIABjnNOHPZ62EY1OFixnfqX/+dUwrFSs5tPgBF/KkC6R7tmbUfnBON6RrGEmu+ajOTOLy23qUZB4CQ53V7nyAWhzqSK+hw==' + + # Parse the line + parsed = provider_class.parse_line(line) + expect(parsed[:name]).to eq('*.example.com') + expect(parsed[:type]).to eq('@cert-authority ssh-rsa') + expect(parsed[:key]).to start_with('AAAAB3NzaC1yc2EA') + + # Test generating back to line format + generated_line = provider_class.to_line(parsed) + expect(generated_line).to eq(line) + end + + it 'handles cert-authority entries with host aliases' do + line = '@cert-authority *.example.com,*.test.com ssh-rsa AAAAB3NzaC1yc2EAAAABIwAAAQEAzwHhxXvIrtfIwrudFqc8yQcIfMudrgpnuh1F3AV6d2BrLgu/yQE7W5UyJMUjfj427sQudRwKW45O0Jsnr33F4mUw+GIMlAAmp9g24/OcrTiB8ZUKIjoPy/cO4coxGi8/NECtRzpD/ZUPFh6OEpyOwJPMb7/EC2Az6Otw4StHdXUYw22zHazBcPFnv6zCgPx1hA7QlQDWTu4YcL0WmTYQCtMUb3FUqrcFtzGDD0ytosgwSd+JyN5vj5UwIABjnNOHPZ62EY1OFixnfqX/+dUwrFSs5tPgBF/KkC6R7tmbUfnBON6RrGEmu+ajOTOLy23qUZB4CQ53V7nyAWhzqSK+hw==' + + parsed = provider_class.parse_line(line) + expect(parsed[:name]).to eq('*.example.com') + expect(parsed[:host_aliases]).to eq(['*.test.com']) + expect(parsed[:type]).to eq('@cert-authority ssh-rsa') + + # Test generating back + generated_line = provider_class.to_line(parsed) + expect(generated_line).to eq(line) + end + end + + describe 'resource creation' do + it 'can create a cert-authority sshkey resource' do + expect { + type.new( + name: '*.example.com', + type: '@cert-authority ssh-rsa', + key: 'AAAAB3NzaC1yc2EAAAA' + ) + }.not_to raise_error + end + + it 'can create a cert-authority resource with aliases' do + expect { + type.new( + name: '*.example.com', + type: '@cert-authority rsa', # Test alias + key: 'AAAAB3NzaC1yc2EAAAA' + ) + }.not_to raise_error + end + + it 'aliases cert-authority key types correctly' do + resource = type.new( + name: '*.example.com', + type: '@cert-authority rsa', # Alias + key: 'AAAAB3NzaC1yc2EAAAA' + ) + expect(resource[:type]).to eq(:'@cert-authority ssh-rsa') + end + end + end end diff --git a/spec/unit/provider/sshkey/parsed_spec.rb b/spec/unit/provider/sshkey/parsed_spec.rb index 64da0da..6366e83 100644 --- a/spec/unit/provider/sshkey/parsed_spec.rb +++ b/spec/unit/provider/sshkey/parsed_spec.rb @@ -38,6 +38,20 @@ def key expect(subject.parse_line('test ssh-rsa ' + key)[:host_aliases]).to eq([]) end + it 'parses cert-authority entries correctly' do + result = subject.parse_line('@cert-authority *.example.com ssh-rsa ' + key) + expect(result[:name]).to eq('*.example.com') + expect(result[:type]).to eq('@cert-authority ssh-rsa') + expect(result[:key]).to eq(key) + end + + it 'parses cert-authority entries with host aliases' do + result = subject.parse_line('@cert-authority *.example.com,*.test.com ssh-rsa ' + key) + expect(result[:name]).to eq('*.example.com') + expect(result[:host_aliases]).to eq(['*.test.com']) + expect(result[:type]).to eq('@cert-authority ssh-rsa') + end + context 'with the sample file' do ['sample', 'sample_with_blank_lines'].each do |sample_file| let(:fixture) { my_fixture(sample_file) } diff --git a/spec/unit/type/sshkey_spec.rb b/spec/unit/type/sshkey_spec.rb index ace6776..eb66d03 100644 --- a/spec/unit/type/sshkey_spec.rb +++ b/spec/unit/type/sshkey_spec.rb @@ -29,7 +29,15 @@ :'ecdsa-sha2-nistp521', :'ssh-ed25519', :ed25519, :'ecdsa-sk', :'sk-ecdsa-sha2-nistp256@openssh.com', - :'ed25519-sk', :'sk-ssh-ed25519@openssh.com' + :'ed25519-sk', :'sk-ssh-ed25519@openssh.com', + :'@cert-authority ssh-dss', :'@cert-authority dsa', + :'@cert-authority ssh-rsa', :'@cert-authority rsa', + :'@cert-authority ecdsa-sha2-nistp256', + :'@cert-authority ecdsa-sha2-nistp384', + :'@cert-authority ecdsa-sha2-nistp521', + :'@cert-authority ssh-ed25519', :'@cert-authority ed25519', + :'@cert-authority ecdsa-sk', :'@cert-authority sk-ecdsa-sha2-nistp256@openssh.com', + :'@cert-authority ed25519-sk', :'@cert-authority sk-ssh-ed25519@openssh.com' ].each do |keytype| it "supports #{keytype} as a type value" do described_class.new(name: 'foo', type: keytype) @@ -56,6 +64,21 @@ expect(key.parameter(:type).value).to eq :'sk-ssh-ed25519@openssh.com' end + it 'aliases :@cert-authority rsa to :@cert-authority ssh-rsa' do + key = described_class.new(name: 'foo', type: :'@cert-authority rsa') + expect(key.parameter(:type).value).to eq :'@cert-authority ssh-rsa' + end + + it 'aliases :@cert-authority dsa to :@cert-authority ssh-dss' do + key = described_class.new(name: 'foo', type: :'@cert-authority dsa') + expect(key.parameter(:type).value).to eq :'@cert-authority ssh-dss' + end + + it 'aliases :@cert-authority ed25519 to :@cert-authority ssh-ed25519' do + key = described_class.new(name: 'foo', type: :'@cert-authority ed25519') + expect(key.parameter(:type).value).to eq :'@cert-authority ssh-ed25519' + end + it "doesn't support values other than ssh-dss, ssh-rsa, dsa, rsa for type" do expect { described_class.new(name: 'whev', type: :'ssh-dsa')