Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions mypy.ini
Original file line number Diff line number Diff line change
Expand Up @@ -9,3 +9,6 @@ ignore_missing_imports = True

[mypy-OpenSSL.*]
ignore_missing_imports = True

[mypy-dilithium_py.*]
ignore_missing_imports = True
2 changes: 2 additions & 0 deletions requirements.txt
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,11 @@ cbor2==5.6.5
cffi==1.17.1
click==8.1.7
cryptography==44.0.2
dilithium-py==1.3.0
mccabe==0.7.0
mypy==1.11.2
mypy-extensions==1.0.0
packaging==25.0
pathspec==0.12.1
platformdirs==4.3.6
pycodestyle==2.12.1
Expand Down
9 changes: 9 additions & 0 deletions webauthn/helpers/cose.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,9 @@ class COSEAlgorithmIdentifier(int, Enum):
`RSASSA_PKCS1_v1_5_SHA_384`
`RSASSA_PKCS1_v1_5_SHA_512`
`RSASSA_PKCS1_v1_5_SHA_1`
`ML_DSA_44`
`ML_DSA_65`
`ML_DSA_87`

https://www.w3.org/TR/webauthn-2/#sctn-alg-identifier
https://www.iana.org/assignments/cose/cose.xhtml#algorithms
Expand All @@ -30,6 +33,9 @@ class COSEAlgorithmIdentifier(int, Enum):
RSASSA_PKCS1_v1_5_SHA_384 = -258
RSASSA_PKCS1_v1_5_SHA_512 = -259
RSASSA_PKCS1_v1_5_SHA_1 = -65535 # Deprecated; here for legacy support
ML_DSA_44 = -48
ML_DSA_65 = -49
ML_DSA_87 = -50


class COSEKTY(int, Enum):
Expand All @@ -43,6 +49,7 @@ class COSEKTY(int, Enum):
OKP = 1
EC2 = 2
RSA = 3
ML_DSA = 7


class COSECRV(int, Enum):
Expand Down Expand Up @@ -77,3 +84,5 @@ class COSEKey(int, Enum):
# RSA
N = -1
E = -2
# ML-DSA
PUB = -1
22 changes: 19 additions & 3 deletions webauthn/helpers/decode_credential_public_key.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,6 @@
from typing import Union
from dataclasses import dataclass

import cbor2

from .cose import COSECRV, COSEKTY, COSEAlgorithmIdentifier, COSEKey
from .exceptions import InvalidPublicKeyStructure, UnsupportedPublicKeyType
from .parse_cbor import parse_cbor
Expand Down Expand Up @@ -33,9 +31,16 @@ class DecodedRSAPublicKey:
e: bytes


@dataclass
class DecodedMLDSAPublicKey:
kty: COSEKTY
alg: COSEAlgorithmIdentifier
pub: bytes


