diff --git a/jose.cabal b/jose.cabal
--- a/jose.cabal
+++ b/jose.cabal
@@ -1,5 +1,6 @@
+cabal-version:       2.2
 name:                jose
-version:             0.8.2.1
+version:             0.8.3
 synopsis:
   Javascript Object Signing and Encryption and JSON Web Token library
 description:
@@ -29,7 +30,6 @@
 copyright:           Copyright (C) 2013-2018  Fraser Tweedale
 category:            Cryptography
 build-type:          Simple
-cabal-version:       >= 1.8
 tested-with:
   GHC==7.10.3, GHC==8.0.2, GHC==8.2.2, GHC==8.4.4, GHC==8.6.5, GHC==8.8.1
 
@@ -37,7 +37,22 @@
   description: Build demonstration programs
   default: False
 
+common common
+  default-language: Haskell2010
+  ghc-options:    -Wall
+
+  build-depends:
+    base >= 4.8 && < 5
+    , aeson >= 0.8.0.1
+    , bytestring == 0.10.*
+    , lens >= 4.16
+    , mtl >= 2
+    , semigroups >= 0.15
+    , text >= 1.1
+
 library
+  import: common
+
   exposed-modules:
     Crypto.JOSE
     Crypto.JOSE.Compact
@@ -61,23 +76,16 @@
     Crypto.JOSE.Types.Orphans
 
   build-depends:
-    base >= 4.8 && < 5
-    , attoparsec
+    attoparsec
     , base64-bytestring >= 1.0 && < 1.2
     , concise >= 0.1
     , containers >= 0.5
     , cryptonite >= 0.7
-    , lens >= 4.16
     , memory >= 0.7
     , monad-time >= 0.1
-    , mtl >= 2
-    , semigroups >= 0.15
     , template-haskell >= 2.4
     , safe >= 0.3
-    , aeson >= 0.8.0.1
     , unordered-containers == 0.2.*
-    , bytestring == 0.10.*
-    , text >= 1.1
     , time >= 1.5
     , network-uri >= 2.6
     , QuickCheck >= 2
@@ -88,7 +96,6 @@
   if impl(ghc<8)
     build-depends: fail
 
-  ghc-options:    -Wall
   hs-source-dirs: src
 
 source-repository head
@@ -96,11 +103,13 @@
   location: https://github.com/frasertweedale/hs-jose.git
 
 test-suite tests
+  import: common
   type:           exitcode-stdio-1.0
   hs-source-dirs: test
   main-is:        Test.hs
   other-modules:
     AESKW
+    Examples
     JWK
     JWS
     JWT
@@ -108,26 +117,20 @@
     Types
 
   build-depends:
-    base
-    , attoparsec
+    attoparsec
     , base64-bytestring
     , containers
     , cryptonite
-    , lens
     , memory
     , monad-time
-    , mtl
-    , semigroups
     , template-haskell
     , safe
-    , aeson
     , unordered-containers
-    , bytestring
-    , text
     , time
     , network-uri
     , vector
     , x509
+    , pem
 
     , concise
     , jose
@@ -140,22 +143,15 @@
     , quickcheck-instances
 
 executable jose-example
+  import: common
   if !flag(demos)
     buildable: False
   hs-source-dirs: example
-  ghc-options:    -Wall
   main-is:  Main.hs
   other-modules:
     KeyDB
     JWS
 
   build-depends:
-    base
-    , aeson
-    , bytestring
-    , lens
-    , mtl
-    , semigroups
-    , text
-    , unix
+    unix
     , jose
diff --git a/src/Crypto/JOSE/JWA/JWK.hs b/src/Crypto/JOSE/JWA/JWK.hs
--- a/src/Crypto/JOSE/JWA/JWK.hs
+++ b/src/Crypto/JOSE/JWA/JWK.hs
@@ -37,6 +37,7 @@
   , curve
   , point
   , ecPrivateKey
+  , ecParametersFromX509
 
   -- * Parameters for RSA Keys
   , RSAPrivateKeyOthElem(..)
@@ -100,6 +101,8 @@
 import qualified Data.HashMap.Strict as M
 import Data.List.NonEmpty (NonEmpty)
 import qualified Data.Text as T
+import Data.X509 as X509
+import Data.X509.EC as X509.EC
 import Test.QuickCheck (Arbitrary(..), arbitrarySizedNatural, elements, oneof, vectorOf)
 
 import Crypto.JOSE.Error
@@ -296,11 +299,21 @@
     $ B.splitAt (B.length s `div` 2) s
 
 curve :: Crv -> ECC.Curve
