-
Notifications
You must be signed in to change notification settings - Fork 3
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
x509 certificates for validation #26
Changes from 3 commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,26 +1,72 @@ | ||
package me.uma.protocol | ||
|
||
import java.security.cert.CertificateFactory | ||
import java.security.cert.X509Certificate | ||
import java.security.interfaces.ECPublicKey | ||
import kotlinx.serialization.Serializable | ||
import kotlinx.serialization.encodeToString | ||
import kotlinx.serialization.json.Json | ||
import me.uma.utils.ByteArrayAsHexSerializer | ||
import me.uma.utils.X509CertificateSerializer | ||
|
||
/** | ||
* Response from another VASP when requesting public keys. | ||
* | ||
* @property signingCertificate PEM encoded X.509 certificate string. | ||
* Used to verify signatures from the VASP. | ||
* @property encryptionCertificate PEM encoded X.509 certificate string. | ||
* Used to encrypt TR info sent to the VASP. | ||
* @property signingPubKey The public key used to verify signatures from the VASP. | ||
* @property encryptionPubKey The public key used to encrypt TR info sent to the VASP. | ||
* @property expirationTimestamp Seconds since epoch at which these pub keys must be refreshed. | ||
* They can be safely cached until this expiration (or forever if null). | ||
*/ | ||
@Serializable | ||
data class PubKeyResponse @JvmOverloads constructor( | ||
data class PubKeyResponse internal constructor( | ||
@Serializable(with = X509CertificateSerializer::class) | ||
val signingCertificate: X509Certificate?, | ||
@Serializable(with = X509CertificateSerializer::class) | ||
val encryptionCertificate: X509Certificate?, | ||
@Serializable(with = ByteArrayAsHexSerializer::class) | ||
val signingPubKey: ByteArray, | ||
private val signingPubKey: ByteArray?, | ||
@Serializable(with = ByteArrayAsHexSerializer::class) | ||
val encryptionPubKey: ByteArray, | ||
private val encryptionPubKey: ByteArray?, | ||
val expirationTimestamp: Long? = null, | ||
) { | ||
@JvmOverloads | ||
constructor(signingKey: ByteArray, encryptionKey: ByteArray, expirationTs: Long? = null) : this( | ||
signingCertificate = null, | ||
encryptionCertificate = null, | ||
signingPubKey = signingKey, | ||
encryptionPubKey = encryptionKey, | ||
expirationTimestamp = expirationTs, | ||
) | ||
|
||
@JvmOverloads | ||
constructor(signingCert: String, encryptionCert: String, expirationTs: Long? = null) : this( | ||
signingCertificate = signingCert.toX509Certificate(), | ||
encryptionCertificate = encryptionCert.toX509Certificate(), | ||
signingPubKey = signingCert.toX509Certificate().getPubKeyBytes(), | ||
encryptionPubKey = encryptionCert.toX509Certificate().getPubKeyBytes(), | ||
expirationTimestamp = expirationTs, | ||
) | ||
|
||
fun getSigningPubKey(): ByteArray { | ||
return if (signingCertificate != null) { | ||
signingCertificate.getPubKeyBytes() | ||
} else { | ||
signingPubKey ?: throw IllegalStateException("No signing public key") | ||
} | ||
} | ||
|
||
fun getEncryptionPubKey(): ByteArray { | ||
return if (encryptionCertificate != null) { | ||
encryptionCertificate.getPubKeyBytes() | ||
} else { | ||
encryptionPubKey ?: throw IllegalStateException("No encryption public key") | ||
} | ||
} | ||
|
||
override fun equals(other: Any?): Boolean { | ||
if (this === other) return true | ||
if (javaClass != other?.javaClass) return false | ||
|
@@ -30,16 +76,37 @@ data class PubKeyResponse @JvmOverloads constructor( | |
if (!signingPubKey.contentEquals(other.signingPubKey)) return false | ||
if (!encryptionPubKey.contentEquals(other.encryptionPubKey)) return false | ||
if (expirationTimestamp != other.expirationTimestamp) return false | ||
if (signingCertificate != other.signingCertificate) return false | ||
if (encryptionCertificate != other.encryptionCertificate) return false | ||
|
||
return true | ||
} | ||
|
||
override fun hashCode(): Int { | ||
var result = signingPubKey.contentHashCode() | ||
result = 31 * result + encryptionPubKey.contentHashCode() | ||
result = 31 * result + (expirationTimestamp?.hashCode() ?: 0) | ||
result = 31 * result + expirationTimestamp.hashCode() | ||
result = 31 * result + signingCertificate.hashCode() | ||
result = 31 * result + encryptionCertificate.hashCode() | ||
return result | ||
} | ||
|
||
fun toJson() = Json.encodeToString(this) | ||
} | ||
|
||
private fun String.toX509Certificate(): X509Certificate { | ||
return CertificateFactory.getInstance("X.509") | ||
.generateCertificate(byteInputStream()) as? X509Certificate | ||
?: throw IllegalStateException("Could not be parsed as X.509 certificate") | ||
} | ||
|
||
private fun X509Certificate.getPubKeyBytes(): ByteArray { | ||
if (publicKey !is ECPublicKey || | ||
!(publicKey as ECPublicKey).params.toString().contains("secp256k1") | ||
) { | ||
throw IllegalStateException("Public key extracted from certificate is not EC/secp256k1") | ||
} | ||
// encryptionPubKey.publicKey is an ASN.1/DER encoded X.509/SPKI key, the last 65 | ||
// bytes are the uncompressed public key | ||
return publicKey.encoded.takeLast(65).toByteArray() | ||
Comment on lines
+109
to
+111
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Ooh cool :-). This starts with There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. yes! added an example of what it looks like here https://github.com/uma-universal-money-address/uma-kotlin-sdk/pull/26/files#diff-c3d18805abe410a5e76b66b0d653e58f34ae72a732bb2d5bb85c033f48d98410R30 |
||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,24 @@ | ||
package me.uma.utils | ||
|
||
import java.io.ByteArrayInputStream | ||
import java.security.cert.CertificateFactory | ||
import java.security.cert.X509Certificate | ||
import kotlinx.serialization.KSerializer | ||
import kotlinx.serialization.descriptors.buildClassSerialDescriptor | ||
import kotlinx.serialization.encoding.Decoder | ||
import kotlinx.serialization.encoding.Encoder | ||
|
||
@OptIn(ExperimentalStdlibApi::class) | ||
class X509CertificateSerializer : KSerializer<X509Certificate> { | ||
override val descriptor = buildClassSerialDescriptor("X509Certificate") | ||
|
||
override fun serialize(encoder: Encoder, value: X509Certificate) { | ||
encoder.encodeString(value.encoded.toHexString()) | ||
} | ||
|
||
override fun deserialize(decoder: Decoder): X509Certificate { | ||
val bytes = decoder.decodeString().hexToByteArray() | ||
return CertificateFactory.getInstance("X.509") | ||
.generateCertificate(ByteArrayInputStream(bytes)) as X509Certificate | ||
} | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We should also test a response that has all 4 for the backwards compat case.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
discussed on slack! will handle this alongside backwards compatibility for the other data models