# KBAC Specification

This document specifies how to implement Key-Based Access Control (KBAC), which is the security and access control framework used by CaSS. KBAC adds security and access control to JSON and JSON-LD objects. It uses mechanisms found in Public Key Infrastructure (PKI) (opens new window) to federate identities, authenticate identities, provide authorization, and encrypt data.

# How to Read this Document

This document describes the data model, functions, and algorithms used to implement KBAC for security, authorization, and encryption. It is intended for developers with a working understanding of encryption and access to code libraries that can be used to perform common encryption tasks.

# Overview

KBAC has the following components:

  1. A permission system that defines what operations an individual or system can perform on any object in CaSS.
  2. An encryption-based enforcement system that enables end-to-end encryption. Cryptographic techniques are used to grant and deny read permissions and to validate the authenticity of objects or fields.
  3. A set of conformance criteria (with various levels of conformance) that ensure that a conforming system follows adequate security procedures and respects permissions, including those that cannot be enforced via encryption.

KBAC assumes that objects are expressed in JSON-LD and adds fields and encryption to these objects to accomplish 1 and 2. KBAC does not specify how and where JSON-LD objects are stored or transmitted but is compatible with NoSQL and SQL databases, with systems that store JSON-LD as objects in a document object model, and with both secure and insecure data transmission protocols (e.g. HTTP and HTTPS). CaSS instances (installed using the code available on GitHub) store objects in a NoSQL database as JSON-LD with KBAC extensions and encryption and conform to the policy requirements of KBAC.

# Specification

# Encodings

In JSON (opens new window), a string is a "a sequence of zero or more Unicode characters, wrapped in double quotes, using backslash escapes." CaSS uses UTF-8 encodings (opens new window) of unicode characters. When necessary, Base64 encoding (opens new window) is used to convert sequences of bytes into strings.

# Entities and Identities

In KBAC, an Entity refers to a person, organization, group, or system.

KBAC assigns identities to entities. KBAC assumes that real-world identities are defined and managed external to CaSS, e.g. via a Single Sign On (SSO) system, an enterprise directory service, OAUTH, or some other means. In CaSS, an identity for an entity is a pair consisting of a public and private key in the sense of Public Key Infrastructure (PKI).

In this document's notation:

  • publickey(entity) represents the public key portion of the identity (also called an Identifier)
  • identity(entity) represents the public/private key pair (also called the Identity)

CaSS should not store any identities that include PII.

# Groups

Identities can belong to groups. Groups are also representable by identities. CaSS assumes that an external service can validate whether an identity belongs to a given group.

# AES encryption

AES encryption (opens new window) (or AES) refers to the Advanced Encryption Standard as established by the National Institute of Standards (NIST) (opens new window). When applying KBAC, the same AES implementation should be used whenever encrypting or decrypting data. CaSS uses AES-256-CTR as defined in IETF RFC3686 (opens new window). This implementation uses a 32-byte secret that is separate from a 32-byte initialization vector (opens new window). In this document we will represent AES as two functions:

ciphertext = aesEncrypt(plaintext, secret, iv)
plaintext = aesDecrypt(ciphertext, secret, iv)

NOTE

Though plaintext will usually be a string, any ordered sequence of bytes may be passed in

# RSA encryption

RSA encryption (opens new window) refers to the asymmetric RSA encryption algorithm. CaSS uses a 2048-bit version of RSA-OAEP (opens new window) for encryption and decryption, and SHA1 with RSA for signing and verification. In this document we will represent the functions relevant to RSA encryption as:

