diff --git a/javatest/src/test/java/me/uma/javatest/UmaTest.java b/javatest/src/test/java/me/uma/javatest/UmaTest.java index a0514ab..b427757 100644 --- a/javatest/src/test/java/me/uma/javatest/UmaTest.java +++ b/javatest/src/test/java/me/uma/javatest/UmaTest.java @@ -20,7 +20,7 @@ public class UmaTest { private static final String PUBKEY_HEX = "04419c5467ea563f0010fd614f85e885ac99c21b8e8d416241175fdd5efd2244fe907e2e6fa3dd6631b1b17cd28798da8d882a34c4776d44cc4090781c7aadea1b"; private static final String PRIVKEY_HEX = "77e891f0ecd265a3cda435eaa73792233ebd413aeb0dbb66f2940babfc9a2667"; - private static final String CERT = "-----BEGIN CERTIFICATE-----\n" + + private static final String CERT_CHAIN = "-----BEGIN CERTIFICATE-----\n" + "MIIB1zCCAXygAwIBAgIUGN3ihBj1RnKoeTM/auDFnNoThR4wCgYIKoZIzj0EAwIw\n" + "QjELMAkGA1UEBhMCVVMxEzARBgNVBAgMCmNhbGlmb3JuaWExDjAMBgNVBAcMBWxv\n" + "cyBhMQ4wDAYDVQQKDAVsaWdodDAeFw0yNDAzMDUyMTAzMTJaFw0yNDAzMTkyMTAz\n" + @@ -31,6 +31,22 @@ public class UmaTest { "bMwwHwYDVR0jBBgwFoAUU87LnQdiP6XIE6LoKU1PZnbtbMwwDwYDVR0TAQH/BAUw\n" + "AwEB/zAKBggqhkjOPQQDAgNJADBGAiEAvsrvoeo3rbgZdTHxEUIgP0ArLyiO34oz\n" + "NlwL4gk5GpgCIQCvRx4PAyXNV9T6RRE+3wFlqwluOc/pPOjgdRw/wpoNPQ==\n" + + "-----END CERTIFICATE-----\n" + + "-----BEGIN CERTIFICATE-----\n" + + "MIICdjCCAV6gAwIBAgIUAekCcU1Qhjo2Y6L2Down9BLdfdUwDQYJKoZIhvcNAQEL\n" + + "BQAwNDELMAkGA1UEBhMCVVMxCzAJBgNVBAgMAmNhMQwwCgYDVQQHDANsb3MxCjAI\n" + + "BgNVBAoMAWEwHhcNMjQwMzA4MDEwNTU3WhcNMjUwMzA4MDEwNTU3WjBAMQswCQYD\n" + + "VQQGEwJVUzELMAkGA1UECAwCY2ExDDAKBgNVBAcMA2xvczEKMAgGA1UECgwBYTEK\n" + + "MAgGA1UECwwBYTBWMBAGByqGSM49AgEGBSuBBAAKA0IABJ11ZAQKylgIzZmuI5NE\n" + + "+DyZ9BUDZhxUPSxTxl+s1am+Lxzr9D7wlwOiiqCYHFWpL6lkCmJcCC06P3RyzXIT\n" + + "KmyjQjBAMB0GA1UdDgQWBBRXgW6xGB3+mTSSUKlhSiu3LS+TKTAfBgNVHSMEGDAW\n" + + "gBTFmyv7+YDpK0WAOHJYAzjynmWsMDANBgkqhkiG9w0BAQsFAAOCAQEAFVAA3wo+\n" + + "Hi/k+OWO/1CFqIRV/0cA8F05sBMiKVA11xB6I1y54aUV4R0jN76fOiN1jnZqTRnM\n" + + "G8rZUfQgE/LPVbb1ERHQfd8yaeI+TerKdPkMseu/jnvI+dDJfQdsY7iaa7NPO0dm\n" + + "t8Nz75cYW8kYuDaq0Hb6uGsywf9LGO/VjrDhyiRxmZ1Oq4JxQmLuh5SDcPfqHTR3\n" + + "VbMC1b7eVXaA9O2qYS36zv8cCUSUl5sOSwM6moaFN+xLtVNJ6ZhKPNS2Gd8znhzZ\n" + + "AQZcDDpXBO6ORNbhVk5A3X6eQX4Ek1HBTa3pcSUQomYAA9TIuVzL6DSot5GWS8Ek\n" + + "usLY8crt6ys3KQ==\n" + "-----END CERTIFICATE-----"; @Test @@ -267,7 +283,7 @@ public void serializeAndDeserializePubKeyResponse() { assertEquals(keysOnlyResponse, parsedResponse); PubKeyResponse certsOnlyResponse = - new PubKeyResponse(CERT, CERT); + new PubKeyResponse(CERT_CHAIN, CERT_CHAIN); json = certsOnlyResponse.toJson(); parsedResponse = umaProtocolHelper.parseAsPubKeyResponse(json); assertNotNull(parsedResponse); diff --git a/uma-sdk/src/commonMain/kotlin/me/uma/protocol/PubKeyResponse.kt b/uma-sdk/src/commonMain/kotlin/me/uma/protocol/PubKeyResponse.kt index e64d091..45b1e3e 100644 --- a/uma-sdk/src/commonMain/kotlin/me/uma/protocol/PubKeyResponse.kt +++ b/uma-sdk/src/commonMain/kotlin/me/uma/protocol/PubKeyResponse.kt @@ -12,10 +12,10 @@ 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 signingCertChain list of X.509 certificates. The order of the certificates is from the + * leaf to the root. Used to verify signatures from the VASP. + * @property encryptionCertChain list of X.509 certificates. The order of the certificates is from the + * leaf to the root. 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. @@ -23,10 +23,8 @@ import me.uma.utils.X509CertificateSerializer */ @Serializable data class PubKeyResponse internal constructor( - @Serializable(with = X509CertificateSerializer::class) - val signingCertificate: X509Certificate?, - @Serializable(with = X509CertificateSerializer::class) - val encryptionCertificate: X509Certificate?, + val signingCertChain: List<@Serializable(with = X509CertificateSerializer::class) X509Certificate>?, + val encryptionCertChain: List<@Serializable(with = X509CertificateSerializer::class) X509Certificate>?, @Serializable(with = ByteArrayAsHexSerializer::class) private val signingPubKey: ByteArray?, @Serializable(with = ByteArrayAsHexSerializer::class) @@ -35,33 +33,33 @@ data class PubKeyResponse internal constructor( ) { @JvmOverloads constructor(signingKey: ByteArray, encryptionKey: ByteArray, expirationTs: Long? = null) : this( - signingCertificate = null, - encryptionCertificate = null, + signingCertChain = null, + encryptionCertChain = 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(), + constructor(signingCertChain: String, encryptionCertChain: String, expirationTs: Long? = null) : this( + signingCertChain = signingCertChain.toX509CertChain(), + encryptionCertChain = encryptionCertChain.toX509CertChain(), + signingPubKey = signingCertChain.toX509CertChain().getPubKeyBytes(), + encryptionPubKey = encryptionCertChain.toX509CertChain().getPubKeyBytes(), expirationTimestamp = expirationTs, ) fun getSigningPublicKey(): ByteArray { - return if (signingCertificate != null) { - signingCertificate.getPubKeyBytes() + return if (signingCertChain != null) { + signingCertChain.getPubKeyBytes() } else { signingPubKey ?: throw IllegalStateException("No signing public key") } } fun getEncryptionPublicKey(): ByteArray { - return if (encryptionCertificate != null) { - encryptionCertificate.getPubKeyBytes() + return if (encryptionCertChain != null) { + encryptionCertChain.getPubKeyBytes() } else { encryptionPubKey ?: throw IllegalStateException("No encryption public key") } @@ -76,8 +74,8 @@ data class PubKeyResponse internal 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 + if (signingCertChain != other.signingCertChain) return false + if (encryptionCertChain != other.encryptionCertChain) return false return true } @@ -86,24 +84,27 @@ data class PubKeyResponse internal constructor( var result = signingPubKey.contentHashCode() result = 31 * result + encryptionPubKey.contentHashCode() result = 31 * result + expirationTimestamp.hashCode() - result = 31 * result + signingCertificate.hashCode() - result = 31 * result + encryptionCertificate.hashCode() + result = 31 * result + signingCertChain.hashCode() + result = 31 * result + encryptionCertChain.hashCode() return result } fun toJson() = Json.encodeToString(this) } -private fun String.toX509Certificate(): X509Certificate { +private fun String.toX509CertChain(): List { return CertificateFactory.getInstance("X.509") - .generateCertificate(byteInputStream()) as? X509Certificate - ?: throw IllegalStateException("Could not be parsed as X.509 certificate") + .generateCertificates(byteInputStream()) + .map { + it 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") - ) { +private fun List.getPubKeyBytes(): ByteArray { + val publicKey = firstOrNull()?.publicKey + ?: throw IllegalStateException("Certificate chain is empty") + if (publicKey !is ECPublicKey || !publicKey.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