Redesign the Keyring REST API (fixes #132)

The new API follows REST rules by using the proper request verbs:
POST, PUT, PATCH and DELETE, instead of just GET.

The legacy GET-only API is still supported for backward compatibility,
but not longer tested or documented.

Add a new query-single-identity operation.

Implement the lock-single-identity operation, which until now had been
documented but not yet implemented.  Whenever a single identity is
locked (released), any other unlocked identities with the same PIN are
flagged to indicate that the PIN is not "fully" unlocked, so that the
next time the PIN is entered, the slot decryption is re-tried for
non-loaded identities, and the locked identity will be unlocked again.

Update the 'keyring' and 'keyringrestful' test scripts:
- refactored to reduce curl command-line clutter in test cases
- now tests the redesigned request verbs and paths
- added a test for GET /restful/keyring/SID
- added a test for PUT /restful/keyring/SID/lock
This commit is contained in:
Andrew Bettison 2018-03-08 10:55:36 +10:30
parent 8242ca0a00
commit 98ec1c9608
7 changed files with 441 additions and 307 deletions

View File

@ -158,7 +158,8 @@ Keyring REST API operations
Returns a list of all currently unlocked identities, one identity per row in
[JSON table][] format. The following parameters are recognised:
* **pin**: see [identity unlocking](#identity-unlocking)
* **pin** (optional) = a passphrase to unlock identities prior to listing;
see [identity unlocking](#identity-unlocking)
The table columns are:
@ -169,46 +170,67 @@ The table columns are:
| `did` | the optional [DID](#did) (telephone number); `null` if none is assigned |
| `name` | the optional string [Name](#name); `null` if none is assigned |
### GET /restful/keyring/add
### POST /restful/keyring/add
Creates a new identity with a random [SID](#serval-id). The following
parameters are recognised:
Creates a new identity with a random [SID](#serval-id). This request does not
accept parameters in the request body (eg, using a [Content-Type][] of
[multipart/form-data][]), but does accept the following [query parameters][] in
the *path*:
* **pin**: if present, then the new identity is protected by the given
passphrase; see [identity unlocking](#identity-unlocking) -- note that the
newly created identity is already unlocked when this request returns,
because the passphrase has been added to the PIN cache
* **did**: the DID (phone number); empty or absent to indicate no DID,
otherwise must conform to the rules for [DID](#did)
* **name**: the name; empty or absent to specify no name, otherwise must
conform to the rules for [Name](#name)
* **pin** (optional) = the passphrase for the new identity; if present, then
the new identity is protected by the given passphrase; see [identity
unlocking](#identity-unlocking) -- note that the newly created identity is
already unlocked when this request returns, because the passphrase has been
added to the PIN cache
* **did** (optional) = the [DID](#did) of the new identity; empty or absent
to indicate no DID, otherwise must be valid
* **name** (optional) = the [Name](#name) of the new identity; empty or
absent to specify no name, otherwise must be valid
If any parameter contains an invalid value then the request returns [400 Bad
Request][400]. Returns [201 Created][201] if an identity is created; the [JSON
result](#keyring-json-result) describes the identity that was created.
### GET /restful/keyring/SID/remove
### GET /restful/keyring/SID
Removes an existing identity with a given [SID](#serval-id). The following
parameters are recognised:
Return the details of an existing unlocked identity with a given
[SID](#serval-id). The following [query parameters][] are recognised:
* **pin**: see [identity unlocking](#identity-unlocking)
* **pin** (optional) = a passphrase to unlock the identity prior to querying
it; see [identity unlocking](#identity-unlocking)
If there is no unlocked identity with the given SID, this request returns [404
Not Found][404]. Otherwise it returns [200 OK][200] and the [JSON
result](#keyring-json-result) describes the identity.
### DELETE /restful/keyring/SID
Removes an existing unlocked identity with a given [SID](#serval-id). The
following [query parameters][] are recognised:
* **pin** (optional) = a passphrase to unlock the identity prior to deleting
it; see [identity unlocking](#identity-unlocking)
If there is no unlocked identity with the given SID, this request returns [404
Not Found][404]. Otherwise it returns [200 OK][200] and the [JSON
result](#keyring-json-result) describes the identity that was removed.
### GET /restful/keyring/SID/set
### PATCH /restful/keyring/SID
Sets and/or clears the [DID](#did) and/or [Name](#name) of the unlocked
identity that has the given [SID](#serval-id). The following parameters are
recognised:
identity that has the given [SID](#serval-id). The following [query
parameters][] are recognised:
* **pin**: see [identity unlocking](#identity-unlocking)
* **did**: the DID (phone number); empty to clear the DID, otherwise must
conform to the rules for [DID](#did)
* **name**: the name; empty to clear the name, otherwise must conform to the
rules for [Name](#name)
* **pin** (optional) = a passphrase to unlock the identity prior to deleting
it; see [identity unlocking](#identity-unlocking)
* **did** (optional) = the [DID](#did) to assign to the identity; empty to
clear the DID, otherwise must be valid
* **name** (optional) = the [Name](#name) to assign to the identity; empty to
clear the name, otherwise must be valid
If a parameter is missing, then the corresponding field of the identity is left
unchanged. If a parameter is set to an empty string, then the corresponding
@ -218,7 +240,7 @@ If any parameter contains an invalid value then the request returns [400 Bad
Request][400]. If there is no unlocked identity with the given SID, this
request returns [404 Not Found][404].
### GET /restful/keyring/SID/lock
### PUT /restful/keyring/SID/lock
Locks an existing identity with a given [SID](#serval-id).
@ -229,7 +251,7 @@ result](#keyring-json-result) describes the identity that was locked.
-----
**Copyright 2015 Serval Project Inc.**
**Copyright 2016-2017 Flinders University**
**Copyright 2016-2018 Flinders University**
![CC-BY-4.0](./cc-by-4.0.png)
Available under the [Creative Commons Attribution 4.0 International licence][CC BY 4.0].

299
keyring.c
View File

@ -131,7 +131,7 @@ static int keyring_load(keyring_file *k, const char *pin)
keyring_bam **b=&k->bam;
size_t offset = 0;
while (offset < k->file_size) {
/* Read bitmap from slab. If offset is zero, read the salt */
/* Read allocmap from slab. If offset is zero, read the salt */
if (fseeko(k->file, (off_t)offset, SEEK_SET)) {
WHYF_perror("fseeko(%d, %zd, SEEK_SET)", fileno(k->file), offset);
return WHY("Could not seek to BAM in keyring file");
@ -140,13 +140,13 @@ static int keyring_load(keyring_file *k, const char *pin)
if (!*b)
return WHYF("Could not allocate keyring_bam structure");
(*b)->file_offset = offset;
/* Read bitmap */
int r = fread((*b)->bitmap, KEYRING_BAM_BYTES, 1, k->file);
/* Read allocation bitmap */
int r = fread((*b)->allocmap, KEYRING_BAM_BYTES, 1, k->file);
if (r != 1) {
WHYF_perror("fread(%p, %zd, 1, %d)", (*b)->bitmap, KEYRING_BAM_BYTES, fileno(k->file));
WHYF_perror("fread(%p, %zd, 1, %d)", (*b)->allocmap, KEYRING_BAM_BYTES, fileno(k->file));
return WHYF("Could not read BAM from keyring file");
}
/* Read salt if this is the first bitmap block.
/* Read salt if this is the first allocmap block.
We setup a context for this self-supplied key-ring salt.
(other keyring salts may be provided later on, resulting in
multiple contexts being loaded) */
@ -169,6 +169,52 @@ static int keyring_load(keyring_file *k, const char *pin)
return 0;
}
static unsigned is_slot_allocated(const keyring_file *k, unsigned slot)
{
assert(slot != 0);
assert(slot < KEYRING_BAM_BITS);
unsigned position = slot & (KEYRING_BAM_BITS - 1);
unsigned byte = position >> 3;
unsigned bit = position & 7;
return (k->bam->allocmap[byte] & (1 << bit)) ? 1 : 0;
}
static unsigned is_slot_loadable(const keyring_file *k, unsigned slot)
{
assert(slot != 0);
assert(slot < KEYRING_BAM_BITS);
unsigned position = slot & (KEYRING_BAM_BITS - 1);
unsigned byte = position >> 3;
unsigned bit = position & 7;
return (k->bam->allocmap[byte] & (1 << bit)) && !(k->bam->loadmap[byte] & (1 << bit));
}
static void mark_slot_allocated(keyring_file *k, unsigned slot, int allocated)
{
assert(slot != 0);
assert(slot < KEYRING_BAM_BITS);
unsigned position = slot & (KEYRING_BAM_BITS - 1);
unsigned byte = position >> 3;
unsigned bit = position & 7;
if (allocated)
k->bam->allocmap[byte] |= (1 << bit);
else
k->bam->allocmap[byte] &= ~(1 << bit);
}
static void mark_slot_loaded(keyring_file *k, unsigned slot, int loaded)
{
assert(slot != 0);
assert(slot < KEYRING_BAM_BITS);
unsigned position = slot & (KEYRING_BAM_BITS - 1);
unsigned byte = position >> 3;
unsigned bit = position & 7;
if (loaded)
k->bam->loadmap[byte] |= (1 << bit);
else
k->bam->loadmap[byte] &= ~(1 << bit);
}
void keyring_iterator_start(keyring_file *k, keyring_iterator *it)
{
assert(k);
@ -321,34 +367,72 @@ void keyring_free(keyring_file *k)
return;
}
int keyring_release_identities_by_pin(keyring_file *f, const char *pin)
/* Release ("lock") all the identities that have a given PIN, by unlinking them from the in-memory
* cache list. DOES call keyring_free_identity(id) on each released identity. When this function
* returns, there are no unlocked identities with the given PIN.
*/
void keyring_release_identities_by_pin(keyring_file *k, const char *pin)
{
keyring_identity **i=&f->identities;
INFO("release identity by PIN");
DEBUGF(keyring, "release identity PIN=%s", alloca_str_toprint(pin));
keyring_identity **i=&k->identities;
while(*i){
keyring_identity *id = (*i);
if (id->PKRPin && strcmp(id->PKRPin, pin) == 0){
if (id->PKRPin && strcmp(id->PKRPin, pin) == 0) {
INFOF("release identity slot=%u SID=%s", id->slot, alloca_tohex_sid_t(*id->box_pk));
(*i) = id->next;
mark_slot_loaded(k, id->slot, 0);
keyring_free_identity(id);
}else{
i=&id->next;
}
}
return 0;
}
/* Release the given single identity by unlinking it from the in-memory cache list. To ensure that
* re-entering the identity's PIN will unlock it again, mark any other unlocked identities that have
* the same PIN as no longer "fully" unlocked, so that keyring_enter_pin() will re-try the
* decryption. Does NOT call keyring_free_identity(id), so the identity's in-memory structure
* remain intact; the caller is responsible for freeing the identity.
*/
void keyring_release_identity(keyring_file *k, keyring_identity *id)
{
INFOF("release identity slot=%u SID=%s", id->slot, alloca_tohex_sid_t(*id->box_pk));
keyring_identity **prev = NULL;
keyring_identity **i;
// find the identity in the keyring's linked list, so it can be unlinked
for (i = &k->identities; *i; i = &(*i)->next) {
keyring_identity *iid = *i;
if (iid == id)
prev = i;
// mark any other identities that have the same PIN as no longer fully unlocked
if (id->PKRPin && iid->PKRPin && strcmp(id->PKRPin, iid->PKRPin) == 0)
iid->is_fully_unlocked = 0;
}
assert(prev); // the identity being released must be in the keyring
(*prev) = id->next;
mark_slot_loaded(k, id->slot, 0);
}
/* Release the single identity with the given SID, by unlinking it from the in-memory cache list.
* See the comment on keyring_release_identity() regarding PIN management. Returns zero if an
* identity was found and released, -1 if no such identity was found. Unlike
* keyring_release_identity(), this function DOES call keyring_free_identity(id) on any released
* identity before returning.
*/
int keyring_release_subscriber(keyring_file *k, const sid_t *sid)
{
keyring_identity **i=&k->identities;
while(*i){
keyring_identity *id = (*i);
if (cmp_sid_t(id->box_pk,sid)==0){
(*i) = id->next;
keyring_free_identity(id);
INFOF("release identity SID=%s", alloca_tohex_sid_t(*sid));
keyring_identity **i;
for (i = &k->identities; *i; i = &(*i)->next) {
keyring_identity *iid = *i;
if (cmp_sid_t(iid->box_pk, sid) == 0) {
keyring_release_identity(k, iid);
keyring_free_identity(iid);
return 0;
}
i=&id->next;
}
return WHYF("Keyring entry for %s not found", alloca_tohex_sid_t(*sid));
return WHYF("cannot release non-existent keyring entry SID=%s", alloca_tohex_sid_t(*sid));
}
void keyring_free_identity(keyring_identity *id)
@ -1064,7 +1148,7 @@ static int keyring_identity_add_keypair(keyring_identity *id, keypair *kp)
return 1;
}
static keyring_identity *keyring_unpack_identity(unsigned char *slot, const char *pin)
static keyring_identity *keyring_unpack_identity(unsigned char *slot_data, const char *pin)
{
/* Skip salt and MAC */
keyring_identity *id = emalloc_zero(sizeof(keyring_identity));
@ -1073,11 +1157,11 @@ static keyring_identity *keyring_unpack_identity(unsigned char *slot, const char
if (pin && *pin)
id->PKRPin = str_edup(pin);
// The two bytes immediately following the MAC describe the rotation offset.
uint16_t rotation = (slot[PKR_SALT_BYTES + PKR_MAC_BYTES] << 8) | slot[PKR_SALT_BYTES + PKR_MAC_BYTES + 1];
uint16_t rotation = (slot_data[PKR_SALT_BYTES + PKR_MAC_BYTES] << 8) | slot_data[PKR_SALT_BYTES + PKR_MAC_BYTES + 1];
/* Pack the key pairs into the rest of the slot as a rotated buffer. */
struct rotbuf rbuf;
rotbuf_init(&rbuf,
slot + PKR_SALT_BYTES + PKR_MAC_BYTES + 2,
slot_data + PKR_SALT_BYTES + PKR_MAC_BYTES + 2,
KEYRING_PAGE_SIZE - (PKR_SALT_BYTES + PKR_MAC_BYTES + 2),
rotation);
while (!rbuf.wrap) {
@ -1231,130 +1315,117 @@ static int keyring_finalise_identity(uint8_t *dirty, keyring_identity *id)
* munged, we then need to verify that the slot is valid, and if so unpack the details of the
* identity.
*/
static int keyring_decrypt_pkr(keyring_file *k, const char *pin, int slot_number)
static int keyring_decrypt_pkr(keyring_file *k, const char *pin, int slot)
{
DEBUGF(keyring, "k=%p, pin=%s slot_number=%d", k, alloca_str_toprint(pin), slot_number);
unsigned char slot[KEYRING_PAGE_SIZE];
DEBUGF(keyring, "k=%p pin=%s slot=%d", k, alloca_str_toprint(pin), slot);
unsigned char slot_data[KEYRING_PAGE_SIZE];
keyring_identity *id=NULL;
/* 1. Read slot. */
if (fseeko(k->file,slot_number*KEYRING_PAGE_SIZE,SEEK_SET))
if (fseeko(k->file,slot*KEYRING_PAGE_SIZE,SEEK_SET))
return WHY_perror("fseeko");
if (fread(slot, KEYRING_PAGE_SIZE, 1, k->file) != 1)
if (fread(slot_data, KEYRING_PAGE_SIZE, 1, k->file) != 1)
return WHY_perror("fread");
/* 2. Decrypt data from slot. */
if (keyring_munge_block(slot, KEYRING_PAGE_SIZE, k->KeyRingSalt, k->KeyRingSaltLen, k->KeyRingPin, pin)) {
WHYF("keyring_munge_block() failed, slot=%u", slot_number);
if (keyring_munge_block(slot_data, KEYRING_PAGE_SIZE, k->KeyRingSalt, k->KeyRingSaltLen, k->KeyRingPin, pin)) {
WHYF("keyring_munge_block() failed, slot=%u", slot);
goto kdp_safeexit;
}
/* 3. Unpack contents of slot into a new identity in the provided context. */
DEBUGF(keyring, "unpack slot %u", slot_number);
if (((id = keyring_unpack_identity(slot, pin)) == NULL))
DEBUGF(keyring, "unpack slot %u", slot);
if (((id = keyring_unpack_identity(slot_data, pin)) == NULL))
goto kdp_safeexit; // Not a valid slot
id->slot = slot_number;
id->slot = slot;
/* 4. Verify that slot is self-consistent (check MAC) */
unsigned char hash[crypto_hash_sha512_BYTES];
if (keyring_identity_mac(id, slot, hash))
if (keyring_identity_mac(id, slot_data, hash))
goto kdp_safeexit;
/* compare hash to record */
if (memcmp(hash, &slot[PKR_SALT_BYTES], crypto_hash_sha512_BYTES)) {
DEBUGF(keyring, "slot %u is not valid (MAC mismatch)", slot_number);
if (memcmp(hash, &slot_data[PKR_SALT_BYTES], crypto_hash_sha512_BYTES)) {
DEBUGF(keyring, "slot %u is not valid (MAC mismatch)", slot);
DEBUG_dump(keyring, "computed",hash,crypto_hash_sha512_BYTES);
DEBUG_dump(keyring, "stored",&slot[PKR_SALT_BYTES],crypto_hash_sha512_BYTES);
DEBUG_dump(keyring, "stored",&slot_data[PKR_SALT_BYTES],crypto_hash_sha512_BYTES);
goto kdp_safeexit;
}
if (keyring_commit_identity(k, id)!=1)
if (!keyring_commit_identity(k, id))
goto kdp_safeexit;
INFOF("unlocked identity slot=%u SID=%s", id->slot, alloca_tohex_sid_t(*id->box_pk));
return 0;
kdp_safeexit:
/* Clean up any potentially sensitive data before exiting */
bzero(slot,KEYRING_PAGE_SIZE);
bzero(slot_data,KEYRING_PAGE_SIZE);
bzero(hash,crypto_hash_sha512_BYTES);
if (id)
keyring_free_identity(id);
return 1;
return -1;
}
/* Try all valid slots with the PIN and see if we find any identities with that PIN.
We might find more than one. */
int keyring_enter_pin(keyring_file *k, const char *pin)
/* Try all valid slots with the PIN and see if we find any identities with that PIN. We might find
* none, or more than one. Returns the total number of unlocked identities with the given PIN,
* including any that were already unlocked before this function was called.
*/
unsigned keyring_enter_pin(keyring_file *k, const char *pin)
{
IN();
DEBUGF(keyring, "k=%p, pin=%s", k, alloca_str_toprint(pin));
if (!pin) pin="";
// Check if PIN is already entered.
int identitiesFound=0;
keyring_identity *id = k->identities;
while(id){
if (pin && *pin){
if (id->PKRPin && strcmp(id->PKRPin, pin) == 0)
identitiesFound++;
}else if(!id->PKRPin)
identitiesFound++;
id=id->next;
unsigned identity_count = 0;
bool_t is_fully_unlocked = 1;
// check if PIN is already entered and all identities with that PIN are currently
{
keyring_identity *id;
for (id = k->identities; id; id = id->next) {
if (*pin ? (id->PKRPin && strcmp(id->PKRPin, pin) == 0) : (!id->PKRPin)) {
++identity_count;
if (!id->is_fully_unlocked)
is_fully_unlocked = 0;
}
}
}
if (identitiesFound)
RETURN(identitiesFound);
// try to decrypt if there are no identities already open with the given PIN, or if the PIN is not
// fully unlocked
if (!identity_count || !is_fully_unlocked) {
unsigned slot;
for(slot=0;slot<k->file_size/KEYRING_PAGE_SIZE;slot++) {
/* slot zero is the BAM and salt, so skip it */
if (slot&(KEYRING_BAM_BITS-1)) {
/* Not a BAM slot, so examine */
size_t file_offset = slot * KEYRING_PAGE_SIZE;
unsigned slots = k->file_size / KEYRING_PAGE_SIZE;
if (slots >= KEYRING_BAM_BITS)
slots = KEYRING_BAM_BITS - 1;
// slot zero is the BAM and salt, so skip it
for (slot = 1; slot < slots; ++slot) {
// only try to decrypt slots that are marked as allocated and not already loaded; the cost of
// decrypting can be up to a second of CPU time on a phone
if (is_slot_loadable(k, slot)) {
if (keyring_decrypt_pkr(k, pin, slot) == 0) {
mark_slot_loaded(k, slot, 1);
++identity_count;
}
}
}
/* See if this part of the keyring file is organised */
keyring_bam *b=k->bam;
while (b && (file_offset >= b->file_offset + KEYRING_SLAB_SIZE))
b=b->next;
if (!b) continue;
/* Now see if slot is marked in-use. No point checking unallocated slots,
especially since the cost can be upto a second of CPU time on a phone. */
int position=slot&(KEYRING_BAM_BITS-1);
int byte=position>>3;
int bit=position&7;
if (b->bitmap[byte]&(1<<bit)) {
/* Slot is occupied, so check it.
We have to check it for each keyring context (ie keyring pin) */
if (keyring_decrypt_pkr(k, pin, slot) == 0)
++identitiesFound;
// now all identities with the given PIN have been unlocked, so mark the PIN being fully
// unlocked
if (!is_fully_unlocked) {
keyring_identity *id;
for (id = k->identities; id; id = id->next) {
if (*pin ? (id->PKRPin && strcmp(id->PKRPin, pin) == 0) : (!id->PKRPin)) {
id->is_fully_unlocked = 1;
}
}
}
if (k->dirty)
keyring_commit(k);
}
RETURN(identitiesFound);
RETURN(identity_count);
OUT();
}
static unsigned test_slot(const keyring_file *k, unsigned slot)
{
assert(slot < KEYRING_BAM_BITS);
unsigned position = slot & (KEYRING_BAM_BITS - 1);
unsigned byte = position >> 3;
unsigned bit = position & 7;
return (k->bam->bitmap[byte] & (1 << bit)) ? 1 : 0;
}
static void set_slot(keyring_file *k, unsigned slot, int bitvalue)
{
assert(slot < KEYRING_BAM_BITS);
unsigned position = slot & (KEYRING_BAM_BITS - 1);
unsigned byte = position >> 3;
unsigned bit = position & 7;
if (bitvalue)
k->bam->bitmap[byte] |= (1 << bit);
else
k->bam->bitmap[byte] &= ~(1 << bit);
}
/* Find free slot in keyring. Slot 0 in any slab is the BAM and possible keyring salt, so only
* search for space in slots 1 and above. TODO: Extend to handle more than one slab!
*/
@ -1364,22 +1435,28 @@ static unsigned find_free_slot(const keyring_file *k)
unsigned slot;
// walk the list of slots, randomising the low order bits of the index
unsigned mask = randombytes_uniform(KEYRING_ALLOC_CHUNK);
for (i = 0; i < KEYRING_BAM_BITS; ++i){
slot = 1+(i ^ mask);
for (i = 0; i < KEYRING_BAM_BITS; ++i) {
slot = 1 + (i ^ mask);
DEBUGF(keyring, "Check %u, is slot %u free?", i, slot);
if (slot < KEYRING_BAM_BITS && !test_slot(k, slot))
if (slot < KEYRING_BAM_BITS && !is_slot_allocated(k, slot))
return slot;
}
return 0;
}
/* Return non-zero if the identity was successfully added, zero if the identity was not added
* because the SID is already used by an existng identity in the keyring.
*/
static int keyring_commit_identity(keyring_file *k, keyring_identity *id)
{
keyring_finalise_identity(&k->dirty, id);
// Do nothing if an identity with this sid already exists
if (keyring_find_identity_sid(k, id->box_pk))
if (keyring_find_identity_sid(k, id->box_pk)) {
DEBUGF(keyring, "identity not committed, SID already in use: SID=%s", alloca_tohex_sid_t(*id->box_pk));
return 0;
set_slot(k, id->slot, 1);
}
mark_slot_allocated(k, id->slot, 1);
mark_slot_loaded(k, id->slot, 1);
keyring_identity **i=&k->identities;
while(*i)
@ -1387,6 +1464,7 @@ static int keyring_commit_identity(keyring_file *k, keyring_identity *id)
*i=id;
add_subscriber(id);
DEBUGF(keyring, "identity committed slot=%u SID=%s", id->slot, alloca_tohex_sid_t(id->subscriber->sid));
return 1;
}
@ -1418,7 +1496,7 @@ keyring_identity *keyring_inmemory_identity(){
keyring_finalise_identity(NULL, id);
if (id)
add_subscriber(id);
INFOF("Created in-memory identity: %s", alloca_tohex_sid_t(id->subscriber->sid));
INFOF("created in-memory identity SID=%s", alloca_tohex_sid_t(id->subscriber->sid));
return id;
}
@ -1449,13 +1527,13 @@ keyring_identity *keyring_create_identity(keyring_file *k, const char *pin)
WHY("no free slots in first slab (no support for more than one slab)");
goto kci_safeexit;
}
DEBUGF(keyring, "Allocate identity into slot %u", id->slot);
/* Mark slot as occupied and internalise new identity. */
if (keyring_commit_identity(k, id)!=1)
if (!keyring_commit_identity(k, id))
goto kci_safeexit;
/* Everything went fine */
INFOF("created identity slot=%u SID=%s", id->slot, alloca_tohex_sid_t(id->subscriber->sid));
k->dirty = 1;
return id;
@ -1467,7 +1545,7 @@ keyring_identity *keyring_create_identity(keyring_file *k, const char *pin)
static int write_random_slot(keyring_file *k, unsigned slot)
{
if (test_slot(k, slot)!=0)
if (is_slot_allocated(k, slot))
return 0;
DEBUGF(keyring, "Fill slot %u with randomness", slot);
@ -1502,8 +1580,9 @@ void keyring_destroy_identity(keyring_file *k, keyring_identity *id)
assert(id->slot != 0);
DEBUGF(keyring, "Destroy identity in slot %u", id->slot);
// Mark the slot as unused in the BAM.
set_slot(k, id->slot, 0);
// Mark the slot as unallocated in the BAM and un-loaded in memory.
mark_slot_allocated(k, id->slot, 0);
mark_slot_loaded(k, id->slot, 0);
// Fill the slot in the file with random bytes.
write_random_slot(k, id->slot);
@ -1526,8 +1605,8 @@ int keyring_commit(keyring_file *k)
if (fseeko(k->file, b->file_offset, SEEK_SET) == -1) {
WHYF_perror("fseeko(%d, %ld, SEEK_SET)", fileno(k->file), (long)b->file_offset);
errorCount++;
} else if (fwrite(b->bitmap, KEYRING_BAM_BYTES, 1, k->file) != 1) {
WHYF_perror("fwrite(%p, %ld, 1, %d)", b->bitmap, (long)KEYRING_BAM_BYTES, fileno(k->file));
} else if (fwrite(b->allocmap, KEYRING_BAM_BYTES, 1, k->file) != 1) {
WHYF_perror("fwrite(%p, %ld, 1, %d)", b->allocmap, (long)KEYRING_BAM_BYTES, fileno(k->file));
errorCount++;
} else if (fwrite(k->KeyRingSalt, k->KeyRingSaltLen, 1, k->file)!=1) {
WHYF_perror("fwrite(%p, %ld, 1, %d)", k->KeyRingSalt, (long)k->KeyRingSaltLen, fileno(k->file));
@ -2104,7 +2183,7 @@ int keyring_load_from_dump(keyring_file *k, unsigned entry_pinc, const char **en
if (id == NULL || idn != last_idn) {
last_idn = idn;
if (id){
if (keyring_commit_identity(k, id)!=1)
if (!keyring_commit_identity(k, id))
keyring_free_identity(id);
else
k->dirty=1;
@ -2129,7 +2208,7 @@ int keyring_load_from_dump(keyring_file *k, unsigned entry_pinc, const char **en
keyring_free_keypair(kp);
}
if (id){
if (keyring_commit_identity(k, id)!=1)
if (!keyring_commit_identity(k, id))
keyring_free_identity(id);
else
k->dirty=1;

View File

@ -21,6 +21,7 @@ Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
#ifndef __SERVAL_DNA__KEYRING_H
#define __SERVAL_DNA__KEYRING_H
#include "lang.h" // for bool_t
#include "serval_types.h" // for sid_t
#include "os.h" // for time_ms_t
@ -46,8 +47,19 @@ struct keyring_challenge{
unsigned char challenge[24];
};
/* An unlocked identity is represented by an instance of one of these structs
* in the linked list starting in the keyring_file structure.
*/
typedef struct keyring_identity {
// A nul-terminated string containing the identity's PIN (passphrase); NULL
// if no PIN (empty passphrase). This string must be free()d before the
// struct is deallocated.
char *PKRPin;
// Whether all other identities in the same keyring file that have the same
// PIN are also unlocked:
bool_t is_fully_unlocked : 1;
struct subscriber *subscriber;
unsigned int slot;
struct keyring_challenge *challenge;
@ -68,7 +80,8 @@ typedef struct keyring_identity {
typedef struct keyring_bam {
size_t file_offset;
unsigned char bitmap[KEYRING_BAM_BYTES];
unsigned char allocmap[KEYRING_BAM_BYTES];
unsigned char loadmap[KEYRING_BAM_BYTES];
struct keyring_bam *next;
} keyring_bam;
@ -99,7 +112,8 @@ keyring_identity *keyring_find_identity_sid(keyring_file *k, const sid_t *sidp);
keyring_identity *keyring_find_identity(keyring_file *k, const identity_t *sign);
void keyring_free(keyring_file *k);
int keyring_release_identities_by_pin(keyring_file *f, const char *pin);
void keyring_release_identities_by_pin(keyring_file *f, const char *pin);
void keyring_release_identity(keyring_file *k, keyring_identity *id);
int keyring_release_subscriber(keyring_file *k, const sid_t *sid);
#define KEYTYPE_CRYPTOBOX 0x01 // must be lowest
@ -122,7 +136,7 @@ extern __thread keyring_file *keyring;
keyring_file *keyring_create_instance();
keyring_file *keyring_open_instance(const char *pin);
keyring_file *keyring_open_instance_cli(const struct cli_parsed *parsed);
int keyring_enter_pin(keyring_file *k, const char *pin);
unsigned keyring_enter_pin(keyring_file *k, const char *pin);
int keyring_set_did_name(keyring_identity *id, const char *did, const char *name);
int keyring_set_pin(keyring_identity *id, const char *pin);
int keyring_sign_message(struct keyring_identity *identity, unsigned char *content, size_t buffer_len, size_t *content_len);

View File

@ -1,5 +1,6 @@
/*
Serval DNA HTTP RESTful interface
Serval DNA Keyring HTTP RESTful interface
Copyright (C) 2018 Flinders University
Copyright (C) 2013-2015 Serval Project Inc.
This program is free software; you can redistribute it and/or
@ -17,7 +18,7 @@ along with this program; if not, write to the Free Software
Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
#include "lang.h" // for FALLTHROUGH
#include "lang.h" // for bool_t, FALLTHROUGH
#include "serval.h"
#include "conf.h"
#include "httpd.h"
@ -28,15 +29,14 @@ Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
DEFINE_FEATURE(http_rest_keyring);
#define keyring_TOKEN_STRLEN (BASE64_ENCODED_LEN(sizeof(rhizome_bid_t) + sizeof(uint64_t)))
#define alloca_keyring_token(bid, offset) keyring_ token_to_str(alloca(keyring_TOKEN_STRLEN + 1), (bid), (offset))
DECLARE_HANDLER("/restful/keyring/", restful_keyring_);
static HTTP_HANDLER restful_keyring_identitylist_json;
static HTTP_HANDLER restful_keyring_add;
static HTTP_HANDLER restful_keyring_remove;
static HTTP_HANDLER restful_keyring_get;
static HTTP_HANDLER restful_keyring_set;
static HTTP_HANDLER restful_keyring_remove;
static HTTP_HANDLER restful_keyring_lock;
static int restful_keyring_(httpd_request *r, const char *remainder)
{
@ -44,35 +44,40 @@ static int restful_keyring_(httpd_request *r, const char *remainder)
int ret = authorize_restful(&r->http);
if (ret)
return ret;
const char *verb = HTTP_VERB_GET;
HTTP_HANDLER *handler = NULL;
const char *end;
if (strcmp(remainder, "identities.json") == 0) {
handler = restful_keyring_identitylist_json;
verb = HTTP_VERB_GET;
remainder = "";
if (r->http.verb == HTTP_VERB_GET && strcmp(remainder, "identities.json") == 0)
return restful_keyring_identitylist_json(r, "");
if (r->http.verb == HTTP_VERB_POST && strcmp(remainder, "add") == 0)
return restful_keyring_add(r, "");
if (parse_sid_t(&r->sid1, remainder, -1, &end) != -1) {
if (strcmp(end, "") == 0) {
if (r->http.verb == HTTP_VERB_GET)
return restful_keyring_get(r, "");
if (r->http.verb == HTTP_VERB_PATCH)
return restful_keyring_set(r, "");
if (r->http.verb == HTTP_VERB_DELETE)
return restful_keyring_remove(r, "");
}
else if (strcmp(remainder, "add") == 0) {
handler = restful_keyring_add;
verb = HTTP_VERB_GET;
remainder = "";
else if (r->http.verb == HTTP_VERB_PUT && strcmp(end, "/lock") == 0)
return restful_keyring_lock(r, "");
}
// Legacy API requests that use GET but should use another verb instead (see above). Eventually
// this support should be removed and some text in the response should refer the client to the
// correct request.
if (r->http.verb == HTTP_VERB_GET) {
if (strcmp(remainder, "add") == 0) {
return restful_keyring_add(r, "");
}
else if (parse_sid_t(&r->sid1, remainder, -1, &end) != -1) {
remainder = end;
if (strcmp(remainder, "/remove") == 0) {
handler = restful_keyring_remove;
remainder = "";
}
else if (strcmp(remainder, "/set") == 0) {
handler = restful_keyring_set;
remainder = "";
if (strcmp(end, "/remove") == 0)
return restful_keyring_remove(r, "");
else if (strcmp(end, "/set") == 0)
return restful_keyring_set(r, "");
else if (strcmp(end, "/lock") == 0)
return restful_keyring_lock(r, "");
}
}
if (handler == NULL)
return 404;
if (r->http.verb != verb)
return 405;
return handler(r, remainder);
}
static int http_request_keyring_response(struct httpd_request *r, uint16_t result, const char *message)
@ -99,12 +104,10 @@ static int http_request_keyring_response_identity(struct httpd_request *r, uint1
json_id_kv[0].value = &json_sid;
json_sid.type = JSON_STRING_NULTERM;
json_sid.u.string.content = alloca_tohex_sid_t(*id->box_pk);
json_id_kv[1].key = "identity";
json_id_kv[1].value = &json_sas;
json_sas.type = JSON_STRING_NULTERM;
json_sas.u.string.content = alloca_tohex_identity_t(&id->sign_keypair->public_key);
if (did) {
json_id_kv[json_id.u.object.itemc].key = "did";
json_id_kv[json_id.u.object.itemc].value = &json_did;
@ -238,6 +241,21 @@ static int restful_keyring_add(httpd_request *r, const char *remainder)
return http_request_keyring_response_identity(r, 201, id);
}
static int restful_keyring_get(httpd_request *r, const char *remainder)
{
if (*remainder)
return 404;
const char *pin = http_request_get_query_param(&r->http, "pin");
if (pin)
keyring_enter_pin(keyring, pin);
keyring_identity *id = keyring_find_identity_sid(keyring, &r->sid1);
if (!id)
return http_request_keyring_response(r, 404, "Identity not found");
int ret = http_request_keyring_response_identity(r, 200, id);
keyring_free_identity(id);
return ret;
}
static int restful_keyring_remove(httpd_request *r, const char *remainder)
{
if (*remainder)
@ -278,3 +296,15 @@ static int restful_keyring_set(httpd_request *r, const char *remainder)
return http_request_keyring_response(r, 500, "Could not store new identity");
return http_request_keyring_response_identity(r, 200, id);
}
static int restful_keyring_lock(httpd_request *r, const char *remainder)
{
if (*remainder)
return 404;
keyring_identity *id = keyring_find_identity_sid(keyring, &r->sid1);
if (!id)
return http_request_keyring_response(r, 404, "Identity not found");
keyring_release_identity(keyring, id);
keyring_free_identity(id);
return http_request_keyring_response(r, 200, "Identity locked");
}

View File

@ -1282,8 +1282,6 @@ static int mdp_search_identities(struct socket_address *client, struct mdp_heade
struct overlay_buffer *payload)
{
assert(keyring != NULL);
keyring_iterator it;
keyring_iterator_start(keyring, &it);
const char *tag=NULL;
const unsigned char *value=NULL;
@ -1297,6 +1295,8 @@ static int mdp_search_identities(struct socket_address *client, struct mdp_heade
}
}
keyring_iterator it;
keyring_iterator_start(keyring, &it);
while(1){
if (value_len){
if (!keyring_find_public_tag_value(&it, tag, value, value_len))

View File

@ -376,6 +376,12 @@ test_ServerLockUnlock() {
assertStdoutGrep --matches=1 --fixed-strings "$SIDA"
assertStdoutGrep --matches=1 --fixed-strings "$TWOA"
assertStdoutLineCount == 4
executeOk_servald id enter pin 'two'
executeOk_servald id list
assertStdoutGrep --matches=1 --fixed-strings "$SIDA"
assertStdoutGrep --matches=1 --fixed-strings "$TWOA"
assertStdoutGrep --matches=1 --fixed-strings "$TWOB"
assertStdoutLineCount == 5
}
teardown_ServerLockUnlock() {
teardown_servald

View File

@ -3,7 +3,7 @@
# Tests for Serval DNA HTTP RESTful interface
#
# Copyright 2013-2015 Serval Project, Inc.
# Copyright 2016 Flinders Univerity
# Copyright 2016-2018 Flinders Univerity
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License
@ -35,7 +35,6 @@ setup() {
set api.restful.users.harry.password potter \
set api.restful.users.ron.password weasley \
set api.restful.users.hermione.password grainger
set_extra_config
if [ -z "$IDENTITY_COUNT" ]; then
create_single_identity
else
@ -44,6 +43,7 @@ setup() {
start_servald_instances +A
wait_until servald_restful_http_server_started +A
get_servald_restful_http_server_port PORTA +A
REQUEST_COUNT=0
}
finally() {
@ -56,10 +56,6 @@ teardown() {
report_all_servald_servers
}
set_extra_config() {
:
}
set_keyring_config() {
executeOk_servald config \
set debug.httpd on \
@ -68,24 +64,37 @@ set_keyring_config() {
set log.console.level debug
}
rest_request() {
local oo
local request_verb="${1?}"
local path="${2?}"
local response_code="${3:-200}"
local preserve="request-$((++REQUEST_COUNT)).json"
executeOk curl \
--silent --show-error \
--write-out '%{http_code}' \
--output response.json \
--dump-header response.headers \
--basic --user harry:potter \
--request "$request_verb" \
"http://$addr_localhost:$PORTA$path"
tfw_cat response.headers response.json
cp response.json "$preserve"
tfw_preserve "$preserve"
assertStdoutIs "$response_code"
}
doc_keyringList="HTTP RESTful list keyring identities as JSON"
setup_keyringList() {
IDENTITY_COUNT=10
setup
}
test_keyringList() {
executeOk curl \
--silent --fail --show-error \
--output list.json \
--dump-header http.headers \
--basic --user harry:potter \
"http://$addr_localhost:$PORTA/restful/keyring/identities.json"
tfw_cat http.headers list.json
tfw_preserve list.json
assert [ "$(jq '.rows | length' list.json)" = $IDENTITY_COUNT ]
rest_request GET "/restful/keyring/identities.json"
assert [ "$(jq '.rows | length' response.json)" = $IDENTITY_COUNT ]
# All SIDs are present in the list.
for SID in ${SIDA[*]}; do
assert [ "$(jq -r '.rows | contains([["'"$SID"'"]])' list.json)" = true ]
assert [ "$(jq -r '.rows | contains([["'"$SID"'"]])' response.json)" = true ]
done
}
@ -97,52 +106,36 @@ setup_keyringListPin() {
}
test_keyringListPin() {
# First, list without supplying the PIN
executeOk curl \
--silent --fail --show-error \
--output list1.json \
--dump-header http.headers \
--basic --user harry:potter \
"http://$addr_localhost:$PORTA/restful/keyring/identities.json"
tfw_cat http.headers list1.json
tfw_preserve list1.json
transform_list_json list1.json ids1.json
rest_request GET "/restful/keyring/identities.json"
transform_list_json response.json ids1.json
assert [ "$(jq 'length' ids1.json)" = $((IDENTITY_COUNT-1)) ]
assertJq ids1.json 'contains([{"sid": "'$SIDA1'"}]) | not'
assertJq ids1.json 'contains([{"sid": "'$SIDA2'"}])'
assertJq ids1.json 'contains([{"sid": "'$SIDA3'"}])'
# Then, list supplying the PIN
executeOk curl \
--silent --fail --show-error \
--output list2.json \
--dump-header http.headers \
--basic --user harry:potter \
"http://$addr_localhost:$PORTA/restful/keyring/identities.json?pin=wif+waf"
tfw_cat http.headers list2.json
tfw_preserve list2.json
transform_list_json list2.json ids2.json
rest_request GET "/restful/keyring/identities.json?pin=wif+waf"
transform_list_json response.json ids2.json
assert [ "$(jq 'length' ids2.json)" = $IDENTITY_COUNT ]
assertJq ids2.json 'contains([{"sid": "'$SIDA1'"}])'
assertJq ids2.json 'contains([{"sid": "'$SIDA2'"}])'
assertJq ids2.json 'contains([{"sid": "'$SIDA3'"}])'
}
doc_keyringGet="HTTP RESTful get single keyring identity as JSON"
test_keyringGet() {
rest_request GET "/restful/keyring/$SIDA1"
assertJq response.json 'contains({"identity": {"sid": "'$SIDA1'", "identity": "'$IDA1'", "did": "'"$DIDA1"'", "name": "'"$NAMEA1"'"}})'
}
doc_keyringAdd="HTTP RESTful add keyring identity"
setup_keyringAdd() {
IDENTITY_COUNT=2
setup
}
test_keyringAdd() {
executeOk curl \
--silent --show-error --write-out '%{http_code}' \
--output add.json \
--dump-header http.headers \
--basic --user harry:potter \
"http://$addr_localhost:$PORTA/restful/keyring/add?did="
tfw_cat http.headers add.json
tfw_preserve add.json
assertStdoutIs '201'
SID="$(jq -r '.identity.sid' add.json)"
ID="$(jq -r '.identity.identity' add.json)"
rest_request POST "/restful/keyring/add?did=" 201
SID="$(jq -r '.identity.sid' response.json)"
ID="$(jq -r '.identity.identity' response.json)"
assert matches_rexp "^${rexp_sid}$" "$SID"
assert matches_rexp "^${rexp_id}$" "$ID"
executeOk_servald keyring list
@ -156,17 +149,9 @@ setup_keyringAddPin() {
setup
}
test_keyringAddPin() {
executeOk curl \
--silent --show-error --write-out '%{http_code}' \
--output add.json \
--dump-header http.headers \
--basic --user harry:potter \
"http://$addr_localhost:$PORTA/restful/keyring/add?pin=1234"
tfw_cat http.headers add.json
tfw_preserve add.json
assertStdoutIs '201'
SID="$(jq -r '.identity.sid' add.json)"
ID="$(jq -r '.identity.identity' add.json)"
rest_request POST "/restful/keyring/add?pin=1234" 201
SID="$(jq -r '.identity.sid' response.json)"
ID="$(jq -r '.identity.identity' response.json)"
executeOk_servald keyring list
assert_keyring_list 2
assertStdoutGrep --stderr --matches=0 "^$SID:$ID::\$"
@ -175,15 +160,8 @@ test_keyringAddPin() {
assertStdoutGrep --stderr --matches=1 "^$SID:$ID::\$"
# Now the server has internalised the PIN, so the new identity appears in the
# list
executeOk curl \
--silent --fail --show-error \
--output list.json \
--dump-header http.headers \
--basic --user harry:potter \
"http://$addr_localhost:$PORTA/restful/keyring/identities.json"
tfw_cat http.headers list.json
tfw_preserve list.json
transform_list_json list.json ids.json
rest_request GET "/restful/keyring/identities.json"
transform_list_json response.json ids.json
assertJq ids.json 'contains([{"sid": "'$SIDA1'"}])'
}
@ -193,16 +171,8 @@ setup_keyringRemove() {
setup
}
test_keyringRemove() {
executeOk curl \
--silent --show-error --write-out '%{http_code}' \
--output add.json \
--dump-header http.headers \
--basic --user harry:potter \
"http://$addr_localhost:$PORTA/restful/keyring/$SIDA1/remove"
tfw_cat http.headers add.json
tfw_preserve add.json
assertStdoutIs '200'
SID="$(jq -r '.identity.sid' add.json)"
rest_request DELETE "/restful/keyring/$SIDA1"
SID="$(jq -r '.identity.sid' response.json)"
assert [ "$SID" = "$SIDA1" ]
executeOk_servald keyring list
assert_keyring_list 1
@ -215,16 +185,8 @@ setup_keyringSetDidName() {
setup
}
test_keyringSetDidName() {
executeOk curl \
--silent --show-error --write-out '%{http_code}' \
--output set.json \
--dump-header http.headers \
--basic --user harry:potter \
"http://$addr_localhost:$PORTA/restful/keyring/$SIDA1/set?did=987654321&name=Joe%20Bloggs"
tfw_cat http.headers set.json
tfw_preserve set.json
assertStdoutIs '200'
assertJq set.json 'contains({"identity": {"sid": "'$SIDA1'", "did": "987654321", "name": "Joe Bloggs"}})'
rest_request PATCH "/restful/keyring/$SIDA1?did=987654321&name=Joe%20Bloggs"
assertJq response.json 'contains({"identity": {"sid": "'$SIDA1'", "did": "987654321", "name": "Joe Bloggs"}})'
executeOk_servald keyring list
assert_keyring_list 2
assertStdoutGrep --stderr --matches=1 "^$SIDA1:$IDA1:987654321:Joe Bloggs\$"
@ -238,54 +200,75 @@ setup_keyringSetDidNamePin() {
}
test_keyringSetDidNamePin() {
# First try with no PIN, and make sure it fails
executeOk curl \
--silent --show-error --write-out '%{http_code}' \
--output set1.json \
--dump-header http.headers \
--basic --user harry:potter \
"http://$addr_localhost:$PORTA/restful/keyring/$SIDA1/set?did=111222333&name=Nobody"
tfw_cat http.headers set1.json
tfw_preserve set1.json
assertStdoutIs '404'
rest_request GET "/restful/keyring/$SIDA1/set?did=111222333&name=Nobody" 404
# Enter incorrect PIN, and make sure it fails
executeOk curl \
--silent --show-error --write-out '%{http_code}' \
--output set2.json \
--dump-header http.headers \
--basic --user harry:potter \
"http://$addr_localhost:$PORTA/restful/keyring/$SIDA1/set?did=444555666&name=Anybody"
tfw_cat http.headers set2.json
tfw_preserve set2.json
assertStdoutIs '404'
rest_request GET "/restful/keyring/$SIDA1/set?did=444555666&name=Anybody&pin=wrong" 404
# Then try with correct PIN, and make sure it succeeds
executeOk curl \
--silent --show-error --write-out '%{http_code}' \
--output set3.json \
--dump-header http.headers \
--basic --user harry:potter \
"http://$addr_localhost:$PORTA/restful/keyring/$SIDA1/set?did=987654321&name=Joe%20Bloggs&pin=xyzabc"
tfw_cat http.headers set3.json
tfw_preserve set3.json
assertStdoutIs '200'
assertJq set3.json 'contains({"identity": {"sid": "'$SIDA1'", "did": "987654321", "name": "Joe Bloggs"}})'
executeOk_servald keyring list --entry-pin=xyzabc
rest_request GET "/restful/keyring/$SIDA1/set?did=987654321&name=Joe%20Bloggs&pin=$PINA1" 200
assertJq response.json 'contains({"identity": {"sid": "'$SIDA1'", "did": "987654321", "name": "Joe Bloggs"}})'
executeOk_servald keyring list --entry-pin="$PINA1"
assert_keyring_list 2
assertStdoutGrep --stderr --matches=1 "^$SIDA1:$IDA1:987654321:Joe Bloggs\$"
# Finally, try again with no PIN, and make sure it succeeds (server has
# internalised the PIN supplied in the last request)
executeOk curl \
--silent --show-error --write-out '%{http_code}' \
--output set4.json \
--dump-header http.headers \
--basic --user harry:potter \
"http://$addr_localhost:$PORTA/restful/keyring/$SIDA1/set?did=321321321&name=Fred+Nurks"
tfw_cat http.headers set4.json
tfw_preserve set4.json
assertStdoutIs '200'
assertJq set4.json 'contains({"identity": {"sid": "'$SIDA1'", "did": "321321321", "name": "Fred Nurks"}})'
executeOk_servald keyring list --entry-pin=xyzabc
rest_request GET "/restful/keyring/$SIDA1/set?did=321321321&name=Fred+Nurks" 200
assertJq response.json 'contains({"identity": {"sid": "'$SIDA1'", "did": "321321321", "name": "Fred Nurks"}})'
executeOk_servald keyring list --entry-pin="$PINA1"
assert_keyring_list 2
assertStdoutGrep --stderr --matches=1 "^$SIDA1:$IDA1:321321321:Fred Nurks\$"
}
doc_keyringLockUnlock="HTTP RESTful lock and unlock PIN-protected identities"
setup_keyringLockUnlock() {
IDENTITY_COUNT=5
PIN1=abc
PIN2=xyz
PINA2="$PIN1"
PINA3="$PIN2"
PINA4="$PIN2"
PINA5="$PIN2"
setup
# The initial list of unlocked identities should only be the single non-PIN identity.
rest_request GET "/restful/keyring/identities.json"
# Unlock PIN1.
rest_request GET "/restful/keyring/identities.json?pin=$PIN1"
# Unlock PIN2.
rest_request GET "/restful/keyring/identities.json?pin=$PIN2"
transform_list_json response.json ids_setup.json
assert [ "$(jq 'length' ids_setup.json)" = $IDENTITY_COUNT ]
assertJq ids_setup.json 'contains([{"sid": "'$SIDA1'"}])'
assertJq ids_setup.json 'contains([{"sid": "'$SIDA2'"}])'
assertJq ids_setup.json 'contains([{"sid": "'$SIDA3'"}])'
assertJq ids_setup.json 'contains([{"sid": "'$SIDA4'"}])'
assertJq ids_setup.json 'contains([{"sid": "'$SIDA5'"}])'
}
test_keyringLockUnlock() {
# Lock the only PIN1 identity and assert that it no longer appears in the
# identity list.
rest_request PUT "/restful/keyring/$SIDA2/lock"
rest_request GET "/restful/keyring/identities.json"
transform_list_json response.json ids1.json
assert [ "$(jq 'length' ids1.json)" = $((IDENTITY_COUNT - 1)) ]
assertJq ids1.json 'contains([{"sid": "'$SIDA2'"}]) | not'
# Lock the second PIN2 identity and assert that it no longer appears in the
# identity list.
rest_request PUT "/restful/keyring/$SIDA4/lock"
rest_request GET "/restful/keyring/identities.json"
transform_list_json response.json ids2.json
assert [ "$(jq 'length' ids2.json)" = $((IDENTITY_COUNT - 2)) ]
assertJq ids2.json 'contains([{"sid": "'$SIDA4'"}]) | not'
# Unlock PIN1 and assert that all PIN1 identities now appear in the identity
# list.
rest_request GET "/restful/keyring/identities.json?pin=$PIN1"
transform_list_json response.json ids3.json
assert [ "$(jq 'length' ids3.json)" = $((IDENTITY_COUNT - 1)) ]
assertJq ids3.json 'contains([{"sid": "'$SIDA2'"}])'
# Unlock PIN2 and assert that all PIN2 identities now appear in the identity
# list.
rest_request GET "/restful/keyring/identities.json?pin=$PIN2"
transform_list_json response.json ids4.json
assert [ "$(jq 'length' ids4.json)" = $IDENTITY_COUNT ]
assertJq ids4.json 'contains([{"sid": "'$SIDA4'"}])'
}
runTests "$@"