Skip to content

Commit

Permalink
RSA config docs, tidy errors
Browse files Browse the repository at this point in the history
  • Loading branch information
dantb committed Jun 5, 2024
1 parent 74c607f commit a61164f
Show file tree
Hide file tree
Showing 5 changed files with 61 additions and 7 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ import ch.epfl.bluebrain.nexus.delta.sdk.syntax.httpResponseFieldsSyntax
import ch.epfl.bluebrain.nexus.delta.sourcing.model.ProjectRef
import ch.epfl.bluebrain.nexus.delta.sourcing.model.Tag.UserTag
import io.circe.syntax._
import io.circe.{Encoder, JsonObject}
import io.circe.{Encoder, Json, JsonObject}

/**
* Enumeration of File rejection types.
Expand Down Expand Up @@ -231,6 +231,10 @@ object FileRejection {
s"Linking or registering a file cannot be performed without a 'filename' or a 'path' that does not end with a filename."
)

final case object InvalidJWSPayload extends FileRejection("Signature missing, flattened JWS format expected")

final case class JWSSignatureExpired(payload: Json) extends FileRejection(s"Token expired for payload: $payload")

final case class CopyRejection(
sourceProj: ProjectRef,
destProject: ProjectRef,
Expand Down Expand Up @@ -281,6 +285,7 @@ object FileRejection {
case FetchRejection(_, _, FetchFileRejection.FileNotFound(_)) => (StatusCodes.InternalServerError, Seq.empty)
case SaveRejection(_, _, SaveFileRejection.ResourceAlreadyExists(_)) => (StatusCodes.Conflict, Seq.empty)
case SaveRejection(_, _, SaveFileRejection.BucketAccessDenied(_, _, _)) => (StatusCodes.Forbidden, Seq.empty)
case JWSSignatureExpired(_) => (StatusCodes.Forbidden, Seq.empty)
case CopyRejection(_, _, _, rejection) => (rejection.status, Seq.empty)
case FetchRejection(_, _, _) => (StatusCodes.InternalServerError, Seq.empty)
case SaveRejection(_, _, _) => (StatusCodes.InternalServerError, Seq.empty)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package ch.epfl.bluebrain.nexus.delta.plugins.storage.files.routes

import cats.effect.{Clock, IO}
import ch.epfl.bluebrain.nexus.delta.kernel.Logger
import ch.epfl.bluebrain.nexus.delta.plugins.storage.files.model.FileRejection.{InvalidJWSPayload, JWSSignatureExpired}
import com.nimbusds.jose.crypto.{RSASSASigner, RSASSAVerifier}
import com.nimbusds.jose.jwk.RSAKey
import com.nimbusds.jose.util.JSONObjectUtils
Expand Down Expand Up @@ -34,16 +35,14 @@ class TokenIssuer(key: RSAKey, tokenValidity: FiniteDuration)(implicit clock: Cl
def verifyJWSPayload(payload: Json): IO[Json] =
for {
jwsObject <- IO.delay(JWSObjectJSON.parse(payload.toString()))
sig <- IO.fromOption(jwsObject.getSignatures.asScala.headOption)(new Exception("Signature missing"))
sig <- IO.fromOption(jwsObject.getSignatures.asScala.headOption)(InvalidJWSPayload)
_ <- IO.delay(sig.verify(verifier))
_ <- log.info(s"Signature verified against payload")
objectPayload = jwsObject.getPayload.toString
_ <- log.info(s"Object payload is $objectPayload")
originalPayload <- IO.fromEither(parser.parse(objectPayload))
_ <- log.info(s"Original payload parsed: $originalPayload")
_ <- log.info(s"Original payload parsed for token: $originalPayload")
now <- clock.realTimeInstant
exp <- IO.delay(sig.getHeader.getCustomParam("exp").asInstanceOf[Long])
_ <- IO.raiseWhen(now.getEpochSecond > exp)(new Exception("Token has expired"))
_ <- IO.raiseWhen(now.getEpochSecond > exp)(JWSSignatureExpired(originalPayload))
} yield originalPayload

private def mkJWSHeader(expSeconds: Long): JWSHeader =
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package ch.epfl.bluebrain.nexus.delta.plugins.storage.files.routes

import akka.http.scaladsl.model.Uri
import cats.effect.IO
import ch.epfl.bluebrain.nexus.delta.plugins.storage.files.model.FileRejection.JWSSignatureExpired
import ch.epfl.bluebrain.nexus.delta.plugins.storage.files.routes.DelegateFilesRoutes.DelegationResponse
import ch.epfl.bluebrain.nexus.delta.rdf.syntax.iriStringContextSyntax
import ch.epfl.bluebrain.nexus.testkit.Generators
Expand All @@ -12,6 +13,7 @@ import io.circe.literal.JsonStringContext
import io.circe.syntax.EncoderOps
import munit.CatsEffectSuite

import java.util.Base64
import scala.concurrent.duration.DurationInt

class TokenIssuerSuite extends CatsEffectSuite with Generators {
Expand Down Expand Up @@ -47,6 +49,21 @@ class TokenIssuerSuite extends CatsEffectSuite with Generators {
_ <- tokenIssuer.verifyJWSPayload(jwsPayload)
} yield ()

program.interceptMessage[Exception]("Token has expired")
program.intercept[JWSSignatureExpired]
}

test("Parsing RSA private key and JWS verification succeed") {
val base64EncodedKey =
"LS0tLS1CRUdJTiBQUklWQVRFIEtFWS0tLS0tTUlJRXZ3SUJBREFOQmdrcWhraUc5dzBCQVFFRkFBU0NCS2t3Z2dTbEFnRUFBb0lCQVFEdXpkaVVDK1k3NEFmS1NETzBoTmlZYWNJZHd6NjdPZXZsaGw4UmlJcTdBNmtibkF2di9CQzE4RXF6VlRqOFhLaWdXMmZvOXoxVk5ZUVNIbnZvWnRxRnNIK000U0QzY3FnMUtrcFduaUxOVWxZUUo3akJLZXg4KzNuSmRqbHpYUmIrVVdIVGJyV1ZGc0tXL3AyYlVPekJxbWF4VzNvZFcxSUlSeFNkVFJUWmtaMStGNDlGRzY0ZmROSnhjNDdMUUwrODhLQS9iMUhIb25kWjdIbGpNOXpyb2ttRUgwaDhxRnlvdmRUdVNkbFRVQTRkOEVMb1FRMTY1N2VlcmJUakVKTEpVUXp5N2lqb2IwakhFMXMvQ2JiS0dUZ1UxL1MwMnY0WklmZExSbXVOMU9QR1QvRWdoM2N3djVZcEc5ZFF3cHg3cVYwSFZaZDJGK0FHRkxnS3ZsaTNBZ01CQUFFQ2dnRUFQRjdkdWMrb1RNcStMVzFEWlFlUW1qZGlVNVBnY0FTY2xsSDZCcnkyRmNFL0p6T3o4TitRZWU1ZGRDaS9WMDAxZEJTbm1FV293N25id1pqalNrVjJTUVh0dVBmUkZiMXV1TUlRT1FXUlZzYlI2eE9mcVhXbnk1RG5vUDY2VjJmWlFFSGlzVWp6cnRVcUxISUI5aG5uUUs2TGQ1cmdyRHRCNmNYT2VGWGNSNFFEZ0x1Wm9wL0NON3lDT3VTbmxHTGhsTklLK3B5QTdLaHZrV1pCdlBoZUdKSWJMdWJHK0x2NElTZW8ydjVINU03SkZtc0FXWDlSYzZNcXMyNThJTHhLMzROdWhGdDdoZWIxK2dGM1BwZTJPMmkrS1o1Z3hKdjJHRHg5VzkyeExNRjA5M21iR3hvMFBmRi93aE5zTGxLR0dkMzJuYWRDQWZNU21MVkFYVUlkQVFLQmdRRDRLZUpRdXdjN25PRzFFMlpyaC9kemw2R2lxck5KWm9IQkcraVF5VThLd1NwV1JFL1RrNUVqMEpXQ0J4Qk0vVk54L3FnZTRwWGVFcy92dSthWk5qMjVidUhoZkE2bXJDRkxCV2l0LzBBbGJsOWRjSlJUYitGRUVtb2ppWGFKWkx3M1RUeEZ4V0tFWGppOFlXcUI3RUNIVzAybnZLcnhBZGpBRDZXNGVseGxrUUtCZ1FEMldFMFFsVTZYK09RTTBBbml1SzN5NXhOcHExMExWQUVBZ1N1S0hLUisxa2txdkVIVWwvUzRsR3Zvamx4OFY1MDJ6Y0lxSWhyQzFHM1hISndaRjE3WWJkNFVlYzhSK1lRU3dXMlgzUW1sMUdNTjUzQlhMZ2pEK1ZQOGEzdmZodUNIQVJKa3MzSFlYZjdweTFvWE1GMHRpSit2NjZSWjYzUVZoVlRHcWU2Vnh3S0JnUUNyditrV3NHb3dFc0tQSEs4Y3FzeFNudFhLQzlQcmI5dExkL0k4Q21iKzdYTk1veGlRT0tnUm5uRnF2VkxGeGVsemtxaHVQNmt6T2RmWmRqVUJRbTN6b1U4SlRGK2pjS3ZXRFJkR25NcWJYVWo1RlVwQ2VNTHg1c0M0ZVpHbFF5ZVVLb3NWU3FlRkx1U2JVOXh2c0w5MExuZVBLRjh5VDNIZ2NyUGgraVZxVVFLQmdRQ0pwMHZnNFYyYWhDU0NtRmw5ekM2L1ZhbytXTmhVTlN1ZUtZKzN6RXVLNkpqWC9YeFhuRlhPTW5tZDZMYjdjRVhVVXVPVmdac3NsV0dQVzFoS21RbVJyTXIwN0IvdWJsd0QwdnczYVBjMEo5cjE4UWFRWUpQYlZsNDg1WjdCaCsrODRMZHpkK1k4dmtGc1NRcGRmTlFFVnB6TXc4TUIwQlQ4MVpWS3NiZzFEd0tCZ1FDVHJ0cCtJekVDQ0hSMHlBTEVsd1JFRGpSTG5RY2MvT21ucWxwNlU5Zy8zNEd5RmRLdUovcTlqWjBCdGVha3ZlYXVucUk2ckpic3puMDdVZktpa2FCK2kzRG1MelphZG9BYjJmL2pTVGhBZS9iUjYwNnd0bExVQTRJRWozb1poWFQrbmVTRWJvRksxSXZScUdtS1VyU1dNSXBKL2ZqV0tuUzBoQW9IYnFkbXBnPT0tLS0tLUVORCBQUklWQVRFIEtFWS0tLS0t"
val rawKey = new String(Base64.getDecoder.decode(base64EncodedKey))
val returnedPayload = genPayload()

for {
parsedPrivateKey <- IO.fromTry(TokenIssuer.parseRSAPrivateKey(rawKey))
rsaKey = TokenIssuer.generateRSAKeyFromPrivate(parsedPrivateKey)
tokenIssuer = new TokenIssuer(rsaKey, tokenValidity = 100.seconds)
jwsPayload <- tokenIssuer.issueJWSPayload(returnedPayload.asJson)
_ <- tokenIssuer.verifyJWSPayload(jwsPayload)
} yield ()
}
}
6 changes: 6 additions & 0 deletions docs/src/main/paradox/docs/delta/api/files-api.md
Original file line number Diff line number Diff line change
Expand Up @@ -437,6 +437,12 @@ Request
Response
: @@snip [listed.json](assets/files/listed.json)

## Delegation & Registration
To support files stored in the cloud, delta allows users to register files already uploaded to S3. This is useful primarily for large files where uploading directly through delta is inefficient and expensive.

There are two use cases: registering an already uploaded file by specifying its path, and asking Delta to generate a path with its standard format.


## Server Sent Events

From Delta 1.5, it is possible to fetch SSEs for all files or just files
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -182,6 +182,33 @@ This configuration tells Delta to log into the `internal` realm (which should ha
}
```

#### S3 storage configuration

Delta's S3 storage integration supports users uploading files to S3 independently and then registering them within Delta.

However, Delta is still responsible for the structure of the bucket so it issues a path to clients via the delegation validation endpoint (TODO link). This involves signing and later verifying a payload using JWS.

To support this functionality Delta must be configured with an RSA private key:
```hocon
amazon {
enabled = true
default-endpoint = "http://s3.localhost.localstack.cloud:4566"
default-access-key = "MY_ACCESS_KEY"
default-secret-key = "CHUTCHUT"
default-bucket = "mydefaultbucket"
prefix = "myprefix"
delegation {
private-key = "${rsa-private-key-new-lines-removed}"
token-duration = "3 days"
}
}
```

To generate such a key in the correct format follow these steps:
1. Generate RSA key: `openssl genrsa -out private_key.pem 2048`
2. Convert to PKCS#8 format: `openssl pkcs8 -topk8 -inform PEM -outform PEM -in private_key.pem -out private_key_pkcs8.pem -nocrypt`
3. Remove line breaks, copy secret: `cat private_key_pkcs8.pem | tr -d '\n' | pbcopy`

### Archive plugin configuration

The archive plugin configuration can be found @link:[here](https://github.com/BlueBrain/nexus/blob/$git.branch$/delta/plugins/archive/src/main/resources/archive.conf){ open=new }.
Expand Down

0 comments on commit a61164f

Please sign in to comment.