Skip to content

Commit

Permalink
MobileSecurityObjectGenerator: Don't use fractional seconds in timest…
Browse files Browse the repository at this point in the history
…amps. (#730)

ISO 18013-5 clause 9.1.2.4 specifically says to not do that and it's
desirable to be able to rely on this for some applications.

Just drop the fractional seconds and print a warning if the caller
passes in timestamps with non-zero fractional seconds.

Also add a unit test to double-check that an `Instant` serialized to a
CBOR tdate doesn't have any fractional seconds part if the `Instant`'s
`nanosecondsOfSecond` property is 0.

Test: New unit tests.
Test: All unit tests pass.

Signed-off-by: David Zeuthen <[email protected]>
  • Loading branch information
davidz25 authored Sep 6, 2024
1 parent 8f11595 commit abfe191
Show file tree
Hide file tree
Showing 5 changed files with 76 additions and 12 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -439,10 +439,13 @@ class IssuingAuthorityState(
val prefix = "issuingAuthority.$authorityId"
val type = settings.getString("$prefix.type") ?: TYPE_DRIVING_LICENSE

// Create AuthKeys and MSOs, make sure they're valid for a long time
val timeSigned = now
val validFrom = now
val validUntil = Instant.fromEpochMilliseconds(validFrom.toEpochMilliseconds() + 365*24*3600*1000L)
// Create AuthKeys and MSOs, make sure they're valid for 30 days. Also make
// sure to not use fractional seconds as 18013-5 calls for this (clauses 7.1
// and 9.1.2.4)
//
val timeSigned = Instant.fromEpochSeconds(now.epochSeconds, 0)
val validFrom = Instant.fromEpochSeconds(now.epochSeconds, 0)
val validUntil = validFrom + 30.days

// Generate an MSO and issuer-signed data for this authentication key.
val docType = if (type == TYPE_EU_PID) EUPID_DOCTYPE else MDL_DOCTYPE
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ import com.android.identity.cbor.CborBuilder
import com.android.identity.cbor.CborMap
import com.android.identity.cbor.toDataItemDateTimeString
import com.android.identity.crypto.EcPublicKey
import com.android.identity.util.Logger
import kotlinx.datetime.Instant

/**
Expand All @@ -37,6 +38,10 @@ class MobileSecurityObjectGenerator(
docType: String,
deviceKey: EcPublicKey
) {
companion object {
private const val TAG = "MobileSecurityObjectGenerator"
}

private val mDigestAlgorithm: String
private val mDocType: String
private val mDeviceKey: EcPublicKey
Expand Down Expand Up @@ -197,10 +202,24 @@ class MobileSecurityObjectGenerator(
require(validUntil > validFrom) {
"The validUntil timestamp should be later than the validFrom timestamp"
}
mSigned = signed
mValidFrom = validFrom
mValidUntil = validUntil
mExpectedUpdate = expectedUpdate

// 18013-5 clause 9.1.2.4 also says to not use fractional seconds so drop those
if (signed.nanosecondsOfSecond != 0 ) {
Logger.w(TAG, "Dropping non-zero fractional seconds for timestamp signed")
}
if (validFrom.nanosecondsOfSecond != 0 ) {
Logger.w(TAG, "Dropping non-zero fractional seconds for timestamp validFrom")
}
if (validUntil.nanosecondsOfSecond != 0 ) {
Logger.w(TAG, "Dropping non-zero fractional seconds for timestamp validUntil")
}
if (expectedUpdate?.nanosecondsOfSecond != 0 ) {
Logger.w(TAG, "Dropping non-zero fractional seconds for timestamp expectedUpdate")
}
mSigned = Instant.fromEpochSeconds(signed.epochSeconds, 0)
mValidFrom = Instant.fromEpochSeconds(validFrom.epochSeconds, 0)
mValidUntil = Instant.fromEpochSeconds(validUntil.epochSeconds, 0)
mExpectedUpdate = expectedUpdate?.let { Instant.fromEpochSeconds(it.epochSeconds, 0) }
}

private fun generateDeviceKeyBuilder(): CborBuilder {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,10 @@ import kotlin.test.assertFailsWith
import kotlin.test.assertNull

class MobileSecurityObjectGeneratorTest {
companion object {
private const val HALF_SEC_IN_NANOSECONDS = 1000000000/2
}

private fun getDigestAlg(digestAlgorithm: String): Algorithm {
return when (digestAlgorithm) {
"SHA-256" -> Algorithm.SHA256
Expand Down Expand Up @@ -353,4 +357,37 @@ class MobileSecurityObjectGeneratorTest {
.generate()
}
}

// Checks that fractional parts of timestamps are dropped by MobileSecurityObjectGenerator, as
// required by 18013-5 clause 9.1.2.4
@Test
fun testNoFractionalSeconds() {
val deviceKeyFromVector = EcPublicKeyDoubleCoordinate(
EcCurve.P256,
TestVectors.ISO_18013_5_ANNEX_D_STATIC_DEVICE_KEY_X.fromHex(),
TestVectors.ISO_18013_5_ANNEX_D_STATIC_DEVICE_KEY_Y.fromHex()
)
val signedTimestamp = Instant.fromEpochSeconds(1800, HALF_SEC_IN_NANOSECONDS)
val validFromTimestamp = Instant.fromEpochSeconds(3600, HALF_SEC_IN_NANOSECONDS)
val validUntilTimestamp = Instant.fromEpochSeconds(7200, HALF_SEC_IN_NANOSECONDS)
val expectedUpdateTimestamp = Instant.fromEpochSeconds(7100, HALF_SEC_IN_NANOSECONDS)

val signedTimestampWholeSeconds = Instant.fromEpochSeconds(1800, 0)
val validFromTimestampWholeSeconds = Instant.fromEpochSeconds(3600, 0)
val validUntilTimestampWholeSeconds = Instant.fromEpochSeconds(7200, 0)
val expectedUpdateTimestampWholeSeconds = Instant.fromEpochSeconds(7100, 0)

val encodedMSO = MobileSecurityObjectGenerator(
"SHA-256",
"org.iso.18013.5.1.mDL", deviceKeyFromVector
)
.setValidityInfo(signedTimestamp, validFromTimestamp, validUntilTimestamp, expectedUpdateTimestamp)
.addDigestIdsForNamespace("org.iso.18013.5.1", generateISODigest("SHA-256"))
.generate()
val mso = MobileSecurityObjectParser(encodedMSO).parse()
assertEquals(signedTimestampWholeSeconds, mso.signed)
assertEquals(validUntilTimestampWholeSeconds, mso.validUntil)
assertEquals(validFromTimestampWholeSeconds, mso.validFrom)
assertEquals(expectedUpdateTimestampWholeSeconds, mso.expectedUpdate)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -124,10 +124,10 @@ class DeviceResponseGeneratorTest {

// Create an authentication key... make sure the authKey used supports both
// mdoc ECDSA and MAC authentication.
val nowMillis = Clock.System.now().toEpochMilliseconds()
timeSigned = Instant.fromEpochMilliseconds(nowMillis)
timeValidityBegin = Instant.fromEpochMilliseconds(nowMillis + 3600 * 1000)
timeValidityEnd = Instant.fromEpochMilliseconds(nowMillis + 10 * 86400 * 1000)
val nowSeconds = Clock.System.now().epochSeconds
timeSigned = Instant.fromEpochSeconds(nowSeconds)
timeValidityBegin = Instant.fromEpochSeconds(nowSeconds + 3600)
timeValidityEnd = Instant.fromEpochSeconds(nowSeconds + 10*86400)
mdocCredential = MdocCredential(
document,
null,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -960,5 +960,10 @@ class CborTests {
assertEquals(
"0(\"2001-09-09T01:46:40Z\")",
Cbor.toDiagnostics(Instant.fromEpochMilliseconds(1000000000000).toDataItemDateTimeString()))

// Check that fractions of a second is printed (only) if the fraction is non-zero.
assertEquals(
"0(\"1970-01-01T00:16:40.500Z\")",
Cbor.toDiagnostics(Instant.fromEpochSeconds(1000, 500000000).toDataItemDateTimeString()))
}
}

0 comments on commit abfe191

Please sign in to comment.