Signum 是一套 Kotlin 多平台密码学/PKI 库,提供硬件级加密签名、ASN.1 编解码及 JOSE/COSE 数据结构支持,解决跨平台加密逻辑共享与硬件安全集成问题。
# Signum – Kotlin Multiplatform 加密/PKI 库及 ASN1 解析器 + 编码器
[](https://plus.a-sit.at/open-source.html)
[](http://www.apache.org/licenses/LICENSE-2.0)
[](http://kotlinlang.org)
[](http://kotlinlang.org)
[](https://www.oracle.com/java/technologies/downloads/#java17)
[](https://support.apple.com/en-gb/108051)
| [-SDK--26-37AA55?logo=android)](https://developer.android.com/tools/releases/platforms#8.0) | [](https://mvnrepository.com/artifact/at.asitplus.signum/) | [](https://s01.oss.sonatype.org/content/repositories/snapshots/at/asitplus/signum/indispensable/) |
|:----------------------------------------------------------------------------------------------------------------------------------------------------------:|:---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------:|:-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------:|
| [-SDK--30-37AA55?logo=android)](https://developer.android.com/tools/releases/platforms#11) | [](https://mvnrepository.com/artifact/at.asitplus.signum/supreme) | [](https://s01.oss.sonatype.org/content/repositories/snapshots/at/asitplus/signum/supreme/) |
## 带有 ASN1 解析器 + 编码器的 Kotlin Multiplatform 加密/PKI 库
* **多平台、平台原生加密** → 查看包含的 [CMP 示例 App](https://a-sit-plus.github.io/signum/app) 了解其实际
运行效果!
* **ECDSA 和 RSA 签名器及验证器**
* **多平台 ECDH 密钥协商**
* **Android 和 iOS 上的硬件支持加密**
* **iOS 和 Android 上的平台原生认证**
* **Android 和 iOS 上无需回调或 Activity 传递的可配置生物识别认证** (✨魔法!✨)
* **多平台 AES**
* **多平台 HMAC**
* **多平台 RSA 加密**
* **多平台 KDF** (使用平台原生哈希)
* PBKDF2
* HKDF
* scrypt
* 公钥 (RSA 和 EC)
* 私钥 (RSA 和 EC)
* 算法标识符 (签名、哈希)
* X509 证书类 (创建、编码、解码)
* 证书签名请求 (CSR)
* 带有易读符号的 ObjectIdentifier 类 (例如 1.2.9.6245.3.72.13.4.7.6)
* 用于操作并创建任意 ASN.1 数据的通用 ASN.1 抽象
* 与 JOSE 相关的数据结构 (JSON Web Keys, JWT 等……)
* 与 COSE 相关的数据结构 (COSE Keys, CWT 等……)
* 100% 纯 Kotlin BitSet
* 将 Multibase 编码器/解码器作为 API 依赖项公开,包含 [Matthew Nelson 出色的 Base16、Base32 和 Base64 编码器](https://github.com/05nelsonm/encoding)
* **ASN.1 解析器和编码器,包含用于生成 ASN.1 结构的 DSL**
* 在所有受支持的平台上解析、创建、探索证书、公钥、CSR 以及**任意 ASN.1 结构**
* 在所有 KMP 目标上提供强大、富有表现力且类型安全的 ASN.1 DSL
最后一点意味着您可以跨平台共享与 ASN.1 相关的逻辑。
最开头的一点意味着您可以在 JVM、Android 和 iOS 上创建和验证签名,并使用平台原生的
加密硬件。
### 请务必查看包含示例和 API 文档的完整手册 [此处](https://a-sit-plus.github.io/signum/)!
本 README 仅提供概述。
完整的手册内容更加全面,为每个模块设有单独的章节,提供了示例以及完整的 API 文档。
## 在您的项目中使用它
该库是为 [Kotlin Multiplatform](https://kotlinlang.org/docs/multiplatform.html) 构建的。目前,它的目标是
JVM、Android 和 iOS。
它由四个模块组成,每个模块都发布在 maven central 上:
| 名称 | 信息 |
|:------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------:|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
|
| **Indispensable ASN.1** 模块,包含已知宇宙中最复杂的 KMP ASN.1 引擎。除了 kotlinx-* 依赖项外,它仅依赖于 [KmmResult](https://github.com/a-sit-plus/kmmresult) 以实现极其顺畅的 iOS 互操作。 |
|
| **Indispensable** 基础模块,包含加密数据结构、算法标识符、X.509 证书等。依赖于 ASN.1 引擎。 |
|
| **Indispensable Josef** JOSE 附加模块,包含 JWS/E/T 特定的数据结构以及用于与基础模块中包含的类型进行相互转换的扩展。包含所有必需的 kotlinx-serialization 魔法,以允许符合规范的序列化/反序列化。 |
|
| **Indispensable Cosef** COSE 附加模块,包含所有 COSE/CWT 特定的数据结构以及用于与基础模块中包含的类型进行相互转换的扩展。包含所有必需的 kotlinx-serialization 魔法,以允许符合规范的序列化/反序列化。 |
|
| **Supreme** KMP 加密提供者,实现了跨移动平台 (Android KeyStore / iOS Secure Enclave) 的硬件支持的签名创建和验证,以及 (在 JVM 上的) JCA 兼容性。 |
这种分离将依赖项保持在最低限度,即:如果 COSE 不相关,它允许仅包含与 JOSE 相关的功能。
更重要的是,在仅有 JVM、iOS 或 Android 的项目中,它允许处理加密材料而不强制包含加密提供者。
只需声明所需的依赖项即可开始:
```
implementation("at.asitplus.signum:indispensable:$version")
```
```
implementation("at.asitplus.signum:indispensable-josef:$version")
```
```
implementation("at.asitplus.signum:indispensable-cosef:$version")
```
```
implementation("at.asitplus.signum:supreme:$supremeVersion")
```
## 理念
在寻找 KMP 加密框架时,您无疑会遇到
[cryptography-kotlin](https://github.com/whyoleg/cryptography-kotlin)。我们也遇到过,它是一个强大的
库,支持比 Signum Supreme 更多的平台和更多的加密操作。
这就引出了一个问题:为什么要从头开始实现另一个不兼容的
加密框架?简短的回答是:Signum 和 cryptography-kotlin 追求不同的目标和优先级。
cryptography-kotlin 力求基于灵活的提供者架构,覆盖广泛的目标和广泛的操作。
另一方面,Signum 专注于紧密的平台集成(**包括硬件支持的加密和认证!**),
以及全面的 ASN.1、JOSE 和 COSE 支持。
更多…
Signum 诞生于跨平台提供加密数据结构(如公钥、签名、
证书、CSR,以及 COSE 和 JOSE 数据)的需求。因此,我们需要一个功能齐全的 ASN.1 引擎以及从
X.509 到 COSE 和 JOSE 数据类型的映射。我们需要跨平台的全面 ASN.1 内省和构建能力。
最值得注意的是,Apple 一直缺乏任何甚至 remotely usable 的东西,
而且由于几个原因,[SwiftASN1](https://github.com/apple/swift-asn1) 根本不在考虑范围内。
最值得注意的是,当我们开始开发 Signum 时它还不存在。因此,在 Apple 平台上**既没有真正可用的 ASN.1 解析器,也没有编码器**
。实际上:根本看不到 KMP ASN.1 编解码器,更不用说类型安全且用户友好的了。
就目前而言,我们的 ASN.1 引擎几乎可以处理您抛给它的任何内容,在某些领域甚至超越了 Bouncy Castle!
在 Signum 开始开发一年多后,cryptography-kotlin 才刚刚添加了基本的 ASN.1 功能。
我们也不知道是否有任何其他库提供基于 kotlinx-serialization 的全面 JOSE 和 COSE 数据结构。
因此,我们自己实现了这些功能,并与我们的通用加密数据结构实现了第一类互操作。
我们还支持平台原生的互操作,这意味着您可以轻松地将 Json Web Key 转换为 JCA key,甚至在 iOS 上转换为 `SecKeyRef`。
拥有加密操作的实际实现仅在我们优先级列表中排在第二位。从
一开始,我们就很清楚,我们希望在 Android 和 iOS 上实现最紧密的平台集成,包括在可能的情况下在硬件中存储密钥材料
并在硬件中执行加密操作。
我们还需要平台原生的认证功能(如果您在移动目标上进行任何关键任务,迟早也会如此!)。
虽然这种方法确实限制了可用的加密操作数量,但它也意味着所有涉及机密(例如私钥)的加密操作都提供与平台原生实现相同的安全保证 —
**因为它们在底层是完全相同**的。最显著的是:**硬件支持的私钥
永远不会离开硬件加密模块**!
这种紧密的集成以及我们对移动设备的关注是以 **Supreme KMP 加密提供者仅支持 JVM、
Android 和 iOS** 为代价的。
另一方面,cryptography-kotlin 允许您在所有 KMP 目标上执行更广泛的加密功能,
最突出的是,它已经支持 RSA 加密、密钥拉伸和密钥派生,而这些正是 Signum 目前所缺乏的。
另一方面,cryptography-kotlin 目前既不提供硬件支持的加密,也不提供认证功能。
下表提供了 Signum 和 cryptography-kotlin 之间的详细对比。
| | Signum | cryptography-kotlin |
|-----------------------------|--------------------------|---------------------------|
| 数字签名 | ✔ (ECDSA, RSA) | ✔ (ECDSA, RSA) |
| 对称加密 | ✔ (AES + ChaChaPoly) | ✔ (AES) |
| 非对称加密 | ✔ (RSA) | ✔ (RSA) |
| 摘要 | ✔ (SHA-1, SHA-2) | ✔ (MD5, SHA-1, SHA-2) |
| MAC | ✔ (HMAC) | ✔ (HMAC) |
| 密钥协商 | ✔ (ECDH) | ✔ (ECDH) |
| KDF/PRF/KSF | ✔ (PBKDF2, HKDF, scrypt) | ✔ (PBKDF2, HKDF) |
| 硬件支持的加密 | ✔ | ✗ |
| 认证 | ✔ | ✗ |
| 功能齐全的 ASN.1 引擎 | ✔ | ✗ |
| COSE | ✔ | ✗ |
| JOSE | ✔ | ✗ |
| 提供者目标 | JVM, Android, iOS | 所有受 KMP 支持的目标 |
## _Supreme_ 演示集锦
_Supreme_ KMP 加密提供者的工作方式与 JCA 不同。其配置是类型安全的,表现力更强且更简洁,
这意味着您最终编写的代码会更少。**没有任何异常抛出!不要忽略任何操作返回的结果!**
### 签名创建
创建签名,请获取一个 `Signer` 实例。
您可以使用 `Signer.Ephemeral` 来为一次性密钥对创建签名器:
```
val signer = Signer.Ephemeral {}.getOrThrow()
val plaintext = "You have this.".encodeToByteArray()
val signature = signer.sign(plaintext).signature
println("Signed using ${signer.signatureAlgorithm}: $signature")
```
如果您想使用同一个临时密钥创建多个签名,您可以获取一个 `EphemeralKey` 实例,然后从中创建签名器:
```
val key = EphemeralKey { rsa {} }.getOrThrow()
val sha256Signer = key.getSigner { rsa { digest = Digests.SHA256 } }.getOrThrow()
val sha384Signer = key.getSigner { rsa { digest = Digests.SHA384 } }.getOrThrow()
```
可以使用配置 DSL 对实例进行配置。
任何未指定的参数都会使用合理、安全的默认值。
#### 平台签名器
在 Android 和 iOS 上,可以检索使用系统安全密钥存储的签名器。
为此,请使用 `PlatformSigningProvider`(在公共代码中),或与 `AndroidKeystoreProvider`/`IosKeychainProvider` 交互(在特定于平台的代码中)。
可以使用 `createSigningKey(alias: String) { /* configuration */ }` 创建新密钥,
并且可以使用 `getSignerForKey(alias: String) { /* configuration */ }` 检索现有密钥的签名器。
例如,创建一个基于 P256 曲线的椭圆曲线密钥,存储在安全硬件中,并使用服务器提供的随机挑战进行密钥认证,可以这样做:
```
val serverChallenge: ByteArray = TODO("This was unpredictably chosen by your server.")
PlatformSigningProvider.createSigningKey(alias = "Swordfish") {
ec {
// you don't even need to specify the curve (P256 is the default) but we'll do it for demonstration purposes
curve = ECCurve.SECP_256_R_1
// you could specify the supported digests explicitly - if you do not, the curve's native digest (for P256, this is SHA256) is supported
}
// see https://a-sit-plus.github.io/signum/supreme/at.asitplus.signum.supreme.sign/-platform-signing-key-configuration-base/-secure-hardware-configuration/index.html
hardware {
// you could use PREFERRED if you want the operation to succeed (without hardware backing) on devices that do not support it
backing = REQUIRED
attestation { challenge = serverChallenge }
protection {
timeout = 5.seconds
factors {
biometry = true
deviceLock = false
}
}
}
}
```
如果此操作成功,它将返回一个 `Signer`。稍后可以使用 `PlatformSigningProvider.getSignerForKey(alias: String)` 检索相同的 `Signer`。
当您使用此 `Signer` 签署数据时,将提示用户使用已注册的指纹授权签名,因为这就是您在创建密钥时所指定的内容。
您可以配置认证提示:
```
val plaintext = "A message".encodeToByteArray()
val signature = signer.sign(plaintext) {
unlockPrompt {
message = "Signing a message to Bobby"
}
}.signature
```
... 但您无法更改您将此密钥配置为需要生物识别的事实。在创建密钥时请考虑到这一点。
在 JVM 上,没有可用的原生安全硬件存储。
可以使用 `JKSProvider { file { /* ... */ } }` 访问基于文件的 keystore。
可以使用 `JKSProvider { withBackingObject{ /* ... */ } }` 或 `JksProvider { customAccessor{ /* ... */ } }` 访问其他 keystore。
有关更多详细信息,请参阅提供者的[配置选项](https://a-sit-plus.github.io/signum/dokka/supreme/at.asitplus.signum.supreme.os/-j-k-s-provider-configuration/index.html)。
#### 密钥认证
Android KeyStore 为硬件支持的密钥提供密钥认证证书。
这些证书通过签名器的 `.attestation` 属性公开。
对于 iOS,Apple 不提供此功能。
相反,我们依托于 iOS App Attestation 来提供一种自制的“密钥认证”方案。
其保证是不同的:您信任的是操作系统,而不是实际的安全硬件;并且您信任我们的库能与操作系统正确交互。
认证类型是可序列化以进行传输的,并且与 Indispensable 的认证模块中的类型相对应。
### 签名验证
要验证签名,请使用 `verifierFor(k: PublicKey)` 获取一个 `Verifier` 实例,可以直接在 `SignatureAlgorithm` 上调用,也可以在某个特定算法(`X509SignatureAlgorithm`、`CoseAlgorithm` 等)上调用。
在 `SignatureAlgorithm` 的伴生对象中还提供了各种类似于众所周知的 JCA 名称的常量。
例如,以下是如何使用公钥验证基本签名:
```
val publicKey: CryptoPublicKey.EC = TODO("You have this and trust it.")
val plaintext = "You want to trust this.".encodeToByteArray()
val signature: CryptoSignature = TODO("This was sent alongside the plaintext.")
val verifier = SignatureAlgorithm.ECDSAwithSHA256.verifierFor(publicKey).getOrThrow()
val isValid = verifier.verify(plaintext, signature).isSuccess
println("Looks good? $isValid")
```
或者以下是如何验证 X.509 证书:
```
val rootCert: X509Certificate = TODO("You have this and trust it.")
val untrustedCert: X509Certificate = TODO("You want to verify that this is trustworthy.")
val verifier = untrustedCert.signatureAlgorithm.verifierFor(rootCert.publicKey).getOrThrow()
val plaintext = untrustedCert.tbsCertificate.encodeToDer()
val signature = untrustedCert.signature
val isValid = verifier.verify(plaintext, signature).isSuccess
println("Certificate looks trustworthy: $isValid")
```
#### 平台验证器
并非每个平台都支持每个算法参数。例如,iOS 不支持曲线 P-521 的原始 ECDSA 验证(针对预哈希数据)。
如果您使用 `.verifierFor`,并且发生这种情况,该库将透明地替换为纯 Kotlin 实现。
如果不希望这样做,您可以通过使用 `.platformVerifierFor` 专门强制使用平台验证器。
这样,该库将永远只充当平台 API(JCA、CryptoKit 等)的代理,而不会使用其自身的实现。
您还可以进一步配置验证器,例如指定在 JVM 上使用的 `provider`。
为此,请将 DSL 配置 lambda 传递给 `verifierFor`/`platformVerifierFor`。
```
val publicKey: CryptoPublicKey.EC = TODO("You have this.")
val plaintext: ByteArray = TODO("This is the message.")
val signature: CryptoSignature.EC = TODO("And this is the signature.")
val verifier = SignatureAlgorithm.ECDSAwithSHA512
.platformVerifierFor(publicKey) { provider = "BC"} /* specify BouncyCastle */
.getOrThrow()
val isValid = verifier.verify(plaintext, signature).isSuccess
println("Is it trustworthy? $isValid")
```
## 对称加密
我们目前支持 ChaCha20-Poly1503、AES-CBC、AES-GCM、AES-KW、AES-ECB,以及一种非常灵活的 AES-CBC-HMAC。
每个对称操作都根植于一种算法,因为算法定义了诸如 nonce 要求、
认证能力等特性。因此,您需要了解该算法。
### 基本用法
一旦您决定使用某种加密算法,加密本身就变得很简单了:
```
val secret = "Top Secret".encodeToByteArray()
val secretKey = SymmetricEncryptionAlgorithm.ChaCha20Poly1305.randomKey()
val encrypted = secretKey.encrypt(secret).getOrThrow(/*handle error*/)
encrypted.decrypt(secretKey).getOrThrow(/*handle error*/) shouldBe secret
```
加密数据始终是结构化的,并且各个组件易于访问:
```
val nonce = encrypted.nonce
val ciphertext = encrypted.encryptedData
val authTag = encrypted.authTag
val keyBytes = secretKey.secretKey.getOrThrow() /*for algorithms with a dedicated MAC key, there's encryptionKey and macKey*/
```
解密从外部源接收的数据也很简单:
```
val box = algo.sealedBox.withNonce(nonce).from(ciphertext, authTag).getOrThrow(/*handle error*/)
box.decrypt(preSharedKey, /*also pass AAD*/ externalAAD).getOrThrow(/*handle error*/) shouldBe secret
//alternatively, pass raw data:
preSharedKey.decrypt(nonce, ciphertext, authTag, externalAAD).getOrThrow(/*handle error*/) shouldBe secret
```
### 自定义 AES-CBC-HMAC
Supreme 支持带有可自定义 HMAC 的 AES-CBC 以提供 AEAD。
这在所有 _Supreme_ 目标上都受支持,其工作方式如下:
```
val payload = "More matter, with less art!".encodeToByteArray()
//define algorithm parameters
val algorithm = SymmetricEncryptionAlgorithm.AES_192.CBC.HMAC.SHA_512
//with a custom HMAC input calculation function
.Custom(32.bytes) { ciphertext, iv, aad -> //A shorter version of RFC 7518
aad + iv + ciphertext + aad.size.encodeTo4Bytes()
}
//any size is fine, really. omitting the override generates a mac key of the same size as the encryption key
val key = algorithm.randomKey(macKeyLength = 32.bit)
val aad = Clock.System.now().toString().encodeToByteArray()
val sealedBox = key.encrypt(
payload,
authenticatedData = aad,
).getOrThrow(/*handle error*/)
//because everything is structured, decryption is simple
val recovered = sealedBox.decrypt(key, aad).getOrThrow(/*handle error*/)
recovered shouldBe payload //success!
//we can also manually construct the sealed box, if we know the algorithm:
val reconstructed = algorithm.sealedBox.withNonce(sealedBox.nonce).from(
encryptedData = sealedBox.encryptedData, /*Could also access authenticatedCipherText*/
authTag = sealedBox.authTag,
).getOrThrow()
val manuallyRecovered = reconstructed.decrypt(
key,
authenticatedData = aad,
).getOrThrow(/*handle error*/)
manuallyRecovered shouldBe payload //great success!
//if we just know algorithm and key bytes, we can also construct a symmetric key
reconstructed.decrypt(
algorithm.keyFrom(key.encryptionKey.getOrThrow(), key.macKey.getOrThrow()).getOrThrow(/*handle error*/),
aad
).getOrThrow(/*handle error*/) shouldBe payload //greatest success!
```
## ASN.1 演示集锦
像 `CryptoPublicKey`、`X509Certificate`、`Pkcs10CertificationRequest` 等类都
实现了 `Asn1Encodable`,并且它们各自的伴生对象实现了 `Asn1Decodable`。
这意味着您可以执行诸如解析和检查证书、创建 CSR 或传输密钥
材料等操作。
### 证书解析
```
val cert = X509Certificate.decodeFromDer(certBytes)
when (val pk = cert.publicKey) {
is CryptoPublicKey.EC -> println(
"Certificate with serial no. ${
cert.tbsCertificate.serialNumber
} contains an EC public key using curve ${pk.curve}"
)
is CryptoPublicKey.RSA -> println(
"Certificate with serial no. ${
cert.tbsCertificate.serialNumber
} contains a ${pk.bits.number} bit RSA public key"
)
}
println("The full certificate is:\n${Json { prettyPrint = true }.encodeToString(cert)}")
println("Re-encoding it produces the same bytes? ${cert.encodeToDer() contentEquals certBytes}")
```
产生以下输出:
{ "tbsCertificate": {…
```
{
"tbsCertificate": {
"serialNumber": "GYIe3KaMWc8=",
"signatureAlgorithm": "ES384",
"issuerName": [
{
"type": "C",
"value": "13024154"
},
{
"type": "O",
"value": "133352657075626C696B204F65737465727265696368202876657274726574656E20647572636820424B4120756E6420424D445729"
},
{
"type": "OU",
"value": "130A542D556D676562756E67"
},
{
"type": "CN",
"value": "132B542D52657075626C696B2D4F657374657272656963682D41757468656E746966697A696572756E672D3031"
}
],
"validFrom": "170D3233303932303132343135305A",
"validUntil": "170D3233303932333132353134395A",
"subjectName": [
{
"type": "C",
"value": "13024154"
},
{
"type": "O",
"value": "133352657075626C696B204F65737465727265696368202876657274726574656E20647572636820424B4120756E6420424D445729"
},
{
"type": "OU",
"value": "130A542D556D676562756E67"
},
{
"type": "CN",
"value": "1340542D42696E64756E67732D5A6572746966696B61742D4157502D3165306436383063656464613439636539313337386462613934326533663432346663663164"
}
],
"publicKey": {
"type": "EC",
"curve": "P-256",
"x": "/wlkNNLhIKmO7tQY1824tD6FSf1/evXzQui1quzsSpw=",
"y": "SggoS/B464PKcHXT9phYxBPOnMEwL/ZC+Q9vZXoxY/g="
},
"extensions": [
{
"id": "1.3.6.1.5.5.7.1.1",
"value": "MDEwLwYIKwYBBQUHMAGGI2h0dHA6Ly9vY3NwMy5vZXN0ZXJyZWljaC5ndi5hdC9vY3Nw"
},
{
"id": "2.5.29.14",
"value": "BBRQQnap5sOMkNX+lCHhWGstLkEe6Q=="
},
{
"id": "2.5.29.35",
"value": "MBaAFAgwoHa6fUvtsBT+jMHkTBAnomXU"
},
{
"id": "2.5.29.31",
"value": "MDQwMqAwoC6GLGh0dHA6Ly9jcmwzLm9lc3RlcnJlaWNoLmd2LmF0L2NybC9vZWd2LzFhY2Ex"
},
{
"id": "2.5.29.15",
"critical": true,
"value": "AwIHgA=="
},
{
"id": "2.5.29.37",
"critical": true,
"value": "MAoGCCsGAQUFBwMC"
},
{
"id": "1.2.40.0.10.2.6.1.1",
"value": "MA2gAwIBAIEGcmVhZGVy"
}
]
},
"signatureAlgorithm": "ES384",
"signature": "MGQCMEAqUL8qRpPwDi7u1qeEXfJp7Pk4GE4diI9GTSTE/yzFEHJD/o6SRy+lCbJgo58+AwIwCTsMgGdWLIMkN9n1KsuLt6jD/FFF1qzHuj5cTH4JeY0bNwLPxvAUVk3V43pCfMgD"
}
```
### 创建 CSR
```
val ecPublicKey: ECPublicKey = TODO("From platform-specific code")
val cryptoPublicKey = CryptoPublicKey.EC.fromJcaPublicKey(ecPublicKey).getOrThrow()
val commonName = "DefaultCryptoService"
val signatureAlgorithm = X509SignatureAlgorithm.ES256
val tbsCsr = TbsCertificationRequest(
version = 0,
subjectName = listOf(RelativeDistinguishedName(AttributeTypeAndValue.CommonName(Asn1String.UTF8(commonName)))),
publicKey = cryptoPublicKey
)
val signed: ByteArray = TODO("pass tbsCsr.encodeToDer() to platform code")
val csr = Pkcs10CertificationRequest(tbsCsr, signatureAlgorithm, signed)
println(csr.encodeToDer())
```
产生以下输出:
### 处理通用 ASN.1 结构
上面展示的魔法基于从头开始 100% KMP 实现的 ASN.1 编码器和解析器。
要解析任何 DER 编码的 ASN.1 结构,请调用以下任意一种:
* `Asn1Element.parse()`,它将消耗所有字节并返回第一个解析出的 ASN.1 元素。
如果发生解析错误或在解析第一个元素后留下了任何尾部字节,此方法将抛出异常。
* `Asn1Element.parseFirst()`,它将尝试解析单个顶层 ASN.1 元素。
任何剩余的字节仍然可以从迭代器中消耗,因为它只会被推进到刚解析完的第一个元素之后。
* `Asn1Element.parseAll()`,它消耗所有字节,解析所有顶层 ASN.1 元素,并将它们作为列表返回。
遇到任何解析错误时抛出异常。
可以通过访问延迟评估的 `.derEncoded` 属性对 `Asn1Element` 进行编码。
即使对于已解析的元素,这也是一次真正的重新编码。解码后原始字节将被丢弃。
**请注意,如果提供了无效数据,解码操作将抛出异常!**
已解析的 `Asn1Element` 既可以是原始类型(可以读取其标签和值),也可以是结构(如集合或
序列),可以根据需要处理其子节点。`Asn1Element` 的子类反映了这一点:
* `Asn1Primitive`
* `Asn1BitString` (为了方便)
* `Asn1PrimitiveOctetString` (为了方便)
* `Asn1Structure`
* `Asn1Sequence` 和 `Asn1SequenceOf`
* `Asn1Set` 和 `Asn1SetOf` (默认对子节点进行排序)
* `Asn1EncapsulatingOctetString` (标记为 OCTET STRING,包含有效的 ASN.1 结构或原始类型)
* `Asn1ExplicitlyTagged` (用户指定的标签 + CONTEXT_SPECIFIC + CONSTRUCTED)
* `Asn1CustomStructure` (任何不符合上述选项的其他 CONSTRUCTED 标签。CONSTRUCTED 位可以被覆盖)
存在便利包装器,可以转换为任何子类型(例如 `.asSequence()`)。如果无法进行转换,这些简写函数将抛出 `Asn1Exception`。
任何复杂的数据结构(如 CSR、公钥、证书……)都实现了 `Asn1Encodable`,这意味着您可以:
* 通过调用 `.encodeToTlv()` 将其封装到 ASN.1 树中
* 通过 `.encodetoDer()` 函数直接获取 DER 编码的字节数组
为原始类型(数字、布尔值、字符串、bigints)提供了一对辅助函数:
* `encodeToAsn1Primitive` 用于生成可直接进行 DER 编码的 `Asn1Primitive`
* `encodeToAsn1ContentBytes` 用于生成 TLV 原始类型的内容字节(即 TLV 中的 _V_)
这些函数还有针对 `Instant` 和 `ByteArray` 的变体。
查看 [Asn1Encoding.kt](indispensable-asn1/src/commonMain/kotlin/at/asitplus/signum/indispensable/asn1/encoding/Asn1Encoding.kt) 以获取
完整的辅助函数列表。
#### 解码值
存在各种辅助函数来促进解码 `Asn1Primitives` 中包含的值,例如 `readInt()`。
为了也支持解码更复杂的结构,复杂类(如证书、CSR 等)的伴生对象
实现了 `Asn1Decodable`,这允许:
* 通过调用 `.decodeFromDer(bytes)` 和 `.decodeFromDerHexString` 直接解析 DER 编码的字节数组
* 通过调用 `.decodefromTlv(src)` 处理 `Asn1Element`
编码和解码函数都有两种 _安全_(即不抛出异常)的变体:
* `…Safe()` 返回一个 [KmmResult](https://github.com/a-sit-plus/kmmresult)
* `…orNull()` 在出错时返回 null
与编码类似,也存在一对用于原始类型的解码函数:
* `decodeToXXX` 在 `Asn1Primitive` 上调用,以将 DER 编码的原始类型解码为目标类型
* `decodeFromAsn1ContentBytes` 在目标类型的伴生对象上调用,以解码 TLV 原始类型的内容字节(即 TLV 中的 _V_)
然而,任何内容都可以被随意解码和标记。因此,存在一个通用的解码函数,它具有以下
签名:
```
inline fun
Asn1Primitive.decode(assertTag: Asn1Element.Tag, decode: (content: ByteArray) -> T)
```
查看 [Asn1Decoding.kt](indispensable-asn1/src/commonMain/kotlin/at/asitplus/signum/indispensable/asn1/encoding/Asn1Decoding.kt) 以获取
完整的辅助函数列表。
#### 用于创建 ASN.1 结构的 ASN1 DSL
虽然完全有可能手动构建 `Asn1Element` 对象的层次结构,但我们提供了一个更方便的
DSL,它返回一个 `Asn1Structure`:
```
Asn1.Sequence {
+ExplicitlyTagged(1uL) {
+Asn1Primitive(Asn1Element.Tag.BOOL, byteArrayOf(0x00)) //or +Asn1.Bool(false)
}
+Asn1.Set {
+Asn1.Sequence {
+Asn1.SetOf {
+PrintableString("World")
+PrintableString("Hello")
}
+Asn1.Set {
+PrintableString("World")
+PrintableString("Hello")
+Utf8String("!!!")
}
}
}
+Asn1.Null()
+ObjectIdentifier("1.2.603.624.97")
+(Utf8String("Foo") withImplicitTag (0xCAFEuL withClass TagClass.PRIVATE))
+PrintableString("Bar")
//fake Primitive
+(Asn1.Sequence { +Asn1.Int(42) } withImplicitTag (0x5EUL without CONSTRUCTED))
+Asn1.Set {
+Asn1.Int(3)
+Asn1.Int(-65789876543L)
+Asn1.Bool(false)
+Asn1.Bool(true)
}
+Asn1.Sequence {
+Asn1.Null()
+Asn1String.Numeric("12345")
+UtcTime(Clock.System.now())
}
} withImplicitTag (1337uL withClass TagClass.APPLICATION)
```
根据 DER 编码,这会生成以下 ASN.1 结构:
```
Application 1337 (9 elem)
[1] (1 elem)
BOOLEAN false
SET (1 elem)
SEQUENCE (2 elem)
SET (2 elem)
PrintableString World
PrintableString Hello
SET (3 elem)
UTF8String !!!
PrintableString World
PrintableString Hello
NULL
OBJECT IDENTIFIER 1.2.603.624.97
Private 51966 (3 byte) Foo
PrintableString Bar
[94] (3 byte) 02012A
SET (4 elem)
BOOLEAN false
BOOLEAN true
INTEGER 3
INTEGER (36 bit) -65789876543
SEQUENCE (3 elem)
NULL
NumericString 12345
UTCTime 2024-09-16 11:53:51 UTC
```
## 限制
* 仅支持 DER 编码和解析 ASN.1 结构
* 高级抽象(例如 `X509Certificate`)在某些方面过于宽松,
而在其他方面又过于严格。
例如:DSA 签名的证书将无法解析为 `X509Certificate` 实例。
同时,多次包含相同扩展名的证书将正常工作,即使它们违反了
规范。
这在实践中无关紧要,因为特定于平台的代码将根据这些
数据结构执行实际的加密操作,并且如果出现问题也会抱怨。
* 没有 OCSP 和 CRL 检查(尽管完全可以从此类证书中解析数据并实现检查)
* 支持的算法数量仅限于常见的内容(抱歉,不支持 Bernstein 曲线 )-:)
## 贡献
非常感谢外部贡献!请务必遵守贡献指南(参见 [CONTRIBUTING.md](CONTRIBUTING.md))。
特别是,本项目的外部贡献受 A-SIT Plus 贡献者许可协议约束(另请参见 [CONTRIBUTING.md](CONTRIBUTING.md))。
| 
由
欧盟 共同资助 | 本项目已获得欧盟 Digital Europe Programme (DIGITAL)、项目 101102655 — POTENTIAL 的资金支持。 |
|:-----------------------------------------------------------------------------------------------:|:-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
Apache 许可证不适用于徽标(包括 A-SIT 徽标)和项目/模块名称,因为这些是
A-SIT/A-SIT Plus GmbH 的唯一财产,未经明确许可,不得在衍生作品中使用!