def decode_credential_public_key(
key: bytes,
) -> Union[DecodedOKPPublicKey, DecodedEC2PublicKey, DecodedRSAPublicKey]:
) -> Union[DecodedOKPPublicKey, DecodedEC2PublicKey, DecodedRSAPublicKey, DecodedMLDSAPublicKey]:
"""
Decode a CBOR-encoded public key and turn it into a data structure.

Expand Down Expand Up @@ -116,5 +121,16 @@ def decode_credential_public_key(
n=n,
e=e,
)
elif kty == COSEKTY.ML_DSA:
pub = decoded_key[COSEKey.PUB]

if not pub:
raise InvalidPublicKeyStructure("ML-DSA credential public key missing pub")

return DecodedMLDSAPublicKey(
kty=kty,
alg=alg,
pub=pub,
)

raise UnsupportedPublicKeyType(f'Unsupported credential public key type "{kty}"')
13 changes: 11 additions & 2 deletions webauthn/helpers/decoded_public_key_to_cryptography.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,13 +14,20 @@
DecodedEC2PublicKey,
DecodedOKPPublicKey,
DecodedRSAPublicKey,
DecodedMLDSAPublicKey,
)
from .exceptions import UnsupportedPublicKey
from .ml_dsa import MLDSAPublicKey


def decoded_public_key_to_cryptography(
public_key: Union[DecodedOKPPublicKey, DecodedEC2PublicKey, DecodedRSAPublicKey]
) -> Union[Ed25519PublicKey, EllipticCurvePublicKey, RSAPublicKey]:
public_key: Union[
DecodedOKPPublicKey,
DecodedEC2PublicKey,
DecodedRSAPublicKey,
DecodedMLDSAPublicKey,
],
) -> Union[Ed25519PublicKey, EllipticCurvePublicKey, RSAPublicKey, MLDSAPublicKey]:
"""Convert raw decoded public key parameters (crv, x, y, n, e, etc...) into
public keys using primitives from the cryptography.io library
"""
Expand Down Expand Up @@ -61,5 +68,7 @@ def decoded_public_key_to_cryptography(
okp_pub_key = Ed25519PublicKey.from_public_bytes(public_key.x)

return okp_pub_key
elif isinstance(public_key, DecodedMLDSAPublicKey):
return MLDSAPublicKey(public_key)
else:
raise UnsupportedPublicKey(f"Unrecognized decoded public key: {public_key}")
4 changes: 4 additions & 0 deletions webauthn/helpers/exceptions.py
Original file line number Diff line number Diff line change
Expand Up @@ -68,3 +68,7 @@ class InvalidBackupFlags(WebAuthnException):

class InvalidCBORData(WebAuthnException):
pass


class MLDSANotSupported(WebAuthnException):
pass
67 changes: 67 additions & 0 deletions webauthn/helpers/ml_dsa.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
from cryptography.exceptions import InvalidSignature
from cryptography.hazmat.primitives.serialization import Encoding, PublicFormat

from .cose import COSEAlgorithmIdentifier
from .exceptions import MLDSANotSupported
from .decode_credential_public_key import DecodedMLDSAPublicKey


class MLDSAPublicKey(DecodedMLDSAPublicKey):
"""
Something vaguely shaped like other PublicKey classes in cryptography. Going with something
like this till the cryptography library itself supports PQC directly.
"""

def __init__(self, decoded_public_key: DecodedMLDSAPublicKey) -> None:
assert_ml_dsa_dependencies()

super().__init__(
kty=decoded_public_key.kty,
alg=decoded_public_key.alg,
pub=decoded_public_key.pub,
)

def verify(self, signature: bytes, data: bytes) -> None:
"""
Verify the ML-DSA signature. Raises `cryptography.exceptions.InvalidSignature` to blend in.
"""
from dilithium_py.ml_dsa import ML_DSA_44, ML_DSA_65, ML_DSA_87

if self.alg == COSEAlgorithmIdentifier.ML_DSA_44:
verified = ML_DSA_44.verify(self.pub, data, signature)
elif self.alg == COSEAlgorithmIdentifier.ML_DSA_65:
verified = ML_DSA_65.verify(self.pub, data, signature)
elif self.alg == COSEAlgorithmIdentifier.ML_DSA_87:
verified = ML_DSA_87.verify(self.pub, data, signature)

if not verified:
raise InvalidSignature()

def public_bytes(self, encoding: Encoding, format: PublicFormat) -> bytes:
"""
From https://datatracker.ietf.org/doc/draft-ietf-cose-dilithium/09/:

"The "pub" parameter is the ML-DSA public key, as described in
Section 5.3 of FIPS-204."

This method simply returns the bytes, with no support for other encodings or formats.
Nothing that A) provides attestation, and B) uses PQC for public keys will use this
method right now.
"""
return self.pub


def assert_ml_dsa_dependencies() -> None:
"""
Check that necessary dependencies are present for handling responses containing ML-DSA public
keys.

Raises:
`webauthn.helpers.exceptions.MLDSANotSupported` if those dependencies are missing
"""
try:
import dilithium_py
except Exception:
raise MLDSANotSupported(
"Please install https://pypi.org/project/dilithium-py to verify ML-DSA responses with py_webauthn"
)
4 changes: 4 additions & 0 deletions webauthn/helpers/verify_signature.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
)
from .cose import COSEAlgorithmIdentifier
from .exceptions import UnsupportedAlgorithm, UnsupportedPublicKey
from .ml_dsa import MLDSAPublicKey


def verify_signature(
Expand All @@ -30,6 +31,7 @@ def verify_signature(
Ed448PublicKey,
X25519PublicKey,
X448PublicKey,
MLDSAPublicKey,
],
signature_alg: COSEAlgorithmIdentifier,
signature: bytes,
Expand Down Expand Up @@ -66,6 +68,8 @@ def verify_signature(
raise UnsupportedAlgorithm(f"Unrecognized RSA signature alg {signature_alg}")
elif isinstance(public_key, Ed25519PublicKey):
public_key.verify(signature, data)
elif isinstance(public_key, MLDSAPublicKey):
public_key.verify(signature, data)
else:
raise UnsupportedPublicKey(
f"Unsupported public key for signature verification: {public_key}"
Expand Down
2 changes: 1 addition & 1 deletion webauthn/registration/generate_registration_options.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
from typing import List, Optional

from webauthn.helpers import generate_challenge, generate_user_handle, byteslike_to_bytes
from webauthn.helpers import generate_challenge, generate_user_handle
from webauthn.helpers.cose import COSEAlgorithmIdentifier
from webauthn.helpers.structs import (
AttestationConveyancePreference,
Expand Down