Index: creddy/attr.c
===================================================================
--- creddy/attr.c	(revision aa33ad991a001be2a9dd44feb3fabfd87a93ba41)
+++ creddy/attr.c	(revision aa33ad991a001be2a9dd44feb3fabfd87a93ba41)
@@ -0,0 +1,225 @@
+#include <assert.h>
+#include <ctype.h>
+#include <string.h>
+#include <time.h>
+
+#include "creddy.h"
+#include "id.h"
+#include "libcreddy.h"
+
+#define ROLE_SEPARATOR " <- "
+#define INTERSECTION_SEP " & "
+#define SHA1_LENGTH 40
+
+struct _creddy_attribute_t {
+    creddy_id_t *issuer;
+    char *role;
+    int validity;
+
+    char **subject;
+    int num_subjects;
+    int size_subjects;
+    int total_len; // total length of all subject strings
+
+    certificate_t *cert; // NULL until baked
+};
+
+static char *_validate_principal(char *keyid);
+static void _append(creddy_attribute_t *attr, char *subject);
+char *_get_role_encoding(creddy_attribute_t *attr);
+
+int creddy_attribute_create(creddy_attribute_t **ret, creddy_id_t *issuer, char *role, int validity) {
+    if (creddy_id_privkey(issuer) == NULL)
+        return CREDDY_ATTRIBUTE_ISSUER_NOKEY;
+    if (!clean_name(role))
+        return CREDDY_ATTRIBUTE_INVALID_ROLE;
+    if (validity < 0)
+        return CREDDY_ATTRIBUTE_INVALID_VALIDITY;
+
+    creddy_attribute_t *attr = xmalloc(sizeof(creddy_attribute_t));
+    attr->issuer = issuer;
+    attr->role = xstrdup(role);
+    attr->validity = validity;
+
+    attr->num_subjects = 0;
+    attr->size_subjects = 4;
+    attr->subject = xmalloc(sizeof(char *) * attr->size_subjects);
+    attr->total_len = 0;
+
+    // NULL until baked
+    attr->cert = NULL;
+
+    *ret = attr;
+    return CREDDY_SUCCESS;
+}
+
+int creddy_attribute_principal(creddy_attribute_t *attr, char *keyid) {
+    char *copy = _validate_principal(keyid);
+    if (copy == NULL)
+        return 0;
+
+    _append(attr, copy);
+    return 1;
+}
+
+int creddy_attribute_role(creddy_attribute_t *attr, char *keyid, char *role) {
+    if (!clean_name(role))
+        return 0;
+
+    char *copy = _validate_principal(keyid);
+    if (copy == NULL)
+        return 0;
+
+    int len = strlen(copy) + strlen(role) + strlen(ROLE_SEPARATOR) + 1;
+    copy = xrealloc(copy, len);
+    strcat(copy, ".");
+    strcat(copy, role);
+
+    _append(attr, copy);
+    return 1;
+}
+
+int creddy_attribute_linking_role(creddy_attribute_t *attr, char *keyid, char *role, char *linked) {
+    if (!clean_name(role) || !clean_name(linked))
+        return 0;
+
+    char *copy = _validate_principal(keyid);
+    if (copy == NULL)
+        return 0;
+
+    int len = strlen(copy) + strlen(role) + strlen(ROLE_SEPARATOR) + 2;
+    copy = xrealloc(copy, len);
+    strcat(copy, ".");
+    strcat(copy, role);
+    strcat(copy, ".");
+    strcat(copy, linked);
+
+    _append(attr, copy);
+    return 1;
+}
+
+int creddy_attribute_bake(creddy_attribute_t *attr) {
+    if (attr->num_subjects == 0)
+        return 0;
+
+    char *role_encoding = _get_role_encoding(attr);
+
+    // create attribute cert
+    time_t not_before = time(NULL);
+    time_t not_after = not_before + attr->validity * 3600 * 60 * 60;
+    chunk_t serial = creddy_generate_serial();
+
+    certificate_t *attr_cert = lib->creds->create(lib->creds,
+        CRED_CERTIFICATE, CERT_X509_AC,
+        BUILD_CERT, creddy_id_cert(attr->issuer),
+        BUILD_NOT_BEFORE_TIME, not_before,
+        BUILD_NOT_AFTER_TIME, not_after,
+        BUILD_SERIAL, serial,
+        BUILD_IETF_GROUP_ATTR, role_encoding,
+        BUILD_SIGNING_CERT, creddy_id_cert(attr->issuer),
+        BUILD_SIGNING_KEY, creddy_id_privkey(attr->issuer),
+        BUILD_END
+    );
+    if (attr_cert == NULL)
+        return 0;
+
+    attr->cert = attr_cert;
+
+    free(role_encoding);
+    free(serial.ptr);
+    return 1;
+}
+
+// returns 1 if the cert hasn't been baked
+int creddy_attribute_write(creddy_attribute_t *attr, FILE *out) {
+    assert(attr != NULL);
+
+    if (attr->cert == NULL)
+        return 0;
+
+    // write to file
+    chunk_t encoding = attr->cert->get_encoding(attr->cert);
+    fwrite(encoding.ptr, encoding.len, 1, out);
+    free(encoding.ptr);
+
+    return 1;
+}
+
+void creddy_attribute_free(creddy_attribute_t *attr) {
+    int i;
+
+    if (attr == NULL)
+        return;
+
+    creddy_id_free(attr->issuer);
+    free(attr->role);
+    DESTROY_IF(attr->cert);
+
+    // free all the subjects
+    for (i = 0; i < attr->num_subjects; ++i)
+        free(attr->subject[i]);
+    free(attr->subject);
+
+    free(attr);
+}
+
+//
+// Helper functions below
+//
+
+// validate a princpal's name
+// makes sure it's a valid SHA1 identifier
+// return values:
+//  success: malloc'd copy with all hex digits lowercase
+//  fail: NULL
+static char *_validate_principal(char *keyid) {
+    int i;
+    char *copy = NULL;
+
+    if (strlen(keyid) != SHA1_LENGTH)
+        return NULL;
+
+    copy = xstrdup(keyid);
+    for (i = 0; i < SHA1_LENGTH; ++i) {
+        copy[i] = tolower(copy[i]);
+        if (!isxdigit(copy[i]))
+            goto error;
+    }
+
+    return copy;
+
+error:
+    free(copy);
+    return NULL;
+}
+
+static void _append(creddy_attribute_t *attr, char *subject) {
+    // expand the array if necessary
+    if (attr->num_subjects == attr->size_subjects) {
+        attr->size_subjects *= 2;
+        attr->subject = xrealloc(attr->subject, attr->size_subjects * sizeof(creddy_attribute_t));
+    }
+
+    attr->subject[attr->num_subjects++] = subject;
+    attr->total_len += strlen(subject);
+}
+
+char *_get_role_encoding(creddy_attribute_t *attr) {
+    int i, num_subjects;
+
+    num_subjects = attr->num_subjects;
+
+    int len = SHA1_LENGTH + 1 + strlen(attr->role) + strlen(ROLE_SEPARATOR);
+    len += attr->total_len + num_subjects * strlen(INTERSECTION_SEP);
+
+    char *encoding = xmalloc(len);
+    sprintf(encoding, "%s.%s" ROLE_SEPARATOR, creddy_id_keyid(attr->issuer), attr->role);
+
+    for (i = 0; i < num_subjects; ++i) {
+        strcat(encoding, attr->subject[i]);
+        if (i < num_subjects - 1)
+            strcat(encoding, INTERSECTION_SEP);
+    }
+
+    return encoding;
+}
Index: creddy/attribute.c
===================================================================
--- creddy/attribute.c	(revision 39fed7cfca3152c1d7385e267e7873c003b07590)
+++ creddy/attribute.c	(revision aa33ad991a001be2a9dd44feb3fabfd87a93ba41)
@@ -1,13 +1,11 @@
 #define GNU_SOURCE
