顯示具有 AES 標籤的文章。 顯示所有文章
顯示具有 AES 標籤的文章。 顯示所有文章

2016年5月1日 星期日

Java JCE - AES Encryption & Decryption @2016-05-01 (English Version)

Foreword:

Why I write this article ?  In my original thinking, I believe that there is a lot of blogs talk about Java AES example. However, I still found some sample code is not clearly to explain the issues of these sample. Therefore, I am afraid someone may really adopt these samples in their production system.

In addition, in order to simplify the explanation, the following sample will ignore the exception handle.

Content:

Some similar sample code that you often find on internet is as the following:
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
class AES_DEFAULT {  
 public static byte[] Encrypt(SecretKey secretKey, String msg) throws Exception  
 {  
  Cipher cipher = Cipher.getInstance("AES"); //: default is AES/ECB/PKCS5Padding
  cipher.init(Cipher.ENCRYPT_MODE, secretKey); 
  System.out.println("AES_DEFAULT IV:"+cipher.getIV());
  System.out.println("AES_DEFAULT Algoritm:"+cipher.getAlgorithm());
  byte[] byteCipherText = cipher.doFinal(msg.getBytes());  
  System.out.println("Encrypted result and base64 encoded:" + Base64.getEncoder().encodeToString(byteCipherText));
  return byteCipherText;  
 }  

 public static byte[] Decrypt(SecretKey secretKey, byte[] cipherText) throws Exception  
 {  
  Cipher cipher = Cipher.getInstance("AES"); 
  cipher.init(Cipher.DECRYPT_MODE, secretKey);  
  byte[] decryptedText = cipher.doFinal(cipherText);  
  String strDecryptedText = new String(decryptedText);
  System.out.println("Decrypted result:" + strDecryptedText);
  return decryptedText;  
 }  

 public static void main(String args[]) throws Exception{
  KeyGenerator keyGen = KeyGenerator.getInstance("AES");
  keyGen.init(128,new SecureRandom( ) );
  SecretKey secretKey = keyGen.generateKey();
  byte[] iv = new byte[16]; 
  SecureRandom prng = new SecureRandom();
  prng.nextBytes(iv);

  byte[] cipher = AES_DEFAULT.Encrypt(secretKey, "I am PlainText!!");
  AES_DEFAULT.Decrypt(secretKey, cipher);  
 }
} 

The above sample code can work well for encryption and decryption, but there are some issues:
  1. The first issue is located at the line 4. It uses the ECB cipher mode, however this cipher mode is not a secure cipher mode for AES, because it will cause the cipher block is the same if the input plain-text is the same. You could refer to the Wiki
  2. The second issue is located at the line 25. We should not use the 128 bits length as the AES KEY. Now is 2016, the recommended length of AES key is at least 256 bits.
  3. The third issue is the line 8. It use the msg.getBytes( ). This style will be fine if the program is running at the same platform or machine. However, your program will run at different platform/machine, and you will find the default charset may be different for different platform/machine. Therefore, this may cause the decrypted result is not as you expected.  

The recommended implementation is as the following sample:

Important warring: You should select the correct cipher mode,  for example: CCM or GCM mode. (Update 2022/03/04) The reason is the CBC mode is vulnerable to padding oracle attacks.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
class AES_CBC_PKCS5PADDING {
 
 public static byte[] Encrypt(SecretKey secretKey, byte[] iv, String msg) throws Exception{
  Cipher cipher = Cipher.getInstance("AES/CBC/PKCS5PADDING"); 
  cipher.init(Cipher.ENCRYPT_MODE, secretKey, new IvParameterSpec(iv));    
  System.out.println("AES_CBC_PKCS5PADDING IV:"+cipher.getIV());
  System.out.println("AES_CBC_PKCS5PADDING Algoritm:"+cipher.getAlgorithm());
  byte[] byteCipherText = cipher.doFinal(msg.getBytes("UTF-8"));
  System.out.println("Encrypted result and base64 encoded:" + Base64.getEncoder().encodeToString(byteCipherText));
  return byteCipherText;
 }
 
 public static void Decrypt(SecretKey secretKey, byte[] cipherText, byte[] iv) throws Exception{
  Cipher cipher = Cipher.getInstance("AES/CBC/PKCS5PADDING"); 
  cipher.init(Cipher.DECRYPT_MODE, secretKey, new IvParameterSpec(iv));    
  byte[] decryptedText = cipher.doFinal(cipherText);
  String strDecryptedText = new String(decryptedText);
  System.out.println("Decrypted result:" + strDecryptedText);
 }
 
 public static void main(String args[]) throws Exception{
  KeyGenerator keyGen = KeyGenerator.getInstance("AES");
  keyGen.init(256,new SecureRandom( ) );
  SecretKey secretKey = keyGen.generateKey();
  byte[] iv = new byte[16]; 
  SecureRandom prng = new SecureRandom();
  prng.nextBytes(iv);

  byte[] cipher = AES_CBC_PKCS5PADDING.Encrypt(secretKey, iv, "I am PlainText!!");
  AES_CBC_PKCS5PADDING.Decrypt(secretKey, cipher, iv);  
 }
}
As you seeing at line 3,  we adopt the CBC cipher mode with PKCS5 Padding. You could refer to the  Padding for the detail.  At line 8, we directly invoke msg.getBytes("UTF-8") to avoid some charset issues. Of course,  you could specify it use ANSI , and it is still work well if your plain-text contain ANSI only,

Here, allow me to remind you. The recommended length of AES key is 256 bits. In addition, you should always generate a new IV to encrypt if you use the same AES key to encrypt data.

You could refer the below sample code to generate the required Secret Key and IV. In addition, you also need to provide the same IV to the decryption side.

  KeyGenerator keyGen = KeyGenerator.getInstance("AES");
  keyGen.init(256,new SecureRandom( ) );
  SecretKey secretKey = keyGen.generateKey();
  byte[] iv = new byte[16]; 
  SecureRandom prng = new SecureRandom();
  prng.nextBytes(iv);

Sometime, you may see the following error message, the reason is the default Oracle's Java Runtime can not generate the AES 256 bits keys because the policy issue.

1
2
3
4
5
6
Exception in thread "main" java.security.InvalidKeyException: Illegal key size or default parameters
 at javax.crypto.Cipher.checkCryptoPerm(Cipher.java:1026)
 at javax.crypto.Cipher.implInit(Cipher.java:801)
 at javax.crypto.Cipher.chooseProvider(Cipher.java:864)
 at javax.crypto.Cipher.init(Cipher.java:1249)
 at javax.crypto.Cipher.init(Cipher.java:1186)

You need to install the Java Cryptography Extension (JCE) Unlimited Strength Jurisdiction Policy Files if you have the same problem at your environment. One more thing that you may need to check is the policy file version should be same as the JRE version at your environment. (The Android will not have this issue)

Finally,  you may ask common questions as the followings:
  1. How to encrypt a stream file with large file size (EX:Video or Audio)?
  2. How to randomly access specific block data of the encrypted file ?
