Python 3 tutorial

Prerequisites

Operating systems
  • Linux on x86_64

  • Windows on x86_64

nShield software
  • Security World 12.80 or later

User permissions
  • A user permitted to connect to the local hardserver and read the Security World key management data (kmdata) files.

Supported Python version
  • Python 3.8.5

Set up the environment for nfpython

To use nShield Python 3 support with another version of Python 3, contact Entrust Support. Other Python versions are not covered by this guide.

The recommended way of developing and deploying an nShield Python 3 application is using the Python virtualenv.

Entrust recommends the following directory layout:

   application
       venv
       mypackage
           __init__.py
           code.py
       tests
           test_mypackage.py
       script.py
       setup.py

Create and configure the virtualenv

You do not need administrator access to create a virtualenv. When launching production applications, you must use the virtualenv that was created for them.

  1. Start a command-line shell.

  2. Change to the directory where you want to store your application files.

  3. Run:

    Linux
    /opt/nfast/python3/bin/python3 -m venv --copies venv
    Windows
    c:\Program Files\nCipher\nfast\python3\python --copies -m venv venv
  4. Configure the virtualenv:

    Linux
    . venv/bin/activate
    pip install /opt/nfast/python3/additional-packages/nfpython*.whl
    Windows (PowerShell)
    venv\Scripts\activate.ps1
    pip install c:\Program Files\nCipher\nfast\python3\additional-packages\nfpython*.whl
  5. Install all required Python packages into the virtualenv.

    To install your application:

    • If your application uses setup.py entrypoint scripts, execute them directly.

    • If you do not use entrypoints, create your own scripts:

      These examples assume that your program is called application.py and your virtualenv is in the venv directory.
      Linux - application.sh
      #!/bin/sh
      HERE=$(dirname $(readlink -f $0))
      . ${HERE}/venv/bin/activate
      python3 ${HERE}/application.py
      Windows - application.ps1
      $PSScriptRoot\venv\Scripts\activate.ps1
      python $PSScriptRoot\application.py

nfpython connections and commands

Send nCore commands to the hardserver or attached HSMs:

  1. Import the nShield python module.

    import nfpython
  2. Set up a connection

    conn = nfpython.connection()
  3. Construct an nCore command:

    c = nfpython.Command()
    c.cmd = "NewEnquiry"
    c.args.module = 1
    c.args.version = 1

    The easiest way to define a new command to send is to create a Command() object, set the attributes starting with the cmd name, then set the args attributes as required.

  4. Send the command and wait for the reply:

    reply = conn.transact(c)
    print(reply.reply.data.one)

    This command prints a reply similar to the following output.

    EnquiryDataOne.releasemajor= 12
                  .releaseminor= 80
                  .releasepatch= 0
                  .checkintimehigh= 0
                  .checkintimelow= 1620120388
                  .flags= Hardware|HasTokens|SupportsCommandState
                  .speedindex= 5200
                  .recommendedminq= 9
                  .recommendedmaxq= 152
                  .hardwareserial= A6BB-8687-A76A
                  .softwaredetails= 12.80.0-40-a769244e11

If the command results in a reply with a status value other than OK, the transact() connection method raises an NFStatusError exception.

To suppress the exception and obtain the original reply regardless of status, use the ignorestatus=True keyword argument:

import nfpython
conn = nfpython.connection()
c = nfpython.Command()
c.cmd = "NewEnquiry"
c.args.module = 1000
reply = conn.transact(c, ignorestatus=True)
print(reply)

This prints:

Reply.cmd= ErrorReturn
     .status= InvalidModule
     .flags= 0x0

PTPython support for nfpython

The nShield Python 3 package includes the pre-installed ptpython. REPL in ptpython supports tab completion of most Python objects, functions, and methods, including those of type nfpython and nfkm.

Linux
/opt/nfast/python3/bin/ptpython
Windows
c:\Program Files\nCipher\nfast\Scripts\ptpython

ptpython is useful for exploring the attributes and types used in different nCore commands and replies, for example:

>>> import nfpython