new unique public and private key pair = generateKeys()
privatekey = private key from PPK = privateKey(PPK)
publickey = public key from PPK = publicKey(PPK)
ciphertext generated by applying RSA to plaintext (< 256 bytes) = rsaEncrypt(plaintext, privatekey)
plaintext = rsaDecrypt(ciphertext, publickey)
signature = rsaSign(plaintext, privatekey)`
rsaVerify(signature, publickey) is true ⇔ rsaSign(signature, publickey) is an identifiable signature that conforms to an agreed-upon format for signatures.

# Cryptographic Encodings

Cryptographic objects shall be encoded in the following fashions prior to storage in JSON:

object encoding
secret Base64
iv Base64
privatekey PKCS#8 encoding with whitespace removed
publickey PEM encoded SubjectPublicKeyInfo with whitespace removed
ciphertext Base64
signature SHA1 encoding before signature creation, Base64 encoding of the signature.

# KBAC Components

A KBAC-conformant object should implement the following JSON-LD fields, if applicable:

  • @context
  • @type
  • @id

Definitions can be found in the JSON-LD Specification (opens new window).

A KBAC-conformant JSON-LD object may contain the following fields:

  • @owner
    • The public keys of entities who are allowed to edit or delete the object.
  • @reader
    • The public keys of entities who are allowed to discover or read the object if the object is encrypted.
  • @signature
    • A cryptographic signature that can be decrypted and validated using the public keys of identities listed in @owner or @reader. (The ability to decrypt a signature with one of these public keys is prima facie evidence that the corresponding identity created the signature.)

Each of these fields shall, in its natural state, be an array.

# KBAC Identifiers

A KBAC-conformant URL shall be a resolvable URL (RFC 1738 (opens new window), 3986 (opens new window), etc.) composed of the following parts:

  • protocol
    • http:// or https://
  • endpoint
    • hostname and path
  • type
    • @context + @type with protocol removed and all sequences of symbols replaced with dots.
      • ex: http://schema.cassproject.org/0.2/competency -> schema.cassproject.org.0.2.competency
  • unique identifier
    • May be a randomly generated GUID
    • May be a canonical identifier with at least one letter or symbol.
  • version optional
    • Time the object was last modified in milliseconds since the epoch.

When the version is omitted, the URL refers to the most recent version of the object.

# Types

# Example Type -- File

A functional example of a JSON-LD File object follows:

{
    "@context": "http://schema.eduworks.com/general/0.1",
    "@type": "file",
    "@owner": [publickey1, publickey2, ...],
    "@signature": [
        rsaSign(toSignableObject(this),privatekey1),
        rsaSign(toSignableObject(this),privatekey2),
        ...
    ],
    "mimeType": string,
    "data": string,
    "name": string
}

# Owners and Signatures

The algorithm to annotate a JSON-LD object follows:

{
    "@owner":[publickey1,publickey2,...],
    "@reader":[publickey3,publickey4,...],
    "@signature":[
        rsaSign(toSignableObject(this),privatekey1),
        rsaSign(toSignableObject(this),privatekey2),
        ...
    ]
}

The algorithm to prepare an object for signing follows:

toSignableObject(object) =
    remove from object the fields: ["@signature","@id"]
    Serialize to JSON with zero whitespace with fields in ASCII order.

To verify an object:

rsaVerify(toSignableObject(object), signature1, publickey1)

To sign an object:

rsaSign(toSignableObject(object), privatekey1)

If an object is annotated with KBAC fields and being stored in a repository, the system storing the object:

  1. Must provide a signatureSheet with a valid signature for at least one owner.
  2. Must remove all invalid signatures.
  3. Should append at least one valid signature to the object.

The repository the object is being stored in:

  1. Must validate all SignatureSheetSignatures in the SignatureSheet by:
    • Ensuring the expiry timestamp has not elapsed.
    • Ensuring that the server url pertains to this machine and, if it specifies an object identifier, the object being stored.
    • Ensuring the signature of the SignatureSheetSignature is valid.
  2. If the object is being modified, must have at least one SignatureSheetSignature common with the unmodified object’s owner.
  3. Must validate any signatures provided with the object.

Any failure of any of these criteria shall result in an error.

# SignatureSheetSignature

A SignatureSheetSignature has the following fields:

{
    "expiry": long, // (unix timestamp)
    "server": url
}

And is generated by:

function createSignatureSheetSignature(ppk,serverUrl,expiryMilliseconds) {
    return {
        "@context": "http://schema.cassproject.org/kbac/0.2/",
        "@type": "TimeLimitedSignature",
        "@owner": publickey(ppk),
        "@signature": rsaSign(toSignableObject(this),privatekey(ppk)),
        "expiry": nowInUnixTime()+expiryMilliseconds,
        "server": serverUrl
    }
}

function signatureSheetSignatureValid(object,serverUrl) {
    return (
        rsaVerify(object remove @signature, @signature1, @owner1) &&
        expiry > nowInUnixTime() &&
        startsWith(server, serverUrl)
    );
}

# SignatureSheet

A signature sheet is an array of SignatureSheetSignature:

[signatureSheetSignature1, signatureSheetSignature2, ...]

# EncryptedValue

EncryptedValue is an object that stores encrypted data. It has the following fields:

{
    "@encryptedType": @context + @type of the object encrypted. Optional.
    "secret": [secret1, secret2]
    "payload": ciphertext
}

The following function specifies the object used to store a secret, iv, and other data for encryption:

function encryptedSecret(obj,field,secret,iv) {
    return {
        "s": secret,
        "f": field,
        "v": iv,
        "d": obj["@id"]
    }
}

function toEncryptedValue(obj, value, field, secret, iv, [publickey1, publickey2, ...]) {
    return {
        "@context": "http://schema.cassproject.org/kbac/0.2/",
        "@type": "EncryptedValue",
        "@encryptedType": obj["@type"],
        "@owner": obj["@owner"],
        "@signature": [rsaSign(toSignableObject(this),obj.@owner1),...],
        "@reader": [publickey1,publickey2,...],
        "secret": [
            rsaEncrypt(encryptedSecret(obj, field, secret, iv), @owner1), ...,
            rsaEncrypt(encryptedSecret(obj, field, secret, iv), @reader1), ...
        ],
        "payload": aesEncrypt(value,secret,iv)
    }
}

Additionally:

  • Secret and IV should be randomly generated.
  • If toEncryptedValue is encrypting an object, the value shall be the serialized object and field shall be omitted.
  • If toEncryptedValue is encrypting a field of an object, the value shall be the value of the field, and field shall be the JSONPath dot-and-bracket notation of the field’s location.
function fromEncryptedValue(obj, [ppk1,ppk2,...]) {
    for all i=ppk, j=obj["secret"], stop on first decryption that results in a valid JSON object
        aesDecrypt(
            obj["plaintext"],
            rsaDecrypt(secretj,privatekey(ppki))["s"],
            rsaDecrypt(secretj,privatekey(ppki))["v"]
        )
    return the result of above.
}

A repository must strip any EncryptedValue objects from search or get results if:

  1. A signatureSheet is not provided with the request.
  2. A signatureSheetSignature is invalid (based on the criteria given previously).
  3. A signatureSheetSignature does not provide a public key matching a key in the @reader or @owner fields of the result under consideration.

# Identity Server, Types and Operations

It is common to use usernames and passwords in order to provide access to a system. In KBAC, a username and password may be used to store and retrieve credentials from a repository. The following types, functions and requirements provide a method of storing credentials in an encrypted fashion.

# Credential

A Credential object stores a private key in an encrypted form and has the following fields:

{
    "iv": iv,
    "ppk": string,
    "displayNameIv": iv,
    "displayName": string
}

Additionally:

  • Iv should be regenerated any time the value of ppk is changed.
  • displayNameIv should be regenerated any time displayName is changed.

A functional definition follows:

createCredential(pk, secret, iv, displayNameIv, displayName) {
    return {
        "@context": "http://schema.cassproject.org/kbac/0.2/",
        "@type": "Credential",
        "iv": iv,
        "ppk": aesEncrypt(pem(ppk),secret,iv),
        "displayNameIv": displayNameIv,
        "displayName": aesEncrypt(displayName,secret,displayNameIv)
    };
}

# Contact

A Contact stores a public key in an encrypted form. It has the following fields:

{
    "iv": iv,
    "pk": string,
    "displayNameIv": iv,
    "displayName": string,
    "sourceIv": iv,
    "source": string
}

A functional definition follows:

function createContact(pk, secret, iv, displayNameIv, sourceIv, displayName, source) {
    return {
        "@context": "http://schema.cassproject.org/kbac/0.2/"
        "@type": "Contact"
        "iv": iv
        "pk": aesEncrypt(pem(pk),secret,iv)
        "displayNameIv": displayNameIv
        "displayName": aesEncrypt(displayName,secret,displayNameIv)
        "sourceIv": displayNameIv
        "source": aesEncrypt(source,secret,sourceIv)
    };
}

# Credentials

A Credentials object stores public and private keys in an encrypted form. It has the following functional definition:

function createCredentials(Credential[], Contact[], pad, token) {
    return {
        "@context": "http://schema.cassproject.org/kbac/0.2/",
        "@type": "Contact",
        "token": /* Optional */ token,
        "credentials": Credential[],
        "contacts": Contact[]
    };
}

# Hashing

The storage and retrieval of user credentials uses hashing. Each repository should generate a random hash, and systems using that repository should retrieve the following from the repository: the hash, the number of hashing iterations, and the length of the hash result.

In CaSS, PBKDF2 (opens new window) using an HMAC SHA-1 hash is used for hashing user credentials, defined by the following parameters and function:

  • Value: string
  • Salt: string
  • Iterations: integer
  • Width: integer, length of the resultant hash in bytes

Additionally:

  • The number of iterations should be at least 5000.
  • The width of the result should be at least 32 bytes.
hash = pbkdf2(value, salt, iterations, width);

# Operations

# Prepare

The following function may be used to splice strings.

function splice(strings) {
    let result;

    /*
    for all strings i and character positions j:
        [stringi,j,stringi+1,j,stringi+2,j,...] + [stringi,j+1,stringi+1,j+1,stringi+2,j+1] + ...
    */

    return result;
}

Any non-displayable characters are omitted.

Given the following:

  • Username
  • Password
  • UsernameSalt
  • PasswordSalt
  • SecretSalt
  • UsernameIterations
  • PasswordIterations
  • SecretIterations
  • UsernameWidth
  • PasswordWidth
  • SecretWidth

The following functions hash a username and password, and provide a secret used to encrypt credentials:

usernameHash = pbkdf2(username, usernameSalt, usernameIterations, usernameWidth);
passwordHash = pbkdf2(password, passwordSalt, passwordIterations, passwordWidth);
secretHash = pbkdf2(splice(username, password), secretSalt, secretIterations, secretWidth);

secretHash is used as the secret in the encryption and decryption of Credential and Contact objects.

# Fetch

To fetch a credential package from a server, construct a CredentialRequest with the following functional definition:

function createCredentialRequest(usernameHash, passwordHash) {
    return {
        "@context": "http://schema.cassproject.org/kbac/0.2/"
        "@type": "CredentialRequest"
        "username": usernameHash
        "password": passwordHash
    }
}

The submission of this request to a server should occur over HTTPS. The response will be a Credentials object.

On the server side, the repository must:

  • Create or Load serverUrl, serverPpk, serverSecret, serverSalt, serverIterations, serverWidth
function saltedId(request) {
    return pbkdf2(
        request["username"],
        serverSalt,
        serverIterations,
        serverWidth
    );
}

function fetchResponse(request) {
    aesDecrypt(/*
        fetch encryptedValue WHERE
        @id = request["username"] AND
        rsaDecrypt(
            encryptedValue["payload"],
            serverSecret,
            saltedId
        )["password"] = request["password"]
    */);

    /* Then replace credentials["token"] with a new random token. */
    /* Store the credentials with the new token in the same fashion as commitResponse. */
    /* Return the result. */
}

# Store

To store a credential package in an identity server, create a CredentialCommit using the following functional definition:

function createCredentialCommit(
    usernameHash, passwordHash, secretHash, token, Credentials[], Contacts[]
) {
    return {
        "@context": "http://schema.cassproject.org/kbac/0.2/",
        "@type": "CredentialCommit",
        "username": usernameHash,
        "password": passwordHash,
        "token": token,
        "credentials": createCredentials(Credentials, Contacts, pk, secretHash)
    };
}

Credentials and Contacts should be created through the following parameterization:

createCredential(
    secret, // secretHash,
    iv, // Random iv,
    ppk, // Ppk of the user,
    displayNameIv, // Random iv,
    displayName // Display name for the user
) {...}

createContact(
    pk, // Pk of the contact
    secret, // secretHash
    iv, // Random iv
    displayNameIv, // Random iv
    sourceIv, // Random iv
    displayName, // Display name of the contact
    source, // Home server of the contact
) {...}

The submission of this request to a server should occur over HTTPS. The response will be a confirmation or error string.

The repository, upon receiving a credentialCommit shall:

  • Create or Load serverUrl, serverPpk, serverSecret, serverSalt, serverIterations, serverWidth
  • Execute the following functional definition:
commitResponse(request):
    if (
        isSuccessful(fetchResponse(request)) &&
        fetchResponse(request)["token"] === request["token"]
    ) {
        const signatureSheet = createSignatureSheetSignature(serverPpk,10000,serverUrl);
        const obj = {
            "@context": "http://schema.cassproject.org/kbac/0.2/",
            "@type": "EncryptedValue",
            "@owner": [pkFromPpk(serverPpk)],
            "@signature": [rsaSign(toSignableObject(this),serverPpk)],
            "plaintext": aesEncrypt(request)
        };
        /* Store obj at saltedId(request) using signatureSheet */
    }

# Repository

A KBAC repository is a REST-based repository, with its reference implementation built in LEVR using ElasticSearch for discovery, but may be implemented using different methods in different storage mediums such as relational databases, triple stores, or as static files.

The repository must follow these rules:

  • Search or Read requests shall hide EncryptedValue objects if an owner or reader signature is not provided in a signature sheet and validated.
  • Objects may only be stored if a signature is provided that validates the written object.
  • Before writing an object to a repository, the repository shall ensure that a valid signature has been provided in a signature sheet that matches one of the owners provided in the object in the repository (if an object exists in the repository), and that the signature in the object is valid.
  • Before deleting an object from a repository, the repository shall ensure that a valid signature in a signature sheet has been provided and matches an owner of the object in the repository.

# Web Service Operation Pseudocode

# Filter

function filter(result, signatureSheet) {
    for (let i = 0; i < result.length; i++) {
        /* Remove result[i] and its children */

        if (result[i].@type === "EncryptedValue") {
            const decryptable = (
                signatureSheetSignatureValid(signatureSheet[0..i], serverUrl) &&
                (result[i].@owner ∩ signatureSheetn.@owner) > 0
            );

            // Unable to decrypt?
            if (decryptable === false) {
                /* Delete result[i]; */
            }
        }
    }

    remove from
        result0..n
    resultn and children of resultn where
        (resultn.@type !== EncryptedValue unless
                signatureSheetSignatureValid(signatureSheet0..n,serverUrl) and
                (resultn.@owner0..j ∩ signatureSheetn.@owner > 0)
}

# Create

function create(object) {
    if (
        (/* SELECT record FROM store WHERE record.@id = id */) == null &&
        rsaVerify(toSignableObject(object), object.@signature0..i, object.@owner0..j)
    ) {
        /* Create object.@id = object */
    }
}

# Read

function retrieve(id, signatureSheet) {
    filter(/* SELECT record FROM store WHERE record.@id = id,signatureSheet */)
}

# Update

function update(object, signatureSheet) {
    if (
        rsaVerify(toSignableObject(object), object.@signature0..i, object.@owner0..j) &&
        signatureSheetSignatureValid(signatureSheet0..n, serverUrl) &&
        (retrieve(object.@id).@owner0..j ∩ signatureSheetn.@owner) > 0
    ) {
        /* Store object.@id = object */
    }
}

# Delete

function delete(object, signatureSheet) {
    if (
        signatureSheetSignatureValid(signatureSheets[i..n]) &&
        (retrieve(object.@id).@owner0..j ∩ signatureSheetn.@owner) > 0
    ) {
        /* Delete object.@id */
    }
}
function search(query, signatureSheet) {
    filter(/* Search for query, signatureSheet */)
}