eddsa #1

Merged
pfm merged 4 commits from eddsa into main 2026-03-28 03:07:13 +00:00
10 changed files with 165 additions and 45 deletions
Showing only changes of commit fae965fa32 - Show all commits

View file

@ -33,11 +33,12 @@ val Deps = new {
}
val Gs = new {
val Datagen: ModuleID = "gs" %% "gs-datagen-core-v0" % "0.1.1"
val Datagen: ModuleID = "gs" %% "gs-datagen-core-v0" % "0.4.1"
}
val BouncyCastle = new {
val Provider: ModuleID = "org.bouncycastle" % "bcprov-jdk18on" % "1.83"
val PKIX: ModuleID = "org.bouncycastle" % "bcpkix-jdk18on" % "1.83"
}
val MUnit: ModuleID = "org.scalameta" %% "munit" % "1.2.2"
@ -65,7 +66,8 @@ lazy val core = project
)
.settings(
libraryDependencies ++= Seq(
Deps.Cats.Effect
Deps.Cats.Effect,
Deps.BouncyCastle.PKIX
)
)

View file

@ -1,12 +1,39 @@
package gs.crypto.v0
import cats.effect.Sync
import cats.syntax.all.*
import java.security.PrivateKey
import java.security.PublicKey
/** Base implementation for [[KeyLoader]] that provides standard parsing
* functionality.
*/
abstract class BaseKeyLoader[F[_]] extends KeyLoader[F]:
/** @return
* The key loader configuration.
*/
abstract class BaseKeyLoader[F[_]: Sync] extends KeyLoader[F]:
def loadPublicKey(
location: String,
algorithm: String
): F[Either[KeyLoadError, PublicKey]] =
loadKey(location).flatMap {
case Left(err) => Sync[F].pure(Left(err))
case Right(key) =>
KeyLoader.loadPublicKey[F](key, algorithm).map(Right(_))
}
def loadPrivateKey(
location: String,
algorithm: String
): F[Either[KeyLoadError, PrivateKey]] =
loadKey(location).flatMap {
case Left(err) => Sync[F].pure(Left(err))
case Right(key) =>
KeyLoader.loadPrivateKey[F](key, algorithm).map(Right(_))
}
/** @return
* The key loader configuration.
*/
def config: BaseKeyLoader.Config
protected def prepareKey(base: String): Either[KeyLoadError, Array[Byte]] =

View file

