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/
| File | Use 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:
cors(oslo.middleware,[cors]);request_id— sets/returnsx-openstack-request-id;http_proxy_to_wsgi— honoursX-Forwarded-Proto/Hostwhen[oslo_middleware] enable_proxy_headers_parsing;authtoken— 401 wins over everything below (no token, invalid token); body{"error": {...}};context— builds theRequestContext;401if the token has no project (barbican/api/middleware/context.py:83); logsBegin processing request/Processed request: <status> - <method> <url>;microversion— microversion-parse for service typekey-manager, versions1.0…1.2(barbican/api/middleware/microversion.py:30): an out-of-range or malformedOpenStack-API-Version→ 406; every response getsOpenStack-API-Version: key-manager 1.xandVary: OpenStack-API-Version(observed);- Pecan app with hooks
JSONErrorHookthenBarbicanTransactionHook(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 onAccept. Most/v1handlers accept anyAccepttype of Python'smimetypesmap (allow_all_content_types,barbican/common/utils.py:63) or only JSON; a type outside the allowed set gives 406 before the handler.GET /andGET /v1acceptapplication/jsonandapplication/json-homeonly (barbican/api/controllers/versions.py:181). GET /v1/secrets/{id}switches between metadata and payload onAccept(JSON / absent /*/*→ metadata; anything else → payload withsecret:decrypt),barbican/api/controllers/__init__.py:30.- Pecan merges a JSON object body into the handler
kwargs(used byPUT /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 thenext/previouslinks (barbican/api/controllers/secrets.py:436).
2.4 Other cross-cutting rules
- References are absolute URLs built from
host_hrefor 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_refsare 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()(observed2026-10-29T06:01:41.521981; Python omits the fraction when microseconds are 0) (barbican/model/models.py:174). Locationheaders 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.StoreCryptoAdapterPluginfor the default path (barbican/plugin/resources.py:345); the plugin is found again by comparing this string withmodule.ClassNameof each loaded plugin (barbican/plugin/interface/secret_store.py:622).kek_data.plugin_name=barbican.plugin.crypto.simple_crypto.SimpleCryptoPluginorbarbican.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_datarow of a secret is read (barbican/plugin/store_crypto.py:119); Barbican writes exactly one.
3.2 From the request to the plugin input
- POST JSON
payload(string) or PUT raw body, with a content type and an optional encoding (01). - 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); fortext/plainthe payload is UTF-8 encoded then base64-encoded; for binary types it is taken as base64 whenpayload_content_encoding/Content-Encodingisbase64, otherwise base64-encoded as-is (PUTapplication/octet-streamraw body). store_cryptobase64-decodes that value: the crypto plugin receives the raw secret bytes (barbican/plugin/store_crypto.py:86).- The normalised content type is stored twice:
encrypted_data.content_typeandsecret_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), callbind_kek_metadata, storealgorithm,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):
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);C_WrapKey(key_wrap_mechanism, MKEK, KEK):CKM_AES_CBC_PAD(default; IV = 16 random bytes whenkey_wrap_generate_iv, default true),CKM_AES_KEY_WRAP_PAD(IV 8 bytes) orCKM_AES_KEY_WRAP_KWP(IV 4 bytes) (barbican/plugin/crypto/pkcs11.py:804);hmac = C_Sign(hmac_mechanism (CKM_SHA256_HMAC default, or CKM_AES_MAC), HMAC key, iv || wrapped_key)— overwrapped_keyalone when there is no IV (barbican/plugin/crypto/p11_crypto.py:471,barbican/plugin/crypto/pkcs11.py:876);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_mechanismis absent on rows written by older releases and then meansCKM_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 fromC_GenerateRandom; Barbican adds PKCS#7 padding (block 128 bits) beforeC_Encryptand removes it afterC_Decrypt(barbican/plugin/crypto/pkcs11.py:622,barbican/plugin/crypto/pkcs11.py:651);CKM_AES_GCM/VENDOR_SAFENET_CKM_AES_GCM: IV 12 bytes fromC_GenerateRandom(whenaes_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). Withaes_gcm_generate_iv = falsethe 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 untilbarbican-manage db cleanhard-deletes rows older than--min-days(barbican/model/clean.py:458).kek_datarows are never deleted while secrets reference them. - Expired secrets are hidden but not deleted (
barbican/model/repositories.py:611) unlessdb 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)
| Store | secret_store_metadata keys | Where the secret is |
|---|---|---|
| KMIP | plugin_name, content_type, key_uuid | KMIP managed object (barbican/plugin/kmip_secret_store.py:153) |
| Vault | plugin_name, content_type, key_id, version (=1; legacy rows have none), alg, bit_length for generated pairs | v1/{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) |
| Dogtag | plugin_name, content_type, KRA key id | Dogtag 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
| Check | Result |
|---|---|
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-simple | 25 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, defaultpublic) orbarbican_endpoint; versionbarbican_api_version(castellan:castellan/key_manager/barbican_key_manager.py:288). Goes through python-barbicanclient, which negotiates the microversion (§ 4.2). store→POST /v1/secretswithpayloadbytes,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). Sosecret_typeand the payload bytes must round-trip exactly.create_key/create_key_pair→POST /v1/ordersthen pollsGET /v1/orders/{id}everyretry_delay(1 s) up tonumber_of_retries(60) forACTIVE, fails onERRORreadingerror_status_codeanderror_reason, then readssecret_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 reachACTIVEwithin 60 s.- Ids are the last path segment of
secret_ref(castellan:castellan/key_manager/barbican_key_manager.py:567). getreads 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 withservice,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>withOpenStack-API-Version: key-manager 1.1, expects the1.1+version document withmin_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 --forcesendsforce=1only at ≥ 1.2 (python-barbicanclient:barbicanclient/v1/secrets.py:600): the server must accept1as true.- Error display uses
titleanddescription(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/offsetand walks pages (skyline-console:src/stores/barbican/secrets.js:39), takes the id after/secrets/insecret_ref(skyline-console:src/stores/barbican/secrets.js:47), reads one secret withAccept: application/jsonand its payload withAccept: */*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'salembic_version=8c74e2d7f1ffso 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_atnaive UTC;statusstringsPENDING,ACTIVE,ERROR(barbican/model/models.py:41). - JSON columns:
orders.meta,order_retry_tasks.retry_args/retry_kwargsarejson.dumpstext (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 cleanand 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/OFFSETwith a separateCOUNT(*); ordering of unsorted lists followscreated_atfor orders/containers [inferred frombarbican/model/repositories.py:1045] and is unspecified for secrets withoutsort.
6. Authentication, policies, configuration and CLI
- Auth: keystonemiddleware semantics (§ 2.1).
ContextMiddlewarerequiresX-Identity-Status: Confirmedand 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_accessdefaults toTrue(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 byTrue:%(enforce_new_defaults)s and (…)(barbican/common/policies/secrets.py:66): with[oslo_policy] enforce_new_defaults = Falseonly the deprecated legacy rules (rolesadmin,creator,observer,audit,key-manager:service-admin) grant access; withTrue(oslo.policy 6 default) only the new ones (admin,member,reader, project scope). oslo.policy 6 always enforces scope (scope_types=['project']). secret:getbecomessecret:decryptfor non-JSONAccept(barbican/api/controllers/__init__.py:57). Orders use a target with onlyproject_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
- 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 ofetc/barbican/barbican-functional.conf, serves the service and runs the tests in a subprocess with anX-Test-Idheader. Python Barbican: 563 ok / 69 skipped / 0 failures in 51 s. Skips: 35 "paging tests temporarily disabled" (upstream), 30 need multiple back ends, 4 needhost_hrefunset. - 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). - 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. - At-rest vectors (§ 3.8): decrypt both vector files; round-trip with
tools/at_rest_reference.py. - barbican-tempest-plugin against a deployed OpenKeel with
[enforce_scope] barbican = Truefor 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/referencemisses/,/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] kekand[p11_crypto_plugin] loginaresecret=Trueoptions). - Error messages for crypto failures must stay generic (
… failure seen - please contact site administrator.):generate_safe_exception_messageexists 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,
405on unsupported methods,any methodonsecret-stores/global-default|preferred(or decide to tighten and document) - [ ] error body
{code, title, description};401body from the auth layer - [ ] microversions 1.0–1.2:
consumersfrom 1.1, consumer-protected delete andforcefrom 1.2, version documents for 1.0 and 1.1+ - [ ] Accept switch on
GET /v1/secrets/{id};*/*→ stored content type on/payload;406rules - [ ] content-type / encoding normalisation (§ 3.2) and
text/plainUTF-8 decoding on read - [ ]
simple_cryptoandp11_cryptobyte formats (§ 3.4, § 3.5), exactplugin_namestrings,kek_labelformat, decrypt with the datum's KEK and stored mechanism - [ ] pass
at-rest-vectors-*and the 665-secret round trip - [ ] project auto-creation, quotas (
-1unlimited,0disabled), 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,ERRORfields read by castellan - [ ] RPC
process_type_orderonbarbican.workers(if Python workers must coexist) and Keystoneidentity.project.deletedhandling - [ ] soft delete everywhere;
db cleancompatibility;alembic_versionkept - [ ] 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.