diff --git a/Cargo.toml b/Cargo.toml
index 764a2a0..93978c5 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -12,6 +12,7 @@ homepage = "https://github.com/zcash-hackworks/zip32"
 repository = "https://github.com/zcash-hackworks/zip32"
 
 [dependencies]
+byteorder = "1"
 lazy_static = "1.0"
 pairing = "0.14.2"
 
diff --git a/src/lib.rs b/src/lib.rs
index 29599e3..f9b68d8 100644
--- a/src/lib.rs
+++ b/src/lib.rs
@@ -1,11 +1,13 @@
 extern crate blake2_rfc;
+extern crate byteorder;
 #[macro_use]
 extern crate lazy_static;
 extern crate pairing;
 extern crate sapling_crypto;
 
 use blake2_rfc::blake2b::{Blake2b, Blake2bResult};
-use pairing::{bls12_381::Bls12, PrimeField, PrimeFieldRepr};
+use byteorder::{ByteOrder, LittleEndian};
+use pairing::{bls12_381::Bls12, Field, PrimeField, PrimeFieldRepr};
 use sapling_crypto::{
     jubjub::{FixedGenerators, JubjubBls12, JubjubEngine, JubjubParams, ToUniform},
     primitives::ViewingKey,
@@ -23,16 +25,30 @@ pub const ZIP32_SAPLING_FVFP_PERSONALIZATION: &'static [u8; 16] = b"ZcashSapling
 
 /// PRF^expand(sk, t) := BLAKE2b-512("Zcash_ExpandSeed", sk || t)
 fn prf_expand(sk: &[u8], t: &[u8]) -> Blake2bResult {
+    prf_expand_vec(sk, &vec![t])
+}
+
+fn prf_expand_vec(sk: &[u8], ts: &[&[u8]]) -> Blake2bResult {
     let mut h = Blake2b::with_params(64, &[], &[], PRF_EXPAND_PERSONALIZATION);
     h.update(sk);
-    h.update(t);
+    for t in ts {
+        h.update(t);
+    }
     h.finalize()
 }
 
 /// An outgoing viewing key
-#[derive(Clone, Copy)]
+#[derive(Clone, Copy, PartialEq)]
 struct OutgoingViewingKey([u8; 32]);
 
+impl OutgoingViewingKey {
+    fn derive_child(&self, i_l: &[u8]) -> Self {
+        let mut ovk = [0u8; 32];
+        ovk.copy_from_slice(&prf_expand_vec(i_l, &[&[0x15], &self.0]).as_bytes()[..32]);
+        OutgoingViewingKey(ovk)
+    }
+}
+
 /// A Sapling expanded spending key
 struct ExpandedSpendingKey<E: JubjubEngine> {
     ask: E::Fs,
@@ -56,6 +72,15 @@ impl<E: JubjubEngine> ExpandedSpendingKey<E> {
         ExpandedSpendingKey { ask, nsk, ovk }
     }
 
+    fn derive_child(&self, i_l: &[u8]) -> Self {
+        let mut ask = E::Fs::to_uniform(prf_expand(i_l, &[0x13]).as_bytes());
+        let mut nsk = E::Fs::to_uniform(prf_expand(i_l, &[0x14]).as_bytes());
+        ask.add_assign(&self.ask);
+        nsk.add_assign(&self.nsk);
+        let ovk = self.ovk.derive_child(i_l);
+        ExpandedSpendingKey { ask, nsk, ovk }
+    }
+
     fn to_bytes(&self) -> [u8; 96] {
         let mut result = [0u8; 96];
         self.ask
@@ -86,6 +111,24 @@ impl<E: JubjubEngine> FullViewingKey<E> {
         }
     }
 
+    fn derive_child(&self, i_l: &[u8], params: &E::Params) -> Self {
+        let i_ask = E::Fs::to_uniform(prf_expand(i_l, &[0x13]).as_bytes());
+        let i_nsk = E::Fs::to_uniform(prf_expand(i_l, &[0x14]).as_bytes());
+        let ak = params
+            .generator(FixedGenerators::SpendingKeyGenerator)
+            .mul(i_ask, params)
+            .add(&self.vk.ak, params);
+        let nk = params
+            .generator(FixedGenerators::ProofGenerationKey)
+            .mul(i_nsk, params)
+            .add(&self.vk.nk, params);
+
+        FullViewingKey {
+            vk: ViewingKey { ak, nk },
+            ovk: self.ovk.derive_child(i_l),
+        }
+    }
+
     fn to_bytes(&self) -> [u8; 96] {
         let mut result = [0u8; 96];
         self.vk
@@ -117,7 +160,7 @@ impl<'a, E: JubjubEngine> From<&'a FullViewingKey<E>> for FVKFingerprint {
 }
 
 /// A Sapling full viewing key tag
-#[derive(Clone, Copy)]
+#[derive(Clone, Copy, Debug, PartialEq)]
 struct FVKTag([u8; 4]);
 
 impl<'a> From<&'a FVKFingerprint> for FVKTag {
@@ -141,7 +184,7 @@ impl FVKTag {
 }
 
 /// A child index for a derived key
-#[derive(Clone, Copy)]
+#[derive(Clone, Copy, Debug, PartialEq)]
 pub enum ChildIndex {
     NonHardened(u32),
     Hardened(u32), // Hardened(n) == n + (1 << 31) == n' in path notation
@@ -161,11 +204,11 @@ impl ChildIndex {
 }
 
 /// A chain code
-#[derive(Clone, Copy)]
+#[derive(Clone, Copy, Debug, PartialEq)]
 struct ChainCode([u8; 32]);
 
 /// A key used to derive diversifiers for a particular child key
-#[derive(Clone, Copy)]
+#[derive(Clone, Copy, Debug, PartialEq)]
 struct DiversifierKey([u8; 32]);
 
 impl DiversifierKey {
@@ -174,6 +217,12 @@ impl DiversifierKey {
         dk_m.copy_from_slice(&prf_expand(sk_m, &[0x10]).as_bytes()[..32]);
         DiversifierKey(dk_m)
     }
+
+    fn derive_child(&self, i_l: &[u8]) -> Self {
+        let mut dk = [0u8; 32];
+        dk.copy_from_slice(&prf_expand_vec(i_l, &[&[0x16], &self.0]).as_bytes()[..32]);
+        DiversifierKey(dk)
+    }
 }
 
 /// A Sapling extended spending key
@@ -196,6 +245,29 @@ pub struct ExtendedFullViewingKey {
     dk: DiversifierKey,
 }
 
+impl std::cmp::PartialEq for ExtendedFullViewingKey {
+    fn eq(&self, rhs: &ExtendedFullViewingKey) -> bool {
+        self.depth == rhs.depth
+            && self.parent_fvk_tag == rhs.parent_fvk_tag
+            && self.child_index == rhs.child_index
+            && self.chain_code == rhs.chain_code
+            && self.fvk.vk.ak == rhs.fvk.vk.ak
+            && self.fvk.vk.nk == rhs.fvk.vk.nk
+            && self.fvk.ovk == rhs.fvk.ovk
+            && self.dk == rhs.dk
+    }
+}
+
+impl std::fmt::Debug for ExtendedFullViewingKey {
+    fn fmt(&self, f: &mut std::fmt::Formatter) -> Result<(), std::fmt::Error> {
+        write!(
+            f,
+            "ExtendedFullViewingKey(d = {}, tag_p = {:?}, i = {:?})",
+            self.depth, self.parent_fvk_tag, self.child_index
+        )
+    }
+}
+
 impl ExtendedSpendingKey {
     pub fn master(seed: &[u8]) -> Self {
         let mut h = Blake2b::with_params(64, &[], &[], ZIP32_SAPLING_MASTER_PERSONALIZATION);
@@ -215,6 +287,40 @@ impl ExtendedSpendingKey {
             dk: DiversifierKey::master(sk_m),
         }
     }
+
+    pub fn derive_child(&self, i: ChildIndex) -> Self {
+        let fvk = FullViewingKey::from_expanded_spending_key(&self.xsk, &JUBJUB);
+        let tmp = match i {
+            ChildIndex::Hardened(i) => {
+                let mut le_i = [0; 4];
+                LittleEndian::write_u32(&mut le_i, i + (1 << 31));
+                prf_expand_vec(
+                    &self.chain_code.0,
+                    &[&[0x11], &self.xsk.to_bytes(), &self.dk.0, &le_i],
+                )
+            }
+            ChildIndex::NonHardened(i) => {
+                let mut le_i = [0; 4];
+                LittleEndian::write_u32(&mut le_i, i);
+                prf_expand_vec(
+                    &self.chain_code.0,
+                    &[&[0x12], &fvk.to_bytes(), &self.dk.0, &le_i],
+                )
+            }
+        };
+        let i_l = &tmp.as_bytes()[..32];
+        let mut c_i = [0u8; 32];
+        c_i.copy_from_slice(&tmp.as_bytes()[32..]);
+
+        ExtendedSpendingKey {
+            depth: self.depth + 1,
+            parent_fvk_tag: FVKFingerprint::from(&fvk).into(),
+            child_index: i,
+            chain_code: ChainCode(c_i),
+            xsk: self.xsk.derive_child(i_l),
+            dk: self.dk.derive_child(i_l),
+        }
+    }
 }
 
 impl<'a> From<&'a ExtendedSpendingKey> for ExtendedFullViewingKey {
@@ -230,10 +336,72 @@ impl<'a> From<&'a ExtendedSpendingKey> for ExtendedFullViewingKey {
     }
 }
 
-#[cfg(test)]
-mod tests {
-    #[test]
-    fn it_works() {
-        assert_eq!(2 + 2, 4);
+impl ExtendedFullViewingKey {
+    pub fn derive_child(&self, i: ChildIndex) -> Result<Self, ()> {
+        let tmp = match i {
+            ChildIndex::Hardened(_) => return Err(()),
+            ChildIndex::NonHardened(i) => {
+                let mut le_i = [0; 4];
+                LittleEndian::write_u32(&mut le_i, i);
+                prf_expand_vec(
+                    &self.chain_code.0,
+                    &[&[0x12], &self.fvk.to_bytes(), &self.dk.0, &le_i],
+                )
+            }
+        };
+        let i_l = &tmp.as_bytes()[..32];
+        let mut c_i = [0u8; 32];
+        c_i.copy_from_slice(&tmp.as_bytes()[32..]);
+
+        Ok(ExtendedFullViewingKey {
+            depth: self.depth + 1,
+            parent_fvk_tag: FVKFingerprint::from(&self.fvk).into(),
+            child_index: i,
+            chain_code: ChainCode(c_i),
+            fvk: self.fvk.derive_child(i_l, &JUBJUB),
+            dk: self.dk.derive_child(i_l),
+        })
+    }
+}
+
+#[cfg(test)]
+mod tests {
+    use super::*;
+
+    #[test]
+    fn derive_nonhardened_child() {
+        let seed = [0; 32];
+        let xsk_m = ExtendedSpendingKey::master(&seed);
+        let xfvk_m = ExtendedFullViewingKey::from(&xsk_m);
+
+        let i_5 = ChildIndex::NonHardened(5);
+        let xsk_5 = xsk_m.derive_child(i_5);
+        let xfvk_5 = xfvk_m.derive_child(i_5);
+
+        assert!(xfvk_5.is_ok());
+        assert_eq!(ExtendedFullViewingKey::from(&xsk_5), xfvk_5.unwrap());
+    }
+
+    #[test]
+    fn derive_hardened_child() {
+        let seed = [0; 32];
+        let xsk_m = ExtendedSpendingKey::master(&seed);
+        let xfvk_m = ExtendedFullViewingKey::from(&xsk_m);
+
+        let i_5h = ChildIndex::Hardened(5);
+        let xsk_5h = xsk_m.derive_child(i_5h);
+        let xfvk_5h = xfvk_m.derive_child(i_5h);
+
+        // Cannot derive a hardened child from an ExtendedFullViewingKey
+        assert!(xfvk_5h.is_err());
+        let xfvk_5h = ExtendedFullViewingKey::from(&xsk_5h);
+
+        let i_7 = ChildIndex::NonHardened(7);
+        let xsk_5h_7 = xsk_5h.derive_child(i_7);
+        let xfvk_5h_7 = xfvk_5h.derive_child(i_7);
+
+        // But we *can* derive a non-hardened child from a hardened parent
+        assert!(xfvk_5h_7.is_ok());
+        assert_eq!(ExtendedFullViewingKey::from(&xsk_5h_7), xfvk_5h_7.unwrap());
     }
 }