Skip to content

Commit 6668f52

Browse files
authored
Merge pull request #260 from duo-labs/mm/pqc-dilithium-alpha
Add alpha support for ML-DSA using Dilithium-py
2 parents 0c8f53b + fb77fe6 commit 6668f52

File tree

9 files changed

+120
-6
lines changed

9 files changed

+120
-6
lines changed

mypy.ini

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,3 +9,6 @@ ignore_missing_imports = True
99

1010
[mypy-OpenSSL.*]
1111
ignore_missing_imports = True
12+
13+
[mypy-dilithium_py.*]
14+
ignore_missing_imports = True

requirements.txt

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,9 +4,11 @@ cbor2==5.6.5
44
cffi==1.17.1
55
click==8.1.7
66
cryptography==44.0.2
7+
dilithium-py==1.3.0
78
mccabe==0.7.0
89
mypy==1.11.2
910
mypy-extensions==1.0.0
11+
packaging==25.0
1012
pathspec==0.12.1
1113
platformdirs==4.3.6
1214
pycodestyle==2.12.1

webauthn/helpers/cose.py

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,9 @@ class COSEAlgorithmIdentifier(int, Enum):
1515
`RSASSA_PKCS1_v1_5_SHA_384`
1616
`RSASSA_PKCS1_v1_5_SHA_512`
1717
`RSASSA_PKCS1_v1_5_SHA_1`
18+
`ML_DSA_44`
19+
`ML_DSA_65`
20+
`ML_DSA_87`
1821
1922
https://www.w3.org/TR/webauthn-2/#sctn-alg-identifier
2023
https://www.iana.org/assignments/cose/cose.xhtml#algorithms
@@ -30,6 +33,9 @@ class COSEAlgorithmIdentifier(int, Enum):
3033
RSASSA_PKCS1_v1_5_SHA_384 = -258
3134
RSASSA_PKCS1_v1_5_SHA_512 = -259
3235
RSASSA_PKCS1_v1_5_SHA_1 = -65535 # Deprecated; here for legacy support
36+
ML_DSA_44 = -48
37+
ML_DSA_65 = -49
38+
ML_DSA_87 = -50
3339

3440

3541
class COSEKTY(int, Enum):
@@ -43,6 +49,7 @@ class COSEKTY(int, Enum):
4349
OKP = 1
4450
EC2 = 2
4551
RSA = 3
52+
ML_DSA = 7
4653

4754

4855
class COSECRV(int, Enum):
@@ -77,3 +84,5 @@ class COSEKey(int, Enum):
7784
# RSA
7885
N = -1
7986
E = -2
87+
# ML-DSA
88+
PUB = -1

webauthn/helpers/decode_credential_public_key.py

Lines changed: 19 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,6 @@
11
from typing import Union
22
from dataclasses import dataclass
33

4-
import cbor2
5-
64
from .cose import COSECRV, COSEKTY, COSEAlgorithmIdentifier, COSEKey
75
from .exceptions import InvalidPublicKeyStructure, UnsupportedPublicKeyType
86
from .parse_cbor import parse_cbor
@@ -33,9 +31,16 @@ class DecodedRSAPublicKey:
3331
e: bytes
3432

3533

