Skip to content

Commit

Permalink
Allow unsupported digest algorithms
Browse files Browse the repository at this point in the history
  • Loading branch information
SgtSilvio committed Jan 4, 2025
1 parent aa269d0 commit fbf831b
Show file tree
Hide file tree
Showing 4 changed files with 120 additions and 49 deletions.
124 changes: 95 additions & 29 deletions src/main/kotlin/io/github/sgtsilvio/oci/registry/OciDigest.kt
Original file line number Diff line number Diff line change
Expand Up @@ -6,64 +6,130 @@ import java.security.MessageDigest
/**
* @author Silvio Giebl
*/
internal class OciDigest(val algorithm: OciDigestAlgorithm, val hash: ByteArray) {
val encodedHash get() = algorithm.encodeHash(hash)

init {
algorithm.validateHash(hash)
}

override fun equals(other: Any?) = when {
this === other -> true
other !is OciDigest -> false
algorithm != other.algorithm -> false
!hash.contentEquals(other.hash) -> false
else -> true
}

override fun hashCode(): Int {
var result = algorithm.hashCode()
result = 31 * result + hash.contentHashCode()
return result
}

override fun toString() = "${algorithm.id}:$encodedHash"
}

internal interface OciDigestAlgorithm {
val id: String

fun encodeHash(hash: ByteArray): String

fun decodeHash(encodedHash: String): ByteArray

fun validateHash(hash: ByteArray): ByteArray

fun validateEncodedHash(encodedHash: String): String

fun createMessageDigest(): MessageDigest
}

