Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/workflows/proofs/tests.txt
Original file line number Diff line number Diff line change
@@ -1 +1 @@
37ec63df5a1906ffa4520b1c2993a228a8a9555e0415b81198933281b74b6a79 7a39b8df88ef160e852c6aac30ee2a8bfdb8074b060d5afc8c72bf5f03d1f0a7 pass
2ceb9a3ceb468c15f0e2932d990f3d214915e5392b621203839c9458dc63eeea 7a39b8df88ef160e852c6aac30ee2a8bfdb8074b060d5afc8c72bf5f03d1f0a7 pass
2 changes: 1 addition & 1 deletion .github/workflows/proofs/transcripts.txt
Original file line number Diff line number Diff line change
@@ -1 +1 @@
11b365a07b04255c18a3acabab1bc76533a6daadff43fe6711c7fc4966a1252a 72c0b9bfd651515ecb3d3f4981a6bdb8c6d52eba2308cd11fea004d31941aeef pass
574eec4433bc7fe0d24e82515d14cd74edc8045c92fc3e52353ac1ae33cc7f51 72c0b9bfd651515ecb3d3f4981a6bdb8c6d52eba2308cd11fea004d31941aeef pass
6 changes: 6 additions & 0 deletions parser-typechecker/src/Unison/Builtin.hs
Original file line number Diff line number Diff line change
Expand Up @@ -1023,6 +1023,12 @@ cryptoBuiltins =
bytes --> bytes --> eithert failure bytes,
B "crypto.Rsa.verify.impl" $
bytes --> bytes --> bytes --> eithert failure boolean,
B "crypto.P256.publicKey.impl" $
bytes --> eithert failure bytes,
B "crypto.P256.signSha256.impl" $
bytes --> bytes --> eithert failure bytes,
B "crypto.P256.verifySha256.impl" $
bytes --> bytes --> bytes --> eithert failure boolean,
-- Argon2id password hashing (raw bytes API)
B "crypto.argon2.hashRaw" $
nat --> nat --> nat --> nat --> bytes --> bytes --> eithert failure bytes,
Expand Down
3 changes: 3 additions & 0 deletions unison-runtime/src/Unison/Runtime/Builtin.hs
Original file line number Diff line number Diff line change
Expand Up @@ -1267,6 +1267,9 @@ declareForeigns = do

declareForeign Untracked 6 Crypto_Argon2_HashRaw
declareForeign Untracked 6 Crypto_Argon2_VerifyRaw
declareForeign Untracked 1 Crypto_P256_publicKey_impl
declareForeign Untracked 2 Crypto_P256_signSha256_impl
declareForeign Untracked 3 Crypto_P256_verifySha256_impl

declareForeignWrap Untracked murmur'hash Universal_murmurHash
declareForeignWrap Untracked murmur'hash Universal_murmurHashUntyped
Expand Down
116 changes: 116 additions & 0 deletions unison-runtime/src/Unison/Runtime/Crypto/P256.hs
Original file line number Diff line number Diff line change
@@ -0,0 +1,116 @@
module Unison.Runtime.Crypto.P256
( derivePublicKey,
signSha256,
verifySha256,
)
where

import Crypto.Hash qualified as Hash
import Crypto.Number.Serialize (i2ospOf_, os2ip)
import Crypto.PubKey.ECC.ECDSA qualified as ECDSA
import Crypto.PubKey.ECC.Prim qualified as ECC
import Crypto.PubKey.ECC.Types qualified as ECC
import Data.ByteString qualified as BS
import Data.Word (Word8)
import Unison.Util.Text (Text)

curve :: ECC.Curve
curve = ECC.getCurveByName ECC.SEC_p256r1

curveOrder :: Integer
curveOrder = ECC.ecc_n (ECC.common_curve curve)

coordinateBytes :: Int
coordinateBytes = 32

uncompressedPrefix :: Word8
uncompressedPrefix = 0x04

derivePublicKey :: BS.ByteString -> Either Text BS.ByteString
derivePublicKey privateKeyBytes = do
privateKey <- parsePrivateKey privateKeyBytes
pure (encodePublicKey (toPublicKey privateKey))

