Skip to content

Signing policies

A signing policy tells ADM what to do with the PDFs of a folder. There are three modes:

Mode What ADM does
OFF Nothing. ADM does not classify the PDFs of the folder.
WATCH ADM records where every PDF comes from. adm_pdf_api.get_provenance returns the status. ADM signs nothing.
SIGN Like WATCH. ADM also signs the PDFs that it makes, with the key of the setting AUTO_SIGN_KEY_ID.

WATCH is the default. No mode refuses an upload. A user can download a PDF, change it, and upload it again. ADM does not sign such a file. It only records that the file is no longer the file that ADM signed. For the statuses, see See where a PDF comes from.

In a folder with the mode SIGN, ADM signs the results of the PDF operations and the saves of the PDF editor. See Know what ADM signs.

Two settings control the policies. Change them on the Settings page, which only administrators can open. See the settings reference.

Key Default What it does
AUTO_SIGN_KEY_ID empty The ID of the signing key (adm_signing_keys.signing_key_id) that signs in folders with the mode SIGN.
AUTO_SIGN_DEFAULT_MODE WATCH The mode of a folder that no policy covers.

Register the key first. See Set up a signing key. Use a key whose private key stays in OpenBao, Vault or OCI Vault. Anyone who can run code as the owner of the ADM schema can sign with a key of the provider DATABASE.

A value of AUTO_SIGN_DEFAULT_MODE that is not OFF, WATCH or SIGN is an error. ADM does not read it as OFF, because a typo must not stop signing without a message. adm_signing_api.get_effective_policy raises adm_error.e_invalid_param for such a value. An upload of a PDF does not fail because of it: ADM classifies the file.

Only an administrator can create, change, switch and delete a policy. Every other user gets adm_error.e_admin_required. A policy has a folder path pattern, a mode, an active flag and a description. This block creates a policy for /contracts/ and one for /contracts/emea/:

declare
l_policy_id adm_signing_policies.policy_id%type;
begin
adm_context_api.system_login;
l_policy_id := adm_signing_policy_api.create_policy(
p_folder_path_pattern => '/contracts/'
, p_signing_mode => adm_signing_policy_api.c_mode_sign
, p_description => 'Sign what ADM makes'
);
l_policy_id := adm_signing_policy_api.create_policy(
p_folder_path_pattern => '/contracts/emea/'
, p_signing_mode => adm_signing_policy_api.c_mode_watch
, p_description => 'Record only'
);
end;
/

A policy with the mode SIGN needs a key that can sign. The setting AUTO_SIGN_KEY_ID must name a key that exists, is active, and has a certificate that is valid now. If it does not, the call raises adm_error.e_signing_key_unusable or adm_error.e_signing_key_not_found, and changes nothing. This is the output of the first create_policy for /contracts/ in a schema where the setting is empty:

ORA-20272: Signing key of the setting AUTO_SIGN_KEY_ID cannot be used: the setting is empty or is not a number

update_policy and activate_policy make the same check when the mode is SIGN. deactivate_policy does not, so an administrator can always switch a policy off.

The pattern is a path prefix. It works like the pattern of a storage policy:

  • A pattern starts with a slash and is written in lower case, because the path of a folder is in lower case. A pattern in capitals is refused.
  • ADM adds a slash to the end of the path of the folder. A folder matches a pattern when its path starts with the pattern. /contracts/ covers the folder /contracts and every folder below it.
  • If several patterns match, the longest pattern wins. /contracts/emea/ beats /contracts/ for the folder /contracts/emea/2026.
  • A pattern without a slash at the end also covers the folders with a longer name. /contracts covers /contracts-archive. End a pattern with a slash.
  • The characters % and _ in a pattern are normal characters.
  • An inactive policy is ignored.
  • A folder that no policy covers has the mode of AUTO_SIGN_DEFAULT_MODE.

adm_signing_api.get_effective_policy returns the mode, the key and the place that the mode comes from. Any user can call it. This block asks for four folders and lists the policies. The folder /contracts/emea/2026 was in /contracts/emea:

