Skip to content

Commit

Permalink
feat: Write documentation (#35)
Browse files Browse the repository at this point in the history
- [x] Document every public member.
- [x] Complete API docs.

Fixes #13.
  • Loading branch information
gnarea authored Mar 18, 2023
1 parent 36cdc7a commit c2e153d
Show file tree
Hide file tree
Showing 21 changed files with 259 additions and 45 deletions.
8 changes: 7 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,16 @@

This is the JVM/Kotlin implementation of [VeraId](https://veraid.net), an offline authentication protocol powered by DNSSEC. This library implements all the building blocks that signature producers and consumers need.

The usage and API documentation is available on [docs.relaycorp.tech](https://docs.relaycorp.tech/veraid-jvm/).

## Contributions

We love contributions! If you haven't contributed to a Relaycorp project before, please take a minute to [read our guidelines](https://github.com/relaycorp/.github/blob/master/CONTRIBUTING.md) first.

## Developer notes

### Naming conventions

We stick to the general naming conventions at Relaycorp, with the following exceptions:

- We distinguish between _serialisation_ (i.e., processing `ByteArray`s), _encoding_ (i.e., processing BouncyCastle ASN.1 objects) and _parsing_ (i.e., processing `String`s).
- We distinguish between _serialisation_ (i.e., processing `ByteArray`s), _encoding_ (i.e., processing BouncyCastle ASN.1 objects) and _parsing_ (i.e., processing `String`s).
96 changes: 96 additions & 0 deletions api.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
# Module veraid

This is the JVM/Kotlin implementation of [VeraId](https://veraid.net), an offline authentication protocol powered by DNSSEC. This library implements all the building blocks that signature producers and consumers need.

The latest version can be found on [Maven Central](https://central.sonatype.com/search?q=veraid&namespace=tech.relaycorp), and the source code on [GitHub](https://github.com/relaycorp/veraid-jvm).

## Signature production

To produce a signature for a given plaintext, you need a _Member Id Bundle_ (produced by a VeraId organisation; e.g., via [VeraId Authority](https://github.com/relaycorp/veraid-authority)) and the Member's private key.

For example, if you wanted to produce signatures valid for up to 30 days for a service identified by the [OID](https://en.wikipedia.org/wiki/Object_identifier) `1.2.3.4.5`, you could implement the following function and call it in your code:

```kotlin
import java.security.PrivateKey
import java.time.ZonedDateTime
import kotlin.time.Duration.Companion.days
import kotlin.time.toJavaDuration
import tech.relaycorp.veraid.pki.MemberIdBundle
import tech.relaycorp.veraid.SignatureBundle

val TTL = 30.days
val SERVICE_OID = "1.2.3.4.5"

fun produceSignature(
plaintext: ByteArray,
memberIdBundleSerialised: ByteArray,
memberSigningKey: PrivateKey,
): ByteArray {
val memberIdBundle = MemberIdBundle.deserialise(memberIdBundleSerialised)
val expiryDate = ZonedDateTime.now().plus(TTL.toJavaDuration())
val signatureBundle = SignatureBundle.generate(
plaintext,
SERVICE_OID,
memberIdBundle,
memberSigningKey,
expiryDate,
)
return signatureBundle.serialise()
}
```

The Signature Bundle contains the Member Id Bundle and the actual signature, but it does not include the `plaintext`.

Note that for signatures to actually be valid for up to 30 days, the TTL override in the VeraId TXT record should allow 30 days or more.

## Signature verification

To verify a VeraId signature, you simply need the Signature Bundle and the plaintext to be verified. For extra security, this library also requires you to confirm the service where you intend to use the plaintext.

If VeraId's maximum TTL of 90 days or the TTL specified by the signature producer may be too large for your application, you may also want to restrict the validity period of signatures.

For example, if you only want to accept signatures valid for the past 30 days in a service identified by `1.2.3.4.5`, you could use the following function:

```kotlin
import java.security.PrivateKey
import java.time.ZonedDateTime
import kotlin.time.Duration.Companion.days
import kotlin.time.toJavaDuration
import tech.relaycorp.veraid.pki.MemberIdBundle
import tech.relaycorp.veraid.SignatureBundle
import tech.relaycorp.veraid.SignatureException

val TTL = 30.days
val SERVICE_OID = "1.2.3.4.5"

suspend fun verifySignature(
plaintext: ByteArray,
signatureBundleSerialised: ByteArray,
): String {
val signatureBundle = SignatureBundle.deserialise(signatureBundleSerialised)

val now = ZonedDateTime.now()
val verificationPeriod = now.minus(TTL.toJavaDuration())..now
val member = try {
signatureBundle.verify(plaintext, SERVICE_OID, verificationPeriod)
} catch (exc: SignatureException) {
throw Exception("Invalid signature bundle", exc)
}

return if (member.userName == null) member.orgName else "${member.userName}@${member.orgName}"
}
```

`verifySignature()` will return the id of the member that signed the plaintext, which looks like `[email protected]` if the member is a user or simply `example.com` if the member is a bot (acting on behalf of the organisation `example.com`).

# Package tech.relaycorp.veraid

Root package for the VeraId library.

# Package tech.relaycorp.veraid.dns

DNS- and DNSSEC-related functionality.

# Package tech.relaycorp.veraid.pki

VeraId's public key infrastructure (e.g., X.509 certificate processing).
1 change: 1 addition & 0 deletions build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -97,6 +97,7 @@ ktlint {
dokkaHtml.configure {
dokkaSourceSets {
configureEach {
includes.from(project.files(), "api.md")
reportUndocumented.set(true)
}
}
Expand Down
4 changes: 2 additions & 2 deletions src/integrationTest/kotlin/TestStubs.kt
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
import tech.relaycorp.veraid.pki.deserializeRSAKeyPair
import tech.relaycorp.veraid.pki.deserialiseRSAKeyPair
import tech.relaycorp.veraid.pki.generateRSAKeyPair

object TestStubs {
const val ORG_NAME = "lib-testing.veraid.net"

private val ORG_PRIVATE_KEY =
TestStubs::class.java.getResourceAsStream("/organisationPrivateKey.der")!!.readAllBytes()
val ORG_KEY_PAIR = ORG_PRIVATE_KEY.deserializeRSAKeyPair()
val ORG_KEY_PAIR = ORG_PRIVATE_KEY.deserialiseRSAKeyPair()

const val USER_NAME = "alice"
val MEMBER_KEY_PAIR = generateRSAKeyPair()
Expand Down
9 changes: 9 additions & 0 deletions src/main/kotlin/tech/relaycorp/veraid/Member.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
package tech.relaycorp.veraid

/**
* VeraId Member.
*
* @property orgName The organisation name.
* @property userName The user's name if the member is a user, or `null` if it's a bot.
*/
public data class Member(val orgName: String, val userName: String?)
4 changes: 4 additions & 0 deletions src/main/kotlin/tech/relaycorp/veraid/Periods.kt
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,10 @@ import java.time.ZoneOffset
import java.time.ZonedDateTime

internal typealias InstantPeriod = ClosedRange<Instant>

/**
* A period of time represented by a start and end date (inclusive).
*/
public typealias DatePeriod = ClosedRange<ZonedDateTime>

internal fun DatePeriod.toInstantPeriod(): InstantPeriod =
Expand Down
45 changes: 44 additions & 1 deletion src/main/kotlin/tech/relaycorp/veraid/SignatureBundle.kt
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@ import org.bouncycastle.asn1.DERSet
import org.bouncycastle.asn1.cms.Attribute
import tech.relaycorp.veraid.dns.DnssecChain
import tech.relaycorp.veraid.dns.InvalidChainException
import tech.relaycorp.veraid.pki.Member
import tech.relaycorp.veraid.pki.MemberCertificate
import tech.relaycorp.veraid.pki.MemberIdBundle
import tech.relaycorp.veraid.pki.OrgCertificate
Expand All @@ -19,10 +18,16 @@ import tech.relaycorp.veraid.utils.x509.CertificateException
import java.security.PrivateKey
import java.time.ZonedDateTime

/**
* VeraId Signature Bundle.
*/
public class SignatureBundle internal constructor(
internal val memberIdBundle: MemberIdBundle,
internal val signedData: SignedData,
) {
/**
* Serialise the bundle.
*/
public fun serialise(): ByteArray = ASN1Utils.serializeSequence(
listOf(
ASN1Integer(0),
Expand All @@ -33,12 +38,30 @@ public class SignatureBundle internal constructor(
false,
)

/**
* Verify the bundle.
*
* @param plaintext The plaintext whose signature is to be verified.
* @param serviceOid The OID of the service to which the signature is bound.
* @param date The date against which to verify the signature.
* @return The member that signed the signature, if verification succeeds.
* @throws SignatureException If the bundle is invalid.
*/
public suspend fun verify(
plaintext: ByteArray,
serviceOid: String,
date: ZonedDateTime,
): Member = verify(plaintext, serviceOid, date..date)

/**
* Verify the bundle.
*
* @param plaintext The plaintext whose signature is to be verified.
* @param serviceOid The OID of the service to which the signature is bound.
* @param datePeriod The period against which to verify the signature.
* @return The member that signed the signature, if verification succeeds.
* @throws SignatureException If the bundle is invalid.
*/
public suspend fun verify(
plaintext: ByteArray,
serviceOid: String,
Expand Down Expand Up @@ -90,6 +113,19 @@ public class SignatureBundle internal constructor(
}

public companion object {
/**
* Generate a new signature bundle.
*
* @param plaintext The plaintext to sign.
* @param serviceOid The OID of the service to which the signature is bound.
* @param memberIdBundle The member id bundle to use for signing.
* @param signingKey The private key for the member certificate in [memberIdBundle].
* @param expiryDate The date after which the signature will be considered invalid.
* @param startDate The date from which the signature will be considered valid.
* @return The bundle.
* @throws SignatureException If the bundle cannot be generated.
*/
@Throws(SignatureException::class)
public fun generate(
plaintext: ByteArray,
serviceOid: String,
Expand Down Expand Up @@ -117,6 +153,13 @@ public class SignatureBundle internal constructor(
return SignatureBundle(memberIdBundle, signedData)
}

/**
* Deserialise a bundle.
*
* @param serialisation The serialised bundle.
* @return The bundle, if it's valid.
* @throws SignatureException If the bundle is invalid.
*/
@Throws(SignatureException::class)
public fun deserialise(serialisation: ByteArray): SignatureBundle {
val sequence = try {
Expand Down
3 changes: 3 additions & 0 deletions src/main/kotlin/tech/relaycorp/veraid/SignatureException.kt
Original file line number Diff line number Diff line change
@@ -1,4 +1,7 @@
package tech.relaycorp.veraid

/**
* Exception representing an invalid/malformed [SignatureBundle].
*/
public class SignatureException(message: String, cause: Throwable? = null) :
VeraidException(message, cause)
3 changes: 3 additions & 0 deletions src/main/kotlin/tech/relaycorp/veraid/VeraidException.kt
Original file line number Diff line number Diff line change
@@ -1,4 +1,7 @@
package tech.relaycorp.veraid

/**
* Base class for all VeraId exceptions.
*/
public abstract class VeraidException(message: String, cause: Throwable? = null) :
Exception(message, cause)
3 changes: 3 additions & 0 deletions src/main/kotlin/tech/relaycorp/veraid/dns/DnsException.kt
Original file line number Diff line number Diff line change
Expand Up @@ -2,4 +2,7 @@ package tech.relaycorp.veraid.dns

import tech.relaycorp.veraid.VeraidException

/**
* Exception representing a DNS- or DNSSEC-related error.
*/
public class DnsException(message: String) : VeraidException(message)
Original file line number Diff line number Diff line change
Expand Up @@ -2,5 +2,8 @@ package tech.relaycorp.veraid.dns

import tech.relaycorp.veraid.VeraidException

/**
* Exception representing an invalid/malformed [DnssecChain].
*/
public class InvalidChainException(message: String, cause: Throwable? = null) :
VeraidException(message, cause)
Original file line number Diff line number Diff line change
Expand Up @@ -2,4 +2,7 @@ package tech.relaycorp.veraid.dns

import tech.relaycorp.veraid.VeraidException

/**
* Exception representing an invalid/malformed [RdataFieldSet].
*/
public class InvalidRdataException(message: String) : VeraidException(message)
22 changes: 13 additions & 9 deletions src/main/kotlin/tech/relaycorp/veraid/pki/Keys.kt
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,9 @@ import java.security.spec.RSAPublicKeySpec
import java.security.spec.X509EncodedKeySpec
import java.util.Base64

/**
* RSA modulus.
*/
public enum class RsaModulus(internal val modulus: Int) {
RSA_2048(2048),
RSA_3072(3072),
Expand Down Expand Up @@ -48,9 +51,7 @@ private val rsaModulusHashMap: Map<RsaModulus, Hash> = mapOf(
* Generate an RSA key pair.
*
* @param modulus The modulus
* @throws PkiException If `modulus` is less than 2048
*/
@Throws(PkiException::class)
public fun generateRSAKeyPair(modulus: RsaModulus = RsaModulus.RSA_2048): KeyPair {
val keyGen = KeyPairGenerator.getInstance("RSA", BC_PROVIDER)
keyGen.initialize(modulus.modulus)
Expand All @@ -74,18 +75,18 @@ internal val PublicKey.orgKeySpec: OrganisationKeySpec
}

/**
* Deserialize the RSA key pair from a private key serialization.
* Deserialise the RSA key pair from a private key serialization.
*/
@Throws(PkiException::class)
public fun ByteArray.deserializeRSAKeyPair(): KeyPair {
val privateKey = this.deserializePrivateKey("RSA") as RSAPrivateCrtKey
public fun ByteArray.deserialiseRSAKeyPair(): KeyPair {
val privateKey = this.deserialisePrivateKey("RSA") as RSAPrivateCrtKey
val keyFactory = KeyFactory.getInstance("RSA", BC_PROVIDER)
val publicKeySpec = RSAPublicKeySpec(privateKey.modulus, privateKey.publicExponent)
val publicKey = keyFactory.generatePublic(publicKeySpec)
return KeyPair(publicKey, privateKey)
}

private fun ByteArray.deserializePrivateKey(algorithm: String): PrivateKey {
private fun ByteArray.deserialisePrivateKey(algorithm: String): PrivateKey {
val privateKeySpec = PKCS8EncodedKeySpec(this)
val keyFactory = KeyFactory.getInstance(algorithm, BC_PROVIDER)
return try {
Expand All @@ -95,10 +96,13 @@ private fun ByteArray.deserializePrivateKey(algorithm: String): PrivateKey {
}
}

public fun ByteArray.deserializeRSAPublicKey(): RSAPublicKey =
deserializePublicKey("RSA") as RSAPublicKey
/**
* Deserialise the RSA public key from a public key serialisation.
*/
public fun ByteArray.deserialiseRSAPublicKey(): RSAPublicKey =
deserialisePublicKey("RSA") as RSAPublicKey

private fun ByteArray.deserializePublicKey(algorithm: String): PublicKey {
private fun ByteArray.deserialisePublicKey(algorithm: String): PublicKey {
val spec = X509EncodedKeySpec(this)
val factory = KeyFactory.getInstance(algorithm, BC_PROVIDER)
return try {
Expand Down
3 changes: 0 additions & 3 deletions src/main/kotlin/tech/relaycorp/veraid/pki/Member.kt

This file was deleted.

15 changes: 15 additions & 0 deletions src/main/kotlin/tech/relaycorp/veraid/pki/MemberCertificate.kt
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,11 @@ import java.security.PrivateKey
import java.security.PublicKey
import java.time.ZonedDateTime

/**
* VeraId Member Certificate.
*
* @property userName The user's name if the member is a user, or `null` if it's a bot.
*/
public class MemberCertificate internal constructor(certificateHolder: X509CertificateHolder) :
Certificate(certificateHolder) {
internal val userName: String? by lazy {
Expand All @@ -21,6 +26,16 @@ public class MemberCertificate internal constructor(certificateHolder: X509Certi
private val FORBIDDEN_USER_NAME_CHARS_REGEX = "[@\t\r\n]".toRegex()
private const val BOT_NAME = "@"

/**
* Issue a new member certificate.
*
* @param userName The user's name if the member is a user, or `null` if it's a bot.
* @param memberPublicKey The member's public key.
* @param orgCertificate The organisation's certificate.
* @param orgPrivateKey The organisation's private key.
* @param expiryDate The certificate's expiry date.
* @param startDate The certificate's start date.
*/
public fun issue(
userName: String?,
memberPublicKey: PublicKey,
Expand Down
Loading

0 comments on commit c2e153d

Please sign in to comment.