Move SSD pin equality checks to typed Kotlin

ober

6ca04b50ef969a50bdd12848f23a23afc0432649

diff --git a/templates/ssd-review.ss b/templates/ssd-review.ss
index 50d7df0..764c718 100644
--- a/templates/ssd-review.ss
+++ b/templates/ssd-review.ss
@@ -1616,6 +1616,7 @@
                       (java net URL)
                       (java nio file Files)
                       (java nio file Path)
+                      (java security MessageDigest)
                       (java util zip ZipEntry))
       (typed-library (com sfb ssdreview)
         (export make-RemoteConfig RemoteConfig?
@@ -1644,6 +1645,7 @@
                 byteArraySizeBytes byteArrayBytesWithinLimit
                 textUtf8Bytes textUtf8SizeBytes textUtf8BytesWithinLimit
                 oldDestinationBytes urlEffectivePort
+                constantTimeBytesEqual constantTimeNullableBytesEqual
                 remoteBearerTokenPresent remoteBearerTokenValid
                 remoteEndpointIsHttps safeRemoteUrlParts
                 remotePinPresent remotePinOrNull remotePinSha256Length
@@ -1674,6 +1676,8 @@
           (kotlin-member-get size))
         (extern (base64Decode (text : String) (flags : Int32)) : Bytes
           (kotlin-call android util Base64 decode))
+        (extern (messageDigestIsEqual (actual : Bytes) (expected : Bytes)) : Bool
+          (kotlin-call java security MessageDigest isEqual))
         (extern (urlPort (url : URL)) : Int32
           (kotlin-member-get port))
         (extern (urlDefaultPort (url : URL)) : Int32
@@ -1805,6 +1809,13 @@
         (def (urlEffectivePort (url : URL)) : Int32
           (let ((port (urlPort url)))
             (if (>= port (int32 0)) port (urlDefaultPort url))))
+        (def (constantTimeBytesEqual (actual : Bytes) (expected : Bytes)) : Bool
+          (messageDigestIsEqual actual expected))
+        (def (constantTimeNullableBytesEqual (actual : Bytes)
+                                             (expected : (Nullable Bytes))) : Bool
+          (if (nullable-null? expected)
+              #f
+              (constantTimeBytesEqual actual (nullable-get expected))))
         (def (remoteEndpointIsHttps (protocol : String) (host : String)) : Bool
           (and (equal? protocol "https") (not (string-blank? host))))
         (def (safeRemoteUrlParts (userInfo : (Nullable String))
@@ -8446,7 +8457,7 @@
        "            val certificate = connection.serverCertificates.firstOrNull()"
        "                ?: throw SSLPeerUnverifiedException(\"Server provided no certificate\")"
        "            val actualPin = MessageDigest.getInstance(\"SHA-256\").digest(certificate.publicKey.encoded)"
-       "            if (!MessageDigest.isEqual(actualPin, config.spkiSha256)) {"
+       "            if (!constantTimeNullableBytesEqual(actualPin, config.spkiSha256)) {"
        "                connection.disconnect()"
        "                throw SSLPeerUnverifiedException(\"SSD service identity pin mismatch\")"
        "            }"
@@ -8470,7 +8481,7 @@
        "                platform.checkServerTrusted(chain, authType)"
        "                val leaf = chain.firstOrNull() ?: throw CertificateException(\"Server provided no certificate\")"
        "                val actual = MessageDigest.getInstance(\"SHA-256\").digest(leaf.publicKey.encoded)"
-       "                if (!MessageDigest.isEqual(actual, expectedPin)) {"
+       "                if (!constantTimeBytesEqual(actual, expectedPin)) {"
        "                    throw CertificateException(\"SSD service identity pin mismatch\")"
        "                }"
        "            }"
diff --git a/tests/ssd-security-test.sh b/tests/ssd-security-test.sh
index 55e5362..5dbebf9 100755
--- a/tests/ssd-security-test.sh
+++ b/tests/ssd-security-test.sh
@@ -12,9 +12,12 @@ cd "$repo"
 JERBOA=${JERBOA:-jerboa}
 $JERBOA jandroid.ss generate tests/fixtures/ssd-app.ss "$tmp/project" \
     >"$tmp/generate.log" 2>&1
-source_file="$tmp/project/app/src/main/java/com/sfb/ssdreview/TruthStore.kt"
+source_dir="$tmp/project/app/src/main/java/com/sfb/ssdreview"
+source_file="$source_dir/TruthStore.kt"
+all_sources="$tmp/all-kotlin-sources.txt"
 manifest="$tmp/project/app/src/main/AndroidManifest.xml"
 test -f "$source_file"
+find "$source_dir" -name '*.kt' -type f -exec cat {} + >"$all_sources"
 cmp supply-chain/gradle-verification-metadata.xml \
     "$tmp/project/gradle/verification-metadata.xml"
 if grep -Fq 'android:usesCleartextTraffic="true"' "$manifest"; then
@@ -29,6 +32,13 @@ require_text() {
     }
 }
 
+require_generated_text() {
+    grep -Fq "$1" "$all_sources" || {
+        printf '%s\n' "missing SSD generated security control: $1" >&2
+        exit 1
+    }
+}
+
 reject_text() {
     if grep -Fq "$1" "$source_file"; then
         printf '%s\n' "forbidden SSD API or transport: $1" >&2
@@ -38,7 +48,7 @@ reject_text() {
 
 require_text 'HttpsURLConnection'
 require_text 'X509TrustManager'
-require_text 'MessageDigest.isEqual'
+require_generated_text 'MessageDigest.isEqual'
 require_text 'setRequestProperty("Authorization", bearerAuthorizationHeader(config.bearerToken))'
 require_text 'context.getSharedPreferences("ssd_review_remote", Context.MODE_PRIVATE)'
 require_text 'Remote SSD service is not locally configured'