Skip to content

Commit

Permalink
Merge pull request #61 from TBD54566975/secp256k1
Browse files Browse the repository at this point in the history
Refactor `Signer.sign` and finish implementing `secp256k1` support
  • Loading branch information
mistermoe authored Oct 9, 2023
2 parents 20ee82c + 26ae727 commit 723d2aa
Show file tree
Hide file tree
Showing 7 changed files with 311 additions and 119 deletions.
2 changes: 1 addition & 1 deletion CODEOWNERS
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
# The format is described: https://github.blog/2017-07-06-introducing-code-owners/

# These owners will be the default owners for everything in the repo.
* @andresuribe87 @mistermoe @nitro-neal
* @andresuribe87 @mistermoe @nitro-neal @tomdaffurn @phoebe-lew

# -----------------------------------------------
# BELOW THIS LINE ARE TEMPLATES, UNUSED
Expand Down
183 changes: 122 additions & 61 deletions crypto/src/main/kotlin/web5/sdk/crypto/Crypto.kt
Original file line number Diff line number Diff line change
@@ -1,14 +1,14 @@
package web5.sdk.crypto

import com.nimbusds.jose.Algorithm
import com.nimbusds.jose.JWSAlgorithm
import com.nimbusds.jose.Payload
import com.nimbusds.jose.jwk.Curve
import com.nimbusds.jose.jwk.JWK
import web5.sdk.crypto.Crypto.generatePrivateKey
import web5.sdk.crypto.Crypto.getPublicKeyBytes
import web5.sdk.crypto.Crypto.publicKeyToBytes
import web5.sdk.crypto.Crypto.sign

public typealias CryptoAlgorithm = Pair<Algorithm, Curve?>