The possible solution is as below :
  • The answer for the first one question is easy. Just study the Java Cipher API doc carefully, then you can invoke the update method of Cipher with multiple times , and invoke the doFinal method at the end.
  • The answer for the 2nd question is to change the cipher mode as the CTR cipher mode. In addition, you need to write a calculate IV method for the target block that you want.
You could refer to the following implementation:
(Note: This sample code does not testing well, please don't adopt it on production system.)

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
class AES_CTR_PKCS5PADDING {
 private static final int BLOCK_SIZE = 16;
 
 public static void Encrypt(SecretKey secretKey, byte[] iv, File plainTextFile, File encryptedFile) throws Exception{
  Cipher cipher = Cipher.getInstance("AES/CTR/PKCS5PADDING"); 
  cipher.init(Cipher.ENCRYPT_MODE, secretKey, new IvParameterSpec(iv));    
  System.out.println("AES_CTR_PKCS5PADDING IV:"+cipher.getIV());
  System.out.println("AES_CTR_PKCS5PADDING Algoritm:"+cipher.getAlgorithm());
  byte buf[] = new byte[4096];
  try (InputStream in = new FileInputStream(plainTextFile);
    OutputStream out = new FileOutputStream(encryptedFile);){
   int readBytes = in.read(buf);   
   while(readBytes > 0){
    byte[] cipherBytes = cipher.update(buf, 0 , readBytes);
    out.write(cipherBytes);
    readBytes = in.read(buf);
   }
   cipher.doFinal();
  }
 }
 
 public static void Decrypt(SecretKey secretKey, byte[] iv, File cipherTextFile, File decryptedFile) throws Exception{
  Cipher cipher = Cipher.getInstance("AES/CTR/PKCS5PADDING"); 
  cipher.init(Cipher.DECRYPT_MODE, secretKey, new IvParameterSpec(iv));    
  
  if(!decryptedFile.exists()){
   decryptedFile.createNewFile(); //: Here, it may be fail if ...
  }
  
  byte buf[] = new byte[4096];
  try (InputStream in = new FileInputStream(cipherTextFile);
    OutputStream out = new FileOutputStream(decryptedFile);){
   int readBytes = in.read(buf);   
   while(readBytes > 0){
    byte[] decryptedBytes = cipher.update(buf, 0 , readBytes);
    out.write(decryptedBytes);
    readBytes = in.read(buf);
   }
   cipher.doFinal();
  }
 }
  
 public static byte[] DecryptPartial(SecretKey secretKey, byte[] iv, File cipherTextFile, int blockIndex, int blockCount ) throws Exception{
  final int offset = blockIndex * BLOCK_SIZE;
  final int bufSize = blockCount * BLOCK_SIZE;

  Cipher cipher = Cipher.getInstance("AES/CTR/PKCS5PADDING"); 
  cipher.init(Cipher.DECRYPT_MODE, secretKey, calculateIVForBlock(new IvParameterSpec(iv), blockIndex ));

  byte[] decryptedBytes = new byte[bufSize];
  try (FileInputStream in = new FileInputStream(cipherTextFile)){
   byte inputBuf[] = new byte[bufSize];
   in.skip(offset);
   int readBytes = in.read(inputBuf);
   decryptedBytes = cipher.update(inputBuf, 0, readBytes);
  }
  return decryptedBytes;
 } 

 private static IvParameterSpec calculateIVForBlock(final IvParameterSpec iv,
         final long blockIndex) {  
     final BigInteger biginIV = new BigInteger(1, iv.getIV());
     final BigInteger blockIV = biginIV.add(BigInteger.valueOf(blockIndex));
     final byte[] blockIVBytes = blockIV.toByteArray();

     // Normalize the blockIVBytes as 16 bytes for IV
     if(blockIVBytes.length == BLOCK_SIZE){
      return new IvParameterSpec(blockIVBytes);
     }
     if(blockIVBytes.length > BLOCK_SIZE ){
      // For example: if the blockIVBytes length is 18, blockIVBytes is [0],[1],...[16],[17]
      // We have to remove [0],[1] , so we change the offset = 2
      int offset = blockIVBytes.length - BLOCK_SIZE;
      return new IvParameterSpec(blockIVBytes, offset, BLOCK_SIZE);
     }
     else{
      // For example: if the blockIVBytes length is 14, blockIVBytes is [0],[1],...[12],[13]
      // We have to insert 2 bytes at head
      final byte[] newBlockIV = new byte[BLOCK_SIZE]; //: default set to 0 for 16 bytes
      int offset = blockIVBytes.length - BLOCK_SIZE;
      System.arraycopy(blockIVBytes, 0, newBlockIV, offset, blockIVBytes.length);
      return new IvParameterSpec(newBlockIV);
     }
 }
 
 private static void createTestFile(String path) throws Exception{
  File test = new File(path);  
  try(FileOutputStream out = new FileOutputStream(test)){

   StringBuffer buf = new StringBuffer(16);

   int blockCount = 100000;
   for(int i = 0 ; i < blockCount ; i ++){
    buf.append(i);
    int size = buf.length();
    for(int j = 0; j < (14-size); j++ ){
     buf.append('#');
    }
    out.write(buf.toString().getBytes());
    out.write("\r\n".getBytes());
    buf.delete(0, 16);
   }   
  }  
 }
 
 public static void main(String args[]) throws Exception{
  KeyGenerator keyGen = KeyGenerator.getInstance("AES");
  keyGen.init(256,new SecureRandom( ) );
  SecretKey secretKey = keyGen.generateKey();
  byte[] iv = new byte[16]; 
  SecureRandom prng = new SecureRandom();
  prng.nextBytes(iv);
  
  {
   String originalFile = "~/PlainText.txt";
   String encryptedFile = "~/CipherText.enc"; 
   String deryptedFile = "~/Decrypted.txt";   

   AES_CTR_PKCS5PADDING.createTestFile(originalFile); //: Create Testing Data
   
   AES_CTR_PKCS5PADDING.Encrypt(secretKey, iv, new File(originalFile), new File(encryptedFile));
   AES_CTR_PKCS5PADDING.Decrypt(secretKey, iv, new File(encryptedFile), new File(deryptedFile));
   byte[] ret = AES_CTR_PKCS5PADDING.DecryptPartial(secretKey, iv, new File(encryptedFile), 100, 10);   
   System.out.println(new String(ret));
  }
 }

Final:

I don't talk about the GCM cipher mode here. In principle, you don't need the GCM if you don't need the authentication

Reference:

* https://en.wikipedia.org/wiki/Block_cipher_mode_of_operation#Electronic_Codebook_.28ECB.29
* https://docs.oracle.com/javase/8/docs/technotes/guides/security/SunProviders.html

Java JCE - AES 的 Encryption & Decryption @2016-05-01

前言:

都已經2016年了,本來不打算寫這篇的,但偶然發現網路上有多個中文部落格,甚至是論壇上分享或是討論 Java 的 AES 的程式,都沒有討論一些問題,深怕一堆人看到這種範例程式就放到你開發的系統上。另外,為了簡化程式來說明,以下的程式並不考慮Exception處理方式。

內容:

先來看網路上常見的程式的寫法:
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
class AES_DEFAULT {  
 public static byte[] Encrypt(SecretKey secretKey, String msg) throws Exception  
 {  
  Cipher cipher = Cipher.getInstance("AES"); //: 等同 AES/ECB/PKCS5Padding
  cipher.init(Cipher.ENCRYPT_MODE, secretKey); 
  System.out.println("AES_DEFAULT IV:"+cipher.getIV());
  System.out.println("AES_DEFAULT Algoritm:"+cipher.getAlgorithm());
  byte[] byteCipherText = cipher.doFinal(msg.getBytes());  
  System.out.println("加密結果的Base64編碼:" + Base64.getEncoder().encodeToString(byteCipherText));
  return byteCipherText;  
 }  

 public static byte[] Decrypt(SecretKey secretKey, byte[] cipherText) throws Exception  
 {  
  Cipher cipher = Cipher.getInstance("AES"); 
  cipher.init(Cipher.DECRYPT_MODE, secretKey);  
  byte[] decryptedText = cipher.doFinal(cipherText);  
  String strDecryptedText = new String(decryptedText);
  System.out.println("解密結果:" + strDecryptedText);
  return decryptedText;  
 }  

 public static void main(String args[]) throws Exception{
  KeyGenerator keyGen = KeyGenerator.getInstance("AES");
  keyGen.init(128,new SecureRandom( ) );
  SecretKey secretKey = keyGen.generateKey();
  byte[] iv = new byte[16]; 
  SecureRandom prng = new SecureRandom();
  prng.nextBytes(iv);

  byte[] cipher = AES_DEFAULT.Encrypt(secretKey, "I am PlainText!!");
  AES_DEFAULT.Decrypt(secretKey, cipher);  
 }
} 
上面這種寫法,這個程式對於加解密的運作是正常的,但會有潛在的3個問題:


  1. 第 1 個問題在第 4 行 這個用法所使用的 Cipher mode 是 ECB,也就是比較不安全的方式。原因是 ECB 對於相同的資料加密後的結果會是一樣的,有興趣可以參考(Wiki上的那張企鵝圖)。 如果你的應用是每次加密時 secret key 都是重新產生的,而且需要被加密的資料每次都完全不同的時候,各自的資料內容本身也是異質性相當高,如果採用這種做法也沒有太大的問題,但還是不建議。因為以加密的應用來說,常見的對象就是檔案或是運用在傳輸加密。而這兩種方式,大部分都會有相同的資料。以檔案來說,相同類型的檔案你用binary編輯器打開檔案,你就可以觀察到檔案的前面都會有雷同的資料。通訊協定更是如此,例如:HTTP通訊協定。
  2. 第 2 個問題在第 25 行 secret key 長度的問題,不應該使用 128 bits 長度,強度太弱。
  3. 第 3 個問題在第 8 行的 msg.getBytes( ),不同的作業系統所使用的預設 charset 可能是不同的。這樣的做法,有可能會造成不同平台在加密時的行為不如你所預期,也就是解密後的內容可能跟你當初要加密的資料不同。


比較建議的寫法是採用下面這種:

Important warring: You should select the correct cipher mode,  for example: CCM or GCM mode. (Update 2022/03/04) The reason is the CBC mode is vulnerable to padding oracle attacks.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
class AES_CBC_PKCS5PADDING {
 
 public static byte[] Encrypt(SecretKey secretKey, byte[] iv, String msg) throws Exception{
  Cipher cipher = Cipher.getInstance("AES/CBC/PKCS5PADDING"); 
  cipher.init(Cipher.ENCRYPT_MODE, secretKey, new IvParameterSpec(iv));    
  System.out.println("AES_CBC_PKCS5PADDING IV:"+cipher.getIV());
  System.out.println("AES_CBC_PKCS5PADDING Algoritm:"+cipher.getAlgorithm());
  byte[] byteCipherText = cipher.doFinal(msg.getBytes("UTF-8"));
  System.out.println("加密結果的Base64編碼:" + Base64.getEncoder().encodeToString(byteCipherText));

  return byteCipherText;
 }
 
 public static void Decrypt(SecretKey secretKey, byte[] cipherText, byte[] iv) throws Exception{
  Cipher cipher = Cipher.getInstance("AES/CBC/PKCS5PADDING"); 
  cipher.init(Cipher.DECRYPT_MODE, secretKey, new IvParameterSpec(iv));    
  byte[] decryptedText = cipher.doFinal(cipherText);
  String strDecryptedText = new String(decryptedText);
  System.out.println("解密結果:" + strDecryptedText);
 }
 
 public static void main(String args[]) throws Exception{
  KeyGenerator keyGen = KeyGenerator.getInstance("AES");
  keyGen.init(256,new SecureRandom( ) );
  SecretKey secretKey = keyGen.generateKey();
  byte[] iv = new byte[16]; 
  SecureRandom prng = new SecureRandom();
  prng.nextBytes(iv);

  byte[] cipher = AES_CBC_PKCS5PADDING.Encrypt(secretKey, iv, "I am PlainText!!");
  AES_CBC_PKCS5PADDING.Decrypt(secretKey, cipher, iv);  
 }
}
在第 3 行明確指定採用 CBC 的 cipher mode,並且指定 Padding 方式,有興趣可參考這篇 Padding 運作方式。在第 8 行,直接指定用 msg.getBytes("UTF-8"),避免一些問題,如果需要加密的內容都是純ANSI,你可以指定用 ANSI 就可以。

此處,還是要強調 AES的加密 Secret Key 的長度建議至少要 256 bit 以上,用同一把 Secret Key 做加密時候,應該都要產生新的 IV 來加密可以參考下面這種寫法來產生所需要的 AES Secret Key 和 IV。另外,IV 在解密時候,也要用當初加密使用的相同IV才可以。

  KeyGenerator keyGen = KeyGenerator.getInstance("AES");
  keyGen.init(256,new SecureRandom( ) );
  SecretKey secretKey = keyGen.generateKey();
  byte[] iv = new byte[16]; 
  SecureRandom prng = new SecureRandom();
  prng.nextBytes(iv);

另外,由於 Oracle 官方預設標準的Java執行環境沒法支援產生 AES 256 bits 長度的 secret key。所以,你會遇到下面這種錯訊息:

1
2
3
4
5
6
Exception in thread "main" java.security.InvalidKeyException: Illegal key size or default parameters
 at javax.crypto.Cipher.checkCryptoPerm(Cipher.java:1026)
 at javax.crypto.Cipher.implInit(Cipher.java:801)
 at javax.crypto.Cipher.chooseProvider(Cipher.java:864)
 at javax.crypto.Cipher.init(Cipher.java:1249)
 at javax.crypto.Cipher.init(Cipher.java:1186)

這表示你的執行環境需要安裝 Java Cryptography Extension (JCE) Unlimited Strength Jurisdiction Policy Files。這裡要特別注意,你務必要安裝跟執行環境JVM相同版本的JCE Policy,否則可能會遇到一些怪異的現象。(Android執行環境不在此限)

最後,常問遇到的問題就是:

  1. 如果檔案很大的Stream類型的檔案(如:Video or Audio)要如何做到加密?
  2. 想要隨意位置讀取已經加密的內容要怎麼做?

建議的做法:


  • 第 1 個問題,很簡單請看清楚 Cipher 的 API doc,採用多次呼叫 update 的方法,最後再呼叫 doFinal 方法即可。
  • 第 2 個問題,只要將 Cipher mode 改為 CTR 即可,另外如果是要隨機存取某個 Block 的資料,必須要自己重新計算那個 Block 開始的 IV。
請參考類似下面的寫法:
(Note: This sample code does not testing well, please don't adopt it on production system.)

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
class AES_CTR_PKCS5PADDING {
 private static final int BLOCK_SIZE = 16;
 
 public static void Encrypt(SecretKey secretKey, byte[] iv, File plainTextFile, File encryptedFile) throws Exception{
  Cipher cipher = Cipher.getInstance("AES/CTR/PKCS5PADDING"); 
  cipher.init(Cipher.ENCRYPT_MODE, secretKey, new IvParameterSpec(iv));    
  System.out.println("AES_CTR_PKCS5PADDING IV:"+cipher.getIV());
  System.out.println("AES_CTR_PKCS5PADDING Algoritm:"+cipher.getAlgorithm());
  byte buf[] = new byte[4096];
  try (InputStream in = new FileInputStream(plainTextFile);
    OutputStream out = new FileOutputStream(encryptedFile);){
   int readBytes = in.read(buf);   
   while(readBytes > 0){
    byte[] cipherBytes = cipher.update(buf, 0 , readBytes);
    out.write(cipherBytes);
    readBytes = in.read(buf);
   }
   cipher.doFinal();
  }
 }
 
 public static void Decrypt(SecretKey secretKey, byte[] iv, File cipherTextFile, File decryptedFile) throws Exception{
  Cipher cipher = Cipher.getInstance("AES/CTR/PKCS5PADDING"); 
  cipher.init(Cipher.DECRYPT_MODE, secretKey, new IvParameterSpec(iv));    
  
  if(!decryptedFile.exists()){
   decryptedFile.createNewFile(); //: Here, it may be fail if ...
  }
  
  byte buf[] = new byte[4096];
  try (InputStream in = new FileInputStream(cipherTextFile);
    OutputStream out = new FileOutputStream(decryptedFile);){
   int readBytes = in.read(buf);   
   while(readBytes > 0){
    byte[] decryptedBytes = cipher.update(buf, 0 , readBytes);
    out.write(decryptedBytes);
    readBytes = in.read(buf);
   }
   cipher.doFinal();
  }
 }
  
 public static byte[] DecryptPartial(SecretKey secretKey, byte[] iv, File cipherTextFile, int blockIndex, int blockCount ) throws Exception{
  final int offset = blockIndex * BLOCK_SIZE;
  final int bufSize = blockCount * BLOCK_SIZE;

  Cipher cipher = Cipher.getInstance("AES/CTR/PKCS5PADDING"); 
  cipher.init(Cipher.DECRYPT_MODE, secretKey, calculateIVForBlock(new IvParameterSpec(iv), blockIndex ));

  byte[] decryptedBytes = new byte[bufSize];
  try (FileInputStream in = new FileInputStream(cipherTextFile)){
   byte inputBuf[] = new byte[bufSize];
   in.skip(offset);
   int readBytes = in.read(inputBuf);
   decryptedBytes = cipher.update(inputBuf, 0, readBytes);
  }
  return decryptedBytes;
 } 

 private static IvParameterSpec calculateIVForBlock(final IvParameterSpec iv,
         final long blockIndex) {  
     final BigInteger biginIV = new BigInteger(1, iv.getIV());
     final BigInteger blockIV = biginIV.add(BigInteger.valueOf(blockIndex));
     final byte[] blockIVBytes = blockIV.toByteArray();

     // Normalize the blockIVBytes as 16 bytes for IV
     if(blockIVBytes.length == BLOCK_SIZE){
      return new IvParameterSpec(blockIVBytes);
     }
     if(blockIVBytes.length > BLOCK_SIZE ){
      // For example: if the blockIVBytes length is 18, blockIVBytes is [0],[1],...[16],[17]
      // We have to remove [0],[1] , so we change the offset = 2
      int offset = blockIVBytes.length - BLOCK_SIZE;
      return new IvParameterSpec(blockIVBytes, offset, BLOCK_SIZE);
     }
     else{
      // For example: if the blockIVBytes length is 14, blockIVBytes is [0],[1],...[12],[13]
      // We have to insert 2 bytes at head
      final byte[] newBlockIV = new byte[BLOCK_SIZE]; //: default set to 0 for 16 bytes
      int offset = blockIVBytes.length - BLOCK_SIZE;
      System.arraycopy(blockIVBytes, 0, newBlockIV, offset, blockIVBytes.length);
      return new IvParameterSpec(newBlockIV);
     }
 }
 
 private static void createTestFile(String path) throws Exception{
  File test = new File(path);  
  try(FileOutputStream out = new FileOutputStream(test)){

   StringBuffer buf = new StringBuffer(16);

   int blockCount = 100000;
   for(int i = 0 ; i < blockCount ; i ++){
    buf.append(i);
    int size = buf.length();
    for(int j = 0; j < (14-size); j++ ){
     buf.append('#');
    }
    out.write(buf.toString().getBytes());
    out.write("\r\n".getBytes());
    buf.delete(0, 16);
   }   
  }  
 }
 
 public static void main(String args[]) throws Exception{
  KeyGenerator keyGen = KeyGenerator.getInstance("AES");
  keyGen.init(256,new SecureRandom( ) );
  SecretKey secretKey = keyGen.generateKey();
  byte[] iv = new byte[16]; 
  SecureRandom prng = new SecureRandom();
  prng.nextBytes(iv);
  
  {
   String originalFile = "~/PlainText.txt";
   String encryptedFile = "~/CipherText.enc"; 
   String deryptedFile = "~/Decrypted.txt";   

   AES_CTR_PKCS5PADDING.createTestFile(originalFile); //: Create Testing Data
   
   AES_CTR_PKCS5PADDING.Encrypt(secretKey, iv, new File(originalFile), new File(encryptedFile));
   AES_CTR_PKCS5PADDING.Decrypt(secretKey, iv, new File(encryptedFile), new File(deryptedFile));
   byte[] ret = AES_CTR_PKCS5PADDING.DecryptPartial(secretKey, iv, new File(encryptedFile), 100, 10);   
   System.out.println(new String(ret));
  }
 }



最後:

這邊我沒有提到另外一種 GCM 的 Cipher Mode,原則上,如果你沒有 Authentication 的需要時候,就不需要用到 GCM。

Reference:

* https://en.wikipedia.org/wiki/Block_cipher_mode_of_operation#Electronic_Codebook_.28ECB.29
* https://docs.oracle.com/javase/8/docs/technotes/guides/security/SunProviders.html

2014年3月12日 星期三

OpenSSL - 金鑰與憑證的管理

Key and Certificate Management

要讓 Web 或者 Service 能夠支援 SSL, 基本上有三個步驟:
1. 產生私有金鑰 (Private Key)
2. 產生 CSR (Certificate Signing Request), 並將 CSR 傳送給 CA
3. 把 CA 所提供的憑證 (Certificate) 安裝在 Web 或 Service的伺服器上

以下利用 OpenSSL 來說明如何完成以上 3 個步驟.

Key Generation

使用 openssl 產生私有金鑰 (private key) 的指令如下:

~$ openssl genrsa -aes256 -out private.key 2048

以下是執行後的輸出畫面, 會要求輸入Pass phrase 來保護這個 RSA private key.

Generating RSA private key, 2048 bit long modulus
.+++
.............................................+++
e is 65537 (0x10001)
Enter pass phrase for private.key:
Verifying - Enter pass phrase for private.key:


這是產生一把長度為 2048 bits 的 RSA private key, 並且使用 AES-256 與輸入的 Pass phrase 來保護這把 Private key。基本上, RSA的 Private key 長度建議至少是2048 bits才是比較安全的。執行後的結果 private.key 是一個加密後的檔案並且以 PEM 的格式儲存,內容類似以下:

-----BEGIN RSA PRIVATE KEY-----
Proc-Type: 4,ENCRYPTED
DEK-Info: AES-256-CBC,4616B291C58D187CE9787AD421E7DA80

9DbRWLR8N29lHz78aUCYWBUzDAK4e7UFfiV+A3oicaHm75FCw5hnf+3ScOUEymWk
Bz+sYKCjHTtD4fMyw4apF1/QmCn6KM+kKY4vsmOx1NXLLNhrX2SpmiZ+dsVfOg0D
IoEhw06Pzd767gy+zr8Q7JeMna1zKG7nzbIMM+WiwmiZKTErl16kyp0HH2OHSkQv
0or3cj83zswhb7VcArt52cJUhNdjkZdb3x+/bbehn5sFCMiNTc4UId5HBH84dyNC
l/KsDmdFfq8kGXhttpllz7pl8naYnamEQ0A+foaWI+MMXmMl5r5kXlDkAkCZ/y9U
LYmBLsSeMwrHVC34M+sCP8GEyeN4znAv9rb0eTs5/zkPo1V/orFxZYaQZwQ0yQn2
pUb10sD7cXrpJ4EQqPJzxOxhFAAq2C35lkYA5yrCwZiH6/Y66PvGYK1W1BMcB9B+
8t3Cn9xiNJ27HQ43oPWu6P07yxJYAPpkJMhiZ6cM1clYbi2ZrYKnajRORIoL0WgI
4d4zGYr48/Pd27ITSUxiBWIVsB8OZPGyjh3ebBGw1fe1i7iC1x/h35b+po8mk6Fg
tmIktPA9z/jUgSaorQsH9YzyA8DU4K1p3LCL4ZxHJJoEKOMv/7avQCRzm+FonWdz
e5rewtx9wNoWKSw4MPU6ozRB2tzW9RUJQeGfCgtwbjMDRQYYLmyLzPshekiR4srF
E3U1aJWwEDVTfdjlEgiB6dKzm/l1ywanBB5Ns84HSsagN7gZEBV3Y7TKlx+9Omd2
N6QIwTG42ERDmsuLlmky6Ju98yMHRNNC7ZrkLhR1c3w+KHht7cIm7VpqouNCYcjR
yhebuXpSakS4mPlYXKmFZlEgJI8NT9H1czn7XsNUE/Ty/wBlhgn5XGbmoRLg8b/L
jNqC/b3N4Xb9nWuirMmPwNf9Ja0M3MRJmjUrA3FJprHrSaJMF7SS3tHeu5vivnrP
P9vfibi8Cu2UVol3/y/gyBNGgsA2UnOz3xzPrY/22NA+VtDILIbsIukeHMGVPzdS
xE3BrgpyF68LrIg/Pb7F5XougYjW9SjuyYnQzum9XgvZGFsZJU+ENOmR+9gk8/iA
v+/RKairKW18E6kaf/9mIfYWWLHSPZyZqqo61Iho/BqO0vOSq7hd5at0KUsbtfr2
4o2cGMp5qYJxPBi5suIR3/H0bZnS72sAVPAeUGEhSi+QX9d9zQYJio8LfxsTZMZj
ArlJY3Lm9cGMEbS/n3jwC7zGE75j4aaNCTWccNWUqhF3p+QOCN3WL/yKXa1nDcx9
4AYt0UT8rHDHNVs1ylK97/e6OwV9bmhXf4shvBavvKcoSPVc2F8c/+yGWl/3BDyZ
MTTNYFN99Yll+g06DhSaQpUACbKpzHfNy83ideruLpqYLptqCEqE66pZZt08AhCg
JKNiTV6c6O09TBfwGz7Cqn+jsCsIN6iUbEOc1lEj9Ga+FtuVFy5h+nrzzSK3pA/W
mhqpnf9a0QmlfkrUBfmjCGyzQ1PnEs0Ki0PyPDISsRUewJXvWsDUoT3+k13nMfdE
hPZoPFjPA6pztnHUxUKDwx9xVmLTLHkLmu3VtWOaWl1qmZP57fMHoKjeaRYNYPN6
-----END RSA PRIVATE KEY-----

Creating Certificate Signing Requests (CSR)

為了向公開具備公信力的 CA 單位提出憑證申請, 必須要提供一個 CSR 的檔案給這個 CA單位。以下就是透過 openssl 使用先前產生的 RSA 金鑰來產生一個 CSR 的檔案,過程當中需要輸入一些相關的資料(可以參考以下範例中的黑色斜線粗體部份)
$ openssl req -new -key private.key -out CSR.csr

Enter pass phrase for private.key:
You are about to be asked to enter information that will be incorporated
into your certificate request.
What you are about to enter is what is called a Distinguished Name or a DN.
There are quite a few fields but you can leave some blank
For some fields there will be a default value,
If you enter '.', the field will be left blank.
-----
Country Name (2 letter code) [AU]:TW
State or Province Name (full name) [Some-State]:.            
Locality Name (eg, city) []:Taipei
Organization Name (eg, company) [Internet Widgits Pty Ltd]:ijeCorp        
Organizational Unit Name (eg, section) []:Security
Common Name (e.g. server FQDN or YOUR name) []:www.ijecorp.com
Email Address []:miller.lai@gmail.com

Please enter the following 'extra' attributes
to be sent with your certificate request
A challenge password []:IM@R.O.C    
An optional company name []:ijeCorp

產生 CSR 後, 可以透過 openssl 來檢測 CSR的內容是否正確,指令如下:
~$ openssl req -text -in CSR.csr -noout

執行後的輸出結果如下:
Certificate Request:
    Data:
        Version: 0 (0x0)
        Subject: C=TW, L=Taipei, O=ijeCorp, OU=Security, CN=www.ijecorp.com/emailAddress=miller.lai@gmail.com
        Subject Public Key Info:
            Public Key Algorithm: rsaEncryption
                Public-Key: (2048 bit)
                Modulus:
                    00:b2:fb:c9:5f:4c:0f:2a:11:d7:81:05:a9:e6:41:
                    43:35:b7:47:ba:be:58:42:32:ad:e9:99:be:c8:82:
                    57:73:ca:87:6c:f9:90:15:1d:4a:6d:cc:fc:81:e2:
                    2e:c2:1f:ed:fd:ae:45:cd:ce:c9:6c:43:f1:47:ef:
                    aa:8d:f8:e3:32:52:10:80:db:0a:b5:fa:be:91:87:
                    94:08:57:86:8e:6b:86:f6:63:05:2d:53:3c:c3:4c:
                    48:ad:4d:ee:f2:de:7c:68:9d:f7:01:5b:a1:99:2e:
                    69:60:b8:b1:59:4c:d8:69:7a:cf:47:bb:3c:c6:6f:
                    5d:99:08:5d:e8:5e:bd:34:e2:52:00:9c:70:62:89:
                    4b:aa:c6:8c:d7:0d:a0:fc:d7:1d:bf:18:9a:d4:e2:
                    b6:f8:26:5d:0b:bb:b0:af:4d:7e:ee:f8:22:cc:18:
                    7f:b7:a4:b4:12:c1:78:34:ed:29:30:b3:fa:93:97:
                    04:4c:a2:32:3b:f4:cb:98:52:6f:ea:11:ab:01:8d:
                    03:b6:d3:b4:cb:0e:f8:3d:cf:2f:ae:3b:51:c9:1d:
                    16:15:e2:d6:cc:8b:35:8e:df:70:c2:c5:18:b7:8f:
                    9f:42:a3:d9:99:55:c0:74:f3:ab:40:98:d0:0a:4d:
                    99:12:2e:92:5b:d6:d3:d6:73:2c:01:76:96:ea:8d:
                    fd:d5
                Exponent: 65537 (0x10001)
        Attributes:
            unstructuredName         :unable to print attribute
            challengePassword        :unable to print attribute
    Signature Algorithm: sha1WithRSAEncryption
         0b:5c:f4:d5:ae:f3:4a:4b:9b:d2:8e:e6:cd:b6:20:2c:9d:61:
         57:f3:9d:7f:95:03:71:9e:95:1b:4c:8f:ad:f8:f9:6c:c3:a6:
         32:be:6a:b9:bf:45:81:0a:6f:6d:3c:29:f6:90:00:81:73:ac:
         65:8d:19:ea:39:86:5d:8f:2b:d7:bf:2f:02:63:01:bb:87:0f:
         dd:3d:81:b2:3c:72:1c:d4:ad:9e:ba:35:e6:41:f2:2f:fb:b8:
         0e:9f:5d:26:2b:52:37:a3:7f:97:b0:35:78:a6:d4:5a:4f:df:
         32:15:9f:45:43:04:95:4e:f4:d6:71:68:57:59:b2:5b:28:e3:
         13:73:96:c8:cf:c1:df:56:e4:b0:f2:d9:ee:e2:67:7a:37:d2:
         ba:f6:17:9e:ca:6a:b2:96:6d:e8:bb:cf:ff:17:13:fb:cc:d6:
         05:95:c2:4b:f9:49:b9:67:d8:c3:39:c9:78:00:56:20:6f:d4:
         2c:5c:66:c7:a0:1b:bd:2f:5d:c7:8b:98:69:93:67:be:44:f6:
         84:b9:8b:ec:45:1b:be:b8:5b:e9:94:00:2f:1c:30:ad:5f:c1:
         57:3c:f9:11:e0:49:85:bd:93:e6:09:54:aa:94:d5:31:b4:89:
         40:8d:c3:b8:e7:f9:80:d7:71:62:dd:51:88:71:51:ed:8f:b4:
         5e:12:93:3a

在確認 CSR 內容正確後,就可以將 CSR 傳送到公認且有公信力的 CA 單位,並請求該單位為此 CSR 產生憑證 (Certificate)。一旦取得 CA 為此 CSR 產生的憑證後, 就可以將它安裝在需要支援 SSL 的伺服器上。

但是,如果只是開發階段需要測試,或只是自己的想要使用並不是要公開對外支援 SSL時候,可以自行產生一個所謂 Self-signed 的憑證。產生 Self-signed 憑證的方式如下:

~$ openssl x509 -req -days 365 -in CSR.csr -signkey private.key -out self-signed.crt

以下是執行後的輸出畫面, 會要求輸入Pass phrase 來取得 private.key 進行 sign 憑證的處理。最後產生一個 PEM 格式的憑證內容在 self-signed.crt 檔案中
Signature ok
subject=/C=TW/L=Taipei/O=ijeCorp/OU=Security/CN=www.ijecorp.com/emailAddress=miller.lai@gmail.com
Getting Private key
Enter pass phrase for private.key:

PEM格式的 self-signed.crt 的內容類似如下:
-----BEGIN CERTIFICATE-----
MIIDgjCCAmoCCQCQyr2MuBjL8jANBgkqhkiG9w0BAQUFADCBgjELMAkGA1UEBhMC
VFcxDzANBgNVBAcMBlRhaXBlaTEQMA4GA1UECgwHaWplQ29ycDERMA8GA1UECwwI
U2VjdXJpdHkxGDAWBgNVBAMMD3d3dy5pamVjb3JwLmNvbTEjMCEGCSqGSIb3DQEJ
ARYUbWlsbGVyLmxhaUBnbWFpbC5jb20wHhcNMTQwMzEyMDEzODU2WhcNMTUwMzEy
MDEzODU2WjCBgjELMAkGA1UEBhMCVFcxDzANBgNVBAcMBlRhaXBlaTEQMA4GA1UE
CgwHaWplQ29ycDERMA8GA1UECwwIU2VjdXJpdHkxGDAWBgNVBAMMD3d3dy5pamVj
b3JwLmNvbTEjMCEGCSqGSIb3DQEJARYUbWlsbGVyLmxhaUBnbWFpbC5jb20wggEi
MA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQCy+8lfTA8qEdeBBanmQUM1t0e6
vlhCMq3pmb7Igldzyods+ZAVHUptzPyB4i7CH+39rkXNzslsQ/FH76qN+OMyUhCA
2wq1+r6Rh5QIV4aOa4b2YwUtUzzDTEitTe7y3nxonfcBW6GZLmlguLFZTNhpes9H
uzzGb12ZCF3oXr004lIAnHBiiUuqxozXDaD81x2/GJrU4rb4Jl0Lu7CvTX7u+CLM
GH+3pLQSwXg07Skws/qTlwRMojI79MuYUm/qEasBjQO207TLDvg9zy+uO1HJHRYV
4tbMizWO33DCxRi3j59Co9mZVcB086tAmNAKTZkSLpJb1tPWcywBdpbqjf3VAgMB
AAEwDQYJKoZIhvcNAQEFBQADggEBAIqbY8rKAJ40JIXmtJJSWrLzZ5R9OXyxKWHy
N/8Szn8oLMeSPX6Fmd2oWGbJEDuHf17l9IkbKnfDIrlW+lbVG4qY2fZmH5GA2rmP
bWMrIzXqH+NJfp9NyUhhCCT59fDrHKiGf13opR8gvOo2ft0GOQ+kZsUn/7t1GoQ9
3Ybch2CNjfob5sEQ+nGDsyg3IRFnunAzeAdKLS9h4BwZOdz2lvVnHNR4Wb/TygDZ
mVL2ntg2uWCWQvJ5QCZlyjXthtp0EeDNiJeL3ThOlsiKujuhjMrqjThZuGqvpVxO
3v46/+iqCkdGuP/C2mZqJwbNYvOoPCBCeBYMXGpDrC2VqwYULy4=
-----END CERTIFICATE-----

此外,如果想要直接從 private key 產生 self-signed 憑證也是可以, 執行的指令如下:

~$ openssl req -new -x509 -days 365 -key private.key -out self-signed-2.crt

但是, 因為 Self-signed 憑證並不是透過公認與具有公信力的 CA 所發行的憑證。因此,當 Web 伺服器是安裝 Self-signed 的憑證來支援 SSL時,一般不同的瀏覽器在第一次瀏覽此網站時,遇到 self-signed 憑證時, 會跳出類似如下圖的畫面。



無論你是透過 CA 取得憑證或是自行產生 self-signed 的憑證, 都可以透過 openssl 指令來檢測憑證的內容,透過以下的指令:
~$ openssl x509 -text -in self-signed.crt -noout

執行後輸出的憑證內容如下,因為它是 self-signed 的憑證,所以只會有基本的資訊:
Certificate:
    Data:
        Version: 1 (0x0)
        Serial Number: 10433359898838879218 (0x90cabd8cb818cbf2)
    Signature Algorithm: sha1WithRSAEncryption
        Issuer: C=TW, L=Taipei, O=ijeCorp, OU=Security, CN=www.ijecorp.com/emailAddress=miller.lai@gmail.com
        Validity
            Not Before: Mar 12 01:38:56 2014 GMT
            Not After : Mar 12 01:38:56 2015 GMT
        Subject: C=TW, L=Taipei, O=ijeCorp, OU=Security, CN=www.ijecorp.com/emailAddress=miller.lai@gmail.com
        Subject Public Key Info:
            Public Key Algorithm: rsaEncryption
                Public-Key: (2048 bit)
                Modulus:
                    00:b2:fb:c9:5f:4c:0f:2a:11:d7:81:05:a9:e6:41:
                    43:35:b7:47:ba:be:58:42:32:ad:e9:99:be:c8:82:
                    57:73:ca:87:6c:f9:90:15:1d:4a:6d:cc:fc:81:e2:
                    2e:c2:1f:ed:fd:ae:45:cd:ce:c9:6c:43:f1:47:ef:
                    aa:8d:f8:e3:32:52:10:80:db:0a:b5:fa:be:91:87:
                    94:08:57:86:8e:6b:86:f6:63:05:2d:53:3c:c3:4c:
                    48:ad:4d:ee:f2:de:7c:68:9d:f7:01:5b:a1:99:2e:
                    69:60:b8:b1:59:4c:d8:69:7a:cf:47:bb:3c:c6:6f:
                    5d:99:08:5d:e8:5e:bd:34:e2:52:00:9c:70:62:89:
                    4b:aa:c6:8c:d7:0d:a0:fc:d7:1d:bf:18:9a:d4:e2:
                    b6:f8:26:5d:0b:bb:b0:af:4d:7e:ee:f8:22:cc:18:
                    7f:b7:a4:b4:12:c1:78:34:ed:29:30:b3:fa:93:97:
                    04:4c:a2:32:3b:f4:cb:98:52:6f:ea:11:ab:01:8d:
                    03:b6:d3:b4:cb:0e:f8:3d:cf:2f:ae:3b:51:c9:1d:
                    16:15:e2:d6:cc:8b:35:8e:df:70:c2:c5:18:b7:8f:
                    9f:42:a3:d9:99:55:c0:74:f3:ab:40:98:d0:0a:4d:
                    99:12:2e:92:5b:d6:d3:d6:73:2c:01:76:96:ea:8d:
                    fd:d5
                Exponent: 65537 (0x10001)
    Signature Algorithm: sha1WithRSAEncryption
         8a:9b:63:ca:ca:00:9e:34:24:85:e6:b4:92:52:5a:b2:f3:67:
         94:7d:39:7c:b1:29:61:f2:37:ff:12:ce:7f:28:2c:c7:92:3d:
         7e:85:99:dd:a8:58:66:c9:10:3b:87:7f:5e:e5:f4:89:1b:2a:
         77:c3:22:b9:56:fa:56:d5:1b:8a:98:d9:f6:66:1f:91:80:da:
         b9:8f:6d:63:2b:23:35:ea:1f:e3:49:7e:9f:4d:c9:48:61:08:
         24:f9:f5:f0:eb:1c:a8:86:7f:5d:e8:a5:1f:20:bc:ea:36:7e:
         dd:06:39:0f:a4:66:c5:27:ff:bb:75:1a:84:3d:dd:86:dc:87:
         60:8d:8d:fa:1b:e6:c1:10:fa:71:83:b3:28:37:21:11:67:ba:
         70:33:78:07:4a:2d:2f:61:e0:1c:19:39:dc:f6:96:f5:67:1c:
         d4:78:59:bf:d3:ca:00:d9:99:52:f6:9e:d8:36:b9:60:96:42:
         f2:79:40:26:65:ca:35:ed:86:da:74:11:e0:cd:88:97:8b:dd:
         38:4e:96:c8:8a:ba:3b:a1:8c:ca:ea:8d:38:59:b8:6a:af:a5:
         5c:4e:de:fe:3a:ff:e8:aa:0a:47:46:b8:ff:c2:da:66:6a:27:
         06:cd:62:f3:a8:3c:20:42:78:16:0c:5c:6a:43:ac:2d:95:ab:
         06:14:2f:2e

以下我們來觀察一個由公開 CA 所 signed 過的憑證內容, 有關 X509v3 extensions 的部分:
Certificate:
    Data:
        Version: 3 (0x2)
        Serial Number:
            18:7a:a9:a8:c2:96:21:0c
        Signature Algorithm: sha256WithRSAEncryption
        Issuer: C=US, O=Apple Inc., OU=Apple Certification Authority, CN=Apple Root CA
        Validity
            Not Before: Feb  1 22:12:15 2012 GMT
            Not After : Feb  1 22:12:15 2027 GMT
        Subject: CN=Developer ID Certification Authority, OU=Apple Certification Authority, O=Apple Inc., C=US
        Subject Public Key Info:
            Public Key Algorithm: rsaEncryption
            RSA Public Key: (2048 bit)
                Modulus (2048 bit):
                    00:89:76:4f:06:5b:9a:41:ee:a5:23:2b:02:a3:5f:
                    d7:73:3f:c0:35:b0:8b:84:0a:3f:06:24:7f:a7:95:
                    3f:eb:4f:0e:93:af:b4:0e:d0:c8:3e:e5:6d:18:b3:
                    1f:e8:89:47:bf:d7:09:08:e4:ff:56:98:29:15:e7:
                    94:9d:b9:35:a3:0a:cd:b4:c0:e1:e2:60:f4:ca:ec:
                    29:78:45:69:69:60:6b:5f:8a:92:fc:9e:23:e6:3a:
                    c2:22:b3:31:4f:1c:ba:f2:b6:34:59:42:ee:b0:a9:
                    02:03:18:91:04:b6:b3:78:2e:33:1f:80:45:0d:45:
                    6f:bb:0e:5a:5b:7f:3a:e7:d8:08:d7:0b:0e:32:6d:
                    fb:86:36:e4:6c:ab:c4:11:8a:70:84:26:aa:9f:44:
                    d1:f1:b8:c6:7b:94:17:9b:48:f7:0b:58:16:ba:23:
                    c5:9f:15:39:7e:ca:5d:c3:32:5f:0f:e0:52:7f:40:
                    ea:be:ac:08:64:95:5b:c9:1a:9c:e5:80:ca:1f:6a:
                    44:1c:6c:3e:c4:b0:26:1f:1d:ec:7b:af:5e:a0:6a:
                    3d:47:a9:58:12:31:3f:20:76:28:6d:1d:1c:b0:c2:
                    4e:11:69:26:8b:cb:d6:d0:11:82:c9:4e:0f:f1:56:
                    74:d0:d9:08:4b:66:78:a2:ab:ac:a7:e2:d2:4c:87:
                    59:c9
                Exponent: 65537 (0x10001)
        X509v3 extensions:
            X509v3 Subject Key Identifier: 
                57:17:ED:A2:CF:DC:7C:98:A1:10:E0:FC:BE:87:2D:2C:F2:E3:17:54
            X509v3 Basic Constraints: critical
                CA:TRUE
            X509v3 Authority Key Identifier: 
                keyid:2B:D0:69:47:94:76:09:FE:F4:6B:8D:2E:40:A6:F7:47:4D:7F:08:5E

            X509v3 CRL Distribution Points: 
                URI:http://crl.apple.com/root.crl

            X509v3 Key Usage: critical
                Digital Signature, Certificate Sign, CRL Sign

 其中幾個比較重要的 X509 Extensions 相關說明如下:
X509v3 Basic Constraints 指定這個憑證是否屬於CA,另外是否為 critical 的限制。
例如:
X509v3 Basic Constraints: critical
    CA:TRUE

X509v3 Key Usage 指定這個憑證可以用來做什麼。
例如:

X509v3 Key Usage: critical
    Certificate Sign, CRL Sign

X509v3 Extended key Usage 指定這個憑證可以用在什麼操作上。
例如:
X509v3 Extended Key Usage: critical
    TLS Web Server Authentication, TLS Web Client Authentication

X509v3 CRL Distribution Points 指定CA所提供的 Revocation List 要去哪邊找。如果這個憑證是每隔一段時間就會更換的話,那麼這個設定就會非常重要。

例如:
X509v3 CRL Distribution Points: 

    Full Name:
      URI:http://www.apple.com/appleca/root.crl

X509v3 Certificate Policies 指定這個憑證是基於什麼 Policy 所簽發出來的。
例如:
X509v3 Certificate Policies: 

    Policy: 1.2.840.113635.100.5.1

      CPS: https://www.apple.com/appleca/

      User Notice:

        Explicit Text: Reliance on this certificate by any party assumes acceptance of the then applicable standard terms and conditions of use, certificate policy and certification practice statements.

Authority Information Access 是用來提供額外的CA資訊,以及即時驗證 certificate revocation的資料。而OCSP是指 CA's Online Certificate Status Protocol 的回應者 (responder)。
例如:
Authority Information Access:
    OCSP - URI: http://ocsp.starfieldtech.com/
    CA Issuers - URI:http://www.cert.com/resp/intermediate.crt

X509v3 Subject Key Identifier 主要在於表示這個 Certificate 包含一個特定的 Public Key。所有的CA Certificate都應該包含這個 extension, 由同一個 CA 所發出來的這些 Certificate 也要包含一個  X509v3 Authority Key Identifier 的 extension , 而它的值會和這個CA 的 Subject Key Identifier 一樣。

X509v3 Authority Key Identifier 指定這張 Certificate 是由哪一個 CA 所發出來的。

以下我們可以透過觀察 Facebook網站使用的 HTTPS Certificate來了解 Authority Key Identifier 和 Subject Key Identifier 的關係。










Root CA 是 DigCert  High  Assurance  EV Root CA 憑證的 Subject Key Identifier 和 Authority Key Identifier 分別如下:

X509v3 Subject Key Identifier: 
    B1:3E:C3:69:03:F8:BF:47:01:D4:98:26:1A:08:02:EF:63:64:2B:C3

X509v3 Authority Key Identifier: 

    keyid:B1:3E:C3:69:03:F8:BF:47:01:D4:98:26:1A:08:02:EF:63:64:2B:C3

Root CA 所簽發的 Intermediate CA - DigiCert SHA2 High Assurance Server CA 憑證的 Subject Key Identifier 和 Authority Key Identifier 分別如下:

X509v3 Subject Key Identifier: 
    51:68:FF:90:AF:02:07:75:3C:CC:D9:65:64:62:A2:12:B8:59:72:3B

X509v3 Authority Key Identifier: 
    keyid:B1:3E:C3:69:03:F8:BF:47:01:D4:98:26:1A:08:02:EF:63:64:2B:C3

最後,由  Intermediate CA 所簽發的最後給Facebook的Web Server的憑證的 Subject Key Identifier 和 Authority Key Identifier 分別如下



X509v3 Subject Key Identifier: 
    43:09:93:40:FA:11:4B:30:33:EC:F2:87:6E:8D:71:18:CF:8A:BC:8E

X509v3 Authority Key Identifier: 
    keyid:51:68:FF:90:AF:02:07:75:3C:CC:D9:65:64:62:A2:12:B8:59:72:3B

在 verify certificate chain 過程中,Facebook 的 Web Server憑證中的 Authority Key Identifier  必須要對應到 Intermediate CA憑證的 Subject Key Identifier 。而 Intermediate CA憑證的 
Authority Key Identifier 必須要對應到 Root CA 憑證的 Subject Key Identifier。

而 Root CA憑證中的Authority Key Identifier 則是對應自己的Subject Key Identifier  

最後,Certificate Chain 的存放順序必須是如下所示,Root CA一定是放在最下方,再依據簽發的順序置放:

-----BEGIN CERTIFICATE-----
Server certificate
-----END CERTIFICATE-----
-----BEGIN CERTIFICATE-----
Intermediate certificate
-----END CERTIFICATE-----
-----BEGIN CERTIFICATE-----
Root CA's certificate
-----END CERTIFICATE-----

Reference:
[1] Standard X.509 V3 Certificate Extensions
[2] Check Certificate Chain
[3] Creating a .pem File for SSL Certificate Installations
[4] CA建置工具:Openssl的管理與使用介紹(上)
[5] How to create SSL X.509 Certificate ?
[6] OpenSSL Cookbook