Calls that are not anonymous are cryptographically signed.
The dfx tool uses ECDSA signatures on the secp256k1 curve, which is given by
\(y^2 = x^3 + 7\) over the field \(\mathbb{F}_p\) where \(p\) is:
pk1 = 2^256 - 2^32 - 2^9 - 2^8 - 2^7 - 2^6 - 2^4 - 1 :: Integer
data Fpk1 = Fpk1 { unFpk1 :: Integer } deriving (Show, Eq)
instance Ring Fpk1 where
Fpk1 a + Fpk1 b = Fpk1 $ (a + b) `mod` pk1
Fpk1 a - Fpk1 b = Fpk1 $ (a - b) `mod` pk1
Fpk1 a * Fpk1 b = Fpk1 $ (a * b) `mod` pk1
fromInteger x = Fpk1 $ x `mod` pk1
instance Field Fpk1 where recip = (^(pk1-2))
orderk1 = 0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEBAAEDCE6AF48A03BBFD25E8CD0364141
One solution is designated the base point:
basek1x = 0x79BE667EF9DCBBAC55A06295CE870B07029BFCDB2DCE28D959F2815B16F81798
basek1y = 0x483ADA7726A3C4655DA4FBFC0E1108A8FD17B448A68554199C47D08FFB10D4B8
Like all other non-trivial points, the base point generates all the others.
Traditionally, mathematicians write the group operation with additive notation,
so one speaks of point addition and point doubling. This causes some friction
with cryptographers, who use multiplicative notation when discussing the
discrete log problem and cryptosystems built upon its variants. We choose
multiplicative notation in our code.
Computing the group operation needs nothing beyond high school mathematics. If
the two given points are distinct, we write down the equation of the line
through them and a little algebra reveals where it intersects the curve a third
time. When the two inputs points are the same, a touch of differential calculus
determines the slope of a tangent line instead.
Either way, we must also negate the y-coordinate of the intersection so that
the operation satisfies the group axioms.
data Ek1Affine = Ek1Affine (Maybe (Fpk1, Fpk1)) deriving Show
instance Ring Ek1Affine where
(+) = undefined
(-) = undefined
Ek1Affine (Just a) * Ek1Affine (Just b) = Ek1Affine $ addEk1Affine a b
Ek1Affine a * Ek1Affine b = Ek1Affine $ a <|> b
fromInteger 1 = Ek1Affine Nothing
addEk1Affine (px, py) (qx, qy)
| px /= qx = slope $ (qy - py) / (qx - px)
| py == qy && py /= 0 = slope $ 3*px^2 / (2*py)
| otherwise = Nothing
where
slope t = Just (rx, ry) where
rx = t^2 - px - qx
ry = (px - rx)*t - py
basek1Affine = Ek1Affine $ Just (basek1x, basek1y)
In practice, divisions are expensive, so we postpone them as long as possible,
replacing many frequent divisions into one single final division, at the cost
of a few more multiplications. In other words, we use projective coordinates:
basek1 = Ek1 $ Just (basek1x, basek1y, 1)
data Ek1 = Ek1 (Maybe (Fpk1, Fpk1, Fpk1)) deriving Show
instance Ring Ek1 where
(+) = undefined
(-) = undefined
Ek1 (Just a) * Ek1 (Just b) = Ek1 $ lineEk1 a b
Ek1 a * Ek1 b = Ek1 $ a <|> b
fromInteger 1 = Ek1 Nothing
instance Field Ek1 where recip (Ek1 m) = Ek1 $ (\(x, y, z) -> (x, -y, z)) <$> m
lineEk1 (px, py, pz) (qx, qy, qz)
| u /= 0 = Just (u*w, t*(u0*u2 - w) - t0*u3, u3*v)
| t == 0 = tanEk1 (px, py, pz)
| otherwise = Nothing
where
sq a = a * a
t0 = py*qz
t1 = qy*pz
t = t0 - t1
u0 = px*qz
u1 = qx*pz
u = u0 - u1
u2 = sq u
v = pz*qz
w = sq t*v - u2*(u0 + u1)
u3 = u*u2
-- Assume y /= 0 since group has odd order.
tanEk1 (x, y, z) = Just $ (u*w, t*(v - w) - 2*sq (u*y), sq u * u) where
sq a = a*a
t = 3*sq x
u = 2*y*z
v = 2*u*x*y
w = sq t - 2*v
normk1 (Ek1 m) = (\(x,y,z) -> let z1 = recip z in (x*z1, y*z1)) <$> m
A private key d is a positive integer less than the order. For example:
dZoo = 0xdd730a61f98fe573c7676e5957727c01cedf3c5a04beba39df92445e0bd2ec87
The corresponding public key is basek1^d. (If we had used additive notation,
instead of exponentiation, we would call this d times the base point.)
A popular representation of such a public key is the byte 04 followed
by the x- and y-coordinates in big-endian, both taking 32 bytes.
In this format, the public key corresponding to dZoo turns out to be:
04
3cc849c77d5ead3aeaf2ea821dc85d6bb10483bbe97875d010ada2629e4a863e
815793de69ae4ffce46d52c4b14ed1a3ae40e85b53b5cb6c7ed6de89d80c4305
This is an uncompressed point; the 04 indicates both coordinates are
present. In other contexts we might see a compressed point, where only the
y-coordinate is given, and the initial byte is 02 or 03 depending on the
parity of the x-coordinate so the recipient can recover the correct one.
To produce the DER encoding of an ECDSA public key, we prepend the magic
string:
derECDSA = unxxs "3056301006072a8648ce3d020106052b8104000a034200"
derECDSAFromXY x y = concat [derECDSA, "\x04", biggie 32 x, biggie 32 y]
We convert a DER public key to a principal IDs by computing its SHA224 hash
then appending the byte 02, indicating a self-authenticating ID in Internet
Computer parlance. Like other principal IDs, we can cook self-authenticating
IDs by prepending a CRC32 checksum, encoding with base32, and inserting dashes.
Some tools expect these.
crc :: String -> Word
crc = xor 0xffffffff . foldl go 0xffffffff where
go rem c = xor (shiftR rem 8) $ crcTable!!(fromIntegral $ xor (fromIntegral $ fromEnum c) (rem .&. 0xff))
crcTable :: [Word]
crcTable = [iterate inner n!!8 | n <- [0..255]] where
inner c
| c .&. 1 == 1 = xor 0xedb88320 bumped
| otherwise = bumped
where bumped = shiftR c 1
cook s = intercalate "-" . chunksOf 5 . base32 $ biggie 4 (crc s) ++ s
base32Alphabet = ['a'..'z'] ++ ['2'..'7']
base32 s = (base32Alphabet!!) . unbits <$> ws
where
ws = chunksOf 5 $ foldr ($) [] $ (bits 8 . ord) <$> s
unbits w = go 0 $ take 5 $ w <> repeat 0 where
go acc [] = acc
go acc (h:t) = go (acc*2 + h) t
bits k n
| k == 0 = id
| otherwise = bits (k - 1) q . (r:) where (q, r) = divMod n 2
The following takes a private key and shows corresponding public key in various
formats. It is slow because of our simplistic arbitrary precision arithmetic
implementation, but (barely) bearable on modern browsers.
privateKeyInfo d = case normk1 $ basek1^d of
Nothing -> "must avoid order of curve"
Just (Fpk1 x, Fpk1 y) -> let
der = derECDSAFromXY x y
prin = sha224 der ++ "\x02"
in unlines
[ "public key (point on y^2 = x^3 + 7): " ++ show (x, y)
, "DER public key: " ++ concatMap xx der
, "Principal ID (raw): " ++ (xx =<< prin)
, "Principal ID (cooked): " ++ cook prin
]
demo_publicKey = putStr $ privateKeyInfo $ unbiggie $ map ord $ unxxs
"dd730a61f98fe573c7676e5957727c01cedf3c5a04beba39df92445e0bd2ec87"
jsEval_ "slowMo('demo_publicKey');"
To sign a 256-bit message with ECDSA, we need a random 256-bit k
It is vital that k be distinct for each message and unpredictable. Famous
breaches have occurred due to failures to observe these rules.
An alternative to randomly choosing k is to use a keyed hash of the message.
For our demo we do so in an ad hoc manner: see RFC 6979 for the right way to do
it! (Or better yet, switch to Ed25519 signatures.)
The result is two 256-bit integers r and s. We write them in big-endian, so
each takes 32 bytes, and concatenate them to form the signature.
egcd a b
| r == 0 = (b, (0, 1))
| (d, (x, y)) <- egcd b r = (d, (y, x - q*y))
where
(q, r) = divMod a b
signk1 d msg = biggie 32 =<< [r, s] where
n = orderk1
k = unbiggie $ map ord $ sha256 $ biggie 32 d ++ msg
kinv = if a < 0 then a + n else a where (_, (a, _)) = egcd k n
Just r = unFpk1 . fst <$> normk1 (basek1^k)
z = unbiggie $ ord <$> msg
s = (z + r*d) `mod` n * kinv `mod` n
A signed call is like an anonymous call with a few extra fields.
We place the DER-encoded public key in the sender_pubkey field of the CBOR
map in the POST body.
We place the principal ID corresponding to the public key in the sender field
of the content map. (Yes, this is redundant, as it contains the same
information as sender_pubkey.)
We compute the request ID as above and append it to the domain separator
\x0Aic-request, then sign its SHA-256 hash (viewed as a big-endian 256-bit
number) with ECDSA to yield a 64-byte signature, which we place in the
sender_sig field.
signedCall der sig content = ($"") $ cborEncode $
CBORTag 55799 $ CBORMap
[ (CBORText "content", content)
, (CBORText "sender_pubkey", CBORBlob $ ord <$> der)
, (CBORText "sender_sig", CBORBlob $ ord <$> sig)
]
The following (eventually) makes signed calls just like dfx, except there’s
no nonce. (Speaking of which, the lower bits of the ingress_expiry field may
suffice as a nonce for some applications because it’s measured in nanoseconds!)
The sender is the example dZoo key.
demo_signedCall = do
now <- readInteger <$> jsEval "Date.now();"
let
Just (Fpk1 x, Fpk1 y) = normk1 $ basek1^dZoo
der = derECDSAFromXY x y
prin = sha224 der ++ "\x02"
t = (now + 120000) * 1000000
canid = "fxa77-fiaaa-aaaae-aaana-cai"
arg = ord <$> unxxs "4449444c036d7b6d6f6c04efd6e40271e1edeb4a71a2f5ed880400c6a4a19806010102012f034745540000"
reqType = "call"
url = "https://icp0.io/api/v2/canister/" ++ canid ++ "/" ++ reqType
content = cborRequest reqType (uncook canid) "http_request" arg (ord <$> prin) t
reqId = cborHash content
sig = signk1 dZoo $ sha256 $ "\x0aic-request" ++ reqId
body = xxs $ signedCall der sig content
putStr $ xxs reqId
nextOut
putStr url
nextOut
putStr canid
nextOut
putStr body
jsEval_ "clickGate('demo_signedCall', postSignedCall);"
We neglect to sign the read state calls, which would take even longer, so this
demo should eventually result in a 403. (Thus even without a signature, we
can distinguish between a pending call and a completed call.)