#!/usr/bin/env python3
"""
Generate a C header file based on a YAML description of PKCS #11
attributes. See comments in attributes.yaml for details.
"""
# Author: Rob Austein
# Copyright (c) 2015, NORDUnet A/S
# All rights reserved.
#
# Redistribution and use in source and binary forms, with or without
# modification, are permitted provided that the following conditions are
# met:
# - Redistributions of source code must retain the above copyright notice,
# this list of conditions and the following disclaimer.
#
# - Redistributions in binary form must reproduce the above copyright
# notice, this list of conditions and the following disclaimer in the
# documentation and/or other materials provided with the distribution.
#
# - Neither the name of the NORDUnet nor the names of its contributors may
# be used to endorse or promote products derived from this software
# without specific prior written permission.
#
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS
# IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED
# TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A
# PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
# HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED
# TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
# PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
# LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
# NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
# SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
# This requires a third-party YAML parser. On Debian-family Linux,
# you can install this with:
#
# sudo apt-get install python-yaml
import os
import sys
import yaml
import argparse
def define_flags(flag_names):
"""
Flag definitions. Called later, here at front of program just to
make them easier to find.
"""
flag_names.create("DEFAULT_VALUE", "Value field contains default")
flag_names.footnote( 1, "REQUIRED_BY_CREATEOBJECT")
flag_names.footnote( 2, "FORBIDDEN_BY_CREATEOBJECT")
flag_names.footnote( 3, "REQUIRED_BY_GENERATE")
flag_names.footnote( 4, "FORBIDDEN_BY_GENERATE")
flag_names.footnote( 5, "REQUIRED_BY_UNWRAP")
flag_names.footnote( 6, "FORBIDDEN_BY_UNWRAP")
flag_names.footnote( 7, "SENSITIVE")
flag_names.footnote( 8, "PERHAPS_MODIFIABLE")
flag_names.footnote( 9, "DEFAULT_IS_TOKEN_SPECIFIC")
flag_names.footnote(10, "ONLY_SO_USER_CAN_SET")
flag_names.footnote(11, "LATCHES_WHEN_TRUE")
flag_names.footnote(12, "LATCHES_WHEN_FALSE")
class PKCS11ParseError(Exception):
"Failure parsing PCKS #11 object definitions from YAML data."
def write_lines(*lines, **d):
"""
Utility to simplify writing formatted text to the output stream.
"""
for line in lines:
args.output_file.write((line % d) + "\n")
class Flags(object):
"""
Descriptor flag database.
Many of these are derived from PKCS #11 Table 15 footnotes
"""
prefix = "P11_DESCRIPTOR_" # Prefix string for all descriptor flags
def __init__(self):
self.names = []
self.notes = {}
self.width = 0
def create(self, name, comment = None):
"""
Create a descriptor flag.
"""
assert len(self.names) < 32
name = self.prefix + name
self.names.append((name, comment))
if len(name) > self.width:
self.width = len(name)
def footnote(self, number, name):
"""
Create a descriptor flag for a PKCS #11 table 15 footnote.
"""
assert number not in self.notes
self.create(name, "Section 10.2 table 15 footnote #%2d" % number)
self.notes[number] = self.prefix + name
def write(self):
"""
Generate the flags, assigning bit positions as we go.
"""
assert len(self.names) < 32
self.width = (((self.width + 4) >> 2) << 2) - 1
bit = 1
for name, comment in self.names:
format = "#define %(name)s 0x%(bit)08x"
if comment is not None:
format += " /* %(comment)s */"
write_lines(format, bit = bit, comment = comment, name = "%-*s" % (self.width, name))
bit <<= 1
class AttributeNumbers(dict):
"""
Attribute names and numbers scraped (yuck) from pkcs11t.h.
"""
def __init__(self, filename):
with open(filename, "r") as f:
for line in f:
word = line.split()
if len(word) <= 2 or word[0] != "#define" or not word[1].startswith("CKA_"):
continue
if word[2] in self:
continue
if word[2].startswith("(CKF_ARRAY_ATTRIBUTE|"):
word[2] = word[2].translate(None, "()").split("|")[1]
self[word[1]] = int(word[2], 16)
class Attribute(object):
"""
Definition of one attribute.
"""
def __init__(self, name, type = None, footnotes = None, default = None, value = None, unimplemented = False):
assert value is None or default is None
self.name = name
self.type = type
self.footnotes = footnotes
self.default = self.convert_integers(default)
self.value = self.convert_integers(value)
self.unimplemented = unimplemented
@staticmethod
def convert_integers(val):
"""
Convert a non-negative integer initialization value into a byte array.
"""
if not isinstance(val, int):
return val
if val < 0:
raise ValueError("Negative integers not legal here: %s" % val)
bytes = []
while val > 0:
bytes.insert(0, val & 0xFF)
val >>= 8
return bytes or [0]
def inherit(self, other):
"""
Merge values from paraent attribute definition, if any.
"""
for k in ("type", "footnotes", "default", "value"):
if getattr(self, k) is None:
setattr(self, k, getattr(other, k))
self.unimplemented = self.unimplemented or other.unimplemented
def format_flags(self):
"""
Generate the descriptor flags field.
"""
flags = []
if self.footnotes:
flags.extend(flag_names.notes[f] for f in self.footnotes)
if self.value is None and self.default is not None:
flags.append("P11_DESCRIPTOR_DEFAULT_VALUE")
flags = " | ".join(flags)
return flags or "0"
def format_size(self):
"""
Generate the descriptor size field.
"""
if isinstance(self.type, str) and self.type.startswith("CK_"):
return "sizeof(%s)" % self.type
elif self.type in ("rfc2279string", "biginteger", "bytearray"):
return "0"
else:
raise PKCS11ParseError("Unknown meta-type %r" % self.type)
def format_length(self):
"""
Generate the descriptor length field.
"""
value = self.value or self.default
if isinstance(value, list):
return "sizeof(const_0x%s)" % "".join("%02x" % v for v in value)
elif value and isinstance(self.type, str) and self.type.startswith("CK_"):
return "sizeof(%s)" % self.type
else:
return "0"
def format_value(self):
"""
Generate the descriptor value field.
"""
value = self.value or self.default
if not value:
return "NULL_PTR"
elif isinstance(value, list):
return "const_0x" + "".join("%02x" % v for v in value)
else:
return "&const_" + value
def format_constant(self, constants):
"""
Generate constant initializer values. These are merged so that we
only end up declaring one copy of each initializer value no matter
how many attributes use it.
"""
value = self.value or self.default
if not self.unimplemented and value:
if isinstance(value, list):
constants.add("static const CK_BYTE const_%s[] = { %s };" % (
"0x" + "".join("%02x" % v for v in value),
", ".join("0x%02x" % v for v in value)))
else:
constants.add("static const %s const_%s = %s;" % (self.type, value, value))
def generate(self):
"""
Generate the descriptor line for this attribute.
"""
if not self.unimplemented:
args.output_file.write(" { %s, %s, %s, %s, %s },\n" % (
self.name, self.format_size(), self.format_length(), self.format_value(), self.format_flags()))
class Class(object):
"""
A PKCS #11 class.
"""
def __init__(self, db, name, superclass = None, concrete = False, **attrs):
assert all(a.startswith("CKA_") for a in attrs), "Non-attribute: %r" % [a for a in attrs if not a.startswith("CKA_")]
self.attributes = dict((k, Attribute(k, **v)) for k, v in attrs.items())
self.db = db
self.name = name
self.superclass = superclass
self.concrete = concrete
def inherit(self, other):
"""
Inherit attributes from parent type.
"""
for k, v in other.attributes.items():
if k not in self.attributes:
self.attributes[k] = v
else:
self.attributes[k].inherit(v)
def collect_constants(self, constants):
"""
Collect initialization constants for all attributes.
"""
if self.concrete:
for a in self.attributes.values():
a.format_constant(constants)
def generate(self):
"""
Generate a descriptor for this type.
"""
if self.concrete:
write_lines("",
"static const p11_attribute_descriptor_t p11_attribute_descriptor_%(name)s[] = {",
name = self.name)
for a in sorted(self.attributes, key = lambda x: attribute_numbers[x]):
self.attributes[a].generate()
write_lines("};",
"",
"static const p11_descriptor_t p11_descriptor_%(name)s = {",
" p11_attribute_descriptor_%(name)s,",
" sizeof(p11_attribute_descriptor_%(name)s)/sizeof(p11_attribute_descriptor_t)",
"};",
name = self.name)
def keyclassmap(self):
"""
Generate a keyclass map entry if this is a concrete key type.
"""
if self.concrete and all(k in self.attributes and self.attributes[k].value for k in ("CKA_CLASS", "CKA_KEY_TYPE")):
write_lines(" { %s, %s, &p11_descriptor_%s }," % (
self.attributes["CKA_CLASS"].value, self.attributes["CKA_KEY_TYPE"].value, self.name))
class DB(object):
"""
Object type database parsed from YAML
"""
def __init__(self, y):
self.ordered = [Class(self, **y) for y in y]
self.named = dict((c.name, c) for c in self.ordered)
for c in self.ordered:
if c.superclass is not None:
c.inherit(self.named[c.superclass])
def generate(self):
"""
Generate output for everything in the database.
"""
constants = set()
for c in self.ordered:
c.collect_constants(constants)
for constant in sorted(constants):
write_lines(constant)
for c in self.ordered:
c.generate()
write_lines("",
"static const p11_descriptor_keyclass_map_t p11_descriptor_keyclass_map[] = {")
for c in self.ordered:
c.keyclassmap()
write_lines("};")
# Main program
parser = argparse.ArgumentParser(description = __doc__, formatter_class = argparse.ArgumentDefaultsHelpFormatter)
parser.add_argument("--pkcs11t-file", help = "Alternate location for pkcs11t.h", default = "pkcs11t.h")
parser.add_argument("yaml_file", help = "Input YAML file", nargs = "?", type = argparse.FileType("r"), default = sys.stdin)
parser.add_argument("output_file", help = "Output .h file", nargs = "?", type = argparse.FileType("w"), default = sys.stdout)
args = parser.parse_args()
attribute_numbers = AttributeNumbers(args.pkcs11t_file)
db = DB(yaml.load(args.yaml_file))
args.output_file.write('''\
/*
* This file was generated automatically from %(input)s by %(script)s. Do not edit this file directly.
*/
typedef struct {
CK_ATTRIBUTE_TYPE type;
CK_ULONG size; /* Size in bytes if this is a fixed-length attribute */
CK_ULONG length; /* Length in bytes of the object to which value points */
const void *value; /* Default or constant depending on P11_DESCRIPTOR_DEFAULT_VALUE */
unsigned long flags; /* (NULL value with P11_DESCRIPTOR_DEFAULT_VALUE means zero length default */
} p11_attribute_descriptor_t;
typedef struct {
const p11_attribute_descriptor_t *attributes;
CK_ULONG n_attributes;
} p11_descriptor_t;
typedef struct {
CK_OBJECT_CLASS object_class;
CK_KEY_TYPE key_type;
const p11_descriptor_t *descriptor;
} p11_descriptor_keyclass_map_t;
''' % dict(script = os.path.basename(sys.argv[0]), input = args.yaml_file.name))
flag_names = Flags()
define_flags(flag_names)
flag_names.write()
write_lines("")
db.generate()