Let's get some basic math out of the way. If you have 8 digits your activation/authentication code only can encode a maximum of $8 \cdot log_2(10)$ bits worth of information. (About 26.5) So public key signatures are out of the question. Second, the probability of success is relatively high. ($10^{-8}$ or 1 in 100 million). That is a lot of numbers to type manually, but the chance of success is too high to consider secure unless we relax the assumptions and requirements made.
So here's what I would consider instead:
- We will trust the majority of users to be honest. It's not a big problem if some users "pirate" activation codes. (I'm personally opposed to "DRM" on principal when it's invasive, compromises user security, or prevents expert analysis of compiled code.) Preventing access to secret information and at the same time preventing tampering is hard.
- Instead we will say we'll assume most users don't have the ability to access symmetric keys on their device. Maybe there is a hardware security module. Maybe law and morality discourages "stealing" activation. Maybe people are just willing to pay and play fair.
- Additionally the user won't or can't delete local records, change the system clock, or modify code. Keeping a symmetric key secret won't help if the user hacks the app's binary to replace something like "if activation fails goto X" with a No-OP.
So first you will want to rate limit guesses. Just like online brute force password guessing, you can lock a person out of guessing temporarily if they fail too many activation-code-entries. Offline brute-forcing isn't something that you may be able to prevent. (If a person reads a secret key from memory in their device and brute forces an activation code on a personal computer then they can confirm if a potential activation code and wait until they find one they know is correct before attempting activation.) But it doesn't matter that this is theoretically possible if we accept the assumptions listed.
To prevent double spending you will want to save a list of activation codes the user has already used. We assume the users doesn't meddle with this list. If you have an app the user can uninstall and reinstall without restoring the spent-code-list then they could reuse codes at the expense of losing all their other data. You can still prevent this within the limits of the assumptions made. Instead of deriving a user ID from a hardware ID you can generate a new random user ID after time the app is reinstalled. Since those spent codes are associated with only the old user ID they cannot be reused this way.
Next you have to worry about the probability of guessing any unspent code. Not just one of them. If you do not allow old purchased but unused activation codes to expire, then there is only a finite number of activation codes you can issue to a user. The greater this number the more likely a person can guess a correct activation code.
Option 1: One solution, based on what I read Nintendo does with some of its freemium games, is to set the maximum number of purchases you can make. After the user spends the equivalent amount of money for a normal full game all the freemium pay-to-play features are replaced with permanent unlimited resources. Presumably the reason for this is to preserve consumer's respect and to prevent parents from complaining that their child spent infinity dollars on in game purchases. This has the dual use of showing good will to the customer (someone may spend the maximum up front just to support your product) and keeping the probability of guessing correct activation codes low.
Option 2: Let purchased activation codes expire. So now you validate that activation codes are correct for the user and correct for the current date.
Crypto-Implementation:
For option 1: I would generate the limited number of activation codes ahead of time and make a database relating user IDs to activation codes. When I sell a customer their activation code I mark that code as used so I don't sell them the same one again. This requires, for example, physical access to the device at the sale point to program activation codes, or online registration (which doesn't require internet access to use activation codes later), or you generate random codes prior to sale of the hardware and record them in the server database and onto the device at the same time. You can store hashes (stretched or not) of the activation codes if you want to further obscure activation codes. This does not make codes less guessable because a 8 digit number cannot contain enough entropy to make guessing impractical.
If you don't want to store any data or instead prefer no activation-code programming or registration. you can instead derive an encryption key from the user ID the customer gives you and use format preserving encryption to generate activation codes. The user says "my ID is x and I want to buy activation code n". The server encrypts n using a key derived from x, converts the result to decimal, and gives it to the user. The user inputs the decimal code into their device, their device derives the same key from it's user ID, and checks if it decrypts to the number stored in its activation-counter, and, if the plaintext and activation counters match, unlock some feature and increment the activation-counter. Since format preserving encryption is bijective on whatever set it uses for input/output only one activation code will map to a number in {0, 1, 2, 3, ...}.
For Option 2: I would use the time based one time password algorithm. Your use case is essentially identical with respect to security as is the case for one time passwords. There server and user device need to hold the same secret key and every user needs there own key. That way Alice's one-time-password is not the same as Bob's one-time-password so they cannot share the same activation code. With web based authentication you set the period length in which identical one-time-passwords are computed (from the secret key and the current time) to just a few seconds. Then you check within a window of a few periods to account for clock differences, user input delay, and communication time. You will want to set the period to be much longer because the transfer of user-ID -> store -> activation-code -> code entry on device is effectively a very slow communications channel. Your time period length could half your activation time and your window should be large enough to accommodate a week or more delay between the time of purchase and the time of activation code entry.
Remember with both methods you need to store spent keys client side (or at least a counter for option 1). You also need to trust the user isn't cracking/hacking/pirating also, but even if you could prevent this with some other user access control system someone capable of hacking your app could just disable the need for activation codes by patching the check out of the code binary.
PS I've written this post over several sittings. Let me know if some part seems unclear. For option 1 there is also sibling of TOTP, HOTP. I've been distracted so many times that I can't remember why I didn't include it.
One more thing. For TOTP and HOTP you need to keep a list of spent activation codes but also need to be conscious of repeat codes. You can instead store the code plus the counter value or the code plus the time. If you don't account for this then after you sell an access code there is a one in $10^8$ chance their code will fail despite paying for the same activation code twice.