Skip to content

Commit d8dbfda

Browse files
sandersander-cb
authored andcommitted
feat: simpler FFI interface based on CBOR
1 parent 2270854 commit d8dbfda

13 files changed

Lines changed: 970 additions & 687 deletions

File tree

.github/workflows/ci.yaml

Lines changed: 31 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ on:
44
push:
55
pull_request:
66

7-
env:
7+
env:
88
CARGO_TERM_COLOR: always
99

1010
jobs:
@@ -19,3 +19,33 @@ jobs:
1919
- run: rustup update ${{ matrix.toolchain }} && rustup default ${{ matrix.toolchain }}
2020
- run: cargo build --verbose
2121
- run: cargo test --verbose
22+
23+
release:
24+
if: startsWith(github.ref, 'refs/tags/v')
25+
runs-on: ubuntu-latest
26+
needs: build_and_test
27+
steps:
28+
- uses: actions/checkout@v4
29+
- run: rustup update stable && rustup default stable
30+
- name: Extract version from tag
31+
id: extract
32+
run: echo "version=${GITHUB_REF##*/}" >> "$GITHUB_OUTPUT"
33+
- run: cargo build --release
34+
- name: Archive and rename binary
35+
run: |
36+
VERSION=${{ steps.extract.outputs.version }}
37+
PLATFORM=linux-x86_64
38+
mkdir -p dist
39+
cp target/release/scal3 dist/
40+
cd dist
41+
tar czf scal3-$VERSION-$PLATFORM.tar.gz scal3
42+
- run: cargo publish --locked
43+
env:
44+
CARGO_REGISTRY_TOKEN: ${{ secrets.CARGO_REGISTRY_TOKEN }}
45+
- name: Create GitHub Release with uploaded binary
46+
run: |
47+
gh release create "${{ steps.extract.outputs.version }}" dist/*.tar.gz \
48+
--title "Release ${{ steps.extract.outputs.version }}" \
49+
--verify-tag
50+
env:
51+
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}

.gitignore

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,8 @@
22
target
33
Cargo.lock
44

5-
.idea
5+
.idea/
6+
.vscode/
67

78
*.aux
89
*.bbl
@@ -14,3 +15,7 @@ Cargo.lock
1415
*.out
1516
*.pdf
1617
*.run.xml
18+
19+
.bsp/
20+
.metals/
21+
.scala-build/

Cargo.toml

Lines changed: 18 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -2,22 +2,32 @@
22
name = "scal3"
33
description = "Verify that systems operate under your sole control (prototype, patent pending)"
44
license = "CC-BY-NC-4.0"
5-
version = "0.2.0"
5+
version = "0.3.0"
66
edition = "2021"
77
repository = "https://github.com/cleverbase/scal3"
88
authors = ["Sander Dijkhuis <sander.dijkhuis@cleverbase.com>"]
99
readme = "src/README.md"
1010
categories = ["authentication", "cryptography"]
1111
exclude = ["/README.md", "/docs/**/*"]
1212

13+
[lib]
14+
crate-type = ["cdylib", "rlib"]
15+
1316
[dependencies]
14-
frost-core = "2.1.0"
15-
frost-p256 = "2.1.0"
16-
p256 = { version = "0.13.2", features = ["serde"] }
17-
rand_chacha = "0.9.0"
18-
sha2 = "0.10.8"
19-
signature = "2.2.0"
20-
hpke = "0.13.0"
17+
getrandom = { version = "0.2.15", default-features = false, features = ["custom"] }
18+
frost-core = { version = "2.1.0", default-features = false, features = ["serialization"] }
19+
frost-p256 = { version = "2.1.0", default-features = false }
20+
p256 = { version = "0.13.2", default-features = false, features = ["ecdsa", "serde"] }
21+
rand_chacha = { version = "0.9.0", default-features = false }
22+
sha2 = { version = "0.10.8", default-features = false }
23+
signature = { version = "2.2.0", default-features = false }
24+
hpke = { version = "0.13.0", default-features = false, features = ["p256"] }
25+
minicbor = { version = "0.26.5", default-features = false, features = ["alloc", "derive"] }
26+
minicbor-serde = { version = "0.4.1", features = ["alloc"] }
27+
minicbor-derive = "0.16.2"
28+
serde = { version = "1.0.219", features = ["derive"] }
29+
serde_bytes = "0.11.17"
30+
once_cell = "1.21.3"
2131

