Não há forma 100% segura de fazer isso. O máximo que você pode fazer é dificultar.
Se você bolar um esquema maluco de verificação de arquivo, verificação de data, de serial de HD, qualquer coisa, nada impede o usuário de criar ou editar o arquivo, de mudar a data do PC, de mudar o serial do HD (existem ferramentas que conseguem fazer isso).
A primeira coisa que um cracker iria fazer é descompilar o programa, crackeá-lo e recompilar. Por isso é recomendado que você use um bom ofuscador.
Melhor ainda, ofusque todas as classes e crie um ClassLoader (também ofuscado) que as leia criptografadas e as carregue on-the-fly. Melhor ainda se você conseguir fazer ele gerar bytecodes dinamicamente (ofuscados, obviamente). Melhor ainda se a cada execução os bytecodes gerados forem diferentes.
Mas, não importa o que você faça, um bom cracker inteligente e determinado uma hora consegue quebrar. O máximo que você pode é dificultar ao máximo a vida desse suposto cracker até que não valha a pena queimar neurônios para tentar quebrar o programa. O porém, é que para chegar nesse ponto, quem vai ter que queimar muito neurônio é você! 
Uma opção interessante, porém de ética questionável, é fazer o programa se autodestruir ou se corromper se ele perceber que alguém está tentando quebrá-lo. :twisted:
Outra coisa interessante é um programa capaz de se automodificar a cada execução. Isso dificulta muito a vida do cracker pois torna quase impossível reproduzir uma mesma execução do programa.
Gerar código defeituoso (ou seja, lixo) no meio de códigos válidos é algo promissor também, desde que mas que não atrapalhem a execução do programa. Principalmente se esse código corresponder a mais de 90% do programa carregado, fica muito difícil separar o joio do trigo.
Enfim, use as mesmas técnicas que os vírus (principalmente os mais antigos) costumavam usar para enganar os softwares anti-vírus e tornar um inferno a vida das pessoas que criavam os anti-vírus.