[<prev] [next>] [<thread-prev] [thread-next>] [day] [month] [year] [list]
Message-ID: <1176796.1768921455@warthog.procyon.org.uk>
Date: Tue, 20 Jan 2026 15:04:15 +0000
From: David Howells <dhowells@...hat.com>
To: Eric Biggers <ebiggers@...nel.org>,
Stephan Mueller <smueller@...onox.de>
Cc: dhowells@...hat.com, linux-crypto@...r.kernel.org,
linux-kernel@...r.kernel.org, Ard Biesheuvel <ardb@...nel.org>,
"Jason A . Donenfeld" <Jason@...c4.com>,
Herbert Xu <herbert@...dor.apana.org.au>
Subject: Python script to generate X509/CMS from NIST testcases
Hi Eric, Stephan,
In case it turns out to be useful to you as a template, here's a script that I
wrote to package NIST ML-DSA testcases from JSON files into rudimentary X.509,
message and CMS signature files and also to produce a C file that contains
those blobs packaged into u8 arrays with a table listing them all.
It also tries to verify each testcase with "openssl smime" - except that that
doesn't work too will for ML-DSA (it did work for RSASSA-PSS, but that's
another script).
David
---
#!/usr/bin/python3
#
# Generate X.509 certificates and CMS messages from NIST ML-DSA SigVer test
# vectors (e.g. prompt.json).
import os
import sys
import datetime
import subprocess
import asn1tools
import json
if len(sys.argv) < 3:
print("Format x509_gen.py <prompt.json> <expectedResults.json>", file=sys.stderr)
exit(2)
OID_rsaEncryption = "1.2.840.113549.1.1.1"
OID_sha1WithRSAEncryption = "1.2.840.113549.1.1.5"
OID_id_mgf1 = "1.2.840.113549.1.1.8"
OID_id_rsassa_pss = "1.2.840.113549.1.1.10"
OID_sha256WithRSAEncryption = "1.2.840.113549.1.1.11"
OID_sha384WithRSAEncryption = "1.2.840.113549.1.1.12"
OID_sha512WithRSAEncryption = "1.2.840.113549.1.1.13"
OID_sha224WithRSAEncryption = "1.2.840.113549.1.1.14"
OID_sha1 = "1.3.14.3.2.26"
OID_sha256 = "2.16.840.1.101.3.4.2.1"
OID_sha384 = "2.16.840.1.101.3.4.2.2"
OID_sha512 = "2.16.840.1.101.3.4.2.3"
OID_sha224 = "2.16.840.1.101.3.4.2.4"
OID_commonName = "2.5.4.3"
OID_subjectKeyIdentifier = "2.5.29.14"
OID_keyUsage = "2.5.29.15"
OID_basicConstraints = "2.5.29.19"
OID_data = "1.2.840.113549.1.7.1"
OID_signed_data = "1.2.840.113549.1.7.2"
OID_id_ml_dsa_44 = "2.16.840.1.101.3.4.3.17"
OID_id_ml_dsa_65 = "2.16.840.1.101.3.4.3.18"
OID_id_ml_dsa_87 = "2.16.840.1.101.3.4.3.19"
###############################################################################
#
# ASN.1 definitions
#
###############################################################################
RSA = asn1tools.compile_string("""
Rsa DEFINITIONS ::= BEGIN
RsaPubKey ::= SEQUENCE {
n INTEGER,
e INTEGER
}
RSASSA-PSS-params ::= SEQUENCE {
hashAlgorithm [0] HashAlgorithm,
maskGenAlgorithm [1] MaskGenAlgorithm,
saltLength [2] INTEGER,
trailerField [3] TrailerField OPTIONAL
}
HashAlgorithm ::= AlgorithmIdentifier
MaskGenAlgorithm ::= AlgorithmIdentifier
TrailerField ::= INTEGER
AlgorithmIdentifier ::= SEQUENCE {
algorithm OBJECT IDENTIFIER,
parameters ANY OPTIONAL
}
END""", 'der')
#------------------------------------------------------------------------------
# X.509
#
X509 = asn1tools.compile_string("""
X509 DEFINITIONS ::= BEGIN
Certificate ::= SEQUENCE {
tbsCertificate TBSCertificate,
signatureAlgorithm AlgorithmIdentifier,
signature BIT STRING
}
TBSCertificate ::= SEQUENCE {
version [ 0 ] Version,
serialNumber CertificateSerialNumber,
signature AlgorithmIdentifier,
issuer Name,
validity Validity,
subject Name,
subjectPublicKeyInfo SubjectPublicKeyInfo,
issuerUniqueID [ 1 ] IMPLICIT UniqueIdentifier OPTIONAL,
subjectUniqueID [ 2 ] IMPLICIT UniqueIdentifier OPTIONAL,
extensions [ 3 ] Extensions OPTIONAL
}
Version ::= INTEGER
CertificateSerialNumber ::= INTEGER
AlgorithmIdentifier ::= SEQUENCE {
algorithm OBJECT IDENTIFIER,
parameters ANY OPTIONAL
}
Name ::= SEQUENCE OF RelativeDistinguishedName
RelativeDistinguishedName ::= SET OF AttributeValueAssertion
AttributeValueAssertion ::= SEQUENCE {
attributeType OBJECT IDENTIFIER,
attributeValue UTF8String -- Really ANY
}
Validity ::= SEQUENCE {
notBefore Time,
notAfter Time
}
Time ::= CHOICE {
utcTime UTCTime,
generalTime GeneralizedTime
}
SubjectPublicKeyInfo ::= SEQUENCE {
algorithm AlgorithmIdentifier,
subjectPublicKey BIT STRING
}
UniqueIdentifier ::= BIT STRING
Extensions ::= SEQUENCE OF Extension
Extension ::= SEQUENCE {
extnid OBJECT IDENTIFIER,
critical BOOLEAN,
extnValue OCTET STRING
}
END""", 'der')
#------------------------------------------------------------------------------
# PKCS#7
#
PKCS7 = asn1tools.compile_string("""
PKCS7 DEFINITIONS ::= BEGIN
PKCS7ContentInfo ::= SEQUENCE {
contentType ContentType,
content [0] EXPLICIT SignedData OPTIONAL
}
ContentType ::= OBJECT IDENTIFIER
SignedData ::= SEQUENCE {
version INTEGER,
digestAlgorithms DigestAlgorithmIdentifiers,
contentInfo ContentInfo,
certificates CHOICE {
certSet [0] IMPLICIT ExtendedCertificatesAndCertificates,
certSequence [2] IMPLICIT Certificates
} OPTIONAL,
crls CHOICE {
crlSet [1] IMPLICIT CertificateRevocationLists,
crlSequence [3] IMPLICIT CRLSequence
} OPTIONAL,
signerInfos SignerInfos
}
ContentInfo ::= SEQUENCE {
contentType ContentType,
content [0] EXPLICIT Data OPTIONAL
}
Data ::= ANY
DigestAlgorithmIdentifiers ::= CHOICE {
daSet SET OF DigestAlgorithmIdentifier,
daSequence SEQUENCE OF DigestAlgorithmIdentifier
}
DigestAlgorithmIdentifier ::= SEQUENCE {
algorithm OBJECT IDENTIFIER,
parameters ANY OPTIONAL
}
ExtendedCertificatesAndCertificates ::= SET OF ExtendedCertificateOrCertificate
ExtendedCertificateOrCertificate ::= CHOICE {
certificate Certificate,
extendedCertificate [0] IMPLICIT ExtendedCertificate
}
ExtendedCertificate ::= Certificate
Certificates ::= SEQUENCE OF Certificate
CertificateRevocationLists ::= SET OF CertificateList
CertificateList ::= SEQUENCE OF Certificate
CRLSequence ::= SEQUENCE OF CertificateList
Certificate ::= BOOLEAN -- This really needs to be ANY, but asn1tools explodes
SignerInfos ::= CHOICE {
siSet SET OF SignerInfo,
siSequence SEQUENCE OF SignerInfo
}
SignerInfo ::= SEQUENCE {
version INTEGER,
sid SignerIdentifier,
digestAlgorithm DigestAlgorithmIdentifier,
authenticatedAttributes CHOICE {
aaSet [0] IMPLICIT SetOfAuthenticatedAttribute,
aaSequence [2] EXPLICIT SEQUENCE OF AuthenticatedAttribute
} OPTIONAL,
digestEncryptionAlgorithm
DigestEncryptionAlgorithmIdentifier,
encryptedDigest EncryptedDigest,
unauthenticatedAttributes CHOICE {
uaSet [1] IMPLICIT SET OF UnauthenticatedAttribute,
uaSequence [3] IMPLICIT SEQUENCE OF UnauthenticatedAttribute
} OPTIONAL
}
SignerIdentifier ::= CHOICE {
issuerAndSerialNumber IssuerAndSerialNumber,
subjectKeyIdentifier [0] IMPLICIT SubjectKeyIdentifier
}
IssuerAndSerialNumber ::= SEQUENCE {
issuer Name,
serialNumber CertificateSerialNumber
}
CertificateSerialNumber ::= INTEGER
SubjectKeyIdentifier ::= OCTET STRING
SetOfAuthenticatedAttribute ::= SET OF AuthenticatedAttribute
AuthenticatedAttribute ::= SEQUENCE {
type OBJECT IDENTIFIER,
values SET OF ANY
}
UnauthenticatedAttribute ::= SEQUENCE {
type OBJECT IDENTIFIER,
values SET OF ANY
}
DigestEncryptionAlgorithmIdentifier ::= SEQUENCE {
algorithm OBJECT IDENTIFIER,
parameters ANY OPTIONAL
}
EncryptedDigest ::= OCTET STRING
Name ::= SEQUENCE OF RelativeDistinguishedName
RelativeDistinguishedName ::= SET OF AttributeValueAssertion
AttributeValueAssertion ::= SEQUENCE {
attributeType OBJECT IDENTIFIER,
attributeValue UTF8String -- Really ANY
}
END""", 'der')
###############################################################################
#
# Write a C data array from a bytestring.
#
###############################################################################
def write_c_hexarray(cfile, name, data):
cfile.write("static const u8 " + name + "[] __initconst = {\n")
need_close = False
for i in range(0, len(data)):
if not need_close:
cfile.write("\t\"")
need_close = True
cfile.write("\\x{:02x}".format(data[i]))
if i & 0xf == 0xf:
cfile.write("\"\n")
need_close = False
if need_close:
cfile.write("\"\n")
cfile.write("};\n")
vector_table = list()
def write_c(cfile, name, x509, data, pkcs7, result):
write_c_hexarray(cfile, name + "_key", x509)
write_c_hexarray(cfile, name + "_data", data)
write_c_hexarray(cfile, name + "_sig", pkcs7)
cfile.write("\n")
vector_table.append("\t{\n");
vector_table.append("\t\t.name\t\t= \"" + name + "\",\n");
vector_table.append("\t\t.key\t\t= " + name + "_key,\n");
vector_table.append("\t\t.data\t\t= " + name + "_data,\n");
vector_table.append("\t\t.sig\t\t= " + name + "_sig,\n");
vector_table.append("\t\t.key_len\t= sizeof(" + name + "_key) - 1,\n");
vector_table.append("\t\t.data_len\t= sizeof(" + name + "_data) - 1,\n");
vector_table.append("\t\t.sig_len\t= sizeof(" + name + "_sig) - 1,\n");
vector_table.append("\t\t.pass\t\t= " + result + ",\n");
vector_table.append("\t},\n");
def write_c_table(cfile, basename):
cfile.write("const struct nist_test_vector " + basename + "[] __initconst = {\n")
for i in vector_table:
cfile.write(i)
cfile.write("};\n")
###############################################################################
#
# Create an X.509 certificate to hold an RSA public key and create a detached
# PKCS#7 message to carry a signature created with it.
#
###############################################################################
def create_rsa_key(n, e):
"""Create an RSA public key"""
pubkey = RSA.encode("RsaPubKey", {
'n' : rsa_n,
'e' : rsa_e
})
def create_rsassa_params(salt_len, digest_alg):
"""Create the parameters for RSASSA-PSS"""
mgf1_params = RSA.encode("AlgorithmIdentifier", {
'algorithm' : digest_alg,
'parameters' : None
})
sig_params = RSA.encode("RSASSA-PSS-params", {
'hashAlgorithm' : {
'algorithm' : digest_alg,
'parameters' : None
},
'maskGenAlgorithm' : {
'algorithm' : OID_id_mgf1,
'parameters' : mgf1_params
},
'saltLength' : salt_len,
})
def create_cert_and_sig(basename, count, pubkey, signature, sig_params, content,
digest_alg, sig_alg, result, cfile):
serial = 0x1234000 + count
# Create an X.509 certificate
x509 = X509.encode("Certificate", {
'tbsCertificate' : {
'version' : 2,
'serialNumber' : serial,
'signature' : {
'algorithm' : sig_alg,
'parameters' : sig_params
},
'issuer' : [
[
{
'attributeType' : OID_commonName,
'attributeValue' : "Fred"
}
]
],
'validity' : {
'notBefore' : ('utcTime', datetime.datetime(2026, 1, 1)),
'notAfter' : ('generalTime', datetime.datetime(2199, 1, 1)),
},
'subject' : [
[
{
'attributeType' : OID_commonName,
'attributeValue' : basename
}
]
],
'subjectPublicKeyInfo' : {
'algorithm' : {
'algorithm' : sig_alg,
'parameters' : None
},
'subjectPublicKey' : (pubkey, len(pubkey)*8)
},
'extensions' : [
{
'extnid' : OID_basicConstraints,
'critical' : True,
'extnValue' : b'\x30\x00'
}, {
'extnid' : OID_keyUsage,
'critical' : False,
'extnValue' : b'\x03\x02\x07\x80'
}, {
'extnid' : OID_subjectKeyIdentifier,
'critical' : False,
'extnValue' : bytes.fromhex("04142B73932CF06C341AA72CCEA4E0AC35A96CCC{:04x}".format(count)),
},
]
},
'signatureAlgorithm' : {
'algorithm' : sig_alg,
'parameters' : sig_params
},
'signature' : (signature, len(signature)*8)
})
# Create a detached PKCS#7 message to use as a signature carrier
pkcs7 = PKCS7.encode("PKCS7ContentInfo", {
'contentType' : OID_signed_data,
'content' : {
'version' : 1,
'digestAlgorithms' : ( 'daSet', [
{
'algorithm' : digest_alg,
'parameters' : None
}
]),
'contentInfo' : {
'contentType' : OID_data
},
'signerInfos' : ('siSet', [
{
'version' : 1,
'sid' : (
'issuerAndSerialNumber', {
'issuer' : [
[
{
'attributeType' : OID_commonName,
'attributeValue' : "Fred"
}
]
],
'serialNumber' : serial
}
),
'digestAlgorithm' : {
'algorithm' : digest_alg,
},
'digestEncryptionAlgorithm' : {
'algorithm' : sig_alg,
'parameters' : sig_params
},
'encryptedDigest' : signature,
}
])
}
})
out = open(basename + ".x509", "wb")
out.write(x509)
out.close()
out = open(basename + ".p7s", "wb")
out.write(pkcs7)
out.close()
out = open(basename + ".data", "wb")
out.write(content)
out.close()
write_c(cfile, basename, x509, content, pkcs7, result)
###############################################################################
#
# Parse FIPS JSON vector file
#
###############################################################################
vecfilename = sys.argv[1]
vecf = open(vecfilename, "r")
testdata = json.load(vecf);
vecf.close()
expfilename = sys.argv[2]
expf = open(expfilename, "r")
expected = json.load(expf);
expf.close()
cfile = open("nist_testdata.c", "w")
def badfile(msg):
print(vecfilename + ": " + msg, file=sys.stderr);
exit(3)
def skipg(tgid, msg):
print("skipping tgId=" + str(tgid) + ": " + msg, file=sys.stderr)
def skipc(tcid, msg):
print("skipping tcId=" + str(tcid) + ": " + msg, file=sys.stderr)
if testdata["mode"] != "sigVer":
badfile("Only sigVer files supported")
if testdata["algorithm"] != "ML-DSA":
badfile("Unsupported algo " + testdata["algorithm"])
if expected["algorithm"] != testdata["algorithm"] or \
expected["revision"] != testdata["revision"] or \
expected["vsId"] != testdata["vsId"] or \
expected["mode"] != testdata["mode"]:
badfile("Doesn't match expected data file")
rname = "nist-"
rname += testdata["algorithm"].replace("-", "").lower() + "-"
rname += testdata["revision"].lower()
rname = rname.replace("-", "_")
count = 0
results = dict()
for tgroup in expected["testGroups"]:
for test in tgroup["tests"]:
tcid = test["tcId"]
results[tcid] = test["testPassed"]
for tgroup in testdata["testGroups"]:
tgid = tgroup["tgId"]
if tgroup["testType"] != "AFT":
skipg(tgid, "Not an Algorithm Functional Test")
continue
if tgroup["signatureInterface"] == "external":
if tgroup["preHash"] == "preHash":
skipg(tgid, "Pre-hashing required")
continue
if tgroup["preHash"] != "pure":
skipg(tgid, "Not pure")
continue
else:
if tgroup["externalMu"]:
skipg(tgid, "External-mu required")
continue
#json.dump(tgroup, sys.stdout)
#skipg(tgid, "Internal")
#continue
if tgroup["parameterSet"] == "ML-DSA-44":
sig_algo = OID_id_ml_dsa_44
hash_algo = None
elif tgroup["parameterSet"] == "ML-DSA-65":
sig_algo = OID_id_ml_dsa_65
hash_algo = None
elif tgroup["parameterSet"] == "ML-DSA-87":
sig_algo = OID_id_ml_dsa_87
hash_algo = None
else:
badfile("Unsupported algo " + testdata["algorithm"])
for test in tgroup["tests"]:
tcid = test["tcId"]
pubkey = bytes.fromhex(test["pk"])
message = bytes.fromhex(test["message"])
signature = bytes.fromhex(test["signature"])
sig_params = None
try:
if test["hashAlg"] == "SHA1":
digest_algo = OID_sha1
elif test["hashAlg"] == "SHA224":
digest_algo = OID_sha224
elif test["hashAlg"] == "SHA256":
digest_algo = OID_sha256
elif test["hashAlg"] == "SHA384":
digest_algo = OID_sha384
elif test["hashAlg"] == "SHA512":
digest_algo = OID_sha512
else:
skipc("Unknown algo:", test["hashAlg"])
continue
except KeyError:
if testdata["algorithm"] != "ML-DSA":
skipc("No hash algo")
continue
digest_algo = OID_sha512
result = results[tcid]
if result:
result_val = "true"
else:
result_val = "false"
name = rname + "_" + str(tcid);
create_cert_and_sig(name, tcid,
pubkey, signature, sig_params, message,
digest_algo,
sig_algo,
result_val,
cfile)
count += 1
os.environ["LD_LIBRARY_PATH"] = "/data/openssl/build/"
status = subprocess.run([ "/data/openssl/build/apps/openssl",
"smime", "-verify", "-binary",
"-inform", "DER", "-in", name + ".p7s",
"-content", name + ".data",
"-certfile", name + ".x509",
"-nointern",
"-noverify",
"-out", "/dev/null"
],
capture_output = True)
if status.returncode != 0:
print("tcId=" + str(tcid) +
": Unexpected failure", name, status.returncode)
#sys.stderr.buffer.write(status.stderr)
#exit(4)
else:
print("tcId=" + str(tcid) + ": Success", name)
write_c_table(cfile, rname)
print("Wrote", count, "test vectors");
Powered by blists - more mailing lists