require 'spec_helper'

shared_examples_for "signing" do |selfsign|
  before :each do
    @options = {}
    @options[:csr] = @csr unless @csr.nil?
    @options[:spki] = @spki unless @spki.nil?
    if @options.key?(:spki)
      @options[:subject] = R509::Subject.new([['CN', 'test']])
    end
  end

  it "with default subject (selfsign:#{selfsign})" do
    if selfsign
      cert = R509::CertificateAuthority::Signer.selfsign(@options)
    else
      cert = @ca.sign(@options)
    end
    subject = (@options[:csr].nil?) ? @options[:subject] : @options[:csr].subject
    expect(cert.subject.to_s).to eq(subject.to_s)
  end

  it "with specified subject (selfsign:#{selfsign})" do
    subject = R509::Subject.new
    subject.CN = 'myCN'
    subject.O = 'Org'
    @options[:subject] = subject
    if selfsign
      cert = R509::CertificateAuthority::Signer.selfsign(@options)
    else
      cert = @ca.sign(@options)
    end
    expect(cert.subject.to_s).to eq('/CN=myCN/O=Org')
  end

  it "with default md (selfsign:#{selfsign})" do
    cert = @ca.sign(@options)
    regex = Regexp.new(R509::MessageDigest::DEFAULT_MD, Regexp::IGNORECASE)
    expect(cert.signature_algorithm).to match(regex)
  end

  it "with specified md (selfsign:#{selfsign})" do
    @options[:message_digest] = 'SHA256'
    if selfsign
      cert = R509::CertificateAuthority::Signer.selfsign(@options)
    else
      cert = @ca.sign(@options)
    end
    expect(cert.signature_algorithm).to match(/sha256/i)
  end

  it "with no :extensions in options hash (selfsign:#{selfsign})" do
    if selfsign
      cert = R509::CertificateAuthority::Signer.selfsign(@options)
      size = 3
    else
      cert = @ca.sign(@options)
      size = 2
    end
    expect(cert.extensions.size).to eq(size)
  end

  it "with empty extensions array (selfsign:#{selfsign})" do
    @options[:extensions] = []
    if selfsign
      cert = R509::CertificateAuthority::Signer.selfsign(@options)
    else
      cert = @ca.sign(@options)
    end
    expect(cert.extensions.size).to eq(0)
  end

  it "with multiple extensions (selfsign:#{selfsign})" do
    exts = []
    exts << R509::Cert::Extensions::BasicConstraints.new(:ca => false)
    exts << R509::Cert::Extensions::InhibitAnyPolicy.new(:value => 4)
    @options[:extensions] = exts
    if selfsign
      cert = R509::CertificateAuthority::Signer.selfsign(@options)
    else
      cert = @ca.sign(@options)
    end
    expect(cert.extensions.size).to eq(2)
    expect(cert.basic_constraints.is_ca?).to eq(false)
    expect(cert.inhibit_any_policy.value).to eq(4)
  end

  it "with random serial when serial is not specified and uses microtime as part of the serial to prevent collision (selfsign:#{selfsign})" do
    now = Time.now
    allow(Time).to receive(:now).and_return(now)
    time = now.to_i.to_s
    if selfsign
      cert = R509::CertificateAuthority::Signer.selfsign(@options)
    else
      cert = @ca.sign(@options)
    end
    expect(cert.serial.to_s.size).to be >= 45
    expect(cert.serial.to_s.index(time)).not_to be_nil
  end

  it "with specified serial number (selfsign:#{selfsign})" do
    @options[:serial] = 11223344
    if selfsign
      cert = R509::CertificateAuthority::Signer.selfsign(@options)
    else
      cert = @ca.sign(@options)
    end
    expect(cert.serial).to eq(11223344)
  end

  it "with default notBefore/notAfter dates (selfsign:#{selfsign})" do
    @options[:not_before] = (Time.now - (6 * 60 * 60)).utc
    @options[:not_after] = (Time.now - (6 * 60 * 60) + (365 * 24 * 60 * 60)).utc
    if selfsign
      cert = R509::CertificateAuthority::Signer.selfsign(@options)
    else
      cert = @ca.sign(@options)
    end
    expect(cert.not_before.ctime).to eq(@options[:not_before].utc.ctime)
    expect(cert.not_after.ctime).to eq(@options[:not_after].utc.ctime)
  end

  it "with specified notBefore/notAfter dates (selfsign:#{selfsign})" do
    @options[:not_before] = Time.now - 5 * 60 * 60
    @options[:not_after] = Time.now + 5 * 60 * 60
    if selfsign
      cert = R509::CertificateAuthority::Signer.selfsign(@options)
    else
      cert = @ca.sign(@options)
    end
    expect(cert.not_before.ctime).to eq(@options[:not_before].utc.ctime)
    expect(cert.not_after.ctime).to eq(@options[:not_after].utc.ctime)
  end

