-
Notifications
You must be signed in to change notification settings - Fork 1
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat: Implement DNSSEC chain retrieval (#15)
Fixes #6. - [x] DnssecChainTest: Avoid actual Internet connections where possible. - [x] Uninstall JUnit/kotlin-test assertions - [x] Implement VeraDnssecChain.serialise()
- Loading branch information
Showing
25 changed files
with
988 additions
and
39 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,11 @@ | ||
# http://editorconfig.org | ||
root = true | ||
|
||
[*] | ||
max_line_length = 100 | ||
|
||
[*.kt] | ||
disabled_rules = import-ordering | ||
|
||
[*.md] | ||
max_line_length = off |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
11 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,10 +1,10 @@ | ||
import io.kotest.matchers.shouldBe | ||
import kotlinx.coroutines.runBlocking | ||
import org.junit.jupiter.api.Test | ||
import kotlin.test.assertEquals | ||
|
||
class PlaceholderTest { | ||
@Test | ||
fun placeholder() = runBlocking { | ||
assertEquals(2, 2) | ||
2 shouldBe 2 | ||
} | ||
} |
This file was deleted.
Oops, something went wrong.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,4 @@ | ||
package tech.relaycorp.vera | ||
|
||
public abstract class VeraException(message: String, cause: Throwable? = null) : | ||
Exception(message, cause) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,3 @@ | ||
package tech.relaycorp.vera.asn1 | ||
|
||
internal class ASN1Exception(message: String, cause: Throwable? = null) : Exception(message, cause) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,86 @@ | ||
package tech.relaycorp.vera.asn1 | ||
|
||
import java.io.IOException | ||
import org.bouncycastle.asn1.ASN1Encodable | ||
import org.bouncycastle.asn1.ASN1EncodableVector | ||
import org.bouncycastle.asn1.ASN1InputStream | ||
import org.bouncycastle.asn1.ASN1ObjectIdentifier | ||
import org.bouncycastle.asn1.ASN1OctetString | ||
import org.bouncycastle.asn1.ASN1Sequence | ||
import org.bouncycastle.asn1.ASN1TaggedObject | ||
import org.bouncycastle.asn1.ASN1VisibleString | ||
import org.bouncycastle.asn1.DEROctetString | ||
import org.bouncycastle.asn1.DERSequence | ||
import org.bouncycastle.asn1.DERTaggedObject | ||
|
||
internal object ASN1Utils { | ||
// private val BER_DATETIME_FORMATTER: DateTimeFormatter = | ||
// DateTimeFormatter.ofPattern("yyyyMMddHHmmss") | ||
|
||
fun makeSequence(items: List<ASN1Encodable>, explicitTagging: Boolean = true): DERSequence { | ||
val messagesVector = ASN1EncodableVector(items.size) | ||
val finalItems = if (explicitTagging) items else items.mapIndexed { index, item -> | ||
DERTaggedObject(false, index, item) | ||
} | ||
finalItems.forEach { messagesVector.add(it) } | ||
return DERSequence(messagesVector) | ||
} | ||
|
||
fun serializeSequence(items: List<ASN1Encodable>, explicitTagging: Boolean = true): ByteArray { | ||
return makeSequence(items, explicitTagging).encoded | ||
} | ||
|
||
@Throws(ASN1Exception::class) | ||
inline fun <reified T : ASN1Encodable> deserializeHomogeneousSequence( | ||
serialization: ByteArray | ||
): Array<T> { | ||
if (serialization.isEmpty()) { | ||
throw ASN1Exception("Value is empty") | ||
} | ||
val asn1InputStream = ASN1InputStream(serialization) | ||
val asn1Value = try { | ||
asn1InputStream.readObject() | ||
} catch (_: IOException) { | ||
throw ASN1Exception("Value is not DER-encoded") | ||
} | ||
val sequence = try { | ||
ASN1Sequence.getInstance(asn1Value) | ||
} catch (_: IllegalArgumentException) { | ||
throw ASN1Exception("Value is not an ASN.1 sequence") | ||
} | ||
return sequence.map { | ||
if (it !is T) { | ||
throw ASN1Exception( | ||
"Sequence contains an item of an unexpected type " + | ||
"(${it::class.java.simpleName})" | ||
) | ||
} | ||
@Suppress("USELESS_CAST") | ||
it as T | ||
}.toTypedArray() | ||
} | ||
|
||
@Throws(ASN1Exception::class) | ||
fun deserializeHeterogeneousSequence(serialization: ByteArray): Array<ASN1TaggedObject> = | ||
deserializeHomogeneousSequence(serialization) | ||
|
||
// fun derEncodeUTCDate(date: ZonedDateTime): DERGeneralizedTime { | ||
// val dateUTC = date.withZoneSameInstant(ZoneOffset.UTC) | ||
// return DERGeneralizedTime(dateUTC.format(BER_DATETIME_FORMATTER)) | ||
// } | ||
|
||
@Throws(ASN1Exception::class) | ||
fun getOID(oidSerialized: ASN1TaggedObject): ASN1ObjectIdentifier { | ||
return try { | ||
ASN1ObjectIdentifier.getInstance(oidSerialized, false) | ||
} catch (exc: IllegalArgumentException) { | ||
throw ASN1Exception("Value is not an OID", exc) | ||
} | ||
} | ||
|
||
fun getVisibleString(visibleString: ASN1TaggedObject): ASN1VisibleString = | ||
ASN1VisibleString.getInstance(visibleString, false) | ||
|
||
fun getOctetString(octetString: ASN1TaggedObject): ASN1OctetString = | ||
DEROctetString.getInstance(octetString, false) | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,5 @@ | ||
package tech.relaycorp.vera.dns | ||
|
||
import tech.relaycorp.vera.VeraException | ||
|
||
public class DnsException(message: String) : VeraException(message) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,6 @@ | ||
package tech.relaycorp.vera.dns | ||
|
||
internal object DnsUtils { | ||
const val DNSSEC_ROOT_DS = | ||
". IN DS 20326 8 2 E06D44B80B8F1D39A95C0B0D7C65D08458E880409BBC683457104237C7F8EC8D" | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,64 @@ | ||
package tech.relaycorp.vera.dns | ||
|
||
import java.io.ByteArrayInputStream | ||
import java.nio.charset.Charset | ||
import kotlinx.coroutines.future.await | ||
import org.xbill.DNS.DClass | ||
import org.xbill.DNS.Flags | ||
import org.xbill.DNS.Message | ||
import org.xbill.DNS.Name | ||
import org.xbill.DNS.Rcode | ||
import org.xbill.DNS.Record | ||
import org.xbill.DNS.Resolver | ||
import org.xbill.DNS.Type | ||
import org.xbill.DNS.dnssec.ValidatingResolver | ||
|
||
internal typealias PersistingResolverInitialiser = (resolverHostName: String) -> PersistingResolver | ||
internal typealias ValidatingResolverInitialiser = (headResolver: Resolver) -> ValidatingResolver | ||
|
||
internal typealias ChainRetriever = suspend ( | ||
domainName: String, | ||
recordType: String, | ||
resolverHostName: String | ||
) -> DnssecChain | ||
|
||
internal class DnssecChain internal constructor(val responses: List<ByteArray>) { | ||
companion object { | ||
private val DNSSEC_ROOT_DS = DnsUtils.DNSSEC_ROOT_DS.toByteArray(Charset.defaultCharset()) | ||
|
||
var persistingResolverInitialiser: PersistingResolverInitialiser = | ||
{ hostName -> PersistingResolver(hostName) } | ||
var validatingResolverInitialiser: ValidatingResolverInitialiser = | ||
{ resolver -> ValidatingResolver(resolver) } | ||
|
||
@JvmStatic | ||
@Throws(DnsException::class) | ||
suspend fun retrieve( | ||
domainName: String, | ||
recordType: String, | ||
resolverHostName: String | ||
): DnssecChain { | ||
val persistingResolver = persistingResolverInitialiser(resolverHostName) | ||
val validatingResolver = validatingResolverInitialiser(persistingResolver) | ||
validatingResolver.loadTrustAnchors(ByteArrayInputStream(DNSSEC_ROOT_DS)) | ||
|
||
val queryRecord = | ||
Record.newRecord(Name.fromString(domainName), Type.value(recordType), DClass.IN) | ||
val queryMessage = Message.newQuery(queryRecord) | ||
val response = validatingResolver.sendAsync(queryMessage).await() | ||
|
||
if (!response.header.getFlag(Flags.AD.toInt())) { | ||
throw DnsException( | ||
"DNSSEC verification failed: ${response.dnssecFailureDescription}" | ||
) | ||
} | ||
if (response.header.rcode != Rcode.NOERROR) { | ||
val rcodeName = Rcode.string(response.header.rcode) | ||
throw DnsException("DNS lookup failed ($rcodeName)") | ||
} | ||
|
||
val responses = persistingResolver.responses.map { it.toWire() } | ||
return DnssecChain(responses) | ||
} | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,19 @@ | ||
package tech.relaycorp.vera.dns | ||
|
||
import org.xbill.DNS.Message | ||
import org.xbill.DNS.Name | ||
import org.xbill.DNS.Section | ||
import org.xbill.DNS.TXTRecord | ||
import org.xbill.DNS.Type | ||
import org.xbill.DNS.dnssec.ValidatingResolver | ||
|
||
internal val Message.dnssecFailureDescription: String? | ||
get() { | ||
val rrsets = this.getSectionRRsets(Section.ADDITIONAL) | ||
val rrset = rrsets.firstOrNull { | ||
it.name == Name.root && | ||
it.type == Type.TXT && | ||
it.dClass == ValidatingResolver.VALIDATION_REASON_QCLASS | ||
} ?: return null | ||
return (rrset.first() as TXTRecord).strings.first() | ||
} |
22 changes: 22 additions & 0 deletions
22
src/main/kotlin/tech/relaycorp/vera/dns/PersistingResolver.kt
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,22 @@ | ||
package tech.relaycorp.vera.dns | ||
|
||
import org.xbill.DNS.Message | ||
import org.xbill.DNS.SimpleResolver | ||
import java.util.concurrent.CompletionStage | ||
import java.util.concurrent.Executor | ||
|
||
/** | ||
* DNSJava resolver that simply stores the responses it resolved. | ||
*/ | ||
internal class PersistingResolver(hostName: String) : SimpleResolver(hostName) { | ||
private val _responses = mutableListOf<Message>() | ||
val responses: List<Message> = _responses | ||
|
||
override fun sendAsync(query: Message, executor: Executor?): CompletionStage<Message> { | ||
val result = super.sendAsync(query, executor) | ||
return result.thenApply { response -> | ||
_responses.add(response) | ||
response | ||
} | ||
} | ||
} |
48 changes: 48 additions & 0 deletions
48
src/main/kotlin/tech/relaycorp/vera/dns/VeraDnssecChain.kt
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,48 @@ | ||
package tech.relaycorp.vera.dns | ||
|
||
import kotlin.jvm.Throws | ||
import org.bouncycastle.asn1.ASN1EncodableVector | ||
import org.bouncycastle.asn1.DEROctetString | ||
import org.bouncycastle.asn1.DERSet | ||
|
||
/** | ||
* Vera DNSSEC chain. | ||
* | ||
* It contains the DNSSEC chain for the Vera TXT RRSet (e.g., `_vera.example.com./TXT`). | ||
*/ | ||
public class VeraDnssecChain internal constructor(internal val responses: List<ByteArray>) { | ||
/** | ||
* Serialise the chain. | ||
*/ | ||
public fun serialise(): ByteArray { | ||
val responsesWrapped = responses.map { DEROctetString(it) } | ||
val vector = ASN1EncodableVector(responsesWrapped.size) | ||
vector.addAll(responsesWrapped.toTypedArray()) | ||
return DERSet(vector).encoded | ||
} | ||
|
||
public companion object { | ||
private const val VERA_RECORD_TYPE = "TXT" | ||
private const val CLOUDFLARE_RESOLVER = "1.1.1.1" | ||
|
||
internal var dnssecChainRetriever: ChainRetriever = DnssecChain.Companion::retrieve | ||
|
||
/** | ||
* Retrieve Vera DNSSEC chain for [organisationName]. | ||
* | ||
* @param organisationName The domain name of the organisation | ||
* @param resolverHost The IPv4 address for the DNSSEC-aware, recursive resolver | ||
* @throws DnsException if there was a DNS- or DNSSEC-related error | ||
*/ | ||
@JvmStatic | ||
@Throws(DnsException::class) | ||
public suspend fun retrieve( | ||
organisationName: String, | ||
resolverHost: String = CLOUDFLARE_RESOLVER | ||
): VeraDnssecChain { | ||
val domainName = "_vera.${organisationName.trimEnd('.')}." | ||
val dnssecChain = dnssecChainRetriever(domainName, VERA_RECORD_TYPE, resolverHost) | ||
return VeraDnssecChain(dnssecChain.responses) | ||
} | ||
} | ||
} |
This file was deleted.
Oops, something went wrong.
Oops, something went wrong.