+
+#include <err.h>
+#include <termios.h>
+
 #include <credentials/keys/private_key.h>
 
 #include "creddy.h"
-#include "id.h"
-
-#include <termios.h>
-
-#define ROLE_SEPARATOR " <- "
-#define INTERSECTION_SEP " & "
-#define SHA1_LENGTH 40
+#include "libcreddy.h"
 
 void attribute_main(options_t *opts) {
@@ -22,84 +20,4 @@
         usage(opts);
 
-    if (!clean_name(opts->role)) {
-        printf("bad role name %s\n", opts->role);
-        usage(opts);
-    }
-
-    role_len += SHA1_LENGTH + 1 + strlen(opts->role);
-    role_len += strlen(ROLE_SEPARATOR);
-
-    for (i = 0; i < opts->num_subjects; ++i) {
-        subject_t *cur = &opts->subjects[i];
-
-        // if we have an ID, make sure it's SHA1 and lowercase
-        if (cur->id) {
-            int j;
-            char *subject_id = cur->id;
-
-            // hex chars (also convert to lowercase)
-            for (j = 0; subject_id[j]; ++j) {
-                subject_id[j] = tolower(subject_id[j]);
-                if (!isxdigit(subject_id[j])) {
-                    printf("Invalid subject ID %s: must be SHA1\n", subject_id);
-                    usage(opts);
-                }
-            }
-
-            // correct length
-            if (j != SHA1_LENGTH) {
-                printf("Invalid subject ID %s: must be SHA1 (wrong length)\n", subject_id);
-                usage(opts);
-            }
-        }
-
-        // otherwise we have a cert
-        else {
-            creddy_id_t *subject = creddy_id_from_file(cur->cert);
-            if (subject == NULL)
-                errx(1, "Can't load subject cert from %s", cur->cert);
-            cur->id = xstrdup(creddy_id_keyid(subject));
-            creddy_id_free(subject);
-        }
-
-        role_len += SHA1_LENGTH;
-
-        // verify the subject role name if present
-        if (cur->role) {
-            char *role = cur->role;
-            char *start[3];
-            int name_parts = 0, j;
-
-            start[name_parts++] = role;
-
-            for (j = 0; role[j] != '\0'; ++j)
-                if (role[j] == '.') {
-                    if (name_parts == 3) {
-                        printf("bad subject role name (too many dots)\n");
-                        usage(opts);
-                    }
-                    start[name_parts++] = &role[j+1];
-                    role[j] = 0;
-                }
-
-            for (j = 0; j < name_parts; ++j)
-                if (!clean_name(start[j])) {
-                    printf("bad subject role name\n");
-                    usage(opts);
-                }
-
-            for (j = 1; j < name_parts; ++j)
-                *(start[j]-1) = '.'; // replace the dot
-
-            role_len += strlen(cur->role) + 1;
-        }
-
-        role_len += sizeof(INTERSECTION_SEP) - 1;
-    }
-
-    if (opts->validity < 0) {
-        printf("Validity must be >= 1 day\n");
-        usage(opts);
-    }
 
     // issuer
@@ -113,60 +31,79 @@
         errx(1, "Can't load private key from %s", opts->key);
 
-    // get the keyids
-    char *issuer_id = creddy_id_keyid(issuer);
+    creddy_attribute_t *attr = NULL;
+    ret = creddy_attribute_create(&attr, issuer, opts->role, opts->validity);
+    if (ret == CREDDY_ATTRIBUTE_ISSUER_NOKEY)
+        abort(); // should never happen
+    if (ret == CREDDY_ATTRIBUTE_INVALID_ROLE)
+        errx(1, "Invalid role name: %s", opts->role);
+    if (ret == CREDDY_ATTRIBUTE_INVALID_VALIDITY)
+        errx(1, "Invalid validity: must be >= 1 day");
 
-    // build the role encoding
-    char *role_encoding = xmalloc(role_len);
-    role_encoding[0] = '\0';
-
-    // role
-    sprintf(role_encoding, "%s.%s" ROLE_SEPARATOR, issuer_id, opts->role);
-
-    // subject(s)
     for (i = 0; i < opts->num_subjects; ++i) {
         subject_t *cur = &opts->subjects[i];
 
-        strcat(role_encoding, cur->id);
-        if (cur->role) {
-            strcat(role_encoding, ".");
-            strcat(role_encoding, cur->role);
+        // if we have a cert we need to get its ID
+        if (cur->cert) {
+            creddy_id_t *subject = creddy_id_from_file(cur->cert);
+            if (subject == NULL)
+                errx(1, "Can't load subject cert from %s", cur->cert);
+            cur->id = xstrdup(creddy_id_keyid(subject));
+            creddy_id_free(subject);
         }
 
-        if (i < opts->num_subjects - 1)
-            strcat(role_encoding, INTERSECTION_SEP);
+        // just a principal, add it
+        if (!cur->role) {
+            ret = creddy_attribute_principal(attr, cur->id);
+            if (!ret)
+                errx(1, "Invalid principal: %s", cur->id);
+        }
+
+        // either role or linking role
+        else {
+            char *role = cur->role;
+            char *start[3];
+            int name_parts = 0, j;
+
+            start[name_parts++] = role;
+
+            // split the role string up into name parts (turn . into \0)
+            for (j = 0; role[j] != '\0'; ++j)
+                if (role[j] == '.') {
+                    if (name_parts == 3) {
+                        printf("bad subject role name (too many dots)\n");
+                        usage(opts);
+                    }
+                    start[name_parts++] = &role[j+1];
+                    role[j] = 0;
+                }
+
+            // role
+            if (name_parts == 1) {
+                ret = creddy_attribute_role(attr, cur->id, start[0]);
+                if (!ret)
+                    errx(1, "Invalid role: %s.%s", cur->id, start[0]);
+            }
+
+            // linking role
+            else {
+                ret = creddy_attribute_linking_role(attr, cur->id, start[0], start[1]);
+                if (!ret)
+                    errx(1, "Invalid linking role: %s.%s.%s", cur->id, start[0], start[1]);
+            }
+        }
     }
 
-    // create attribute cert
-    time_t not_before = time(NULL);
-    time_t not_after = not_before + opts->validity * 3600 * 60 * 60;
-    chunk_t serial = creddy_generate_serial();
-
-    certificate_t *attr_cert = lib->creds->create(lib->creds,
-        CRED_CERTIFICATE, CERT_X509_AC,
-        BUILD_CERT, creddy_id_cert(issuer),
-        BUILD_NOT_BEFORE_TIME, not_before,
-        BUILD_NOT_AFTER_TIME, not_after,
-        BUILD_SERIAL, serial,
-        BUILD_IETF_GROUP_ATTR, role_encoding,
-        BUILD_SIGNING_CERT, creddy_id_cert(issuer),
-        BUILD_SIGNING_KEY, creddy_id_privkey(issuer),
-        BUILD_END
-    );
-    if (attr_cert == NULL)
-        errx(1, "Couldn't build attribute cert");
-
-    // write to file
-    chunk_t encoding = attr_cert->get_encoding(attr_cert);
+    ret = creddy_attribute_bake(attr);
+    if (!ret)
+        errx(1, "Couldn't bake attribute cert");
 
     FILE *out = fopen(opts->out, "w");
     if (out == NULL)
-        err(1, "Can't open attribute cert output file %s", opts->out);
-    fwrite(encoding.ptr, encoding.len, 1, out);
+        err(1, "Couldn't open attr cert file %s for writing", opts->out);
+
+    creddy_attribute_write(attr, out);
+
     fclose(out);
 
-    free(role_encoding);
-    free(serial.ptr);
-    DESTROY_IF(attr_cert);
-    free(encoding.ptr);
-    creddy_id_free(issuer);
+    creddy_attribute_free(attr);
 }
Index: creddy/libcreddy.h
===================================================================
--- creddy/libcreddy.h	(revision 39fed7cfca3152c1d7385e267e7873c003b07590)
+++ creddy/libcreddy.h	(revision aa33ad991a001be2a9dd44feb3fabfd87a93ba41)
@@ -7,4 +7,7 @@
 #define CREDDY_GENERATE_INVALID_CN          -1
 #define CREDDY_GENERATE_INVALID_VALIDITY    -2
+#define CREDDY_ATTRIBUTE_ISSUER_NOKEY       -3
+#define CREDDY_ATTRIBUTE_INVALID_ROLE       -4
+#define CREDDY_ATTRIBUTE_INVALID_VALIDITY   -5
 
 /**
@@ -46,3 +49,38 @@
 void creddy_id_free(creddy_id_t *id);
 
+/**
+ * Attribute cert
+ */
+typedef struct _creddy_attribute_t creddy_attribute_t;
+
+//
+// Here's the skinny:
+//  Attribute cert objects don't contain an actual cert until they're baked.
+//  First you construct the object using creddy_attribute_create, then you add
+//  subjects to it using creddy_attribute_{principal,role,linking_role}.
+//  Finally you bake it. Once you've done that, you can access the DER encoding
+//  or write it to a file.
+//
+
+// create an attribute cert
+// validity is in days
+// returns one of CREDDY_SUCCESS or CREDDY_ATTRIBUTE_* (see top)
+int creddy_attribute_create(creddy_attribute_t **attr, creddy_id_t *issuer, char *role, int validity);
+
+// add a principal subject to the cert
+int creddy_attribute_principal(creddy_attribute_t *attr, char *keyid);
+
+// add a role subject
+int creddy_attribute_role(creddy_attribute_t *attr, char *keyid, char *role);
+
+// add a linking role subject
+int creddy_attribute_linking_role(creddy_attribute_t *attr, char *keyid, char *role, char *linked);
+
+// create the attribute cert once all the subjects have been added
+// can return 0 if there are no subjects or there's a problem building the cert
+int creddy_attribute_bake(creddy_attribute_t *attr);
+
+// destroy the cert
+void creddy_attribute_free(creddy_attribute_t *attr);
+
 #endif /* __LIBCREDDY_H__ */
