ECC公鑰格式詳解

本文首先介紹公鑰格式相關的若干概念/技術,隨後以示例的方式剖析DER格式的ECC公鑰,最後介紹如何使用Java生成、解析和使用ECC公鑰。html

ASN.1

Abstract Syntax Notation One (ASN.1)是一種接口描述語言,提供了一種平臺無關的描述數據結構的方式。ASN.1是ITU-T、ISO、以及IEC的標準,普遍應用於電信和計算機網絡領域,尤爲是密碼學領域。java

ASN.1與你們熟悉的Protocol BuffersApache Thrift很是類似,均可以經過schema來定義數據結構,提供跨平臺的數據序列化和反序列化能力。不一樣的是,ASN.1早在1984年就被定爲標準,比這二者要早不少年,並獲得了普遍的應用,被用來定義了不少世界範圍內普遍使用的數據結構,有大量的RFC文檔使用ASN.1定義協議、數據格式等。好比https所使用的X.509證書結構,就是使用ASN.1定義的。算法

ASN.1定義了若干基礎的數據類型和結構類型:apache

Topic Description
Basic Types BIT STRING
BOOLEAN
INTEGER
NULL
OBJECT IDENTIFIER
OCTET STRING
String Types BMPString
IA5String
PrintableString
TeletexString
UTF8String
Constructed Types SEQUENCE
SET
CHOICE

上述的基礎類型能夠在這裏找到詳盡的解釋。咱們可使用這些來描述咱們本身的數據結構:windows

FooQuestion ::= SEQUENCE {
        trackingNumber INTEGER,
        question       IA5String
    }

如上定義了一個名爲FooQuestion的數據結構。它是一個SEQUENCE結構,包含了一個INTEGER一個IA5String
一個具體的FooQuestion能夠描述爲:網絡

myQuestion FooQuestion ::= {
        trackingNumber     5,
        question           "Anybody there?"
    }

用ASN.1定義的數據結構實例,能夠序列化爲二進制的BER、文本類型的JSON、XML等。數據結構

Object Identifier

Object Identifier (OID)是一項由ITU和ISO/IEC制定的標準,用來惟一標識對象、概念,或者其它任何具備全球惟一特性的東西。dom

一個OID表現爲用.分隔的一串數字,好比橢圓曲線secp256r1的OID是這樣:ide

1.2.840.10045.3.1.7

其每一個數字的含義以下:工具

iso(1) member-body(2) us(840) ansi-X9-62(10045) curves(3) prime(1) 7

OID是全局統一分配的,所有的OID能夠看作一棵多叉樹,每個有效的OID表現爲樹上的一個節點。當前全部的OID能夠在這裏找到。

OID是ASN.1的基本類型。

BER & DER

Basic Encoding Rules (BER)是一種自描述的ASN.1數據結構的二進制編碼格式。每個編碼後的BER數據依次由數據類型標識(Type identifier),長度描述(Length description), 實際數據(actual Value)排列而成,即BER是一種二進制TLV編碼。TLV編碼的一個好處,是數據的解析者不須要讀取完整的數據,僅從一個不完整的數據流就能夠開始解析。

Distinguished Encoding Rules (DER)是BER的子集,主要是消除了BER的一些不肯定性的編碼規則,好比在BER中Boolean類型true的value字節,能夠爲任何小於255大於0的整數,而在DER中,value字節只能爲255。DER的這種肯定性,保證了一個ASN.1數據結構,在編碼爲爲DER後,只會有一種正確的結果。這使得DER更適合用在數字簽名領域,好比X.509中普遍使用了DER。

關於各類ASN.1數據類型是如何被編碼爲DER,能夠在這裏找到詳盡的解釋。

若是有DER數據須要解析查看內容,這裏有一個很方便的在線工具

用DER來編碼ASN.1小節中自定義的myQuestion以下:

0x30 0x13 0x02 0x01 0x05 0x16 0x0e 0x41 0x6e 0x79 0x62 0x6f 064 0x79 0x20 0x74 0x68 0x65 0x72 0x65 0x3f
---  ---  ---  ---  ---  ---  ---  --------------------------------------------------------------------
 ^    ^    ^    ^    ^    ^    ^                                   ^
 |    |    |    |    |    |    |                                   |
 |    |    | INTEGER | IA5STRING                                   |
 |    |    | LEN=1   | TAG     |                                   |
 |    |    |         |         |                                   |
 |    | INTEGER   INTEGER   IA5STRING                          IA5STRING
 |    | TAG       VALUE(5)  LEN=14                             VALUE("Anybody there?")
 |    |
 |    |  ----------------------------------------------------------------------------------------------
 |    |                                              ^
 |  SEQUENCE LEN=19                                  |
 |                                                   |
