require 'spec_helper'
require 'puppet/test_ca'

require 'puppet/certificate_factory'

describe Puppet::CertificateFactory, :unless => RUBY_PLATFORM == 'java' do
  let :serial    do OpenSSL::BN.new('12') end
  let :name      do "example.local" end
  let :x509_name do OpenSSL::X509::Name.new([['CN', name]]) end
  let :key       do OpenSSL::PKey::RSA.new(Puppet[:keylength]) end
  let :csr       do
    csr = Puppet::SSL::CertificateRequest.new(name)
    csr.generate(key)
    csr
  end
  let(:issuer) { Puppet::TestCa.new.ca_cert }

  describe "when generating the certificate" do
    it "should return a new X509 certificate" do
      a = subject.build(:server, csr, issuer, serial)
      b = subject.build(:server, csr, issuer, serial)
      # The two instances are equal in every aspect except that they are
      # different instances - they are `==`, but not hash `eql?`
      expect(a).not_to eql(b)
    end

    it "should set the certificate's version to 2" do
      expect(subject.build(:server, csr, issuer, serial).version).to eq(2)
    end

    it "should set the certificate's subject to the CSR's subject" do
      cert = subject.build(:server, csr, issuer, serial)
      expect(cert.subject).to eq x509_name
    end

    it "should set the certificate's issuer to the Issuer's subject" do
      cert = subject.build(:server, csr, issuer, serial)
      expect(cert.issuer).to eq issuer.subject
    end

    it "should set the certificate's public key to the CSR's public key" do
      cert = subject.build(:server, csr, issuer, serial)
      expect(cert.public_key).to be_public
      expect(cert.public_key.to_s).to eq(csr.content.public_key.to_s)
    end

    it "should set the certificate's serial number to the provided serial number" do
      cert = subject.build(:server, csr, issuer, serial)
      expect(cert.serial).to eq(serial)
    end

    it "should have 24 hours grace on the start of the cert" do
      cert = subject.build(:server, csr, issuer, serial)
      expect(cert.not_before).to be_within(30).of(Time.now - 24*60*60)
    end

    it "should not allow a non-integer TTL" do
      [ 'foo', 1.2, Time.now, true ].each do |ttl|
        expect { subject.build(:server, csr, issuer, serial, ttl) }.to raise_error(ArgumentError)
      end
    end

    it "should respect a custom TTL for the CA" do
      now = Time.now.utc
      expect(Time).to receive(:now).at_least(:once).and_return(now)
      cert = subject.build(:server, csr, issuer, serial, 12)
      expect(cert.not_after.to_i).to eq(now.to_i + 12)
    end

    it "should adds an extension for the nsComment" do
      cert = subject.build(:server, csr, issuer, serial)
      expect(cert.extensions.map {|x| x.to_h }.find {|x| x["oid"] == "nsComment" }).to eq(
        { "oid"      => "nsComment",
          # Note that this output is due to a bug in OpenSSL::X509::Extensions
          # where the values of some extensions are not properly decoded
          "value"    => ".(Puppet Ruby/OpenSSL Internal Certificate",
          "critical" => false }
      )
    end

    it "should add an extension for the subjectKeyIdentifer" do
      cert = subject.build(:server, csr, issuer, serial)
      ef = OpenSSL::X509::ExtensionFactory.new(issuer, cert)
      expect(cert.extensions.map { |x| x.to_h }.find {|x| x["oid"] == "subjectKeyIdentifier" }).to eq(
        ef.create_extension("subjectKeyIdentifier", "hash", false).to_h
      )
    end

    it "should add an extension for the authorityKeyIdentifer" do
      cert = subject.build(:server, csr, issuer, serial)
      ef = OpenSSL::X509::ExtensionFactory.new(issuer, cert)
      expect(cert.extensions.map { |x| x.to_h }.find {|x| x["oid"] == "authorityKeyIdentifier" }).to eq(
        ef.create_extension("authorityKeyIdentifier", "keyid:always", false).to_h
      )
    end

    # See #2848 for why we are doing this: we need to make sure that
    # subjectAltName is set if the CSR has it, but *not* if it is set when the
    # certificate is built!
    it "should not add subjectAltNames from dns_alt_names" do
      Puppet[:dns_alt_names] = 'one, two'
      # Verify the CSR still has no extReq, just in case...
      expect(csr.request_extensions).to eq([])
      cert = subject.build(:server, csr, issuer, serial)

      expect(cert.extensions.find {|x| x.oid == 'subjectAltName' }).to be_nil
    end

    it "should add subjectAltName when the CSR requests them" do
      Puppet[:dns_alt_names] = ''

      expect = %w{one two} + [name]

      csr = Puppet::SSL::CertificateRequest.new(name)
      csr.generate(key, :dns_alt_names => expect.join(', '))

      expect(csr.request_extensions).not_to be_nil
      expect(csr.subject_alt_names).to match_array(expect.map{|x| "DNS:#{x}"})

      cert = subject.build(:server, csr, issuer, serial)
      san = cert.extensions.find {|x| x.oid == 'subjectAltName' }
      expect(san).not_to be_nil
      expect.each do |name|
        expect(san.value).to match(/DNS:#{name}\b/i)
      end
    end

    it "can add custom extension requests" do
      csr = Puppet::SSL::CertificateRequest.new(name)
      csr.generate(key)

      allow(csr).to receive(:request_extensions).and_return([
        {'oid' => '1.3.6.1.4.1.34380.1.2.1', 'value' => 'some-value'},
        {'oid' => 'pp_uuid', 'value' => 'some-uuid'},
      ])

      cert = subject.build(:client, csr, issuer, serial)

      # The cert must be signed before being later DER-decoding
      signer = Puppet::SSL::CertificateSigner.new
      signer.sign(cert, key)
      wrapped_cert = Puppet::SSL::Certificate.from_instance cert

      priv_ext = wrapped_cert.custom_extensions.find {|ext| ext['oid'] == '1.3.6.1.4.1.34380.1.2.1'}
      uuid_ext = wrapped_cert.custom_extensions.find {|ext| ext['oid'] == 'pp_uuid'}

      # The expected results should be DER encoded, the Puppet cert wrapper will turn
      # these into normal strings.
      expect(priv_ext['value']).to eq 'some-value'
      expect(uuid_ext['value']).to eq 'some-uuid'
    end

    # Can't check the CA here, since that requires way more infrastructure
    # that I want to build up at this time.  We can verify the critical
    # values, though, which are non-CA certs. --daniel 2011-10-11
    { :ca            => 'CA:TRUE',
      :terminalsubca => ['CA:TRUE', 'pathlen:0'],
      :server        => 'CA:FALSE',
      :ocsp          => 'CA:FALSE',
      :client        => 'CA:FALSE',
    }.each do |name, value|
      it "should set basicConstraints for #{name} #{value.inspect}" do
        cert = subject.build(name, csr, issuer, serial)
        bc = cert.extensions.find {|x| x.oid == 'basicConstraints' }
        expect(bc).to be
        expect(bc.value.split(/\s*,\s*/)).to match_array(Array(value))
      end
    end
  end
end
