7장부터.java


7장부터.doc


챕터7 암호화 프로젝트 관리

안전한 개발 방법을 거의 이해하지 못하거나 준수하지 못하여 암호화 시스템의 취약점은 노출된다.

보안 강화 프로젝트는 다른 소프트웨어 프로젝트처럼 진행되어야 한다.

이 작업의 프로젝트 관리자의 최선책은 개발팀에 보안 문화(관행)을 조성하는 것이다.


7.1 보안 문화(culture)

보안에는 우선순위를 정하는 문화가 필요하다.

소프트웨어의 수명주기에 걸쳐 산출된 사건들을 제공함으로써 이 과정들은 보안문제를 해결하기 위한 로드맵을 제공한다.

아무도 자신의 시스템이 다른 팀을 위해 연구되고 싶어하지 않는다.

장기적으로 보안 문화를 개발하는것은 고객에게 부가적인 가치를 제공할 수 있는 기회로 보여진다.

고객이 보안을 강조하는 경우 팀의 지속적인 관점에서 보안을 유지하도록 하는것이 가장 좋다.

프로젝트 관리자는 그러한 기능을 수행함으로서 고객에게 더 많은 보안을 배려할 수 있도록 한 점을 확인해야 한다.


7.2 고객 참여

보안 관련 전문가로써 우리는 고객이 중요하게 여기는 해당 응용 프로그램에 적절한 보안을 제공하고 싶다.

간혹 고객으로부터 보상을 받을 수 없는 보안 기능의 구현으로 인해 시간이 소비되는 부분을 고객에게 납득시켜야 한다.

팀의 보안 문화에 포함된 고객은 프로젝트 관리자의 의제에 포함되어야 한다.

법률과 규정은 관리자의 역할로서 이 책의 첫번째 부분에 다뤄진다.

법의 준수는 일반적으로 고객의 책임이므로 이러한 요구사항은 보안 의식 수준을 높이는데 도움을 준다.

단점은 이러한 요구사항은 종종 고객에게 방해물로 여겨지며 구체적인 요구사항보다는 시스템이 정말 안전하다는 보장을 받는것에 초점을 맞춘다.

고객과 처음부터 함께 작업할때 우리의 목표는 얼마나 많은 보안 위협이 존재하는지를 이해시키며 고객들이 보안 기능을 받아들이도록 한다.

이상적으로 내부 정보 보안팀은 필요한 보안 전문 지식을 제공함으로서 고객을 위한 대행자 역할을 하는것이다.

이러한 경우에 노련한 정보 보안팀은 허용되는 위험과 그렇지 않은것을 고객과 상의해야 한다.

특히 팀은 응용 프로그램 보안에 고객에 컨설턴트를 제공하면서 허용 가능한 수준의 위험 레벨까지 줄이기 위한 완화 전략을 제공해야 한다.

정보 보안팀이 제대로 적용될 수 없는 경우 개발팀이 고객의 위험 수용 정도를 고려하여 결정한다.

먼저 위협이 무엇인지를 평가하고 적절한 완화 방법을 결정한다.

일반적인 고객이 주의하는 위험을 살펴보면 :

-접근 거부, 비인가 접근, 비인가 수정


8.요구사항을 타이트하게 분석하기


9.디자인 강화

디자인을 강화하는 것은 요구사항을 완벽하게 반영해나가는 과정이라고 할 수 있다.

디자인 시작단계에서 요구사항을 분석하고 확인한 후 세부 개발 과정에서 보완해나간다.

디자인 강화를 위한 세가지 접근방식이 있는데

가이드 라인에 의존하기, 보완원칙에 기반하여 팀 작업을 수행하기, 경험과 최선의 예제를 선별하기이다.

불행하게도 시스템에 대한 위협에 대해 엄격한 분석이 이뤄지지 않기 때문에 이러한 접근 방식은 중요한 컨트롤을 놓칠 가능성이 있다.

위협모델은 이러한 시스템의 취약점에 대응하는데에 사용되어진다.

위협모델은 시스템의 나머지 부분에서도 참조되어지므로 개발 고정에서 가장 중요한 과정이다.

보안 패턴은 팀이 특정 위협에 가장 적합한 컨트롤을 적용하도록 개발하고 전체 개발팀은 다양한 환경에 적용되는 보안패턴을 저장할 수 있도록 해야 한다.

마지막으로 암호화와 빠른 검색은 서로 아이러니한 관계를 지니고 있다.

암호화된 데이터는 검색되지 않도록 설계하는것이 가장 최선이나 검색기능이 필요한 경우 검색 가능한 행의 범위를 최대한 줄이고 민감하지 않은 정보만을 선별해야 한다.