SEQUENCE TAG                                  SEQUENCE VALUE

PEM

DER格式是ASN.1數據的二進制編碼,計算機處理方便,但不利於人類處理,好比不方便直接在郵件正文中粘貼發送。PEM是DER格式的BASE64編碼。除此以外,PEM在DER的BASE64先後各增長了一行,用來標識數據內容。示例以下:

-----BEGIN PUBLIC KEY-----
MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQDMYfnvWtC8Id5bPKae5yXSxQTt
+Zpul6AnnZWfI2TtIarvjHBFUtXRo96y7hoL4VWOPKGCsRqMFDkrbeUjRrx8iL91
4/srnyf6sh9c8Zk04xEOpK1ypvBz+Ks4uZObtjnnitf0NBGdjMKxveTq+VE7BWUI
yQjtQ8mbDOsiLLvh7wIDAQAB
-----END PUBLIC KEY-----

X.509

X.509是一項描述公鑰證書結構的標準,普遍使用在HTTPS協議中,定義在RFC 3280

X.509使用ASN.1來描述公鑰證書的結構,一般編碼爲DER格式,也能夠進一步BASE64編碼爲可打印的PEM格式。V3版本的X.509結構以下:

Certificate  ::=  SEQUENCE  {
        tbsCertificate       TBSCertificate,
        signatureAlgorithm   AlgorithmIdentifier,
        signatureValue       BIT STRING  }

    TBSCertificate  ::=  SEQUENCE  {
        version         [0]  EXPLICIT Version DEFAULT v1,
        serialNumber         CertificateSerialNumber,
        signature            AlgorithmIdentifier,
        issuer               Name,
        validity             Validity,
        subject              Name,
        subjectPublicKeyInfo SubjectPublicKeyInfo,
        issuerUniqueID  [1]  IMPLICIT UniqueIdentifier OPTIONAL,
                             -- If present, version MUST be v2 or v3
        subjectUniqueID [2]  IMPLICIT UniqueIdentifier OPTIONAL,
                             -- If present, version MUST be v2 or v3
        extensions      [3]  EXPLICIT Extensions OPTIONAL
                             -- If present, version MUST be v3
        }

   Version  ::=  INTEGER  {  v1(0), v2(1), v3(2)  }

   CertificateSerialNumber  ::=  INTEGER

   Validity ::= SEQUENCE {
        notBefore      Time,
        notAfter       Time }

   Time ::= CHOICE {
        utcTime        UTCTime,
        generalTime    GeneralizedTime }

   UniqueIdentifier  ::=  BIT STRING

   SubjectPublicKeyInfo  ::=  SEQUENCE  {
        algorithm            AlgorithmIdentifier,
        subjectPublicKey     BIT STRING  }

   Extensions  ::=  SEQUENCE SIZE (1..MAX) OF Extension

   Extension  ::=  SEQUENCE  {
        extnID      OBJECT IDENTIFIER,
        critical    BOOLEAN DEFAULT FALSE,
        extnValue   OCTET STRING  }

SubjectPublicKeyInfo

如上一節所示,SubjectPublicKeyInfo是公鑰證書格式X.509的組成部分。SubjectPublicKeyInfo結構使用ASN.1描述,其中使用了橢圓曲線公私鑰加密算法的SubjectPublicKeyInfo結構定義在RFC 5480

其結構以下:

SubjectPublicKeyInfo  ::=  SEQUENCE  {
        algorithm            AlgorithmIdentifier,
        subjectPublicKey     BIT STRING
   }

   AlgorithmIdentifier  ::=  SEQUENCE  {
        algorithm   OBJECT IDENTIFIER,
        parameters  ANY DEFINED BY algorithm OPTIONAL
   }

能夠看到AlgorithmIdentifier也是一個SEQUENCE,其parameters部分取決於algorithm的具體取值。

對不限制的ECC公鑰使用算法的場景,algorithm取值:

1.2.840.10045.2.1

