cryptopals

Matasano's cryptopals challenges (cryptopals.com).
Log | Files | Refs | README | LICENSE

commit 5a78edf198ec514771a118d8b04643b258fd8849
parent 67c35aa9cbe9074d927086e6d46f7e3b64004354
Author: Jared Tobin <jared@jtobin.io>
Date:   Tue, 22 Aug 2023 14:42:49 -0230

Add 6.42.

Diffstat:
Mcryptopals.cabal | 1+
Mdocs/s5.md | 3++-
Mdocs/s6.md | 36++++++++++++++++++++++++++++++++++++
Mlib/Cryptopals/RSA.hs | 72+++++++++++++++++++++++++++++++++++++++++++++---------------------------
Mlib/Cryptopals/RSA/Attacks.hs | 23+++++++++++++++++++++++
5 files changed, 107 insertions(+), 28 deletions(-)

diff --git a/cryptopals.cabal b/cryptopals.cabal @@ -52,6 +52,7 @@ library , containers , cryptonite , HTTP + , integer-logarithms , integer-roots , mwc-random , network diff --git a/docs/s5.md b/docs/s5.md @@ -374,7 +374,8 @@ greatest common denominator of 1. Encryption and decryption are then just modular exponentiation operations using the keys. To go from Natural to ByteString and back, I used some old functions -- roll and unroll -- that I wrote for -[urbit-hob](http://git.jtobin.io/urbit-hob) a few years back: +[urbit-hob](http://git.jtobin.io/urbit-hob) a few years back (though +actually I think I cribbed them from the Data.Binary internals): data Key = Key Natural Natural deriving (Eq, Show) diff --git a/docs/s6.md b/docs/s6.md @@ -85,3 +85,39 @@ to crack the juicy secret: Shame, shame. +#### 6.42 + +The idea here is simple, but clever: assemble something that, to a +sloppy verifier, looks to be validly PKCS#1 v1.5-encoded, put a bunch +of junk bytes at the end of it, and manipulate everything such that +the result is a cube (or at least has an approximate cube root). Then +calculate the approximate cube root and pass that off as a signature. + +Cryptopals.RSA implements some functions for PKCS#1 v1.5 encoding (as +defined in [RFC-2313](https://datatracker.ietf.org/doc/html/rfc2313)), +and, in particular, the requisite broken verification. 'sign' and +'verify' implement a signature scheme using that encoding and SHA512. +'forge' in Cryptopals.RSA.Attacks implements the desired forging +function. + +Let's test it out the basic signing and verification functionality: + + > Keypair sec pub@(Pub e n) <- keygen 1024 + > let msg = "hi mom" + > let (_, sig) = sign sec msg + > verify pub msg sig + True + > verify pub "hi mum" sig + False + +and now the forgery, produced of course without the secret key: + + > let gis = forge n msg + > verify pub msg gis + True + > verify pub "hi mum" gis + False + + + + diff --git a/lib/Cryptopals/RSA.hs b/lib/Cryptopals/RSA.hs @@ -3,8 +3,12 @@ module Cryptopals.RSA ( , Keypair(..) , keygen + , bitl + , unroll , roll + , unroll' + , roll' , invmod , invmod' @@ -14,11 +18,11 @@ module Cryptopals.RSA ( , pkcs1v1p5encode , pkcs1v1p5verify + , asnSha512 + , asnSha1 , sign , verify - , sign' - , verify' ) where import qualified Cryptopals.DH as DH @@ -28,21 +32,41 @@ import qualified Data.Binary as DB import qualified Data.Bits as B import qualified Data.ByteString as BS import qualified Data.ByteString.Lazy as BL -import Data.List (unfoldr) +import qualified Math.NumberTheory.Logarithms as L import Numeric.Natural --- | Simple little-endian ByteString encoding for Naturals. +-- bit length +bitl :: Natural -> Int +bitl (fromIntegral -> n) + | n > 0 = succ . L.integerLog2' $ n + | otherwise = 0 + +-- big-endian natural encoding unroll :: Natural -> BS.ByteString unroll nat = case nat of 0 -> BS.singleton 0 - _ -> BS.pack (unfoldr step nat) + _ -> BS.reverse $ BS.unfoldr step nat where step 0 = Nothing step i = Just (fromIntegral i, i `B.shiftR` 8) --- | Simple little-endian ByteString decoding for Naturals. +-- big-endian bytestring decoding roll :: BS.ByteString -> Natural -roll = foldr unstep 0 . BS.unpack where +roll = BS.foldl' unstep 0 where + unstep a b = a `B.shiftL` 8 B..|. fromIntegral b + +-- little-endian natural encoding +unroll' :: Natural -> BS.ByteString +unroll' nat = case nat of + 0 -> BS.singleton 0 + _ -> BS.unfoldr step nat + where + step 0 = Nothing + step i = Just (fromIntegral i, i `B.shiftR` 8) + +-- little-endian bytestring decoding +roll' :: BS.ByteString -> Natural +roll' = BS.foldr unstep 0 where unstep b a = a `B.shiftL` 8 B..|. fromIntegral b -- egcd/invmod adapted from https://rosettacode.org/wiki/Modular_inverse @@ -95,6 +119,8 @@ keygen siz = loop where Nothing -> loop Just d -> pure $ Keypair (Sec d n) (Pub e n) +-- XX padding for crypt ops / other hash functions for signatures + encrypt :: Key -> BS.ByteString -> BS.ByteString encrypt key msg = case key of Sec {} -> error "encrypt: need public key" @@ -105,22 +131,7 @@ decrypt key cip = case key of Pub {} -> error "decrypt: need secret key" Sec d n -> unroll (DH.modexp (roll cip) d n) --- sign without padding -sign' :: Key -> BS.ByteString -> (BS.ByteString, BS.ByteString) -sign' key msg = case key of - Pub {} -> error "sign': need secret key" - Sec d n -> - let h = fromIntegral $ CS.integerDigest (CS.sha512 (BL.fromStrict msg)) - in (msg, unroll (DH.modexp h d n)) - --- verify without padding -verify' :: Key -> BS.ByteString -> BS.ByteString -> Bool -verify' key msg sig = case key of - Sec {} -> error "verify': need public key" - Pub e n -> - let h = fromIntegral $ CS.integerDigest (CS.sha512 (BL.fromStrict msg)) - in h == DH.modexp (roll sig) e n - +-- sign using SHA512 sign :: Key -> BS.ByteString -> (BS.ByteString, BS.ByteString) sign key msg = case key of Pub {} -> error "sign: need secret key" @@ -128,17 +139,18 @@ sign key msg = case key of let padded = pkcs1v1p5encode key msg in (msg, unroll (DH.modexp (roll padded) d n)) +-- verify using broken pkcs1 (SHA512) verification verify :: Key -> BS.ByteString -> BS.ByteString -> Bool verify key msg sig = case key of Sec {} -> error "verify: need public key" Pub e n -> let h = BL.toStrict $ CS.bytestringDigest (CS.sha512 (BL.fromStrict msg)) r = DH.modexp (roll sig) e n - in case pkcs1v1p5verify (unroll r) of + in case pkcs1v1p5verify (BS.cons 0 (unroll r)) of -- BE-storage hack Nothing -> False Just l -> h == l --- pkcs#1 v1.5-encode a message +-- pkcs#1 v1.5-encode a message (using SHA512) pkcs1v1p5encode :: Key -> BS.ByteString -> BS.ByteString pkcs1v1p5encode key msg = BS.cons 0x00 (BS.snoc (BS.cons 0x01 ffs) 0x00) <> asnSha512 <> has @@ -150,8 +162,8 @@ pkcs1v1p5encode key msg = ffs = BS.replicate len 0xff has = BL.toStrict $ CS.bytestringDigest (CS.sha512 (BL.fromStrict msg)) --- sloppy pkcs#1 v1.5 verification; doesn't check message terminates --- after hash +-- sloppy pkcs#1 v1.5 verification (SHA512); doesn't verify the length +-- of the padding pkcs1v1p5verify :: BS.ByteString -> Maybe BS.ByteString pkcs1v1p5verify = checknul where checknul bs = case BS.uncons bs of @@ -195,3 +207,9 @@ asnSha512 = BS.pack [ , 0x40 ] +-- ASN.1 encoding of SHA1 +asnSha1 :: BS.ByteString +asnSha1 = BS.pack [ + 0x30, 0x21, 0x30, 0x09, 0x06, 0x05, 0x2b, 0x0e, 0x03 + , 0x02, 0x1a, 0x05, 0x00, 0x04, 0x14 + ] diff --git a/lib/Cryptopals/RSA/Attacks.hs b/lib/Cryptopals/RSA/Attacks.hs @@ -25,6 +25,8 @@ import Pipes import qualified Pipes.Prelude as P import qualified System.Random.MWC as MWC +-- e=3 broadcast attack + e3BroadcastAttack :: (BS.ByteString, Key) -- ciphertext / pubkey -> (BS.ByteString, Key) -- ciphertext / pubkey @@ -46,6 +48,8 @@ e3BroadcastAttack (c0, p0) (c1, p1) (c2, p2) = case (p0, p1, p2) of _ -> error "e3BroadcastAttack: require public keys" +-- unpadded message recovery oracle + type Digests = HS.HashSet Integer umrClient :: MonadIO m => Key -> Producer BS.ByteString m () @@ -104,3 +108,22 @@ umrrecover key s msg = case key of Sec {} -> error "umrrecover: need public key" Pub e n -> unroll $ (roll msg `mod` n * invmod' s n) `mod` n +-- bleichenbacher's e=3 signature forgery + +fencode :: Natural -> BS.ByteString -> BS.ByteString +fencode mod msg = + let has = BL.toStrict $ CS.bytestringDigest (CS.sha512 (BL.fromStrict msg)) + len = bitl mod `quot` 8 + pad = + BS.cons 0x00 + . BS.cons 0x01 + . BS.cons 0xff + $ BS.cons 0x00 asnSha512 + vil = pad <> has + in vil <> BS.replicate (len - BS.length vil) 0 + +forge :: Natural -> BS.ByteString -> BS.ByteString +forge mod msg = + let f = fencode mod msg + in unroll $ R.integerCubeRoot (roll f) + 1 +