/**
* Cryptography utility object providing key generation, signature creation, and other crypto-related functionalities.
*
Expand All @@ -17,7 +17,7 @@ import web5.sdk.crypto.Crypto.sign
* It offers convenience methods to:
* - Generate private keys ([generatePrivateKey])
* - Create digital signatures ([sign])
* - Retrieve public key bytes ([getPublicKeyBytes])
* - conversion from JWK <-> bytes ([publicKeyToBytes])
* - Get relevant key generators and signers based on algorithm and curve type.
*
* Internally, it utilizes predefined mappings to pair algorithms and curve types with their respective [KeyGenerator]
Expand All @@ -38,13 +38,10 @@ import web5.sdk.crypto.Crypto.sign
* @see Signer for signing functionalities.
*/
public object Crypto {
private val keyGenerators = mapOf<Algorithm, Map<Curve?, KeyGenerator>>(
JWSAlgorithm.EdDSA to mapOf(
Curve.Ed25519 to Ed25519
),
JWSAlgorithm.ES256K to mapOf<Curve?, KeyGenerator>(
Curve.SECP256K1 to Secp256k1
)
private val keyGenerators = mapOf<CryptoAlgorithm, KeyGenerator>(
Pair(Secp256k1.algorithm, null) to Secp256k1,
Pair(Secp256k1.algorithm, Curve.SECP256K1) to Secp256k1,
Pair(Ed25519.algorithm, Curve.Ed25519) to Ed25519
)

private val keyGeneratorsByMultiCodec = mapOf<Int, KeyGenerator>(
Expand All @@ -55,21 +52,15 @@ public object Crypto {
)

private val multiCodecsByAlgorithm = mapOf(
JWSAlgorithm.EdDSA to mapOf(
Curve.Ed25519 to Ed25519.pubMulticodec
),
JWSAlgorithm.ES256K to mapOf(
Curve.SECP256K1 to Secp256k1.pubMulticodec
)
Pair(Secp256k1.algorithm, null) to Secp256k1.pubMulticodec,
Pair(Secp256k1.algorithm, Curve.SECP256K1) to Secp256k1.pubMulticodec,
Pair(Ed25519.algorithm, Curve.Ed25519) to Ed25519.pubMulticodec
)

private val signers = mapOf<Algorithm, Map<Curve?, Signer>>(
JWSAlgorithm.EdDSA to mapOf(
Curve.Ed25519 to Ed25519
),
JWSAlgorithm.ES256K to mapOf(
Curve.SECP256K1 to Secp256k1
)
private val signers = mapOf<CryptoAlgorithm, Signer>(
Pair(Secp256k1.algorithm, null) to Secp256k1,
Pair(Secp256k1.algorithm, Curve.SECP256K1) to Secp256k1,
Pair(Ed25519.algorithm, Curve.Ed25519) to Ed25519
)

/**
Expand All @@ -89,61 +80,91 @@ public object Crypto {
/**
* Computes a public key from the given private key, utilizing relevant [KeyGenerator].
*
* @param privateKey The JWK private key used to compute the public key.
* @param privateKey The private key used to compute the public key.
* @return The computed public key as a JWK object.
*/
public fun computePublicKey(privateKey: JWK): JWK {
val rawCurve = privateKey.toJSONObject()["crv"]
val curve = rawCurve?.let { Curve.parse(it.toString()) }
val generator = getKeyGenerator(privateKey.algorithm, curve)

return generator.getPublicKey(privateKey)
return generator.computePublicKey(privateKey)
}

/**
* Signs a payload using the specified private key and options.
* Signs a payload using a private key.
*
* This function utilizes the appropriate [Signer] to generate a digital signature
* of the provided payload using the provided private key.
*
* @param privateKey The JWK private key used for signing.
* @param payload The payload to be signed.
* @param options Options for the signing process, possibly including additional metadata or specifications.
* @throws IllegalArgumentException if the curve or algorithm of the private key is not supported.
* @param privateKey The JWK private key to be used for generating the signature.
* @param payload The byte array data to be signed.
* @param options Options for the signing operation, may include specific parameters relevant to the algorithm.
* @return The digital signature as a byte array.
*/
public fun sign(privateKey: JWK, payload: Payload, options: SignOptions) {
public fun sign(privateKey: JWK, payload: ByteArray, options: SignOptions? = null): ByteArray {
val rawCurve = privateKey.toJSONObject()["crv"]
val curve = rawCurve?.let { Curve.parse(it.toString()) }

val signer = getSigner(privateKey.algorithm, curve)

signer.sign(privateKey, payload, options)
return signer.sign(privateKey, payload, options)
}

/**
* Verifies the JSON Web Signature (JWS) using the provided public key.
* Verifies a signature against a signed payload using a public key.
*
* This function utilizes the relevant verifier, determined by the algorithm and curve
* used in the JWK, to ensure the provided signature is valid for the signed payload
* using the provided public key. The algorithm used can either be specified in the
* public key JWK or passed explicitly as a parameter. If it is not found in either,
* an exception will be thrown.
*
* ## Note
* Algorithm **MUST** either be present on the [JWK] or be provided explicitly
*
* @param publicKey The JWK public key to be used for verifying the signature.
* @param signedPayload The byte array data that was signed.
* @param signature The signature that will be verified.
* @param algorithm Optional parameter: the algorithm used for signing/verification,
* if not provided in the JWK. Default is null.
*
* @param publicKey The JSON Web Key (JWK) containing the public key used for verification.
* @param jws The JSON Web Signature string to verify.
* @throws IllegalArgumentException if neither the JWK nor the explicit parameter
* provides an algorithm.
*
*/
public fun verify(publicKey: JWK, jws: String) {
val rawCurve = publicKey.toJSONObject()["crv"]
val curve = rawCurve?.let { Curve.parse(it.toString()) }
public fun verify(publicKey: JWK, signedPayload: ByteArray, signature: ByteArray, algorithm: Algorithm? = null) {
val alg = publicKey.algorithm ?: algorithm
?: throw IllegalArgumentException("Algorithm must either be set on JWK or provided explicitly.")

val curve = getJwkCurve(publicKey)
val verifier = getVerifier(alg, curve)

val verifier = getSigner(publicKey.algorithm, curve)
verifier.verify(publicKey, jws)
verifier.verify(publicKey, signedPayload, signature)
}


/**
* Extracts the public key's bytes.
* Converts a [JWK] public key into its byte array representation.
*
* This function retrieves the public key's byte representation using relevant key generators.
* @param publicKey A [JWK] object representing the public key to be converted.
* @return A [ByteArray] representing the byte-level information of the provided public key.
*
* @param publicKey The JWK public key to extract bytes from.
* @return The byte array representation of the public key.
* @throws IllegalArgumentException if the algorithm or curve is not supported.
* ### Example
* ```kotlin
* val publicKeyBytes = publicKeyToBytes(myJwkPublicKey)
* ```
*
* ### Note
* This function assumes that the provided [JWK] contains valid curve and algorithm
* information. Malformed or invalid [JWK] objects may result in exceptions or
* unexpected behavior.
*
* ### Throws
* - [IllegalArgumentException] If the algorithm or curve in [JWK] is not supported or invalid.
*/
public fun getPublicKeyBytes(publicKey: JWK): ByteArray {
val rawCurve = publicKey.toJSONObject()["crv"]
val curve = rawCurve?.let { Curve.parse(it.toString()) }
public fun publicKeyToBytes(publicKey: JWK): ByteArray {
val curve = getJwkCurve(publicKey)
val generator = getKeyGenerator(publicKey.algorithm, curve)

return generator.publicKeyToBytes(publicKey)
Expand All @@ -161,15 +182,9 @@ public object Crypto {
* @throws IllegalArgumentException if the algorithm or curve is not supported.
*/
public fun getKeyGenerator(algorithm: Algorithm, curve: Curve? = null): KeyGenerator {
val keyGenAlgorithm = keyGenerators.getOrElse(algorithm) {
return keyGenerators.getOrElse(Pair(algorithm, curve)) {
throw IllegalArgumentException("Algorithm $algorithm not supported")
}

val keyGenerator = keyGenAlgorithm.getOrElse(curve) {
throw IllegalArgumentException("Curve $curve not supported")
}

return keyGenerator
}

/**
Expand Down Expand Up @@ -200,14 +215,60 @@ public object Crypto {
* @throws IllegalArgumentException if the algorithm or curve is not supported.
*/
public fun getSigner(algorithm: Algorithm, curve: Curve? = null): Signer {
val signerAlgorithm = signers.getOrElse(algorithm) {
return signers.getOrElse(Pair(algorithm, curve)) {
throw IllegalArgumentException("Algorithm $algorithm not supported")
}
}

val signer = signerAlgorithm.getOrElse(curve) {
throw IllegalArgumentException("Curve $curve not supported")
}
/**
* Retrieves a [Signer] to be used for verification based on the provided algorithm and curve.
*
* This function fetches the appropriate [Signer], which contains the verification
* logic for the cryptographic approach determined by the specified algorithm and curve.
*
* @param algorithm The cryptographic algorithm to find a verifier for.
* @param curve The cryptographic curve to find a verifier for.
* @return The corresponding [Signer] capable of verification.
* @throws IllegalArgumentException if the algorithm or curve is not supported.
*/
public fun getVerifier(algorithm: Algorithm, curve: Curve? = null): Signer {
return getSigner(algorithm, curve)
}

return signer
/**
* Extracts the cryptographic curve information from a [JWK] object.
*
* This function parses and returns the curve type used in a JWK.
* May return `null` if the curve information is not present or unsupported.
*
* @param jwk The JWK object from which to extract curve information.
* @return The [Curve] used in the JWK, or `null` if the curve is not defined or recognized.
*/
public fun getJwkCurve(jwk: JWK): Curve? {
val rawCurve = jwk.toJSONObject()["crv"]

return rawCurve?.let { Curve.parse(it.toString()) }
}

/**
* Retrieves the multicodec identifier associated with a given cryptographic algorithm and curve.
*
* This function consults a predefined mapping of cryptographic algorithm and curve pairs to their
* respective multicodec identifiers, returning the matched identifier.
* Multicodec identifiers are useful for encoding the format or type of the key in systems that
* leverage multiple cryptographic standards.
*
* @param algorithm The cryptographic [Algorithm] for which the multicodec is requested.
* @param curve The cryptographic [Curve] associated with the algorithm, or null if not applicable.
* @return The multicodec identifier as an [Int] if a mapping exists, or null if the algorithm and curve
* combination is not supported or mapped.
*
* ### Example
* ```kotlin
* val multicodec = getAlgorithmMultiCodec(JWSAlgorithm.EdDSA, Curve.Ed25519)
* ```
*/
public fun getAlgorithmMultiCodec(algorithm: Algorithm, curve: Curve?): Int? {
return multiCodecsByAlgorithm[Pair(algorithm, curve)]
}
}
36 changes: 18 additions & 18 deletions crypto/src/main/kotlin/web5/sdk/crypto/Ed25519.kt
Original file line number Diff line number Diff line change
@@ -1,12 +1,9 @@
package web5.sdk.crypto

import com.google.crypto.tink.subtle.Ed25519Sign
import com.google.crypto.tink.subtle.Ed25519Verify
import com.nimbusds.jose.Algorithm
import com.nimbusds.jose.JWSAlgorithm
import com.nimbusds.jose.JWSHeader
import com.nimbusds.jose.JWSObject
import com.nimbusds.jose.Payload
import com.nimbusds.jose.crypto.Ed25519Signer
import com.nimbusds.jose.crypto.Ed25519Verifier
import com.nimbusds.jose.jwk.Curve
import com.nimbusds.jose.jwk.JWK
import com.nimbusds.jose.jwk.KeyType
Expand All @@ -20,6 +17,8 @@ import web5.sdk.crypto.Ed25519.algorithm
import web5.sdk.crypto.Ed25519.keyType
import web5.sdk.crypto.Ed25519.privMultiCodec
import web5.sdk.crypto.Ed25519.pubMulticodec
import java.security.GeneralSecurityException
import java.security.SignatureException

/**
* Implementation of the [KeyGenerator] and [Signer] interfaces, specifically utilizing
Expand Down Expand Up @@ -64,7 +63,7 @@ public object Ed25519 : KeyGenerator, Signer {
* @param privateKey The private key in JWK format.
* @return The corresponding public key in JWK format.
*/
override fun getPublicKey(privateKey: JWK): JWK {
override fun computePublicKey(privateKey: JWK): JWK {
require(privateKey is OctetKeyPair) { "private key must be an Octet Key Pair (kty: OKP)" }

return privateKey.toOctetKeyPair().toPublicJWK()
Expand Down Expand Up @@ -107,25 +106,26 @@ public object Ed25519 : KeyGenerator, Signer {
.build()
}

override fun sign(privateKey: JWK, payload: Payload, options: SignOptions?): String {
val jwsHeader = JWSHeader.Builder(JWSAlgorithm.EdDSA)
.keyID(privateKey.keyID)
.build()
override fun sign(privateKey: JWK, payload: ByteArray, options: SignOptions?): ByteArray {
validateKey(privateKey)

val jws = JWSObject(jwsHeader, payload)
val signer = Ed25519Signer(privateKey as OctetKeyPair)
jws.sign(signer)
val privateKeyBytes = privateKeyToBytes(privateKey)
val signer = Ed25519Sign(privateKeyBytes)

return jws.serialize()
return signer.sign(payload)
}

override fun verify(publicKey: JWK, jws: String, options: VerifyOptions?) {
override fun verify(publicKey: JWK, signedPayload: ByteArray, signature: ByteArray, options: VerifyOptions?) {
validateKey(publicKey)

val parsedJws = JWSObject.parse(jws)
val verifier = Ed25519Verifier(publicKey.toOctetKeyPair())
val publicKeyBytes = publicKeyToBytes(publicKey)
val verifier = Ed25519Verify(publicKeyBytes)

parsedJws.verify(verifier)
try {
verifier.verify(signature, signedPayload)
} catch (e: GeneralSecurityException) {
throw SignatureException(e.message, e)
}
}

/**
Expand Down
4 changes: 2 additions & 2 deletions crypto/src/main/kotlin/web5/sdk/crypto/KeyGenerator.kt
Original file line number Diff line number Diff line change
Expand Up @@ -76,15 +76,15 @@ public interface KeyGenerator {
* Derives a public key from the private key provided. Applicable for asymmetric Key Generators only.
* Implementers of symmetric key generators should throw an UnsupportedOperation Exception
*/
public fun getPublicKey(privateKey: JWK): JWK
public fun computePublicKey(privateKey: JWK): JWK

/**
* Converts a private key to bytes.
*/
public fun privateKeyToBytes(privateKey: JWK): ByteArray

/**
* Converts a public key to bytes. Applicable for asymmetric Key Generators only.
* Converts a public key to bytes. Applicable for asymmetric [KeyGenerator] implementations only.
* Implementers of symmetric key generators should throw an UnsupportedOperation Exception
*/
public fun publicKeyToBytes(publicKey: JWK): ByteArray
Expand Down
Loading

0 comments on commit 723d2aa

Please sign in to comment.