Move SSD remote URL checks to typed Kotlin
ober
2edcccb057ceda43b5e4cca31f1ebabf44a53d70
--- a/templates/ssd-review.ss +++ b/templates/ssd-review.ss @@ -1598,8 +1598,11 @@ textUtf8Bytes textUtf8SizeBytes textUtf8BytesWithinLimit oldDestinationBytes urlEffectivePort remoteBearerTokenPresent remoteBearerTokenValid + remoteEndpointIsHttps safeRemoteUrlParts remotePinPresent remotePinOrNull remotePinSha256Length - remoteRelativePathSafe + remotePinBytesPresent remotePinBytesValid + remotePinAllowedForProtocol remoteConfigHasPin + remoteRelativePathSafe remoteOriginMatches shouldCompareMoreCompleteTruth shouldWriteRemoteTruth shouldReplaceLocalByTimes zipEntryIsDirectory zipEntryDeclaresExcessiveSize) @@ -1741,6 +1744,14 @@ (def (urlEffectivePort (url : URL)) : Int32 (let ((port (urlPort url))) (if (>= port (int32 0)) port (urlDefaultPort url)))) + (def (remoteEndpointIsHttps (protocol : String) (host : String)) : Bool + (and (equal? protocol "https") (not (string-blank? host)))) + (def (safeRemoteUrlParts (userInfo : (Nullable String)) + (query : (Nullable String)) + (fragment : (Nullable String))) : Bool + (and (nullable-null? userInfo) + (and (nullable-null? query) + (nullable-null? fragment)))) (def (remoteBearerTokenPresent (token : String)) : Bool (not (string-blank? token))) (def (remoteBearerTokenValid (token : String)) : Bool @@ -1760,10 +1771,30 @@ (nullable-none Bytes))) (def (remotePinSha256Length (pin : Bytes)) : Bool (= (bytesSize pin) (int32 32))) + (def (remotePinBytesPresent (pin : (Nullable Bytes))) : Bool + (not (nullable-null? pin))) + (def (remotePinBytesValid (pin : (Nullable Bytes))) : Bool + (if (remotePinBytesPresent pin) + (remotePinSha256Length (nullable-get pin)) + #t)) + (def (remotePinAllowedForProtocol (protocol : String) + (pin : (Nullable Bytes))) : Bool + (or (equal? protocol "https") (not (remotePinBytesPresent pin)))) + (def (remoteConfigHasPin (config : RemoteConfig)) : Bool + (remotePinBytesPresent (RemoteConfig-spkiSha256 config))) (def (remoteRelativePathSafe (relative : String)) : Bool (and (not (string-blank? relative)) (and (not (string-starts-with? relative "/")) (not (string-contains? relative ".."))))) + (def (remoteOriginMatches (candidateProtocol : String) + (candidateHost : String) + (candidatePort : Int32) + (baseProtocol : String) + (baseHost : String) + (basePort : Int32)) : Bool + (and (equal? candidateProtocol baseProtocol) + (and (equal? candidateHost baseHost) + (= candidatePort basePort)))) (def (shouldCompareMoreCompleteTruth (replaceIfMoreComplete : Bool) (destination : File)) : Bool (and replaceIfMoreComplete (fileExists destination))) @@ -8253,16 +8284,16 @@ " val pinText = preferences.getString(\"spki_sha256\", null)?.trim().orEmpty()" " val token = preferences.getString(\"bearer_token\", null)?.trim().orEmpty()" " val url = URL(endpoint.trimEnd('/'))" - " require(url.protocol == \"https\" && url.host.isNotBlank()) { \"HTTPS endpoint required\" }" - " require(url.userInfo == null && url.query == null && url.ref == null) { \"Unsafe endpoint URL\" }" + " require(remoteEndpointIsHttps(url.protocol, url.host)) { \"HTTPS endpoint required\" }" + " require(safeRemoteUrlParts(url.userInfo, url.query, url.ref)) { \"Unsafe endpoint URL\" }" " if (remoteBearerTokenPresent(token)) {" " require(remoteBearerTokenValid(token)) {" " \"Invalid remote authentication token\"" " }" " }" " val pin = remotePinOrNull(pinText, Base64.NO_WRAP)" - " if (pin != null) require(remotePinSha256Length(pin)) { \"SPKI SHA-256 pin must decode to 32 bytes\" }" - " require(url.protocol == \"https\" || pin == null) { \"SPKI pin requires HTTPS\" }" + " require(remotePinBytesValid(pin)) { \"SPKI SHA-256 pin must decode to 32 bytes\" }" + " require(remotePinAllowedForProtocol(url.protocol, pin)) { \"SPKI pin requires HTTPS\" }" " return RemoteConfig(url, pin, token)" " }" "" @@ -8272,7 +8303,7 @@ " val candidate = URL(config.apiUrl.toString().trimEnd('/') + \"/\" + relative)" " val basePort = urlEffectivePort(config.apiUrl)" " val candidatePort = urlEffectivePort(candidate)" - " require(candidate.protocol == config.apiUrl.protocol && candidate.host == config.apiUrl.host && candidatePort == basePort) {" + " require(remoteOriginMatches(candidate.protocol, candidate.host, candidatePort, config.apiUrl.protocol, config.apiUrl.host, basePort)) {" " \"Remote URL escaped configured HTTPS origin\"" " }" " return candidate" @@ -8287,7 +8318,7 @@ " ): HttpsURLConnection {" " val config = remoteConfig ?: throw IllegalStateException(\"Remote SSD service is not locally configured\")" " val connection = (remoteUrl(relative).openConnection() as HttpsURLConnection).apply {" - " if (config.spkiSha256 != null) {" + " if (remoteConfigHasPin(config)) {" " sslSocketFactory = pinnedSocketFactory(config)" " }" " requestMethod = method" @@ -8305,7 +8336,7 @@ " }" " }" " connection.connect()" - " if (connection is HttpsURLConnection && config.spkiSha256 != null) {" + " if (connection is HttpsURLConnection && remoteConfigHasPin(config)) {" " val certificate = connection.serverCertificates.firstOrNull()" " ?: throw SSLPeerUnverifiedException(\"Server provided no certificate\")" " val actualPin = MessageDigest.getInstance(\"SHA-256\").digest(certificate.publicKey.encoded)"