암호화된 데이터를 감시할 수 있고 검색에 해독되는 행의 수를 제한할 프로필을 사용한다.


10. 보안 개발

개발은 디자인에 따라 응용 프로그램에서 실행되는 모든 사항에 대해서 다룬다.

데이터베이스의 암호화를 구축하는 개발팀은 철저하게 해당 기술의 보안적 의미를 이해하는데 시간이 필요하다.

여기서 제시한 지침은 발견, 검색등의 프로세스를 형성할 수는 있지만

결국 각각의 기술과 언어는 팀원들이 제품의 일관성을 보장하기 위해 규격화된 문서의 집합을 가져야 한다.

개발팀이 사용하고자하는 플랫폼이나 언어는 특정되어져 있다.

조직의 ERP(전자지원관리)나 고객 관계 관리 플랫폼은 자바와 C가 별개의 가이드라인을 갖는다.

두가지 가이드 라인으로 "어떻게" 와 "해야 되는것/하지 말아야 되는것"으로 분류할 수 있다.

"How to"가이드라인은 권한체크, 경로 설정, SQL 쿼리문 생성등과 같은 일반적인 시나리오를 해결하는 방법이다.

접근제어와 암호화를 위한 인프라가 표준화되어 있는 경우 지침을 가장 잘 사용하기 위한 지침도 포함시켜야 한다.

"Should/Should not"방법은 코딩 및 구성 기준을 제시한다.

"객체 멤버가 private으로 분명하게 존재"하지 않는지 항상 마크하고 "키를 절대 문자열로 표기금지"등과 같은 가이드를 제시한다.

명명 규칙과 같은 구성은 항상 강력한 권한을 갖는 박스를 사용하는가 등등을 알 수 있도록 지침을 사용한다.


11.테스트

디자인 강화를 통해 기능테스티 시 고찰해야 할 사항으로

-접근제어

-데이터의 무결성

-로깅과 모니터링

-일반적인 위협

-정보의 기밀성


특정 상황에서는 테스트를 할 수 없는 여건일 경우가 있다.

데이터나 메모리에서 지워졌거나 보안의 취약점이 드러난 API를 테스트에 사용할 수 없다.

응용 프로그램의 사양이 조건을 충족하는지 확인하려면 테스트시 수동,자동으로 혼합된 검사에 의존할 필요가 있다.

검사는 개발시 사용한 동일한 지침을 따르도록 한다.

이러한 테스트시 추가적인 툴이나 기술이 필요해지므로 보다 많은 개발시간이 소요된다.

개발자에게 어떤 툴이 사용되었고, 테스트 동안 사용되거나 변경된 내용은 무엇이며 , 스스로 결함을 발견하고 수정할 수 있도록 해줘야 한다.


-침투 테스트

침투 테스트를 통해 사전에 지정된 통제에 의해 정확히 작동하는지 확인해야 한다.

이러한 기능 테스트를 통해 소프트웨어의 결함을 발견하도록 한다.


보안도구의 우선순위를 결정하기 위해 결함 매트릭스를 이용한다.(p149)

Y축은 얼마나 악용이 쉬운지를, X측은 이를 해결하는것이 쉬운지를 나타내는 척도이다.

각각의 요소들은 위협모델에 근간하여 위치하였다.

주의깊게 봐야할 구간은 2,3사분면 구간이다.

쉽게 완화되는 위협이라고 해서 더 심각한 위협보다 우선순위가 높아지는것은 좋지 않다.

1사분면에서 위험도에 근거하여 A,B.. 그 외의 순서로 처리되는 것이 옳다.

취약점의 우선순위를 상의한 후 우선순위 완화를 위해 예약 프로세스를 만든다.

침투 테스트는 키 매니져를 비롯하여 고객, 키 창고와 엔진을 대상으로 실시한다.

타사의 HSM을 사용하는 경우에도 예외일 수는 없다.


테스트는 흔히 기능 테스트와 침투 테스트로 나뉜다고 볼 수 있다.

위협모델은 테스트에 사용될 훌륭한 소스를 제공해 줄 것이다.

프로젝트를 마감할때 취약점에 대한 우선순위를 고객에게 공개하도록 한다.

고객은 각각의 사항을 평가하고 스케쥴이나 우선순위를 수정하기도 한다.


12.배포, 방어, 폐기

응용 프로그램의 출시에 앞서 배포, 방어, 폐기는 종종 괄시되곤 한다.

보완 관점에서 배포된 프로그램의 수명주기를 주목할 필요가 있다.

