blob: 79783c300b9e674a9500049c08445413890e0977 [file] [log] [blame]
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You under the Apache License, Version 2.0
* (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.apache.tuweni.devp2p.v5.internal
import com.google.common.cache.Cache
import com.google.common.cache.CacheBuilder
import org.apache.tuweni.bytes.Bytes
import org.apache.tuweni.crypto.Hash
import org.apache.tuweni.crypto.SECP256K1
import org.apache.tuweni.devp2p.EthereumNodeRecord
import org.apache.tuweni.devp2p.v5.AuthenticationProvider
import org.apache.tuweni.devp2p.v5.storage.RoutingTable
import org.apache.tuweni.devp2p.v5.encrypt.AES128GCM
import org.apache.tuweni.devp2p.v5.encrypt.SessionKeyGenerator
import org.apache.tuweni.devp2p.v5.misc.AuthHeader
import org.apache.tuweni.devp2p.v5.misc.HandshakeInitParameters
import org.apache.tuweni.devp2p.v5.misc.SessionKey
import org.apache.tuweni.rlp.RLP
import java.util.concurrent.TimeUnit
class DefaultAuthenticationProvider(
private val keyPair: SECP256K1.KeyPair,
private val routingTable: RoutingTable
) : AuthenticationProvider {
private val sessionKeys: Cache<String, SessionKey> = CacheBuilder
.newBuilder()
.expireAfterWrite(SESSION_KEY_EXPIRATION, TimeUnit.MINUTES)
.build()
private val nodeId: Bytes = Hash.sha2_256(routingTable.getSelfEnr())
@Synchronized
override fun authenticate(handshakeParams: HandshakeInitParameters): AuthHeader {
// Generate ephemeral key pair
val ephemeralKeyPair = SECP256K1.KeyPair.random()
val ephemeralKey = ephemeralKeyPair.secretKey()
val destEnr = EthereumNodeRecord.fromRLP(handshakeParams.destEnr)
val destNodeId = Hash.sha2_256(handshakeParams.destEnr)
// Perform agreement
val secret = SECP256K1.calculateKeyAgreement(ephemeralKey, destEnr.publicKey())
// Derive keys
val sessionKey = SessionKeyGenerator.generate(nodeId, destNodeId, secret, handshakeParams.idNonce)
sessionKeys.put(destNodeId.toHexString(), sessionKey)
val signature = sign(keyPair, handshakeParams)
return generateAuthHeader(
routingTable.getSelfEnr(),
signature,
handshakeParams,
sessionKey.authRespKey,
ephemeralKeyPair.publicKey()
)
}
@Synchronized
override fun findSessionKey(nodeId: String): SessionKey? {
return sessionKeys.getIfPresent(nodeId)
}
@Synchronized
override fun setSessionKey(nodeId: String, sessionKey: SessionKey) {
sessionKeys.put(nodeId, sessionKey)
}
@Synchronized
override fun finalizeHandshake(senderNodeId: Bytes, authHeader: AuthHeader) {
val ephemeralPublicKey = SECP256K1.PublicKey.fromBytes(authHeader.ephemeralPublicKey)
val secret = SECP256K1.calculateKeyAgreement(keyPair.secretKey(), ephemeralPublicKey)
val sessionKey = SessionKeyGenerator.generate(senderNodeId, nodeId, secret, authHeader.idNonce)
val decryptedAuthResponse = AES128GCM.decrypt(authHeader.authResponse, sessionKey.authRespKey, Bytes.EMPTY)
RLP.decodeList(Bytes.wrap(decryptedAuthResponse)) { reader ->
reader.skipNext()
val signatureBytes = reader.readValue()
val enrRLP = reader.readValue()
val enr = EthereumNodeRecord.fromRLP(enrRLP)
val publicKey = enr.publicKey()
val signatureVerified = verifySignature(signatureBytes, authHeader.idNonce, publicKey)
if (!signatureVerified) {
throw IllegalArgumentException("Signature is not verified")
}
sessionKeys.put(senderNodeId.toHexString(), sessionKey)
routingTable.add(enrRLP)
}
}
private fun sign(keyPair: SECP256K1.KeyPair, params: HandshakeInitParameters): SECP256K1.Signature {
val signValue = Bytes.wrap(DISCOVERY_ID_NONCE, params.idNonce)
val hashedSignValue = Hash.sha2_256(signValue)
return SECP256K1.sign(hashedSignValue, keyPair)
}
private fun verifySignature(signatureBytes: Bytes, idNonce: Bytes, publicKey: SECP256K1.PublicKey): Boolean {
val signature = SECP256K1.Signature.fromBytes(signatureBytes)
val signValue = Bytes.wrap(DISCOVERY_ID_NONCE, idNonce)
val hashedSignValue = Hash.sha2_256(signValue)
return SECP256K1.verify(hashedSignValue, signature, publicKey)
}
private fun generateAuthHeader(
enr: Bytes,
signature: SECP256K1.Signature,
params: HandshakeInitParameters,
authRespKey: Bytes,
ephemeralPubKey: SECP256K1.PublicKey
): AuthHeader {
val plain = RLP.encodeList { writer ->
writer.writeInt(VERSION)
writer.writeValue(signature.bytes())
writer.writeValue(enr) // TODO: Seq number if enrSeq from WHOAREYOU is equal to local, else nothing
}
val zeroNonce = Bytes.wrap(ByteArray(ZERO_NONCE_SIZE))
val authResponse = AES128GCM.encrypt(authRespKey, zeroNonce, plain, Bytes.EMPTY)
return AuthHeader(params.authTag, params.idNonce, ephemeralPubKey.bytes(), Bytes.wrap(authResponse))
}
companion object {
private const val SESSION_KEY_EXPIRATION: Long = 5
private const val ZERO_NONCE_SIZE: Int = 12
private const val VERSION: Int = 5
private val DISCOVERY_ID_NONCE: Bytes = Bytes.wrap("discovery-id-nonce".toByteArray())
}
}