OpenKeel: the sovereign French Cloud solution of the inherent group, with a European reach Discover inherent

Reverse-spec · Services

barbican — Rewrite pack

Rewrite pack — barbican — branch stable/2026.2

Branch stable/2026.2 @ de32745a7a4f — repository https://opendev.org/openstack/barbican — analysed on 2026-10-29

Target: a Go re-implementation (OpenKeel) that is a drop-in replacement for barbican-api, barbican-worker and barbican-keystone-listener, shares the database with Python Barbican (decrypts what Python wrote, writes what Python can read) and keeps castellan, python-barbicanclient/OSC, Skyline and barbican-tempest-plugin working unchanged.

Client repositories cited: skyline-apiserver stable/2026.2 @ 4af72bfd4577 (prefix skyline-apiserver:), castellan stable/2026.2 @ bd6bd342982d (prefix castellan:), python-barbicanclient stable/2026.2 @ 8162006 (python-barbicanclient:), skyline-console stable/2026.2 @ 806d2003 (skyline-console:), barbican-tempest-plugin master @ f370cf0 (barbican-tempest-plugin:).

1. Contents of _rewrite/

FileUse in a rewrite
[routes_stable-2026.2.json](_rewrite/routes_stable-2026.2.json)the 57 operations: path, method, real handler file:line, policy rule, accepted Content-Type, body validator, statuses the handler sets, microversion checks, and the source line of each Pecan tree edge
[openapi_stable-2026.2.json](_rewrite/openapi_stable-2026.2.json)OpenAPI 3.0.3 built from the routes, the validator schemas and the api-ref tables; x-policy, x-handler, x-content-types, x-microversions; types and stubs, not the authority on behaviour
[request-schemas_stable-2026.2.json](_rewrite/request-schemas_stable-2026.2.json)the 11 JSON schemas of the validators, dumped from instances (business checks on top of them are in 01 and 02)
[api-ref_stable-2026.2.json](_rewrite/api-ref_stable-2026.2.json), [api-ref-gaps_stable-2026.2.json](_rewrite/api-ref-gaps_stable-2026.2.json)the 47 documented operations with their parameter, attribute and status tables (Barbican's own rst format); code operations missing from the docs
[policies_stable-2026.2.json](_rewrite/policies_stable-2026.2.json), [policies_stable-2026.2.yaml](_rewrite/policies_stable-2026.2.yaml)the 82 default rules (new defaults + deprecated legacy rules)
[config_stable-2026.2.conf](_rewrite/config_stable-2026.2.conf)oslo-config-generator sample (all namespaces of etc/oslo-config-generator/barbican.conf)
[schema-sqlite-migrated_stable-2026.2.sql](_rewrite/schema-sqlite-migrated_stable-2026.2.sql)reference schema: empty SQLite migrated with barbican-manage db upgrade (Alembic head 8c74e2d7f1ff)
[schema-mysql_stable-2026.2.sql](_rewrite/schema-mysql_stable-2026.2.sql), [schema-postgresql_stable-2026.2.sql](_rewrite/schema-postgresql_stable-2026.2.sql)DDL from the ORM models (MySQL/MariaDB is the OpenKeel target)
[schema-diff_stable-2026.2.md](_rewrite/schema-diff_stable-2026.2.md)migrations vs models: 0 differences (26 tables, indexes, uniques, foreign keys)
[exchanges-functional_stable-2026.2.json](_rewrite/exchanges-functional_stable-2026.2.json)2,792 HTTP exchanges of Barbican's black-box suite (functionaltests/, 632 tests) against the full paste pipeline and a real Keystone; each exchange carries its test id; tokens redacted
[functional-run_stable-2026.2.json](_rewrite/functional-run_stable-2026.2.json)modules run and counts: 563 ok, 69 skipped, 0 failures
[exchanges-api-tests_stable-2026.2.json](_rewrite/exchanges-api-tests_stable-2026.2.json)659 exchanges of the in-process API unit tests (435 tests, no failure): covers secret consumers, transport keys and secret stores that the functional suite skips; no middleware, context injected
[response-schemas-functional_stable-2026.2.json](_rewrite/response-schemas-functional_stable-2026.2.json) / [.md](_rewrite/response-schemas-functional_stable-2026.2.md), [response-schemas-api-tests_stable-2026.2.json](_rewrite/response-schemas-api-tests_stable-2026.2.json) / [.md](_rewrite/response-schemas-api-tests_stable-2026.2.md)response JSON schemas inferred per operation and status, with x-microversions on fields seen only from 1.1
[observed-errors-functional_stable-2026.2.md](_rewrite/observed-errors-functional_stable-2026.2.md), [observed-errors-api-tests_stable-2026.2.md](_rewrite/observed-errors-api-tests_stable-2026.2.md)every (operation, status, title, description) seen, numbers and UUIDs templated
[at-rest-vectors-simple-crypto_stable-2026.2.json](_rewrite/at-rest-vectors-simple-crypto_stable-2026.2.json)25 real rows (secrets, encrypted_data, kek_data, secret_store_metadata) written by Python Barbican, with the master KEK (public test key) and the expected plaintext
[at-rest-vectors-p11-emulated_stable-2026.2.json](_rewrite/at-rest-vectors-p11-emulated_stable-2026.2.json)8 rows produced by the real P11CryptoPlugin over a software PKCS#11 (CBC/GCM × CBC_PAD/KWP), with MKEK and HMAC key bytes
[tools/barbican_routes.py](_rewrite/tools/barbican_routes.py)Pecan route walker, schema dumper, api-ref parser, OpenAPI builder
[tools/at_rest_reference.py](_rewrite/tools/at_rest_reference.py)executable specification of encryption at rest (no Barbican import) + checks against Barbican + vector generators
[tools/schema_diff.py](_rewrite/tools/schema_diff.py)migrated vs model schema comparison
[rewrite-pack-index_stable-2026.2.md](_rewrite/rewrite-pack-index_stable-2026.2.md)index and how each file was produced

Library versions of the extraction venv (Python 3.12, upper-constraints stable/2026.2): pecan 1.8.0, WebOb 1.8.11, cryptography 43.0.3, SQLAlchemy 2.0.52, alembic 1.19.1, oslo.db 18.1.1, oslo.policy 6.0.1, oslo.messaging 18.3.1, keystonemiddleware 13.0.1, castellan 5.8.0, microversion-parse 2.1.0. The conformance adapter is ~/.claude/skills/generate-reverse-spec/scripts/adapters/barbican.py (skill side, not in this directory).

2. HTTP behaviour that the contracts do not show

2.1 Middleware pipeline

/v1 pipeline barbican-api-keystone (etc/barbican/barbican-api-paste.ini:16), outermost first:

  1. cors (oslo.middleware, [cors]);
  2. request_id — sets/returns x-openstack-request-id;
  3. http_proxy_to_wsgi — honours X-Forwarded-Proto/Host when [oslo_middleware] enable_proxy_headers_parsing;
  4. authtoken — 401 wins over everything below (no token, invalid token); body {"error": {...}};
  5. context — builds the RequestContext; 401 if the token has no project (barbican/api/middleware/context.py:83); logs Begin processing request / Processed request: <status> - <method> <url>;
  6. microversion — microversion-parse for service type key-manager, versions 1.0…1.2 (barbican/api/middleware/microversion.py:30): an out-of-range or malformed OpenStack-API-Version → 406; every response gets OpenStack-API-Version: key-manager 1.x and Vary: OpenStack-API-Version (observed);
  7. Pecan app with hooks JSONErrorHook then BarbicanTransactionHook (barbican/api/app.py:53).

/ (barbican_version) has no authtoken/context; /healthcheck is oslo.middleware. The optional audit filter sits just before the app (etc/barbican/barbican-api-paste.ini:20).

Inside the app, the order of checks for a handler is: Pecan routing (404/405) → _lookup loads the entity (404 before any policy check, so existence of another project's secret is not hidden: it gets 403, an unknown id 404) → Accept negotiation (406) → handle_exceptions → enforce_rbac (403) → enforce_content_types (415) → body validation (400/413) → business code.

2.2 Error body

Always {"code", "title", "description"} for Barbican's own errors (02 § 1). Clients display title: description (python-barbicanclient:barbicanclient/client.py:138) and castellan only looks at the status code (castellan:castellan/key_manager/barbican_key_manager.py:706): the texts need not be byte-identical, but keep the status codes and the title (HTTP reason phrase).

2.3 Content negotiation (Pecan)

  • Handlers are exposed with template='json' and Pecan negotiates on Accept. Most /v1 handlers accept any Accept type of Python's mimetypes map (allow_all_content_types, barbican/common/utils.py:63) or only JSON; a type outside the allowed set gives 406 before the handler. GET / and GET /v1 accept application/json and application/json-home only (barbican/api/controllers/versions.py:181).
  • GET /v1/secrets/{id} switches between metadata and payload on Accept (JSON / absent / */* → metadata; anything else → payload with secret:decrypt), barbican/api/controllers/__init__.py:30.
  • Pecan merges a JSON object body into the handler kwargs (used by PUT /v1/project-quotas/{id}, barbican/api/controllers/quotas.py:94).
  • Unknown query parameters are ignored, except that list endpoints copy all of them (minus limit/offset) into the next/previous links (barbican/api/controllers/secrets.py:436).

2.4 Other cross-cutting rules

  • References are absolute URLs built from host_href or the request (barbican/common/utils.py:67); clients extract the id as the last path segment (castellan:castellan/key_manager/barbican_key_manager.py:567, skyline-console:src/stores/barbican/secrets.js:47).
  • Container secret_refs are validated against the same base URL (barbican/common/validators.py:713): a rewrite behind a different hostname than its clients use will reject containers.
  • Timestamps are naive UTC ISO 8601 without zone, from datetime.isoformat() (observed 2026-10-29T06:01:41.521981; Python omits the fraction when microseconds are 0) (barbican/model/models.py:174).
  • Location headers on 201 (secrets, containers, container secrets, metadata item, transport keys) and on consumer registration (wrong value, keep it, 01).

3. Encryption at rest: formal specification

This is the part a rewrite cannot get from the API reference. Everything below is checked by _rewrite/tools/at_rest_reference.py (functions ref_*, which import only cryptography).

3.1 Rows involved

secrets(id, project_id → projects.id, secret_type, algorithm, bit_length, mode, …)
  ├─ secret_store_metadata(secret_id, key, value)   key='plugin_name' → store plugin FQCN
  │                                                 key='content_type' → normalised content type
  │                                                 (+ back-end keys for external stores)
  └─ encrypted_data(secret_id, kek_id → kek_data.id, content_type, cypher_text TEXT, kek_meta_extended TEXT)
kek_data(id, project_id, plugin_name, kek_label, active, bind_completed, algorithm, bit_length, mode, plugin_meta TEXT)
  • secret_store_metadata.plugin_name = barbican.plugin.store_crypto.StoreCryptoAdapterPlugin for the default path (barbican/plugin/resources.py:345); the plugin is found again by comparing this string with module.ClassName of each loaded plugin (barbican/plugin/interface/secret_store.py:622).
  • kek_data.plugin_name = barbican.plugin.crypto.simple_crypto.SimpleCryptoPlugin or barbican.plugin.crypto.p11_crypto.P11CryptoPlugin (barbican/plugin/store_crypto.py:281, barbican/common/utils.py:170); used to pick the decrypting plugin (barbican/plugin/crypto/manager.py:124).
  • A rewrite must write and match these exact strings, even though they are Python class paths.
  • kek_data.kek_label = project-<keystone project id>-key-<uuid4> (barbican/model/repositories.py:992); for PKCS#11 it is also the cache key of the unwrapped KEK.
  • The decrypting KEK is **the row referenced by encrypted_data.kek_id**, active or not (barbican/plugin/store_crypto.py:122, barbican/plugin/store_crypto.py:126). The active KEK (one per project and plugin; the oldest wins if several, the others are deactivated, barbican/model/repositories.py:965) is only used to encrypt new data.
  • Only the first encrypted_data row of a secret is read (barbican/plugin/store_crypto.py:119); Barbican writes exactly one.

3.2 From the request to the plugin input

  1. POST JSON payload (string) or PUT raw body, with a content type and an optional encoding (01).
  2. Normalisation (barbican/plugin/util/translations.py:25): the content type is normalised (text/plain; charset=utf-8 → text/plain, barbican/plugin/util/mime_types.py:64); for text/plain the payload is UTF-8 encoded then base64-encoded; for binary types it is taken as base64 when payload_content_encoding/Content-Encoding is base64, otherwise base64-encoded as-is (PUT application/octet-stream raw body).
  3. store_crypto base64-decodes that value: the crypto plugin receives the raw secret bytes (barbican/plugin/store_crypto.py:86).
  4. The normalised content type is stored twice: encrypted_data.content_type and secret_store_metadata.content_type (barbican/plugin/store_crypto.py:313, barbican/plugin/resources.py:346).

On read, the plugin output (raw bytes) is base64-encoded by store_crypto (barbican/plugin/store_crypto.py:137) and decoded again by denormalize_after_decryption: text/plain → UTF-8 string (406 if invalid), binary → bytes (barbican/plugin/util/translations.py:89). ref_payload_to_raw in the tool reproduces step 2–3.

3.3 store_crypto layer

  • Encrypt: encrypted_data.cypher_text = base64.b64encode(ResponseDTO.cypher_text) — standard base64 with padding, of whatever bytes the crypto plugin returned; kek_meta_extended = the plugin's string or NULL (barbican/plugin/store_crypto.py:314).
  • Decrypt: DecryptDTO(base64.b64decode(cypher_text)) (barbican/plugin/store_crypto.py:129).
  • First use of a project with a crypto plugin: insert an unbound kek_data (bind_completed = false, status = ACTIVE), call bind_kek_metadata, store algorithm, bit_length, mode, plugin_meta, bind_completed = true (barbican/plugin/store_crypto.py:277, barbican/plugin/store_crypto.py:320). All in the request transaction.

3.4 simple_crypto (default)

Master KEKs: [simple_crypto_plugin] kek is a MultiStrOpt of Fernet keys (44-character url-safe base64 of 32 bytes) (barbican/plugin/crypto/simple_crypto.py:35). No default: without it the plugin does not load and every secret operation fails with 500 (barbican/plugin/crypto/simple_crypto.py:59). Note: the option is read by the plugin module's own ConfigOpts, parsed at import from the default config locations (barbican/plugin/crypto/simple_crypto.py:29, barbican/plugin/crypto/simple_crypto.py:48).

Fernet (cryptography fernet spec, what Python uses): key = signing_key(16) || encryption_key(16); token = url-safe base64 (with = padding) of 0x80 || timestamp(uint64 BE, seconds) || IV(16 random) || AES-128-CBC(encryption_key, IV, PKCS7(plaintext)) || HMAC-SHA256(signing_key, all previous bytes). Barbican never passes a TTL, so the timestamp is not checked on decrypt.

Project KEK (bind_kek_metadata, barbican/plugin/crypto/simple_crypto.py:116):

  • pkek = Fernet.generate_key() (a new Fernet key: 32 random bytes, url-safe base64);
  • kek_data.plugin_meta = Fernet(master_keks[0]).encrypt(pkek) — the Fernet token (ASCII) is stored as-is (in MariaDB a TEXT column holding the token characters; SQLite stores it as a BLOB);
  • algorithm = 'aes', bit_length = 128, mode = 'cbc' (informational).

Unwrap (_get_kek, barbican/plugin/crypto/simple_crypto.py:68): pkek = MultiFernet([Fernet(k) for k in master_keks]).decrypt(plugin_meta) — every configured master key is tried, in order; the first matching HMAC wins.

Encrypt / decrypt (barbican/plugin/crypto/simple_crypto.py:81, barbican/plugin/crypto/simple_crypto.py:97): token = Fernet(pkek).encrypt(raw); ResponseDTO(token, None). So:

encrypted_data.cypher_text = base64std( fernet_token_ascii( pkek, raw ) )      kek_meta_extended = NULL
raw = Fernet(MultiFernet(masters).decrypt(kek_data.plugin_meta)).decrypt( base64std_decode(cypher_text) )

(two base64 layers: standard outside, url-safe inside). Vector check: the first vector of at-rest-vectors-simple-crypto_stable-2026.2.json has cypher_text starting Z0FBQUFBQn… = base64 of gAAAAAB… (Fernet version byte 0x80).

Generation: symmetric = os.urandom(bit_length // 8) then encrypt (barbican/plugin/crypto/simple_crypto.py:127); asymmetric = RSA (e=65537) or DSA with cryptography, private key PKCS#8 (PEM for RSA, DER for DSA), encrypted with BestAvailableEncryption(passphrase) when a passphrase is given; public key SubjectPublicKeyInfo (PEM / DER); the passphrase itself is stored as a third secret (barbican/plugin/crypto/simple_crypto.py:135). Supported sizes: symmetric 64/128/192/256 bits (×2 for xts), asymmetric 1024/2048/4096 (barbican/plugin/crypto/base.py:60, barbican/plugin/crypto/simple_crypto.py:239).

Rotation (barbican-manage simple_crypto …, 04 § 5): rewrap_pkek = MultiFernet(masters).rotate(plugin_meta) (re-encrypt with masters[0], barbican/cmd/simple_crypto.py:98); new_pkek = deactivate and create an unbound KEK; rewrap_secrets = MultiFernet([new_pkek, old_pkek]).rotate(token) and repoint kek_id (barbican/plugin/crypto/simple_crypto.py:104, barbican/cmd/simple_crypto.py:51).

Go mapping: any Fernet implementation that follows the spec interoperates (e.g. github.com/fernet/fernet-go: fernet.DecryptKey(tok, ttl=0, keys...) with ttl = 0 meaning no expiry check; fernet.EncryptAndSign). Use standard base64 (encoding/base64.StdEncoding) for the outer layer, url-safe with padding for keys and tokens.

3.5 p11_crypto (PKCS#11 HSM)

HSM objects (token objects created by barbican-manage hsm gen_mkek/gen_hmac): the master KEK mkek_label (CKK_AES, mkek_length = 32 bytes, wrap/unwrap) and the HMAC key hmac_label (hmac_key_type default CKK_AES, sign/verify), found with C_FindObjects on CKA_CLASS=CKO_SECRET_KEY, CKA_KEY_TYPE, CKA_LABEL; more than one match → error (barbican/plugin/crypto/pkcs11.py:598, barbican/plugin/crypto/p11_crypto.py:391).

Project KEK (_generate_wrapped_kek, barbican/plugin/crypto/p11_crypto.py:455):

  1. C_GenerateKey(CKM_AES_KEY_GEN, CKA_VALUE_LEN = pkek_length (32), CKA_TOKEN=false, CKA_EXTRACTABLE=true, CKA_SENSITIVE=always_set_cka_sensitive, encrypt/decrypt) (barbican/plugin/crypto/pkcs11.py:760);
  2. C_WrapKey(key_wrap_mechanism, MKEK, KEK): CKM_AES_CBC_PAD (default; IV = 16 random bytes when key_wrap_generate_iv, default true), CKM_AES_KEY_WRAP_PAD (IV 8 bytes) or CKM_AES_KEY_WRAP_KWP (IV 4 bytes) (barbican/plugin/crypto/pkcs11.py:804);
  3. hmac = C_Sign(hmac_mechanism (CKM_SHA256_HMAC default, or CKM_AES_MAC), HMAC key, iv || wrapped_key) — over wrapped_key alone when there is no IV (barbican/plugin/crypto/p11_crypto.py:471, barbican/plugin/crypto/pkcs11.py:876);
  4. kek_data.plugin_meta = compact JSON (separators=(',', ':'), barbican/plugin/crypto/p11_crypto.py:130): json {"iv":"<b64|null>","wrapped_key":"<b64>","hmac":"<b64>","mkek_label":"<label>","hmac_label":"<label>","key_wrap_mechanism":"CKM_AES_CBC_PAD"} (standard base64; key_wrap_mechanism is absent on rows written by older releases and then means CKM_AES_CBC_PAD, barbican/plugin/crypto/p11_crypto.py:408); algorithm = 'AES', bit_length = pkek_length × 8, mode = 'CBC' (barbican/plugin/crypto/p11_crypto.py:266).

Unwrap (_load_kek, barbican/plugin/crypto/p11_crypto.py:420): look up the MKEK and HMAC key by the labels stored in the row (so rows survive a label change in the configuration), C_Verify the HMAC over iv || wrapped_key, then C_UnwrapKey into a session object (CKA_TOKEN=false, sensitive, extractable, encrypt/decrypt) (barbican/plugin/crypto/pkcs11.py:849). The handle is cached per kek_label for pkek_cache_ttl (900 s), at most pkek_cache_limit (100) entries, oldest evicted and destroyed (barbican/plugin/crypto/p11_crypto.py:306).

Secret encryption (barbican/plugin/crypto/p11_crypto.py:222), mechanism [p11_crypto_plugin] encryption_mechanism:

  • CKM_AES_CBC (default): IV = 16 bytes from C_GenerateRandom; Barbican adds PKCS#7 padding (block 128 bits) before C_Encrypt and removes it after C_Decrypt (barbican/plugin/crypto/pkcs11.py:622, barbican/plugin/crypto/pkcs11.py:651);
  • CKM_AES_GCM / VENDOR_SAFENET_CKM_AES_GCM: IV 12 bytes from C_GenerateRandom (when aes_gcm_generate_iv, default true), tag 128 bits appended by the HSM to the ciphertext, no AAD (barbican/plugin/crypto/pkcs11.py:669, barbican/plugin/crypto/pkcs11.py:698). With aes_gcm_generate_iv = false the HSM generates the IV and Barbican takes the last 16 bytes of the output as "IV" (barbican/plugin/crypto/pkcs11.py:694) [inferred vendor behaviour, untested]. On decrypt, a 16-byte IV marks rows written by an older release that padded the plaintext: trailing PKCS#7-looking bytes are stripped (barbican/plugin/crypto/pkcs11.py:737).
  • Storage: encrypted_data.cypher_text = base64std(ciphertext[+tag]), kek_meta_extended = compact JSON {"iv":"<b64>","mechanism":"CKM_AES_CBC"} (barbican/plugin/crypto/p11_crypto.py:233). Decryption uses the mechanism stored in the row, not the configured one (barbican/plugin/crypto/p11_crypto.py:244): a rewrite must support every mechanism ever configured.
  • Symmetric generation: C_GenerateRandom(bit_length / 8) then encrypt as above (barbican/plugin/crypto/p11_crypto.py:271); asymmetric generation is not implemented.

Go mapping: github.com/miekg/pkcs11 exposes the same calls; mechanism constants are in barbican/plugin/crypto/pkcs11.py:138–barbican/plugin/crypto/pkcs11.py:147 (note VENDOR_SAFENET_CKM_AES_GCM = 0x8000011c, and the CK_AES_GCM_PARAMS layout with ulIvBits, barbican/plugin/crypto/pkcs11.py:389). Keep the PKCS#7 handling on the client side for CBC and the HMAC-before-unwrap check.

3.6 Lifecycle facts that matter for the data

  • Secret deletion soft-deletes encrypted_data; the ciphertext and the KEK stay until barbican-manage db clean hard-deletes rows older than --min-days (barbican/model/clean.py:458). kek_data rows are never deleted while secrets reference them.
  • Expired secrets are hidden but not deleted (barbican/model/repositories.py:611) unless db clean --soft-delete-expired-secrets.
  • A project deleted in Keystone has its rows soft-deleted by the listener; its KEK rows too (barbican/model/repositories.py:290).

3.7 External stores (references only)

Storesecret_store_metadata keysWhere the secret is
KMIPplugin_name, content_type, key_uuidKMIP managed object (barbican/plugin/kmip_secret_store.py:153)
Vaultplugin_name, content_type, key_id, version (=1; legacy rows have none), alg, bit_length for generated pairsv1/{kv_mountpoint}/[data/]{kv_path/}{key_id} = {"type", "value": hex(raw), …} (barbican/plugin/castellan_secret_store.py:42, castellan:castellan/key_manager/vault_key_manager.py:386)
Dogtagplugin_name, content_type, KRA key idDogtag KRA (barbican/plugin/dogtag.py:148)

For OpenKeel the Vault/castellan path is the natural external store; a rewrite reading Vault rows must hex-decode value and, for metadata without version, apply the legacy base64 heuristic (barbican/plugin/castellan_secret_store.py:57).

3.8 Evidence

CheckResult
at_rest_reference.py verify-simple on the database written by the functional run (simple_crypto, test master KEK)665/665 secrets: ref_simple_decrypt (no Barbican code) returns exactly the bytes of Barbican's StoreCryptoAdapterPlugin.get_secret (658 of them soft-deleted by test clean-up)
at_rest_reference.py vectors-simple25 vectors over every (secret_type, content_type) seen, 3 per kind
at_rest_reference.py vectors-p11 (real P11CryptoPlugin, software PKCS#11)8/8 rows decrypted by ref_p11_unwrap_project_kek + ref_p11_decrypt identically to the plugin; mechanisms emulated per PKCS#11 v2.40, not validated on an HSM

A Go implementation should pass both vector files (decrypt every row to expected.raw_b64) and round-trip: encrypt with Go, decrypt with ref_* (or with Python Barbican pointed at the same database).

4. Client expectations

4.1 castellan ([key_manager] backend = barbican; Nova, Cinder, Glance, Octavia…)

  • Discovery: endpoint from the catalogue (service_type=key-manager, interface [barbican] barbican_endpoint_type, default public) or barbican_endpoint; version barbican_api_version (castellan:castellan/key_manager/barbican_key_manager.py:288). Goes through python-barbicanclient, which negotiates the microversion (§ 4.2).
  • store → POST /v1/secrets with payload bytes, algorithm, bit_length, name, secret_type (symmetric, public, private, passphrase, certificate, opaque); public/private keys and certificates are sent as PEM (converted from DER) and read back as PEM then converted to DER (castellan:castellan/key_manager/barbican_key_manager.py:441, castellan:castellan/key_manager/barbican_key_manager.py:580). So secret_type and the payload bytes must round-trip exactly.
  • create_key / create_key_pair → POST /v1/orders then polls GET /v1/orders/{id} every retry_delay (1 s) up to number_of_retries (60) for ACTIVE, fails on ERROR reading error_status_code and error_reason, then reads secret_ref / container_ref, and deletes the order (and, for pairs, the container) (castellan:castellan/key_manager/barbican_key_manager.py:511, castellan:castellan/key_manager/barbican_key_manager.py:369). The order must reach ACTIVE within 60 s.
  • Ids are the last path segment of secret_ref (castellan:castellan/key_manager/barbican_key_manager.py:567).
  • get reads metadata then payload; **404 → ManagedObjectNotFoundError**, other 4xx → KeyManagerError (castellan:castellan/key_manager/barbican_key_manager.py:706).
  • delete(force) → DELETE /v1/secrets/{id}[?force=1] (castellan:castellan/key_manager/barbican_key_manager.py:744); add_consumer / remove_consumer → secret consumers API with service, resource_type, resource_id (castellan:castellan/key_manager/barbican_key_manager.py:778); list → GET /v1/secrets?secret_type=… (castellan:castellan/key_manager/barbican_key_manager.py:838).

4.2 python-barbicanclient / OSC (openstack secret …)

  • Microversion negotiation: GET <endpoint> with OpenStack-API-Version: key-manager 1.1, expects the 1.1+ version document with min_version/max_version; a 406 means "server only supports 1.0" (python-barbicanclient:barbicanclient/v1/client.py:125). Client-supported versions 1.0–1.2; it picks the highest common (python-barbicanclient:barbicanclient/v1/client.py:30).
  • delete --force sends force=1 only at ≥ 1.2 (python-barbicanclient:barbicanclient/v1/secrets.py:600): the server must accept 1 as true.
  • Error display uses title and description (python-barbicanclient:barbicanclient/client.py:138).

4.3 Skyline

  • skyline-apiserver only proxies (catalogue name barbican, skyline-apiserver:skyline_apiserver/config/openstack.py:138) and ships a copy of Barbican's policy rules for UI decisions (skyline-apiserver:skyline_apiserver/policy/manager/barbican.py:130) — keep the rule names.
  • skyline-console lists secrets and containers with limit/offset and walks pages (skyline-console:src/stores/barbican/secrets.js:39), takes the id after /secrets/ in secret_ref (skyline-console:src/stores/barbican/secrets.js:47), reads one secret with Accept: application/json and its payload with Accept: */* in parallel (skyline-console:src/stores/barbican/secrets.js:106): */* must return the payload in its stored content type.

4.4 barbican-tempest-plugin

API tests for secrets, containers, orders, consumers (secret and container), secret metadata, quotas, and a regression test for CVE-2022-3100 (barbican-tempest-plugin:barbican_tempest_plugin/tests/api/test_cve_2022_3100.py:27); RBAC tests require [enforce_scope] barbican = True and use the new default personas (admin, member, reader, and a member of another project) (barbican-tempest-plugin:barbican_tempest_plugin/tests/rbac/v1/base.py:66). The rewrite must implement the new-default rules exactly (§ 6).

5. Database strategy

  • Reuse the schema as is (schema-sqlite-migrated / schema-mysql), including the dead certificate tables, and keep Alembic's alembic_version = 8c74e2d7f1ff so that Python tools (barbican-manage db clean, rewrap commands) keep working against the same database.
  • IDs are UUID4 strings generated by the application (barbican/model/models.py:96); created_at/updated_at naive UTC; status strings PENDING, ACTIVE, ERROR (barbican/model/models.py:41).
  • JSON columns: orders.meta, order_retry_tasks.retry_args/retry_kwargs are json.dumps text (barbican/model/models.py:73).
  • Integrity kept by code, not constraints: one active KEK per (project, plugin) — enforce it or replicate "oldest wins, deactivate others"; soft-deleted rows must be ignored everywhere except db clean and decryption of soft-deleted secrets is never needed by the API.
  • No row locking; uniqueness races are absorbed by catching duplicate-key errors (barbican/common/resources.py:52). With Galera, keep each request in one transaction and retry on deadlock.
  • Paging is LIMIT/OFFSET with a separate COUNT(*); ordering of unsorted lists follows created_at for orders/containers [inferred from barbican/model/repositories.py:1045] and is unspecified for secrets without sort.

6. Authentication, policies, configuration and CLI

  • Auth: keystonemiddleware semantics (§ 2.1). ContextMiddleware requires X-Identity-Status: Confirmed and a project; allow_anonymous_access (default false) would create a read-only anonymous context (barbican/api/middleware/context.py:59).
  • Policy target built per request (barbican/api/controllers/__init__.py:52): {"enforce_new_defaults": <bool>, "target": {"secret"|"container"|"order": {"project_id": <keystone id>, "creator_id": …, "read": "read" (if the user is in the read ACL), "read_project_access": <bool>}}}, flattened to dotted keys (target.secret.project_id…) (barbican/api/controllers/__init__.py:162). With no ACL rows, read_project_access defaults to True (barbican/api/controllers/__init__.py:239).
  • Rule language: oslo.policy (role:, project_id:%(…)s, user_id:%(…)s, True:%(…)s, 'read':%(…)s, rule:, and/or/not). The new defaults are guarded by True:%(enforce_new_defaults)s and (…) (barbican/common/policies/secrets.py:66): with [oslo_policy] enforce_new_defaults = False only the deprecated legacy rules (roles admin, creator, observer, audit, key-manager:service-admin) grant access; with True (oslo.policy 6 default) only the new ones (admin, member, reader, project scope). oslo.policy 6 always enforces scope (scope_types=['project']).
  • secret:get becomes secret:decrypt for non-JSON Accept (barbican/api/controllers/__init__.py:57). Orders use a target with only project_id/creator_id (barbican/api/controllers/orders.py:67).
  • Configuration to keep compatible (sample in _rewrite/config_stable-2026.2.conf): [DEFAULT] host_href, admin_role, max_allowed_secret_in_bytes (20000), max_allowed_request_size_in_bytes (25000), default_limit_paging (10), max_limit_paging (100), db_auto_create, [database], [queue], [retry_scheduler], [keystone_notifications], [quotas], [secretstore], [secretstore:*], [crypto], [simple_crypto_plugin], [p11_crypto_plugin], [kmip_plugin], [vault_plugin], [keystone_authtoken], [oslo_policy], [cors]. OpenKeel's Go rewrite uses YAML, but the semantics (and the master KEK value format) must be the same.
  • CLI: barbican-manage db upgrade|clean|sync_secret_stores, simple_crypto rewrap_pkek|new_pkek|rewrap_secrets, hsm … (05 § 4). The rotation commands are part of the key-management contract: keep them (or document that the Python CLI is used against the shared DB).

7. Conformance strategy

  1. Black-box suite: functionaltests/ of this repository is the closest thing to a conformance suite. Re-run it against the rewrite with the adapter (conformance_capture.py scenarios … --adapter adapters/barbican.py): it starts a real Keystone in a subprocess with the users and roles of etc/barbican/barbican-functional.conf, serves the service and runs the tests in a subprocess with an X-Test-Id header. Python Barbican: 563 ok / 69 skipped / 0 failures in 51 s. Skips: 35 "paging tests temporarily disabled" (upstream), 30 need multiple back ends, 4 need host_href unset.
  2. Differential testing against exchanges-functional_stable-2026.2.json: replay each test's requests in order and compare status, Content-Type, response body shape (response-schemas-functional) and error texts; substitute generated UUIDs and URLs (host and port differ).
  3. API unit-test exchanges (exchanges-api-tests) document controller behaviour without middleware (context injected, is_admin, no policy): use them for the operations the functional suite skips (secret consumers, transport keys, secret stores), not as policy evidence.
  4. At-rest vectors (§ 3.8): decrypt both vector files; round-trip with tools/at_rest_reference.py.
  5. barbican-tempest-plugin against a deployed OpenKeel with [enforce_scope] barbican = True for RBAC.

Fixtures of the original tests for reference: barbican/tests/utils.py:60 (API test base: in-memory SQLite, test master KEK barbican/tests/utils.py:78), barbican/tests/database_utils.py, functionaltests/api/base.py, functionaltests/common/client.py:238.

8. Gaps, non-functional requirements and checklist

8.1 Gaps found while building the pack

  • No Pecan support in the skill's extractors: routes come from tools/barbican_routes.py (tree edges listed by hand with their source lines, handlers and decorators read from Pecan's metadata).
  • doc/source/api/reference misses /, /v1, transport keys, GET …/consumers/{id}, PUT /v1/orders (_rewrite/api-ref-gaps_stable-2026.2.json).
  • The functional run used [oslo_policy] enforce_new_defaults = False (legacy roles, as the functional tests' user set expects); the new-default matrix is covered only by barbican-tempest-plugin, which was not run.
  • PKCS#11, KMIP, Vault and Dogtag not run against real back ends; multiple back ends not run (30 skipped tests).
  • Keystone listener not captured (needs a bus); behaviour from the code.
  • Response-schema inference and observed errors depend on the tests' coverage: secret consumers and transport keys only appear in the unit-test capture.

8.2 Non-functional requirements

  • Never log payloads, KEKs or master keys (Python logs only ids and project ids; [simple_crypto_plugin] kek and [p11_crypto_plugin] login are secret=True options).
  • Error messages for crypto failures must stay generic (… failure seen - please contact site administrator.): generate_safe_exception_message exists to avoid leaking crypto details (barbican/api/__init__.py:77).
  • Payload cap 20,000 bytes, request cap 25,000 bytes.
  • PKCS#11 session handling: one caching session for master-key lookups and KEK unwraps, sessions per operation for encryption; re-initialise the library once on PKCS11Exception (barbican/plugin/crypto/p11_crypto.py:210).
  • Order generation within castellan's 60 × 1 s polling window.

8.3 Checklist

  • [ ] 57 operations with the same paths, methods, statuses, 405 on unsupported methods, any method on secret-stores/global-default|preferred (or decide to tighten and document)
  • [ ] error body {code, title, description}; 401 body from the auth layer
  • [ ] microversions 1.0–1.2: consumers from 1.1, consumer-protected delete and force from 1.2, version documents for 1.0 and 1.1+
  • [ ] Accept switch on GET /v1/secrets/{id}; */* → stored content type on /payload; 406 rules
  • [ ] content-type / encoding normalisation (§ 3.2) and text/plain UTF-8 decoding on read
  • [ ] simple_crypto and p11_crypto byte formats (§ 3.4, § 3.5), exact plugin_name strings, kek_label format, decrypt with the datum's KEK and stored mechanism
  • [ ] pass at-rest-vectors-* and the 665-secret round trip
  • [ ] project auto-creation, quotas (-1 unlimited, 0 disabled), paging clamps and links
  • [ ] ACL semantics (default implicit read ACL, project-access, PUT vs PATCH), policy target shape, both legacy and new-default rules
  • [ ] orders: 202, commit before cast, worker/in-process modes, ERROR fields read by castellan
  • [ ] RPC process_type_order on barbican.workers (if Python workers must coexist) and Keystone identity.project.deleted handling
  • [ ] soft delete everywhere; db clean compatibility; alembic_version kept
  • [ ] functionaltests 563/563 against the rewrite

Source: reverse-spec-barbican/07-rewrite-pack_stable-2026.2.md. The _rewrite/ and _inventory/ files are not published on the site.