declare
l_policy adm_signing_api.t_effective_policy;
begin
adm_context_api.system_login;
for r in (
select f.folder_id
, f.folder_path
from adm_folders f
where f.folder_path in ('/contracts', '/contracts/emea', '/contracts/emea/2026', '/archive')
order by f.folder_path
)
loop
l_policy := adm_signing_api.get_effective_policy(r.folder_id);
dbms_output.put_line(rpad(r.folder_path, 22) || rpad(l_policy.signing_mode, 7) || l_policy.source);
end loop;
end;
/
Folder Mode Decided by
/contracts SIGN POLICY
/contracts/emea WATCH POLICY
/contracts/emea/2026 WATCH POLICY
/archive WATCH DEFAULT

The source is POLICY when a policy decided, and DEFAULT when the setting decided. The member policy_id names the policy.

ADM reads the path of the folder at every call and keeps no cache. When an administrator moves a folder, the folder has the policy of its new place. After the move of 2026 into /archive, the folder /archive/2026 has the mode WATCH and the source DEFAULT. A lookup takes about 0.04 ms with 21 policies (measured on Oracle 26ai Free, 20 000 calls).

In a folder with the mode SIGN, ADM signs the result of a PDF operation before it stores it. The document gets one version, and that version is signed. The target folder of the result decides, not the folder of the source.

Operation Result in a SIGN folder
merge, split, watermark, add_text, add_image, add_comments, add_page_numbers, fill_form, flatten_form, add_attachments, convert_to_pdf Signed with the key of AUTO_SIGN_KEY_ID. split signs every part.
protect Not signed. AOP cannot sign an encrypted file.
to_images, get_attachments Not signed. The results are images, or the files that are in the PDF.
sign Signed with the key of the caller only.
A save in the PDF editor (a new version, or a new PDF) Signed with the key of AUTO_SIGN_KEY_ID. See Sign the saves of the PDF editor.
An upload Not signed. ADM records the status.

ADM signs for the system, not for a user. The user needs no grant for the key, and an embed session can run the operations. There is no public call for this signature: ADM makes it inside the operation, after it has checked the rights of the user. adm_pdf_api.sign stays the call for a signature of a person.

This block makes a watermark of offer.pdf as the user JDOE. The folder /users/jdoe/contracts has the mode SIGN, and AUTO_SIGN_KEY_ID names the key ADM system. JDOE has no grant for the key. The block shows the signature, the status of the new version, and the audit entry:

declare
l_source adm_documents.document_id%type;
l_result adm_documents.document_id%type;
l_infos adm_pdf_api.t_signature_infos;
begin
adm_context_api.system_user_login('JDOE');
select d.document_id
into l_source
from adm_documents d
join adm_folders f on f.folder_id = d.folder_id
where f.folder_path = '/users/jdoe/contracts'
and d.document_name = 'offer.pdf';
l_result := adm_pdf_api.watermark(l_source, 'DRAFT');
l_infos := adm_pdf_api.verify_signatures(l_result);
dbms_output.put_line('signatures: ' || l_infos.count);
dbms_output.put_line('key: ' || l_infos(1).signing_key_name);
dbms_output.put_line('whole file: ' || case when l_infos(1).covers_whole_file then 'yes' else 'no' end);
dbms_output.put_line('status: ' || adm_pdf_api.get_provenance(l_result));
for r in (
select a.action_details
from adm_audit_log a
where a.document_id = l_result
and a.action_details like 'PDF %'
) loop
dbms_output.put_line(replace(r.action_details, ', ', chr(10) || ' '));
end loop;
end;
/
signatures: 1
key: ADM system
whole file: yes
status: ADM_SIGNED
PDF watermark
signed by ADM with the key 3063 (ADM system
provider DATABASE
certificate 370090093DA92F98F5CB3BF77A61235CBDF0E21C30B00C3B02CD2E33907C5512
input SHA-256 B558E5EBE841BEE5CBDCB256755A4E87C9C713D2D4A13787EE290467673BDD04
output SHA-256 197D947076BC21451CDC9C06C0DBD194C2E4C45B001905BFB0D14030BA426CD3
signed digest FBEAE79CEBF63DBA5B752A051FE2CB709E02614393C1B5666F0013C1F713F4A6
time stamp server none): offer.pdf -> offer (watermarked).pdf

