-
Notifications
You must be signed in to change notification settings - Fork 1
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Create self-signed certificate under iOS
This implementas a rudimentary DER encoder for X.509 certificates
- Loading branch information
Showing
5 changed files
with
256 additions
and
10 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
133 changes: 133 additions & 0 deletions
133
vclib/src/commonMain/kotlin/at/asitplus/wallet/lib/jws/X509Certificate.kt
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,133 @@ | ||
package at.asitplus.wallet.lib.jws | ||
|
||
import at.asitplus.wallet.lib.CryptoPublicKey | ||
import at.asitplus.wallet.lib.jws.JwsExtensions.encodeToByteArray | ||
import io.matthewnelson.encoding.base16.Base16 | ||
import io.matthewnelson.encoding.core.Decoder.Companion.decodeToByteArray | ||
import kotlinx.datetime.Instant | ||
|
||
data class TbsCertificate( | ||
val version: Int = 2, | ||
val serialNumber: Long, | ||
val signatureAlgorithm: JwsAlgorithm, | ||
val issuer: String, | ||
val validFrom: Instant, | ||
val validUntil: Instant, | ||
val subject: String, | ||
val subjectPublicKey: CryptoPublicKey | ||
) { | ||
fun encodeToDer(): ByteArray { | ||
return (version.encodeAsVersion() + | ||
serialNumber.encodeToDer() + | ||
signatureAlgorithm.encodeToDer() + | ||
issuer.encodeAsCommonName() + | ||
(validFrom.encodeToDer() + validUntil.encodeToDer()).sequence() + | ||
subject.encodeAsCommonName() + | ||
subjectPublicKey.encodeToDer()) | ||
.sequence() | ||
} | ||
} | ||
|
||
data class X509Certificate( | ||
val tbsCertificate: TbsCertificate, | ||
val signatureAlgorithm: JwsAlgorithm, | ||
val signature: ByteArray | ||
) { | ||
fun encodeToDer(): ByteArray { | ||
return (tbsCertificate.encodeToDer() + | ||
signatureAlgorithm.encodeToDer() + | ||
signature.encodeAsBitString()).sequence() | ||
} | ||
|
||
override fun equals(other: Any?): Boolean { | ||
if (this === other) return true | ||
if (other == null || this::class != other::class) return false | ||
|
||
other as X509Certificate | ||
|
||
if (tbsCertificate != other.tbsCertificate) return false | ||
if (signatureAlgorithm != other.signatureAlgorithm) return false | ||
if (!signature.contentEquals(other.signature)) return false | ||
|
||
return true | ||
} | ||
|
||
override fun hashCode(): Int { | ||
var result = tbsCertificate.hashCode() | ||
result = 31 * result + signatureAlgorithm.hashCode() | ||
result = 31 * result + signature.contentHashCode() | ||
return result | ||
} | ||
} | ||
|
||
private fun String.encodeAsCommonName(): ByteArray { | ||
return ("550403".decodeToByteArray(Base16()).oid() + this.encodeToDer()).sequence().set().sequence() | ||
} | ||
|
||
private fun Int.encodeAsVersion(): ByteArray = encodeToDer().wrapInAsn1Tag(0xA0.toByte()) | ||
|
||
private fun Int.encodeToDer(): ByteArray = | ||
encodeToByteArray().dropWhile { it == 0.toByte() }.toByteArray().wrapInAsn1Tag(0x02) | ||
|
||
private fun Long.encodeToDer(): ByteArray = | ||
encodeToByteArray().dropWhile { it == 0.toByte() }.toByteArray().wrapInAsn1Tag(0x02) | ||
|
||
private fun CryptoPublicKey.encodeToDer(): ByteArray = when (this) { | ||
is CryptoPublicKey.Ec -> this.encodeToDer() | ||
} | ||
|
||
private fun CryptoPublicKey.Ec.encodeToDer(): ByteArray { | ||
val ecKeyTag = "2A8648CE3D0201".decodeToByteArray(Base16()).oid() | ||
val ecEncryptionNullTag = "2A8648CE3D030107".decodeToByteArray(Base16()).oid() | ||
val content = (byteArrayOf(0x04.toByte()) + x + y).encodeAsBitString() | ||
return ((ecKeyTag + ecEncryptionNullTag).sequence() + content).sequence() | ||
} | ||
|
||
private fun ByteArray.encodeAsBitString(): ByteArray = (byteArrayOf(0x00) + this).wrapInAsn1Tag(0x03) | ||
|
||
private fun String.encodeToDer(): ByteArray = this.encodeToByteArray().wrapInAsn1Tag(0x0c) | ||
|
||
private fun Instant.encodeToDer(): ByteArray { | ||
val matchResult = | ||
Regex("[0-9]{2}([0-9]{2})-([0-9]{2})-([0-9]{2})T([0-9]{2}):([0-9]{2}):([0-9]{2})\\.([0-9]+)Z") | ||
.matchEntire(toString()) | ||
?: throw IllegalArgumentException("instant serialization failed") | ||
val year = matchResult.groups[1]?.value ?: throw IllegalArgumentException("instant serialization failed") | ||
val month = matchResult.groups[2]?.value ?: throw IllegalArgumentException("instant serialization failed") | ||
val day = matchResult.groups[3]?.value ?: throw IllegalArgumentException("instant serialization failed") | ||
val hour = matchResult.groups[4]?.value ?: throw IllegalArgumentException("instant serialization failed") | ||
val minute = matchResult.groups[5]?.value ?: throw IllegalArgumentException("instant serialization failed") | ||
val seconds = matchResult.groups[6]?.value ?: throw IllegalArgumentException("instant serialization failed") | ||
val string = "$year$month$day$hour$minute${seconds}Z" | ||
return string.encodeToByteArray().wrapInAsn1Tag(0x17) | ||
} | ||
|
||
private fun JwsAlgorithm.encodeToDer(): ByteArray { | ||
return when (this) { | ||
JwsAlgorithm.ES256 -> "2A8648CE3D040302".decodeToByteArray(Base16()).oid().sequence() | ||
else -> TODO() | ||
} | ||
} | ||
|
||
private fun ByteArray.sequence() = this.wrapInAsn1Tag(0x30) | ||
|
||
private fun ByteArray.set() = this.wrapInAsn1Tag(0x31) | ||
|
||
private fun ByteArray.oid() = this.wrapInAsn1Tag(0x06) | ||
|
||
private fun ByteArray.wrapInAsn1Tag(tag: Byte): ByteArray { | ||
return byteArrayOf(tag) + this.size.encodeLength() + this | ||
} | ||
|
||
private fun Int.encodeLength(): ByteArray { | ||
if (this < 128) { | ||
return byteArrayOf(this.toByte()) | ||
} | ||
if (this < 0x100) { | ||
return byteArrayOf(0x81.toByte(), this.toByte()) | ||
} | ||
if (this < 0x8000) { | ||
return byteArrayOf(0x82.toByte(), (this ushr 8).toByte(), this.toByte()) | ||
} | ||
throw IllegalArgumentException("length $this") | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
95 changes: 95 additions & 0 deletions
95
vclib/src/jvmTest/kotlin/at/asitplus/wallet/lib/jws/X509CertificateJvmTest.kt
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,95 @@ | ||
package at.asitplus.wallet.lib.jws | ||
|
||
import at.asitplus.wallet.lib.CryptoPublicKey | ||
import at.asitplus.wallet.lib.agent.jcaName | ||
import at.asitplus.wallet.lib.jws.JwsExtensions.ensureSize | ||
import io.kotest.core.spec.style.FreeSpec | ||
import io.kotest.matchers.shouldBe | ||
import io.matthewnelson.encoding.base16.Base16 | ||
import io.matthewnelson.encoding.core.Encoder.Companion.encodeToString | ||
import kotlinx.datetime.toKotlinInstant | ||
import org.bouncycastle.asn1.x500.X500Name | ||
import org.bouncycastle.asn1.x509.SubjectPublicKeyInfo | ||
import org.bouncycastle.cert.X509v3CertificateBuilder | ||
import org.bouncycastle.operator.ContentSigner | ||
import org.bouncycastle.operator.jcajce.JcaContentSignerBuilder | ||
import java.math.BigInteger | ||
import java.security.KeyPair | ||
import java.security.KeyPairGenerator | ||
import java.security.Signature | ||
import java.security.cert.CertificateFactory | ||
import java.security.interfaces.ECPublicKey | ||
import java.time.Instant | ||
import java.util.Date | ||
import kotlin.math.absoluteValue | ||
import kotlin.random.Random | ||
import kotlin.time.Duration.Companion.days | ||
|
||
class X509CertificateJvmTest : FreeSpec({ | ||
|
||
lateinit var ecCurve: EcCurve | ||
lateinit var keyPair: KeyPair | ||
|
||
beforeTest { | ||
ecCurve = EcCurve.SECP_256_R_1 | ||
keyPair = KeyPairGenerator.getInstance("EC").also { | ||
it.initialize(256) | ||
}.genKeyPair() | ||
} | ||
|
||
"Certificates match" { | ||
val ecPublicKey = keyPair.public as ECPublicKey | ||
val keyX = ecPublicKey.w.affineX.toByteArray().ensureSize(ecCurve.coordinateLengthBytes) | ||
val keyY = ecPublicKey.w.affineY.toByteArray().ensureSize(ecCurve.coordinateLengthBytes) | ||
val keyId = MultibaseHelper.calcKeyId(ecCurve, keyX, keyY)!! | ||
val cryptoPublicKey = CryptoPublicKey.Ec(curve = ecCurve, keyId = keyId, x = keyX, y = keyY) | ||
|
||
// create certificate with bouncycastle | ||
val notBeforeDate = Date.from(Instant.now()) | ||
val notAfterDate = Date.from(Instant.now().plusSeconds(30.days.inWholeSeconds)) | ||
val serialNumber: BigInteger = BigInteger.valueOf(Random.nextLong().absoluteValue) | ||
val commonName = "DefaultCryptoService" | ||
val issuer = X500Name("CN=$commonName") | ||
val builder = X509v3CertificateBuilder( | ||
/* issuer = */ issuer, | ||
/* serial = */ serialNumber, | ||
/* notBefore = */ notBeforeDate, | ||
/* notAfter = */ notAfterDate, | ||
/* subject = */ issuer, | ||
/* publicKeyInfo = */ SubjectPublicKeyInfo.getInstance(keyPair.public.encoded) | ||
) | ||
val signatureAlgorithm = JwsAlgorithm.ES256 | ||
val contentSigner: ContentSigner = JcaContentSignerBuilder(signatureAlgorithm.jcaName).build(keyPair.private) | ||
val certificateHolder = builder.build(contentSigner) | ||
|
||
// create certificate with our structure | ||
val tbsCertificate = TbsCertificate( | ||
version = 2, | ||
serialNumber = serialNumber.toLong(), | ||
issuer = commonName, | ||
validFrom = notBeforeDate.toInstant().toKotlinInstant(), | ||
validUntil = notAfterDate.toInstant().toKotlinInstant(), | ||
signatureAlgorithm = signatureAlgorithm, | ||
subject = commonName, | ||
subjectPublicKey = cryptoPublicKey | ||
) | ||
val signed = Signature.getInstance(signatureAlgorithm.jcaName).apply { | ||
initSign(keyPair.private) | ||
update(tbsCertificate.encodeToDer()) | ||
}.sign() | ||
val x509Certificate = X509Certificate(tbsCertificate, signatureAlgorithm, signed) | ||
|
||
val kotlinEncoded = x509Certificate.encodeToDer() | ||
val jvmEncoded = certificateHolder.encoded | ||
println("Certificates will never entirely match because of randomness in ECDSA signature") | ||
//kotlinEncoded shouldBe jvmEncoded | ||
println(kotlinEncoded.encodeToString(Base16())) | ||
println(jvmEncoded.encodeToString(Base16())) | ||
|
||
kotlinEncoded.drop(7).take(228) shouldBe jvmEncoded.drop(7).take(228) | ||
|
||
val parsedFromKotlinCertificate = CertificateFactory.getInstance("X.509").generateCertificate(kotlinEncoded.inputStream()) | ||
parsedFromKotlinCertificate.verify(keyPair.public) | ||
} | ||
|
||
}) |