Compile encrypted SystemVerilog IP, and encrypt your own IP for RyuSim with ryusim-protect.
IEEE 1735 defines how an IP vendor ships HDL source encrypted, so that only the tools the
vendor chose can read it. The source carries `pragma protect envelopes: the
design text encrypted with a random AES session key, and that session key encrypted
(wrapped) with each tool's RSA public key.
RyuSim does not ship the RSA private key that unwraps its session keys. Seiraiyu's key
broker holds it. When a source file contains an envelope, ryusim compile sends
the wrapped session key to the broker over HTTPS, gets the session key back, decrypts the
design in memory and compiles it. Keeping the private key off the user's machine answers the
key-extraction attacks that Speith et al. showed against local key storage
(IEEE S&P 2022).
Every compile of protected source needs network access to the broker. There is no offline mode.
Pass the encrypted file like any other source. No extra option is needed:
ryusim compile --top pipelined_adder pipelined_adder.svp
RyuSim checks every source file for protected regions, whatever its extension.
.svp is a convention. The broker URL defaults to
https://broker.ryusim.com/api/v1; --broker-url changes it, and
ryusim -v compile prints the one in use. If the RYUSIM_API_KEY
environment variable is set, RyuSim sends it to the broker as a bearer token. The public
broker does not require one.
In a cocotb flow, list the .svp file in VERILOG_SOURCES. The
cocotb guide covers the rest.
This file is encrypted for RyuSim. The ports and the parameter are plaintext; the body is
not. Save it as pipelined_adder.svp:
module pipelined_adder #(
parameter WIDTH = 8
) (
input logic clk,
input logic rst,
input logic [WIDTH-1:0] a,
input logic [WIDTH-1:0] b,
output logic [WIDTH:0] sum,
output logic valid
);
`pragma protect version = 2
`pragma protect encrypt_agent = "ryusim-protect"
`pragma protect key_keyowner = "Seiraiyu"
`pragma protect key_keyname = "RyuSim-RSA-2026-01"
`pragma protect key_method = rsa
`pragma protect key_block
KUNB27Vcm4Xhq16d59nvsQGT9HzJjaRgve+imwNX9AlD2TVngWh3FIVi3XyIjf5DV7OU1vgT09gcxODE7Kf4fBH02x8TULG7rSJGpW9tjmx+6SPceQVVaa4hT7w9bwN2v5BGHPgeTerZuVs6H4g/I/kl6CHkQAJgEIFU51N6ucMqiYbcPhyc15VybXq1iBNI6QKFNw+QtA4EOQMfnww33g8qBO826U3sq1FYQ7vNCsUHgncMyZERKkqYuYUplwYfq50DlzTNnltbhZSM9du2xNOlicmDcKgMvjeyVPbrUrRQ8k5Gl0+9j6cj8MOtL6wMtBTVX0nTt7wb61eTuB2YVFKfODpQXRyu2ZPuNP8lrAPfyzE33ElLsxq6sZh2psweTmhbXMKrkYD3quuaImbR2L3GbWiH/4Mo/t0VLC3kb/zh+gB/K42jJRfA5ljqK9XqI9fyOWIYyBk0iU4UftiObE27sWbWy17Xm8BtqQXqpEdOrhbkZYJS1+dVgi3XHgmlRY18+nykBZWAlfzif85AQnNcRzzDvf3y5bItxUx9EF3iXGsQ1qrAEUN+iRY0dWC7+Hf8gY/qqTSjtzbyDlpS8CweitBSG3Q4IqBrIqyHjmNHRo4p7ehb8WG9pYkEP9zyQCf9/GLXZrZ4BRJ+8iKP9bc74H0C5utAZbg3caNUjnA=
`pragma protect data_method = "aes-256-gcm"
`pragma protect iv = "6cdf01d6adb98217219aa68f"
`pragma protect begin_protected
`pragma protect data_block
iFJYVYfclfGcGOgXdun96UA+IerbCZf0YgSyfGCvPie6shRDy/TtJ7ggMgB60XIiD9Tr0XUMip+D4pVtbhGkq34u3PjKaXX4IuJ7wiyyN8zrBiDLQd65pqYTeiw3ySFRXpjlPIEcY1szNCdwqyKOSGxFwG9KPwFKD8P6wjlK0cQwlYpcE1I/eNarUUd4AO0CoucZsyQLdK6RO6xaRZGNSiCXIdtTniato5B7KHV59RXAW7Rr0EyJLMxIKnd98GpiAXya8HXsREhSQKuX2YlLPb2Pxha+JvWEVZWvOT6kQJJkYA6rbApkNxJOP8QYHSVv+388SKc6lZLsoYbTJtDDndw1OA5cF4wZZzSIS4E4JF4OxKscVsvQKjkS5MIqPBSe0abTLKX8nVTNRmD/DGbt3h4ox/dGYlJ6Ye7SqSxXqq8AE6IU8Df8rBhqPA06mDMA3ErSh7n/75nspnP9pKm/RzyAS0r/aoNM57sgxYFvVgUsTKWfVjZc2iiPyI5sKkICyyazdaKN+9B99SPnh03GhN2Bs9y1hQXBKU8MIcHKd5JRTfOMSC4YTPpg37rhg3/4YDCWvwb7ohRmulTWROB/hHA691thNoQHtqgNlQaPc9qaUAlqZl803OPRCsGD2pZ2KgFEX7ScYDqd3qXGheRROyDfVmaROpea5kjJunGqLKNhmDpsh0nrHOvZD0tnjQt6ozct1B3zrioqR3AkrkPKtp53sGIpBZVEOR0/H8nWfo9jT2WTH2RXCXrpFQ2dmv4RdcI7AgxbrJMTT775g2UqP+18WUMNPtmTI36yXBJ5IWwCh0RlXo7c8zIFlI3xyiNVaKnAXjnY5FkQv03XbtYStn/JRej3W20xBSh5pZhNOzD5Q+YI0Qmia6lsqUavriu+FR+jogCc/9KutYDJloMGF1/H1K/SKg8S
`pragma protect auth_tag = "bf5f6dc6a5edcd53cd5273d581df6ebc"
`pragma protect end_protected
endmodule
Save a cocotb test as test_pipelined_adder.py:
import cocotb
from cocotb.triggers import RisingEdge, Timer
from cocotb.clock import Clock
@cocotb.test()
async def test_pipelined_add(dut):
"""Verify the encrypted pipelined adder compiles and simulates."""
cocotb.start_soon(Clock(dut.clk, 10, unit="ns").start())
# Reset
dut.rst.value = 1
dut.a.value = 0
dut.b.value = 0
for _ in range(3):
await RisingEdge(dut.clk)
dut.rst.value = 0
# Apply inputs and wait for pipeline to produce results
dut.a.value = 10
dut.b.value = 20
for _ in range(5):
await RisingEdge(dut.clk)
await Timer(1, unit="ns")
assert dut.valid.value == 1, "valid should be asserted after pipeline fill"
result = int(dut.sum.value)
assert result == 30, f"Expected 10+20=30, got {result}"
dut._log.info(f"Encrypted IP works! sum={result}")
and a Makefile:
SIM ?= ryusim
TOPLEVEL_LANG := verilog
VERILOG_SOURCES = $(PWD)/pipelined_adder.svp
COCOTB_TOPLEVEL = pipelined_adder
COCOTB_TEST_MODULES = test_pipelined_adder
include $(shell cocotbext-ryusim-config --makefiles)/Makefile.sim
Run make. The test passes:
71.00ns INFO cocotb.pipelined_adder Encrypted IP works! sum=30
...
** TESTS=1 PASS=1 FAIL=0 SKIP=0 71.00 0.00 84278.49 **
The blocks in this section are not run in the site's CI because they reach the broker over the network. The output above is from a run against the 2.1.16 build.
begin_protected … end_protected region:
the key blocks with their key_keyowner and key_keyname, the
data_method, the iv, the data_block and the
auth_tag.key_keyowner is Seiraiyu and checks that the
data_method is one it can decrypt. Both checks fail without any network
request.POST <broker-url>/unwrap (by default
https://broker.ryusim.com/api/v1/unwrap). For an https:// URL,
TLS certificate and host verification are on. RyuSim also accepts an
http:// broker URL without a warning. Over plain HTTP, the key block, the
RYUSIM_API_KEY bearer token and the returned session key cross the network
unencrypted. Use only https:// broker URLs.
The broker decrypts it with the RSA private key and returns the AES session key. Each
attempt has a 10-second connect timeout and a 30-second total timeout. Connection errors
and HTTP 5xx answers are retried, up to 3 attempts with a 2 s and then a 4 s pause. HTTP 4xx
answers are not retried.OPENSSL_cleanse. The parser keeps its own copy of the
source in memory until the compile process exits.obj_dir, compiled, and
deleted when the build succeeds (ryusim -v compile prints
Removed protected source: <file> for each). The object files stay in
obj_dir for linking. The module's generated header also stays; it names the
module's ports and internal signals.data_method | Status |
|---|---|
aes-256-gcm (also aes256-gcm) | Supported. 256-bit key, 96-bit IV, 128-bit authentication tag. A modified payload or tag fails the compile. This is what ryusim-protect writes. |
aes-128-cbc (also aes128-cbc) | Supported, with a warning: warning: <file> uses AES-128-CBC without integrity protection. CBC has no authentication tag, so tampering is not detected. |
| anything else | Rejected before the broker is contacted. |
ryusim-protect encrypts SystemVerilog for IEEE 1735 distribution. It is in
the bin/ directory of the release archive, next to ryusim, and the
installer puts it in the same place as ryusim. Its commands are
encrypt, validate, info,
list-recipients and keygen. The examples below ran with
ryusim-protect 2.1.16. The site's CI does not run them, because its build
tree has no ryusim-protect.
Put `pragma protect begin and `pragma protect end (IEEE 1800-2023
clause 34.5) around the part to encrypt. Everything outside the markers stays plaintext:
the module header, ports, parameters, typedefs and defines. Users can then see the interface
and override parameters. Save this as my_ip.sv:
module my_ip #(
parameter int WIDTH = 8
) (
input logic clk,
input logic [WIDTH-1:0] data_in,
output logic [WIDTH-1:0] data_out
);
`pragma protect begin
always_ff @(posedge clk)
data_out <= data_in + 1'b1;
`pragma protect end
endmodule
ryusim-protect encrypt --input my_ip.sv --output my_ip.svp --recipient ryusim
Encrypted my_ip.sv -> my_ip.svp (1 recipient(s))
The result keeps the plaintext header and replaces the marked region with the envelope.
The session key, IV and ciphertext are random, so every run gives a different file. This is
one such my_ip.svp; the error examples further down use it:
module my_ip #(
parameter int WIDTH = 8
) (
input logic clk,
input logic [WIDTH-1:0] data_in,
output logic [WIDTH-1:0] data_out
);
`pragma protect version = 2
`pragma protect encrypt_agent = "ryusim-protect"
`pragma protect key_keyowner = "Seiraiyu"
`pragma protect key_keyname = "RyuSim-RSA-2026-01"
`pragma protect key_method = rsa
`pragma protect key_block
FPGM0net9ZMVSNjeS7ViKSf+oVfsF3Svf43W9LsNrqj/m9ctrp57ZHgudpqwFKEnHUQwK9OUdaO/1WjkrAMtA02KR+Lq93C4ERpydtqVemPFwtv2Luc/nmayQd8NYRK0aF6KSTDayJXCS+nUpBjCsU+nCoTAUseZnQnm5CJP+YlF5vEKbxkMhlzzCX0zKLWpi8m482DRpQsffGBbTcFVhrPgrLS+/K7zF+GItazmV1FFUndg+aaq7aXxw5aKLVweiGREWVctdA87RLkbVcokkM/+cQOrwCWn6ZVzg5CqBn2LzKW6VrEkb9eixFdOjF2eNWQJFG6/cbAu8cZfv064Ppy8aieFYlejjTpmY9PLB6ousXfuzmvId2YVwtqDkqLC32EC/0UvRdWdZks8blmgzkiNB52bx/OKIXc4dTLKMH3rPGWKht/1v6DU6FZ2d09GYVvcsu5QoB4jB2rhApTY57CR2kkKywo56IkAhv6sjhme+2nlWqtyMzEEKmbGOoVxlHC/BkZlHb7P28zvPwwyD3u7K+TcNIc/0G4Sypydneq4uKD6/p/zH0KRxNPglHRqhP5y+aRWrZ3yAKSJuW/1a/0cZViAc7FHKbIE3cQ+/TfXmlnS84bekCpZ2zMJlTtqhoQ9iJ52KYmiF8wiTLwxULH3MlhRDdH0DcAum0Mx72o=
`pragma protect data_method = "aes-256-gcm"
`pragma protect iv = "6edaf810bbba88ae4b301dfc"
`pragma protect begin_protected
`pragma protect data_block
aGiCkSoF2UY7SO7Zmzqw9Q/6aT0R8aHNINMO2isieqpas2JPZa7SsSBAY/MEgN/J7mYf85irgwkZyKQTILdZAfQ=
`pragma protect auth_tag = "416268653c3a42231c7be1ce394a5758"
`pragma protect end_protected
endmodule
ryusim-protect always encrypts with AES-256-GCM and wraps the session key
with RSA-OAEP (SHA-256). In 2.1.16 its --data-method option changes only the
data_method label written into the envelope, not the cipher, so a file written
with --data-method aes-128-cbc does not compile. Leave the option at its default
(aes-256-gcm).
--recipient names a tool by its registry ID. Give it more than once to encrypt
one file for several tools. Each recipient gets its own key block holding the same session
key, wrapped with that tool's public key; the data block is shared. Removing a recipient
means encrypting again.
ryusim-protect list-recipients
Known Vendor Recipients:
Vendor Key Owner Key Name Notes
------------------------------------------------------------------------------------------
ryusim Seiraiyu RyuSim-RSA-2026-01 RyuSim's own key
vivado Xilinx xilinxt_2019_11 AMD/Xilinx Vivado
questa Mentor Graphics Corporation MGC-VERIF-SIM-RSA-2 Siemens Questa
quartus Altera Corporation Altera-Quartus-RSA-1 Intel Quartus
vcs Synopsys SNPS-VCS-RSA-2 Synopsys VCS
xcelium Cadence Design Systems. CDS-RSA-KEY-VER-1 Cadence Xcelium
Only RyuSim's public key ships with RyuSim. For another registry ID,
ryusim-protect looks for <Key Owner>_<Key Name>_public.pem
in the ieee1735/ directory next to its bin/ and in
~/.ryusim/ieee1735/, and stops if the file is not there:
Error: Public key for 'vivado' not found. Searched:
...
For non-RyuSim recipients, use --recipient-key with the vendor's public key file.
Get the vendor's public key from the vendor, then either copy it to that name in
~/.ryusim/ieee1735/ or give it directly with --recipient-key,
--recipient-owner and --recipient-name (one of each per
recipient):
ryusim-protect encrypt --input my_ip.sv --output my_ip.svp \
--recipient ryusim \
--recipient-key Xilinx_xilinxt_2019_11_public.pem \
--recipient-owner Xilinx --recipient-name xilinxt_2019_11
ryusim-protect keygen --key-owner <owner> --key-name <name> writes an
RSA-2048 key pair (<owner>_<name>_private.pem and
<owner>_<name>_public.pem) for testing a flow with your own key.
validate checks that every required envelope field is present. It does not
decrypt anything or check the base64 data.
ryusim-protect validate my_ip.svp
VALID: All required envelope fields present.
A file with fields missing prints INVALID: Missing required fields: … and
exits with status 1. info prints the envelope metadata, including every key
owner, so you can see which tools a file was encrypted for:
ryusim-protect info my_ip.svp
Version: 2
Encrypt Agent: ryusim-protect
Data Method: aes-256-gcm
Key Owners (1):
- Seiraiyu
Key Names (1):
- RyuSim-RSA-2026-01
IV: 6edaf810bbba88ae4b301dfc
| Threat | What RyuSim does |
|---|---|
Opening the .svp in an editor | The body is AES-encrypted. |
| Extracting the private key from the simulator | RyuSim does not contain it. The broker unwraps session keys server-side. |
| Padding-oracle attacks and modified payloads | AES-256-GCM rejects a modified payload or tag. Files using AES-128-CBC get no such check. |
| Generated C++ left on disk | The .cpp files of protected modules are deleted after a successful build. Their headers and object files remain. |
| Decrypted source left in memory | RyuSim zeroes its working copies. The parser's copy lives until the compile process exits. |
| Abuse of the broker | The broker rate-limits requests; with an API key the limit is per key, without one per client address. It answers HTTP 403 to an API key it does not recognize. |
| A compromised key | Keys are versioned by name (RyuSim-RSA-2026-01), and the broker can hold more than one at once. |
ryusim compile can read the decrypted source from
memory, and the design from the parsed and intermediate forms after that.The messages below are printed by ryusim compile, which then prints
Error: Compilation failed and exits with status 1.
error: no IEEE 1735 key block for key_owner="Seiraiyu" in <file>The file was encrypted for other tools but not for RyuSim. ryusim-protect info
lists the key owners it has. Ask the IP vendor for a file encrypted with
--recipient ryusim. The check runs before any network request, so this
reproduces offline:
sed 's/key_keyowner = "Seiraiyu"/key_keyowner = "Acme"/' my_ip.svp > acme.svp
ryusim compile --top my_ip acme.svp 2>&1 || true
error: unsupported data_method "<method>" in <file>The envelope names a cipher RyuSim does not decrypt (see Data
methods). Ask the vendor for an aes-256-gcm file. Also checked before any
network request:
sed 's/data_method = "aes-256-gcm"/data_method = "des-cbc"/' my_ip.svp > des.svp
ryusim compile --top my_ip des.svp 2>&1 || true
fatal: cannot connect to <broker-url> — network required for encrypted IP (<reason>; 3 attempts)No attempt reached the broker. The reason in parentheses comes from libcurl: a DNS failure,
a refused or timed-out connection (a firewall that blocks HTTPS on port 443), or a TLS
certificate that did not verify. Check the
network, the proxy settings and --broker-url. curl -s
https://broker.ryusim.com/health shows whether the broker is up. Behind a proxy that
inspects TLS, add the proxy's CA certificate to the system trust store. The three attempts
take about 6 seconds when the connection is refused, as with this unused local port:
ryusim compile --top my_ip --broker-url http://127.0.0.1:9/api/v1 my_ip.svp 2>&1 || true
fatal: cannot connect to http://127.0.0.1:9/api/v1 — network required for encrypted IP (Couldn't connect to server; 3 attempts)
Error: Compilation failed
fatal: broker rejected request (HTTP 403) — check broker.ryusim.com statusThe broker refused the request. It does this when RYUSIM_API_KEY holds a key
it does not recognize. Unset the variable or fix the key. If it persists, contact
Seiraiyu support.
fatal: no IEEE 1735 key block found for key_owner="Seiraiyu", key_name="<name>"The broker has no private key under that name (HTTP 404). The file was encrypted with a
RyuSim key the broker does not hold; ryusim-protect info shows the key name.
fatal: broker error (HTTP <code>): <body>Any other non-200 answer. HTTP 429 means the rate limit was hit; the body says how many seconds to wait.
fatal: integrity check failed for encrypted IP in <file> — file may be tamperedThe AES-256-GCM tag does not match: the file changed after it was encrypted, through corruption in transfer or on purpose. Download it again, compare its SHA-256 checksum if the vendor publishes one, and transfer it in binary mode (not FTP ASCII mode). If it still fails, contact the IP vendor. This check runs after the broker returns the session key.
error: <file>:<line>: malformed IEEE 1735 envelope — missing key_block and data_blockA protected region ends without a key block or a data block. A region that never ends
gives error: <file>: unterminated pragma protect region. Run
ryusim-protect validate <file> to see which fields are missing, and get the
file from the vendor again.
For encrypted SystemVerilog to compile in RyuSim, its envelope needs a key block with
key_keyowner="Seiraiyu" and key_keyname="RyuSim-RSA-2026-01",
wrapped with RyuSim's public key. The release archive's ieee1735/ directory
(installed to <prefix>/ieee1735/) holds:
Seiraiyu_RyuSim-RSA-2026-01_public.pem: the RSA-2048 public key in PEM
format.ryusim_ieee1735.json: the same facts, machine-readable:{
"tool": "RyuSim",
"vendor": "Seiraiyu",
"key_owner": "Seiraiyu",
"key_name": "RyuSim-RSA-2026-01",
"key_method": "rsa",
"key_size": 2048,
"supported_data_methods": ["aes-256-gcm", "aes-128-cbc"],
"supported_languages": ["verilog", "systemverilog"],
"public_key_file": "Seiraiyu_RyuSim-RSA-2026-01_public.pem",
"broker_url": "https://broker.ryusim.com/api/v1/unwrap"
}
Use aes-256-gcm as the data_method.
To add your tool to the ryusim-protect list-recipients registry, email
ip-protection@seiraiyu.com with the tool and
vendor name, the key_keyowner and key_keyname strings your tool
expects, and your RSA public key in PEM format. Seiraiyu checks the key by encrypting a test
file that your tool must decrypt, then adds the tool in the next release.