Skip to content

2FA with Time-based One-time Passwords Demo

Warning

This feature is experimental! Its syntax and usage may change in the future. This will not be considered a breaking change.

Info

For a brief overview on how to use 2FA with Time-based One-time Passwords, please refer to the policy language specification.

Overview

In this demo, we present two-factor authentication support for SCONE sessions using a simple secure python service. For the SCONE Configuration and Attestation Service (CAS) to provide the enclaved python service with its secure configuration, the service must not only be attested. A One-time password (OTP) must also be supplied to the CAS, whereby the OTP is generated using the Time-based One-time Password algorithm (TOTP) with a RFC4648 base32 encoded shared secret as input.

Demo

The python service calculates a*x+b, whereby the user supplies x and the secret configuration supplies the service with the factor a and constant b. We begin by create a native Docker image for said service:

# cd into temp dir or your diretory of choice
cd $(mktemp -d)

# create empty program files
touch Dockerfile
touch my_service.py

Now to create the actual service, which we save to my_service.py:

#!/usr/bin/env python

import argparse

def main():
    args = parse_args()
    result = function(
        a=args.factor,
        x=args.user_input,
        b=args.constant
    )
    print(f"{result}")

def function(a, x, b):
    return a * x + b

def parse_args():
    parser = argparse.ArgumentParser(description="Calculates a simple function.")

    # user input arg
    parser.add_argument(
        "-x",
        "--user-input",
        type=int,
        action="store",
        help="User input of the function",
        required=True
    )

    # session arg
    parser.add_argument(
        "-a",
        "--factor",
        type=int,
        action="store",
        help="Session input of the function's factor",
        required=True
    )

    # session arg
    parser.add_argument(
        "-b",
        "--constant",
        type=int,
        action="store",
        help="Session input of the function's constant",
        required=True
    )

    return parser.parse_args()

if __name__ == "__main__":
    main()

Along with the corresponding Dockerfile:

FROM alpine:3.10

RUN apk add --no-cache python3 python3-dev 

RUN mkdir -p /app
WORKDIR /app
COPY my_service.py .

ENTRYPOINT [ "python3", "my_service.py" ]

Now to build and test the native version:

docker build . --tag my_native_service
docker run -it --rm my_native_service -x 42 -a 4 -b 8
# 176

We now sconify the native service using the sconify image tool. We thereby disable the session upload as we create a session manually afterwards. Further, we save its build resources to an extra directory:

# pull the sconify image tool
docker pull registry.scontain.com:5050/sconecuratedimages/sconecli:sconify-image-scone5.6

# create dir for sconify build resources
mkdir -p sconify-resources

# generate a random id for the session
SESSION_NAME=my_session_"$RANDOM""$RANDOM""$RANDOM"

# sconify the native service
docker run \
    --volume $PWD/sconify-resources:/build-resources \
    --volume /var/run/docker.sock:/var/run/docker.sock \
    registry.scontain.com:5050/sconecuratedimages/sconecli:sconify-image-scone5.6 \
    sconify_image \
        --from=my_native_service \
        --to=my_sconified_service \
        --binary=/usr/bin/python3.7 \
        --binary-fs \
        --fs-dir=/app \
        --name="$SESSION_NAME" \
        --verbose \
        --disable-session-upload \
        --allow-tcb-vulnerabilities \
        --allow-debug-mode \
        --cas=5-6-0.scone-cas.cf \
        --command="python3 my_service.py -a 4 -b 12 -x @@1"

To enable the 2FA with OTP, we will need a base32-encoded shared secret. Such a secret can be generated on https://totp.info/ (although you will need to add 3 extra chars). With this secret, we continue by modifying the session generated in sconify-resources/session.yml (or you can copy/pase the following session):

name: my_session_<random-id>
version: "0.3"

# Access control:
#   - only the data owner (CREATOR) can read or update the session
#   - even the data owner cannot read the session secrets (i.e., the volume key and tag) or delete the session

access_policy:
  read:
   - CREATOR
  update:
   - CREATOR

services:
   - name: service
     image_name: service_image
     mrenclaves: [032491dba2e7ec27f793a478ab556c26b87111b11657c93f5554a1a2975567e5]
     command: "python3 my_service.py -a 4 -b 12 -x @@1"
     environment:
         SCONE_MODE: hw
         LD_LIBRARY_PATH: "/lib:/usr/lib:/usr/local/lib:"
         PATH: "/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin"
     pwd: /app

images:
  - name: service_image

security:
  attestation:
    # we add the shared secret here
    one_time_password_shared_secret: BASE32SECRET3232BASE32SECRET3232
    tolerate: [hyperthreading, insecure-igpu, outdated-tcb, software-hardening-needed, insecure-configuration, debug-mode]
    ignore_advisories: "*"

Info

We use secure arguments in this session for -a and -x: the values of a and b remain hidden. The user will only be able to supply the value for x: this is denoted by @@1.

We must now upload this session to a CAS:

# image we use to upload to CAS
docker pull registry.scontain.com:5050/sconecuratedimages/sconecli:alpine3.10-scone5.6

# upload session to CAS
docker run -it --rm \
    --volume=$PWD/sconify-resources:/sconify-resources \
    --entrypoint=scone \
    registry.scontain.com:5050/sconecuratedimages/sconecli:alpine3.10-scone5.6 \
    session create \
        --only_for_testing-disable-attestation-verification \
        --cas=5-6-0.scone-cas.cf \
        --name=$SESSION_NAME \
        /sconify-resources/session.yml

Warning

We do not attest the CAS in this demo. Typically, we would attest the CAS before uploading the session, to ensure its trustworthiness.

Now that we have uploaded the session, we can run our service! To execute the sconified service in an enclave, we require an SGX device. It can be determined with the determine_sgx_device function. Further, we must generate an OTP from the shared secret. We can achieve this with https://totp.info/ as well. We must then set a custom SCONE_CONFIG_ID env variable, with which the sconified service will request the configuration from the CAS. This variable also holds the OTP, which it will use to authenticate itself. We append the OTP, delimited with an @, to the session name:

docker run -it --rm \
    --device="/dev/isgx" \
    --env="SCONE_CONFIG_ID=$SESSION_NAME/service@<otp>" \
    --entrypoint=python3 \
    my_sconified_service 42
# 180

Info

As we use secure arguments, we have to overwrite the entrypoint and command of the sconified python image, such that the first argument is the value of x, i.e., 42 in this example execution.