-curve = ECC.getCurveByName . curveName where
-  curveName P_256 = ECC.SEC_p256r1
-  curveName P_384 = ECC.SEC_p384r1
-  curveName P_521 = ECC.SEC_p521r1
+curve = ECC.getCurveByName . review fromCurveName
 
+-- | Conversion from known curves and back again.
+fromCurveName :: Prism' ECC.CurveName Crv
+fromCurveName = prism'
+  (\case
+    P_256 -> ECC.SEC_p256r1
+    P_384 -> ECC.SEC_p384r1
+    P_521 -> ECC.SEC_p521r1)
+  (\case
+    ECC.SEC_p256r1 -> Just P_256
+    ECC.SEC_p384r1 -> Just P_384
+    ECC.SEC_p521r1 -> Just P_521
+    _              -> Nothing)
+
 point :: ECKeyParameters -> ECC.Point
 point k = ECC.Point (f ecX) (f ecY) where
   f l = case view l k of
@@ -315,6 +328,17 @@
 ecPrivateKey (ECKeyParameters _ _ _ (Just (Types.SizedBase64Integer _ d))) = pure d
 ecPrivateKey _ = throwing _KeyMismatch "Not an EC private key"
 
+ecParametersFromX509 :: X509.PubKeyEC -> Maybe ECKeyParameters
+ecParametersFromX509 pubKeyEC = do
+  ecCurve <- X509.EC.ecPubKeyCurve pubKeyEC
+  curveName <- X509.EC.ecPubKeyCurveName pubKeyEC
+  crv <- preview fromCurveName curveName
+  pt <- X509.EC.unserializePoint ecCurve (X509.pubkeyEC_pub pubKeyEC)
+  (x, y) <- case pt of
+    ECC.PointO    -> Nothing
+    ECC.Point x y ->
+      pure (Types.makeSizedBase64Integer x, Types.makeSizedBase64Integer y)
+  pure $ ECKeyParameters crv x y Nothing
 
 -- | Parameters for RSA Keys
 --
diff --git a/src/Crypto/JOSE/JWK.hs b/src/Crypto/JOSE/JWK.hs
--- a/src/Crypto/JOSE/JWK.hs
+++ b/src/Crypto/JOSE/JWK.hs
@@ -255,6 +255,12 @@
 fromRSAPublic :: RSA.PublicKey -> JWK
 fromRSAPublic = fromKeyMaterial . RSAKeyMaterial . toRSAPublicKeyParameters
 
+-- | Convert an EC public key into a JWK
+--
+fromECPublic :: X509.PubKeyEC -> Maybe JWK
+fromECPublic = fmap (fromKeyMaterial . ECKeyMaterial) . ecParametersFromX509
+
+
 -- | Convert octet string into a JWK
 --
 fromOctets :: Cons s s Word8 Word8 => s -> JWK
@@ -281,7 +287,8 @@
 fromX509CertificateMaybe cert = do
   k <- case (X509.certPubKey . X509.signedObject . X509.getSigned) cert of
     X509.PubKeyRSA k -> pure (fromRSAPublic k)
-    _ -> {- TODO EC -} Nothing
+    X509.PubKeyEC k -> fromECPublic k
+    _ -> Nothing
   pure $ k & set jwkX5cRaw (Just (pure cert))
 
 
diff --git a/src/Crypto/JOSE/Types.hs b/src/Crypto/JOSE/Types.hs
--- a/src/Crypto/JOSE/Types.hs
+++ b/src/Crypto/JOSE/Types.hs
@@ -25,6 +25,7 @@
     Base64Integer(..)
   , _Base64Integer
   , SizedBase64Integer(..)
+  , makeSizedBase64Integer
   , genSizedBase64IntegerOf
   , checkSize
   , Base64Octets(..)
@@ -127,6 +128,10 @@
 genSizedBase64IntegerOf :: Int -> Gen SizedBase64Integer
 genSizedBase64IntegerOf n =
   SizedBase64Integer n . bsToInteger <$> genByteStringOf n
