require 'spec_helper'
require 'stringio'
require 'r509/csr'


describe R509::Csr do
    before :all do
        @cert = TestFixtures::CERT
        @cert_san = TestFixtures::CERT_SAN
        @csr = TestFixtures::CSR
        @csr_newlines = TestFixtures::CSR_NEWLINES
        @csr_no_begin_end = TestFixtures::CSR_NO_BEGIN_END
        @csr_der = TestFixtures::CSR_DER
        @csr_public_key_modulus = TestFixtures::CSR_PUBLIC_KEY_MODULUS
        @csr_invalid_signature = TestFixtures::CSR_INVALID_SIGNATURE
        @csr2 = TestFixtures::CSR2
        @csr3 = TestFixtures::CSR3
        @csr_der = TestFixtures::CSR_DER
        @csr_dsa = TestFixtures::CSR_DSA
        @csr4_multiple_attrs = TestFixtures::CSR4_MULTIPLE_ATTRS
        @key3 = TestFixtures::KEY3
        @key_csr2 = TestFixtures::KEY_CSR2
        @dsa_key = TestFixtures::DSA_KEY
        @csr_unknown_oid = TestFixtures::CSR_UNKNOWN_OID
    end

    it "raises an exception when passing non-hash" do
        expect { R509::Csr.new('invalid') }.to raise_error(ArgumentError, 'Must provide a hash of options')
    end
    it "key creation defaults to RSA when no type or key is passed" do
        csr = R509::Csr.new(:subject => [['CN','testing.rsa']], :bit_strength => 1024)
        csr.rsa?.should == true
        csr.dsa?.should == false
    end
    it "returns expected value for to_der" do
        csr = R509::Csr.new(:csr => @csr)
        csr.to_der.should == @csr_der
    end
    it "loads a csr with extraneous newlines" do
        csr = R509::Csr.new(:csr => @csr_newlines)
        csr.to_pem.should match(/-----BEGIN CERTIFICATE REQUEST-----/)
    end
    it "loads a csr with no begin/end lines" do
        csr = R509::Csr.new(:csr => @csr_no_begin_end)
        csr.to_pem.should match(/-----BEGIN CERTIFICATE REQUEST-----/)
    end
    it "returns true from #has_private_key? when private key is present" do
        csr = R509::Csr.new(:bit_strength => 512, :subject => [['CN','private-key-check.com']])
        csr.has_private_key?.should == true
    end
    it "returns false from #has_private_key? when private key is not present" do
        csr = R509::Csr.new(:csr => @csr)
        csr.has_private_key?.should == false
    end
    it "key creation defaults to 2048 when no bit strength or key is passed" do
        csr = R509::Csr.new(:subject => [['CN','testing2048.rsa']])
        csr.bit_strength.should == 2048
    end
    it "creates a CSR when a key is provided" do
        csr = R509::Csr.new(:key => @key3, :subject => [['CN','pregenerated.com']], :bit_strength => 1024)
        csr.to_pem.should match(/CERTIFICATE REQUEST/)
        #validate the CSR matches the key
        csr.req.verify(csr.key.public_key).should == true
    end
    it "loads successfully when an R509::PrivateKey is provided" do
        key = R509::PrivateKey.new(:key => @key3)
        expect { R509::Csr.new(:key => key, :csr => @csr3)}.to_not raise_error
    end
    it "raises an exception when you pass a cert and subject" do
        expect { R509::Csr.new(:cert => @cert, :subject => [['CN','fail.com']]) }.to raise_error(ArgumentError,'Can only provide one of cert, subject, or csr')
    end
    it "raises an exception when you pass a cert and CSR" do
        expect { R509::Csr.new(:cert => @cert, :csr => @csr) }.to raise_error(ArgumentError,'Can only provide one of cert, subject, or csr')
    end
    it "raises an exception when you pass a subject and CSR" do
        expect { R509::Csr.new(:subject => [['CN','error.com']], :csr => @csr) }.to raise_error(ArgumentError,'Can only provide one of cert, subject, or csr')
    end
    it "raises an exception for not providing valid type when key is nil" do
        expect { R509::Csr.new(:subject => [['CN','error.com']], :type => :invalid_symbol) }.to raise_error(ArgumentError,'Must provide :rsa or :dsa as type when key is nil')
    end
    it "raises an exception when you don't provide cert, subject, or CSR" do
        expect { R509::Csr.new(:bit_strength => 1024) }.to raise_error(ArgumentError,'Must provide one of cert, subject, or csr')
    end
    it "raises an exception if you provide a list of domains with an existing CSR" do
        expect { R509::Csr.new(:csr => @csr, :san_names => ['moredomainsiwanttoadd.com']) }.to raise_error(ArgumentError,'You can\'t add domains to an existing CSR')
    end
    it "changes the message_digest to DSS1 when passed a DSA key" do
        csr = R509::Csr.new(:subject => [["CN","dsasigned.com"]], :key => @dsa_key)
        csr.message_digest.name.should == 'dss1'
        csr.signature_algorithm.should == 'dsaWithSHA1'
        #dss1 is actually the same as SHA1
        #Yes this is confusing
        #see http://www.ruby-doc.org/stdlib-1.9.3/libdoc/openssl/rdoc/OpenSSL/PKey/DSA.html
    end
    it "changes the message_digest to DSS1 when creating a DSA key" do
        csr = R509::Csr.new(:subject => [["CN","dsasigned.com"]], :type => :dsa, :bit_strength => 512)
        csr.message_digest.name.should == 'dss1'
        csr.signature_algorithm.should == 'dsaWithSHA1'
        #dss1 is actually the same as SHA1
        #Yes this is confusing
        #see http://www.ruby-doc.org/stdlib-1.9.3/libdoc/openssl/rdoc/OpenSSL/PKey/DSA.html
    end
    it "signs a CSR properly when passed a DSA key" do
        csr = R509::Csr.new(:subject => [["CN","dsasigned.com"]], :key => @dsa_key)
        csr.verify_signature.should == true
    end
    it "signs a CSR properly when creating a DSA key" do
        csr = R509::Csr.new(:subject => [["CN","dsasigned.com"]], :type => :dsa, :bit_strength => 512)
        csr.verify_signature.should == true
    end
    it "writes to pem" do
        csr = R509::Csr.new(:csr => @csr)
        sio = StringIO.new
        sio.set_encoding("BINARY") if sio.respond_to?(:set_encoding)
        csr.write_pem(sio)
        sio.string.should == @csr
    end
    it "writes to der" do
        sio = StringIO.new
        sio.set_encoding("BINARY") if sio.respond_to?(:set_encoding)
        csr = R509::Csr.new(:csr => @csr)
        csr.write_der(sio)
        sio.string.should == @csr_der
    end
    it "duplicate SAN names should be removed" do
        csr = R509::Csr.new( :bit_strength => 512, :subject => [['CN','test2345.com']], :san_names => ["test2.local","test.local","test.local"] )
        csr.san_names.should == ["test2.local", "test.local"]
    end
    it "creates a valid hash object with to_hash" do
        csr = R509::Csr.new(:csr => @csr)
        csr.to_hash[:subject].kind_of?(R509::Subject).should == true
        csr.to_hash[:subject].to_s.should == '/CN=test.local/O=Testing CSR'
        csr.to_hash[:san_names].should == ["test.local", "additionaldomains.com", "saniam.com"]
    end
    it "san_names is an empty array when there are no SAN names" do
        csr = R509::Csr.new( :subject => [['CN','langui.sh'],['emailAddress','ca@langui.sh']], :bit_strength => 512)
        csr.san_names.should == []
    end
    context "when initialized" do
        it "raises exception when providing invalid csr" do
            expect { R509::Csr.new({:csr => 'invalid csr'}) }.to raise_error(OpenSSL::X509::RequestError)
        end
        it "raises exception when providing invalid key" do
            expect { R509::Csr.new({:csr => @csr, :key => 'invalid key'}) }.to raise_error(R509::R509Error,"Failed to load private key. Invalid key or incorrect password.")
        end
    end
    context "when passing a cert to generate" do
        it "returns a valid pem" do
            csr = R509::Csr.new( :bit_strength => 1024, :cert => @cert )
            csr.to_pem.should match(/CERTIFICATE REQUEST/)
        end
        it "has a public key length of 2048 by default" do
            csr = R509::Csr.new( :cert => @cert )
            csr.bit_strength.should == 2048
        end
        it "generates a matching CSR" do
            csr = R509::Csr.new( :bit_strength => 1024, :cert => @cert )
            csr.subject.to_s.should == '/C=US/ST=Illinois/L=Chicago/O=Paul Kehrer/CN=langui.sh'
        end
        it "SAN domains from the cert should be encoded in the request" do
            csr = R509::Csr.new( :bit_strength => 1024, :cert => @cert_san )
            csr.san_names.should == ["langui.sh"]
        end
        it "duplicate SAN names should be removed" do
            csr = R509::Csr.new( :cert => @cert, :san_names => ["test2.local","test.local","test.local"] )
            csr.san_names.should == ["test2.local", "test.local"]
        end
        it "SAN names added in addition to those present in the cert should be merged" do
            csr = R509::Csr.new( :cert => @cert_san, :san_names => ["test2.local","test.local","test.local"] )
            csr.san_names.should == ["langui.sh","test2.local", "test.local"]
        end
    end
    context "when passing a subject array" do
        it "generates a matching CSR" do
            csr = R509::Csr.new( :subject=> [['CN','langui.sh'],['ST','Illinois'],['L','Chicago'],['C','US'],['emailAddress','ca@langui.sh']], :bit_strength => 1024)
            csr.subject.to_s.should == '/CN=langui.sh/ST=Illinois/L=Chicago/C=US/emailAddress=ca@langui.sh'
        end
        it "adds SAN domains to a generated CSR" do
            csr = R509::Csr.new( :subject => [['CN','langui.sh'],['emailAddress','ca@langui.sh']], :bit_strength => 1024, :san_names => ['domain2.com','domain3.com'])
            csr.subject.to_s.should == '/CN=langui.sh/emailAddress=ca@langui.sh'
            csr.san_names.should == ["domain2.com", "domain3.com"]
        end
        it "generates a matching csr when supplying raw oids" do
            csr = R509::Csr.new( :subject => [['2.5.4.3','common name'],['2.5.4.15','business category'],['2.5.4.7','locality'],['1.3.6.1.4.1.311.60.2.1.3','jurisdiction oid openssl typically does not know']], :bit_strength => 1024 )
            csr.subject.to_s.should == "/CN=common name/businessCategory=business category/L=locality/jurisdictionOfIncorporationCountryName=jurisdiction oid openssl typically does not know"
        end
    end
    context "when supplying an existing csr" do
        it "populates the bit_strength" do
            csr = R509::Csr.new({ :csr => @csr })
            csr.bit_strength.should == 2048
        end
        it "populates the subject" do
            csr = R509::Csr.new({ :csr => @csr })
            csr.subject.to_s.should == '/CN=test.local/O=Testing CSR'
        end
        it "parses the san names" do
            csr = R509::Csr.new({ :csr => @csr })
            csr.san_names.should == ["test.local", "additionaldomains.com", "saniam.com"]
        end
        it "parses san names when there are multiple non-SAN attributes" do
            csr = R509::Csr.new({ :csr => @csr4_multiple_attrs })
            csr.san_names.should == ["adomain.com", "anotherdomain.com", "justanexample.com"]
        end
        it "fetches a subject component" do
            csr = R509::Csr.new({ :csr => @csr })
            csr.subject_component('CN').to_s.should == 'test.local'
        end
        it "returns nil when subject component not found" do
            csr = R509::Csr.new({ :csr => @csr })
            csr.subject_component('OU').should be_nil
        end
        it "returns the signature algorithm" do
            csr = R509::Csr.new({ :csr => @csr })
            csr.signature_algorithm.should == 'sha1WithRSAEncryption'
        end
        it "returns RSA key algorithm for RSA CSR" do
            csr = R509::Csr.new({ :csr => @csr })
            csr.key_algorithm.should == 'RSA'
        end
        it "returns DSA key algorithm for DSA CSR" do
            csr = R509::Csr.new({ :csr => @csr_dsa })
            csr.key_algorithm.should == 'DSA'
        end
        it "returns the public key" do
            #this is more complex than it should have to be. diff versions of openssl
            #return subtly diff PEM encodings so we need to look at the modulus (n)
            #but beware, because n is not present for DSA certificates
            csr = R509::Csr.new({ :csr => @csr })
            csr.public_key.n.to_i.should == @csr_public_key_modulus.to_i
        end
        it "returns true with valid signature" do
            csr = R509::Csr.new({ :csr => @csr })
            csr.verify_signature.should == true
        end
        it "returns false on invalid signature" do
            csr = R509::Csr.new({ :csr => @csr_invalid_signature })
            csr.verify_signature.should == false
        end
        it "works when the CSR has unknown OIDs" do
            csr = R509::Csr.new(:csr => @csr_unknown_oid)
            csr.subject["1.2.3.4.5.6.7.8.9.8.7.6.5.4.3.2.1.0.0"].should == "random oid!"
            csr.subject["1.3.3.543.567.32.43.335.1.1.1"].should == "another random oid!"
        end
    end
    context "when supplying a key with csr" do
        it "raises exception on non-matching key" do
            expect { R509::Csr.new({:csr => @csr, :key => @key_csr2}) }.to raise_error(R509::R509Error, 'Key does not match request.')
        end
        it "accepts matching key" do
            csr = R509::Csr.new({:csr => @csr2, :key => @key_csr2})
            csr.to_pem.should == @csr2
        end
    end
    context "when setting alternate signature algorithms" do
        it "sets sha1 if you pass an invalid message digest" do
            csr = R509::Csr.new(:message_digest => 'sha88', :bit_strength => 512, :subject => [['CN','langui.sh']])
            csr.message_digest.name.should == 'sha1'
            csr.signature_algorithm.should == "sha1WithRSAEncryption"
        end
        it "sets sha256 properly" do
            csr = R509::Csr.new(:message_digest => 'sha256', :bit_strength => 512, :subject => [['CN','sha256-signature-alg.test']])
            csr.message_digest.name.should == 'sha256'
            csr.signature_algorithm.should == "sha256WithRSAEncryption"
        end
        it "sets sha512 properly" do
            csr = R509::Csr.new(:message_digest => 'sha512', :bit_strength => 1024, :subject => [['CN','sha512-signature-alg.test']])
            csr.message_digest.name.should == 'sha512'
            csr.signature_algorithm.should == "sha512WithRSAEncryption"
        end
        it "sets md5 properly" do
            csr = R509::Csr.new(:message_digest => 'md5', :bit_strength => 512, :subject => [['CN','md5-signature-alg.test']])
            csr.message_digest.name.should == 'md5'
            csr.signature_algorithm.should == "md5WithRSAEncryption"
        end
    end
    it "checks rsa?" do
        csr = R509::Csr.new({:csr => @csr})
        csr.rsa?.should == true
        csr.dsa?.should == false
    end
    it "gets RSA bit strength" do
        csr = R509::Csr.new({:csr => @csr})
        csr.bit_strength.should == 2048
    end
    it "checks dsa?" do
        csr = R509::Csr.new({:csr => @csr_dsa})
        csr.rsa?.should == false
        csr.dsa?.should == true
    end
    it "gets DSA bit strength" do
        csr = R509::Csr.new({:csr => @csr_dsa})
        csr.bit_strength.should == 1024
    end

end