네트워크와 더불어 꾸준히 침입시도를 모니터링 해야 한다.

배포 및 폐기 단계에서 주기적으로 대두되는 취약점에 대해 검증과 감시가 필요하다.

해당 응용프로그램을 폐기할때는 철저하게 민감한 데이터가 같이 삭제되어야 한다.


예제코드

13.예제에 대해

앞서 설명한 설계 및 실제 방법론을 구현한다.

이 코드는 자바 1.4.2를 기반으로 구성되었으며 바닐라SQL과 MS-SQL에서 테스트했다.

암호화 API를 사용하는 라이브러리의 기능에 따라 몇가지 다른 언어로 변환이 어려울 수도 있다.

여기 포함된 자바 패키지는 cryptodb의 cryptodb패키지를 사용했다.

핵심패키지는 키가 절대로 다른 곳으로 가지 않는 가상 HSM처럼 구성되었다.

바이트로 구성된 키는 코어 public접근이어도 코어 메소드 외에서 참조되려 할때 0을 리턴한다.

메모리에 남이 있는 키 데이터를 보호하기에 효과적이다.

예를 들어 키가 삭제되도 엔진 내부에서 키 데이터를 사용하기 위해 키 데이터 배열을 복제할 가능성이 있다.

참조하고자 하는 배열이 엔진의 배열과 동일한 메모리를 가르키기 때문이다.

0으로 만드는 작업을 통해 배열의 내부를 비워야한다.

예제는 지금까지 논의한 다양한 구성 요소의 기본 기능을 구현하는 방법을 보여준다.

예외처리 및 로깅 기능은 모든 상용화 프로그램에 포함되어야 한다.

이 예제들은 16진수화된 암호화된 데이터와 키를 2진 데이터로 변환하여 데이터베이스에 저장한다.


데이터 변환코드는 util클래스에 있다.

bytes2HexString 은 배열을 문자열로 바꿔주는 기능을 구현한다.


final public class Utils{

public static String bytes2HexString(byte[] theBytes){

StringBuffer hexString = new StringBuffer();

String convertedByte="";


for(int i=0; i<theBytes.length; i++){

convertedByte = Integer.toHexString(theBytes[i]);

if(convertedByte.length()<2){

convertedByte="00".substring(convertedByte.length()) + convertedByte;

}

else if(convertedByte.length()>2){

convertedByte = convertedByte.substring(convertedByte.length()-2);

}

hexString.append(convertedByte.toUpperCase());

}

return hexString.toString();

}//bytes2HexString

};


이번에는 0102 문자열을 다시 배열로 만드는 메소드를 구성하겠다.

public static byte[] hexSting2Byte(String hexString){

byte[] theBytes = new byte[hexString.length()/2];

byte leftHalf = 0x0;

byte rightHalf = 0x0;

for(int i=0, j=0; i<hexString.length()/2; i++, j=i*2){

rightHalf = (byte)(Byte.parseByte(hexString.substring(j+1, j+2),16)&(byte)0xf);

leftHalf = (byte)((Byte.parseByte(hexString.substring(j, j+1),16)<<4)&(byte)0xf0);

theBytes[i] = (byte)(leftHalf|rightHalf);

return theBytes;

}

}


p162 conn객체로 DB연동하는건 생략하도록 하겠다.


14.키 창고

(p165)

key_id, key_data, kek_id를 갖는 "Local_Key_Store"테이블을 하나 생성한다. key_id는 프라이머리키이다.

키 스토어에서 키를 식벽하기 위해 사용된다.

"Key-encrypting-Keys"는 다른 테이블로 생성한다.

activation_data는 키를 암호화 키가 사용가능한 상태가 된 시간을 정의한다.


package cryptodb.com;

public class LocalKey {

private String keyData = null;

private byte[] kek = null;

private String keyID= null;

private String kekID = null;

private byte[] rawKey = null;

public LocalKey(String keyID, String keyData, byte[] kek){

this.kekID = keyID;

this.keyData = keyData;

this.kek = kek;

}

public LocalKey(){}

}


로컬키가 생성되면 암호화, 복호화를 수행하지만 내부에서 암호화 작업을 하려면 먼저 복호키가 필요하다(?)

(p166)

암호 복호키는 "Key-encrypting-Keys"에서 "ScecretKeySpect"를 작성한다.(내부 메소드)

cipher 객체들은 실질적인 암호화 기능을 수행한다.

예제에서 ECB모드에서 AES알고리즘을 사용하면서 패딩을 사용하지 않도록 하겠다.

서로 다른 키를 사용하여 본질적으로 랜덤값을 만들 예정이고, 데이터의 크기도 딱 128bit라 패딩은 필요없다.