signSha256 :: BS.ByteString -> BS.ByteString -> Either Text BS.ByteString
signSha256 privateKeyBytes message = do
privateKey <- parsePrivateKey privateKeyBytes
let digest = Hash.hash message :: Hash.Digest Hash.SHA256
signature =
ECDSA.deterministicNonce Hash.SHA256 privateKey digest $
\nonce -> ECDSA.signDigestWith nonce privateKey digest
pure (encodeSignature signature)

verifySha256 :: BS.ByteString -> BS.ByteString -> BS.ByteString -> Either Text Bool
verifySha256 publicKeyBytes message signatureBytes = do
publicKey <- parsePublicKey publicKeyBytes
signature <- parseSignature signatureBytes
pure (ECDSA.verify Hash.SHA256 publicKey signature message)

parsePrivateKey :: BS.ByteString -> Either Text ECDSA.PrivateKey
parsePrivateKey bytes
| BS.length bytes /= coordinateBytes =
Left "p256: private key must be 32 bytes"
| d <= 0 =
Left "p256: private key scalar must be non-zero"
| d >= curveOrder =
Left "p256: private key scalar is out of range"
| otherwise =
Right (ECDSA.PrivateKey curve d)
where
d = os2ip bytes

parsePublicKey :: BS.ByteString -> Either Text ECDSA.PublicKey
parsePublicKey bytes
| BS.length bytes /= 1 + (2 * coordinateBytes) =
Left "p256: public key must be 65 bytes in uncompressed SEC1 form"
| BS.head bytes /= uncompressedPrefix =
Left "p256: public key must start with 0x04 (uncompressed SEC1 form)"
| point == ECC.PointO =
Left "p256: public key point at infinity is invalid"
| not (ECC.isPointValid curve point) =
Left "p256: public key point is not on the curve"
| ECC.pointMul curve curveOrder point /= ECC.PointO =
Left "p256: public key point is not in the prime-order subgroup"
| otherwise =
Right (ECDSA.PublicKey curve point)
where
(xBytes, yBytes) = BS.splitAt coordinateBytes (BS.tail bytes)
point = ECC.Point (os2ip xBytes) (os2ip yBytes)

parseSignature :: BS.ByteString -> Either Text ECDSA.Signature
parseSignature bytes
| BS.length bytes /= 2 * coordinateBytes =
Left "p256: signature must be 64 bytes"
| r <= 0 || r >= curveOrder =
Left "p256: signature r component is out of range"
| s <= 0 || s >= curveOrder =
Left "p256: signature s component is out of range"
| otherwise =
Right (ECDSA.Signature r s)
where
(rBytes, sBytes) = BS.splitAt coordinateBytes bytes
r = os2ip rBytes
s = os2ip sBytes

toPublicKey :: ECDSA.PrivateKey -> ECDSA.PublicKey
toPublicKey privateKey =
ECDSA.PublicKey curve (ECC.pointBaseMul curve (ECDSA.private_d privateKey))

encodePublicKey :: ECDSA.PublicKey -> BS.ByteString
encodePublicKey publicKey =
case ECDSA.public_q publicKey of
ECC.Point x y ->
BS.concat
[ BS.singleton uncompressedPrefix,
i2ospOf_ coordinateBytes x,
i2ospOf_ coordinateBytes y
]
ECC.PointO ->
error "p256: attempted to encode point at infinity"