>>> c = nfpython.Command()

>>> c.cmd = "NewEnquiry"

>>> c.args
{'flags': '0x0', 'version': 0, 'module': 0}

>>>
 [F4] Emacs  237/237 [F3] History [F6] Paste [F2] Menu - CPython 3.8.5

Worked nfpython example for hash, sign, and verify

nShield Security World software includes several Python 3 example files.

The default location for these files is:

  • Linux: /opt/nfast/python3/examples

  • Windows: C:\Program Files\nCipher\nfast\python3\examples

The files hashing.py, keys.py and signing.py make up a sample application that signs data using a previously generated RSA key. The application performs the following steps: load keys, digest data, and perform a sign and verify operation using an attached HSM.

  1. Generate a module-protected RSA key:

    Linux
    /opt/nfast/bin/generatekey -b simple protect=module type=RSA size=2048 ident=signer
    Windows
    c:\Program Files\nCipher\nfast\bin\generatekey -b simple protect=module type=RSA size=2048 ident=signer
  2. Find and load the keys

    Finding saved keys requires the nfpython and nfkm modules.

    To load an existing key, you need to know the appname and ident of the saved key, and you need the nfpython connection.

    import nfpython
    import nfkm
    
    appname = "simple"
    ident = "signer"
    module = 1
    
    conn = nfpython.connection(needworldinfo=True)
    appident = nfkm.KeyIdent(appname=appname, ident=ident)
    keydata = nfkm.findkey(conn, appident)
    
    # load the private key from keydata.privblob
    # or public key from keydata.pubblob
    cmd = nfpython.Command(cmd="LoadBlob")
    cmd.args.blob = keydata.privblob
    cmd.args.module = module
    
    # load the blob and get a Key ID
    rep = conn.transact(cmd)
    keyid = rep.reply.idka
  3. Process the data.

    For large amounts of data, nShield software provides channels to perform crypto operations in an incremental stream.

    This example uses the ChannelOpen and ChannelUpdate commands to compute a SHA256 hash with the HSM.

    Entrust typically recommends a ChannelUpdate size of around 8000 bytes. However, you might find that larger or smaller sizes give better results depending on network conditions or HSM speed ratings.

    message = b"hello world" * 10240
    chunksize = 8000
    
    conn = nfpython.connection(needworldinfo=True)
    c = nfpython.Command()
    c.cmd = "ChannelOpen"
    c.args.type = "simple"
    c.args.mode = "sign"
    c.args.mech = "SHA256Hash"
    
    rep = conn.transact(c)
    channel = rep.reply.idch
    
    c = nfpython.Command()
    c.cmd = "ChannelUpdate"
    # split the message up into small chunks and transmit each in sequence
    for chunk in (message[i:i+chunksize] for i in range(0, len(message), chunksize)):
        c.args.idch = channel
        c.args.input = nfpython.ByteBlock(chunk, fromraw=True)
        conn.transact(c)
    
    # obtain the hash value by setting the final flag
    c.args.input = nfpython.ByteBlock()
    c.args.flags |= "final"
    rep = conn.transact(c)
    digest = rep.reply.output
  4. Sign the digest using the loaded private key:

    # compute the hash (either using nfpython or hashlib)
    digest = digest_message(conn, message)
    
    # perform a signature
    plain = nfpython.Hash32(digest)
    c = nfpython.Command()
    c.cmd = "Sign"
    c.args.mech = "RSAhSHA256pPKCS1"
    c.args.key = privkey
    c.args.plain.type = "Hash32"
    c.args.plain.data.data = plain
    
    rep = conn.transact(c)
    signature = rep.reply.sig
  5. Verify the signature.

    Verification requires a public key, the signature plaintext, and the signature data.

    If the signature is invalid, transact(cmd) raises an NFStatusError exception with its status attribute set to "VerifyFailed".
    digest = nfpython.Hash32(hashbytes)
    
    c = nfpython.Command()
    c.cmd = "Verify"
    c.args.key = pubkey
    c.args.plain.type = "Hash32"
    c.args.plain.data.data = digest
    c.args.sig = signature
    
    conn.transact(c)