// id: https://github.com/opencontainers/image-spec/blob/main/descriptor.md#registered-algorithms
// standardName: https://docs.oracle.com/en/java/javase/21/docs/specs/security/standard-names.html#messagedigest-algorithms
internal enum class OciDigestAlgorithm(val id: String, val standardName: String, private val hashByteLength: Int) {
// hashAlgorithmName: https://docs.oracle.com/en/java/javase/21/docs/specs/security/standard-names.html#messagedigest-algorithms
internal enum class StandardOciDigestAlgorithm(
override val id: String,
private val hashAlgorithmName: String,
private val hashByteLength: Int,
) : OciDigestAlgorithm {
SHA_256("sha256", "SHA-256", 32),
SHA_512("sha512", "SHA-512", 64);

internal fun decode(encodedHash: String): ByteArray = Hex.decodeHex(checkEncodedHash(encodedHash))
override fun encodeHash(hash: ByteArray): String = Hex.encodeHexString(validateHash(hash))

private fun checkEncodedHash(encodedHash: String): String {
if (encodedHash.length == (hashByteLength * 2)) return encodedHash
throw IllegalArgumentException("encoded hash '$encodedHash' has wrong length ${encodedHash.length}, $standardName requires ${hashByteLength * 2}")
}
override fun decodeHash(encodedHash: String): ByteArray = Hex.decodeHex(validateEncodedHash(encodedHash))

internal fun encode(hash: ByteArray): String = Hex.encodeHexString(checkHash(hash))
override fun validateHash(hash: ByteArray): ByteArray {
if (hash.size != hashByteLength) {
throw IllegalArgumentException("hash has wrong length ${hash.size}, $hashAlgorithmName requires $hashByteLength")
}
return hash
}

internal fun checkHash(hash: ByteArray): ByteArray {
if (hash.size == hashByteLength) return hash
throw IllegalArgumentException("hash has wrong length ${hash.size}, $standardName requires $hashByteLength")
override fun validateEncodedHash(encodedHash: String): String {
if (encodedHash.length != (hashByteLength * 2)) {
throw IllegalArgumentException("encoded hash '$encodedHash' has wrong length ${encodedHash.length}, $hashAlgorithmName requires ${hashByteLength * 2}")
}
// TODO check that all chars are [a-f0-9]
return encodedHash
}

internal fun createMessageDigest(): MessageDigest = MessageDigest.getInstance(standardName)
override fun createMessageDigest(): MessageDigest = MessageDigest.getInstance(hashAlgorithmName)

override fun toString() = id
}

internal data class OciDigest(val algorithm: OciDigestAlgorithm, val hash: ByteArray) {
val encodedHash get() = algorithm.encode(hash)
private val OCI_DIGEST_ALGORITHM_REGEX = Regex("[a-z0-9]+(?:[.+_-][a-z0-9]+)*")
private val OCI_DIGEST_ENCODED_HASH_REGEX = Regex("[a-zA-Z0-9=_-]+")

private class UnsupportedOciDigestAlgorithm(override val id: String) : OciDigestAlgorithm {

init {
algorithm.checkHash(hash)
require(OCI_DIGEST_ALGORITHM_REGEX.matches(id)) {
"digest algorithm '$id' does not match $OCI_DIGEST_ALGORITHM_REGEX"
}
}

override fun encodeHash(hash: ByteArray) = validateEncodedHash(hash.toString(Charsets.ISO_8859_1))

override fun decodeHash(encodedHash: String) = validateEncodedHash(encodedHash).toByteArray(Charsets.ISO_8859_1)

override fun validateHash(hash: ByteArray): ByteArray {
encodeHash(hash)
return hash
}

override fun validateEncodedHash(encodedHash: String): String {
require(OCI_DIGEST_ENCODED_HASH_REGEX.matches(encodedHash)) {
"digest encoded hash '$encodedHash' does not match $OCI_DIGEST_ENCODED_HASH_REGEX"
}
return encodedHash
}

override fun createMessageDigest() = throw UnsupportedOperationException("unsupported digest algorithm '$id'")

override fun equals(other: Any?) = when {
this === other -> true
other !is OciDigest -> false
algorithm != other.algorithm -> false
!hash.contentEquals(other.hash) -> false
other !is UnsupportedOciDigestAlgorithm -> false
id != other.id -> false
else -> true
}

override fun hashCode(): Int {
var result = algorithm.hashCode()
result = 31 * result + hash.contentHashCode()
return result
}
override fun hashCode(): Int = id.hashCode()

override fun toString() = algorithm.id + ":" + encodedHash
override fun toString() = id
}

internal fun String.toOciDigest(): OciDigest {
val colonIndex = indexOf(':')
if (colonIndex == -1) {
throw IllegalArgumentException("missing ':' in digest '$this'")
}
val algorithm = when (substring(0, colonIndex)) {
OciDigestAlgorithm.SHA_256.id -> OciDigestAlgorithm.SHA_256
OciDigestAlgorithm.SHA_512.id -> OciDigestAlgorithm.SHA_512
else -> throw IllegalArgumentException("unsupported algorithm in digest '$this'")
val algorithm = when (val algorithmId = substring(0, colonIndex)) {
StandardOciDigestAlgorithm.SHA_256.id -> StandardOciDigestAlgorithm.SHA_256
StandardOciDigestAlgorithm.SHA_512.id -> StandardOciDigestAlgorithm.SHA_512
else -> UnsupportedOciDigestAlgorithm(algorithmId)
}
return OciDigest(algorithm, algorithm.decode(substring(colonIndex + 1)))
return OciDigest(algorithm, algorithm.decodeHash(substring(colonIndex + 1)))
}

internal fun ByteArray.calculateOciDigest(algorithm: OciDigestAlgorithm) =
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -193,7 +193,11 @@ class OciRegistryHandler(
data: ByteArray,
response: HttpServerResponse,
): Mono<Void> {
val actualDigest = data.calculateOciDigest(digest?.algorithm ?: OciDigestAlgorithm.SHA_256)
val actualDigest = try {
data.calculateOciDigest(digest?.algorithm ?: StandardOciDigestAlgorithm.SHA_256)
} catch (e: UnsupportedOperationException) {
return response.sendBadRequest()
}
if ((digest != null) && (digest != actualDigest)) {
return response.sendBadRequest()
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,21 +10,21 @@ import org.junit.jupiter.api.assertThrows
class OciDigestTest {

@Test
fun toOciDigest() {
fun stringToOciDigest() {
val digest = "sha256:0123456789012345678901234567890123456789012345678901234567890123".toOciDigest()
assertEquals("sha256", digest.algorithm)
assertEquals("0123456789012345678901234567890123456789012345678901234567890123", digest.hash)
assertEquals(StandardOciDigestAlgorithm.SHA_256, digest.algorithm)
assertEquals("0123456789012345678901234567890123456789012345678901234567890123", digest.encodedHash)
}

@Test
fun toOciDigest_unknownAlgorithm() {
fun stringToOciDigest_unknownAlgorithm() {
val digest = "foo+bar.wab_47-11:abcdefghijklmnopqrstuvwxyz_ABCDEFGHIJKLMNOPQRSTUVWXYZ-0123456789=".toOciDigest()
assertEquals("foo+bar.wab_47-11", digest.algorithm)
assertEquals("abcdefghijklmnopqrstuvwxyz_ABCDEFGHIJKLMNOPQRSTUVWXYZ-0123456789=", digest.hash)
assertEquals("foo+bar.wab_47-11", digest.algorithm.id)
assertEquals("abcdefghijklmnopqrstuvwxyz_ABCDEFGHIJKLMNOPQRSTUVWXYZ-0123456789=", digest.encodedHash)
}

@Test
fun toOciDigest_withoutColon_throws() {
fun stringToOciDigest_withoutColon_throws() {
assertThrows<IllegalArgumentException> {
"sha256-0123456789012345678901234567890123456789012345678901234567890123".toOciDigest()
}
Expand Down
25 changes: 13 additions & 12 deletions src/test/kotlin/io/github/sgtsilvio/oci/registry/OciRegistryTest.kt
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ import kotlin.io.path.createDirectories
import kotlin.io.path.writeBytes
import kotlin.io.path.writeText
import kotlin.random.Random
import kotlin.random.nextUInt

/**
* @author Silvio Giebl
Expand Down Expand Up @@ -56,14 +57,14 @@ class OciRegistryTest {
}

private inner class ManifestByTagData {
val repository = "example/repository-" + Random.nextInt()
val tag = "tag-" + Random.nextInt()
val mediaType = "mediaType-" + Random.nextInt()
val repository = "example/repository-" + Random.nextUInt()
val tag = "tag-" + Random.nextUInt()
val mediaType = "mediaType-" + Random.nextUInt()
val manifest = """{"mediaType":"$mediaType"}"""

init {
val digestAlgorithm = "alg-" + Random.nextInt()
val digestHash = "hash-" + Random.nextInt()
val digestAlgorithm = "alg-" + Random.nextUInt()
val digestHash = "hash-" + Random.nextUInt()
storageDir.resolve("blobs/$digestAlgorithm/${digestHash.substring(0, 2)}/$digestHash")
.createDirectories()
.resolve("data")
Expand Down Expand Up @@ -110,10 +111,10 @@ class OciRegistryTest {
}

private inner class ManifestByDigestData {
val repository = "example/repository-" + Random.nextInt()
val digestAlgorithm = "alg-" + Random.nextInt()
val digestHash = "hash-" + Random.nextInt()
val mediaType = "mediaType-" + Random.nextInt()
val repository = "example/repository-" + Random.nextUInt()
val digestAlgorithm = "alg-" + Random.nextUInt()
val digestHash = "hash-" + Random.nextUInt()
val mediaType = "mediaType-" + Random.nextUInt()
val manifest = """{"mediaType":"$mediaType"}"""

init {
Expand Down Expand Up @@ -179,9 +180,9 @@ class OciRegistryTest {
}

private inner class BlobData {
val repository = "example/repository-" + Random.nextInt()
val digestAlgorithm = "alg-" + Random.nextInt()
val digestHash = "hash-" + Random.nextInt()
val repository = "example/repository-" + Random.nextUInt()
val digestAlgorithm = "alg-" + Random.nextUInt()
val digestHash = "hash-" + Random.nextUInt()
val blob = ByteArray(256) { it.toByte() }

init {
Expand Down

0 comments on commit fbf831b

Please sign in to comment.