34+
@dataclass
35+
class DecodedMLDSAPublicKey:
36+
kty: COSEKTY
37+
alg: COSEAlgorithmIdentifier
38+
pub: bytes
39+
40+
3641
def decode_credential_public_key(
3742
key: bytes,
38-
) -> Union[DecodedOKPPublicKey, DecodedEC2PublicKey, DecodedRSAPublicKey]:
43+
) -> Union[DecodedOKPPublicKey, DecodedEC2PublicKey, DecodedRSAPublicKey, DecodedMLDSAPublicKey]:
3944
"""
4045
Decode a CBOR-encoded public key and turn it into a data structure.
4146
@@ -116,5 +121,16 @@ def decode_credential_public_key(
116121
n=n,
117122
e=e,
118123
)
124+
elif kty == COSEKTY.ML_DSA:
125+
pub = decoded_key[COSEKey.PUB]
126+
127+
if not pub:
128+
raise InvalidPublicKeyStructure("ML-DSA credential public key missing pub")
129+
130+
return DecodedMLDSAPublicKey(
131+
kty=kty,
132+
alg=alg,
133+
pub=pub,
134+
)
119135

120136
raise UnsupportedPublicKeyType(f'Unsupported credential public key type "{kty}"')

webauthn/helpers/decoded_public_key_to_cryptography.py

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,13 +14,20 @@
1414
DecodedEC2PublicKey,
1515
DecodedOKPPublicKey,
1616
DecodedRSAPublicKey,
17+
DecodedMLDSAPublicKey,
1718
)
1819
from .exceptions import UnsupportedPublicKey
20+
from .ml_dsa import MLDSAPublicKey
1921

2022

2123
def decoded_public_key_to_cryptography(
22-
public_key: Union[DecodedOKPPublicKey, DecodedEC2PublicKey, DecodedRSAPublicKey]
23-
) -> Union[Ed25519PublicKey, EllipticCurvePublicKey, RSAPublicKey]:
24+
public_key: Union[
25+
DecodedOKPPublicKey,
26+
DecodedEC2PublicKey,
27+
DecodedRSAPublicKey,
28+
DecodedMLDSAPublicKey,
29+
],
30+
) -> Union[Ed25519PublicKey, EllipticCurvePublicKey, RSAPublicKey, MLDSAPublicKey]:
2431
"""Convert raw decoded public key parameters (crv, x, y, n, e, etc...) into
2532
public keys using primitives from the cryptography.io library
2633
"""
@@ -61,5 +68,7 @@ def decoded_public_key_to_cryptography(
6168
okp_pub_key = Ed25519PublicKey.from_public_bytes(public_key.x)
6269

6370
return okp_pub_key
71+
elif isinstance(public_key, DecodedMLDSAPublicKey):
72+
return MLDSAPublicKey(public_key)
6473
else:
6574
raise UnsupportedPublicKey(f"Unrecognized decoded public key: {public_key}")

webauthn/helpers/exceptions.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -68,3 +68,7 @@ class InvalidBackupFlags(WebAuthnException):
6868

6969
class InvalidCBORData(WebAuthnException):
7070
pass
71+
72+
73+
class MLDSANotSupported(WebAuthnException):
74+
pass

webauthn/helpers/ml_dsa.py

Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
from cryptography.exceptions import InvalidSignature
2+
from cryptography.hazmat.primitives.serialization import Encoding, PublicFormat
3+
4+
from .cose import COSEAlgorithmIdentifier
5+
from .exceptions import MLDSANotSupported
6+
from .decode_credential_public_key import DecodedMLDSAPublicKey
7+
8+
9+
class MLDSAPublicKey(DecodedMLDSAPublicKey):
10+
"""
11+
Something vaguely shaped like other PublicKey classes in cryptography. Going with something
12+
like this till the cryptography library itself supports PQC directly.
13+
"""
14+
15+
def __init__(self, decoded_public_key: DecodedMLDSAPublicKey) -> None:
16+
assert_ml_dsa_dependencies()
17+
18+
super().__init__(
19+
kty=decoded_public_key.kty,
20+
alg=decoded_public_key.alg,
21+
pub=decoded_public_key.pub,
22+
)
23+
24+
def verify(self, signature: bytes, data: bytes) -> None:
25+
"""
26+
Verify the ML-DSA signature. Raises `cryptography.exceptions.InvalidSignature` to blend in.
27+
"""
28+
from dilithium_py.ml_dsa import ML_DSA_44, ML_DSA_65, ML_DSA_87
29+
30+
if self.alg == COSEAlgorithmIdentifier.ML_DSA_44:
31+
verified = ML_DSA_44.verify(self.pub, data, signature)
32+
elif self.alg == COSEAlgorithmIdentifier.ML_DSA_65:
33+
verified = ML_DSA_65.verify(self.pub, data, signature)
34+
elif self.alg == COSEAlgorithmIdentifier.ML_DSA_87:
35+
verified = ML_DSA_87.verify(self.pub, data, signature)
36+
37+
if not verified:
38+
raise InvalidSignature()
39+
40+
def public_bytes(self, encoding: Encoding, format: PublicFormat) -> bytes:
41+
"""
42+
From https://datatracker.ietf.org/doc/draft-ietf-cose-dilithium/09/:
43+
44+
"The "pub" parameter is the ML-DSA public key, as described in
45+
Section 5.3 of FIPS-204."
46+
47+
This method simply returns the bytes, with no support for other encodings or formats.
48+
Nothing that A) provides attestation, and B) uses PQC for public keys will use this
49+
method right now.
50+
"""
51+
return self.pub
52+
53+
54+
def assert_ml_dsa_dependencies() -> None:
55+
"""
56+
Check that necessary dependencies are present for handling responses containing ML-DSA public
57+
keys.
58+
59+
Raises:
60+
`webauthn.helpers.exceptions.MLDSANotSupported` if those dependencies are missing
61+
"""
62+
try:
63+
import dilithium_py
64+
except Exception:
65+
raise MLDSANotSupported(
66+
"Please install https://pypi.org/project/dilithium-py to verify ML-DSA responses with py_webauthn"
67+
)

webauthn/helpers/verify_signature.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818
)
1919
from .cose import COSEAlgorithmIdentifier
2020
from .exceptions import UnsupportedAlgorithm, UnsupportedPublicKey
21+
from .ml_dsa import MLDSAPublicKey
2122

2223

2324
def verify_signature(
@@ -30,6 +31,7 @@ def verify_signature(
3031
Ed448PublicKey,
3132
X25519PublicKey,
3233
X448PublicKey,
34+
MLDSAPublicKey,
3335
],
3436
signature_alg: COSEAlgorithmIdentifier,
3537
signature: bytes,
@@ -66,6 +68,8 @@ def verify_signature(
6668
raise UnsupportedAlgorithm(f"Unrecognized RSA signature alg {signature_alg}")
6769
elif isinstance(public_key, Ed25519PublicKey):
6870
public_key.verify(signature, data)
71+
elif isinstance(public_key, MLDSAPublicKey):
72+
public_key.verify(signature, data)
6973
else:
7074
raise UnsupportedPublicKey(
7175
f"Unsupported public key for signature verification: {public_key}"

webauthn/registration/generate_registration_options.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
from typing import List, Optional
22

3-
from webauthn.helpers import generate_challenge, generate_user_handle, byteslike_to_bytes
3+
from webauthn.helpers import generate_challenge, generate_user_handle
44
from webauthn.helpers.cose import COSEAlgorithmIdentifier
55
from webauthn.helpers.structs import (
66
AttestationConveyancePreference,

0 commit comments

Comments
 (0)