migration to 2.0. New version with reinforced signed box to include timestamp and expiration

This commit is contained in:
Sergey Chernov 2024-06-08 16:18:07 +07:00
parent b86c7a853e
commit fe0cb59c51
9 changed files with 123 additions and 50 deletions

6
.gitignore vendored
View File

@ -39,4 +39,8 @@ bin/
.vscode/
### Mac OS ###
.DS_Store
.DS_Store
# Other
.kotlin
.idea

View File

@ -0,0 +1,8 @@
<component name="ArtifactManager">
<artifact type="jar" name="crypto2-js-0.1.1-SNAPSHOT">
<output-path>$PROJECT_DIR$/build/libs</output-path>
<root id="archive" name="crypto2-js-0.1.1-SNAPSHOT.jar">
<element id="module-output" name="crypto2.jsMain" />
</root>
</artifact>
</component>

View File

@ -0,0 +1,8 @@
<component name="ArtifactManager">
<artifact type="jar" name="crypto2-jvm-0.1.1-SNAPSHOT">
<output-path>$PROJECT_DIR$/build/libs</output-path>
<root id="archive" name="crypto2-jvm-0.1.1-SNAPSHOT.jar">
<element id="module-output" name="crypto2.jvmMain" />
</root>
</artifact>
</component>

2
.idea/misc.xml generated
View File

@ -4,7 +4,7 @@
<component name="FrameworkDetectionExcludesConfiguration">
<file type="web" url="file://$PROJECT_DIR$" />
</component>
<component name="ProjectRootManager" version="2" languageLevel="JDK_1_8" project-jdk-name="17 (5)" project-jdk-type="JavaSDK">
<component name="ProjectRootManager" version="2" languageLevel="JDK_17" default="true" project-jdk-name="17 (5)" project-jdk-type="JavaSDK">
<output url="file://$PROJECT_DIR$/out" />
</component>
</project>

View File

@ -1,46 +1,35 @@
import org.jetbrains.kotlin.gradle.plugin.KotlinJsCompilerType
plugins {
kotlin("multiplatform") version "1.9.20"
id("org.jetbrains.kotlin.plugin.serialization") version "1.9.20"
kotlin("multiplatform") version "2.0.0"
id("org.jetbrains.kotlin.plugin.serialization") version "2.0.0"
`maven-publish`
}
group = "net.sergeych"
version = "0.1.1-SNAPSHOT"
version = "0.2.1-SNAPSHOT"
repositories {
mavenCentral()
maven("https://maven.universablockchain.com/")
maven("https://gitea.sergeych.net/api/packages/SergeychWorks/maven")
mavenLocal()
}
kotlin {
jvm {
jvmToolchain(8)
withJava()
testRuns.named("test") {
executionTask.configure {
useJUnitPlatform()
}
}
}
js(KotlinJsCompilerType.IR) {
browser {
}
}
val hostOs = System.getProperty("os.name")
val isArm64 = System.getProperty("os.arch") == "aarch64"
val isMingwX64 = hostOs.startsWith("Windows")
@Suppress("UNUSED_VARIABLE")
val nativeTarget = when {
hostOs == "Mac OS X" && isArm64 -> macosArm64("native")
hostOs == "Mac OS X" && !isArm64 -> macosX64("native")
hostOs == "Linux" && isArm64 -> linuxArm64("native")
hostOs == "Linux" && !isArm64 -> linuxX64("native")
isMingwX64 -> mingwX64("native")
else -> throw GradleException("Host OS is not supported in Kotlin/Native.")
jvm()
// {
// jvmToolchain(8)
// withJava()
// testRuns.named("test") {
// executionTask.configure {
// useJUnitPlatform()
// }
// }
// }
js(IR) {
browser()
nodejs()
}
linuxX64("native")
val ktor_version = "2.3.6"
@ -53,13 +42,13 @@ kotlin {
val commonMain by getting {
dependencies {
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.7.3")
implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:1.5.1")
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.8.1")
implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:1.7.0")
implementation("com.ionspin.kotlin:multiplatform-crypto-libsodium-bindings:0.9.0")
api("com.ionspin.kotlin:bignum:0.3.8")
api("net.sergeych:mp_bintools:0.0.6")
api("net.sergeych:mp_bintools:0.1.5-SNAPSHOT")
api("net.sergeych:mp_stools:1.4.1")
}
}
@ -80,11 +69,11 @@ kotlin {
}
}
val jsTest by getting
val nativeMain by getting {
dependencies {
}
}
val nativeTest by getting
// val nativeMain by getting {
// dependencies {
// }
// }
// val nativeTest by getting
}
}

View File

