Python AEAD encryption using Post Quantum Cryptography (ML-KEM)
This is a simple python library and cli using ML-KEM to wrap encrypt/decrypt arbitrary data.
This is basically hybrid encryption where an ML-KEM keypair's public key is used to generate a sharedSecret which afer a key derivation is used as an AES-GCM encryption key ultimately used to encrypt the data.
This library also supports Google Cloud KMS ML-KEM
NOTE: this library is note supported by Google and is experimental/unreviewed (caveat emptor)
Also see:
-
Key Encapsulation ML-KEM
This library currently use liboqs-python which itself has a dependency on C and installation of liboqs.so. Eventually, when there's a pure-python implementation of mlkem, i'll use that instead. see pyca/cryptography/issues/12824.
Please note later on the keyFormat would adopt rfc9629 if/when officially approved.
QuickStart
Using docker image from example/Dockerfile
docker run -v /dev/urandom:/dev/urandom -ti salrashid123/openssl-liboqs-python
then within the container
git clone https://github.com/salrashid123/python_pqc_wrapping.git
cd python_pqc_wrapping/example/
virtualenv env
source env/bin/activate
pip3 install python-pqc-wrapping
# encrypt
python3 encrypt.py
# decrypt
python3 decrypt.py
### to print key details
openssl pkey -in certs/bare-seed-768.pem -text
openssl asn1parse -inform PEM -in certs/bare-seed-768.pem
openssl asn1parse -inform PEM -in certs/pub-ml-kem-768-bare-seed.pem
Setup
First install liboqs-python
apt install astyle cmake gcc ninja-build libssl-dev python3-pytest python3-pytest-xdist unzip xsltproc doxygen graphviz python3-yaml valgrind git
git clone --depth=1 https://github.com/open-quantum-safe/liboqs
cmake -S liboqs -B liboqs/build -DBUILD_SHARED_LIBS=ON
cmake --build liboqs/build --parallel 8
cmake --build liboqs/build --target install
# if you want a different installation directory (eg /apps/liboqs),
### add -DCMAKE_INSTALL_PREFIX=/apps/liboqs to the configure command above.
### when running the samples, set the env var
### export OQS_INSTALL_PATH=/apps/liboqs
also on
if you alreeady have liboqs installed,
export OQS_INSTALL_PATH=/path/to/liboqs
pip install python-pqc-wrapping
Usage
If you want to encrypt data intended for a remote system, the remote system must first generate an ML-KEM key pair and share the public key
If Bob wants to encrypt data for Alice
-
Alice generates
MK-KEMkeypair (pub.pem,priv.pem) -
Alice shares public key
pub.pemwith Bob
Encrypt (Bob):
-
generate encapsulation data using
pub.pemkemSharedSecret, kemCipherText = ML_KEM_Encapsulate( pub.pem ) -
Derive a new key using
kemSharedSecretto AEAD key to encryptplainTextderivedKey = HKDF( kemSharedSecret )cipherText = AEAD_Encrypt( derivedKey, plainText ) -
Bob sends
[ kemCipherText, cipherText ]to Alice
Decrypt (Alice):
-
derive same shared secret using private key
priv.pemkemSharedSecret = ML_KEM_Decapsulate( priv.pem, kemCipherText ) -
derivedKey = HKDF( kemSharedSecret ) -
plaintext = AEAD_Decrypt( derivedKey, cipherText )
This extends how ml-kem is used by employing the kemSharedSecret as a wrapping AES256-GCM encryptionKey. For reference, the basic flow is described here in FIPS 203 (page 12)
Key Generation
This repo only support PEM encoded files which encodes the bare-seed. See the #openssl-key-formats section below.
The PEM file itself is described in draft-ietf-lamps-kyber-certificates-11 where just the seed is required (see seciton C.1.2.1. Seed Format)
To generate a key, simply openssl 3.5.0+ which you can get in a dockerfile format at Post-Quantum Cryptography (PQC) scratchpad.
or with openssl
$ openssl -version
OpenSSL 3.5.0-dev (Library: OpenSSL 3.5.0-dev )
### generate ML-KEM-768
$ openssl genpkey -algorithm mlkem768 \
-provparam ml-kem.output_formats=bare-seed \
-out priv-ml-kem-768-bare-seed.pem
openssl pkey -in priv-ml-kem-768-bare-seed.pem -pubout -out pub-ml-kem-768-bare-seed.pem
If your openssl version does not support mlkem, you can use a dockerfile
docker run -v /dev/urandom:/dev/urandom -ti salrashid123/openssl-pqs:3.5.0-dev
The example/util folder contains two samples
-
this generates a key from a known seed and create the public and private PEM key files. Just comment out the static seed to create new keys.
-
this reads a PEM public and private key and creates an mlkem client and server
if you want ot use go, see this example
Library
Encrypt
To encrypt, you need to provide the PEM format of the ML-KEM public key:
from python_pqc_wrapping import BaseWrapper
public_key_file_path= 'certs/pub-ml-kem-768-bare-seed.pem'
with open(public_key_file_path, 'r') as f:
public_key_string = f.read()
client_data = '{"foo":"bar"}'
be = BaseWrapper(publicKey=public_key_string,keyName="mykey", clientData=client_data)
en = be.encrypt(plaintext=b'foo', aad=b'myaad')
print(en)
Decrypt
To decrypt, you need to provide the PEM bare-seed format of the public key.
from python_pqc_wrapping import BaseWrapper
private_key_file_path= 'certs/bare-seed-768.pem'
with open(private_key_file_path, 'r') as f:
private_key_string = f.read()
with open("/tmp/encrypted.json", 'r') as file:
encrypted_data = file.read()
client_data = '{"foo":"bar"}'
bd = BaseWrapper(privateKey=private_key_string, clientData=client_data)
dn = bd.decrypt(blob_info=encrypted_data, aad=b'myaad')
print(dn)
see example/ folder:
Client Data
You can also embed additional arbitrary JSON data into the protobuf as client_data structure. This data is not included in the encryption and is not directly related to the AdditionalData (AAD) associated with aes-gcm.
The client data field is instead just unencrypted/unverified data you can associate with the encoded key.
However, you can canonicalize the client_data as JSON, hash that and use that hash value as the AAD. In effect, the client_data can then be used as part of the integrity calculation.
For example, if the client data is
"clientData": {
"location": {
"region": "us",
"zone": "central"
},
"provider": "pqc"
}
Note that you can specify the client data either in the overall wrapper config or during each encrypt/decrypt method. TODO: If specified in the encrypt/decrypt methods, it takes priority.
GCP KMS
This library also supports encapsulation using GCP KMS MLKEM
To use this mode, first create a kms key
gcloud kms keyrings create kem_kr --location=global
gcloud kms keys create kem_key_1 \
--keyring kem_kr \
--location global \
--purpose "key-encapsulation" \
--default-algorithm ml-kem-768 \
--protection-level "software"
gcloud kms keys versions get-public-key 1 \
--key kem_key_1 \
--keyring kem_kr \
--location global \
--output-file /tmp/kem_pub.nist \
--public-key-format nist-pqc
The extract the public key into PEM format:
$ openssl --version
OpenSSL 3.5.0-dev (Library: OpenSSL 3.5.0-dev )
### for ML-KEM-768
$ { echo -n "MIIEsjALBglghkgBZQMEBAIDggShAA==" | base64 -d ; cat /tmp/kem_pub.nist; } | openssl asn1parse -inform DER -in -
0:d=0 hl=4 l=1202 cons: SEQUENCE
4:d=1 hl=2 l= 11 cons: SEQUENCE
6:d=2 hl=2 l= 9 prim: OBJECT :ML-KEM-768
17:d=1 hl=4 l=1185 prim: BIT STRING
$ cd example/
$ { echo -n "MIIEsjALBglghkgBZQMEBAIDggShAA==" | base64 -d ; cat /tmp/kem_pub.nist; } \
| openssl pkey -inform DER -pubin -pubout -out certs/pub-ml-kem-768-kms.pem
then if your environment is authorized to access the key (eg, GOOGLE_APPLICATION_CREDENTIALS service account below has access to the key above).
The uri you will specify to decrypt is set with the kms uri prefixed with gcpkms://. For example
private_key_file_path= 'gcpkms://projects/core-eso/locations/global/keyRings/kem_kr/cryptoKeys/kem_key_1/cryptoKeyVersions/1'
bd = BaseWrapper(privateKey=private_key_file_path, clientData=client_data, decrypt=True)
Wrapped Key format
There are two levels of encryption involved with this library and is best described in this flow:
TODO: Reformat proto to use KEMRecipientInfo: Using Key Encapsulation Mechanism (KEM) Algorithms in the Cryptographic Message Syntax (CMS), A PQC Almanac
- Encrypt
-
Read the
ML-KEMPublic key and generatesharedCiphertextandsharedSecretsharedCiphertext, shared_secret_server = server.encap_secret(public_key_bytes)
-
Create new direct
aes-gcmclient but first derive a new key fromshared_secret_serverand then use that as the key. Finally, prepend the nonce/iv to the iv into the ciphertextderived_key = KDF(shared_secret_server) aesgcm = AESGCM(derived_key) nonce = os.urandom(self.DEFAULT_IV_SIZE) ciphertext = nonce + aesgcm.encrypt(nonce, plaintext, aad)
ciphertext: the encrypted data wrapped usingkemSharedSecretamd already includes the initialization vectorwrappedKey: the ML-KEMsharedCiphertext
{
"ciphertext": "TdCcswd2btZN4DjioUVZU33lK4YyAq/PltA18M4nQA==",
"keyInfo": {
"mechanism": "1",
"keyId": "mykey",
"wrappedKey": "ewoibmFtZSI6ICJteWtleSIsCiJ2ZXJzaW9uIjogMiwKInR5cGUiOiAibWxfa2VtXzc2OCIsCiJrZW1DaXBoZXJUZXh0IjogIk1mTW00ZG00bjZKU3pFbjJCSmZ1QXJIRndkMmxTU0JHVGFiMGhkTFZpNEhYSXN6MlVwSlVUUXE0WUpKUVpiZTFtOUpqZzlqbXpidForZklhbEJvQWpFa0lMNGRkemN2UTJWK2pxM0FKSGpLRTNZWTl0V3kxbjRTWmhTYm9SMy9scmRwd29Odm9FemRKWGhOdHZVMDJDVktYRDFpdFpwaGsrT0dNV1hSNWJUTmtpeHZQellwbTdaTmpmWTZhSzZRUG5DNmZtby9DMkdTQSswL2VRdDhOeXVHQ25TMUhJVFVDSDVvQjZrL2FzdTkxYmNuSHpKMzY2cjNuSEdROGRJSkphYlpoa1JsanN0YTJHZXd0VzNENzE2MzhqVUZyT1h1ZUpmRENQbEQwRmRHeU1zZm1TTDlEdHVWZ1RmdVNkNUJJU3RqN1lJUHRGK2RBQmNoR0FvZmM0dXZvNStzdXNNWXAxZEhjM1ZaVzMrcVF4Z1pIM1J2bTg3b0hSUXhZVGs4cmt4SkRJcktIUkxTc2tUR2JlYU4zSmFwZ3FtQmJndmV2MVd4Zy9mdmdyWkRlWlBrelI4T29Jd29UVWRzc0NUUDV4MTRvYVZkbk9iVmdGLytyOUVYRklhVklUb1J1Ri9XNWE5Q0lsSzlES2JoK21vb2pmSVpyd2F6amlIR2FpM0R5VkdqSFJUU0RBeDZyNmZPcnlJUFh5QXpRMVhseFBsa2gyZjlVMGw1RTNqdlhEMUJMSGtUM1o1WUl2VENVNDNXTjBNVkRSb01PemhoNy9zWGp6RENwY1B1dkpjemVjQ2hCNWNFQXJYV2JDR2RIc2ZtTWNEeGRSZ0lRUDUyS1FvS0VqaEtQNUhVb1BFWU5GOFdSNy9DbVEydXV6U2I1K0RteEcrODJHSk9IUW5oa1dMajRlYXYvcXJ0YmlsdFZjbURzaFR6eXBXVnlZV2Y2cWZLc3BJU0p1c3RYWjJKbURwVmNDN3c2UndhQ3BsTzVtME5qdHFqdmdDZTVtcjQrNnh0S3p6OHJJek1PalhielI4ZEdsaHpKWkxTZGhJQTl3eHRJa0pQUGxuYUw3QzY2NGVob3l4WGRsRElxdVFCdXNONHJsZmNoVDloZnQvODhkMjlLajM1cE84Nkg3bzhZbThWcEZSVmRJMU1SNENqKzZ6MURFdmpLVnk2c1FJVGl5M21ZeUxqZ1orcUZEMWVsL3M4VjBHUWl0bHpBeEMyY3c2bDVKckJYUzk3M2Y0VGJPdGd6V0lyZmcxNFhNRTJhb3NIL2pTaktkdFRVVEZzKzlpY0Q1cnA5ZmNjNC92aS9NajFzZW5YNFhqbDFEUTBuSGRQd29oK2pzQVZaem8vMnpJNXcwTng0dGpFeEU5TVJtVnRkSFV3bDk1QmxvYmt5djZGcmJBRDJlQ0I1SzhoRU5OSzc2QTBubE5NT2NtMlFpeVBLNGZCZVBTZytsb2tMeDh2aG5EeTRSaENkN3JtS1dpMytiUkFjMTNnTndxZG9HS0RVSkVPRVU5azJBM2pNdkxQNVE4ZE0rQzhZckp6eGJzNFJvUVdYWkp6emhyWGdPbUlJWHJOTG9XMjN2NTNTdWEyeWFpRVdPaCtXS2dtOVN2ZWRlR3NscWFSL01iUzVhVXBrNUJaa29Uc1VGWUFka3pnQi9XdENETGlQOW9RZEh3Q1RHTWZ5YmE1THJyRmsyczBzRDgrZjNBcm5veWJyM24xQzRBL29DbUZ3SXJKaTF2OUI0eTVBbHIzdEVaUHdQL0FxQllxZnNzcXVKOFFwOC8rVUUzc0UrVmMrdkVpVEhNUjlzTVMwL1VYMlRySlBaK05WVHVFTTdRZnQzQktoS2NPQVNvWlFIQlo3bXpoQWV2SllFeGtKaFR1Qm9kb1BUSTRWbzFPVE5wZW0zTjd6OEpmOFh5amFTbVB0d3JzPSIsCiJrZGZTYWx0IjogIlcyc3R0ZC8vdmozWUtnWWMiCn0="
},
"clientData": {
"foo": "bar"
}
}
If you base64decode the wrappedKey
kemCipherTexttheML-KEMciphertext (egsharedCiphertext). Once this is decapsulated, this becomes an AES GCM after KDF
The decoded keyfile is:
{
"name": "mykey",
"version": 2,
"type": "ml_kem_768",
"kemCipherText": "MfMm4dm4n6JSzEn2BJfuArHFwd2lSSBGTab0hdLVi4HXIsz2UpJUTQq4YJJQZbe1m9Jjg9jmzbtZ+fIalBoAjEkIL4ddzcvQ2V+jq3AJHjKE3YY9tWy1n4SZhSboR3/lrdpwoNvoEzdJXhNtvU02CVKXD1itZphk+OGMWXR5bTNkixvPzYpm7ZNjfY6aK6QPnC6fmo/C2GSA+0/eQt8NyuGCnS1HITUCH5oB6k/asu91bcnHzJ366r3nHGQ8dIJJabZhkRljsta2GewtW3D71638jUFrOXueJfDCPlD0FdGyMsfmSL9DtuVgTfuSd5BIStj7YIPtF+dABchGAofc4uvo5+susMYp1dHc3VZW3+qQxgZH3Rvm87oHRQxYTk8rkxJDIrKHRLSskTGbeaN3JapgqmBbgvev1Wxg/fvgrZDeZPkzR8OoIwoTUdssCTP5x14oaVdnObVgF/+r9EXFIaVIToRuF/W5a9CIlK9DKbh+moojfIZrwazjiHGai3DyVGjHRTSDAx6r6fOryIPXyAzQ1XlxPlkh2f9U0l5E3jvXD1BLHkT3Z5YIvTCU43WN0MVDRoMOzhh7/sXjzDCpcPuvJczecChB5cEArXWbCGdHsfmMcDxdRgIQP52KQoKEjhKP5HUoPEYNF8WR7/CmQ2uuzSb5+DmxG+82GJOHQnhkWLj4eav/qrtbiltVcmDshTzypWVyYWf6qfKspISJustXZ2JmDpVcC7w6RwaCplO5m0NjtqjvgCe5mr4+6xtKzz8rIzMOjXbzR8dGlhzJZLSdhIA9wxtIkJPPlnaL7C664ehoyxXdlDIquQBusN4rlfchT9hft/88d29Kj35pO86H7o8Ym8VpFRVdI1MR4Cj+6z1DEvjKVy6sQITiy3mYyLjgZ+qFD1el/s8V0GQitlzAxC2cw6l5JrBXS973f4TbOtgzWIrfg14XME2aosH/jSjKdtTUTFs+9icD5rp9fcc4/vi/Mj1senX4Xjl1DQ0nHdPwoh+jsAVZzo/2zI5w0Nx4tjExE9MRmVtdHUwl95Blobkyv6FrbAD2eCB5K8hENNK76A0nlNMOcm2QiyPK4fBePSg+lokLx8vhnDy4RhCd7rmKWi3+bRAc13gNwqdoGKDUJEOEU9k2A3jMvLP5Q8dM+C8YrJzxbs4RoQWXZJzzhrXgOmIIXrNLoW23v53Sua2yaiEWOh+WKgm9SvedeGslqaR/MbS5aUpk5BZkoTsUFYAdkzgB/WtCDLiP9oQdHwCTGMfyba5LrrFk2s0sD8+f3Arnoybr3n1C4A/oCmFwIrJi1v9B4y5Alr3tEZPwP/AqBYqfssquJ8Qp8/+UE3sE+Vc+vEiTHMR9sMS0/UX2TrJPZ+NVTuEM7Qft3BKhKcOASoZQHBZ7mzhAevJYExkJhTuBodoPTI4Vo1OTNpem3N7z8Jf8XyjaSmPtwrs=",
"kdfSalt": "W2sttd//vj3YKgYc"
}
If you want to use cli to decode everything run
$ cat /tmp/encrypted.json | jq -r '.'
$ cat /tmp/encrypted.json | jq -r '.keyInfo.wrappedKey' | base64 -d - | jq -r '.'
Versions
The following lists the Version values used in the encoding of the key.
Its important to decrypt using a cli or library version which is consistent with the proto or key encoding formats.
| KeyVersion | Date |
|---|---|
| 2 | 1/2/26 |
| 3 | 1/7/26 |
Build
If you want to regenerate with protoc:
$ /usr/local/bin/protoc --version
libprotoc 25.1
$ /usr/bin/protoc --proto_path=/usr/include/ -I/usr/include/ -I ./ --include_imports --proto_path=pqcwrappb/ \
--experimental_allow_proto3_optional --include_source_info \
--descriptor_set_out=pqcwrappb/wrap.proto.pb --pyi_out=. \
--python_out=. pqcwrappb/wrap.proto pqcwrappb/blobwrap.proto
To test locally,
python3 setup.py sdist bdist_wheel
cd example
virtualenv env
source env/bin/activate
pip3 install ../
## depending on the variant provider
# pip3 install -r requirements-gcp.txt
### to deploy/upload
# virtualenv env
# source env/bin/activate
# python3 -m pip install --upgrade build
# python3 -m pip install --upgrade twine
# python3 -m build
# python3 -m twine upload --repository testpypi dist/*
# python3 -m twine upload dist/*
Openssl key formats
Openssl PEM files encodes a custom 'format' prefix as shown here.
For example, if you generated the key with a seed-only, the PEM file will have a prefix of 0x8040 for the raw key:
$ openssl asn1parse -inform PEM -in example/certs/bare-seed-768.pem
0:d=0 hl=2 l= 82 cons: SEQUENCE
2:d=1 hl=2 l= 1 prim: INTEGER :00
5:d=1 hl=2 l= 11 cons: SEQUENCE
7:d=2 hl=2 l= 9 prim: OBJECT :ML-KEM-768
18:d=1 hl=2 l= 64 prim: OCTET STRING [HEX DUMP]:67E6BC81C846808002CED71BBF8A8C4195AF2A37614C4C81C0B649601B29BEAA33CBFF214A0DC459749362C8B3D4DD7C754A0D611D51D3449C2FA47C1DC49C5E
$ openssl asn1parse -inform PEM -in example/certs/seed-only-768.pem
0:d=0 hl=2 l= 84 cons: SEQUENCE
2:d=1 hl=2 l= 1 prim: INTEGER :00
5:d=1 hl=2 l= 11 cons: SEQUENCE
7:d=2 hl=2 l= 9 prim: OBJECT :ML-KEM-768
18:d=1 hl=2 l= 66 prim: OCTET STRING [HEX DUMP]:804067E6BC81C846808002CED71BBF8A8C4195AF2A37614C4C81C0B649601B29BEAA33CBFF214A0DC459749362C8B3D4DD7C754A0D611D51D3449C2FA47C1DC49C5E
For a list of all prefixes:
static const ML_COMMON_PKCS8_FMT ml_kem_768_p8fmt[NUM_PKCS8_FORMATS] = {
{ "seed-priv", 0x09aa, 0, 0x308209a6, 0x0440, 6, 0x40, 0x04820960, 0x4a, 0x0960, 0, 0, },
{ "priv-only", 0x0964, 0, 0x04820960, 0, 0, 0, 0, 0x04, 0x0960, 0, 0, },
{ "oqskeypair", 0x0e04, 0, 0x04820e00, 0, 0, 0, 0, 0x04, 0x0960, 0x0964, 0x04a0 },
{ "seed-only", 0x0042, 2, 0x8040, 0, 2, 0x40, 0, 0, 0, 0, 0, },
{ "bare-priv", 0x0960, 4, 0, 0, 0, 0, 0, 0, 0x0960, 0, 0, },
{ "bare-seed", 0x0040, 4, 0, 0, 0, 0x40, 0, 0, 0, 0, 0, },
};
Note, you can extract the seed from a key using openssl:
$ openssl pkey -in example/certs/seed-only-768.pem -text
ML-KEM-768 Private-Key:
seed:
67:e6:bc:81:c8:46:80:80:02:ce:d7:1b:bf:8a:8c:
41:95:af:2a:37:61:4c:4c:81:c0:b6:49:60:1b:29:
be:aa:33:cb:ff:21:4a:0d:c4:59:74:93:62:c8:b3:
d4:dd:7c:75:4a:0d:61:1d:51:d3:44:9c:2f:a4:7c:
1d:c4:9c:5e
Which as hex is 67E6BC81C846808002CED71BBF8A8C4195AF2A37614C4C81C0B649601B29BEAA33CBFF214A0DC459749362C8B3D4DD7C754A0D611D51D3449C2FA47C1DC49C5E
Since this repo only supports the bare-seed key, you'll need to convert it
## create a key with default seed-priv (implicitly by default or by specifying ml-kem.output_formats )
openssl genpkey -algorithm mlkem768 -out priv-ml-kem-768-seed-priv.pem
openssl asn1parse -in priv-ml-kem-768-seed-priv.pem
openssl genpkey -algorithm mlkem768 \
-provparam ml-kem.output_formats=seed-priv \
-out priv-ml-kem-768-seed-priv.pem
openssl asn1parse -in priv-ml-kem-768-seed-priv.pem
## print the seed
openssl pkey -in priv-ml-kem-768-seed-priv.pem -text
ML-KEM-768 Private-Key:
seed:
bf:bd:29:76:bd:01:87:e3:75:0e:5c:46:4e:fc:e0:
5a:0a:b6:ca:0a:b4:0c:f7:c4:90:08:1b:54:83:1f:
12:18:25:50:15:7f:49:e0:24:7b:92:b7:b9:b2:de:
49:21:74:53:71:9a:81:71:c6:cd:15:83:23:da:d2:
c6:6d:ef:2b
### now convert
openssl pkey -in priv-ml-kem-768-seed-priv.pem \
-provparam ml-kem.output_formats=bare-seed \
-out priv-ml-kem-768-bare-seed.pem
### and veify the seed is the same
openssl pkey -in priv-ml-kem-768-bare-seed.pem -text
ML-KEM-768 Private-Key:
seed:
bf:bd:29:76:bd:01:87:e3:75:0e:5c:46:4e:fc:e0:
5a:0a:b6:ca:0a:b4:0c:f7:c4:90:08:1b:54:83:1f:
12:18:25:50:15:7f:49:e0:24:7b:92:b7:b9:b2:de:
49:21:74:53:71:9a:81:71:c6:cd:15:83:23:da:d2:
c6:6d:ef:2b
also see
- OpenSSL Position and Plans on Private Key Formats for the ML-KEM and ML-DSA Post-quantum (PQ) Algorithms
- Let’s All Agree to Use Seeds as ML-KEM Keys
Ml-KEM x509 Certificate
You can also encode the ML-KEM public key within a signed x509 certificate. For details, see ml-kem x509
References
Metadata
Release files for python-pqc-wrapping 0.0.15
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | |
|---|---|---|---|
| python_pqc_wrapping-0.0.15.tar.gz | 31.7 kB | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| python_pqc_wrapping-0.0.15-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 56.2 kB
Release files / python_pqc_wrapping-0.0.15.tar.gz
| Download URL | python_pqc_wrapping-0.0.15.tar.gz |
|---|---|
| Size | 31.7 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
af5ac478dc5f251a466424c77e8f881147e8a11f844da1c6950f0eb695011d03
|
|
BLAKE2b-256 checksum How to use checksums |
a3360c69fbbf04181f0504b3c0ed8bbf154ee84f8b73a2dfd8a3a113f036250d
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/6.1.0 CPython/3.13.7
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Jan 7, 2026.
Transparency logRelease files / python_pqc_wrapping-0.0.15-py3-none-any.whl
| Download URL | python_pqc_wrapping-0.0.15-py3-none-any.whl |
|---|---|
| Size | 24.4 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
b8f4ec19f1a49bc5934b969818d378d4e7779d9960d14b9246e41e3374ebe4ed
|
|
BLAKE2b-256 checksum How to use checksums |
32d3a9931b7ae2585b15a4490488f4e64509166e9613d2a986561f8fc1821712
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/6.1.0 CPython/3.13.7
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Jan 7, 2026.
Transparency log