前言
前段时间,项目上有需求对于重要文件的传输接收时,接收端需要对文件进行安全校验,采用数字签名的方式确保数据来源的安全性以及数据完整性。之前未接触过密码安全方面的知识,现将实施过程中所遇所学记录下来~
本文将按照以下知识内容来记录:
1 数字签名概念
2 实现方式介绍
3 JCA步骤分析
上述三个部分,由原理 -> 实现 -> 代码,按实现逻辑顺序展开描述~
正文
1 数字签名概念
1.1 概述
互联网作为一种当今社会普遍使用的信息交换平台,越来越成为人们生活与工作中不可或缺的一部分,但是各种各样的具有创新性、高复杂度的网络攻击方法也是层出不穷。为了保障网络中数据传输的保密性、完整性、传输服务的可用性和传输实体的真实性、可追溯性、不可否认性,通常情况下所采用的方法除了被动防卫型的安全技术(如防火墙技术),很大程度上依赖基于密码学的网络信息加密技术。
数字签名是公钥密码学发展过程中衍生出的一种安全认证技术,主要用于提供实体认证、认证密钥传输和认证密钥协商等服务。简单来说,发送消息的一方可以通过添加一个起签名作用的编码来代表消息文件的特征,如果文件发生了改变,那么对应的数字签名也会改变,即不同的文件经过编码得到的数字签名是不同的,如果密码攻击者视图对原始消息进行篡改和冒充等操作,由于无法获取消息发送方的私钥,因此也就无法得到正确的数字签名;若消息的接收方视图否认和伪造数字签名,则公证方可以用正确的数字签名对消息进行验证,判断消息是否来源于发送方,这样的方式很好的保证了消息文件的完整性、鉴定发送者身份的真实性与不可否认性。
1.2 数字签名基本概念
一个数字签名方案需要满足以下三个条件:不可否认性、不可伪造性、可仲裁性。
数字签名算法与公钥加密算法类似,是私钥或公钥控制下的数学变换,而且通过可以从公钥加密算法派生而来。签名是利用签名者的私钥对消息进行计算、变换,验证则是利用公钥检验该签名是否是由签名者签署的。只有掌握了签名者的私钥才能得到签名者的签名,从而实现不可否认性、不可伪造性和可仲裁性。
数字签名是公钥密码体制衍生出来的最重要的技术之一。相较于在纸上书写的物理签名,数字签名首先对信息进行处理,再将其绑附一个私钥进行传输,形成一个比特串的标识形式,可以实现用户对电子信息来源的认证,并对信息进行签名,确认并保证信息的完整性与有效性。
1.3 数字签名原理
原理:
(1)系统的初始化,生成数字签名中所需的参数
(2)发送方使用摘要函数对消息计算消息摘要
(3)发送方使用自己的私钥对消息摘要进行加密,得到数字签名
(4)发送方把消息原文和数字签名发送给接收方
(5)接收方通过使用发送方的公钥对数字签名进行解密来验证发送方的签名,如果能够基于消息摘要以及公钥能够对数字签名成功解密,那么就能够确保数据确实来源于发送方并且数据在网络传输的过程中,并未被修改
原理图:
1.4 数字签名方案定义
一般来说,数字签名方案由三个集合和三个算法组成。三个集合分别是消息空间、签名空间和密钥空间(公钥和私钥);三个算法如下:
(1)密钥生成算法(Generate)
这是一个概率多项式时间算法,输入为安全参数,输出 私钥sk 和 公钥 pk。
(2)签名生成算法(Sign)
这是一个多项式时间算法,输入为 私钥sk 和 带签消息m,输出对应于私钥sk的关于消息m的 签名sig(m)。
(3)签名验证算法(Verify)
验签算法,输入为 签名值sig(m)、消息m 和 公钥pk,输出为 正确(true)或者 错误(false)。
2 实现方式介绍
国际开源界已经为我们提供了两个密码学相关的函数库:OpenSSL 和 JCA/JCE( JCA 全称是 Java Cryptography Architecture,Java密码学框架;JCE 全称是 Java Cryptography Extension,Java密码学扩展)。
国际库对国产算法支持不那么友好,国产库可以更好的支持国产密算法的密码函数库。例如 BouncyCastle 开源库。
从功能上讲 OpenSSL 更为强大,OpenSSL 不但提供了编程用的 API 函数,还提供了强大的命令行工具,可以通过命令来进行常用的加解密、签名验签、证书操作等功能。而 JCA/JCE 是纯粹用 Java 实现的,OpenSSL 是基于 C语言 实现的。
2.1 OpenSSL
2.1.1 语言使用方式适配
C语言
C语言 + JNI
2.1.2 OpenSSL支持的对称加密算法
OpenSSL一共提供了8种对称加密算法,其中7种事分组加密算法,仅有1种流加密算法是 RC4。7种分组加密算法分别是 AES、DES、Blowfish、CAST、IDEA、RC2、RC4、RC5。均支持电子密码本模式(ECB)、加密分组链接模式(CBC)、加密反馈模式(CFB)和输出反馈模式(OFB)这4种常用的分组密码加密模式。其中,AES使用的加密反馈模式(CFB)和输出反馈模式(OFB)的分组长度为128位,其他算法使用的是64位。
2.1.3 OpenSSL支持的非对称加密算法
OpenSSL一共实现4种非对称加密算法,包括 DH算法、RSA算法、DSA算法 和 ECC椭圆曲线算法。DH算法一般用于密钥交换;RSA算法既可用于密钥交换,也可以用于数字签名,数据加解密也能支持,不过执行速度缓慢;DSA算法一般只用于数字签名。ECC也可用于数字签名。
2.1.4 OpenSSL支持的信息摘要算法
OpenSSL实现了5种信息摘要算法,分别是 MD2、MD5、MDC2、SHA(SHA1)、RIPEMD 和 DSS。OpenSSL实现了 DSS 标准中规定的两种信息摘要算法:DSS 和 DSS1。
2.1.5 OpenSSL密钥和证书管理
OpenSSL实现了 ASN.1 的证书和密钥相关标准,提供了对证书、公钥、私钥、证书请求以及 CRL 等数据对象的 DER、PEM 和 BASE64 的编解码能力。OpenSSL提供了产生各种公开密钥对和对称密钥的方法,同时提供了对公钥和私钥的 DER 编解码能力,并实现了私钥的 PKCS#12 和 PKCS#8 编解码功能。OpenSSL 在标准中提供了对私钥的加密保护功能,使得密钥可以安全的进行存储和分发。
OpenSSL实现了对证书的 X.509 标准编解码、PKCS#12 格式的编解码 以及 PKCS#7格式的编解码功能。提供了一种文本数据库,支持证书的管理功能,包括证书密钥产生、请求产生、证书签发、吊销和验证等功能。
OpenSSL提供的 CA 应用程序就是一个小型的证书管理中心,实现了证书签发的整个流程和证书管理的大部分机制。
2.1.6 OpenSSL命令行工具
2.1.6.1 ECC secp256r1 生成私钥
描述:基于 secp256r1 生成 私钥
命令:openssl ecparam -genkey -name prime256v1 -noout -out private.pem
运行:
2.1.6.2 查看私钥
描述:查看生成的 私钥 文件,也会显示 对应 公钥的 x 和 y
命令:openssl ec -in private.pem -text -noout
运行:
2.1.6.3 基于私钥生成公钥
描述:基于 私钥 文件,生成 公钥 文件
命令:openssl ec -in private.pem -pubout -out public.pem
运行:
2.1.6.4 基于 SHA256 算法摘要签名
描述:基于 私钥 文件以及 SHA256 算法 给 测试文件TextPublicKey.txt 签名,生成签名文件 ec.sig
命令:openssl dgst -sha256 -sign private.pem -out ec.sig TextPublicKey.txt
运行:
2.1.6.5 验签
描述:基于 公钥 文件给 签名文件 ec.sig 验签。验签通过结果为 Verified OK
命令:openssl dgst -sha256 -verify public.pem -signature ec.sig TextPublicKey.txt
运行:
2.2 JCA/JCE
2.2.1 简介
JAVA 中密码安全方面的库即是 JCA(Java Cryptography Architecture,Java密码学框架)。JCA 是 JAVA 安全平台的主要部分,包含一个 “provider” 体系以及一系列 API,用于数字签名、消息摘要、证书与证书验证、加密(对称/非对称,块/流密码)、密钥生成与管理 以及 安全随机数产生等。
JCA 本身并不负责算法的具体实现,任何第三方都可以提供具体的实现并在运行时加载。
2.2.2 JCA 中的密码服务提供者
JCA 引入了 CSP 的概念。CSP 是 JCA 的密码服务提供者,包含一个或多个签名算法、消息摘要算法、密钥产生算法、密钥工厂、密钥库创建与密钥管理、算法参数管理、算法参数产生、证书工厂等。
在 JDK 的典型安装中,安装了一个或几个提供器程序包。用户可以通过静态或动态的方式添加新的提供器,每一个提供器以唯一的名称被引用。用户可以给不同的提供器指定优先级次序。JCA提供了一套 API,允许用户查询安装了哪些提供器以及它们所提供的服务。我们可以来查询下现有支持的提供器有哪些以及提供了哪些服务:
/*
* 函数说明:获取 JCA 默认的 Provider 列表
*/
public void getProviderList() {
Log.d(TAG , "getProviderList");
Provider[] arr = Security.getProviders();
Log.d(TAG , "JCA Provider List:");
int i = 0;
for (Provider p : arr) {
Log.d(TAG , i++ + ": provider = " + p.toString() + "; info = " + p.getInfo());
}
}
运行结果:
D/FirstFragment: getProviderList
D/FirstFragment: JCA Provider List:
D/FirstFragment: 0: provider = AndroidNSSP version 1.0; info = Android Network Security Policy Provider
D/FirstFragment: 1: provider = AndroidOpenSSL version 1.0; info = Android's OpenSSL-backed security provider
D/FirstFragment: 2: provider = CertPathProvider version 1.0; info = Provider of CertPathBuilder and CertPathVerifier
D/FirstFragment: 3: provider = AndroidKeyStoreBCWorkaround version 1.0; info = Android KeyStore security provider to work around Bouncy Castle
D/FirstFragment: 4: provider = BC version 1.61; info = BouncyCastle Security Provider v1.61
D/FirstFragment: 5: provider = HarmonyJSSE version 1.0; info = Harmony JSSE Provider
D/FirstFragment: 6: provider = AndroidKeyStore version 1.0; info = Android KeyStore security provider
上述运行结果是 Android 9 平台中默认的 Provider 提供者列表,以及每个 Provider 所提供的服务:
AndroidNSSP、AndroidOpenSSL、CertPathProvider、AndroidKeyStoreBCWorkaround、BC、HarmonyJSSE、AndroidKeyStore
当多个提供器同时可用时,将建立一个优先级次序。这个次序也是查找请求服务的次序。当最高级提供器中没有提供请求服务时,可以按照优先级次序查询下一个提供器。如果所有提供器都没有提供请求服务时,就会提示 NoSuchAlgorithmException 异常。
如何安装一个提供器?可以采用以下两种方法安装提供器类:
(1)将包含提供器类的 ZIP 和 JAR 文件放在 CLASSPATH 下
(2)将提供器 JAR 文件作为已安装和绑定的文件扩展
下一步,可以用动态或静态的方法将提供器添加到已批准的提供器列表中。如果要在静态条件下实现这一点,可以编辑 Java 安全属性文件,设置的属性是 security.provider.n=masterClassName,此设置定义了一个提供器,并制定了它的优先级次序 n。提供器优先级次序也就是查找请求算法的次序。
masterClassName 指定提供器的主类(master class),这一点在提供器的资源文件中设置,此类总是 Provider类 的子类。它的构造器设置各种属性。在 JCA 的 API 中,提供器通过属性查找请求的算法和其他设施。假设主类是 COM.abcd.provider.Abcd,为了将 Abcd 的优先级设置为3,可以在安全属性文件中加入一行:
security.provider.3=COM.abcd.provider.Abcd
也可以在 Security类 中通过调用 addProvider 或 insertProviderAt 方法动态的注册提供器。注册类型也不是一成不变的,但只能由被赋予充足许可权的可信程序完成。
在 Android 9 原生代码中,如果 java.security 文件没有找到,Security 会进行默认 Provider 添加
路径:android/libcore/ojluni/src/main/java/java/security
文件:Security.java
/*
* Initialize to default values, if <java.home>/lib/java.security
* is not found.
* initializeStatic() 触发在 security.properties 文件找不到时触发
*/
private static void initializeStatic() {
// Android-changed: Use Conscrypt and BC, not the sun.security providers.
/*
props.put("security.provider.1", "sun.security.provider.Sun");
props.put("security.provider.2", "sun.security.rsa.SunRsaSign");
props.put("security.provider.3", "com.sun.net.ssl.internal.ssl.Provider");
props.put("security.provider.4", "com.sun.crypto.provider.SunJCE");
props.put("security.provider.5", "sun.security.jgss.SunProvider");
props.put("security.provider.6", "com.sun.security.sasl.Provider");
*/
props.put("security.provider.1", "com.android.org.conscrypt.OpenSSLProvider");
props.put("security.provider.2", "sun.security.provider.CertPathProvider");
props.put("security.provider.3", "com.android.org.bouncycastle.jce.provider.BouncyCastleProvider");
props.put("security.provider.4", "com.android.org.conscrypt.JSSEProvider");
}
2.3 BouncyCastle库
2.3.1 简介
Bouncy Castle 是一种用于 JAVA 平台的开放源码的轻量级密码技术包,支持大量的密码算法,能够提供数字证书转换所需要的类和方法。基于 JAVA 标准库的 java.security 包提供的标准机制,允许第三方提供商无缝接入。Bouncy Castle 就是一个提供很多算法支持的第三方库。Bouncy Castle 库从 1.59 版本开始已经基本实现支持国密算法(SM2、SM3、SM4)。在 Android 9版本原生代码中发现已经集成了 BouncyCastle 库了。
2.3.2 使用方式
2.3.2.1 官网下载 Jar 包
官网链接:https://www.bouncycastle.org/latest_releases.html
2.3.2.2 注册
注册 Bouncy Castle 是通过下面的语句实现的
Security.addProvider(new org.bouncycastle.jce.provider.BouncyCastleProvider());
注册只需要启动时注册一次,后续就可以使用 Bouncy Castle 提供的所有哈希算法和加密算法了。
3 JCA步骤分析
本章将以 ECC算法 签名与验签为例,基于 JCA 实现功能。
将整个调用逻辑可以按以下顺序描述:
* 基于算法和曲率参数,生成双钥,公钥和私钥
* 私钥签名
* 公钥验签
3.1 java.security 常用类
3.1.1 security 包常用类表
类说明KeyFactoryKeyFactory 用于将密钥(Key 类型的不透明加密密钥)转换成密钥规范(底层密钥的透明表示),反之亦然KeyFactorySpi此类为 KeyFactory 类定义服务提供者接口(SPI)KeyPair此类是简单的密钥对(公钥和私钥)持有者KeyPairGeneratorKeyPairGenerator 类用于生成公钥和私钥对。可以基于特定算法生成密钥对KeyPairGeneratorSpiKeyPairGeneratorSpi 是抽象类,用于实现生成双钥MessageDigestMessageDigest 类用于为应用程序提供信息摘要算法功能,如 MD5 或 SHA 算法MessageDigestSpiMessageDigestSpi 为 MessageDigest 类定义服务提供者接口(SPI)SecureRandomSecureRandom 类提供强加密随机数生成器SignatureSignature 类用来为应用程序提供数字签名算法功能。数字签名用于确保数据的验证和完整性SignatureSpiSignatureSpi 类为 Signature 类定义了服务提供者接口(SPI)
3.1.2 类解析
3.1.2.1 KeyPairGenerator类
功能:生成密钥对
构造:静态方法 getInstance()
介绍:有两种方式可以生成密钥对,以算法无关的方式、以特定算法的方式。
区别:两种方式的区别是初始化;getInstance() 方法中如果指定了算法参数,那么将会以特定算法生成密钥;如果没有指定参数,那么就是以随机的方式生成密钥。
与算法无关的初始化:
所有密钥对生成器共享密钥大小和随机源的概念。随机源将会使用优先级最高的安装程序提供的 SecureRandom实现 作为随机源,如果没有安装的提供程序提供 SecureRandom 的实现,则使用系统提供的随机源。
特定于算法的初始化:
对于已存在一组特定于算法的参数的情况,有 initialize() 方法具有 AlgorithmParameterSpec 参数用于指定算法规范参数。
3.1.2.2 MessageDigest类
功能:MessageDigest 类是一种提供密码安全的消息摘要。
介绍:
密码安全消息摘要采用任意长度的输入,然后产生一个固定长度的输出,称为一个摘要或散列。
对于特定类型的消息摘要算法而言,可通过调用 MessageDigest 类中的 getInstance() 静态方法得到 MessageDigest 对象。第一种方法传入 algorithm 算法名初始化 MessageDigest 对象;第二种方法制定了任意提供器名称,但是必须保证提供器有请求算法的实现。
public static MessageDigest getInstance(String algorithm) throws NoSuchAlgorithmException
public static MessageDigest getInstance(String algorithm, String provider) throws NoSuchAlgorithmException, NoSuchProviderException
为了计算一些数据的摘要,应当向被初始化的消息摘要对象提供数据,这一点可以通过调用 udapte() 方法来实现。
public void update(byte input)
public void update(byte[] input, int offset, int len)
public void update(byte[] input)
通过 update() 方法将数据传递给 MessageDigest 对象后,可以调用 digest() 方法计算摘要。
public byte[] digest()
public byte[] digest(byte[] input)
public int digest(byte[] buf, int offset, int len) throws DigestException
前两个方法会返回已计算的摘要,第三个方法将计算得到的摘要存储在提供器的 buf 缓存中,从 offset 开始,len 是分配给摘要的字节数,返回值是存储在 buf 中的真正字节数。
3.1.2.3 Signature类
功能:Signature 类用来为应用程序提供数字签名算法功能。数字签名用于确保数据的验证和完整性。
介绍:
Signature 对象属于模式对象,也就是说,Signature 对象总是处于某个特定状态,对它只能进行特定类型的操作。对象所处状态在它们各自的类中以最终整型常量表示。通常一个签名对象有三种状态:UNINITIALIZED(未初始化)、SIGN(签名)、VERIFY(验证)。
Signature 对象可以通过静态方法 getInstance() 创建
public static Signature getInstance(String algorithm) throws NoSuchAlgorithmException
public static Signature getInstance(String algorithm, Provider provider) throws NoSuchAlgorithmException
Signature 对象在创建时处于 UNINITIALIZED 状态,所以在使用前必须先对其进行初始化。Signature 类定义了两种初始化方法(initSign 和 initVerify),它们可以分别将 Signature 对象的状态转换成 SIGN 和 VERIFY 状态。
如果要进行签名操作,就需要用产生签名的实体私钥初始化对象,调用 initSign 方法实现,将 Signature 对象切换为 SIGN 状态。
public final void initSign(PrivateKey privateKey) throws InvalidKeyException
如果要进行验签操作,就需要用产生签名的实体公钥初始化对象,调用 initVerify 方法实现,将 Signature 对象切换为 VERIFY 状态。
public final void initVerify(PublicKey publicKey) throws InvalidKeyException
如果 Signature 对象已经进行了签名初始化,就可以将待签署的数据传递给对象,这可通过调用一次或多次 update 方法来实现。
public final void update(byte b) throws SignatureException
public final void update(byte[] data) throws SignatureException
public final void update(byte[] data, int off, int len) throws SignatureException
对 udpate 方法的调用一直持续到所有带签署的数据全部传递给 Signature 对象。
如果需要产生签名,只需要调用 sign() 方法,第一种方法以 byte 数组的形式返回签名。第二种方法返回的签名结果存储在设定的缓冲区 outbuf 中,offset 表示缓冲区的初值,len 表示 outbuf 的长度,这种方法返回的是实际存储的字节数。签名可被带有两个整型参数 r、s 的标准 ASN.1 序列编码。
public final byte[] sign() throws SignatureException
public final int sign(byte[] outbuf, int offset, int len) throws SignatureException
调用 sign() 方法可以重置 Signature 对象的状态,使它返回到调用 initSign 方法前的状态。也就是说 Signature 对象被重置,并且可以通过重新调用 udpate() 和 sign() 用于同一个私钥产生另一个不同的签名,或者重新调用 initSign() 指定不同的私钥,重新调用 initVerify() 初始化 Signature 对象验证签名。
如果 Signature 对象已经进行了验证初始化,处于 VERIFY 状态,可以通过向对象提供验证数据来验证签名是否使相关数据的合法签名。可以通过调用一次或多次 udpate() 方法来实现
public final void update(byte b) throws SignatureException
public final void update(byte[] data) throws SignatureException
public final void update(byte[] data, int off, int len) throws SignatureException
对 update() 方法的调用一直持续到向 Signature 对象提供所有签名数据为止。可以通过 verify() 方法来验证签名。此处传递的参数是私钥签名生成的数据,返回值是 boolean 类型,true 表示验签成功;false 表示验签失败。
public final boolean verify(byte[] signature) throws SignatureException
public final boolean verify(byte[] signature, int offset, int length) throws SignatureException
调用 verify() 方法可以重置 Signature 对象的状态,使它返回到调用 initVerify() 方法前的状态。也就是说 Signature 对象被重置,可以验证身份的另一个签名,该身份的公钥在调用 initVerify() 指定,或者重新调用 initSign() 初始化 Signature 对象来产生签名。
3.2 调用解析
下面步骤以 ECC 算法,secp256r1 参数为例
3.2.1 获取双钥
基于算法和曲率参数,生成双钥,公钥和私钥
public static KeyPair getKeyPair() throws Exception {
Log.d(TAG , "KeyPair()");
// 基于 EC 算法获取 特定算法的 KeyPairGenerateor 对象
KeyPairGenerator keyPairGenerator = KeyPairGenerator.getInstance("EC");
// 获取 EC 参数集对象
ECGenParameterSpec kpgparams = new ECGenParameterSpec("secp256r1");
// 使用指定参数集初始化密钥对生成对象
keyPairGenerator.initialize(kpgparams);
// 获取密钥对,得到 KeyPair 对象,包含公钥和私钥
KeyPair keyPair = keyPairGenerator.generateKeyPair();
return keyPair;
}
// 获取 ECPublickey 公钥对象
public static ECPublicKey getPubKey(KeyPair) {
ECPublicKey pubKey = (ECPublicKey) keyPair.getPublic();
return pubKey;
}
// 获取 ECPrivateKey 私钥对象
public static ECPrivateKey getPriKey(KeyPair) {
ECPrivateKey priKey = (ECPrivateKey) keyPair.getPrivate();
return priKey;
}
3.2.2 私钥签名
/*
* 参数描述:
* String content :要计算签名的数据源
* ECPrivateKey prikey :私钥对象
*/
public static byte[] sign(String content, ECPrivateKey prikey) throws Exception {
Log.d(TAG , "sign()");
// 获取签名对象 signature,参数 SINGATURE 描述签名算法和消息摘要算法
Signature signature = Signature.getInstance(SINGATURE); // SINGATURE 这里使用 “SHA256WITHECDSA”
// 获取 algorithm 参数,这里拿到的就是初始化时传入的 SINGATURE 参数
String algorithm = signature.getAlgorithm();
Log.d(TAG , "sign algorithm suan fa is " + algorithm);
// 调用 initSign 函数,传入私钥,切换签名对象 signature 为 SIGN 状态,准备签名
signature.initSign(prikey);
// 调用 update 函数,将要计算签名的数据源传给 signature 对象
signature.update(content.getBytes());
// 调用签名接口,获取签名数据
byte[] sign = signature.sign();
return sign;
}
3.2.3 公钥验签
/*
* 参数描述:
* String content :签名对应的数据源
* byte[] sign :签名数据
* ECPublicKey publicKey :公钥对象
*/
public static boolean verify(String content, byte[] sign, ECPublicKey publicKey) throws Exception {
Log.d(TAG , "verify()");
// 获取签名对象 signature,参数 SINGATURE 描述签名算法和消息摘要算法
Signature signature = Signature.getInstance(SINGATURE); // 这里是直接使用 “SHA256WITHECDSA”
// 调用 initVerify 函数,传入公钥,切换签名对象 signature 为 VERIFY 状态,准备验签
signature.initVerify(publicKey);
// 调用 udapte 函数,将原始数据传递给 signature 对象
signature.update(content.getBytes());
// 调用验签接口,传入签名数据,返回结果为 布尔类型
boolean result = signature.verify(sign);
return result;
}
3.3 JCA实现验签
在项目中,我们更多的可能是进行验签操作,由上面的描述可知,对于验签实现而言,只要 JCA 支持所用的算法及曲率,提供算法服务,那么可以直接调用接口完成,对于算法的实现原理,在完成需求的前提下,我们可不深入研究。那么对于验签而言,实际上重要就是需要去构造我们所需要的数据给到验签接口,但实际项目中,客户给过来的签名、公钥可能并不能直接使用,需要我们进行一定的转换,才能转变成接口中需要的参数对象。这里一定一定要跟客户对接清楚,他们提供的数据是什么数据,是否裁剪格式,是否仅有效数据。建议问清楚~否则只能在无限排查问题的路上渐行渐远了。经验之谈,避免踩坑。
从上面的验签接口来看,我们所需要的参数有 String content、byte[] sign、ECPublicKey publicKey。
content 是原数据,这个数据具体是什么,取决于你采用什么验签方式,使用原数据还是 Hash 值等,这个根据 SINGATURE 参数确定到底传入什么。
sign 是签名数据,客户使用 私钥 对于 content 生成的签名数据。
publicKey 是公钥对象,ECPublicKey 类型对象,这里的转换比较麻烦,客户给过来的一般是byte类型公钥数据,我们需要把它转换成我们可使用的对象参数。
3.3.1 实现说明
此处我们可以使用 OpenSSL 生成公钥和私钥,用 JCA 对验签过程进行还原。(下面数据展示的是验证所需要的数据,数据没有全部明示,大家若想进行 Demo 验证,可以从本文的 2.1.6 节自行 OpenSSL 命令生成数据进行替换验证)
签名算法:ECC算法
曲率参数:secp256r1
摘要算法:SHA256
公钥:MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEysPwVnknbQsCqW9qGTFIc8MUQhgq7oG+jIDIrLlVhPULKqbqCFNHVz2AMg8aqRTzPG09qyBgUhsFPMPrv27YUg==
私钥:MHcCAQEEIPiHGLkz5BpUTEG6b6hm/N9WAuSVBgOuuMC7eEn5ztTYoAoGCCqGSM49AwEHoUQDQgAEysPwVnknbQsCqW9qGTFIc8MUQhgq7oG+jIDIrLlVhPULKqbqCFNHVz2AMg8aqRTzPG09qyBgUhsFPMPrv27YUg==
签名:生成的签名为 sig 类型文件,直接 cat 会显示乱码(如 2.1.6.4 所示),可以用 od 命令用十六进制输出 ec.sig 文件 (od -v -An -tx1 ec.sig)
原数据:TextPublicKey.txt
3.3.2 验签参数解析
3.3.2.1 原数据 content
前面我们提到 content 数据代表的是 被签数据,当然也有可能是 被签数据的 Hash 值。而区分点在于 Signature signature = Signature.getInstance(SINGATURE)。Signature 对象初始化的方式:
/**
* Creates a Signature object for the specified algorithm.
*
* @param algorithm the standard string name of the algorithm.
* See the Signature section in the <a href=
* "https://docs.oracle.com/javase/8/docs/technotes/guides/security/StandardNames.html#Signature">
* Java Cryptography Architecture Standard Algorithm Name Documentation</a>
* for information about standard algorithm names.
*/
protected Signature(String algorithm) {
this.algorithm = algorithm;
}
algorithm 如下:
从 JDK8 的说明文档中,Signature 类的实例化参数有如上列,是基于 摘要算法+签名算法 的组合形式为参数,表示 JDK8 支持 RSA、DSA、ECDSA 签名算法;MD5、MD2、SHA1、SHA224、SHA256、SHA384、SHA512 摘要算法。
基于本篇示例,JDK 8 对于 ECC 算法的支持有 NONEwithECDSA、SHA1withECDSA、SHA224withECDSA、SHA256withECDSA、SHA384withECDSA、SHA512withECDSA。
问题:那么这些参数有什么区别呢?
我们以 NONEwithECDSA 参数和 SHA256withECDSA 参数分析,当使用 “NONEwithECDSA” 算法进行签名时,会直接对原始数据进行 ECDSA 签名,不会事先对原始数据进行 HASH 计算;当使用 “SHA256withECDSA” 算法进行签名时,会先对原始数据进行 SHA256 算法的 HASH 运算,再对 HASH 结果的二进制形式进行 ECDSA 签名。
问题:那么如何影响验签时传入的参数呢?
对于数字签名而言,是先对数据进行摘要计算,再对摘要进行签名,再进行摘要对比的过程。
在验签时,如果使用了 “NONEwithECDSA” 算法,那我们在验签接口中传入 content 数据时,就不能将原始数据直接传递进去,而是传入该数据的 Hash 值(对应签名时摘要计算的算法),因为这里验签时 NONEwithECDSA 参数,也不会计算该数据的 HASH 值,直接进行验签,所以要求传入的数据就是 HASH 值;
在验签时,如果使用了 “SHA256withECDSA” 算法,那我们在验签接口中传入 content 数据时,就可以直接将原始数据传递进去,因为在验签时 SHA256withECDSA 参数,会先对数据进行 HASH 计算,再对 HASH 结果的二进制形式进行 ECDSA 验签。
3.3.2.2 签名 sign
在进行 ECC 签名的过程中,发现使用相同的原始数据,相同的算法,生成 ECDSA 签名时,每次产生的签名结果的长度与内容均可能不同。完整 ECC 签名数据包含一定格式:
签名格式为 TLV 嵌套格式,签名的主体分为 R 和 S 两部分。R、S 的长度跟 ECC 私钥的长度一致。总体格式如下:
签名格式:30 + LEN1 + 02 + LEN2 + 00(optional) + R + 02 + LEN3 + 00(optional) + S
说明:
00(optional):R、S 前都有 00(optional),00 为可选参数,取决于 R 或 S 的第一个字节是否 大于 0x80。若大于 0x80,则需要在 R 或 S 前加 1 个字节的 0x00
LEN3:0x00(optional) + S 的长度
LEN2:0x00(optional) + R 的长度
LEN1:LEN2 + LEN3 + 4 (这 4 个字节指的是 02 LEN2 和 02 LEN3)
签名总长度:LEN1 + 2 (这 2 个字节指的是 30 和 LEN1)
示例:3046022100a7f981d7e6a315f73d1720ee7b896c1b42b2714965c57cef72728126ab383f9a02210085b7297e9314c22dd4992154067fcb18e36887d7cf9249129cece6c82e48aa40
格式拆分:
30 46
02 21 00 a7f981d7e6a315f73d1720ee7b896c1b42b2714965c57cef72728126ab383f9a
02 21 00 85b7297e9314c22dd4992154067fcb18e36887d7cf9249129cece6c82e48aa40
对于 签名 格式有以下场景,示例如下:
场景一: R 和 S 第一个字节 均大于 0x80
R: 8ef67fbb03cade43ce07aea00d6168fe6c04938fd26cae9ecbb9635b270e5d19
S: 9c46466f23f1fc8caf15645e9ce0bc108169d9f473eea92f154c23474e0f0f84
结果:
30460221008ef67fbb03cade43ce07aea00d6168fe6c04938fd26cae9ecbb9635b270e5d190221009c46466f23f1fc8caf15645e9ce0bc108169d9f473eea92f154c23474e0f0f84
30 46
02 21 00 8ef67fbb03cade43ce07aea00d6168fe6c04938fd26cae9ecbb9635b270e5d19
02 21 00 9c46466f23f1fc8caf15645e9ce0bc108169d9f473eea92f154c23474e0f0f84
场景二: R 的第一个字节 大于 0x80, S 的第一个字节 小于 0x80
R: 8ef67fbb03cade43ce07aea00d6168fe6c04938fd26cae9ecbb9635b270e5d19
S: 6c46466f23f1fc8caf15645e9ce0bc108169d9f473eea92f154c23474e0f0f84
结果:
30450221008ef67fbb03cade43ce07aea00d6168fe6c04938fd26cae9ecbb9635b270e5d1902206c46466f23f1fc8caf15645e9ce0bc108169d9f473eea92f154c23474e0f0f84
30 45
02 21 00 8ef67fbb03cade43ce07aea00d6168fe6c04938fd26cae9ecbb9635b270e5d19
02 20 6c46466f23f1fc8caf15645e9ce0bc108169d9f473eea92f154c23474e0f0f84
场景三:R 的第一个字节 小于 0x80, S 的第一个字节 大于 0x80
R: 6ef67fbb03cade43ce07aea00d6168fe6c04938fd26cae9ecbb9635b270e5d19
S: 9c46466f23f1fc8caf15645e9ce0bc108169d9f473eea92f154c23474e0f0f84
结果:
304502206ef67fbb03cade43ce07aea00d6168fe6c04938fd26cae9ecbb9635b270e5d190221009c46466f23f1fc8caf15645e9ce0bc108169d9f473eea92f154c23474e0f0f84
30 45
02 20 6ef67fbb03cade43ce07aea00d6168fe6c04938fd26cae9ecbb9635b270e5d19
02 21 00 9c46466f23f1fc8caf15645e9ce0bc108169d9f473eea92f154c23474e0f0f84
场景四:R 和 S 的第一个字节 均小于 0x80
R: 6ef67fbb03cade43ce07aea00d6168fe6c04938fd26cae9ecbb9635b270e5d19
S: 6c46466f23f1fc8caf15645e9ce0bc108169d9f473eea92f154c23474e0f0f84
结果:
304402206ef67fbb03cade43ce07aea00d6168fe6c04938fd26cae9ecbb9635b270e5d1902206c46466f23f1fc8caf15645e9ce0bc108169d9f473eea92f154c23474e0f0f84
30 44
02 20 6ef67fbb03cade43ce07aea00d6168fe6c04938fd26cae9ecbb9635b270e5d19
02 20 6c46466f23f1fc8caf15645e9ce0bc108169d9f473eea92f154c23474e0f0f84
3.3.2.3 公钥 publicKey
公钥对象 publicKey 是 ECPublicKey 类型的,让我们来看下这个类的定义
/**
* The interface to an elliptic curve (EC) public key
*/
public interface ECPublicKey extends PublicKey, ECKey {
/**
* The class fingerprint that is set to indicate
* serialization compatibility.
*/
static final long serialVersionUID = -3314988629879632826L;
/**
* Returns the public point W.
* @return the public point W.
*/
ECPoint getW();
}
public class ECPoint {
private final BigInteger x;
private final BigInteger y;
/**
* This defines the point at infinity.
*/
public static final ECPoint POINT_INFINITY = new ECPoint();
// private constructor for constructing point at infinity
private ECPoint() {
this.x = null;
this.y = null;
}
/**
* Creates an ECPoint from the specified affine x-coordinate
* {@code x} and affine y-coordinate {@code y}.
* @param x the affine x-coordinate.
* @param y the affine y-coordinate.
* @exception NullPointerException if {@code x} or
* {@code y} is null.
*/
public ECPoint(BigInteger x, BigInteger y) {
if ((x==null) || (y==null)) {
throw new NullPointerException("affine coordinate x or y is null");
}
this.x = x;
this.y = y;
}
/**
* Returns the affine x-coordinate {@code x}.
* Note: POINT_INFINITY has a null affine x-coordinate.
* @return the affine x-coordinate.
*/
public BigInteger getAffineX() {
return x;
}
/**
* Returns the affine y-coordinate {@code y}.
* Note: POINT_INFINITY has a null affine y-coordinate.
* @return the affine y-coordinate.
*/
public BigInteger getAffineY() {
return y;
}
/**
* Compares this elliptic curve point for equality with
* the specified object.
* @param obj the object to be compared.
* @return true if {@code obj} is an instance of
* ECPoint and the affine coordinates match, false otherwise.
*/
public boolean equals(Object obj) {
if (this == obj) return true;
if (this == POINT_INFINITY) return false;
if (obj instanceof ECPoint) {
return ((x.equals(((ECPoint)obj).x)) &&
(y.equals(((ECPoint)obj).y)));
}
return false;
}
/**
* Returns a hash code value for this elliptic curve point.
* @return a hash code value.
*/
public int hashCode() {
if (this == POINT_INFINITY) return 0;
return x.hashCode() << 5 + y.hashCode();
}
}
ECPublicKey 是一个接口,提供 getw() 函数获取公钥点。ECPoint 类定义的是椭圆曲线上的一个点,其中成员变量 BigInteger x 和 BigInteger y 表示的是 x 坐标和 y 坐标,那么 x 和 y 则是 ECPublicKey 对象的有效数据。
再看下 OpenSSL 生成的公钥参数
内容为:MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEysPwVnknbQsCqW9qGTFIc8MUQhgq7oG+jIDIrLlVhPULKqbqCFNHVz2AMg8aqRTzPG09qyBgUhsFPMPrv27YUg==
上述公钥是 Base64 编码格式,而在验签时所需要做的是将这段数据转换成 ECPulibcKey 对象。更多的情况下,客户在把公钥传输过来时,并不会直接把这段 Base64 编码格式的字符串发过来,而是会发送公钥的有效数据 ECPoint 中的大数对象 x 和 y。
经过我们多次测试发现,每次生成的 公钥数据 的 Base64 编码数据前面都有相同的头数据,那么公钥是存在一定格式的,对于不同的算法,曲率参数,格式是不一样的,数据头是不一样的。经过测试发现:
在此算法条件下,每个 ECC Prime256r1 公钥都有头数据 “MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAE”。
以上述公钥为例:
MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAE 为数据头部分
ysPwVnknbQsCqW9qGTFIc8MUQhgq7oG+jIDIrLlVhPULKqbqCFNHVz2AMg8aqRTzPG09qyBgUhsFPMPrv27YUg== 为数据本身
下面我们用 Demo 去看下 ECC 公钥 x 和 y 输出的是什么,数据头和数据本身怎么表示:
KeyPair keyPair = EccUtil1.getKeyPair();
Log.d(TAG, "getKeyPair = " + keyPair.toString());
ECPublicKey pubKey = (ECPublicKey) keyPair.getPublic();
Log.d(TAG , "ECC public key Base64 is " + EccUtil1.getPublicKey(keyPair));
BigInteger x = pubKey.getW().getAffineX();
Log.d(TAG, "Public Key Affine x = " + x);
byte[] bytex = x.toByteArray();
Log.d(TAG, "Public Key Affine byte [" + bytex.length + "] x = " + Arrays.toString(bytex));
BigInteger y = pubKey.getW().getAffineY();
Log.d(TAG, "Public Key Affine y = " + y);
byte[] bytey = y.toByteArray();
Log.d(TAG, "Public Key Affine byte [" + bytey.length + "] y = " + Arrays.toString(bytey));
byte[] byteArrayPublicKey = pubKey.getEncoded();
Log.d(TAG , "ECC public key length is " + toHex(byteArrayPublicKey).length());
Log.d(TAG , "ECC public key byte is " + toHex(byteArrayPublicKey));
结果示例:
D/FirstFragment: getKeyPair = java.security.KeyPair@3805f43
D/EccUtil1: getPublicKey()
D/FirstFragment: ECC public key Base64 is MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEQNqw3Zkqe4BcRFy8dsjRWAaiXJf4goTGOjmEToFj+kftj0w4VYugIyMaWN+Zae6zzcWOBNKypH04rVHxxNo18g==
D/FirstFragment: Public Key Affine x = 29334415651086242101511857698229420991127863951410088077182843825133153679943
D/FirstFragment: Public Key Affine byte [32] x = [64, -38, -80, -35, -103, 42, 123, -128, 92, 68, 92, -68, 118, -56, -47, 88, 6, -94, 92, -105, -8, -126, -124, -58, 58, 57, -124, 78, -127, 99, -6, 71]
D/FirstFragment: Public Key Affine y = 107451330295985795852732547066383192153013201075574164791032092156133547587058
D/FirstFragment: Public Key Affine byte [33] y = [0, -19, -113, 76, 56, 85, -117, -96, 35, 35, 26, 88, -33, -103, 105, -18, -77, -51, -59, -114, 4, -46, -78, -92, 125, 56, -83, 81, -15, -60, -38, 53, -14]
D/FirstFragment: ECC public key length is 182
D/FirstFragment: ECC public key byte is 3059301306072a8648ce3d020106082a8648ce3d0301070342000440dab0dd992a7b805c445cbc76c8d15806a25c97f88284c63a39844e8163fa47ed8f4c38558ba023231a58df9969eeb3cdc58e04d2b2a47d38ad51f1c4da35f2
上面 Demo 示例公钥 x 和 y 的输出,我们来对分析下:
ECC public key Base64 是 公钥的 Base64编码 输出,由上面的 ECC 公钥格式可知,数据可分为,数据头和数据本身,如下:
数据头:MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAE
数据本身:QNqw3Zkqe4BcRFy8dsjRWAaiXJf4goTGOjmEToFj+kftj0w4VYugIyMaWN+Zae6zzcWOBNKypH04rVHxxNo18g==
affine x 是 ECPoint 对象中的 getX() 方法获取的 BigInteger 大数对象
Affine byte [32] x 是 将上面 affine x 大数对象转换成 byte 数组的形式输出
同理
Affine y 是 ECPoint 对象中的 getY() 方法获取的 BigInteger 大数对象
Affine byte [33] y 是 将上面 affine y 大数对象转换成 byte 数组的形式输出。这里第一位是 -19为负数,所以数组有 33 个元素,第一个元素为 0。对比上述转换,数组前面没有 0。
我们可以用数据中的几位数据转换成 Base64 码,去对应一下 公钥 的数据:
byte64-38-80二进制位010000001101101010110000Base64 二进制位010000001101101010110000Base64 编码QNqw
从表中可以发现,前三位 byte 数据是可以对应 Base64编码 的前四位数据 “QNqw”
byte-671-19二进制位111110100100011111101101Base64 二进制位111110100100011111101101Base64 编码+kft
再将 x 后两位数据和 y 第一位数据进行转换,也可以匹配上 “+kft”
ECC public key length is 182 是表述 ECC 公钥数据的 byte 的字符串表示位数,182 个字符表示 91 个字节,组成部分是 27字节 + 64字节。27个字节是数据头的长度,64个字节是公钥的有效数据,包含 32 个字节的 x 数据 和 32 个字节的 y 数据。
下面代码是将 ECPublicKey 对象的 ECPoint 中的大数对象 x 和 y 转换成 ECPublicKey 对象,我们以上述 x 和 y 的数据来试验转换成 ECPublicKey 对象的过程。
/*
* 函数说明:将 ECPublicKey 对象的有效数据 x 和 y,有效数据转换成 ECPublicKey 对象
* 参数说明:byte[] data 对象
*/
private PublicKey encodePublicKey(byte[] data) throws InvalidKeySpecException, NoSuchAlgorithmException, InvalidAlgorithmParameterException {
// 1 P256_HEAD[] 直接定义“MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAE”。下面可以重新去实现一个头数组,其实应该是一样的,实际上应该是此算法和曲率下的 固定头 数组。
byte[] P256_HEAD = null;
/*
if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.O) {
Log.d(TAG,"android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.O");
// P256_HEAD = Base64.getDecoder().decode("MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAE");
} else {
Log.e(TAG,"Error (never go): android.os.Build.VERSION.SDK_INT < android.os.Build.VERSION_CODES.O");
}
*/
// 2 P256_HEAD[] 重新去实现一个 Base64 格式的头数据
KeyPairGenerator kpg = KeyPairGenerator.getInstance("EC");
ECGenParameterSpec m = new ECGenParameterSpec("secp256r1"); // 成功,等同于下面的 prime256v1
// ECGenParameterSpec m = new ECGenParameterSpec("prime256v1"); // 成功
kpg.initialize(m);
KeyPair kp = kpg.generateKeyPair();
byte[] encoded = kp.getPublic().getEncoded();
P256_HEAD = Arrays.copyOf(encoded, encoded.length - 2 * (256 / Byte.SIZE));
Log.d(TAG, "P256_HEAD data = " + toHex(P256_HEAD) + " length = " + P256_HEAD.length);
byte[] encodedKey = new byte[P256_HEAD.length + data.length];
System.arraycopy(P256_HEAD, 0, encodedKey, 0, P256_HEAD.length);
System.arraycopy(data, 0, encodedKey, P256_HEAD.length, data.length);
X509EncodedKeySpec spec = new X509EncodedKeySpec(encodedKey);
KeyFactory kf = null;
try {
kf = KeyFactory.getInstance("EC");
} catch (NoSuchAlgorithmException e) {
Log.e(TAG,"KeyFactory get EC failed!");
e.printStackTrace();
}
return kf.generatePublic(spec);
}
3.3.3 Base64编码
用途:Base64 的主要用途是把一些二进制数转成普通字符,方便在网络上传输
概述:Base64 编码就是一种基于 64 个可打印字符来表示二进制数据的表示方法,从下面码表中可以看出,字符选用了 A-Z、a-z、0-9、+、/ 这 64 个可打印字符,数值代表字符的索引,是标准 Base64 协议规定的,不能更改。
Base64 编码索引表:
转换原理:Base64 的码表只有 64 个字符,如果要表达 64 个字符,那么使用 6 bit就可以完全表示,2^6 = 64,而正常的字符是使用 8 bit表示的。而 8 和 6 的最小公倍数是 24,所以 4 个 Base64字符 可以表示 3 个标准的 ASCII 字符。如果是字符串转成 Base64 码,就会先把对应的字符串转换为 ASCII 码表对应的数字,再把数字转换为二进制,也就是将字符串挨个转换成二进制数据,比如 字符 ‘a’,其 ASCII 码是 97,对应二进制就是 01100001,把 8 个二进制位 提取成 6 个,剩下的 2 个二进制和后面的二进制继续拼接,最后把 6 位的二进制前面加上 00,转换成十进制值,通过上面的 Base64 编码索引表转换成对应的 Base64 码。
转换过程:
(1)将二进制数据中的每三个字节分为一组,每个字节占 8 位,也就是 24 个二进制位
(2)将上面的 24 个二进制位每 6 个为一组,共分为 4 组
(3)在每组 6 位前面添加 2 个 0,每组由 6 个变为 8 个二进制位,总共有 32 个二进制位,也就是 四个 字节
(4)根据 Base64 编码对照表将每个字节转化成对应的 Base64 字符
文本abc二进制位011000010110001001100011Base64 二进制位011000010110001001100011Base64 编码YWJj
这里使用 6 位数据来进行描述字符,就有 64 种字符表示;举一反三,如果是 5 位数据描述字符,就对应 32 种字符,也就是说可以设计 Base32。
为什么 6 位数据又要在 前面添加 2 个 0,变成 8 位呢?这是因为计算机存储的最小单位是 字节,就是 8位,所以,要在每组 6 位前面添加 2 个 0 凑成 8 位一个字节存储,既不改变原始数据大小,也能补充字节位。
如果是 8 位拆分为 6 位 转换的话,那么又会有以下两种其他情况:
情况一:两个字节的数据,无法正常转换为 6 位数据。
两个字节共 16 个二进制位,依旧按照规则进行分组。此时共有 16 个二进制位,每 6 个一组,则第三组缺少 2 位,用 0 补齐,得到 3 个 Base64 编码,第四组完全没有数据则用 “=” 补上。
文本BC二进制位0100001001000011Base64 二进制位010000100100001100(补齐)无数据,“=” 填充Base64 编码QKM=
情况二:一个字节的数据,无法正常转换为 6 位数据。
一个字节共 8 个二进制位,依照按照规则进行分组。此时共有 8 个二进制位,每 6 个一组,则第二组缺少 4 位,用 0 补齐,得到 2 个 Base64 编码,第三组、第四组没有对应数据,都用 “=” 补上。
文本A二进制位01000001Base64 二进制位010000010000(补齐)无数据,“=” 填充无数据,“=” 填充Base64 编码QQ==
3.3.4 代码实现验签
在上面的介绍过后,我们利用 JCA 实现 ECC 验签的实现过程,我们利用 JCA 提供的验签接口,将 公钥、签名、原数据 进行处理验签:
公钥:
cac3f05679276d0b02a96f6a19314873c31442182aee81be8c80c8acb95584f50b2aa6ea085347573d80320f1aa914f33c6d3dab2060521b053cc3ebbf6ed852
这里的公钥数据是基于原始 PublicKey 对象的 Base64 编码转换而来,得到 128 位的十六进制编码,就是 64 个字节的数据。
MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEysPwVnknbQsCqW9qGTFIc8MUQhgq7oG+jIDIrLlVhPULKqbqCFNHVz2AMg8aqRTzPG09qyBgUhsFPMPrv27YUg==
这是公钥原始 Base64 数据,这里我们将会对这个 Base64 编码数据转换成 128位 的有效数据,以还原验签原始过程
转换过程:
- 数据头部分删除:MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAE
- Base64格式有效数据转换成十六进制输出:ysPwVnknbQsCqW9qGTFIc8MUQhgq7oG+jIDIrLlVhPULKqbqCFNHVz2AMg8aqRTzPG09qyBgUhsFPMPrv27YUg==
- 得到公钥有效数据cac3f05679276d0b02a96f6a19314873c31442182aee81be8c80c8acb95584f50b2aa6ea085347573d80320f1aa914f33c6d3dab2060521b053cc3ebbf6ed852发现跟上述 2.1.6.2 图中的 public 数据一致。ps:04 是前缀。原始十六进制公钥是由 前缀 04 + public.x + public.y组成,大小是65,不加 04 是 64 字节。ECPoint 是长度为 65 字节的 OCTET STRING,其中第一个字节代表 ECPoint 是否经过压缩,如果为 0x04,代表没有压缩。剩下的 64 个字节,前 32 个字节,表示ECPoint 的 X 坐标,后 32 个字节表示 ECPoint 的 Y 坐标。
转换代码:
Test.java
String publicKeyBase64 = "ysPwVnknbQsCqW9qGTFIc8MUQhgq7oG+jIDIrLlVhPULKqbqCFNHVz2AMg8aqRTzPG09qyBgUhsFPMPrv27YUg==";
Log.d(TAG, "public Key Base64 = " + publicKeyBase64);
// 将 Base64 编码转换成 ASCII 码 byte[] 数组
Log.d(TAG, "Public Key Affine byte [" + EccUtil1.base64ToByte(publicKeyBase64).length + "] y = " + Arrays.toString(EccUtil1.base64ToByte(publicKeyBase64)));
// 将 ASCII 码 byte[] 数组转换成 十六进制 字符串
Log.d(TAG ,"Public Key HEX length is " + toHex(EccUtil1.base64ToByte(publicKeyBase64)).length() + ", HEX data is " + toHex(EccUtil1.base64ToByte(publicKeyBase64)));
EccUtil1.java
/*
* 将 base64 编码转化成 ASCII byte[] 数组
*/
public static byte[] base64ToByte(String str) {
byte[] result = null;
if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.O) {
result = Base64.getDecoder().decode(str);
}
return result;
}
/*
* 将 ASCII 码 byte[] 数组转换成 十六进制 字符串
*/
public static String toHex(byte[] data) {
StringBuilder sb = new StringBuilder(data.length * 2);
for (int i = 0; i < data.length; i++) {
String hex = Integer.toHexString(data[i]);
if (hex.length() == 1) {
sb.append("0");
} else if (hex.length() == 8) {
hex = hex.substring(6);
}
sb.append(hex);
}
return sb.toString().toLowerCase(Locale.getDefault());
}
验证输出:
2022-10-12 15:56:50.383 13277-13277/com.example.signdemo D/FirstFragment: public Key Base64 = ysPwVnknbQsCqW9qGTFIc8MUQhgq7oG+jIDIrLlVhPULKqbqCFNHVz2AMg8aqRTzPG09qyBgUhsFPMPrv27YUg==
2022-10-12 15:56:50.385 13277-13277/com.example.signdemo D/FirstFragment: Public Key Affine byte [64] y = [-54, -61, -16, 86, 121, 39, 109, 11, 2, -87, 111, 106, 25, 49, 72, 115, -61, 20, 66, 24, 42, -18, -127, -66, -116, -128, -56, -84, -71, 85, -124, -11, 11, 42, -90, -22, 8, 83, 71, 87, 61, -128, 50, 15, 26, -87, 20, -13, 60, 109, 61, -85, 32, 96, 82, 27, 5, 60, -61, -21, -65, 110, -40, 82]
2022-10-12 15:56:50.386 13277-13277/com.example.signdemo D/FirstFragment: Public Key HEX length is 128, HEX data is cac3f05679276d0b02a96f6a19314873c31442182aee81be8c80c8acb95584f50b2aa6ea085347573d80320f1aa914f33c6d3dab2060521b053cc3ebbf6ed852
签名:0e b6 c3 bc f0 40 df 3d 00 72 3c 52 75 74 67 f0 06 46 69 81 11 e3 78 87 4d d5 ab d8 5d 7f 75 bf 1d 2d 1f 89 d7 70 b1 0a ce 1f bf b9 a4 ef 5d 83 18 9d 2d 17 3e a8 76 a4 27 f3 1c 1d 1c 0b a2 0e
这里的签名数据已经将格式进行拆除,仅保留签名的有效数据。我们在验签前需要对这个签名数据进行格式恢复,才能在接口中使用。
原数据:TextPublicKey.txt
内容如下
International
6e234043720c3c40b021f8e03df06582c5541b8cd12d400320f57e54f5dd06cbc2d1f1a9048c599645bb49507e825760033a5290fb5fd419d174720f732a5700
验签完成过程如下:
验证代码:
Test.java
public void VerifyTest() {
String publicKey = "cac3f05679276d0b02a96f6a19314873c31442182aee81be8c80c8acb95584f50b2aa6ea085347573d80320f1aa914f33c6d3dab2060521b053cc3ebbf6ed852";
String sign1 = "0eb6c3bcf040df3d00723c52757467f00646698111e378874dd5abd85d7f75bf1d2d1f89d770b10ace1fbfb9a4ef5d83189d2d173ea876a427f31c1d1c0ba20e";
String context = "/data/data/com.example.signdemo/TextPublicKey.txt";
// 1 公钥转换
ECPublicKey Verify_publicKey = null;
try {
Verify_publicKey = (ECPublicKey) EccUtil1.encodePublicKey(EccUtil1.strToByteArray(publicKey));
} catch (Exception e) {
e.printStackTrace();
}
Log.d(TAG,"KeyFactory get EC success! Verify_publicKey = " + Verify_publicKey);
// 2 签名数据格式恢复
byte[] Verify_sign = null;
try {
Verify_sign = EccUtil1.getSignByte(EccUtil1.strToByteArray(sign1));
} catch (Exception e) {
e.printStackTrace();
}
Log.d(TAG, "Verify_sign data = " + sign1 + " length = " + sign1.length());
Log.d(TAG, "Verify_sign data = " + toHex(Verify_sign) + " length = " + Verify_sign.length);
// 3 验签
boolean ret = false;
try {
ret = EccUtil1.verify_byte(EccUtil1.readFileByBytes(context),Verify_sign,Verify_publicKey);
} catch (Exception e) {
e.printStackTrace();
}
Log.d(TAG, "verify result = " + ret);
}
EcccUtil1.java
/*
* 将 公钥 的 x 和 y 有效数据,转换成 ECPublicKey 对象
*/
public static PublicKey encodePublicKey(byte[] data) throws InvalidKeySpecException, NoSuchAlgorithmException, InvalidAlgorithmParameterException {
// 1 P256_HEAD[] 这个数组的头数据在此算法下是固定的,下面可以重新去实现一个头数组
byte[] P256_HEAD = null;
/*
if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.O) {
Log.d(TAG,"android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.O");
// P256_HEAD = Base64.getDecoder().decode("MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAE");
} else {
Log.e(TAG,"Error (never go): android.os.Build.VERSION.SDK_INT < android.os.Build.VERSION_CODES.O");
}
*/
// 2 P256_HEAD[] 重新去实现一个 Base64 格式的头数据
KeyPairGenerator kpg = KeyPairGenerator.getInstance("EC");
ECGenParameterSpec m = new ECGenParameterSpec("secp256r1"); // 成功,等同于下面的 prime256v1
// ECGenParameterSpec m = new ECGenParameterSpec("prime256v1"); // 成功
kpg.initialize(m);
KeyPair kp = kpg.generateKeyPair();
// 重新实现一个 Base64 格式的头数据
byte[] encoded = kp.getPublic().getEncoded();
P256_HEAD = Arrays.copyOf(encoded, encoded.length - 2 * (256 / Byte.SIZE));
Log.d(TAG, "P256_HEAD data = " + toHex(P256_HEAD) + " length = " + P256_HEAD.length);
byte[] encodedKey = new byte[P256_HEAD.length + data.length];
System.arraycopy(P256_HEAD, 0, encodedKey, 0, P256_HEAD.length);
System.arraycopy(data, 0, encodedKey, P256_HEAD.length, data.length);
X509EncodedKeySpec spec = new X509EncodedKeySpec(encodedKey);
KeyFactory kf = null;
try {
kf = KeyFactory.getInstance("EC");
} catch (NoSuchAlgorithmException e) {
Log.e(TAG,"KeyFactory get EC failed!");
e.printStackTrace();
}
return kf.generatePublic(spec);
}
/*
* 将签名数据恢复格式
*/
public static byte[] getSignByte(byte[] sign) throws Exception {
Log.d(TAG,"getSignByte start()");
long stime = System.currentTimeMillis();
if (sign.length != 64) {
// 异常情况,传递的原始签名应该是 64 字节
throw new Exception("Error ! Parm sign Error , length must be 64");
}
// 确定签名数据的长度
int signLength = 0;
if (((int) sign[0] & 0xFF) > ((int) 0x80 & 0xFF) && ((int) sign[32] & 0xFF) > ((int) 0x80 & 0xFF)) {
signLength = 72;
} else if (((int) sign[0] & 0xFF) > ((int) 0x80 & 0xFF) || ((int) sign[32] & 0xFF) > ((int) 0x80 & 0xFF)) {
signLength = 71;
} else {
signLength = 70;
}
if (signLength == 0 ) {
throw new Exception("Error ! sign length is Zero, please check");
}
byte[] result = new byte[signLength];
int resultIndex = 0;
result[0] = 0x30;
result[1] = (byte) (signLength - 2);
result[2] = 0x02;
if (((int) sign[0] & 0xFF) > ((int) 0x80 & 0xFF)) {
result[3] = 0x21;
result[4] = 0x00;
resultIndex = 5;
} else {
result[3] = 0x20;
resultIndex = 4;
}
Log.d(TAG,"signLength = " + signLength);
for (int index = 0; index < sign.length; index ++) {
if (index == 32) {
if (((int) sign[index] & 0xFF) > ((int) 0x80 & 0xFF)) {
result[resultIndex] = 0x02;
resultIndex = resultIndex + 1;
result[resultIndex] = 0x21;
resultIndex = resultIndex + 1;
result[resultIndex] = 0x00;
resultIndex = resultIndex + 1;
result[resultIndex] = sign[index];
resultIndex = resultIndex + 1;
} else {
result[resultIndex] = 0x02;
resultIndex = resultIndex + 1;
result[resultIndex] = 0x20;
resultIndex = resultIndex + 1;
result[resultIndex] = sign[index];
resultIndex = resultIndex + 1;
}
// Log.d(TAG,"resultIndex = " + resultIndex + ", index = " + index + "Integer.toHexString(result[resultIndex] & 0xFF) = " + Integer.toHexString(result[resultIndex] & 0xFF) + ", Integer.toHexString(sign[index] & 0xFF)" + Integer.toHexString(sign[index] & 0xFF));
continue;
}
// Log.d(TAG,"resultIndex = " + resultIndex + ", index = " + index + "Integer.toHexString(result[resultIndex] & 0xFF) = " + Integer.toHexString(result[resultIndex] & 0xFF) + ", Integer.toHexString(sign[index] & 0xFF)" + Integer.toHexString(sign[index] & 0xFF));
result[resultIndex++] = sign[index];
}
long etime = System.currentTimeMillis();
Log.d(TAG,"getSignByte end() exec Time = " + (etime - stime) + " ms");
return result;
}
/*
* 将传入的文件用字节的方式读取,返回一个 byte[] 字节数组
*/
public static byte[] readFileByBytes(String fileName) {
File file = new File(fileName);
InputStream in = null;
byte[] txt = new byte[(int) file.length()];
try {
in = new FileInputStream(file);
int tempbyte;
int i = 0;
while ((tempbyte = in.read()) != -1) {
txt[i] = (byte) tempbyte;
i++;
}
in.close();
return txt;
} catch (IOException e) {
e.printStackTrace();
return txt;
}
}
/*
* 基于 SHA256 摘要算法, ECC 签名算法, 进行签名验证
*/
public static boolean verify_byte(byte[] content, byte[] sign, PublicKey publicKey) throws InvalidKeyException, SignatureException, NoSuchAlgorithmException {
Log.d(TAG , "SHA256WITHECDSA verify_byte()");
Signature signature = Signature.getInstance("SHA256WITHECDSA"); // 这里是直接使用 “SHA256WITHECDSA”
signature.initVerify(publicKey);
signature.update(content);
return signature.verify(sign);
}
运行结果:
2022-10-12 15:56:50.383 13277-13277/com.example.signdemo D/FirstFragment: start ECCTEST!
2022-10-12 15:56:50.383 13277-13277/com.example.signdemo D/FirstFragment: public Key Base64 = ysPwVnknbQsCqW9qGTFIc8MUQhgq7oG+jIDIrLlVhPULKqbqCFNHVz2AMg8aqRTzPG09qyBgUhsFPMPrv27YUg==
2022-10-12 15:56:50.385 13277-13277/com.example.signdemo D/FirstFragment: Public Key Affine byte [64] y = [-54, -61, -16, 86, 121, 39, 109, 11, 2, -87, 111, 106, 25, 49, 72, 115, -61, 20, 66, 24, 42, -18, -127, -66, -116, -128, -56, -84, -71, 85, -124, -11, 11, 42, -90, -22, 8, 83, 71, 87, 61, -128, 50, 15, 26, -87, 20, -13, 60, 109, 61, -85, 32, 96, 82, 27, 5, 60, -61, -21, -65, 110, -40, 82]
2022-10-12 15:56:50.386 13277-13277/com.example.signdemo D/FirstFragment: Public Key HEX length is 128, HEX data is cac3f05679276d0b02a96f6a19314873c31442182aee81be8c80c8acb95584f50b2aa6ea085347573d80320f1aa914f33c6d3dab2060521b053cc3ebbf6ed852
2022-10-12 15:56:50.394 13277-13277/com.example.signdemo D/EccUtil1: P256_HEAD data = 3059301306072a8648ce3d020106082a8648ce3d03010703420004 length = 27
2022-10-12 15:56:50.396 13277-13277/com.example.signdemo D/FirstFragment: KeyFactory get EC success! Verify_publicKey = Public-Key: (256 bit)
00000000 04 ca c3 f0 56 79 27 6d 0b 02 a9 6f 6a 19 31 48 |....Vy'm...oj.1H|
00000010 73 c3 14 42 18 2a ee 81 be 8c 80 c8 ac b9 55 84 |s..B.*........U.|
00000020 f5 0b 2a a6 ea 08 53 47 57 3d 80 32 0f 1a a9 14 |..*...SGW=.2....|
00000030 f3 3c 6d 3d ab 20 60 52 1b 05 3c c3 eb bf 6e d8 |.<m=. `R..<...n.|
00000040 52 |R|
2022-10-12 15:56:50.396 13277-13277/com.example.signdemo D/EccUtil1: getSignByte start()
2022-10-12 15:56:50.396 13277-13277/com.example.signdemo D/EccUtil1: signLength = 70
2022-10-12 15:56:50.396 13277-13277/com.example.signdemo D/EccUtil1: getSignByte end() exec Time = 0 ms
2022-10-12 15:56:50.396 13277-13277/com.example.signdemo D/FirstFragment: Verify_sign data = 0eb6c3bcf040df3d00723c52757467f00646698111e378874dd5abd85d7f75bf1d2d1f89d770b10ace1fbfb9a4ef5d83189d2d173ea876a427f31c1d1c0ba20e length = 128
2022-10-12 15:56:50.397 13277-13277/com.example.signdemo D/FirstFragment: Verify_sign data = 304402200eb6c3bcf040df3d00723c52757467f00646698111e378874dd5abd85d7f75bf02201d2d1f89d770b10ace1fbfb9a4ef5d83189d2d173ea876a427f31c1d1c0ba20e length = 70
2022-10-12 15:56:50.403 13277-13277/com.example.signdemo D/EccUtil1: SHA256WITHECDSA verify_byte()
2022-10-12 15:56:50.407 13277-13277/com.example.signdemo D/FirstFragment: verify result = true
以上,就是关于 ECC 验签的整个过程了~
本篇中分享了 数据签名与验签的基本概念,原理,实现方式 展开介绍,基于 JCA 与 OpenSSL 协作 实现了 ECC 加签与验签的过程,在 Android 与 JDK 的支持下,梳理了整个逻辑。
文中如有理解错误,代码实现问题,待优化修改处,请大家不吝赐教,共同进步~
版权归原作者 Yang_Mao_Shan 所有, 如有侵权,请联系我们删除。