@ -2,7 +2,6 @@ package gs.crypto.v0
import cats.effect.Sync
import cats.syntax.all.*
import java.nio.charset.Charset
import java.nio.file.Files
import java.nio.file.Path
@ -14,8 +13,7 @@ import java.nio.file.Path
* The expected character set for the loaded file.
*/
class FileKeyLoader[F[_]: Sync](
val config: BaseKeyLoader.Config,
val charset: Charset
val config: BaseKeyLoader.Config
) extends BaseKeyLoader[F]:
/** @inheritDocs
@ -28,5 +26,5 @@ class FileKeyLoader[F[_]: Sync](
case false =>
Sync[F].pure(Left(KeyLoadError.PathNotRegularFile(p, location)))
case true =>
Sync[F].delay(Files.readString(p, charset)).map(prepareKey)
Sync[F].delay(Files.readString(p)).map(prepareKey)
}

View file

@ -1,38 +1,61 @@
package gs.crypto.v0
import cats.Applicative
import cats.effect.Sync
import java.nio.charset.Charset
import java.nio.charset.StandardCharsets
import java.security.KeyFactory
import java.security.PrivateKey
import java.security.PublicKey
import java.security.spec.PKCS8EncodedKeySpec
import java.security.spec.X509EncodedKeySpec
/** Interface for loading keys (e.g. public/private keys) into memory.
*/
trait KeyLoader[F[_]]:
/** Load the key from the given location.
/** Load the key from the given location. This gets the raw bytes, but does
* not handle _any_ standard encodings such as PKCS8 or X509. These are
* handled by helper functions (static) or [[BaseKeyLoader]].
*
* @param location
* The location of the key.
* @return
* The loaded key.
* The key's raw bytes - ready for further decoding.
*/
def loadKey(location: String): F[Either[KeyLoadError, Array[Byte]]]
object KeyLoader:
def stringLoader[F[_]: Applicative](
config: BaseKeyLoader.Config
def stringLoader[F[_]: Sync](
config: BaseKeyLoader.Config = BaseKeyLoader.Defaults.Config
): StringKeyLoader[F] =
new StringKeyLoader[F](config)
new StringKeyLoader(config)
def fileLoader[F[_]: Sync](
config: BaseKeyLoader.Config,
charset: Charset = StandardCharsets.UTF_8
config: BaseKeyLoader.Config = BaseKeyLoader.Defaults.Config
): FileKeyLoader[F] =
new FileKeyLoader(config, charset)
new FileKeyLoader(config)
def resourceLoader[F[_]: Sync](
config: BaseKeyLoader.Config
config: BaseKeyLoader.Config = BaseKeyLoader.Defaults.Config
): ResourceKeyLoader[F] =
new ResourceKeyLoader(config)
def loadPublicKey[F[_]: Sync](
publicKeyRawBytes: Array[Byte],
algorithm: String
): F[PublicKey] =
Sync[F].delay {
val spec = new X509EncodedKeySpec(publicKeyRawBytes)
val keyFactory = KeyFactory.getInstance(algorithm)
keyFactory.generatePublic(spec)
}
def loadPrivateKey[F[_]: Sync](
privateKeyRawBytes: Array[Byte],
algorithm: String
): F[PrivateKey] =
Sync[F].delay {
val spec = new PKCS8EncodedKeySpec(privateKeyRawBytes)
val keyFactory = KeyFactory.getInstance(algorithm)
keyFactory.generatePrivate(spec)
}
end KeyLoader

View file

@ -3,4 +3,8 @@ package gs.crypto.v0
/** Used to verify cryptographic signatures.
*/
trait SignatureVerifier[F[_]]:
def verify(signature: Signature): F[SignatureValidity]
def verify(
signature: Signature,
data: Array[Byte]
): F[SignatureValidity]

View file

@ -1,19 +1,15 @@
package gs.crypto.v0
import cats.Applicative
import cats.effect.Sync
/** Implementation of [[KeyLoader]] that loads keys directly from input strings.
*
* @param config
* The key loader configuration.
*/
class StringKeyLoader[F[_]: Applicative](
class StringKeyLoader[F[_]: Sync](
val config: BaseKeyLoader.Config
) extends BaseKeyLoader[F]:
/** @inheritDocs
*/
override def loadKey(
location: String
): F[Either[KeyLoadError, Array[Byte]]] =
Applicative[F].pure(prepareKey(location))
override def loadKey(location: String): F[Either[KeyLoadError, Array[Byte]]] =
Sync[F].delay(prepareKey(location))

View file

@ -0,0 +1,7 @@
package gs.crypto.v0.eddsa
object Ed25519:
val Algorithm: String = "Ed25519"
end Ed25519

View file

@ -5,7 +5,7 @@ import gs.crypto.v0.Signature
import gs.crypto.v0.Signer
import java.nio.charset.Charset
import java.nio.charset.StandardCharsets
import org.bouncycastle.crypto.params.Ed25519PrivateKeyParameters
import java.security.PrivateKey
/** Implementation of [[Signer]] that uses the Ed25519 system to calculate
* signatures.
@ -16,18 +16,17 @@ import org.bouncycastle.crypto.params.Ed25519PrivateKeyParameters
* The private key.
*/
final class Ed25519Signer[F[_]: Sync](
privateKey: Array[Byte]
privateKey: PrivateKey
) extends Signer[F]:
private lazy val params = new Ed25519PrivateKeyParameters(privateKey)
/** @inheritDocs
*/
override def sign(data: Array[Byte]): F[Signature] =
Sync[F].delay {
val s = new org.bouncycastle.crypto.signers.Ed25519Signer()
val _ = s.init(true, params)
val _ = s.update(data, 0, data.length)
Signature(s.generateSignature())
val s = java.security.Signature.getInstance(Ed25519.Algorithm)
val _ = s.initSign(privateKey)
val _ = s.update(data)
Signature(s.sign())
}
/** @inheritDocs

View file

@ -4,7 +4,7 @@ import cats.effect.Sync
import gs.crypto.v0.Signature
import gs.crypto.v0.SignatureValidity
import gs.crypto.v0.SignatureVerifier
import org.bouncycastle.crypto.params.Ed25519PublicKeyParameters
import java.security.PublicKey
/** Implementation of [[SignatureVerifier]] that uses the Ed25519 system to
* verify signatures.
@ -15,15 +15,18 @@ import org.bouncycastle.crypto.params.Ed25519PublicKeyParameters
* The public key.
*/
final class Ed25519Verifier[F[_]: Sync](
publicKey: Array[Byte]
publicKey: PublicKey
) extends SignatureVerifier[F]:
private lazy val params = new Ed25519PublicKeyParameters(publicKey)
/** @inheritDocs
*/
override def verify(signature: Signature): F[SignatureValidity] =
override def verify(
signature: Signature,
data: Array[Byte] // TODO: CLEAN UP
): F[SignatureValidity] =
Sync[F].delay {
val s = new org.bouncycastle.crypto.signers.Ed25519Signer()
val _ = s.init(false, params)
SignatureValidity(s.verifySignature(signature.unwrap()))
val s = java.security.Signature.getInstance(Ed25519.Algorithm)
val _ = s.initVerify(publicKey)
val _ = s.update(data)
SignatureValidity(s.verify(signature.unwrap()))
}

View file

@ -0,0 +1,61 @@
package gs.crypto.v0.eddsa
import cats.effect.IO
import cats.effect.unsafe.IORuntime
import gs.crypto.v0.KeyLoader
import gs.crypto.v0.SignatureValidity
import java.security.PrivateKey
import java.security.PublicKey
import java.util.UUID
import munit.Location
class Ed25519Tests extends munit.FunSuite:
import Ed25519Tests.Resources
given IORuntime = IORuntime.global
def iotest(
name: String
)(
f: => IO[Unit]
)(
using
Location
): Unit =
test(name)(f.unsafeRunSync())
val keyLoader = KeyLoader.resourceLoader[IO]()
iotest(
"should sign some data and verify that signature"
) {
val data = UUID.randomUUID().toString()
loadKeysOrFail().flatMap { case (publicKey, privateKey) =>
val signer = new Ed25519Signer[IO](privateKey)
val verifier = new Ed25519Verifier[IO](publicKey)
for
signature <- signer.sign(data)
validity <- verifier.verify(signature, data.getBytes())
yield assertEquals(validity, SignatureValidity.Valid)
}
}
private def loadKeysOrFail(): IO[(PublicKey, PrivateKey)] =
keyLoader.loadPublicKey(Resources.PublicKey, Ed25519.Algorithm).flatMap {
case Left(_) => fail("Failed to load public key.")
case Right(pub) =>
keyLoader.loadPrivateKey(Resources.PrivateKey, Ed25519.Algorithm).map {
case Left(_) => fail("Failed to load private key.")
case Right(priv) => (pub, priv)
}
}
object Ed25519Tests:
object Resources:
val PublicKey: String = "public.pem"
val PrivateKey: String = "private.pem"
end Resources
end Ed25519Tests