即: iso(1) member-body(2) us(840) ansi-X9-62(10045) keyType(2) 1

在該種類場景下,parameters的定義以下:

ECParameters ::= CHOICE {
        namedCurve         OBJECT IDENTIFIER
    }

即parameters指定了ECC公鑰所使用的橢圓曲線。其可選的值有:

secp192r1 OBJECT IDENTIFIER ::= {
        iso(1) member-body(2) us(840) ansi-X9-62(10045) curves(3) prime(1) 1 }

    sect163k1 OBJECT IDENTIFIER ::= {
        iso(1) identified-organization(3) certicom(132) curve(0) 1 }

    sect163r2 OBJECT IDENTIFIER ::= {
        iso(1) identified-organization(3) certicom(132) curve(0) 15 }

    secp224r1 OBJECT IDENTIFIER ::= {
        iso(1) identified-organization(3) certicom(132) curve(0) 33 }

    sect233k1 OBJECT IDENTIFIER ::= {
        iso(1) identified-organization(3) certicom(132) curve(0) 26 }

    sect233r1 OBJECT IDENTIFIER ::= {
        iso(1) identified-organization(3) certicom(132) curve(0) 27 }

    secp256r1 OBJECT IDENTIFIER ::= {
        iso(1) member-body(2) us(840) ansi-X9-62(10045) curves(3) prime(1) 7 }

    sect283k1 OBJECT IDENTIFIER ::= {
        iso(1) identified-organization(3) certicom(132) curve(0) 16 }

    sect283r1 OBJECT IDENTIFIER ::= {
        iso(1) identified-organization(3) certicom(132) curve(0) 17 }

    secp384r1 OBJECT IDENTIFIER ::= {
        iso(1) identified-organization(3) certicom(132) curve(0) 34 }

    sect409k1 OBJECT IDENTIFIER ::= {
        iso(1) identified-organization(3) certicom(132) curve(0) 36 }

    sect409r1 OBJECT IDENTIFIER ::= {
        iso(1) identified-organization(3) certicom(132) curve(0) 37 }

    secp521r1 OBJECT IDENTIFIER ::= {
        iso(1) identified-organization(3) certicom(132) curve(0) 35 }

    sect571k1 OBJECT IDENTIFIER ::= {
        iso(1) identified-organization(3) certicom(132) curve(0) 38 }

    sect571r1 OBJECT IDENTIFIER ::= {
        iso(1) identified-organization(3) certicom(132) curve(0) 39 }

algorithm肯定後,再來看下subjectPublicKey,對ECC公鑰來說,subjectPublicKey就是ECPoint:

ECPoint ::= OCTET STRING

是長度爲65字節的OCTET STRING,其中第一個字節表明ECPoint是否通過壓縮,若是爲0x04,表明沒有壓縮。剩下的64個字節,前32個字節,表示ECPoint的X座標,後32個字節表示ECPoint的Y座標。

OCTET STRING類型的ECPoint在轉換爲BIT STRING類型的subjectPublicKey時,按照大端字節序轉換。

ECC Public Key Example

咱們以一個DER編碼的ECC公鑰爲例,詳細剖析一下X.509 ECC公鑰的格式。公鑰內容以下:

0x30 0x59 0x30 0x13 0x06 0x07 
0x2a 0x86 0x48 0xce 0x3d 0x02 
0x01 0x06 0x08 0x2a 0x86 0x48 
0xce 0x3d 0x03 0x01 0x07 0x03 
0x42 0x00 0x04 0x13 0x32 0x8e 
0x0c 0x11 0x8a 0x70 0x1a 0x9e 
0x18 0xa3 0xa9 0xa5 0x65 0xd8 
0x41 0x68 0xce 0x2f 0x5b 0x11 
0x94 0x57 0xec 0xe3 0x67 0x76 
0x4a 0x3f 0xb9 0xec 0xd1 0x15 
0xd0 0xf9 0x56 0x8b 0x15 0xe6 
0x06 0x2d 0x72 0xa9 0x45 0x56 
0x99 0xb0 0x9b 0xb5 0x30 0x90 
0x8d 0x2e 0x31 0x0e 0x95 0x68 
0xcc 0xcc 0x19 0x5c 0x65 0x53 
0xba

經過前面的介紹,咱們已經知道這是一個ASN.1格式的SubjectPublicKeyInfo的DER編碼,是一個TLV類型的二進制數據。如今咱們逐層解析下:

