# 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:
- A permission system that defines what operations an individual or system can perform on any object in CaSS.
- 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.
- 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.)
- A cryptographic signature that can be decrypted and validated using the public keys of identities listed in
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
- ex:
- 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:
- Must provide a signatureSheet with a valid signature for at least one owner.
- Must remove all invalid signatures.
- Should append at least one valid signature to the object.
The repository the object is being stored in:
- 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.
- If the object is being modified, must have at least one SignatureSheetSignature common with the unmodified object’s owner.
- 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:
- A signatureSheet is not provided with the request.
- A signatureSheetSignature is invalid (based on the criteria given previously).
- 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 */
}
}
# Search
function search(query, signatureSheet) {
filter(/* Search for query, signatureSheet */)
}