@ -1,11 +1,64 @@
package net.sergeych.crypto2
import kotlinx.datetime.Instant
import kotlinx.serialization.Serializable
import net.sergeych.bipack.BipackEncoder
import net.sergeych.utools.now
@Serializable
class Seal(
val publicKey: SigningKey.Public,
val signature: UByteArray
val signature: UByteArray,
val createdAt: Instant,
val expiresAt: Instant? = null,
) {
inline fun verify(message: UByteArray) = publicKey.verify(signature, message)
@Suppress("unused")
@Serializable
class SealedData(
val message: UByteArray,
val createdAt: Instant?,
val validUntil: Instant?,
)
/**
* Return true if the seal is correct, see [verify]
*/
fun isValid(message: UByteArray): Boolean = kotlin.runCatching { verify(message) }.isSuccess
/**
* Return Result containing success or error reason if the seal is not correct
*/
fun check(message: UByteArray): Result<Unit> = kotlin.runCatching { verify(message) }
/**
* Check that message is correct for this seal and throws exception if it is not.
* Note that tampering [createdAt] and [expiresAt] invalidate the seal too.
*
* See [check] and [isValid] for non-throwing checks.
*
* @throws ExpiredSignatureException
* @throws IllegalSignatureException
*/
fun verify(message: UByteArray) {
val n = now()
if (createdAt > n) throw IllegalSignatureException("signature's timestamp in the future")
expiresAt?.let {
if (n >= it) throw ExpiredSignatureException("signature expired at $it")
}
val data = BipackEncoder.encode(SealedData(message, createdAt, expiresAt))
if (!publicKey.verify(signature, data.toUByteArray()))
throw IllegalSignatureException()
}
companion object {
operator fun invoke(
key: SigningKey.Secret, message: UByteArray,
createdAt: Instant = now(),
expiresAt: Instant? = null,
): Seal {
val data = BipackEncoder.encode(SealedData(message, createdAt, expiresAt)).toUByteArray()
return Seal(key.publicKey, key.sign(data), createdAt, expiresAt)
}
}
}

View File

@ -8,6 +8,9 @@ import kotlinx.serialization.Transient
* instances and [SignedBox.plus] to add more signatures (signing keys), and
* [SignedBox.contains] to check for a specific key signature presence.
*
* Signatures, [Seal], incorporate creation time and optional expiration which are
* also signed and checked upon deserialization.
*
* It is serializable and checks integrity on deserialization. If any of seals does not
* match the signed [message], it throws [IllegalSignatureException] _on deserialization_.
* E.g., if you have it deserialized, it is ok, check it contains all needed keys among
@ -31,7 +34,7 @@ class SignedBox(
*/
operator fun plus(key: SigningKey.Secret): SignedBox =
if (key.publicKey in this) this
else SignedBox(message, seals + key.seal(message), false)
else SignedBox(message, seals + key.seal(message),false)
/**
* Check that it is signed with a specified key.
@ -43,7 +46,7 @@ class SignedBox(
init {
if (seals.isEmpty()) throw IllegalArgumentException("there should be at least one seal")
if (checkOnInit) {
if (!seals.all { it.verify(message) }) throw IllegalSignatureException()
for( s in seals ) s.verify(message)
}
}
@ -59,7 +62,8 @@ class SignedBox(
* @param keys a list of keys to sign with, should be at least one key.
* @throws IllegalArgumentException if keys are not specified.
*/
operator fun invoke(data: UByteArray, vararg keys: SigningKey.Secret): SignedBox =
SignedBox(data, keys.map { it.seal(data) }, false)
operator fun invoke(data: UByteArray, vararg keys: SigningKey.Secret): SignedBox {
return SignedBox(data, keys.map { it.seal(data) }, false)
}
}
}

View File

@ -2,9 +2,11 @@ package net.sergeych.crypto2
import com.ionspin.kotlin.crypto.signature.InvalidSignatureException
import com.ionspin.kotlin.crypto.signature.Signature
import kotlinx.datetime.Instant
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
import net.sergeych.crypto2.SigningKey.Secret
import net.sergeych.crypto2.SigningKey.Companion.pair
import net.sergeych.utools.now
/**
* Keys in general: public, secret and later symmetric too.
@ -60,7 +62,9 @@ sealed class SigningKey {
fun sign(message: UByteArray): UByteArray = Signature.detached(message, packed)
fun seal(message: UByteArray): Seal = Seal(this.publicKey, sign(message))
fun seal(message: UByteArray, validUntil: Instant? = null): Seal =
Seal(this, message, now(), validUntil)
override fun toString(): String = "Sct:${super.toString()}"
}
@ -75,4 +79,7 @@ sealed class SigningKey {
}
}
class IllegalSignatureException: RuntimeException("signed data is tampered or signature is corrupted")
open class IllegalSignatureException(text: String="signed data is tampered or signature is corrupted")
: IllegalStateException(text)
class ExpiredSignatureException(text: String): IllegalSignatureException(text)

View File

@ -23,10 +23,10 @@ class KeysTest {
val data = "8 rays dev!".encodeToUByteArray()
val data1 = "8 rays dev!".encodeToUByteArray()
val s = stk.seal(data)
assertTrue(s.verify(data))
s.verify(data)
data1[0] = 0x01u
assertFalse(s.verify(data1))
assertFalse(s.isValid(data1))
val p2 = SigningKey.pair()
val p3 = SigningKey.pair()