0x30 (SEQUENCE TAG: SubjectPublicKeyInfo) 0x59 (SEQUENCE LEN=89)
        0x30 (SEQUENCE TAG: AlgorithmIdentifier) 0x13 (SEQUENCE LEN=19)
                0x06 (OID TAG: Algorithm) 0x07 (OID LEN=7)
                        0x2a 0x86 0x48 0xce 0x3d 0x02 0x01 (OID VALUE="1.2.840.10045.2.1": ecPublicKey/Unrestricted Algorithm Identifier)
                0x06 (OID TAG: ECParameters:NamedCurve) 0x08 (OID LEN=8)
                        0x2a 0x86 0x48 0xce 0x3d 0x03 0x01 0x07 (OID VALUE="1.2.840.10045.3.1.7": Secp256r1/prime256v1)
        0x03 (BIT STRING TAG: SubjectPublicKey:ECPoint) 0x42 (BIT STRING LEN=66) 0x00 (填充bit數量爲0)
                0x04 (未壓縮的ECPoint)
                0x13 0x32 0x8e 0x0c 0x11 0x8a 0x70 0x1a 0x9e 0x18 0xa3 0xa9 0xa5 0x65 0xd8 0x41 0x68 0xce 0x2f 0x5b 0x11 0x94 0x57 0xec 0xe3 0x67 0x76 0x4a 0x3f 0xb9 0xec 0xd1 (ECPoint:X)
                0x15 0xd0 0xf9 0x56 0x8b 0x15 0xe6 0x06 0x2d 0x72 0xa9 0x45 0x56 0x99 0xb0 0x9b 0xb5 0x30 0x90 0x8d 0x2e 0x31 0x0e 0x95 0x68 0xcc 0xcc 0x19 0x5c 0x65 0x53 0xba (ECPoint:Y)

Java Code

本節給出使用使用Java來生成ECC公私鑰、編碼解碼ECC公私鑰、使用ECC進行簽名驗籤、加密解密相關的示例代碼供參考。在Java中使用ECC算法有如下幾點須要注意:

  • JDK1.7開始內置了ECC公私鑰生成、簽名驗籤,但沒有實現加密解密,所以須要使用BouncyCastle來作Security Provider;
  • 在Java中使用高級別的加解密算法,好比AES使用256bit密鑰、ECC使用Secp256r1等須要更新JRE的security policy文件,不然會報相似「Illegal key size or default parameters」這樣的錯誤。具體怎樣更換policy文件,能夠參考這裏
  • 實際項目開發過程當中,可能發現有傳遞給Java的公鑰不是完整的X.509 SubjectPublicKeyInfo,好比只傳遞了一個65字節的ECPoint過來,這種狀況能夠跟對方溝通清楚所使用的Algorithm以及NamedCurve,補全DER數據後,再使用Java Security庫解析。
JDK 1.7
依賴:org.bouncycastle:bcprov-jdk15on:1.59

import com.sun.jersey.core.util.Base64;
import java.security.InvalidKeyException;
import java.security.KeyFactory;
import java.security.KeyPair;
import java.security.KeyPairGenerator;
import java.security.NoSuchAlgorithmException;
import java.security.NoSuchProviderException;
import java.security.PrivateKey;
import java.security.PublicKey;
import java.security.SecureRandom;
import java.security.Security;
import java.security.Signature;
import java.security.SignatureException;
import java.security.spec.InvalidKeySpecException;
import java.security.spec.PKCS8EncodedKeySpec;
import java.security.spec.X509EncodedKeySpec;
import javax.crypto.BadPaddingException;
import javax.crypto.Cipher;
import javax.crypto.IllegalBlockSizeException;
import javax.crypto.NoSuchPaddingException;
import org.apache.commons.codec.binary.Hex;
import org.apache.commons.codec.binary.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

public class ECCUtils {

  private static final Logger LOGGER = LoggerFactory.getLogger(ECCUtils.class);
  private static final String PROVIDER = "BC";

  private static final byte[] PUB_KEY_TL= new byte[26];

  static {
    Security.addProvider(new org.bouncycastle.jce.provider.BouncyCastleProvider());

    try {
      KeyPair keyPair = genKeyPair();
      PublicKey publicKeyExample = keyPair.getPublic();
      System.arraycopy(publicKeyExample.getEncoded(), 0, PUB_KEY_TL, 0, 26);
    } catch (Exception e) {
      LOGGER.error("沒法初始化算法", e);
    }
  }