cipher는 복호화를 하기 위해 "Key-encrypting-Keys"의 "ScecretKeySpect"로 초기화 된 후

다시 복호화할 것이다.


final private byte[] decryptKey(){

SecretKeySpec key = null;

}

try{

SecretKeySpec kekSpec = new SecretKeySpec(kek, "AES");

Cipher cipher = Cipher.getInstance("AES/ECB/NoPadding");

cipher.init(Cipher.DECRYPT_MODE, kekSpec);

rawKey = cipher.doFinal(/*getKeyBytes()*/);

}

catch(NoSuchAlgorithmException nase){}

catch(NoSuchPaddingException nspe){}

catch(InvalidKeyException ike){}

catch(BadPaddingException bpe){}

return rawKey;


SecretKeySpec은 보는바와 같이 Cipher 객체를 필요로 한다.

getSecretKeySpec 메소드를 통해 LocalKey에 붙은 SecretKeySpec의 버젼을 자체적으로 리턴한다.

그러나 이 메소드는 코어 밖에서는 호출되지 않는다.


final SecretKeySpec getSecretKeySpec(){

byte[] = rawKey = decryptKey();

SecretKeySpec key = new SecretKeySpec(rawKey, "AES");

return key;

}


로컬키 객체는 더이상 암호화 작업을 수행할 필요가 없으므로 "실제 키를 0으로" 만든다.

final public void wipe(){

wipeRawKey();

wipeKek();

}

final public void wipeRawKey(){

if(rawKey != null){

for(int i=0; i<rawKey.length; i++){

rawKey[i] = (byte)0x00;

}

}

rawKey = null;

}

final public void wipeKek(){

if(kek != null){

for(int i=0; i<kek.length; i++){

kek[i] = (byte)0x00;

}

}

kek = null;

}


"Key-encrypting-Key" 생성하기.

매개변수 없이 새로 생성된 Key-encrypting-Key 의 id를 리턴한다.

public 메소드이며 새로운 SHA1PRNG로 가 필요할때 코어의 외부에서 호출된다.


final public String generateNewKey(){

byte[] rawKey = new byte[16];

String kekId = null;

PreparedStatement pstmt = null;


try

//SHA1PRNG로 Key-encrypting-Key를 생성하는 난수를 발생

SecureRandom kekGenerator = SecureRandom.getInstance("SHA1PRNG");

kekGenerator.nextBytes(rawKey);

}

/*

새롭게 생성된 Key-encrypting-Key 는 데이터베이스에 삽입되고 생성된 ID는 자동으로 키ID로 반환된다.

키의 활성화 날짜는 키가 생성된 현재날짜로 설정된다.

오래된 Key-encrypting-Key 는 여전히 복호화에 사용되지만 아직 이들에게 새로운 키가 적용되지 않았다.

*/

String sqlStmt = "insert into key_encrypting_keys" + "valuse (NULL, ?, now() )";

Connection conn = DbManager.getDbConnection();


pstmt = conn.prepareStatement(sqlStmt, Statement.RETURN_GENERATED_KEYS);

pstmt.setBytes(1, rawKey);

int row = pstmt.executeUpdate();

ResultSet rs = pstmt.getGenerateKeys();


if (rs.next()){

kekId = rs.getString(1);

}

}

catch(NoSuchAlgorithmException e){}

catch(SQLException e){}

}

//예외가 캐치된 경우 리소스를 닫고 rawKey를 0바이트로 덮어쓴다.


finally{

if(pstmt != null){

try{

pstmt.close();

}

catch(SQLException sqle){

//igonore

}

pstmt = null;

}

if(rawKey != null){

for(int i=0; i<rawKey.length; i++){

rawKey[i] = (byte)0x00;

}

}

}

return keyId;

}


select count(*) from  Key_Encryping_Keys를 통해 Key_Encryping_Key가 존재함을 확인할 수 있다.

Key_Encryping_Key는 자기 자신을 암호화하지는 않는다.

이것은 암호화의 복잡도를 더해주고 제한된 키 저장소에 저장되어 있다.


"-로컬 키 저장소에 키를 생성하기"(p172)

키를 Key_Encryping_Key로 암호화할 수 있고 실제 데이터 키를 암호화하기 위한 방법이 필요하다.

GenerateKey()메소드는 키를 생성하고 매개변수 없이 키id를 갖는 로컬키 객체를 반환한다.

즉, 리턴된 로컬키 객체는 즉시 필요한 모든것을 포함하고 있다.

