1. 程式人生 > >深入理解簽名驗籤

深入理解簽名驗籤

數字簽名中,包含了兩個過程:
1.對要簽名的資訊,用指定的hash演算法,獲取資訊的hash值.
2.用私鑰,對hash值進行加密,輸出加密串(也就是簽名值).
以上方式也就是裸簽名,PKCS#1

驗證簽名:
1.對要簽名的資訊(也就是簽名原文),用指定的hash演算法,獲取資訊的hash值.
2.用公鑰資訊,解密簽名值,從中獲取加密的hash串,和上面獲取的hash值進行對比,一致則認為驗籤通過,不一致則不通過.

需要主要,如果呼叫遠端簽名,因為根據要簽名的資料格式的不同,所以我們本地驗籤的時候,也要根據不同的簽名方式,來進行驗證(也就說,他們那邊簽名的時候,真正用來簽名的位元組陣列是怎麼來的)

目前常見的幾種方式:(P1簽名驗籤)

contentType==CT_MESSAGE時,為待簽名的訊息;
contentType==CT_BASE64_DATA時,為待簽名的base64編碼資料;
contentType==CT_HASH時為待簽名的HASH;
contentType==CT_FILE_URL時為待簽名檔案地址URL
contentType== CT_STORAGE時為待簽名內容儲存編號