  public static KeyPair genKeyPair() throws NoSuchAlgorithmException, NoSuchProviderException {
    KeyPairGenerator keyPairGenerator = KeyPairGenerator.getInstance("EC", PROVIDER);
    keyPairGenerator.initialize(256, new SecureRandom());
    return keyPairGenerator.generateKeyPair();
  }

  public static String encodePublicKey(PublicKey publicKey) {
    return StringUtils.newStringUtf8(Base64.encode(publicKey.getEncoded()));
  }

  public static PublicKey decodePublicKey(String keyStr)
      throws NoSuchProviderException, NoSuchAlgorithmException {
    byte[] keyBytes = getPubKeyTLV(keyStr);

    X509EncodedKeySpec keySpec = new X509EncodedKeySpec(keyBytes);
    KeyFactory keyFactory = KeyFactory.getInstance("EC", PROVIDER);
    try {
      return keyFactory.generatePublic(keySpec);
    } catch (InvalidKeySpecException e) {
      LOGGER.error("無效的ECC公鑰", e);
      return null;
    }
  }

  private static byte[] getPubKeyTLV(String keyStr) {
    byte[] keyBytes = Base64.decode(StringUtils.getBytesUtf8(keyStr));

    if(keyBytes.length == 65) {
      byte[] tlv = new byte[91];
      System.arraycopy(PUB_KEY_TL, 0, tlv, 0, 26);
      System.arraycopy(keyBytes, 0, tlv, 26, 65);
      return tlv;
    }

    return keyBytes;
  }

  public static String encodePrivateKey(PrivateKey privateKey) {
    return StringUtils.newStringUtf8(Base64.encode(privateKey.getEncoded()));
  }

  public static PrivateKey decodePrivateKey(String keyStr)
      throws NoSuchProviderException, NoSuchAlgorithmException {
    byte[] keyBytes = Base64.decode(StringUtils.getBytesUtf8(keyStr));
    PKCS8EncodedKeySpec keySpec = new PKCS8EncodedKeySpec(keyBytes);
    KeyFactory keyFactory = KeyFactory.getInstance("EC", PROVIDER);
    try {
      return keyFactory.generatePrivate(keySpec);
    } catch (InvalidKeySpecException e) {
      LOGGER.error("無效的ECC私鑰", e);
      return null;
    }
  }

  public static byte[] encrypt(byte[] content, PublicKey publicKey)
      throws NoSuchPaddingException, NoSuchAlgorithmException, InvalidKeyException, BadPaddingException, IllegalBlockSizeException, NoSuchProviderException {
    Cipher cipher = Cipher.getInstance("ECIES", PROVIDER);
    cipher.init(Cipher.ENCRYPT_MODE, publicKey);
    return cipher.doFinal(content);
  }

  public static byte[] decrypt(byte[] content, PrivateKey privateKey)
      throws NoSuchPaddingException, NoSuchAlgorithmException, InvalidKeyException, BadPaddingException, IllegalBlockSizeException, NoSuchProviderException {
    Cipher cipher = Cipher.getInstance("ECIES", PROVIDER);
    cipher.init(Cipher.DECRYPT_MODE, privateKey);
    return cipher.doFinal(content);
  }

  public static byte[] signature(byte[] content, PrivateKey privateKey)
      throws NoSuchAlgorithmException, InvalidKeyException, SignatureException {
    Signature signature = Signature.getInstance("SHA256withECDSA");
    signature.initSign(privateKey);
    signature.update(content);
    return signature.sign();
  }

  public static boolean verify(byte[] content, byte[] sign, PublicKey publicKey)
      throws NoSuchAlgorithmException, InvalidKeyException {
    Signature signature = Signature.getInstance("SHA256withECDSA");
    signature.initVerify(publicKey);
    try {
      signature.update(content);
      return signature.verify(sign);
    } catch (SignatureException e) {
      LOGGER.warn("無效的簽名", e);
      return false;
    }

  }
}

總結:密碼學相關的標準、協議不少,原理每每須要一些數學基礎。想要程序立刻work起來可能容易,想要搞清楚原理,須要花些時間才行。

相關文章
相關標籤/搜索