+
+-- | Create a 'SizedBase64Integer'' from an 'Integer'.
+makeSizedBase64Integer :: Integer -> SizedBase64Integer
+makeSizedBase64Integer x = SizedBase64Integer (intBytes x) x
 
 instance FromJSON SizedBase64Integer where
   parseJSON = withText "full size base64url integer" $ parseB64Url (\bytes ->
diff --git a/test/Examples.hs b/test/Examples.hs
new file mode 100644
--- /dev/null
+++ b/test/Examples.hs
@@ -0,0 +1,70 @@
+-- | Miscellaneous end-to-end examples.
+{-# LANGUAGE OverloadedStrings #-}
+{-# LANGUAGE LambdaCase #-}
+module Examples where
+
+import Control.Lens (_Right, (&), (?~), (.~))
+import Control.Lens.Extras (is)
+import qualified Data.ByteString.Char8 as BC8
+import qualified Data.PEM as PEM
+import Data.Time.Clock.POSIX (posixSecondsToUTCTime)
+import qualified Data.X509 as X509
+
+import Crypto.JOSE.JWK
+import Crypto.JWT
+
+import Test.Hspec
+
+spec :: Spec
+spec = do
+  fromX509VerifySpec
+
+fromX509VerifySpec :: Spec
+fromX509VerifySpec =
+  describe "Decode PEM-encoded X509 EC certificate and verify" $ do
+    let certs = do
+          pems <- PEM.pemParseBS pemEncodedX509
+          mapM (X509.decodeSignedCertificate . PEM.pemContent) pems
+
+    it "successfully parses the single X509 certificate" $ do
+      certs `shouldSatisfy` \case Right [_] -> True; _ -> False
+
+    case certs of
+      Right [x509] -> do
+        let errorOrJWK = fromX509Certificate x509 :: Either JWTError JWK
+        it "successfully converts the X509 certificate to a JWK" $
+          errorOrJWK `shouldSatisfy` is _Right
+
+        it "verifies a token signed using ES256 and matches expected claims" $
+          (do
+            es256jwk <- errorOrJWK
+            jwt <- decodeCompact es256token
+            verifyClaimsAt valSettings es256jwk now jwt) `shouldBe`
+          Right expectedClaims
+
+      _ -> pure ()
+  where
+
+  es256token = "eyJ0eXAiOiAiSldUIiwgImFsZyI6ICJFUzI1NiJ9.eyJuYmYiOiAxNDQ0NDc4NDAwLCAiaXNzIjogInh4eCJ9.lArczfN-pIL8oUU-7PU83u-zfXougXBZj6drFeKFsPEoVhy9WAyiZlRshYqjTSXdaw8yw2L-ovt4zTUZb2PWMg"
+
+  pemEncodedX509 = BC8.unlines
+    [ "-----BEGIN CERTIFICATE-----"
+    , "MIIBcDCCARagAwIBAgIJAMZmuGSIfvgzMAoGCCqGSM49BAMCMBMxETAPBgNVBAMM"
+    , "CHdoYXRldmVyMB4XDTE4MDgxMDE0Mjg1NFoXDTE4MDkwOTE0Mjg1NFowEzERMA8G"
+    , "A1UEAwwId2hhdGV2ZXIwWTATBgcqhkjOPQIBBggqhkjOPQMBBwNCAATPwn3WCEXL"
+    , "mjp/bFniDwuwsfu7bASlPae2PyWhqGeWwe23Xlyx+tSqxlkXYe4pZ23BkAAscpGj"
+    , "yn5gXHExyDlKo1MwUTAdBgNVHQ4EFgQUElRjSoVgKjUqY5AXz2o74cLzzS8wHwYD"
+    , "VR0jBBgwFoAUElRjSoVgKjUqY5AXz2o74cLzzS8wDwYDVR0TAQH/BAUwAwEB/zAK"
+    , "BggqhkjOPQQDAgNIADBFAiEA4yQ/88ZrUX68c6kOe9G11u8NUaUzd8pLOtkKhniN"
+    , "OHoCIHmNX37JOqTcTzGn2u9+c8NlnvZ0uDvsd1BmKPaUmjmm"
+    , "-----END CERTIFICATE-----"
+    ]
+
+  now = posixSecondsToUTCTime 1588512349
+
+  expectedClaims = emptyClaimsSet
+    & claimIss ?~ "xxx"
+    & claimNbf ?~ NumericDate (posixSecondsToUTCTime 1444478400)
+
+  valSettings = defaultJWTValidationSettings (const True)
+    & jwtValidationSettingsIssuerPredicate .~ (== "xxx")
diff --git a/test/Test.hs b/test/Test.hs
--- a/test/Test.hs
+++ b/test/Test.hs
@@ -17,6 +17,7 @@
 import Test.Tasty.QuickCheck
 
 import AESKW
+import Examples
 import JWK
 import JWS
 import JWT
@@ -35,4 +36,5 @@
   jwk <- testSpec "JWK" JWK.spec
   jws <- testSpec "JWS" JWS.spec
   jwt <- testSpec "JWT" JWT.spec
-  return $ testGroup "Unit tests" [types, jwk, jws, jwt]
+  examples <- testSpec "Examples" Examples.spec
+  return $ testGroup "Unit tests" [types, jwk, jws, jwt, examples]
