mirror of
https://github.com/digital-asset/daml.git
synced 2024-11-10 10:46:11 +03:00
Add limited JCS implementation [DPP-1176] (#14626)
* Add limited JCS implementation changelog_begin changelog_end
This commit is contained in:
parent
e6f3ec034e
commit
e1f831a248
75
ledger/participant-integration-api/JCS.md
Normal file
75
ledger/participant-integration-api/JCS.md
Normal file
@ -0,0 +1,75 @@
|
||||
|
||||
# JCS
|
||||
|
||||
This document describes the JCS implementation used for ledger metering
|
||||
|
||||
## Background
|
||||
|
||||
As part of the ledger metering tamperproofing design a decision was made to use the JSON
|
||||
Canonicalization Scheme (JCS) to render a byte array that represented the contents of the
|
||||
metering report. The MAC of this byte array is then appended to the metering report JSON
|
||||
as a tamperproofing measure. The JCS spec is published
|
||||
as [RFC 8785](https://datatracker.ietf.org/doc/html/rfc8785).
|
||||
|
||||
## Java Implementation
|
||||
|
||||
The java reference implementation provided in the RFC is
|
||||
by [Samuel Erdtman](https://github.com/erdtman/java-json-canonicalization). Concerns
|
||||
about this implementation are that it:
|
||||
* Has not been released since 2020
|
||||
* Has only has one committer
|
||||
* Has vulnerability [CVE-2020-15250](https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2020-15250)
|
||||
* Does its own JSON parsing
|
||||
* Has relatively few lines of testing given the size of the code base
|
||||
|
||||
## Javascript Implementation
|
||||
|
||||
Erdtman also has a [javascript implementation of the algorithm in 32 lines of javascript](https://github.com/erdtman/canonicalize/blob/master/lib/canonicalize.js).
|
||||
|
||||
The reason this implementation is so small is because:
|
||||
* Javascript has native support for JSON
|
||||
* The JCS spec uses the [javascript standard for number formatting](https://262.ecma-international.org/10.0/#sec-tostring-applied-to-the-number-type)
|
||||
* The JCS uses JSON.stringify to serialize strings
|
||||
|
||||
## DA Implementation
|
||||
|
||||
Our starting point is similar to the situation in the javascript implementation in that:
|
||||
* We have a parsed JSON object (or standard libraries we use to do this)
|
||||
* We have JSON library functions that provide methods to stringify JSON objects.
|
||||
|
||||
For this reason the `Jcs.scala` class implements an algorithm similar to that in javascript.
|
||||
|
||||
## Number Limitation
|
||||
|
||||
When testing this implementation we discovered that the number formatting did not follow
|
||||
the javascript standard. This is in part because scala implementations usually support
|
||||
numeric values to be larger than a `IEEE-754` 64 bit `Double` (e.g. `BigDecimal`).
|
||||
|
||||
### Workaround
|
||||
|
||||
By adding a limitation that we only support whole numbers smaller than 2^52 in absolute size
|
||||
and formatting them without scientific notation (using `BigInt`) we avoid these problems.
|
||||
|
||||
This limitation is not restrictive for the ledger metering JSON whose only numeric field
|
||||
is the event count.
|
||||
|
||||
### Approximation
|
||||
|
||||
The following approximates that javascript spec double formatting rules but was discarded
|
||||
in favour of explicitly limiting the implementation.
|
||||
|
||||
```scala
|
||||
def serialize(bd: BigDecimal): String = {
|
||||
if (bd.isWhole && bd.toBigInt.toString().length < 22) {
|
||||
bd.toBigInt.toString()
|
||||
} else {
|
||||
val s = bd.toString()
|
||||
if (s.contains('E')) {
|
||||
s.replaceFirst("(\\.0|)E", "e")
|
||||
} else {
|
||||
s.replaceFirst("0+$", "")
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
@ -0,0 +1,52 @@
|
||||
// Copyright (c) 2022 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package com.daml.platform.apiserver.meteringreport
|
||||
|
||||
import spray.json.{CompactPrinter, JsArray, JsNumber, JsObject, JsString, JsValue}
|
||||
|
||||
/** A limited implementation of JCS https://datatracker.ietf.org/doc/html/rfc8785
|
||||
*
|
||||
* The limitations of this implementation are that:
|
||||
* - only whole numbers are supported
|
||||
* - only numbers with an absolute value less than pow(2,52) are supported
|
||||
*
|
||||
* The primary reason for the above is that this is suitable for our needs and avoids
|
||||
* getting into difficult areas regarding the JCS spec
|
||||
*
|
||||
* For details see JCS.md
|
||||
*/
|
||||
object Jcs {
|
||||
|
||||
import scalaz._
|
||||
import scalaz.syntax.traverse._
|
||||
import std.either._
|
||||
import std.vector._
|
||||
|
||||
val MaximumSupportedAbsSize: BigDecimal = BigDecimal(2).pow(52)
|
||||
type FailOr[T] = Either[String, T]
|
||||
|
||||
def serialize(json: JsValue): FailOr[String] = json match {
|
||||
case JsNumber(value) if !value.isWhole =>
|
||||
Left(s"Only whole numbers are supported, not $value")
|
||||
case JsNumber(value) if value.abs >= MaximumSupportedAbsSize =>
|
||||
Left(
|
||||
s"Only numbers with an abs size less than $MaximumSupportedAbsSize are supported, not $value"
|
||||
)
|
||||
case JsNumber(value) =>
|
||||
Right(value.toBigInt.toString())
|
||||
case JsArray(elements) =>
|
||||
elements.traverse(serialize).map(_.mkString("[", ",", "]"))
|
||||
case JsObject(fields) =>
|
||||
fields.toVector.sortBy(_._1).traverse(serializePair).map(_.mkString("{", ",", "}"))
|
||||
case leaf => Right(compact(leaf))
|
||||
}
|
||||
|
||||
private def compact(v: JsValue): String = CompactPrinter(v)
|
||||
|
||||
private def serializePair(kv: (String, JsValue)): FailOr[String] = {
|
||||
val (k, v) = kv
|
||||
serialize(v).map(vz => s"${compact(JsString(k))}:$vz")
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,100 @@
|
||||
// Copyright (c) 2022 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package com.daml.platform.apiserver.meteringreport
|
||||
|
||||
import com.daml.lf.data.Ref
|
||||
import com.daml.lf.data.Time.Timestamp
|
||||
import com.daml.platform.apiserver.meteringreport.Jcs.{MaximumSupportedAbsSize, serialize}
|
||||
import com.daml.platform.apiserver.meteringreport.MeteringReport.{
|
||||
ApplicationReport,
|
||||
ParticipantReport,
|
||||
Request,
|
||||
}
|
||||
import org.scalatest.Assertion
|
||||
import org.scalatest.matchers.should.Matchers
|
||||
import org.scalatest.wordspec.AnyWordSpec
|
||||
import spray.json.{JsArray, JsBoolean, JsNull, JsNumber, JsObject, JsString, JsValue, enrichAny}
|
||||
|
||||
class JcsSpec extends AnyWordSpec with Matchers {
|
||||
|
||||
private val uno: JsValue = JsNumber(1)
|
||||
private val badNumber: JsValue = JsNumber(1.2)
|
||||
|
||||
def assert(value: JsValue, expected: String): Assertion = {
|
||||
val actual = Jcs.serialize(value)
|
||||
actual shouldBe Right(expected)
|
||||
}
|
||||
|
||||
"Jcs" should {
|
||||
"serialize JsString" in {
|
||||
assert(JsString("a\tb\nc"), "\"a\\tb\\nc\"")
|
||||
}
|
||||
"serialize JsBoolean" in {
|
||||
assert(JsBoolean(true), "true")
|
||||
assert(JsBoolean(false), "false")
|
||||
}
|
||||
"serialize JsNull" in {
|
||||
assert(JsNull, "null")
|
||||
}
|
||||
"serialize JsNumber(0)" in {
|
||||
assert(JsNumber(0), "0")
|
||||
}
|
||||
"serialize JsNumber < 2^52" in {
|
||||
assert(JsNumber(MaximumSupportedAbsSize - 1), "4503599627370495")
|
||||
assert(JsNumber(-MaximumSupportedAbsSize + 1), "-4503599627370495")
|
||||
}
|
||||
"not serialize JsNumber >= 2^52" in {
|
||||
serialize(JsNumber(MaximumSupportedAbsSize)).isLeft shouldBe true
|
||||
serialize(JsNumber(-MaximumSupportedAbsSize)).isLeft shouldBe true
|
||||
}
|
||||
"not serialize Decimals" in {
|
||||
serialize(badNumber).isLeft shouldBe true
|
||||
}
|
||||
"serialize JsNumber without scientific notation" in {
|
||||
assert(JsNumber(1000000000000000d), "1000000000000000")
|
||||
assert(JsNumber(-1000000000000000d), "-1000000000000000")
|
||||
}
|
||||
"serialize JsArray" in {
|
||||
assert(JsArray(Vector(true, false).map(b => JsBoolean(b))), "[true,false]")
|
||||
}
|
||||
"not serialize a JsArray containing an invalid number" in {
|
||||
serialize(JsArray(Vector(badNumber))).isLeft shouldBe true
|
||||
}
|
||||
"serialize JsObject in key order" in {
|
||||
assert(JsObject(Map("b" -> uno, "c" -> uno, "a" -> uno)), "{\"a\":1,\"b\":1,\"c\":1}")
|
||||
}
|
||||
"not serialize JsObject containing invalid number" in {
|
||||
serialize(JsObject(Map("n" -> badNumber))).isLeft shouldBe true
|
||||
}
|
||||
"serialize report" in {
|
||||
val application = Ref.ApplicationId.assertFromString("a0")
|
||||
val from = Timestamp.assertFromString("2022-01-01T00:00:00Z")
|
||||
val to = Timestamp.assertFromString("2022-01-01T00:00:00Z")
|
||||
val report = ParticipantReport(
|
||||
participant = Ref.ParticipantId.assertFromString("p0"),
|
||||
request = Request(from, Some(to), Some(application)),
|
||||
`final` = false,
|
||||
applications = Seq(ApplicationReport(application, 272)),
|
||||
)
|
||||
val reportJson: JsValue = report.toJson
|
||||
assert(
|
||||
reportJson,
|
||||
expected = "{" +
|
||||
"\"applications\":[{" +
|
||||
"\"application\":\"a0\"," +
|
||||
"\"events\":272" +
|
||||
"}]," +
|
||||
"\"final\":false," +
|
||||
"\"participant\":\"p0\"," +
|
||||
"\"request\":{" +
|
||||
"\"application\":\"a0\"," +
|
||||
"\"from\":\"2022-01-01T00:00:00Z\"," +
|
||||
"\"to\":\"2022-01-01T00:00:00Z\"" +
|
||||
"}" +
|
||||
"}",
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
Loading…
Reference in New Issue
Block a user