end

describe R509::CertificateAuthority::Signer do
  context "error handling" do
    before :each do
      test_ca_config = TestFixtures.test_ca_config
      @ca = R509::CertificateAuthority::Signer.new(test_ca_config)
    end

    it "raises error unless you provide a proper config (or nil)" do
      expect { R509::CertificateAuthority::Signer.new('invalid') }.to raise_error(R509::R509Error, 'config must be a kind of R509::Config::CAConfig')
    end

    it "raises an error if you don't pass csr or spki" do
      expect { @ca.sign({}) }.to raise_error(ArgumentError, "You must supply either :csr or :spki")
    end

    it "raises an error if you pass a config that has no private key for ca_cert" do
      config = R509::Config::CAConfig.new(:ca_cert => R509::Cert.new(:cert => TestFixtures::TEST_CA_CERT))
      expect { R509::CertificateAuthority::Signer.new(config) }.to raise_error(R509::R509Error, "You must have a private key associated with your CA certificate to issue")
    end

    it "raises an error if you pass both csr and spki" do
      csr = R509::CSR.new(:csr => TestFixtures::CSR)
      spki = R509::SPKI.new(:spki => TestFixtures::SPKI, :subject => [['CN', 'test']])
      expect { @ca.sign(:spki => spki, :csr => csr) }.to raise_error(ArgumentError, "You can't pass both :csr and :spki")
    end

    it "raise an error if you don't pass an R509::SPKI in :spki" do
      spki = OpenSSL::Netscape::SPKI.new(TestFixtures::SPKI)
      expect { @ca.sign(:spki => spki) }.to raise_error(ArgumentError, 'You must pass an R509::SPKI object for :spki')
    end

    it "raise an error if you pass :spki without :subject" do
      spki = R509::SPKI.new(:spki => TestFixtures::SPKI)
      expect { @ca.sign(:spki => spki) }.to raise_error(ArgumentError, 'You must supply :subject when passing :spki')
    end

    it "raise an error if you don't pass an R509::CSR in :csr" do
      csr = OpenSSL::X509::Request.new(TestFixtures::CSR)
      expect { @ca.sign(:csr => csr) }.to raise_error(ArgumentError, 'You must pass an R509::CSR object for :csr')
    end

    it "raises an error if attempting to self-sign without a key" do
      csr = R509::CSR.new(:csr => TestFixtures::CSR)
      expect { R509::CertificateAuthority::Signer.selfsign(:csr => csr) }.to raise_error(ArgumentError, "CSR must also have a private key to self sign")
    end

    it "raises error when passing non-hash to selfsign method" do
      expect { R509::CertificateAuthority::Signer.selfsign(TestFixtures::CSR) }.to raise_error(ArgumentError, "You must pass a hash of options consisting of at minimum :csr")
    end

  end

  context "RSA CSR + CA" do
    before :all do
      test_ca_config = TestFixtures.test_ca_config
      @ca = R509::CertificateAuthority::Signer.new(test_ca_config)
      @csr = R509::CSR.new(:subject => [['C', 'US'], ['ST', 'Illinois'], ['L', 'Chicago'], ['O', 'Paul Kehrer'], ['CN', 'langui.sh']], :bit_strength => 1024)
    end

    it_validates "signing", false
    it_validates "signing", true # selfsign

    context "key in signed cert" do
      it "returns key when CSR contains key" do
        cert = R509::CertificateAuthority::Signer.selfsign(:csr => @csr)
        expect(cert.key).not_to be_nil
        expect(cert.key).to eq(@csr.key)
        cert = @ca.sign(:csr => @csr)
        expect(cert.key).not_to be_nil
        expect(cert.key).to eq(@csr.key)
      end
      it "does not return key when CSR has no key" do
        csr = R509::CSR.new(:csr => TestFixtures::CSR)
        cert = @ca.sign(:csr => csr)
        expect(cert.key).to be_nil
      end
    end
  end

  context "RSA SPKI + CA" do
    before :all do
      test_ca_config = TestFixtures.test_ca_config
      @ca = R509::CertificateAuthority::Signer.new(test_ca_config)
      key = R509::PrivateKey.new(:bit_strength => 1024)
      @spki = R509::SPKI.new(:key => key)
    end

    it_validates "signing", false

    context "key in signed cert" do
      it "does not return key with SPKI" do
        cert = @ca.sign(:spki => @spki, :subject => R509::Subject.new(:CN => 'test'))
        expect(cert.key).to be_nil
      end
    end
  end

  context "Elliptic Curve CSR + CA", :ec => true do
    before :all do
      test_ca_ec = R509::Config::CAConfig.from_yaml("test_ca_ec", File.read("#{File.dirname(__FILE__)}/../fixtures/config_test_ec.yaml"), :ca_root_path => "#{File.dirname(__FILE__)}/../fixtures")
      @ca = R509::CertificateAuthority::Signer.new(test_ca_ec)
      @csr = R509::CSR.new(:subject => [['CN', 'elliptic curves']], :type => "ec")
    end

    it_validates "signing", false
    it_validates "signing", true # selfsign
  end

  context "Elliptic Curve SPKI + CA", :ec => true do
    before :all do
      test_ca_ec = R509::Config::CAConfig.from_yaml("test_ca_ec", File.read("#{File.dirname(__FILE__)}/../fixtures/config_test_ec.yaml"), :ca_root_path => "#{File.dirname(__FILE__)}/../fixtures")
      @ca = R509::CertificateAuthority::Signer.new(test_ca_ec)
      private_key = R509::PrivateKey.new(:type => "ec")
      @spki = R509::SPKI.new(:key => private_key)
    end

    it_validates "signing", false
  end

  context "DSA CSR + CA", :ec => true do
    before :all do
      test_ca_dsa = R509::Config::CAConfig.from_yaml("test_ca_dsa", File.read("#{File.dirname(__FILE__)}/../fixtures/config_test_dsa.yaml"), :ca_root_path => "#{File.dirname(__FILE__)}/../fixtures")

      @ca = R509::CertificateAuthority::Signer.new(test_ca_dsa)
      @csr = R509::CSR.new(:subject => [['CN', 'elliptic curves']], :type => "dsa", :bit_strength => 512)
    end

    it_validates "signing", false
    it_validates "signing", true # selfsign
  end

  context "DSA SPKI + CA", :ec => true do
    before :all do
      test_ca_dsa = R509::Config::CAConfig.from_yaml("test_ca_dsa", File.read("#{File.dirname(__FILE__)}/../fixtures/config_test_dsa.yaml"), :ca_root_path => "#{File.dirname(__FILE__)}/../fixtures")
      @ca = R509::CertificateAuthority::Signer.new(test_ca_dsa)
      private_key = R509::PrivateKey.new(:type => "dsa", :bit_strength => 512)
      @spki = R509::SPKI.new(:key => private_key)
    end

    it_validates "signing", false
  end
end