encodeSignature :: ECDSA.Signature -> BS.ByteString
encodeSignature signature =
BS.concat
[ i2ospOf_ coordinateBytes (ECDSA.sign_r signature),
i2ospOf_ coordinateBytes (ECDSA.sign_s signature)
]
39 changes: 39 additions & 0 deletions unison-runtime/src/Unison/Runtime/Foreign/Function.hs
Original file line number Diff line number Diff line change
Expand Up @@ -184,6 +184,7 @@ import Unison.Runtime.ANF.Rehash (checkGroupHashes)
import Unison.Runtime.ANF.Serialize qualified as ANF
import Unison.Runtime.Array qualified as PA
import Unison.Runtime.Builtin
import Unison.Runtime.Crypto.P256 qualified as P256
import Unison.Runtime.Crypto.Rsa qualified as Rsa
import Unison.Runtime.Exception (die, exn)
import Unison.Runtime.FFI.DLL
Expand Down Expand Up @@ -650,6 +651,15 @@ foreignCallHelper = \case
Crypto_Ed25519_verify_impl ->
mkForeign $
pure . verifyEd25519Wrapper
Crypto_P256_publicKey_impl ->
mkForeign $
pure . deriveP256PublicKeyWrapper
Crypto_P256_signSha256_impl ->
mkForeign $
pure . signP256Sha256Wrapper
Crypto_P256_verifySha256_impl ->
mkForeign $
pure . verifyP256Sha256Wrapper
Crypto_Rsa_sign_impl ->
mkForeign $
pure . signRsaWrapper
Expand Down Expand Up @@ -1546,6 +1556,30 @@ verifyEd25519Wrapper (public0, msg0, sig0) = case validated of
"ed25519: Secret key structure invalid"
errMsg _ = "ed25519: unexpected error"

deriveP256PublicKeyWrapper ::
Bytes.Bytes -> Either Failure Bytes.Bytes
deriveP256PublicKeyWrapper private0 =
bimapFailure "p256" $
Bytes.fromByteString <$> P256.derivePublicKey (Bytes.toArray private0 :: ByteString)

signP256Sha256Wrapper ::
(Bytes.Bytes, Bytes.Bytes) -> Either Failure Bytes.Bytes
signP256Sha256Wrapper (private0, msg0) =
bimapFailure "p256" $
Bytes.fromByteString
<$> P256.signSha256
(Bytes.toArray private0 :: ByteString)
(Bytes.toArray msg0 :: ByteString)

verifyP256Sha256Wrapper ::
(Bytes.Bytes, Bytes.Bytes, Bytes.Bytes) -> Either Failure Bool
verifyP256Sha256Wrapper (public0, msg0, sig0) =
bimapFailure "p256" $
P256.verifySha256
(Bytes.toArray public0 :: ByteString)
(Bytes.toArray msg0 :: ByteString)
(Bytes.toArray sig0 :: ByteString)

signRsaWrapper ::
(Bytes.Bytes, Bytes.Bytes) -> Either Failure Bytes.Bytes
signRsaWrapper (secret0, msg0) = case validated of
Expand All @@ -1571,6 +1605,11 @@ verifyRsaWrapper (public0, msg0, sig0) = case validated of
sig = Bytes.toArray sig0 :: ByteString
validated = Rsa.parseRsaPublicKey (Bytes.toArray public0 :: ByteString)

bimapFailure :: Text -> Either Text a -> Either Failure a
bimapFailure _ = \case
Left err -> Left (F.Failure Ty.cryptoFailureRef err unitValue)
Right value -> Right value