CT_HASH:告訴簽名方,我這個是對簽名原文hash過了的hash串,你們直接對這個值進行加密即可,不需要再hash了.
所以這種方式,我們本地驗籤的時候,要將原文放入進行驗籤,而不是hash過後的hash值(因為驗籤的時候,它又會hash一次)

  /**
     * 測試hash方式的簽名驗籤
     * @throws Exception 
     */
    @Test
    public void testSignWithHash() {
        try {
            CertificateFactory cf = CertificateFactory.getInstance("X.509");  
            Certificate usercert = cf.generateCertificate(new FileInputStream(USER_CERT)); 
            PublicKey publicKey = usercert.getPublicKey
(); //讀取pfx證書上的祕鑰對資訊 BouncyCastleProvider provider = new BouncyCastleProvider(); Signature signature = Signature.getInstance("SHA1withRSA"); MessageDigest digest = MessageDigest.getInstance("SHA-1", provider); List<UserInfo> userInfoList = userInfoService.findAll(); UserInfo userInfo = userInfoList.get(0); Map<String, Object> puserCert = new HashMap<>(); puserCert.put("userInfo", userInfo); List<UserCert> userCertList = userCertService.getListByParam(puserCert); UserCert userCert = userCertList.get(0); YuanZi_P1SignRequest yuanZiP1SignRequest = new YuanZi_P1SignRequest(); yuanZiP1SignRequest.setAlias(userInfo.getAlias()); yuanZiP1SignRequest.setHashAlg("SHA1"); yuanZiP1SignRequest.setContentType("CT_HASH"); String content = "我是簽名原文"; byte[] dataToSign = digest.digest(content.getBytes(Charset.forName("UTF-8"))); System.out.println("dataToSign:"+Base64.encodeBytes(dataToSign)); yuanZiP1SignRequest.setContent(Base64.encodeBytes(dataToSign)); YuanZi_P1SignResponse yuanZiP1SignResponse = yuanZiService.signP1(yuanZiP1SignRequest); System.out.println("原子服務返回的簽名值:"+yuanZiP1SignResponse.getSignedData()); byte[] dataHadSignature = Base64.decode(yuanZiP1SignResponse.getSignedData()); YuanZi_VerifyP1SignRequest yuanZiVerifyP1SignRequest = new YuanZi_VerifyP1SignRequest(); yuanZiVerifyP1SignRequest.setSignedData(yuanZiP1SignResponse.getSignedData()); yuanZiVerifyP1SignRequest.setContentType("CT_HASH"); yuanZiVerifyP1SignRequest.setContent(Base64.encodeBytes(dataToSign)); yuanZiVerifyP1SignRequest.setHashAlg("SHA1"); yuanZiVerifyP1SignRequest.setCert(userCert.getCertBuf().getCertsignBuf()); YuanZi_VerifyP1SignResponse yuanZiVerifyP1SignResponse = yuanZiService.verifyP1Sign(yuanZiVerifyP1SignRequest); System.out.println(yuanZiVerifyP1SignResponse.getCode()); //本地驗籤 boolean verify=false; signature.initVerify(usercert); //使用公鑰初始化簽名物件,用於驗證簽名 signature.update(content.getBytes(Charset.forName("UTF-8"))); verify= signature.verify(dataHadSignature); //得到驗證結果 System.out.println("本地的驗簽結果:"+verify); } catch (Exception e) { e.printStackTrace(); // TODO: handle exception } }

CT_MESSAGE:即,告訴簽名方,我發過去的內容,就是簽名的原文.一般來說,因為簽名值都是位元組陣列,所以簽名方會根據約定的編碼方式,對這個簽名原文按規定的編碼方式,取它的位元組陣列之後,進行簽名

我們本地驗籤的時候,也要對原文做同樣的操作

 /**
         * 測試原子服務P1方式的簽名和驗籤通過
         * @throws Exception 
         */
        @Test
        public void testSign_P1_CT_MESSAGE() {
            try {
                CertificateFactory cf = CertificateFactory.getInstance("X.509");  
                Certificate usercert = cf.generateCertificate(new FileInputStream(USER_CERT)); 
                PublicKey publicKey = usercert.getPublicKey();
                //讀取pfx證書上的祕鑰對資訊
                BouncyCastleProvider provider = new BouncyCastleProvider();
                Signature signature = Signature.getInstance("SHA1withRSA"); 
                MessageDigest digest = MessageDigest.getInstance("SHA-1", provider);

                List<UserInfo> userInfoList = userInfoService.findAll();
                UserInfo userInfo = userInfoList.get(0);
                Map<String, Object> puserCert = new HashMap<>();
                puserCert.put("userInfo", userInfo);
                List<UserCert> userCertList = userCertService.getListByParam(puserCert);
                UserCert userCert = userCertList.get(0);
                YuanZi_P1SignRequest yuanZiP1SignRequest = new YuanZi_P1SignRequest();
                yuanZiP1SignRequest.setAlias(userInfo.getAlias());
                yuanZiP1SignRequest.setHashAlg("SHA1");
                yuanZiP1SignRequest.setContentType("CT_MESSAGE");
                String content = "我是簽名原文";
                yuanZiP1SignRequest.setContent(content);
                YuanZi_P1SignResponse yuanZiP1SignResponse = yuanZiService.signP1(yuanZiP1SignRequest);
                System.out.println("原子服務返回的簽名值:"+yuanZiP1SignResponse.getSignedData());
                byte[] dataHadSignature = Base64.decode(yuanZiP1SignResponse.getSignedData());

                YuanZi_VerifyP1SignRequest yuanZiVerifyP1SignRequest = new YuanZi_VerifyP1SignRequest();
                yuanZiVerifyP1SignRequest.setSignedData(yuanZiP1SignResponse.getSignedData());
                yuanZiVerifyP1SignRequest.setContentType("CT_MESSAGE");
                yuanZiVerifyP1SignRequest.setContent(content);
                yuanZiVerifyP1SignRequest.setHashAlg("SHA1");
                yuanZiVerifyP1SignRequest.setCert(userCert.getCertBuf().getCertsignBuf());
                YuanZi_VerifyP1SignResponse yuanZiVerifyP1SignResponse = yuanZiService.verifyP1Sign(yuanZiVerifyP1SignRequest);
                System.out.println(yuanZiVerifyP1SignResponse.getCode());

                //本地驗籤
                boolean verify=false;  
                signature.initVerify(usercert);  //使用公鑰初始化簽名物件,用於驗證簽名  
                signature.update(content.getBytes(Charset.forName("UTF-8")));
                verify= signature.verify(dataHadSignature); //得到驗證結果  
                System.out.println("本地的驗簽結果:"+verify);


            } catch (Exception e) {
                e.printStackTrace();
                // TODO: handle exception
            }

        }

CT_BASE64_DATA:因為有時候,我們要簽名的原文,它就是一個byte[],但是為了方便傳輸,一般簽名方都要求接受的是一個字串.所以就有了這種簽名方式.即:我們需要對簽名原文的byte[]做base64編碼,簽名方收到之後,再做解碼,並且把解碼後的byte[]進行簽名.
所以我們本身驗籤的時候,只要傳入byte[]就行.
第三方返回的簽名值,一般也是簽名值byte[]做base64編碼後的字串,所以我們要做base64解碼,才能獲取到簽名值byte[].

/**
         * 測試原子服務P1方式的CT_BASE64_DATA簽名和驗籤通過
         * @throws Exception 
         * 
         * 注意多種簽名方式,你傳給它要簽名的資料,和它最後執行簽名的時候,真正要籤的那個原文可能跟你傳的不是同一個(比如需要做String 轉為byte,base64編碼要解碼)
         */
        @Test
        public void testSign_P1_CT_BASE64_DATA() {
            try {
                CertificateFactory cf = CertificateFactory.getInstance("X.509");  
                Certificate usercert = cf.generateCertificate(new FileInputStream(USER_CERT)); 
                PublicKey publicKey = usercert.getPublicKey();
                //讀取pfx證書上的祕鑰對資訊
                BouncyCastleProvider provider = new BouncyCastleProvider();
                Signature signature = Signature.getInstance("SHA1withRSA"); 
                MessageDigest digest = MessageDigest.getInstance("SHA-1", provider);

                List<UserInfo> userInfoList = userInfoService.findAll();
                UserInfo userInfo = userInfoList.get(0);
                Map<String, Object> puserCert = new HashMap<>();
                puserCert.put("userInfo", userInfo);
                List<UserCert> userCertList = userCertService.getListByParam(puserCert);
                UserCert userCert = userCertList.get(0);
                YuanZi_P1SignRequest yuanZiP1SignRequest = new YuanZi_P1SignRequest();
                yuanZiP1SignRequest.setAlias(userInfo.getAlias());
                yuanZiP1SignRequest.setHashAlg("SHA1");
                yuanZiP1SignRequest.setContentType("CT_BASE64_DATA");
                byte[] dataToSign = new byte[10];
                String base64ToSign = Base64.encodeBytes(dataToSign);

                yuanZiP1SignRequest.setContent(base64ToSign);
                YuanZi_P1SignResponse yuanZiP1SignResponse = yuanZiService.signP1(yuanZiP1SignRequest);
                System.out.println("原子服務返回的簽名狀態:"+yuanZiP1SignResponse.getCode());
                System.out.println("原子服務返回的簽名值:"+yuanZiP1SignResponse.getSignedData());
                //簽名後的
                byte[] dataHadSignature = Base64.decode(yuanZiP1SignResponse.getSignedData());

                YuanZi_VerifyP1SignRequest yuanZiVerifyP1SignRequest = new YuanZi_VerifyP1SignRequest();
                yuanZiVerifyP1SignRequest.setSignedData(yuanZiP1SignResponse.getSignedData());
                yuanZiVerifyP1SignRequest.setContentType("CT_BASE64_DATA");
                yuanZiVerifyP1SignRequest.setContent(base64ToSign);
                yuanZiVerifyP1SignRequest.setHashAlg("SHA1");
                yuanZiVerifyP1SignRequest.setCert(userCert.getCertBuf().getCertsignBuf());
                YuanZi_VerifyP1SignResponse yuanZiVerifyP1SignResponse = yuanZiService.verifyP1Sign(yuanZiVerifyP1SignRequest);
                System.out.println(yuanZiVerifyP1SignResponse.getCode());
                System.out.println(yuanZiVerifyP1SignResponse.getMessage());
                //本地驗籤
                boolean verify=false;  
                signature.initVerify(publicKey);  //使用公鑰初始化簽名物件,用於驗證簽名  
                //原子
                signature.update(dataToSign);
                verify= signature.verify(dataHadSignature); //得到驗證結果  
                System.out.println("本地的驗簽結果:"+verify);


            } catch (Exception e) {
                e.printStackTrace();
                // TODO: handle exception
            }

        }

P7簽名:
P7簽名其實也就是P1簽名的基礎上,附加一些其他的資訊(因為P1是裸簽名,只有加密串,除了本人自己,根本不知道簽名證書,公鑰,簽名演算法,簽名用的hash演算法,時間戳,簽名原文等等資訊是什麼,不便於驗證簽名,所以就需要有P7簽名作為補充,其實也就是在P1之後,在加密串的基礎上,附加這些資訊上去)

所以我們在itextpdf的原始碼上能看到,其實它做簽名的時候,也就是先構建出一個要簽名的字串,然後丟給某個私鑰進行簽名,獲得簽名值,它再對這個簽名值附加:證書鏈,時間戳等等資訊,然後構建出一個P7簽名,然後放入PDF檔案中.

PKCS#1:定義RSA公開金鑰演算法加密和簽名機制,主要用於組織PKCS#7中所描述的數字簽名和數字信封[22]。
PKCS#7:定義一種通用的訊息語法,包括數字簽名和加密等用於增強的加密機制,PKCS#7與PEM相容,所以不需其他密碼操作,就可以將加密的訊息轉換成PEM訊息[26]。