Coverage for /pythoncovmergedfiles/medio/medio/usr/local/lib/python3.11/site-packages/securesystemslib/signer/_tkey_signer.py: 37%

Shortcuts on this page

r m x   toggle line displays

j k   next/prev highlighted chunk

0   (zero) top of page

1   (one) first highlighted chunk

76 statements  

1"""ML-DSA-44 Signer for Tillitis TKey""" 

2 

3from __future__ import annotations 

4 

5import logging 

6from urllib import parse 

7 

8from securesystemslib.exceptions import KeyMismatchError, UnsupportedLibraryError 

9from securesystemslib.signer._constants import MLDSA_44_1 

10from securesystemslib.signer._key import Key, SSlibKey 

11from securesystemslib.signer._signature import Signature 

12from securesystemslib.signer._signer import SecretsHandler, Signer 

13from securesystemslib.signer._utils import get_mldsa_payload 

14 

15TKEY_IMPORT_ERROR = None 

16try: 

17 from cryptography.hazmat.primitives import serialization 

18 from cryptography.hazmat.primitives.asymmetric.mldsa import MLDSA44PublicKey 

19 from keylet import SignApp, TKeySign 

20except ImportError as e: 

21 TKEY_IMPORT_ERROR = f"TKeySigner: {e}" 

22 

23 

24logger = logging.getLogger(__name__) 

25 

26 

27class TKeySigner(Signer): 

28 """Post-Quantum signer for the Tillitis TKey security token. 

29 

30 Supports signing scheme MLDSA_44_1. 

31 

32 The private key URI is: ``tkey:[device_path]?digest=<hex_prefix>&[passphrase=true]`` 

33 

34 * ``digest`` is required in the URI: The device binary (identified by its 

35 digest hash prefix) is part of the private key seed. A key can only 

36 be used with the same exact binary. 

37 * ``device_path`` is optional and not typically needed as device detection is 

38 automatic. 

39 * If ``passphrase=true`` is present in the URI, a ``secrets_handler`` must be 

40 provided to ``Signer.from_priv_key_uri()`` to supply the passphrase secret. 

41 

42 Examples: 

43 * ``tkey:?digest=7c75714`` 

44 * ``tkey:?digest=7c75714&passphrase=true`` 

45 * ``tkey:/dev/ttyACM0?digest=7c75714&passphrase=true`` 

46 """ 

47 

48 SCHEME = "tkey" 

49 

50 def __init__( 

51 self, 

52 device_path: str | None, 

53 public_key: SSlibKey, 

54 passphrase: str | None, 

55 digest: str | None, 

56 ) -> None: 

57 if TKEY_IMPORT_ERROR: 

58 raise UnsupportedLibraryError(TKEY_IMPORT_ERROR) 

59 

60 self._public_key = public_key 

61 

62 app = SignApp.load_mldsa(digest=digest) 

63 self._tkey = TKeySign(app, device_path, passphrase) 

64 

65 # key derivation depends on passphrase: compare keys to make sure 

66 raw_pubkey = self._tkey.get_pubkey() 

67 if public_key.scheme == MLDSA_44_1: 

68 key = SSlibKey.from_crypto(MLDSA44PublicKey.from_public_bytes(raw_pubkey)) 

69 else: 

70 raise ValueError(f"unsupported scheme {public_key.scheme}") 

71 

72 if key.keyval != self.public_key.keyval: 

73 raise KeyMismatchError( 

74 "TKey public key does not match: This could mean incorrect Passphrase." 

75 ) 

76 

77 @property 

78 def public_key(self) -> SSlibKey: 

79 return self._public_key 

80 

81 @classmethod 

82 def from_priv_key_uri( 

83 cls, 

84 priv_key_uri: str, 

85 public_key: Key, 

86 secrets_handler: SecretsHandler | None = None, 

87 ) -> TKeySigner: 

88 if not isinstance(public_key, SSlibKey): 

89 raise ValueError(f"expected SSlibKey for {priv_key_uri}") 

90 

91 uri = parse.urlparse(priv_key_uri) 

92 if uri.scheme != cls.SCHEME: 

93 raise ValueError(f"TKeySigner does not support {priv_key_uri}") 

94 

95 # Extract device path (empty or "/" triggers auto-detect) 

96 device_path = uri.path if uri.path not in ("", "/") else None 

97 

98 # Extract query parameters 

99 query_params = parse.parse_qs(uri.query) 

100 

101 digest = None 

102 if "digest" in query_params: 

103 digest = query_params["digest"][0] 

104 

105 if not digest: 

106 raise ValueError("TKey URI must include 'digest'") 

107 

108 pass_str = query_params.get("passphrase", ["false"])[0] 

109 if pass_str.lower() != "true": 

110 passphrase = None 

111 elif secrets_handler is not None: 

112 passphrase = secrets_handler("passphrase") 

113 else: 

114 raise ValueError( 

115 "TKey URI has 'passphrase' but no secrets_handler was given" 

116 ) 

117 

118 return cls( 

119 device_path, 

120 public_key=public_key, 

121 passphrase=passphrase, 

122 digest=digest, 

123 ) 

124 

125 @classmethod 

126 def import_( 

127 cls, 

128 digest: str | None = None, 

129 device_path: str | None = None, 

130 passphrase: str | None = None, 

131 ) -> tuple[str, SSlibKey]: 

132 """Import public key and signer details from a TKey device. 

133 

134 Args: 

135 digest: Optional digest or digest prefix of device binary. If not given, 

136 the current default device binary is used. 

137 device_path: Optional COM port path. Typically not useful as the port may 

138 be dynamic 

139 passphrase: Optional "User Supplied Secret". Will be used as part of the 

140 seed for the private key 

141 

142 Returns: 

143 Tuple of private key URI string and public key 

144 """ 

145 if TKEY_IMPORT_ERROR: 

146 raise UnsupportedLibraryError(TKEY_IMPORT_ERROR) 

147 

148 app = SignApp.load_mldsa(digest=digest) 

149 with TKeySign(app, device_path, passphrase) as tk: 

150 raw_pubkey = tk.get_pubkey() 

151 

152 # Build URI with digest prefix and optional passphrase boolean 

153 query = {"digest": app.digest[:7]} 

154 

155 if passphrase is not None: 

156 query["passphrase"] = "true" # noqa: S105 

157 

158 key = SSlibKey.from_crypto(MLDSA44PublicKey.from_public_bytes(raw_pubkey)) 

159 

160 # Only encode path if it was explicitly passed as argument 

161 path = device_path if device_path is not None else "" 

162 uri = f"{cls.SCHEME}:{path}?{parse.urlencode(query)}" 

163 

164 return uri, key 

165 

166 def sign(self, payload: bytes) -> Signature: 

167 """Signs payload with Tillitis TKey.""" 

168 

169 # Provide the pub key bytes for mu calculation 

170 pk_pem = self.public_key.keyval["public"].encode("utf-8") 

171 public_key = serialization.load_pem_public_key(pk_pem) 

172 key_bytes = public_key.public_bytes( 

173 encoding=serialization.Encoding.Raw, 

174 format=serialization.PublicFormat.Raw, 

175 ) 

176 

177 # Use TUF-specific message prefix and digest as payload 

178 sig_bytes = self._tkey.sign(get_mldsa_payload(payload, 1), key_bytes) 

179 return Signature(self.public_key.keyid, sig_bytes.hex())