2232
[dev-dependencies]
2333
hmac = "0.12.1"

demo.scala

Lines changed: 246 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,246 @@
1+
//> using scala 3.7.0
2+
//> using dependency "net.java.dev.jna:jna:5.17.0"
3+
//> using dependency "io.bullet::borer-core:1.16.1"
4+
//> using dependency "io.bullet::borer-derivation:1.16.1"
5+
//> using dependency "org.bouncycastle:bcprov-jdk18on:1.81"
6+
7+
import com.sun.jna.ptr.{IntByReference, PointerByReference}
8+
import com.sun.jna.{Library, Native, Pointer}
9+
import io.bullet.borer.*
10+
import io.bullet.borer.derivation.MapBasedCodecs.*
11+
import io.bullet.borer.derivation.key
12+
import org.bouncycastle.jce.interfaces.{ECPrivateKey, ECPublicKey}
13+
import org.bouncycastle.jce.provider.BouncyCastleProvider
14+
import org.bouncycastle.jce.spec.ECPublicKeySpec
15+
16+
import java.security.{KeyFactory, KeyPairGenerator, MessageDigest, SecureRandom, Security, Signature}
17+
import javax.crypto.{KeyAgreement, KeyGenerator, Mac, SecretKey}
18+
import scala.util.Try
19+
20+
val libraryPath = "target/release/libscal3.so"
21+
22+
object authentication:
23+
private object impl:
24+
private trait AuthenticationLibrary extends Library:
25+
def scal3_process(ip: Array[Byte], il: Int, op: PointerByReference, ol: IntByReference): Unit
26+
def scal3_free(ptr: Pointer, len: Int): Unit
27+
28+
private val library: AuthenticationLibrary = Native.load(libraryPath, classOf[AuthenticationLibrary])
29+
30+
given AdtEncodingStrategy = AdtEncodingStrategy.flat(typeMemberName = "type")
31+
32+
sealed trait Request derives Codec
33+
object Request:
34+
@key("register") case class Register(mask: Array[Byte], randomness: Array[Byte], provider: Array[Byte])
35+
extends Request derives Codec
36+
@key("accept") case class Accept(
37+
provider: Array[Byte],
38+
verifierSecret: Array[Byte],
39+
verifier: Array[Byte],
40+
device: Array[Byte]
41+
) extends Request
42+
derives Codec
43+
@key("challenge") case class Challenge(randomness: Array[Byte]) extends Request derives Codec
44+
@key("authenticate") case class Authenticate(
45+
mask: Array[Byte],
46+
randomness: Array[Byte],
47+
provider: Array[Byte],
48+
subscriber: Array[Byte],
49+
verifier: Array[Byte],
50+
device: Array[Byte],
51+
challenge: Array[Byte],
52+
hash: Array[Byte]
53+
) extends Request
54+
derives Codec
55+
@key("pass") case class Pass(proof: Array[Byte], authentication: Long) extends Request derives Codec
56+
@key("prove") case class Prove(
57+
randomness: Array[Byte],
58+
provider: Array[Byte],
59+
verifierSecret: Array[Byte],
60+
verifier: Array[Byte],
61+
device: Array[Byte],
62+
hash: Array[Byte],
63+
passSecret: Array[Byte],
64+
pass: Array[Byte]
65+
) extends Request
66+
derives Codec
67+
@key("verify") case class Verify(
68+
verifier: Array[Byte],
69+
device: Array[Byte],
70+
hash: Array[Byte],
71+
authenticator: Array[Byte],
72+
proof: Array[Byte],
73+
client: Array[Byte]
74+
) extends Request
75+
derives Codec
76+
77+
def invoke[Response](request: Request)(using Decoder[Response]): Try[Response] =
78+
val input = Cbor.encode(request).toByteArray
79+
val outputPtr = PointerByReference()
80+
val outputLen = IntByReference()
81+
impl.library.scal3_process(input, input.length, outputPtr, outputLen)
82+
val output =
83+
try outputPtr.getValue.getByteArray(0, outputLen.getValue)
84+
finally impl.library.scal3_free(outputPtr.getValue, outputLen.getValue)
85+
Cbor.decode(output).to[Response].valueTry
86+
87+
object domain:
88+
import impl.*
89+
90+
case class SubscriberState(mask: Array[Byte], randomness: Array[Byte], provider: ECPublicKey)
91+
case class ProviderState(provider: ECPublicKey, secret: Array[Byte], verifier: Array[Byte], device: ECPublicKey)
92+
case class Registration(subscriber: Array[Byte], verifier: Array[Byte]) derives Codec
93+
case class PassResponse(sender: Array[Byte], pass: Array[Byte]) derives Codec
94+
case class Transcript(authenticator: Array[Byte], proof: Array[Byte], client: Array[Byte]) derives Codec
95+
object service:
96+
def register(state: SubscriberState): Registration = invoke[Registration](
97+
Request.Register(state.mask, state.randomness, state.provider.getQ.getEncoded(true))
98+
).get
99+
def accept(state: ProviderState): Boolean =
100+
invoke[String](
101+
Request.Accept(
102+
state.provider.getQ.getEncoded(true),
103+
state.secret,
104+
state.verifier,
105+
state.device.getQ.getEncoded(true)
106+
)
107+
).get == "accepted"
108+
def challenge(randomness: Array[Byte]): Array[Byte] = {
109+
case class Response(challenge: Array[Byte]) derives Codec
110+
invoke[Response](Request.Challenge(randomness)).get.challenge
111+
}
112+
def authenticate(
113+
state: SubscriberState,
114+
registration: Registration,
115+
device: ECPublicKey,
116+
challenge: Array[Byte],
117+
hash: Array[Byte]
118+
): (Array[Byte], Array[Byte] => PassResponse) = {
119+
case class AuthenticateResponse(digest: Array[Byte], authentication: Long) derives Codec
120+
val response = invoke[AuthenticateResponse](
121+
Request.Authenticate(
122+
state.mask,
123+
state.randomness,
124+
state.provider.getQ.getEncoded(true),
125+
registration.subscriber,
126+
registration.verifier,
127+
device.getQ.getEncoded(true),
128+
challenge,
129+
hash
130+
)
131+
).get
132+
def pass(proof: Array[Byte]): PassResponse =
133+
invoke[PassResponse](Request.Pass(proof, response.authentication)).get
134+
(response.digest, pass)
135+
}
136+
def prove(
137+
randomness: Array[Byte],
138+
state: ProviderState,
139+
hash: Array[Byte],
140+
passSecret: Array[Byte],
141+
pass: Array[Byte]
142+
): Transcript =
143+
invoke[Transcript](
144+
Request.Prove(
145+
randomness,
146+
state.provider.getQ.getEncoded(true),
147+
state.secret,
148+
state.verifier,
149+
state.device.getQ.getEncoded(true),
150+
hash,
151+
passSecret,
152+
pass
153+
)
154+
).get
155+
def verify(verifier: Array[Byte], device: ECPublicKey, hash: Array[Byte], transcript: Transcript): Boolean =
156+
invoke[String](
157+
Request.Verify(
158+
verifier,
159+
device.getQ.getEncoded(true),
160+
hash,
161+
transcript.authenticator,
162+
transcript.proof,
163+
transcript.client
164+
)
165+
).get == "verified"
166+
167+
object crypto:
168+
object domain:
169+
def generateRandomness(): Array[Byte] = {
170+
val randomness = Array.ofDim[Byte](32)
171+
SecureRandom().nextBytes(randomness)
172+
randomness
173+
}
174+
175+
case class HmacSha256Key(k: SecretKey):
176+
def hmac(msg: Array[Byte]): Array[Byte] =
177+
val mac = Mac.getInstance("HmacSHA256")
178+
mac.init(k)
179+
mac.doFinal(msg)
180+
object HmacSha256Key:
181+
def generate(): HmacSha256Key =
182+
val generator = KeyGenerator.getInstance("HmacSHA256")
183+
generator.init(256)
184+
HmacSha256Key(generator.generateKey())
185+
186+
case class P256Key(sk: ECPrivateKey, pk: ECPublicKey):
187+
def ecdh(pk: Array[Byte]): Array[Byte] =
188+
val params = sk.getParameters
189+
val curve = params.getCurve
190+
val point = curve.decodePoint(pk)
191+
val spec = ECPublicKeySpec(point, params)
192+
val factory = KeyFactory.getInstance("EC", "BC")
193+
val other = factory.generatePublic(spec)
194+
val agreement = KeyAgreement.getInstance("ECDH", "BC")
195+
agreement.init(sk)
196+
agreement.doPhase(other, true)
197+
agreement.generateSecret()
198+
def ecdsa(digest: Array[Byte]): Array[Byte] =
199+
val signature = Signature.getInstance("NONEwithECDSAinP1363format")
200+
signature.initSign(sk)
201+
signature.update(digest)
202+
signature.sign()
203+
204+
object P256Key:
205+
def generate(): P256Key =
206+
val generator = KeyPairGenerator.getInstance("EC", "BC")
207+
generator.initialize(256)
208+
val pair = generator.generateKeyPair()
209+
P256Key(pair.getPrivate.asInstanceOf[ECPrivateKey], pair.getPublic.asInstanceOf[ECPublicKey])
210+
211+
case class SecureArea(ec: P256Key, secret: HmacSha256Key)
212+
213+
def sha256(msg: Array[Byte]): Array[Byte] = MessageDigest.getInstance("SHA-256").digest(msg)
214+
215+
@main def main(): Unit = {
216+
import authentication.domain.*, crypto.domain.*
217+
218+
Security.addProvider(BouncyCastleProvider())
219+
220+
val provider = SecureArea(P256Key.generate(), HmacSha256Key.generate())
221+
val device = SecureArea(P256Key.generate(), HmacSha256Key.generate())
222+
223+
val subscriberState = SubscriberState(device.secret.hmac("123456".getBytes), generateRandomness(), provider.ec.pk)
224+
val registration = service.register(subscriberState)
225+
val providerState =
226+
ProviderState(provider.ec.pk, provider.ec.ecdh(registration.subscriber), registration.verifier, device.ec.pk)
227+
assert(service.accept(providerState))
228+
229+
val challengeData = """{"nonce":"123456","timestamp":"1748886705"}""".getBytes
230+
val challenge = service.challenge(provider.secret.hmac(challengeData))
231+
val clientData = """{"operation":"log-in","session":"68c9eeeddfa5fb50"}""".getBytes
232+
val (digest, pass) = service.authenticate(subscriberState, registration, device.ec.pk, challenge, sha256(clientData))
233+
val proof = device.ec.ecdsa(digest)
234+
val result = pass(proof)
235+
val transcript = service.prove(
236+
provider.secret.hmac(challengeData),
237+
providerState,
238+
sha256(clientData),
239+
provider.ec.ecdh(result.sender),
240+
result.pass
241+
)
242+
243+
assert(service.verify(registration.verifier, device.ec.pk, sha256(clientData), transcript))
244+
245+
println("Demo completed successfully")
246+
}

0 commit comments

Comments
 (0)