diff --git a/make-keypairs/Main.hs b/make-keypairs/Main.hs index d77171e5303c5fedfedabacef1830f9c687e5581..3e5eea01f14048b4fca3c12a2d041a2be5de5fa6 100644 --- a/make-keypairs/Main.hs +++ b/make-keypairs/Main.hs @@ -1,11 +1,9 @@ module Main where import qualified Crypto.PubKey.RSA as RSA -import Data.ASN1.BinaryEncoding (DER (DER)) -import Data.ASN1.Encoding (ASN1Encoding (encodeASN1)) -import Data.ASN1.Types (ASN1Object (toASN1)) -import qualified Data.ByteString.Lazy as LB -import Data.X509 (PrivKey (PrivKeyRSA)) +import qualified Data.ByteString as B +import Tahoe.SDMF.Internal.Keys (signatureKeyToBytes) +import Tahoe.SDMF.Keys (Signature (..)) -- | The size of the keys to generate. bits :: Int @@ -21,8 +19,12 @@ main = do genKey :: Show a => a -> IO () genKey n = do - (_, priv) <- RSA.generate bits e - let bytes = encodeASN1 DER (toASN1 (PrivKeyRSA priv) []) - LB.writeFile ("test/data/rsa-privkey-" <> show n <> ".der") bytes + print "Generating RSA key..." + (_, priv) <- RSA.generate (bits `div` 8) e + print $ "Serializing key " <> show n + let bytes = signatureKeyToBytes (Signature priv) + print $ "Generated them (" <> show (B.length bytes) <> " bytes)" + B.writeFile ("test/data/rsa-privkey-" <> show n <> ".der") bytes + print "Wrote them to the file." where e = 0x10001 diff --git a/src/Tahoe/SDMF/Internal/Keys.hs b/src/Tahoe/SDMF/Internal/Keys.hs index 40ab74034f3fd03a7bd005c7b8740122558e61f3..3478225d46f7ddddfff4ccfc7ff82f3b247f0229 100644 --- a/src/Tahoe/SDMF/Internal/Keys.hs +++ b/src/Tahoe/SDMF/Internal/Keys.hs @@ -3,14 +3,16 @@ module Tahoe.SDMF.Internal.Keys where import Prelude hiding (Read) +import Control.Monad (when) import Crypto.Cipher.AES (AES128) import Crypto.Cipher.Types (Cipher (cipherInit, cipherKeySize), IV, KeySizeSpecifier (KeySizeFixed)) import Crypto.Error (maybeCryptoError) import qualified Crypto.PubKey.RSA as RSA import Crypto.Random (MonadRandom) import Data.ASN1.BinaryEncoding (DER (DER)) -import Data.ASN1.Encoding (ASN1Encoding (encodeASN1)) -import Data.ASN1.Types (ASN1Object (toASN1)) +import Data.ASN1.Encoding (ASN1Encoding (encodeASN1), decodeASN1') +import Data.ASN1.Types (ASN1 (End, IntVal, Null, OID, OctetString, Start), ASN1ConstructionType (Sequence), ASN1Object (fromASN1, toASN1)) +import Data.Bifunctor (Bifunctor (first)) import qualified Data.ByteArray as ByteArray import qualified Data.ByteString as B import Data.ByteString.Base32 (encodeBase32Unpadded) @@ -20,14 +22,19 @@ import Data.X509 (PrivKey (PrivKeyRSA), PubKey (PubKeyRSA)) import Tahoe.CHK.Crypto (taggedHash, taggedPairHash) import Tahoe.CHK.Server (StorageServerID) -newtype KeyPair = KeyPair {toPrivateKey :: RSA.PrivateKey} +newtype KeyPair = KeyPair {toPrivateKey :: RSA.PrivateKey} deriving newtype (Show) toPublicKey :: KeyPair -> RSA.PublicKey toPublicKey = RSA.private_pub . toPrivateKey newtype Verification = Verification {unVerification :: RSA.PublicKey} newtype Signature = Signature {unSignature :: RSA.PrivateKey} + deriving newtype (Eq, Show) + data Write = Write {unWrite :: AES128, writeKeyBytes :: ByteArray.ScrubbedBytes} +instance Show Write where + show (Write _ bs) = T.unpack $ T.concat ["<WriteKey ", encodeBase32Unpadded (ByteArray.convert bs), ">"] + data Read = Read {unRead :: AES128, readKeyBytes :: ByteArray.ScrubbedBytes} newtype StorageIndex = StorageIndex {unStorageIndex :: B.ByteString} @@ -74,10 +81,10 @@ mutableWriteKeyTag = "allmydata_mutable_privkey_to_writekey_v1" -- | Compute the read key for a given write key for an SDMF share. deriveReadKey :: Write -> Maybe Read deriveReadKey w = - Read <$> key <*> pure sbs + Read <$> key <*> pure (ByteArray.convert sbs) where - sbs = writeKeyBytes w - key = maybeCryptoError . cipherInit . taggedHash keyLength mutableReadKeyTag . ByteArray.convert $ sbs + sbs = taggedHash keyLength mutableReadKeyTag . ByteArray.convert . writeKeyBytes $ w + key = maybeCryptoError . cipherInit $ sbs mutableReadKeyTag :: B.ByteString mutableReadKeyTag = "allmydata_mutable_writekey_to_readkey_v1" @@ -85,10 +92,10 @@ mutableReadKeyTag = "allmydata_mutable_writekey_to_readkey_v1" -- | Compute the data encryption/decryption key for a given read key for an SDMF share. deriveDataKey :: SDMF_IV -> Read -> Maybe Data deriveDataKey (SDMF_IV iv) r = - Data <$> key <*> pure sbs + Data <$> key <*> pure (ByteArray.convert sbs) where - sbs = readKeyBytes r - key = maybeCryptoError . cipherInit . taggedPairHash keyLength mutableDataKeyTag (B.pack . ByteArray.unpack $ iv) . ByteArray.convert $ sbs + sbs = B.take keyLength . taggedPairHash keyLength mutableDataKeyTag (B.pack . ByteArray.unpack $ iv) . ByteArray.convert . readKeyBytes $ r + key = maybeCryptoError . cipherInit $ sbs mutableDataKeyTag :: B.ByteString mutableDataKeyTag = "allmydata_mutable_readkey_to_datakey_v1" @@ -108,4 +115,49 @@ verificationKeyToBytes = LB.toStrict . encodeASN1 DER . flip toASN1 [] . PubKeyR PublicKey. -} signatureKeyToBytes :: Signature -> B.ByteString -signatureKeyToBytes = LB.toStrict . encodeASN1 DER . flip toASN1 [] . PrivKeyRSA . unSignature +signatureKeyToBytes = LB.toStrict . encodeASN1 DER . toPKCS8 + where + -- The ASN1Object instance for PrivKeyRSA can interpret an x509 + -- "Private-Key Information" (aka PKCS8; see RFC 5208, section 5) + -- structure but it _produces_ some other format. We must have exactly + -- this format. + -- + -- XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX + -- + -- RFC 5208 says: + -- + -- privateKey is an octet string whose contents are the value of the + -- private key. The interpretation of the contents is defined in the + -- registration of the private-key algorithm. For an RSA private key, + -- for example, the contents are a BER encoding of a value of type + -- RSAPrivateKey. + -- + -- The ASN.1 BER encoding for a given structure is *not guaranteed to be + -- unique*. This means that in general there is no guarantee of a unique + -- bytes representation of a signature key in this scheme so *key + -- derivations are not unique*. If any two implementations disagree on + -- this encoding (which BER allows them to do) they will not interoperate. + toPKCS8 (Signature privKey) = + [ Start Sequence + , IntVal 0 + , Start Sequence + , OID [1, 2, 840, 113549, 1, 1, 1] + , Null + , End Sequence + , -- Our ASN.1 encoder doesn't even pretend to support BER. Use DER! + -- It results in the same bytes as Tahoe-LAFS is working with so ... + -- Maybe we're lucky or maybe Tahoe-LAFS isn't actually following + -- the spec. + OctetString (LB.toStrict . encodeASN1 DER . toASN1 (PrivKeyRSA privKey) $ []) + , End Sequence + ] + +-- | Decode a private key from the Tahoe-LAFS canonical bytes representation. +signatureKeyFromBytes :: B.ByteString -> Either String Signature +signatureKeyFromBytes bs = do + asn1s <- first show $ decodeASN1' DER bs + (key, extra) <- fromASN1 asn1s + when (extra /= []) (Left $ "left over data: " <> show extra) + case key of + (PrivKeyRSA privKey) -> Right $ Signature privKey + _ -> Left ("Expect RSA private key, found " <> show key) diff --git a/tahoe-ssk.cabal b/tahoe-ssk.cabal index 2b45eec86bb69af0380e0a52e3045169f136c5e3..d7e3e7b2fa120ba97e87eda4899e187d4bda37ad 100644 --- a/tahoe-ssk.cabal +++ b/tahoe-ssk.cabal @@ -149,4 +149,5 @@ executable make-keypairs , base , bytestring , cryptonite + , tahoe-ssk , x509 diff --git a/test/Spec.hs b/test/Spec.hs index 4031d0cfd4382a4b948a1b676cc8c4b9f66374ee..b576ac03cbd1e9a4bec6e7772dbebd3093cacb11 100644 --- a/test/Spec.hs +++ b/test/Spec.hs @@ -9,7 +9,6 @@ import Hedgehog ( import Crypto.Cipher.Types (makeIV) import Data.ASN1.BinaryEncoding (DER (DER)) import Data.ASN1.Encoding (decodeASN1') -import Data.ASN1.Types (ASN1Object (fromASN1)) import qualified Data.Binary as Binary import Data.Binary.Get (ByteOffset) import qualified Data.ByteArray as ByteArray @@ -17,10 +16,10 @@ import qualified Data.ByteString as B import Data.ByteString.Base32 (encodeBase32Unpadded) import qualified Data.ByteString.Lazy as LB import qualified Data.Text as T -import Data.X509 (PrivKey (PrivKeyRSA)) -import Generators (shareHashChains, shares) +import Generators (genRSAKeys, shareHashChains, shares) import System.IO (hSetEncoding, stderr, stdout, utf8) import Tahoe.SDMF (Share) +import Tahoe.SDMF.Internal.Keys (signatureKeyFromBytes, signatureKeyToBytes) import qualified Tahoe.SDMF.Keys as Keys import Test.Tasty (TestTree, defaultMain, testGroup) import Test.Tasty.HUnit (assertEqual, testCase) @@ -38,23 +37,62 @@ tests = property $ do hashChain <- forAll shareHashChains tripping hashChain Binary.encode decode' + , testProperty "Signatures round-trip through signatureKeyToBytes . signatureKeyFromBytes" $ + property $ do + key <- forAll genRSAKeys + tripping (Keys.Signature . Keys.toPrivateKey $ key) signatureKeyToBytes signatureKeyFromBytes + , testCase "Signature byte-serializations round-trip through signatureKeyFromBytes . signatureKeyToBytes" $ do + let keyPaths = + [ -- Check ours + "test/data/rsa-privkey-0.der" + , "test/data/rsa-privkey-1.der" + , "test/data/rsa-privkey-2.der" + , "test/data/rsa-privkey-3.der" + , "test/data/rsa-privkey-4.der" + , -- And one from Tahoe-LAFS + "test/data/tahoe-lafs-generated-rsa-privkey.der" + ] + checkSignatureRoundTrip p = + B.readFile p >>= \original -> + let (Right sigKey) = signatureKeyFromBytes original + serialized = signatureKeyToBytes sigKey + in do + -- They should decode to the same structure. This + -- has the advantage of representing differences a + -- little more transparently than the next + -- assertion. + assertEqual + "decodeASN1 original /= decodeASN1 serialized" + (decodeASN1' DER original) + (decodeASN1' DER serialized) + -- Also check the raw bytes in case there + -- are different representations of the + -- structure possible. The raw bytes + -- matter because we hash them in key + -- derivations. + assertEqual "original /= serialized" original serialized + -- Check them all + mapM_ checkSignatureRoundTrip keyPaths , testCase "derived keys equal known-correct values" $ -- The path is relative to the root of the package, which is where -- at least some test runners will run the test process. If B.readFile "test/data/rsa-privkey-0.der" >>= \privBytes -> - let (Just iv) = Keys.SDMF_IV <$> makeIV (B.replicate 16 0x42) - expectedWriteKey = ("ae6e6cgcllhty4z5l4dp5v7gee" :: T.Text) - expectedReadKey = ("rbx5xh5rztefvazy7sq32sw34y" :: T.Text) - expectedDataKey = ("4ay4y6itvk7cvynpyok3qmxf5y" :: T.Text) + let -- Load the test key. + (Right sigKey) = signatureKeyFromBytes privBytes + + -- Hard-code the expected result. + expectedWriteKey = ("v7iymuxkc5yv2fomi3xwbjdd4e" :: T.Text) + expectedReadKey = ("6ir6husgx6ubro3tbimmzskqri" :: T.Text) + expectedDataKey = ("bbj67exlrkfcaqutwlgwvukbfe" :: T.Text) - (Right asn1s) = decodeASN1' DER privBytes - (Right (PrivKeyRSA privKey, [])) = fromASN1 asn1s - (Just w@(Keys.Write _ derivedWriteKey)) = Keys.deriveWriteKey (Keys.Signature privKey) + -- Derive all the keys. + (Just iv) = Keys.SDMF_IV <$> makeIV (B.replicate 16 0x42) + (Just w@(Keys.Write _ derivedWriteKey)) = Keys.deriveWriteKey sigKey (Just r@(Keys.Read _ derivedReadKey)) = Keys.deriveReadKey w (Just d@(Keys.Data _ derivedDataKey)) = Keys.deriveDataKey iv r - -- Format a key as text for convenient comparison to - -- expected value. + -- A helper to format a key as text for convenient + -- comparison to expected value. fmtKey = T.toLower . encodeBase32Unpadded . ByteArray.convert in do -- In general it might make more sense to convert expected diff --git a/test/data/tahoe-lafs-generated-rsa-privkey.der b/test/data/tahoe-lafs-generated-rsa-privkey.der new file mode 100644 index 0000000000000000000000000000000000000000..ba71aa1212413a9f581dbc52594ea9fee5338ff0 Binary files /dev/null and b/test/data/tahoe-lafs-generated-rsa-privkey.der differ