keynub-licdongle (empty) → 1.1.1
raw patch · 7 files changed
+1212/−0 lines, 7 filesdep +Win32dep +basedep +bytestring
Dependencies added: Win32, base, bytestring, directory, filepath, keynub-licdongle, process, unix
Files
- CHANGELOG.md +3/−0
- LICENSE +201/−0
- README.md +82/−0
- keynub-licdongle.cabal +55/−0
- src/KeyNub/LicDongle.hs +436/−0
- src/KeyNub/LicDongle/Library.hs +252/−0
- test/StubTest.hs +183/−0
+ CHANGELOG.md view
@@ -0,0 +1,3 @@+## 1.1.1++- First release on Hackage, at the version of the SDK it belongs to.
+ LICENSE view
@@ -0,0 +1,201 @@+ Apache License+ Version 2.0, January 2004+ http://www.apache.org/licenses/++ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION++ 1. Definitions.++ "License" shall mean the terms and conditions for use, reproduction,+ and distribution as defined by Sections 1 through 9 of this document.++ "Licensor" shall mean the copyright owner or entity authorized by+ the copyright owner that is granting the License.++ "Legal Entity" shall mean the union of the acting entity and all+ other entities that control, are controlled by, or are under common+ control with that entity. For the purposes of this definition,+ "control" means (i) the power, direct or indirect, to cause the+ direction or management of such entity, whether by contract or+ otherwise, or (ii) ownership of fifty percent (50%) or more of the+ outstanding shares, or (iii) beneficial ownership of such entity.++ "You" (or "Your") shall mean an individual or Legal Entity+ exercising permissions granted by this License.++ "Source" form shall mean the preferred form for making modifications,+ including but not limited to software source code, documentation+ source, and configuration files.++ "Object" form shall mean any form resulting from mechanical+ transformation or translation of a Source form, including but+ not limited to compiled object code, generated documentation,+ and conversions to other media types.++ "Work" shall mean the work of authorship, whether in Source or+ Object form, made available under the License, as indicated by a+ copyright notice that is included in or attached to the work+ (an example is provided in the Appendix below).++ "Derivative Works" shall mean any work, whether in Source or Object+ form, that is based on (or derived from) the Work and for which the+ editorial revisions, annotations, elaborations, or other modifications+ represent, as a whole, an original work of authorship. For the purposes+ of this License, Derivative Works shall not include works that remain+ separable from, or merely link (or bind by name) to the interfaces of,+ the Work and Derivative Works thereof.++ "Contribution" shall mean any work of authorship, including+ the original version of the Work and any modifications or additions+ to that Work or Derivative Works thereof, that is intentionally+ submitted to Licensor for inclusion in the Work by the copyright owner+ or by an individual or Legal Entity authorized to submit on behalf of+ the copyright owner. For the purposes of this definition, "submitted"+ means any form of electronic, verbal, or written communication sent+ to the Licensor or its representatives, including but not limited to+ communication on electronic mailing lists, source code control systems,+ and issue tracking systems that are managed by, or on behalf of, the+ Licensor for the purpose of discussing and improving the Work, but+ excluding communication that is conspicuously marked or otherwise+ designated in writing by the copyright owner as "Not a Contribution."++ "Contributor" shall mean Licensor and any individual or Legal Entity+ on behalf of whom a Contribution has been received by Licensor and+ subsequently incorporated within the Work.++ 2. Grant of Copyright License. Subject to the terms and conditions of+ this License, each Contributor hereby grants to You a perpetual,+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable+ copyright license to reproduce, prepare Derivative Works of,+ publicly display, publicly perform, sublicense, and distribute the+ Work and such Derivative Works in Source or Object form.++ 3. Grant of Patent License. Subject to the terms and conditions of+ this License, each Contributor hereby grants to You a perpetual,+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable+ (except as stated in this section) patent license to make, have made,+ use, offer to sell, sell, import, and otherwise transfer the Work,+ where such license applies only to those patent claims licensable+ by such Contributor that are necessarily infringed by their+ Contribution(s) alone or by combination of their Contribution(s)+ with the Work to which such Contribution(s) was submitted. If You+ institute patent litigation against any entity (including a+ cross-claim or counterclaim in a lawsuit) alleging that the Work+ or a Contribution incorporated within the Work constitutes direct+ or contributory patent infringement, then any patent licenses+ granted to You under this License for that Work shall terminate+ as of the date such litigation is filed.++ 4. Redistribution. You may reproduce and distribute copies of the+ Work or Derivative Works thereof in any medium, with or without+ modifications, and in Source or Object form, provided that You+ meet the following conditions:++ (a) You must give any other recipients of the Work or+ Derivative Works a copy of this License; and++ (b) You must cause any modified files to carry prominent notices+ stating that You changed the files; and++ (c) You must retain, in the Source form of any Derivative Works+ that You distribute, all copyright, patent, trademark, and+ attribution notices from the Source form of the Work,+ excluding those notices that do not pertain to any part of+ the Derivative Works; and++ (d) If the Work includes a "NOTICE" text file as part of its+ distribution, then any Derivative Works that You distribute must+ include a readable copy of the attribution notices contained+ within such NOTICE file, excluding those notices that do not+ pertain to any part of the Derivative Works, in at least one+ of the following places: within a NOTICE text file distributed+ as part of the Derivative Works; within the Source form or+ documentation, if provided along with the Derivative Works; or,+ within a display generated by the Derivative Works, if and+ wherever such third-party notices normally appear. The contents+ of the NOTICE file are for informational purposes only and+ do not modify the License. You may add Your own attribution+ notices within Derivative Works that You distribute, alongside+ or as an addendum to the NOTICE text from the Work, provided+ that such additional attribution notices cannot be construed+ as modifying the License.++ You may add Your own copyright statement to Your modifications and+ may provide additional or different license terms and conditions+ for use, reproduction, or distribution of Your modifications, or+ for any such Derivative Works as a whole, provided Your use,+ reproduction, and distribution of the Work otherwise complies with+ the conditions stated in this License.++ 5. Submission of Contributions. Unless You explicitly state otherwise,+ any Contribution intentionally submitted for inclusion in the Work+ by You to the Licensor shall be under the terms and conditions of+ this License, without any additional terms or conditions.+ Notwithstanding the above, nothing herein shall supersede or modify+ the terms of any separate license agreement you may have executed+ with Licensor regarding such Contributions.++ 6. Trademarks. This License does not grant permission to use the trade+ names, trademarks, service marks, or product names of the Licensor,+ except as required for reasonable and customary use in describing the+ origin of the Work and reproducing the content of the NOTICE file.++ 7. Disclaimer of Warranty. Unless required by applicable law or+ agreed to in writing, Licensor provides the Work (and each+ Contributor provides its Contributions) on an "AS IS" BASIS,+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or+ implied, including, without limitation, any warranties or conditions+ of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A+ PARTICULAR PURPOSE. You are solely responsible for determining the+ appropriateness of using or redistributing the Work and assume any+ risks associated with Your exercise of permissions under this License.++ 8. Limitation of Liability. In no event and under no legal theory,+ whether in tort (including negligence), contract, or otherwise,+ unless required by applicable law (such as deliberate and grossly+ negligent acts) or agreed to in writing, shall any Contributor be+ liable to You for damages, including any direct, indirect, special,+ incidental, or consequential damages of any character arising as a+ result of this License or out of the use or inability to use the+ Work (including but not limited to damages for loss of goodwill,+ work stoppage, computer failure or malfunction, or any and all+ other commercial damages or losses), even if such Contributor+ has been advised of the possibility of such damages.++ 9. Accepting Warranty or Additional Liability. While redistributing+ the Work or Derivative Works thereof, You may choose to offer,+ and charge a fee for, acceptance of support, warranty, indemnity,+ or other liability obligations and/or rights consistent with this+ License. However, in accepting such obligations, You may act only+ on Your own behalf and on Your sole responsibility, not on behalf+ of any other Contributor, and only if You agree to indemnify,+ defend, and hold each Contributor harmless for any liability+ incurred by, or claims asserted against, such Contributor by reason+ of your accepting any such warranty or additional liability.++ END OF TERMS AND CONDITIONS++ APPENDIX: How to apply the Apache License to your work.++ To apply the Apache License to your work, attach the following+ boilerplate notice, with the fields enclosed by brackets "[]"+ replaced with your own identifying information. (Don't include+ the brackets!) The text should be enclosed in the appropriate+ comment syntax for the file format. We also recommend that a+ file or class name and description of purpose be included on the+ same "printed page" as the copyright notice for easier+ identification within third-party archives.++ Copyright [yyyy] [name of copyright owner]++ Licensed under the Apache License, Version 2.0 (the "License");+ you may not use this file except in compliance with the License.+ You may obtain a copy of the License at++ http://www.apache.org/licenses/LICENSE-2.0++ Unless required by applicable law or agreed to in writing, software+ distributed under the License is distributed on an "AS IS" BASIS,+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.+ See the License for the specific language governing permissions and+ limitations under the License.
+ README.md view
@@ -0,0 +1,82 @@+# KeyNub License Dongle — Haskell package++```haskell+import KeyNub.LicDongle++main :: IO ()+main = withDongle open $ \d -> do -- first dongle, or openSerial "..."+ _ <- verifyGenuine d -- throws unless genuine+ secret <- withSession d $ -- closed on every exit path+ appDecrypt d sealed -- <- build the licence check on this+ ...+```++**Pure Haskell.** The package calls the SDK's flat companion API through+function pointers resolved at run time, so nothing is linked at build time,+there is no C to compile and nothing sits in the path of the check that a+customer could substitute. `base`, `bytestring`, `directory`, `filepath` and+the platform's loader package (`unix`, or `Win32` on Windows) are the+dependencies. GHC 9.4 or later, on Windows, Linux and macOS.++## Setup++```+cabal install keynub-licdongle+```++or `keynub-licdongle` in `build-depends`. The package does not carry the+native library. Take `keynub_licdongle_flat` for your platform from the SDK's+[natives folder](https://github.com/AB-KeyNub/KeyNub-SDK/blob/master/NATIVES.md)+and either put it where the operating system finds libraries (next to the+executable, or on `PATH`, `LD_LIBRARY_PATH`, `DYLD_LIBRARY_PATH`), or name it+before the first call:++```haskell+setLibraryPath "/opt/keynub/libkeynub_licdongle_flat.so"+```++`KEYNUB_LICDONGLE_FLAT_LIBRARY` in the environment does the same. In a clone+of the SDK repository the package finds `natives/<platform>/` on its own, from+the working directory upwards, so the samples run with nothing set. A process+loads the library once; `loadedLibraryPath` tells which. On Linux, install the+udev rule described in+[`NATIVES.md`](https://github.com/AB-KeyNub/KeyNub-SDK/blob/master/NATIVES.md)+so the dongle is accessible without root.++## Notes++- Results are records (`Info`, `GenuineResult`, `Device`, `RecordInfo`); byte+ data is `ByteString`. The package calls the SDK's flat API: integer handles+ and caller-provided buffers, no C structures and no hand-written layouts.+- Failures throw `LicDongleError` with `errorStatus` (`NoDevice`,+ `NotGenuine`, `AuthRequired`, ...), `errorCode`, `errorOperation` and+ `errorDetail`; loading problems throw `LibraryError`.+- `isGenuine` is the non-throwing form for a gate and **fails closed**: every+ failure gives `False`.+- `withDongle` and `withSession` are `bracket`s: the dongle and the session+ are closed on every exit path, exceptions included.+- `eraseAllRecords` is deliberately separate from `eraseRecord`: an+ accidentally empty name must not wipe the dongle.+- Records are transferred in one call; the flat API has no progress+ reporting.++> Read [`docs/integration-security.md`](https://github.com/AB-KeyNub/KeyNub-SDK/blob/master/docs/integration-security.md)+> before writing the check. `ok <- isGenuine d; unless ok exitFailure` is one+> conditional branch, and patching one of those in a release binary is a+> beginner exercise. Route something the program needs through `appEncrypt` and+> `appDecrypt`, so removing the check removes the data.++## Tests++`cabal test` runs without a dongle: it compiles a stand-in for the flat C API+(`bindings/flat/licd_flat.c` over `bindings/julia/test/stub/licd_stub.c`) with+the C compiler on the path and exercises every call against it. In a package+taken from Hackage the SDK sources are not present; point `KEYNUB_SDK_ROOT` at+a clone, or `KEYNUB_LICDONGLE_FLAT_LIBRARY` at a compiled stand-in.++## Links++- [KeyNub License Dongle for Haskell](https://www.keynub.com/developers/haskell/): the product, and how to+ order one+- [Source, samples and issue tracker](https://github.com/AB-KeyNub/KeyNub-SDK) on GitHub+- [Native library for your platform](https://github.com/AB-KeyNub/KeyNub-SDK/blob/master/NATIVES.md)
+ keynub-licdongle.cabal view
@@ -0,0 +1,55 @@+cabal-version: 3.0+name: keynub-licdongle+version: 1.1.1+synopsis: Client for the KeyNub USB license dongle+description:+ Verify that a KeyNub USB license dongle is genuine, read and write the+ license records it stores, use its hardware counters and encrypt data so+ that only a dongle can decrypt it. Pure Haskell over the SDK's flat C API:+ the native library is loaded at run time, nothing is linked.+homepage: https://www.keynub.com/developers/haskell/+bug-reports: https://github.com/AB-KeyNub/KeyNub-SDK/issues+license: Apache-2.0+license-file: LICENSE+author: KeyNub+maintainer: info@keynub.com+copyright: 2026 KeyNub+category: Hardware+build-type: Simple+extra-doc-files: README.md+ CHANGELOG.md+tested-with: GHC == 9.12.4++source-repository head+ type: git+ location: https://github.com/AB-KeyNub/KeyNub-SDK.git+ subdir: bindings/haskell++common shared+ default-language: Haskell2010+ ghc-options: -Wall+ build-depends: base >= 4.14 && < 5++library+ import: shared+ hs-source-dirs: src+ exposed-modules: KeyNub.LicDongle+ KeyNub.LicDongle.Library+ build-depends: bytestring >= 0.10 && < 0.13,+ directory >= 1.3 && < 1.4,+ filepath >= 1.4 && < 1.6+ if os(windows)+ build-depends: Win32 >= 2.6 && < 2.15+ else+ build-depends: unix >= 2.7 && < 2.9++test-suite stub+ import: shared+ type: exitcode-stdio-1.0+ hs-source-dirs: test+ main-is: StubTest.hs+ build-depends: keynub-licdongle,+ bytestring,+ directory,+ filepath,+ process >= 1.6 && < 1.7
+ src/KeyNub/LicDongle.hs view
@@ -0,0 +1,436 @@+{-# LANGUAGE ScopedTypeVariables #-}+-- | KeyNub License Dongle for Haskell.+--+-- Calls the SDK's flat companion API (@keynub_licdongle_flat@) through+-- function pointers resolved at run time, so nothing is linked at build time+-- and the package is pure Haskell. Handles are 'Dongle' values; byte data is+-- 'ByteString'; every failure is a 'LicDongleError' exception carrying the+-- library's status code, the operation and its diagnostic detail.+--+-- @+-- import KeyNub.LicDongle+--+-- main = withDongle open $ \\d -> do+-- _ <- verifyGenuine d -- throws unless genuine+-- secret <- withSession d $ appDecrypt d sealed -- build the licence check on this+-- ...+-- @+module KeyNub.LicDongle+ ( -- * The native library+ libraryVersion+ , libraryPath+ , loadedLibraryPath+ , setLibraryPath+ , statusText+ -- * Failures+ , Status(..)+ , statusFromCode+ , LicDongleError(..)+ , LibraryError(..)+ -- * Dongles+ , Device(..)+ , devices+ , Dongle+ , open+ , openSerial+ , openPath+ , close+ , withDongle+ , serial+ , Info(..)+ , info+ , lastErrorDetail+ -- * Authenticity+ , GenuineResult(..)+ , verifyGenuine+ , isGenuine+ , setTrustRoot+ -- * Session+ , sessionOpen+ , sessionClose+ , withSession+ , authorizeWrite+ , rotateWriteKey+ -- * Records+ , RecordInfo(..)+ , records+ , readRecord+ , writeRecord+ , eraseRecord+ , eraseAllRecords+ -- * Counters+ , readCounter+ , incrementCounter+ -- * Application data+ , Scope(..)+ , appEncrypt+ , appDecrypt+ ) where++import Control.Exception (Exception(..), SomeException, bracket, bracket_, catch, throwIO)+import Control.Monad (forM, when)+import Data.Bits ((.&.))+import Data.ByteString (ByteString)+import qualified Data.ByteString as BS+import Data.Word (Word8)+import Foreign.C.String (CString, peekCString, withCString)+import Foreign.C.Types (CInt(..))+import Foreign.Marshal.Alloc (alloca, allocaBytes)+import Foreign.Marshal.Array (advancePtr, allocaArray, peekArray)+import Foreign.Ptr (Ptr, castPtr, nullPtr)+import Foreign.Storable (peek, poke, pokeByteOff)++import KeyNub.LicDongle.Library++-- | The library's status codes, by name.+data Status+ = InvalidArg | NoDevice | AccessDenied | IOFailure | Timeout | ProtocolError+ | NotGenuine | CertInvalid | SessionExpired | TagMismatch | Range | StorageFull+ | Busy | NotFound | AuthRequired | FirmwareIncompatible | SdkTooOld | Cancelled+ | NotImplemented | Internal+ | Unknown Int -- ^ a code this binding does not know+ deriving (Eq, Show)++-- | The 'Status' for a raw status code.+statusFromCode :: Int -> Status+statusFromCode c = case c of+ -1 -> InvalidArg; -2 -> NoDevice; -3 -> AccessDenied; -4 -> IOFailure+ -5 -> Timeout; -6 -> ProtocolError; -7 -> NotGenuine; -8 -> CertInvalid+ -9 -> SessionExpired; -10 -> TagMismatch; -11 -> Range; -12 -> StorageFull+ -13 -> Busy; -14 -> NotFound; -15 -> AuthRequired; -16 -> FirmwareIncompatible+ -17 -> SdkTooOld; -18 -> Cancelled; -19 -> NotImplemented; -20 -> Internal+ _ -> Unknown c++-- | A failed call: the status, its raw code, the C function that failed and+-- the library's diagnostic detail for it (often empty).+data LicDongleError = LicDongleError+ { errorStatus :: Status+ , errorCode :: Int+ , errorOperation :: String+ , errorDetail :: String+ } deriving (Eq, Show)++instance Exception LicDongleError where+ displayException e =+ errorOperation e ++ ": " ++ show (errorStatus e) ++ " (" ++ show (errorCode e) ++ ")"+ ++ (if null (errorDetail e) then "" else ": " ++ errorDetail e)++-- | An attached dongle, as listed by 'devices'.+data Device = Device+ { deviceSerial :: String -- ^ the serial, as hex+ , devicePath :: String -- ^ the device path 'openPath' takes+ } deriving (Eq, Show)++-- | An open dongle. The library holds up to 32 at a time; release them with+-- 'close' or use 'withDongle'.+newtype Dongle = Dongle CInt+ deriving (Eq, Show)++-- | Plaintext device information, read without a session.+data Info = Info+ { protocolVersion :: (Int, Int)+ , firmwareVersion :: (Int, Int, Int)+ , secureElementReady :: Bool+ , provisioned :: Bool+ , watchdogReboot :: Bool -- ^ the previous boot ended in a watchdog reset+ , isolated :: Bool+ , writeAuthRotated :: Bool -- ^ 'rotateWriteKey' has replaced the shipped write key+ , dataCapacity :: Int -- ^ record storage, bytes+ , dataFree :: Int+ } deriving (Eq, Show)++-- | What a genuine dongle proves: the serial from its certificate and the day+-- it was personalised (@YYYY-MM-DD@, or empty).+data GenuineResult = GenuineResult+ { genuineSerial :: String+ , provisionedDate :: String+ } deriving (Eq, Show)++-- | A record on the dongle.+data RecordInfo = RecordInfo+ { recordName :: String+ , recordSize :: Int+ } deriving (Eq, Show)++-- | Who can decrypt data sealed with 'appEncrypt'.+data Scope+ = DeviceScope -- ^ only the dongle that sealed it+ | DeveloperScope -- ^ any dongle issued to the same developer+ deriving (Eq, Show)++serialSize, dateSize, pathSize, errorSize :: Int+serialSize = 15+dateSize = 11+pathSize = 512+errorSize = 256++rangeCode :: CInt+rangeCode = -11++-- Failure handling: every flat call returns 0 or a negative status.+check :: Maybe Dongle -> String -> CInt -> IO ()+check h op rc+ | rc == 0 = pure ()+ | otherwise = do+ detail <- maybe (pure "") lastErrorDetail h+ throwIO (LicDongleError (statusFromCode (fromIntegral rc)) (fromIntegral rc) op detail)++withOutInt :: (Ptr CInt -> IO CInt) -> IO (CInt, CInt)+withOutInt f = alloca $ \p -> do+ poke p 0+ rc <- f p+ v <- peek p+ pure (rc, v)++-- A NUL-terminated string into a caller buffer of the given capacity.+readString :: Int -> (CString -> CInt -> IO CInt) -> IO (CInt, String)+readString cap f = allocaBytes cap $ \buf -> do+ pokeByteOff buf 0 (0 :: Word8)+ rc <- f buf (fromIntegral cap)+ s <- if rc == 0 then peekCString buf else pure ""+ pure (rc, s)++-- Bytes of unknown length: ask with a capacity of 0, the library answers+-- Range and the size needed, then read into a buffer of that size.+readBytes :: Maybe Dongle -> String -> (Ptr Word8 -> CInt -> Ptr CInt -> IO CInt) -> IO ByteString+readBytes h op f = do+ (rc0, need) <- withOutInt (f nullPtr 0)+ if rc0 == 0+ then pure BS.empty+ else if rc0 /= rangeCode+ then check h op rc0 >> pure BS.empty+ else allocaBytes (fromIntegral need) $ \buf -> do+ (rc, len) <- withOutInt (f buf need)+ check h op rc+ BS.packCStringLen (castPtr buf, fromIntegral len)++useBytes :: ByteString -> (Ptr Word8 -> CInt -> IO a) -> IO a+useBytes bs f = BS.useAsCStringLen bs $ \(p, n) -> f (castPtr p) (fromIntegral n)++-- | @(major, minor, patch)@ of the loaded native library; loads it if no other+-- call has.+libraryVersion :: IO (Int, Int, Int)+libraryVersion = do+ a <- api+ allocaArray 3 $ \arr -> do+ rc <- fVersion a arr (advancePtr arr 1) (advancePtr arr 2)+ check Nothing "licdf_version" rc+ [x, y, z] <- map fromIntegral <$> peekArray 3 arr+ pure (x, y, z)++-- | The library's text for a status code.+statusText :: Int -> IO String+statusText code = do+ a <- api+ snd <$> readString errorSize (fStrerror a (fromIntegral code))++-- | The library's diagnostic detail for the most recent failure on a dongle.+lastErrorDetail :: Dongle -> IO String+lastErrorDetail (Dongle h) = do+ a <- api+ snd <$> readString errorSize (fLastError a h)++-- | The attached dongles.+devices :: IO [Device]+devices = do+ a <- api+ (rc, n) <- withOutInt (fDeviceCount a)+ check Nothing "licdf_device_count" rc+ forM [0 .. n - 1] $ \i -> do+ (rc1, s) <- readString pathSize (fDeviceSerial a i)+ check Nothing "licdf_device_serial" rc1+ (rc2, p) <- readString pathSize (fDevicePath a i)+ check Nothing "licdf_device_path" rc2+ pure (Device s p)++-- | Opens the first dongle.+open :: IO Dongle+open = openSerial ""++-- | Opens the dongle with that serial (an empty serial means the first one).+openSerial :: String -> IO Dongle+openSerial s = do+ a <- api+ h <- withCString s (fOpen a)+ handleOf "licdf_open" h++-- | Opens the dongle at a device path from 'devices'.+openPath :: String -> IO Dongle+openPath p = do+ a <- api+ h <- withCString p (fOpenPath a)+ handleOf "licdf_open_path" h++handleOf :: String -> CInt -> IO Dongle+handleOf op h+ | h > 0 = pure (Dongle h)+ | otherwise = throwIO (LicDongleError (statusFromCode (fromIntegral h)) (fromIntegral h) op "")++-- | Releases a dongle; its session ends with it.+close :: Dongle -> IO ()+close (Dongle h) = do+ a <- api+ fClose a h >>= check Nothing "licdf_close"++-- | Opens a dongle, runs the action, and closes the dongle on every exit path.+withDongle :: IO Dongle -> (Dongle -> IO a) -> IO a+withDongle acquire = bracket acquire close++-- | The dongle's serial, as hex.+serial :: Dongle -> IO String+serial d@(Dongle h) = do+ a <- api+ (rc, s) <- readString serialSize (fGetSerial a h)+ check (Just d) "licdf_get_serial" rc+ pure s++-- | Plaintext device information.+info :: Dongle -> IO Info+info d@(Dongle h) = do+ a <- api+ allocaArray 8 $ \arr -> do+ let p i = advancePtr arr i+ rc <- fGetInfo a h (p 0) (p 1) (p 2) (p 3) (p 4) (p 5) (p 6) (p 7)+ check (Just d) "licdf_get_info" rc+ v <- map fromIntegral <$> peekArray 8 arr+ let flags = v !! 5 :: Int+ flag b = flags .&. b /= 0+ pure Info+ { protocolVersion = (v !! 0, v !! 1)+ , firmwareVersion = (v !! 2, v !! 3, v !! 4)+ , secureElementReady = flag 0x01+ , provisioned = flag 0x02+ , watchdogReboot = flag 0x04+ , isolated = flag 0x08+ , writeAuthRotated = flag 0x10+ , dataCapacity = v !! 6+ , dataFree = v !! 7+ }++-- | Proves that the dongle is genuine: certificate chain to the trusted root+-- and a live challenge-response. Throws unless it is.+verifyGenuine :: Dongle -> IO GenuineResult+verifyGenuine d@(Dongle h) = do+ a <- api+ alloca $ \pg -> allocaBytes serialSize $ \ps -> allocaBytes dateSize $ \pd -> do+ poke pg 0+ pokeByteOff ps 0 (0 :: Word8)+ pokeByteOff pd 0 (0 :: Word8)+ rc <- fVerifyGenuine a h pg ps (fromIntegral serialSize) pd (fromIntegral dateSize)+ check (Just d) "licdf_verify_genuine" rc+ g <- peek pg+ when (g == 0) $ throwIO (LicDongleError NotGenuine (-7) "licdf_verify_genuine" "")+ GenuineResult <$> peekCString ps <*> peekCString pd++-- | 'True' only when the dongle proves genuine. Every failure, of any kind,+-- gives 'False'.+isGenuine :: Dongle -> IO Bool+isGenuine d = (verifyGenuine d >> pure True) `catch` \(_ :: SomeException) -> pure False++-- | Replaces the CA root that 'verifyGenuine' checks against (a certificate in+-- DER form). Applications do not need this.+setTrustRoot :: Dongle -> ByteString -> IO ()+setTrustRoot d@(Dongle h) der = do+ a <- api+ useBytes der (fSetTrustRoot a h) >>= check (Just d) "licdf_set_trust_root"++-- | Opens the encrypted session that records, counters and application-data+-- encryption need.+sessionOpen :: Dongle -> IO ()+sessionOpen d@(Dongle h) = do+ a <- api+ fSessionOpen a h >>= check (Just d) "licdf_session_open"++-- | Ends the session; the write role ends with it.+sessionClose :: Dongle -> IO ()+sessionClose d@(Dongle h) = do+ a <- api+ fSessionClose a h >>= check (Just d) "licdf_session_close"++-- | Runs the action inside a session, closing it on every exit path.+withSession :: Dongle -> IO a -> IO a+withSession d = bracket_ (sessionOpen d) (sessionClose d)++-- | Elevates the session to the write role with the dongle's write key, a+-- P-256 private key in PKCS#8 DER form.+authorizeWrite :: Dongle -> ByteString -> IO ()+authorizeWrite d@(Dongle h) key = do+ a <- api+ useBytes key (fWriteAuth a h) >>= check (Just d) "licdf_write_auth"++-- | Replaces the dongle's write key with the given one (P-256, PKCS#8 DER).+-- From the next session on, only that key elevates. Needs the write role.+rotateWriteKey :: Dongle -> ByteString -> IO ()+rotateWriteKey d@(Dongle h) key = do+ a <- api+ useBytes key (fWriteAuthRotate a h) >>= check (Just d) "licdf_write_auth_rotate"++-- | The records on the dongle. Needs a session.+records :: Dongle -> IO [RecordInfo]+records d@(Dongle h) = do+ a <- api+ (rc, n) <- withOutInt (fRecordCount a h)+ check (Just d) "licdf_record_count" rc+ forM [0 .. n - 1] $ \i -> allocaBytes pathSize $ \buf -> alloca $ \psz -> do+ pokeByteOff buf 0 (0 :: Word8)+ poke psz 0+ rc1 <- fRecordName a h i buf (fromIntegral pathSize) psz+ check (Just d) "licdf_record_name" rc1+ RecordInfo <$> peekCString buf <*> (fromIntegral <$> peek psz)++-- | Reads a record. Needs a session.+readRecord :: Dongle -> String -> IO ByteString+readRecord d@(Dongle h) name = do+ a <- api+ withCString name $ \cn -> readBytes (Just d) "licdf_record_read" (fRecordRead a h cn)++-- | Replaces (or creates) a record. Needs the write role.+writeRecord :: Dongle -> String -> ByteString -> IO ()+writeRecord d@(Dongle h) name bytes = do+ a <- api+ withCString name $ \cn -> useBytes bytes (fRecordWrite a h cn) >>= check (Just d) "licdf_record_write"++-- | Erases one record. Needs the write role. An empty name is refused; use+-- 'eraseAllRecords' to erase everything.+eraseRecord :: Dongle -> String -> IO ()+eraseRecord d@(Dongle h) name = do+ a <- api+ withCString name (fRecordErase a h) >>= check (Just d) "licdf_record_erase"++-- | Erases every record. Needs the write role.+eraseAllRecords :: Dongle -> IO ()+eraseAllRecords d@(Dongle h) = do+ a <- api+ fRecordEraseAll a h >>= check (Just d) "licdf_record_erase_all"++-- | Reads a hardware monotonic counter (0 or 1). Needs a session.+readCounter :: Dongle -> Int -> IO Int+readCounter d@(Dongle h) i = do+ a <- api+ (rc, v) <- withOutInt (fCounterRead a h (fromIntegral i))+ check (Just d) "licdf_counter_read" rc+ pure (fromIntegral v)++-- | Increments a counter, irreversibly, and gives the new value. Needs the+-- write role.+incrementCounter :: Dongle -> Int -> IO Int+incrementCounter d@(Dongle h) i = do+ a <- api+ (rc, v) <- withOutInt (fCounterIncrement a h (fromIntegral i))+ check (Just d) "licdf_counter_increment" rc+ pure (fromIntegral v)++-- | Seals data so that only a dongle can open it. Needs a session. This is the+-- pair to build a licence check on: route something the program needs through+-- it, so that removing the check removes the data.+appEncrypt :: Dongle -> Scope -> ByteString -> IO ByteString+appEncrypt d@(Dongle h) scope plain = do+ a <- api+ let code = case scope of DeviceScope -> 0; DeveloperScope -> 1+ useBytes plain $ \pp n -> readBytes (Just d) "licdf_app_encrypt" (fAppEncrypt a h code pp n)++-- | Opens data sealed with 'appEncrypt'. Needs a session.+appDecrypt :: Dongle -> ByteString -> IO ByteString+appDecrypt d@(Dongle h) packed = do+ a <- api+ useBytes packed $ \pp n -> readBytes (Just d) "licdf_app_decrypt" (fAppDecrypt a h pp n)
+ src/KeyNub/LicDongle/Library.hs view
@@ -0,0 +1,252 @@+{-# LANGUAGE CPP #-}+{-# LANGUAGE ForeignFunctionInterface #-}+{-# LANGUAGE ScopedTypeVariables #-}+-- | The native library: found, loaded once per process, and its functions+-- resolved by name into an 'Api' table. Applications use "KeyNub.LicDongle";+-- this module is exposed for the two path functions and the error type.+module KeyNub.LicDongle.Library+ ( Api(..)+ , api+ , setLibraryPath+ , libraryPath+ , loadedLibraryPath+ , LibraryError(..)+ ) where++import Control.Concurrent.MVar+import Control.Exception (Exception, SomeException, throwIO, try)+import Data.Word (Word8)+import Foreign.C.String (CString)+import Foreign.C.Types (CInt(..))+import Foreign.Ptr (FunPtr, Ptr, castFunPtr)+import System.Directory (doesFileExist, getCurrentDirectory)+import System.Environment (lookupEnv)+import System.FilePath ((</>), takeDirectory)+import System.IO.Unsafe (unsafePerformIO)+import qualified System.Info++#if defined(mingw32_HOST_OS)+import Foreign.Ptr (castPtrToFunPtr)+import System.Win32.DLL (getProcAddress, loadLibrary)+import System.Win32.Types (HMODULE)+#else+import System.Posix.DynamicLinker (DL, RTLDFlags(..), dlopen, dlsym)+#endif++-- | Loading problems: no library found, or a library without the expected+-- functions.+newtype LibraryError = LibraryError String+ deriving (Eq, Show)++instance Exception LibraryError++-- Every shape of function the flat API has.+type F_ppp = Ptr CInt -> Ptr CInt -> Ptr CInt -> IO CInt+type F_p = Ptr CInt -> IO CInt+type F_isi = CInt -> CString -> CInt -> IO CInt+type F_s = CString -> IO CInt+type F_i = CInt -> IO CInt+type F_ibi = CInt -> Ptr Word8 -> CInt -> IO CInt+type F_info = CInt -> Ptr CInt -> Ptr CInt -> Ptr CInt -> Ptr CInt -> Ptr CInt+ -> Ptr CInt -> Ptr CInt -> Ptr CInt -> IO CInt+type F_genuine = CInt -> Ptr CInt -> CString -> CInt -> CString -> CInt -> IO CInt+type F_ip = CInt -> Ptr CInt -> IO CInt+type F_iip = CInt -> CInt -> Ptr CInt -> IO CInt+type F_name = CInt -> CInt -> CString -> CInt -> Ptr CInt -> IO CInt+type F_isp = CInt -> CString -> Ptr CInt -> IO CInt+type F_read = CInt -> CString -> Ptr Word8 -> CInt -> Ptr CInt -> IO CInt+type F_write = CInt -> CString -> Ptr Word8 -> CInt -> IO CInt+type F_is = CInt -> CString -> IO CInt+type F_enc = CInt -> CInt -> Ptr Word8 -> CInt -> Ptr Word8 -> CInt -> Ptr CInt -> IO CInt+type F_dec = CInt -> Ptr Word8 -> CInt -> Ptr Word8 -> CInt -> Ptr CInt -> IO CInt++foreign import ccall "dynamic" mk_ppp :: FunPtr F_ppp -> F_ppp+foreign import ccall "dynamic" mk_p :: FunPtr F_p -> F_p+foreign import ccall "dynamic" mk_isi :: FunPtr F_isi -> F_isi+foreign import ccall "dynamic" mk_s :: FunPtr F_s -> F_s+foreign import ccall "dynamic" mk_i :: FunPtr F_i -> F_i+foreign import ccall "dynamic" mk_ibi :: FunPtr F_ibi -> F_ibi+foreign import ccall "dynamic" mk_info :: FunPtr F_info -> F_info+foreign import ccall "dynamic" mk_genuine :: FunPtr F_genuine -> F_genuine+foreign import ccall "dynamic" mk_ip :: FunPtr F_ip -> F_ip+foreign import ccall "dynamic" mk_iip :: FunPtr F_iip -> F_iip+foreign import ccall "dynamic" mk_name :: FunPtr F_name -> F_name+foreign import ccall "dynamic" mk_isp :: FunPtr F_isp -> F_isp+foreign import ccall "dynamic" mk_read :: FunPtr F_read -> F_read+foreign import ccall "dynamic" mk_write :: FunPtr F_write -> F_write+foreign import ccall "dynamic" mk_is :: FunPtr F_is -> F_is+foreign import ccall "dynamic" mk_enc :: FunPtr F_enc -> F_enc+foreign import ccall "dynamic" mk_dec :: FunPtr F_dec -> F_dec++-- | The flat API's functions, resolved from the loaded library.+data Api = Api+ { fVersion :: F_ppp+ , fDeviceCount :: F_p+ , fDeviceSerial :: F_isi+ , fDevicePath :: F_isi+ , fOpen :: F_s+ , fOpenPath :: F_s+ , fClose :: F_i+ , fSetTrustRoot :: F_ibi+ , fGetSerial :: F_isi+ , fGetInfo :: F_info+ , fVerifyGenuine :: F_genuine+ , fSessionOpen :: F_i+ , fSessionClose :: F_i+ , fWriteAuth :: F_ibi+ , fWriteAuthRotate :: F_ibi+ , fRecordCount :: F_ip+ , fRecordName :: F_name+ , fRecordSize :: F_isp+ , fRecordRead :: F_read+ , fRecordWrite :: F_write+ , fRecordErase :: F_is+ , fRecordEraseAll :: F_i+ , fCounterRead :: F_iip+ , fCounterIncrement :: F_iip+ , fAppEncrypt :: F_enc+ , fAppDecrypt :: F_dec+ , fStrerror :: F_isi+ , fLastError :: F_isi+ }++data State = State+ { chosen :: Maybe FilePath -- named with setLibraryPath, not yet loaded+ , loaded :: Maybe (FilePath, Api)+ }++{-# NOINLINE stateVar #-}+stateVar :: MVar State+stateVar = unsafePerformIO (newMVar (State Nothing Nothing))++-- | Names the native library to load. Call it before the first call; once a+-- library is loaded, naming a different one throws 'LibraryError'.+setLibraryPath :: FilePath -> IO ()+setLibraryPath p = modifyMVar_ stateVar $ \st -> case loaded st of+ Just (lp, _) | lp /= p ->+ throwIO (LibraryError ("the KeyNub library is already loaded from " ++ lp+ ++ "; a process loads it once"))+ _ -> pure st { chosen = Just p }++-- | The library in use, or the one the next call would load.+libraryPath :: IO FilePath+libraryPath = do+ st <- readMVar stateVar+ case loaded st of+ Just (lp, _) -> pure lp+ Nothing -> resolve (chosen st)++-- | The library in use, once one is loaded.+loadedLibraryPath :: IO (Maybe FilePath)+loadedLibraryPath = fmap fst . loaded <$> readMVar stateVar++-- | The function table, loading the library on the first call.+api :: IO Api+api = modifyMVar stateVar $ \st -> case loaded st of+ Just (_, a) -> pure (st, a)+ Nothing -> do+ p <- resolve (chosen st)+ a <- load p+ pure (st { loaded = Just (p, a) }, a)++-- The path given to setLibraryPath, then KEYNUB_LICDONGLE_FLAT_LIBRARY, then+-- natives/<platform>/ from the working directory upwards (a clone of the SDK+-- repository), then the bare name for the system loader to find.+resolve :: Maybe FilePath -> IO FilePath+resolve (Just p) = pure p+resolve Nothing = do+ env <- lookupEnv "KEYNUB_LICDONGLE_FLAT_LIBRARY"+ case env of+ Just p | not (null p) -> pure p+ _ -> do+ found <- searchNatives+ pure (maybe basename id found)++basename :: FilePath+basename = case System.Info.os of+ "mingw32" -> "keynub_licdongle_flat.dll"+ "darwin" -> "libkeynub_licdongle_flat.dylib"+ _ -> "libkeynub_licdongle_flat.so"++-- The natives/<platform> folder name of the SDK repository for this process.+platformFolder :: Maybe String+platformFolder = case (System.Info.os, System.Info.arch) of+ ("mingw32", "x86_64") -> Just "win-x64"+ ("mingw32", "i386") -> Just "win-x86"+ ("mingw32", "aarch64") -> Just "win-arm64"+ ("linux", "x86_64") -> Just "linux-x64"+ ("linux", "aarch64") -> Just "linux-arm64"+ ("darwin", "x86_64") -> Just "osx-x64"+ ("darwin", "aarch64") -> Just "osx-arm64"+ _ -> Nothing++searchNatives :: IO (Maybe FilePath)+searchNatives = case platformFolder of+ Nothing -> pure Nothing+ Just rid -> getCurrentDirectory >>= go rid+ where+ go rid dir = do+ let candidate = dir </> "natives" </> rid </> basename+ present <- doesFileExist candidate+ if present+ then pure (Just candidate)+ else let parent = takeDirectory dir+ in if parent == dir then pure Nothing else go rid parent++load :: FilePath -> IO Api+load path = do+ sym <- opener path+ let get :: String -> IO (FunPtr a)+ get name = castFunPtr <$> sym name+ Api <$> (mk_ppp <$> get "licdf_version")+ <*> (mk_p <$> get "licdf_device_count")+ <*> (mk_isi <$> get "licdf_device_serial")+ <*> (mk_isi <$> get "licdf_device_path")+ <*> (mk_s <$> get "licdf_open")+ <*> (mk_s <$> get "licdf_open_path")+ <*> (mk_i <$> get "licdf_close")+ <*> (mk_ibi <$> get "licdf_set_trust_root")+ <*> (mk_isi <$> get "licdf_get_serial")+ <*> (mk_info <$> get "licdf_get_info")+ <*> (mk_genuine <$> get "licdf_verify_genuine")+ <*> (mk_i <$> get "licdf_session_open")+ <*> (mk_i <$> get "licdf_session_close")+ <*> (mk_ibi <$> get "licdf_write_auth")+ <*> (mk_ibi <$> get "licdf_write_auth_rotate")+ <*> (mk_ip <$> get "licdf_record_count")+ <*> (mk_name <$> get "licdf_record_name")+ <*> (mk_isp <$> get "licdf_record_size")+ <*> (mk_read <$> get "licdf_record_read")+ <*> (mk_write <$> get "licdf_record_write")+ <*> (mk_is <$> get "licdf_record_erase")+ <*> (mk_i <$> get "licdf_record_erase_all")+ <*> (mk_iip <$> get "licdf_counter_read")+ <*> (mk_iip <$> get "licdf_counter_increment")+ <*> (mk_enc <$> get "licdf_app_encrypt")+ <*> (mk_dec <$> get "licdf_app_decrypt")+ <*> (mk_isi <$> get "licdf_strerror")+ <*> (mk_isi <$> get "licdf_last_error")++-- Opens the library and gives a symbol lookup over it.+opener :: FilePath -> IO (String -> IO (FunPtr ()))+#if defined(mingw32_HOST_OS)+opener path = do+ r <- try (loadLibrary path) :: IO (Either SomeException HMODULE)+ handle <- either (cannotLoad path) pure r+ pure $ \name -> do+ r2 <- try (getProcAddress handle name) :: IO (Either SomeException (Ptr ()))+ either (missing path name) (pure . castPtrToFunPtr) r2+#else+opener path = do+ r <- try (dlopen path [RTLD_NOW]) :: IO (Either SomeException DL)+ dl <- either (cannotLoad path) pure r+ pure $ \name -> do+ r2 <- try (dlsym dl name) :: IO (Either SomeException (FunPtr ()))+ either (missing path name) pure r2+#endif++cannotLoad :: FilePath -> SomeException -> IO a+cannotLoad path e = throwIO (LibraryError ("cannot load the KeyNub library " ++ path ++ ": " ++ show e))++missing :: FilePath -> String -> SomeException -> IO a+missing path name _ = throwIO (LibraryError (path ++ " does not export " ++ name))
+ test/StubTest.hs view
@@ -0,0 +1,183 @@+{-# LANGUAGE ScopedTypeVariables #-}+-- Every call of the binding against a stand-in for the flat C API: the SDK's+-- flat layer compiled together with the C ABI stand-in+-- (bindings/julia/test/stub/licd_stub.c, one imaginary dongle held in memory)+-- into one shared library, with a C compiler from the path (cc, gcc, clang or+-- zig cc). KEYNUB_LICDONGLE_FLAT_LIBRARY naming an already compiled stand-in+-- skips the build. Exit code 0 when every check passed.+module Main (main) where++import Control.Exception (SomeException, try)+import Control.Monad (forM_, unless)+import Data.Bits (xor)+import qualified Data.ByteString as BS+import qualified Data.ByteString.Char8 as BC+import Data.IORef+import Data.List (sort)+import System.Directory (createDirectoryIfMissing, doesFileExist, getCurrentDirectory, getTemporaryDirectory)+import System.Environment (lookupEnv)+import System.Exit (exitFailure, exitSuccess)+import System.FilePath ((</>), takeDirectory)+import qualified System.Info+import System.Process (readProcessWithExitCode)+import System.Exit (ExitCode(..))++import KeyNub.LicDongle++main :: IO ()+main = do+ lib <- standIn+ setLibraryPath lib+ failures <- newIORef (0 :: Int)+ let check cond what = unless cond $ do+ modifyIORef' failures (+ 1)+ putStrLn (" FAIL " ++ what)+ fails st what act = do+ r <- try act+ case r of+ Left (e :: LicDongleError) -> check (errorStatus e == st) (what ++ ": " ++ show (errorStatus e))+ Right _ -> check False (what ++ ": no failure")+ serialValue = "04A1B2C3D4E5F6"+ factoryKey = BS.pack [0x30, 0x10, 0x01, 0x02, 0x03]+ replacementKey = BS.pack [0x30, 0x11, 0x09, 0x08, 0x07, 0x06]++ v <- libraryVersion+ check (v == (9, 8, 7)) "the stand-in reports 9.8.7"+ t <- statusText (-2)+ check (t == "no device") "strerror text"++ ds <- devices+ check (ds == [Device serialValue "stub:0"]) "enumeration"+ fails NoDevice "open with a wrong serial" (openSerial "nope")+ fails NoDevice "open at a wrong path" (openPath "stub:9")++ d <- open+ s <- serial d+ check (s == serialValue) "serial"+ i <- info d+ check (protocolVersion i == (1, 0) && firmwareVersion i == (2, 3, 4)) "versions"+ check (secureElementReady i && provisioned i && isolated i) "flags set"+ check (not (watchdogReboot i) && not (writeAuthRotated i)) "flags clear"+ check (dataCapacity i == 1024 * 1024 && dataFree i == 1000000) "storage"+ g <- verifyGenuine d+ check (genuineSerial g == serialValue && provisionedDate g == "2026-08-15") "genuine"+ isGenuine d >>= \ok -> check ok "isGenuine"++ fails CertInvalid "a non-DER root is refused" (setTrustRoot d (BS.pack [0x02, 0x01, 0x00]))+ setTrustRoot d (BS.pack ([0x30, 0x82, 0x01, 0x00] ++ replicate 128 0xAB))+ fails CertInvalid "verification under a foreign root" (verifyGenuine d)+ isGenuine d >>= \ok -> check (not ok) "isGenuine fails closed"+ setTrustRoot d (BS.pack ([0x30, 0x82, 0x01, 0x00] ++ replicate 128 0x01))+ isGenuine d >>= \ok -> check ok "genuine again under the issuing root"++ fails SessionExpired "records need a session" (records d)+ sessionOpen d+ let payload = BC.pack "license-blob-0123456789"+ fails AuthRequired "writing needs the write role" (writeRecord d "lic" payload)+ fails NotGenuine "a wrong key does not elevate" (authorizeWrite d (BS.pack [0x30, 0x00]))+ authorizeWrite d factoryKey+ writeRecord d "lic" payload+ readRecord d "lic" >>= \r -> check (r == payload) "read back what was written"+ writeRecord d "cfg" (BC.pack "cfgdata")+ recs <- records d+ check (sort (map recordName recs) == ["cfg", "lic"]) "record names"+ check ([recordSize r | r <- recs, recordName r == "lic"] == [BS.length payload]) "record size"+ readRecord d "cfg" >>= \r -> check (BC.unpack r == "cfgdata") "a string is stored as its bytes"+ fails NotFound "reading a missing record" (readRecord d "nope")+ fails InvalidArg "an empty name never erases" (eraseRecord d "")+ records d >>= \rs -> check (length rs == 2) "nothing erased by mistake"+ eraseRecord d "cfg"+ records d >>= \rs -> check (map recordName rs == ["lic"]) "one record after the erase"+ writeRecord d "empty" BS.empty+ readRecord d "empty" >>= \r -> check (r == BS.empty) "an empty record reads back empty"++ before <- readCounter d 0+ incrementCounter d 0 >>= \n -> check (n == before + 1) "increment returns the new value"+ c0 <- readCounter d 0+ c1 <- readCounter d 1+ check (c0 == before + 1 && c1 == 0) "counters"+ fails Range "a counter the dongle lacks" (readCounter d 7)++ let secret = BS.pack [fromIntegral ((3 * k + 7) `mod` 256) | k <- [0 .. 99 :: Int]]+ forM_ [(DeviceScope, 0), (DeveloperScope, 1)] $ \(scope, byte) -> do+ blob <- appEncrypt d scope secret+ check (BS.length blob > BS.length secret) ("sealed data is longer, " ++ show scope)+ check (BS.head blob == byte) ("scope byte " ++ show scope)+ appDecrypt d blob >>= \r -> check (r == secret) ("round trip " ++ show scope)+ let tampered = BS.init blob `BS.snoc` (BS.last blob `xor` 1)+ fails TagMismatch ("tampered data " ++ show scope) (appDecrypt d tampered)+ eraseAllRecords d+ records d >>= \rs -> check (null rs) "erase all leaves nothing"++ rotateWriteKey d replacementKey+ writeRecord d "lic" (BC.pack "still-writable")+ sessionClose d+ info d >>= \i2 -> check (writeAuthRotated i2) "the rotation flag is set"+ sessionOpen d+ fails NotGenuine "the factory key no longer elevates" (authorizeWrite d factoryKey)+ authorizeWrite d replacementKey+ writeRecord d "lic" (BC.pack "new-key-writes")+ readRecord d "lic" >>= \r -> check (BC.unpack r == "new-key-writes") "the new key writes"+ sessionClose d+ close d+ r <- try (serial d)+ check (either (\(_ :: LicDongleError) -> True) (const False) r) "a closed handle refuses calls"++ n <- readIORef failures+ if n == 0+ then putStrLn "keynub-licdongle: every call passed against the ABI stand-in" >> exitSuccess+ else putStrLn (show n ++ " check(s) failed") >> exitFailure++-- The compiled stand-in: from the environment, or built from the SDK sources+-- found by walking up from the working directory.+standIn :: IO FilePath+standIn = do+ env <- lookupEnv "KEYNUB_LICDONGLE_FLAT_LIBRARY"+ case env of+ Just p | not (null p) -> pure p+ _ -> do+ root <- sdkRoot+ tmp <- getTemporaryDirectory+ let dir = tmp </> "keynub-haskell-stub"+ out = dir </> libName+ createDirectoryIfMissing True dir+ let args = ["-shared", "-O1", "-DLICD_BUILD_SHARED", "-DLICDF_BUILD_SHARED",+ "-I" ++ (root </> "core" </> "include"), "-I" ++ (root </> "bindings" </> "flat"),+ "-o", out,+ root </> "bindings" </> "flat" </> "licd_flat.c",+ root </> "bindings" </> "julia" </> "test" </> "stub" </> "licd_stub.c"]+ ++ (if System.Info.os == "mingw32" then [] else ["-fPIC"])+ ok <- tryCompilers [("cc", []), ("gcc", []), ("clang", []), ("zig", ["cc"])] args+ unless ok $ do+ putStrLn "the C ABI stand-in could not be compiled: no C compiler (cc, gcc, clang, zig cc) on the path"+ exitFailure+ pure out+ where+ libName = case System.Info.os of+ "mingw32" -> "keynub_licdongle_flat.dll"+ "darwin" -> "libkeynub_licdongle_flat.dylib"+ _ -> "libkeynub_licdongle_flat.so"+ tryCompilers [] _ = pure False+ tryCompilers ((exe, pre) : rest) args = do+ r <- try (readProcessWithExitCode exe (pre ++ args) "") :: IO (Either SomeException (ExitCode, String, String))+ case r of+ Right (ExitSuccess, _, _) -> pure True+ Right (_, _, err) -> putStrLn (exe ++ ": " ++ err) >> tryCompilers rest args+ Left _ -> tryCompilers rest args++-- The SDK root: KEYNUB_SDK_ROOT, or the first directory upwards holding+-- bindings/flat/licd_flat.c.+sdkRoot :: IO FilePath+sdkRoot = do+ env <- lookupEnv "KEYNUB_SDK_ROOT"+ case env of+ Just p | not (null p) -> pure p+ _ -> getCurrentDirectory >>= go+ where+ go dir = do+ present <- doesFileExist (dir </> "bindings" </> "flat" </> "licd_flat.c")+ if present then pure dir else do+ let parent = takeDirectory dir+ if parent == dir+ then putStrLn "the SDK sources were not found above the working directory; set KEYNUB_SDK_ROOT" >> exitFailure+ else go parent