The audit entry is the entry of the operation. It names the key, the SHA-256 of the file before the signature (input) and after it (output), and the digest that the key signed. It holds no key material. The recording used a key of the provider DATABASE.

Understand the checks of a signed operation

Section titled “Understand the checks of a signed operation”

ADM fails closed. When it cannot sign, the operation fails, and ADM stores nothing:

  • Before a file is read, ADM makes sure that AUTO_SIGN_KEY_ID names a key that is active and has a certificate that is valid now, and that AOP is 26.1.2 or later. These are the checks of create_policy. Otherwise the call raises adm_error.e_signing_key_unusable, adm_error.e_signing_key_not_found or adm_error.e_aop_too_old.
  • After AOP has made the signed file, ADM makes sure that the new signature is valid for the certificate of the key and covers the whole file. Otherwise the call raises adm_error.e_signature_invalid.

A disabled key or an expired certificate stops every PDF operation in the SIGN folders. This is the output of the watermark above when the key ADM system is disabled:

ORA-20272: Signing key ADM system cannot be used: it is disabled

The signatures of the sources follow these rules:

  • A source that ADM signed (ADM_SIGNED) needs no p_invalidate_signatures. The new signature replaces the old one. When the source has one signature, AOP gets the unsigned base: the bytes in front of that signature. So the result holds no data of the old signature. ADM uses the base only when the base has no signature. When the audit entry of the old signature exists, the SHA-256 of the base must also be the input SHA-256 of that entry. If a check fails, AOP gets the signed file, and the audit entry says (1 signature(s) invalidated).
  • A source with a signature of another certificate, or a source that was changed after ADM signed it (MODIFIED), is refused with adm_error.e_pdf_signed, as in every folder. Pass p_invalidate_signatures => true to accept that its signature is lost. ADM signs the result.
  • A source with two or more valid signatures is refused before it goes to AOP, also with p_invalidate_signatures => true. AOP cannot sign a changed copy of such a file.

This is the error for a PDF with two valid signatures, from the same recorded run:

ORA-20290: Document 15090451595079264306803316911089 has 2 valid signatures. ADM cannot sign a changed copy of a PDF with two or more valid signatures, and the signing policy of the target folder requires a signature.

The audit entry of an operation with a replaced signature says (1 ADM signature(s) replaced).

After the store, ADM runs the hook AFTER_PDF_SIGNED, see Run code after ADM signs a PDF.

The PDF editor on the file details page (pages 10 and 13) saves a new file. It does not change the old file. In a folder with the mode SIGN, ADM signs the saved file with the key of AUTO_SIGN_KEY_ID before it stores the new version. A new PDF that a user makes in the editor is signed in the same way. The rules are the rules of a PDF operation:

  • ADM fails closed. A key that cannot sign, or a new signature that is not valid, makes the save fail, and ADM stores no version.
  • The audit entry names the key and the hashes. It starts with PDF editor save.
  • After the store, ADM runs the hook AFTER_PDF_SIGNED with the ID of the new version.
  • The merge window of VERSION_MERGE_WINDOW_MINUTES applies. A second save of the same user in the window replaces the version of the first save. ADM signs each save, so the latest version is always signed.

The browser writes the file again before it sends it. ADM cannot see what the user changed. So ADM decides about the signatures from the version that the editor opened, not from the saved file:

