Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

sha1 token signing workaround #1054

Merged
merged 3 commits into from
Mar 22, 2024
Merged

sha1 token signing workaround #1054

merged 3 commits into from
Mar 22, 2024

Conversation

jonkeane
Copy link
Contributor

Seeing the use of digest for md5 hashes made me realize that we could do the same for the sha1 hashs (and PKI for signing).

I tested this publishing to connect using an existing token from RStudio and it worked just fine.

I also ran the tests in docker running rockylinux9 with FIPS mode enabled (using the docker file described in #928 and running R CMD build . && R CMD check .) There was one test that fails, though this looks like an encoding issue unless I'm missing something.

[59] "Running the tests in �tests/testthat.R� failed."
[60] "Last 13 lines of output:"
[61] " 'test-appMetadata-quarto.R:54:3', 'test-appMetadata-quarto.R:64:3',"
[62] " 'test-appMetadata-quarto.R:74:3', 'test-appMetadata-quarto.R:90:3',"
[63] " 'test-appMetadata-quarto.R:115:3', 'test-appMetadata-quarto.R:134:3',"
[64] " 'test-appMetadata.R:14:3', 'test-deploySite.R:2:3'"
[65] " "
[66] " �� Failed tests ����������������������������������������������������������������"
[67] " �� Failure ('test-bundlePackage.R:145:3'): package_record works ����������������"
[68] " windows1251Record$Author (`actual`) not equal to \"Се�гей ��ин\" (`expected`)."
[69] " "
[70]" `actual`: \"\\xd1\\xe5\\xf0\\xe3\\xe5\\xe9 \\xc1\\xf0\\xe8\\xed\""
[71] " `expected`: \"Се�гей ��ин\" "
[72] " "
[73] " [ FAIL 1 | WARN 3 | SKIP 145 | PASS 631 ]"

We should still move away from sha1 (possibly by moving to default to API keys, which already work and avoid this issue), but this PR buys us some time to do that work (both here and on the connect side).

R/http.R Outdated
rawsig <- openssl::signature_create(charToRaw(canonicalRequest), key = private_key)
# convert key into PKI format for signing, note this only accepts RSA, but
# that's what rsconnect generates already
pki_key <- PKI::PKI.load.key(strsplit(openssl::write_pem(private_key), "\n")[[1]], format = "PEM")
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm sure there's a better way to do this, but I wanted to get feedback on this quicker

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is equivalent:

pem <- openssl::write_pem(private_key)
pem_lines <- readLines(textConnection(pem))
pki_key <- PKI::PKI.load.key(pem_lines, format = "PEM")

Comment on lines 25 to 26
# openssl::base64_encode(openssl::rsa_keygen(2048L))
key <- readLines(test_path("fake-key"))
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

PKI doesn't support non-RSA keys. If I understand correctly this is totally fine because we only generate RSA keys.

Was the use of ed25519 just because it was shorter?

I also committed these changes separately to be able to pull that and see if tests past there.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm guessing so, yes (shorter).

I would also be fine if the fake-key were a constant in-code rather than a file living alongside the tests. You could store it in PEM format, which has the start/end markers and (generally) shorter lines.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks, I pulled the PEM format into the test. It's not the prettiest, but the sidecar file also isn't either.

Copy link
Contributor

@aronatkins aronatkins left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I feel like we should have a test proving that this implementation mirrors the old implementation. Right now, we're using the textual snapshot as a way of convincing ourselves, but it would be nice to have something that's enforced by code, as well.

It might mean refactoring signatureHeaders so we can pass the canonicalRequest into some signRequest helper, but that would let us test equivalence beyond the signature check. We could confirm that the "new" implementation returns the same signature as the old approach, which used openssl.

signRequestPrivateKey_old <- function(private_key, canonicalRequest) {
  rawsig <- openssl::signature_create(charToRaw(canonicalRequest), key = private_key)
  openssl::base64_encode(rawsig)
}

signRequestPrivateKey_new <- function(private_key, canonicalRequest) {
  pem <- openssl::write_pem(private_key)
  key <- readLines(textConnection(pem))
  pki_key <- PKI::PKI.load.key(key, format = "PEM")
  digested <- digest::digest(charToRaw(canonicalRequest), "sha1", serialize = FALSE, raw = TRUE)
  rawsig <- PKI::PKI.sign(key = pki_key, digest = digested)
  openssl::base64_encode(rawsig)
}

R/http.R Outdated
rawsig <- openssl::signature_create(charToRaw(canonicalRequest), key = private_key)
# convert key into PKI format for signing, note this only accepts RSA, but
# that's what rsconnect generates already
pki_key <- PKI::PKI.load.key(strsplit(openssl::write_pem(private_key), "\n")[[1]], format = "PEM")
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is equivalent:

pem <- openssl::write_pem(private_key)
pem_lines <- readLines(textConnection(pem))
pki_key <- PKI::PKI.load.key(pem_lines, format = "PEM")

Comment on lines 25 to 26
# openssl::base64_encode(openssl::rsa_keygen(2048L))
key <- readLines(test_path("fake-key"))
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm guessing so, yes (shorter).

I would also be fine if the fake-key were a constant in-code rather than a file living alongside the tests. You could store it in PEM format, which has the start/end markers and (generally) shorter lines.

@aronatkins
Copy link
Contributor

Forgot to mention: This needs NEWS.

@jonkeane
Copy link
Contributor Author

Thanks for the detailed review. I'm broadly onboard with the suggestions and will get to doing them shortly. But if this ends up blocking anything, (as always) I don't mind if you pushed to this branch to unblock it.

@jonkeane
Copy link
Contributor Author

This is ready for review again

Copy link
Contributor

@aronatkins aronatkins left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for that additional test.

@jonkeane
Copy link
Contributor Author

Thanks for that additional test.

Of course. Oh and I should have mentioned this FTR in the previous comment: I wanted to split that test out into it's own test_that block (so it's clear what it's testing, but didn't want to also have to move the key elsewhere or duplicate it, so tacked it on as the least-disruptive option.

@aronatkins
Copy link
Contributor

move the key elsewhere

No worries. You could create a helper function adjacent to the test_that block, if that helps separate things. That helper could return something that resembles a "token", so you have both the public and private keys.

generateTokenFixed <- function() {
    return list(
        token = "TDECAFBAD",
        public_key = "...",
        private_key = "...",
    )
}
test_that("thing one", {
    token <- generateTokenFixed()
    # check for stable signature (snapshot)
})
test_that("thing two", {
    token <- generateTokenFixed()
    # check for equivalent signature (openssl)
})

I don't think this adjustment is necessary, though. If we were to continue to expand the tests in this area, it's a natural next step.

@jonkeane jonkeane merged commit ae514c4 into main Mar 22, 2024
10 checks passed
@jonkeane jonkeane deleted the token_signing_workaround branch March 22, 2024 17:01
@venpopov
Copy link

venpopov commented May 19, 2024

Unfortunately (re)-introducing the PKI dependency in this PR makes it impossible to install rsconnect on MacOS when R is installed from homebrew rather than CRAN: #1073

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

3 participants