기고문2012. 10. 8. 11:37

본 내용은 월간 마이크로소프트웨어 2012년 12월호에 기고된 내용입니다 :)


--


전자세금계산서 제도는 사업자가 전자적 방법에 의해 세금계산서를 교부하고, 교부한 전자세금계산서를 정해진 기한 내 국세청에 전송하는 것으로서 종이세금계산서 사용에 따른 납세협력 비용을 줄이고, 거짓 세금계산서 발급 차단 등 세무거래 투명성 확보를 위해 지난 2010년부터 시행되었다. 전자세금계산서 발급은 법인사업자의 경우 2011년부터, 매출 10억 이상 개인사업자는 올해부터 의무화되었으며, 전송지연 또는 미전송 시 공급가액 0.1%~0.3%의 가산세가 부과되므로 사업자들의 면밀한 대비가 요구된다.



전자세금계산서 표준인증 


전자세금계산서를 발생하는 사용자는 자체적으로 시스템을 보유하고 발행하는 경우와 ASP(Application Service Provider) 서비스를 사용하는 경우로 구분될 수 있다. 자체적으로 사내의 ERP 시스템과 연동하는 전자의 경우와 전자세금계산서 ASP 사업자로 허가를 받기 위해서는 전자세금계산서 표준인증 과정을 성공적으로 통과하고 인증번호를 부여받아야만 한다. 전자세금계산서 ASP 사업자와 달리 ASP 사용자는 표준인증을 받을 필요가 없다. 전자세금계산서 표준인증은 정보통신산업진흥원 전자세금계산서 인증 시스템(http://www.taxcerti.or.kr/etax/)에 회원가입 후 법인 공인인증서 등록을 완료한 후에 검증 받을 수 있다. 


표준인증은 전자세금계산서 검증, 암호화된 전자세금계산서 검증, 전자세금계산서 제출 검증, 전자세금계산서 처리결과 응답 검증, 전자세금계산서 처리결과 요청 검증, 상호운용성 전자세금계산서 제출 및 처리결과 전송 종합 검증 그리고 상호운용성 전자세금계산서 결과 요청 검증 등 총 7개의 단계로 이루어져 있다. 모든 단계는 연속적으로 이루어져야하며 각 단계를 모두 성공해야 검증을 통과하게 된다. 따라서 각 단계 진행 중 하나라도 실패할 경우 검증은 통과하지 못하게 되며, 해당 단계에서 검증이 종료된다. 검증 실패 후 재 검증을 시도할 경우 1단계부터 다시 시작해야 한다. 


본 포스팅에서는 표준인증의 1단계인 전자세금계산서 전자서명 검증 부분만을 다루고 있으며, 표준인증 7단계에 대한 보다 상세한 정보는 정보통신산업진흥원 전자세금계산서 인증 시스템 게시판의 “전자세금계산서 개발지침 v1.0”을 통해 확인할 수 있다.




준비사항


표준인증의 1단계 전자세금계산서 전자서명 검증을 위해서는 먼저 JCE_Policy 파일을 패치해야 한다. JCE란 Java Cryptography Extension의 약자로서 자바 암호 구조(JCA : Java Cryptography Architecture)에서 지원하지 않는 대칭키 알고리즘과 암호화 구조를 위해 제공되는 확장 패키지이다. JDK 1.4.x부터 JCE가 기본적으로 포함이 되어 있는데 미국 수출 통상법에 따라 사용할 수 있는 키 길이 등에 제한이 걸려있다는 문제가 있다. 아래 그림과 같이 Oracle의 JAVA 다운로드 페이지에서 키 길이 등에 제한이 없는 정책파일을 다운로드 받고 압축을 해제하면 local_policy.jar, US_export_policy.jar 라는 2개의 파일이 있는데 이 파일들을 %JAVA_HOME%/lib/security 아래에 덮어씀으로써 문제를 해결할 수 있다. 만약 인증서의 비밀키 파일을 로드하는데 “There are bigger problems than just policy files: java.security.InvalidKeyException: Illegal key size or default parameters“와 같은 예외가 발생한다면 JCE_Policy 파일 패치가 제대로 이루어지지 않는 것이다.




전자세금계산서 전자문서 


전자세금계산서 XML 문서 예제는 다음와 같다.

<?xml version="1.0" encoding="UTF-8"?>
<TaxInvoice xsi:schemaLocation="urn:kr:or:kec:standard:Tax:ReusableAggregateBusinessInformationEntitySchemaModule:1:0 http://www.kec.or.kr/standard/Tax/TaxInvoiceSchemaModule_1.0.xsd" xmlns="urn:kr:or:kec:standard:Tax:ReusableAggregateBusinessInformationEntitySchemaModule:1:0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
  <ExchangedDocument>
    <ID>201208064100000100000001</ID>
    <IssueDateTime>20120806174653</IssueDateTime>
    <ReferencedDocument>
      <ID>201208064100000100000001</ID>
    </ReferencedDocument>
  </ExchangedDocument>
  <TaxInvoiceDocument>
    <IssueID>201208064100000100000001</IssueID>
    <TypeCode>0101</TypeCode>
    <IssueDateTime>20091005</IssueDateTime>
    <PurposeCode>01</PurposeCode>
  </TaxInvoiceDocument>
  <TaxInvoiceTradeSettlement>
    <InvoicerParty>
      <ID>2158757426</ID>
      <TypeCode>서비스</TypeCode>
      <NameText>(주)헬로월드</NameText>
      <ClassificationCode>정보처리</ClassificationCode>
      <SpecifiedPerson>
        <NameText>대표자명</NameText>
      </SpecifiedPerson>
      <DefinedContact>
        <DepartmentNameText>담당부서</DepartmentNameText>
        <PersonNameText>담당자이름</PersonNameText>
        <TelephoneCommunication>전화번호</TelephoneCommunication>
        <URICommunication>전자우편</URICommunication>
      </DefinedContact>
      <SpecifiedAddress>
        <LineOneText>서울시 강남구 삼성동</LineOneText>
      </SpecifiedAddress>
    </InvoicerParty>
    <InvoiceeParty>
      <ID>2158757426</ID>
      <TypeCode>서비스</TypeCode>
      <NameText>(주)헬로월드</NameText>
      <ClassificationCode>정보처리</ClassificationCode>
      <SpecifiedOrganization>
        <BusinessTypeCode>01</BusinessTypeCode>
      </SpecifiedOrganization>
      <SpecifiedPerson>
        <NameText>대표자명</NameText>
      </SpecifiedPerson>
      <PrimaryDefinedContact>
        <DepartmentNameText>담당부서</DepartmentNameText>
        <PersonNameText>담당자이름</PersonNameText>
        <TelephoneCommunication>전화번호</TelephoneCommunication>
        <URICommunication>전자우편</URICommunication>
      </PrimaryDefinedContact>
      <SecondaryDefinedContact>
        <DepartmentNameText>담당부서</DepartmentNameText>
        <PersonNameText>담당자이름</PersonNameText>
        <TelephoneCommunication>전화번호</TelephoneCommunication>
        <URICommunication>전자우편</URICommunication>
      </SecondaryDefinedContact>
      <SpecifiedAddress>
        <LineOneText>서울시 강남구 삼성동</LineOneText>
      </SpecifiedAddress>
    </InvoiceeParty>
    <SpecifiedPaymentMeans>
      <TypeCode>10</TypeCode>
      <PaidAmount>150000</PaidAmount>
    </SpecifiedPaymentMeans>
    <SpecifiedMonetarySummation>
      <ChargeTotalAmount>136364</ChargeTotalAmount>
      <TaxTotalAmount>13636</TaxTotalAmount>
      <GrandTotalAmount>150000</GrandTotalAmount>
    </SpecifiedMonetarySummation>
  </TaxInvoiceTradeSettlement>
  <TaxInvoiceTradeLineItem>
    <SequenceNumeric>01</SequenceNumeric>
    <InvoiceAmount>136364</InvoiceAmount>
    <ChargeableUnitQuantity>1</ChargeableUnitQuantity>
    <NameText>물품명</NameText>
    <PurchaseExpiryDateTime>20090928</PurchaseExpiryDateTime>
    <TotalTax>
      <CalculatedAmount>13636</CalculatedAmount>
    </TotalTax>
    <UnitPrice>
      <UnitAmount>150000</UnitAmount>
    </UnitPrice>
  </TaxInvoiceTradeLineItem>
</TaxInvoice>
각 항목은 위에서부터 순서대로 관리정보(ExchangedDocument), 기본정보(TaxInvoiceDocument), 계산서/거래처/결제 정보(TaxInvoiceTradeSettlement), 상품정보(TaxInvoiceTradeLineItem)를 담고 있다. 전자세금계산서에는 상당히 많은 양의 정보가 구체적으로 담기다 보니 XML문서가 크고 복합해 읽기 어려운 경향이 있는데, 여기서 눈여겨 볼 노드는 <TaxInvoiceDocument/Issue. ID>의 이다. 이 노드는 세금계산서 작성연월일 8자리 yyyymmdd, 국세청 등록번호 8자리 NNNNNNNN, 알파벳 소문자와 숫자 조합 8자리 ssssssss를 결합한 yyyymmddNNNNNNNNssssssss 형태의 24자리 문자열로서 전자세금계산서의 승인번호로 사용되며, 전자세금계산서를 식별할 수 있는 기본 키 이다.
위 예제 전자세금계산서 문서는 전자서명 부분을 포함하고 있지 않은데, 사업자 공인인증서를 통해 전자서명을 수행하면 관리정보(ExchangedDocument)와 기본정보(TaxInvoiceDocument) 사이에 전자서명 정보를 나타내는 <Signature> 노드가 추가된다. 전자서명을 수행할 때 주의해야 할 점은 전자세금계산서에서 공급사업자의 사업자번호를 의미하는 <InvoicerParty/ID>와 전자서명에 사용되는 사업자 공인인증서의 사업자번호가 일치해야 한다는 것이다. (전자세금계산서의 모든 항목에 대한 상세한 정의는 전자세금계산서 개발지침 v1.0의 부록A에서 확인할 수 있다.)


전자서명 

전자세금계산서의 법적 효력 발생을 위해서는 전자서명을 수행해야 한다. 전자서명의 방법은 W3C에서 권고하는 “XML Signature Syntax and Processing" 규약을 따른다. 사업자 및 국세청은 전자서명 시 법적 효력을 위해 공인인증기관에서 발급한 전자서명용 인증서만을 사용해야 하는 것에 주의한다. 전자서명의 전체 프로세스는 다음과 같다.


전자세금계산서 XML 문서는 가장먼저 정규화 과정을 거쳐야 한다. 왜냐하면 XML 문서가 논리적으로 동일하더라도 물리적으로는 띄어쓰기 방식이나 XML 주석, 문서 인코딩 방식 등의 차이로 인해 물리적으로는 다양하게 표현 가능하기 때문이다. 전자세금계산서 XML 문서의 전자서명은 XPath 필터링을 통해 전자서명에 사용되는 데이터를 전자세금계산서 XML 자체 내에서 추출하게 되는데, 논리적으로 동일하더라도 물리적으로 서로 다르게 표현된 XML 문서라면 전자서명 값이 서로 달라지므로 XML 문서의 물리적 표현 방식을 일정한 규칙 하에 통일하는 정규화 과정이 선행되어야 하는 것이다.

정규화 된 XML 문서는 전체 XML데이터 중에서 실제 서명 대상이 되는 데이터를 선택하기 위해 XPath 필터링을 수행한다. 하나의 전자서명 대상에 대해서 다양한 XPath 표현식이 존재할 수 있지만, 각 전자서명 모듈간 상호운용성 보장을 위해 아래 정의된 XPath 표현식만을 사용해야 한다.


위 과정을 통해 전자서명 대상 데이터를 추출하고, 해시(Digest)값을 생성한다. 해시 알고리즘으로는 공인인증서의 갱신/발급 기준일 에 따라 SHA-1 또는 SHA256를 사용한다. 생성 된 해시 값은 <ds:SignedInfo>노드 하위의 <ds:DigestValue>에 추가된다. 최종 전자서명은 <ds:SignedInfo>노드에 대해 해시값과 마찬가지로 공인인증서의 구분에 따라 전자서명 알고리즘 RSAWithSHA-1 또는 RSAWithSHA256를 통해 이루어진다. 전자서명값은 <ds:SignedInfo>노드 하위의 <ds:SignatureMethod>에 추가된다.

마지막으로 전자세금계산서 XML 문서를 수신하는 국세청에서 해당 전자서명의 유효성을 검증하기 위해 전자서명에 사용된 인증서가 필요하며 이에 따라 인증서 정보를 전자세금계산서 XML 문서에 추가해야 한다.

아래 소스코드는 전자세금계산서 전자서명 프로세스에 따라 Apache XML Security 라이브러리를 사용해 전자서명을 수행하는 Signer Class 이다.
import java.security.PrivateKey;
import java.security.cert.X509Certificate;

import javax.xml.XMLConstants;

import org.apache.xml.security.exceptions.XMLSecurityException;
import org.apache.xml.security.signature.XMLSignature;
import org.apache.xml.security.transforms.Transforms;
import org.w3c.dom.Document;
import org.w3c.dom.Element;
import org.w3c.dom.NodeList;

public class Signer {

	private Document document;
	private PrivateKey privateKey;
	private X509Certificate x509Cert;
	
	/**
	 * @param document 전자세금계산서 XML 문서
	 * @param privateKey PKCS#8 개인키
	 * @param x509Cert X.509 공인인증서
	 */
	public Signer(Document document, PrivateKey privateKey, X509Certificate x509Cert) {
		this.document = document;
		this.privateKey = privateKey;
		this.x509Cert = x509Cert;
	}

	public Document doSign() throws XMLSecurityException {
		// ROOT 노드
		NodeList nodeList = document.getElementsByTagName("TaxInvoice"); 
		Element root = (Element)nodeList.item(0);
		
		// SignatureMethodURI - the Signature method to be used.
		// CanonicalizationMethodURI - the canonicalization algorithm to be used to c14nize the SignedInfo element.
		XMLSignature sig = new XMLSignature(document, 
				XMLConstants.XMLNS_ATTRIBUTE_NS_URI,
				"http://www.w3.org/2000/09/xmldsig#rsa-sha1",
				"http://www.w3.org/TR/2001/REC-xml-c14n-20010315");
		
		// TaxInvoiceDocument
		nodeList = document.getElementsByTagName("TaxInvoiceDocument");
		Element taxInvoiceDocElement = (Element)nodeList.item(0);
				
		// ExchangedDocument와 TaxInvoiceDocument 사이에 Signature를 삽입
		root.insertBefore(sig.getElement(), taxInvoiceDocElement);
		
		Transforms transforms = new Transforms(document);
		
		// 정규화
		transforms.addTransform("http://www.w3.org/TR/2001/REC-xml-c14n-20010315");
		
		// XPath 필터링
		String xPath = "not(self::*[name() = 'TaxInvoice'] | ancestor-or-self::*[name() = 'ExchangedDocument'] | ancestor-or-self::ds:Signature)";
		Element xpath = document.createElementNS("http://www.w3.org/2000/09/xmldsig#", "XPath");
		xpath.appendChild(document.createTextNode(xPath));
		xpath.setPrefix("ds");
		
		transforms.addTransform("http://www.w3.org/TR/1999/REC-xpath-19991116", xpath);
		
		// referenceURI - URI according to the XML Signature specification.
		// trans - List of transformations to be applied.
		// digestURI - URI of the digest algorithm to be used.
		sig.addDocument("", transforms, "http://www.w3.org/2000/09/xmldsig#sha1");
		
		sig.addKeyInfo(x509Cert);
		sig.sign(privateKey); 
		
		return document;
	}

	
}

공인인증서의 종류에는 PEM, DER, PKCS#12 가 있는데 위 소스코드의 예제에서 사용되는 공인인증서는 DER (Distingulished Encoding Rules) 형식의 공인인증서이다. DER형식의 공인인증서는 *.der, *.key라는 2개의 파일이 있는데 der가 공인인증서이며 key는 개인키 파일이다. 공인인증서에서 공개키를 가져오는 것은 다음과 같이 간단하게 처리할 수 있다.

public static X509Certificate readX509Cert(String filePath) throws Exception {
	return (X509Certificate) 
		CertificateFactory.getInstance("X.509")
			.generateCertificate(
				FileUtils.openInputStream(new File(filePath)));
}
개인키를 가져오는 과정은 꽤 복합한데 이는 우리나라에서 사용되는 공인인증서는 개인키를 SEED 블록 암호화 알고리즘을 통해 암호화하기 때문에 이를 복호화해서 개인키를 구해야 하기 때문이다. 또한 SEED 블록 암호화 알고리즘에 사용되는 OID(Object IDentifier)는 초기벡터 IV 의 생성 방식에 따라 2가지로 구분되며 이를 모두 처리해줘야 한다. 
OID는 개인키 암호화에 사용된 알고리즘 식별자 이며, OID에 대한 자세한 정보는 http://www.oid-info.com/cgi-bin/display 에서 확인할 수 있다. 또한 SEED 블록 암호화에 대한 정의는 한국정보보호진흥원의 암호 알고리즘 규격 KCAC.TS.ENC를 참고하도록 한다.

다음은 *.key에서 암호화된 개인키를 가져오는 TrustCertAndKey class 이다.
// *.der 파일의 공인인증서는 *.key 형태의 개인키 파일이 따로 존재한다.
// *.key 파일에는 SEED 암호화 알고리즘을 통해 암호화된 개인키를 저장하고 있기 때문에 이를 복호화해서 개인키를 구해야 한다.
// 현재 우리나라 전자서명인증체계에서 SEED 블럭 암호화 알고리즘에 사용되는 OID는 초기 벡터(IV)의 생성 방식에 따라 2가지로 구분할 수 있다.
// 한국정보보호진흥원(http://www.rootca.or.kr/)의 암호 알고리즘 규격(KCAC.TS.ENC)
public static PrivateKey readPrivateKey(String filePath, String passwd) throws Exception {
	byte[] encodedKey = FileUtils.readFileToByteArray(new File(filePath));
	EncryptedPrivateKeyInfo epki = new EncryptedPrivateKeyInfo(encodedKey);
		
	String OID = epki.getAlgName();
		
	if( "1.2.410.200004.1.15".equals(OID) ) { // Key Generation with SHA1 and Encryption with SEED CBC mode
		// 추출키(DK) 생성
		// 8바이트의 salt와 이터레이션카운트 를 선탯하여 PBKDF1를 통해 DK를 생성한다.
		// salt는 공인 인증서의 21~28바이트 사이의 8바이트를 사용하며, 이터레이션 카운트는 31~32바이트의 2바이트이며 해쉬 함수의 반복 횟수를 나타낸다.
		byte[] salt = new byte[8];
		System.arraycopy(encodedKey, 20, salt, 0, 8);

		byte[] cBytes = new byte[4];
		System.arraycopy(encodedKey, 30, cBytes, 2, 2);
		ByteBuffer buffer = ByteBuffer.wrap(cBytes);
		buffer.order(ByteOrder.BIG_ENDIAN);
		
		int iterationCount = buffer.getInt();

		// Password Based Key Derivation Function, 패스워드 기반의 키 추출 함수
		// PBKDF1 RFC2898(http://www.ietf.org/rfc/rfc2898.txt) 참조
		byte[] derivedKey = new byte[20];
		MessageDigest md = MessageDigest.getInstance("SHA1", "BC");
		md.update(passwd.getBytes());
		md.update(salt);
		derivedKey = md.digest();
		for(int i=1; i<iterationCount; i++) {
			derivedKey = md.digest(derivedKey);
		}

		byte key[] = new byte[16];
		byte iv[] = new byte[16];
		byte ivTemp[] = new byte[4];
		byte derivedIV[] = new byte[20];
		
		// DK에서 처음 16바이트를 암호화 키(K)로 사용
	        System.arraycopy(derivedKey, 0, key, 0, 16);
	        
	        // DK에서 암호화 키(K)를 제외한 나머지 4바이트를 SHA-1으로 해쉬하여 20바이트의 값(DIV)를 생성하고, 그중 처음 16바이트를 초기 벡터(IV)로 사용
	        System.arraycopy(derivedKey, 16, ivTemp, 0, 4);
	        MessageDigest sha1 = MessageDigest.getInstance("SHA-1", "BC");
	        derivedIV = sha1.digest(ivTemp);
	        System.arraycopy(derivedIV, 0, iv, 0, 16);
	        
	        IvParameterSpec ivSpec = new IvParameterSpec(iv);
	        SecretKeySpec secKey = new SecretKeySpec(key, "SEED");
	        
	        Cipher cipher = Cipher.getInstance("SEED/CBC/PKCS5Padding", "BC");
	        cipher.init(Cipher.DECRYPT_MODE , secKey, ivSpec);
	        
	        byte[] decryptedKey = cipher.doFinal(epki.getEncryptedData());
	        
	        PKCS8EncodedKeySpec ks = new PKCS8EncodedKeySpec(decryptedKey);
		KeyFactory kf = KeyFactory.getInstance("RSA", "BC");
		return kf.generatePrivate(ks);
	}
	else if( "1.2.410.200004.1.4".equals(OID) ) { // SEED Encryption (CBC mode) 
		byte[] salt = new byte[8];
		System.arraycopy(encodedKey, 20, salt, 0, 8);

		byte[] cBytes = new byte[4];
		System.arraycopy(encodedKey, 30, cBytes, 2, 2);
		ByteBuffer buffer = ByteBuffer.wrap(cBytes);
		buffer.order(ByteOrder.BIG_ENDIAN);
		
		int iterationCount = buffer.getInt();
			
		byte[] derivedKey = new byte[20];
		MessageDigest md = MessageDigest.getInstance("SHA1", "BC");
		md.update(passwd.getBytes());
		md.update(salt);
		derivedKey = md.digest();
		for(int i=1; i<iterationCount; i++) {
			derivedKey = md.digest(derivedKey);
		}
			
		byte key[] = new byte[16];
		System.arraycopy(derivedKey, 0, key, 0, 16);
			
		// 추출키(DK) 와 상관없이 16 바이트의 초기 벡터(IV) 는 아래와 같은값 으로 표현 으로 고정하여 사용한다.
		// IV 문자열 값은 “0123456789012345”
		byte iv[] = { 48, 49, 50, 51, 52, 53, 54, 55, 56, 57, 48, 49, 50, 51, 52, 53 };
			
		IvParameterSpec ivSpec = new IvParameterSpec(iv);
        	SecretKeySpec secKey = new SecretKeySpec(key, "SEED");
	        
        	Cipher cipher = Cipher.getInstance("SEED/CBC/PKCS5Padding", "BC");
        	cipher.init(Cipher.DECRYPT_MODE , secKey, ivSpec);
	        
        	byte[] decryptedKey = cipher.doFinal(epki.getEncryptedData());
	        
        	PKCS8EncodedKeySpec ks = new PKCS8EncodedKeySpec(decryptedKey);
		KeyFactory kf = KeyFactory.getInstance("RSA", "BC");
		return kf.generatePrivate(ks);
	}
		
	throw new Exception("not support OID: "+OID);
}

자 이제 공인인증서 파일, 개인키 파일, 개인키 비밀번호, 전자세금계산서 XML 파일, 전자서명 된 전자세금계산서 XML 파일 출력 경로를 프로그램 실행 인자로 하는 main 함수의 구현은 다음과 같이 작성한다. 각종 암복호화 알고리즘을 제공해주는 BouncyCastle 라이브러리 추가와 Apache XML Security를 초기화 하는 것에 주의한다.

import java.io.File;
import java.io.FileOutputStream;
import java.security.PrivateKey;
import java.security.Security;
import java.security.cert.X509Certificate;

import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;

import org.apache.commons.cli.CommandLine;
import org.apache.commons.cli.CommandLineParser;
import org.apache.commons.cli.HelpFormatter;
import org.apache.commons.cli.Options;
import org.apache.commons.cli.PosixParser;
import org.apache.log4j.Logger;
import org.apache.xml.security.utils.XMLUtils;
import org.bouncycastle.jce.provider.BouncyCastleProvider;
import org.w3c.dom.Document;

import com.hellowd.etax.Signer;
import com.hellowd.etax.TrustCertAndKey;

public class TaxInvoiceSignerWrapper {

	public static Logger logger = Logger.getLogger(TaxInvoiceSignerWrapper.class);
	
	public static Document readXml(File source) throws Exception {
		DocumentBuilderFactory dbf = DocumentBuilderFactory.newInstance();
		dbf.setNamespaceAware(true);
		DocumentBuilder db = dbf.newDocumentBuilder();
		Document document = db.parse(source);
		return document;
	}
	
	public static void main(String[] args) throws Exception {
		Options options = new Options();
		options.addOption("d", "der", true, "der file");// DER암호화 인증서 파일
		options.addOption("k", "key", true, "private key file");	// 인증서PKCS#8 Key
		options.addOption("p", "passwd", true, "key password"); // 인증서 비밀번호
		options.addOption("s", "source", true, "tax invoice xml file");
		options.addOption("o", "out", true, "signed tax invoice xml file");
		
		CommandLineParser parser = new PosixParser();
		CommandLine cmd = parser.parse(options, args);
		
		if( !cmd.hasOption("d") || !cmd.hasOption("k") || !cmd.hasOption("p") ) {
			HelpFormatter formatter = new HelpFormatter();
			formatter.printHelp("Help...", options);
			return;
		}
		
		String derFile = cmd.getOptionValue("d");
		String keyFile = cmd.getOptionValue("k");
		String passwd = cmd.getOptionValue("p");
		String source = cmd.getOptionValue("s");
		String output = cmd.getOptionValue("o");
		
		logger.debug("Der File Path: "+derFile);
		logger.debug("Key File Path: "+keyFile);
		logger.debug("Key Password: "+passwd);
		logger.debug("taxinvoice xml file: "+source);
		logger.debug("signed taxinvoice xml file: "+output);
		
		// BouncyCastle을 프로바이더로 추가
		Security.addProvider(new BouncyCastleProvider());
		// Apaceh Xml Security 초기화
		org.apache.xml.security.Init.init();
		
		X509Certificate x509Cert = TrustCertAndKey.readX509Cert(derFile);
		PrivateKey privateKey = TrustCertAndKey.readPrivateKey(keyFile, passwd);
		
		logger.debug(x509Cert);
		logger.debug(x509Cert.getSigAlgOID()); // 1.2.840.113549.1.1.5(RSA (PKCS #1 v1.5) with SHA-1 signature)
		
		logger.debug(privateKey);
		
		Document document = readXml(new File(source));
		
		Signer signer = new Signer(document, privateKey, x509Cert);
		signer.doSign();
		
		FileOutputStream os = new FileOutputStream(new File(output));
		XMLUtils.outputDOM(document, os);
	}
}

전자서명 검증

전자서명 된 전자세금계산서 XML 문서 예제는 다음과 같다.
<TaxInvoice xmlns="urn:kr:or:kec:standard:Tax:ReusableAggregateBusinessInformationEntitySchemaModule:1:0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="urn:kr:or:kec:standard:Tax:ReusableAggregateBusinessInformationEntitySchemaModule:1:0 http://www.kec.or.kr/standard/Tax/TaxInvoiceSchemaModule_1.0.xsd">
  <ExchangedDocument>
    <ID>201208064100000100000001</ID>
    <IssueDateTime>20120806174653</IssueDateTime>
    <ReferencedDocument>
      <ID>201208064100000100000001</ID>
    </ReferencedDocument>
  </ExchangedDocument>
  <ds:Signature xmlns:ds="http://www.w3.org/2000/09/xmldsig#">
<ds:SignedInfo>
<ds:CanonicalizationMethod Algorithm="http://www.w3.org/TR/2001/REC-xml-c14n-20010315"></ds:CanonicalizationMethod>
<ds:SignatureMethod Algorithm="http://www.w3.org/2000/09/xmldsig#rsa-sha1"></ds:SignatureMethod>
<ds:Reference URI="">
<ds:Transforms>
<ds:Transform Algorithm="http://www.w3.org/TR/2001/REC-xml-c14n-20010315"></ds:Transform>
<ds:Transform Algorithm="http://www.w3.org/TR/1999/REC-xpath-19991116">
<ds:XPath>not(self::*[name() = 'TaxInvoice'] | ancestor-or-self::*[name() = 'ExchangedDocument'] | ancestor-or-self::ds:Signature)</ds:XPath>
</ds:Transform>
</ds:Transforms>
<ds:DigestMethod Algorithm="http://www.w3.org/2000/09/xmldsig#sha1"></ds:DigestMethod>
<ds:DigestValue>S8F6mYpe6roBxGzZRZlqyOtKjjk=</ds:DigestValue>
</ds:Reference>
</ds:SignedInfo>
<ds:SignatureValue>
MMiM0czp9gOJRu+l1L7wxkvGKcGcC5CzFKwz2PWbW4s86kkv2hm2XtzXH5qG5XvI3fcxxG/3M8nr
ndYu9oOq4YMHRRS7exWhTAsfMZGaS2eUh7T+emI4GZh2xlBXPFP6Dlor+HUL+JNxc0WyhCZNlrSm
JM6k+GkYXGCmg/Z7QRM=
</ds:SignatureValue>
<ds:KeyInfo>
<ds:X509Data>
<ds:X509Certificate>
MIIFIzCCBAugAwIBAgIEDrFIfjANBgkqhkiG9w0BAQUFADBKMQswCQYDVQQGEwJrcjEQMA4GA1UE
CgwHeWVzc2lnbjEVMBMGA1UECwwMQWNjcmVkaXRlZENBMRIwEAYDVQQDDAl5ZXNzaWduQ0EwHhcN
MTEwODMxMTUwMDAwWhcNMTIwOTAxMTQ1OTU5WjCBizELMAkGA1UEBhMCa3IxEDAOBgNVBAoMB3ll
c3NpZ24xEzARBgNVBAsMCnhVc2U0RXNlcm8xDDAKBgNVBAsMA0lCSzEOMAwGA1UECwwFMjY4NDQx
NzA1BgNVBAMMLijso7wp7Zes66Gc7JuU65OcKDI2ODQ0KTAwMDM2ODIyMDExMDkwMTA5MzkxNDAw
gZ8wDQYJKoZIhvcNAQEBBQADgY0AMIGJAoGBANngCizCg4RYkgb9Leio4tn0OIj7kjZ461xbrPOS
exAufiULx9gxBdymWegsrrz8gof7lY1udZKn852aZNmdm1wxSGnqeA9OvZDiQ7cJB2Hj0vb103C1
nIqmpVgHm0D5gD8+aCu6xHHC9JcEv5U5L43VjgQgN2Jv78YQ325FJLo1AgMBAAGjggJRMIICTTCB
jwYDVR0jBIGHMIGEgBRK+70zLYux0YyUa//gQjZfHJHLCKFopGYwZDELMAkGA1UEBhMCS1IxDTAL
BgNVBAoMBEtJU0ExLjAsBgNVBAsMJUtvcmVhIENlcnRpZmljYXRpb24gQXV0aG9yaXR5IENlbnRy
YWwxFjAUBgNVBAMMDUtJU0EgUm9vdENBIDGCAidgMB0GA1UdDgQWBBS++Ex9zPIVCl5hlf3dqEfD
VVQTnzAOBgNVHQ8BAf8EBAMCBsAwegYDVR0gAQH/BHAwbjBsBgoqgxqMmkUBAQYIMF4wLgYIKwYB
BQUHAgIwIh4gx3QAIMd4yZ3BHLKUACCs9cd4x3jJncEcACDHhbLIsuQwLAYIKwYBBQUHAgEWIGh0
dHA6Ly93d3cueWVzc2lnbi5vci5rci9jcHMuaHRtMGAGA1UdEQRZMFegVQYJKoMajJpECgEBoEgw
RgwRKOyjvCntl6zroZzsm5Trk5wwMTAvBgoqgxqMmkQKAQEBMCEwBwYFKw4DAhqgFgQUdai0Vxm/
JQUlCgBHXM62/dmCvnkwcgYDVR0fBGswaTBnoGWgY4ZhbGRhcDovL2RzLnllc3NpZ24ub3Iua3I6
Mzg5L291PWRwM3A2MjgwOSxvdT1BY2NyZWRpdGVkQ0Esbz15ZXNzaWduLGM9a3I/Y2VydGlmaWNh
dGVSZXZvY2F0aW9uTGlzdDA4BggrBgEFBQcBAQQsMCowKAYIKwYBBQUHMAGGHGh0dHA6Ly9vY3Nw
Lnllc3NpZ24ub3JnOjQ2MTIwDQYJKoZIhvcNAQEFBQADggEBAGwj1RRBqQgU27sO9lCUHce2TOs1
76n0hn5oxrwgyUE/tEqm+UjOvbAX5M1/10oICzze4InfHIuuctgDuGDOP3AN/7iTcVXicBX1r/yQ
pfm4Sr9aawN8MajL5ERhzZXJ9VULo4wZrCR5+ClkthbbqLQvebPax/u5WNgoq98b+/T6fuaCepkx
KswyncT6RUYj06xDydp9dxbsiaJdZKuEvRRyIuJ7ubFuBKRNNLxKT5FCImH4GwsLpNYe63KGX8Ag
+EWgkptQbSximgR8BGDWR0YzQCj1I6lANd99vtr6YNETQo9zyeryilDE7jaRjeo0j8uRZ1tKhqrn
X3IueQP15VM=
</ds:X509Certificate>
</ds:X509Data>
</ds:KeyInfo>
</ds:Signature><TaxInvoiceDocument>
    <IssueID>201208064100000100000001</IssueID>
    <TypeCode>0101</TypeCode>
    <IssueDateTime>20091005</IssueDateTime>
    <PurposeCode>01</PurposeCode>
  </TaxInvoiceDocument>
  <TaxInvoiceTradeSettlement>
    <InvoicerParty>
      <ID>2158757426</ID>
      <TypeCode>서비스</TypeCode>
      <NameText>(주)헬로월드</NameText>
      <ClassificationCode>정보처리</ClassificationCode>
      <SpecifiedPerson>
        <NameText>대표자명</NameText>
      </SpecifiedPerson>
      <DefinedContact>
        <DepartmentNameText>담당부서</DepartmentNameText>
        <PersonNameText>담당자이름</PersonNameText>
        <TelephoneCommunication>전화번호</TelephoneCommunication>
        <URICommunication>전자우편</URICommunication>
      </DefinedContact>
      <SpecifiedAddress>
        <LineOneText>서울시 강남구 삼성동</LineOneText>
      </SpecifiedAddress>
    </InvoicerParty>
    <InvoiceeParty>
      <ID>2158757426</ID>
      <TypeCode>서비스</TypeCode>
      <NameText>(주)헬로월드</NameText>
      <ClassificationCode>정보처리</ClassificationCode>
      <SpecifiedOrganization>
        <BusinessTypeCode>01</BusinessTypeCode>
      </SpecifiedOrganization>
      <SpecifiedPerson>
        <NameText>대표자명</NameText>
      </SpecifiedPerson>
      <PrimaryDefinedContact>
        <DepartmentNameText>담당부서</DepartmentNameText>
        <PersonNameText>담당자이름</PersonNameText>
        <TelephoneCommunication>전화번호</TelephoneCommunication>
        <URICommunication>전자우편</URICommunication>
      </PrimaryDefinedContact>
      <SecondaryDefinedContact>
        <DepartmentNameText>담당부서</DepartmentNameText>
        <PersonNameText>담당자이름</PersonNameText>
        <TelephoneCommunication>전화번호</TelephoneCommunication>
        <URICommunication>전자우편</URICommunication>
      </SecondaryDefinedContact>
      <SpecifiedAddress>
        <LineOneText>서울시 강남구 삼성동</LineOneText>
      </SpecifiedAddress>
    </InvoiceeParty>
    <SpecifiedPaymentMeans>
      <TypeCode>10</TypeCode>
      <PaidAmount>150000</PaidAmount>
    </SpecifiedPaymentMeans>
    <SpecifiedMonetarySummation>
      <ChargeTotalAmount>136364</ChargeTotalAmount>
      <TaxTotalAmount>13636</TaxTotalAmount>
      <GrandTotalAmount>150000</GrandTotalAmount>
    </SpecifiedMonetarySummation>
  </TaxInvoiceTradeSettlement>
  <TaxInvoiceTradeLineItem>
    <SequenceNumeric>01</SequenceNumeric>
    <InvoiceAmount>136364</InvoiceAmount>
    <ChargeableUnitQuantity>1</ChargeableUnitQuantity>
    <NameText>물품명</NameText>
    <PurchaseExpiryDateTime>20090928</PurchaseExpiryDateTime>
    <TotalTax>
      <CalculatedAmount>13636</CalculatedAmount>
    </TotalTax>
    <UnitPrice>
      <UnitAmount>150000</UnitAmount>
    </UnitPrice>
  </TaxInvoiceTradeLineItem>
</TaxInvoice>

전자서명 된 전자세금계산서 XML 문서의 유효성을 확인하기 위해 앞서 언급한 정보통신산업진흥원 전자세금계산서 인증 시스템(http://www.taxcerti.or.kr/etax/)에서 전자세금계산서 단위 검증을 수행해 볼 수 있다. 전자서명 과정에 문제가 없다면 다음과 같이 모든 테스트 케이스를 성공한 것을 확인할 수 있다. (단위검증은 회원가입 후 법인 공인인증서 등록을 하지 않아도 수행해 볼 수 있다.)



* 첨부

1. taxInvoiceSigner.zip 예제 소스 코드

2. 전자세금계산서 예제 TaxInvoice.xml

3. 전자서명 된 전자세금계산서 예제 TaxInvoice_Signed.xml


taxInvoiceSigner.zip


taxInvoice.xml


taxInvoice_signed.xml



* 참고문헌 및 자료

1. 전자세금계산서 개발지침 v1.0

2. 한국인터넷진흥원 암호 알고리즘 규격 KCAC.TS.ENC v1.21

3. 공인인증서로 전자서명 해보기 http://blog.kangwoo.kr/49

4. Apache XML Security http://santuario.apache.org/

5. BouncyCastle http://www.bouncycastle.org/

6. PBKDF1 RFC2898 http://www.ietf.org/rfc/rfc2898.txt

Posted by devop

댓글을 달아 주세요