The full example would use the following code:

keys.py
import nfpython
import nfkm


def load_key(conn, appname: str, ident: str, module=0, private=True) -> nfpython.KeyID:
    """
    Load a key given an appname and ident
    :param conn:
    :param appname: Key appname, eg "simple"
    :param ident: Key ident
    :param module: module to load the key on (default 0 = any)
    :param private: load the private blob if true
    :return: the loaded key
    """
    appident = nfkm.KeyIdent(appname=appname, ident=ident)
    keydata = nfkm.findkey(conn, appident)

    cmd = nfpython.Command(cmd="LoadBlob")
    if private:
        cmd.args.blob = keydata.privblob
    else:
        cmd.args.blob = keydata.pubblob
    cmd.args.module = module

    rep = conn.transact(cmd)
    keyid = rep.reply.idka
    return keyid
hashing.py
import nfpython


def digest_message(conn, message: bytes, module=0, mech="Sha256Hash", chunksize=8000) -> bytes:
    """
    Hash a binary string using the HSM and ChannelUpdate commands
    :param conn:
    :param message: binary message to hash.
    :param module: HSM to sign with (default 0 = any)
    :param mech: hash mechanism name
    :param chunksize: digest block size
    :return:
    """
    c = nfpython.Command()
    c.cmd = "ChannelOpen"
    c.args.type = "simple"
    c.args.mode = "sign"
    c.args.mech = mech
    c.args.module = module

    rep = conn.transact(c)
    channel = rep.reply.idch

    c = nfpython.Command()
    c.cmd = "ChannelUpdate"
    # split the message up into small chunks and transmit each in sequence
    for chunk in (message[i:i+chunksize] for i in range(0, len(message), chunksize)):
        c.args.idch = channel
        c.args.input = nfpython.ByteBlock(chunk, fromraw=True)
        conn.transact(c)

    # obtain the hash value by setting the final flag
    c.args.input = nfpython.ByteBlock()
    c.args.flags |= "final"
    rep = conn.transact(c)
    digest = rep.reply.output
    return digest
signing.py
#!/usr/bin/env python3

import nfpython
from keys import load_key
from hashing import digest_message


def sign_message(conn, privkey: nfpython.KeyID, message: bytes) -> (nfpython.Hash32, nfpython.CipherText):
    """
    Hash and sign a binary string using the HSM and a loaded RSA private key
    :param conn:
    :param privkey: KeyID of loaded key
    :param message: bytes to sign
    :return: the digest hash and signature
    """
    digest = digest_message(conn, message)
    plain = nfpython.Hash32(digest)
    c = nfpython.Command()
    c.cmd = "Sign"
    c.args.mech = "RSAhSHA256pPKCS1"
    c.args.key = privkey
    c.args.plain.type = "Hash32"
    c.args.plain.data.data = plain

    rep = conn.transact(c)
    signature = rep.reply.sig
    return plain, signature


def verify_signature(conn, pubkey: nfpython.KeyID, digest, signature) -> bool:
    """
    Verify a signature using the HSM and a loaded public key
    :param conn:
    :param pubkey:
    :param digest:
    :param signature:
    :return:
    """
    digest[0] = 1
    cmd = nfpython.Command()
    cmd.cmd = "Verify"
    cmd.args.key = pubkey
    cmd.args.plain.type = "Hash32"
    cmd.args.plain.data.data = digest
    cmd.args.sig = signature

    conn.transact(cmd)


def run():
    conn = nfpython.connection(needworldinfo=True)
    privkey = load_key(conn, appname="simple", ident="signer")
    pubkey = load_key(conn, appname="simple", ident="signer", private=False)

    message_bytes = b"hello world" * 1024
    print(f"Hash and Sign {len(message_bytes)} bytes..")
    digest, signature = sign_message(conn, privkey, message_bytes)
    print("Verifying..")
    verify_signature(conn, pubkey, digest, signature)
    print("Done.")


if __name__ == "__main__":
    run()