-- | Hash a password with Argon2id using the provided options and salt.
-- Takes: (memory KiB, iterations, parallelism, outputLen, password, salt)
-- Returns: Raw hash bytes or failure
Expand Down
6 changes: 6 additions & 0 deletions unison-runtime/src/Unison/Runtime/Foreign/Function/Type.hs
Original file line number Diff line number Diff line change
Expand Up @@ -156,6 +156,9 @@ data ForeignFunc
| Crypto_hmac
| Crypto_Argon2_HashRaw
| Crypto_Argon2_VerifyRaw
| Crypto_P256_publicKey_impl
| Crypto_P256_signSha256_impl
| Crypto_P256_verifySha256_impl
| Crypto_Ed25519_sign_impl
| Crypto_Ed25519_verify_impl
| Crypto_Rsa_sign_impl
Expand Down Expand Up @@ -618,6 +621,9 @@ foreignFuncBuiltinName = \case
Crypto_hmac -> "crypto.hmac"
Crypto_Argon2_HashRaw -> "crypto.argon2.hashRaw"
Crypto_Argon2_VerifyRaw -> "crypto.argon2.verifyRaw"
Crypto_P256_publicKey_impl -> "crypto.P256.publicKey.impl"
Crypto_P256_signSha256_impl -> "crypto.P256.signSha256.impl"
Crypto_P256_verifySha256_impl -> "crypto.P256.verifySha256.impl"
Crypto_Ed25519_sign_impl -> "crypto.Ed25519.sign.impl"
Crypto_Ed25519_verify_impl -> "crypto.Ed25519.verify.impl"
Crypto_Rsa_sign_impl -> "crypto.Rsa.sign.impl"
Expand Down
2 changes: 2 additions & 0 deletions unison-runtime/tests/Suite.hs
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import System.IO
import System.IO.CodePage (withCP65001)
import Unison.Test.Runtime.ANF qualified as ANF
import Unison.Test.Runtime.ANF.Serialization qualified as ANF.Serialization
import Unison.Test.Runtime.Crypto.P256 qualified as P256
import Unison.Test.Runtime.Crypto.Rsa qualified as Rsa
import Unison.Test.Runtime.MCode qualified as MCode
import Unison.Test.Runtime.MCode.Serialization qualified as MCode.Serialization
Expand All @@ -21,6 +22,7 @@ test =
ANF.Serialization.test,
MCode.test,
MCode.Serialization.test,
P256.test,
Rsa.test,
UnisonSources.test
]
Expand Down
55 changes: 55 additions & 0 deletions unison-runtime/tests/Unison/Test/Runtime/Crypto/P256.hs
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
module Unison.Test.Runtime.Crypto.P256 where

import Data.ByteString (ByteString)
import Data.ByteString.Char8 qualified as BC
import Data.Maybe (fromJust)
import EasyTest
import Text.Hex (decodeHex)
import Unison.Runtime.Crypto.P256 qualified as P256

test :: Test ()
test =
scope "p256" $
tests
[ scope "derivePublicKey" derivePublicKeyTest,
scope "signSha256" signSha256Test,
scope "verifySha256" verifySha256Test
]

privateKey :: ByteString
privateKey =
fromJust $
decodeHex
"0000000000000000000000000000000000000000000000000000000000000001"

publicKey :: ByteString
publicKey =
fromJust $
decodeHex
"046b17d1f2e12c4247f8bce6e563a440f277037d812deb33a0f4a13945d898c2964fe342e2fe1a7f9b8ee7eb4a7c0f9e162bce33576b315ececbb6406837bf51f5"

message :: ByteString
message = BC.pack "hello"

signature :: ByteString
signature =
fromJust $
decodeHex
"fe485cf09056db3fec44540ce06a9abf8ac1498d5cae2fa5c36519afaff98487448fcd767dfeee08940317d1e01d4296b9bd323629771d118ea10674dbb47755"

derivePublicKeyTest :: Test ()
derivePublicKeyTest =
expectEqual (Right publicKey) (P256.derivePublicKey privateKey)

signSha256Test :: Test ()
signSha256Test =
expectEqual (Right signature) (P256.signSha256 privateKey message)

verifySha256Test :: Test ()
verifySha256Test =
tests
[ expectEqual (Right True) (P256.verifySha256 publicKey message signature),
expectEqual
(Right False)
(P256.verifySha256 publicKey (BC.pack "hello!") signature)
]
2 changes: 2 additions & 0 deletions unison-runtime/unison-runtime.cabal
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,7 @@ library
Unison.Runtime.Builtin
Unison.Runtime.Builtin.Types
Unison.Runtime.Canonicalizer
Unison.Runtime.Crypto.P256
Unison.Runtime.Crypto.Rsa
Unison.Runtime.Debug
Unison.Runtime.Decompile
Expand Down Expand Up @@ -196,6 +197,7 @@ test-suite runtime-tests
Unison.Test.Gen
Unison.Test.Runtime.ANF
Unison.Test.Runtime.ANF.Serialization
Unison.Test.Runtime.Crypto.P256
Unison.Test.Runtime.Crypto.Rsa
Unison.Test.Runtime.MCode
Unison.Test.Runtime.MCode.Serialization
Expand Down
Loading
Loading