The opened version What the editor and ADM do
No signature ADM signs the save.
ADM_SIGNED The editor deletes the signature fields after the load, before the user edits. The save has no data of the old signature. ADM signs it, and the audit entry says (1 ADM signature(s) replaced).
A signature of another certificate, or MODIFIED The editor shows a warning that the signature is lost. It keeps the fields. ADM signs the save, and the audit entry says (1 signature(s) invalidated).
Two or more valid signatures The editor shows a warning that a save fails. A save raises adm_error.e_auto_sign_multi_signed, and ADM signs nothing.

The editor deletes only the signature fields. The other form fields of the PDF stay. In a folder with the mode OFF or WATCH, the editor deletes nothing and ADM signs nothing. The editor shows the warning for every signed PDF, because the save makes the signature invalid. Only a user who can edit the document sees a warning.

adm_pdf_api.get_editor_signature_info returns the facts that the editor page reads. Any user with view rights can call it.

The signature proves that the key signed these exact bytes. A change of a signed byte makes the check fail. The signature does not prove that the content is true, or who wrote the inputs. A file can get more bytes behind the signature. Then the signature is still valid for its range, and covers_whole_file of verify_signatures is false. Require a valid signature that covers the whole file.

With the provider DATABASE, everyone who can run code as the owner of the ADM schema can sign with the key. Then a signature of ADM proves little. Use a key of Transit (OpenBao, Vault) or OCI Vault when the signature must prove that ADM made the file.

Find the PDFs that changed after ADM signed them

Section titled “Find the PDFs that changed after ADM signed them”

The view adm_report_pdf_provenance_v has one row for each version of a PDF document. A row gives the status of the version, the status of the version before it, and the signing mode of the folder. For the statuses, see See where a PDF comes from.

The column no_longer_pure_adm is Y in two cases:

  • The version is MODIFIED. An ADM signature is in the file, and a check of it fails.
  • The version before was ADM_SIGNED, and this version is not. For example, a user downloaded a signed PDF, changed it, and uploaded it again.

This query lists the documents whose latest version is no longer the file that ADM signed:

select v.folder_path
, v.document_name
, v.version_number
, v.previous_provenance
, v.pdf_provenance
, v.signing_mode
, v.version_created_by
, v.version_created_date
from adm_report_pdf_provenance_v v
where v.no_longer_pure_adm = 'Y'
and v.version_status = 'CURRENT'
and v.deleted_flag = 'N'
order by v.version_created_date desc;

Remove the condition on version_status to also get the older versions. version_created_by is the user who stored the version.

The view reads stored columns. It does not read a file and does not call AOP. A version that ADM did not classify has the status UNKNOWN. These are the versions from before ADM kept the status, and the versions in a folder with the mode OFF. adm_pdf_provenance_api.backfill classifies the older versions. The column signing_mode is the mode of the folder now, found like adm_signing_api.get_effective_policy finds it. When a folder moves, its mode can change, but the status of a version does not change.

Call Does
update_policy(p_policy_id, p_folder_path_pattern, p_signing_mode, p_description) Changes pattern, mode and description.
deactivate_policy(p_policy_id) The policy is ignored. The folders use the next shorter pattern, or the default mode.
activate_policy(p_policy_id) The policy applies again.
delete_policy(p_policy_id) Deletes the policy.
list_policies Returns all policies with the longest pattern first.

A pattern that another policy has raises adm_error.e_signing_policy_exists. An ID that no policy has raises adm_error.e_signing_policy_not_found.

A trigger on adm_signing_policies writes an entry to the audit trail for every change, also for a change that someone makes with SQL. An update that changes nothing writes no entry. The entries of the three calls above, and a change of the mode of /contracts/emea/ to OFF, look like this:

CREATE_SIGNING_POLICY: Signing policy 253 created: pattern /contracts/ -> SIGN; active: Y
CREATE_SIGNING_POLICY: Signing policy 254 created: pattern /contracts/emea/ -> WATCH; active: Y
UPDATE_SIGNING_POLICY: Signing policy 254 (pattern /contracts/emea/) updated: mode WATCH -> OFF; description changed