로컬 키 객체를 생성하고 준비하는 과정은 encryptkey()와 savekey()메소드를 통해 수행된다.

이것은 public 메소드이며 로컬키 객체안에 암호화되지 않은 키 데이터를 0로 만든다.

final public LocalKey generateKey(){

LocalKey localKey = new LocalKey();

byte[] rawKey = new byte[16];

try{

SecureRandom ivgenerator = SecureRandom.getInstance("SHA1PRNG");

ivGenerator.nextBytes(rawKey);

localKey.setRawKey(rawKey);

}

catch(NoSuchAlgorithmException e){}

encryptKey(localKey);

saveKey(localKey);

localKey.wipe();

return localKey;

};


"키를 암호화하기"

encryptKey()메소드는 로컬키 객체가 포험한 암호화되지 않은 키를 가져와 자기자신의 Key_encrypt_key를 사용해 암호화한다.

로컬키 객체는 키가 암호화되며 Key_encrypt_key의 ID로 업데이트된다.


final private void encrypkey(LocalKey localkey){

PreparedStatement pstmt = null;

ResultSet rs = null;

byte[] encryptedKey = null;

}

//쿼리는 Key_encrypt_key를 키 저장소에서 키를 검색하거나 가장 최근에 활성화된 날짜를 검색하는데 사용된다.

//가장 상위에 검색된 최근키가 현재의 라이브 키임을 인지할 수 있다.

try{

String query = "select * " + "from Key_encrypt_keys " + "where activation_date <= now() " + "ordery by activation_date DESC";

Connection conn = DbManager.getDbConnection();

pstmt = conn.prepareStatement(query);

rs = pstmt.executeQuery();


byte[] keyData = null;

if(rs.next()){

KeyData = rs.getBytes("key_data");

localKey.setkekId(rs.getString("kek_id"));

}

else{

System.out.println("Key_encrypt_key를 찾을 수 없다.");

}

이전에 본 복호화 예제와 유사하지만 Cipher 객체가 암호화를 위해 초기화된다는 점이 다르다.

암호화된 키가 16진 문자열로 암호화되는것을 주목하자. 

상용화 시스템에선 키가 배열 바이트에 남지만 16진 문자열이 접근성을 확보하여 테이블 검사가 용이해진다.

메소드에 리턴하기 전에 민감한 키 데이터를 로컬키 객체가 호출할때 0로 만든다.

그 후 수동으로 kek를 포함하는 배열을 반환하도록 한다.

이 수동 제로링을 통해 암,복호화 사이의 키는 비대칭의 특성을 갖도록 한다.

키를 복호하고자 할때 wipe메소드를 사용하여 로컬키 클래스의 바이트배열을 0으로 만든다.

복호시 LocalKeyStore에서 생성되어 이주된 로컬키 객체들이 코드에서 동작한다.

이후 들여다 볼 내용이지만 LocalKeyStore는 로컬키에 kek를 쓴다.

복호화 작업을 수행한 후에는 두 배열의 내용을 비워둬야 한다.

반면 키를 암호화 할 시에는 로컬키 객체는 kek로 이주되지 않는다.

encryptkey메소드에 존재하는 배열에 대해서만 참조한다.

wipe메소드를 호출하면 로컬키의 wipe메소드를 호출할때 영향을 주지 않는다.

따라서 kek를 포함한 배열의 제로링을 수동으로 조작해야 한다.


SecretKeySpec kekSpec = new SecretKeySpec(keyData, "AES");

Cipher cipher = Cipher.getInstance("AES/ECB/NoPadding");

cipher.init(Cipher.ENCRYPT_MODE, kekSpec);

encryptedKey = cipher.doFinal(local.getRawKey());

localKey.setKeyData(Utils.ByTes2HexStirng(encryptedKey, ""));

}

catch(SQLException ex){}

catch(NoSuchAlgorithmException nase){}

catch(NoSuchPaddingException nspe){}

catch(IllegalBlockSizeException ibse){}

finally{

if(rs != null){

try{

rs.close();

}

catch(SQLException sqlex){

igonore();

}

rs = null;

}

if(pstmt != null){

try{

pstmt.close();

}

catch(SQLException sqlex){

igonre();

}

pstmt = null;

}

localKey.wipe();

if(keyData != null){

for(int i=0; i<keyData.length; i++){

keyData[i] = (byte)0x00;

}

}

keyData = null;

}

}

'MS-SQL' 카테고리의 다른 글

4.3  (0) 2013.05.05
4.10  (0) 2013.05.05
4.17  (0) 2013.05.05
5.1  (0) 2013.05.05
5.8  (0) 2013.05.05
,