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

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

122 statements  

1"""Signer implementation for project sigstore.""" 

2 

3from __future__ import annotations 

4 

5import json 

6import logging 

7from typing import Any 

8from urllib import parse 

9 

10from securesystemslib.exceptions import ( 

11 UnsupportedLibraryError, 

12 UnverifiedSignatureError, 

13 VerificationError, 

14) 

15from securesystemslib.signer._signer import ( 

16 Key, 

17 SecretsHandler, 

18 Signature, 

19 Signer, 

20) 

21from securesystemslib.signer._utils import compute_default_keyid 

22 

23IMPORT_ERROR = "sigstore library required to use 'sigstore-oidc' keys" 

24 

25# ruff: noqa: PLC0415 

26 

27logger = logging.getLogger(__name__) 

28 

29 

30class SigstoreKey(Key): 

31 """Sigstore verifier. 

32 

33 NOTE: The Sigstore key and signature serialization formats are not yet 

34 considered stable in securesystemslib. They may change in future releases 

35 and may not be supported by other implementations. 

36 """ 

37 

38 DEFAULT_KEY_TYPE = "sigstore-oidc" 

39 DEFAULT_SCHEME = "Fulcio" 

40 

41 def __init__( 

42 self, 

43 keyid: str, 

44 keytype: str, 

45 scheme: str, 

46 keyval: dict[str, Any], 

47 unrecognized_fields: dict[str, Any] | None = None, 

48 ): 

49 for content in ["identity", "issuer"]: 

50 if content not in keyval or not isinstance(keyval[content], str): 

51 raise ValueError(f"{content} string required for scheme {scheme}") 

52 super().__init__(keyid, keytype, scheme, keyval, unrecognized_fields) 

53 

54 @classmethod 

55 def from_dict(cls, keyid: str, key_dict: dict[str, Any]) -> SigstoreKey: 

56 keytype, scheme, keyval = cls._from_dict(key_dict) 

57 return cls(keyid, keytype, scheme, keyval, key_dict) 

58 

59 def to_dict(self) -> dict: 

60 return self._to_dict() 

61 

62 def verify_signature(self, signature: Signature, data: bytes) -> None: 

63 try: 

64 from sigstore.errors import VerificationError as SigstoreVerifyError 

65 from sigstore.models import Bundle 

66 from sigstore.verify import Verifier 

67 from sigstore.verify.policy import Identity 

68 except ImportError as e: 

69 raise VerificationError(IMPORT_ERROR) from e 

70 

71 try: 

72 verifier = Verifier.production() 

73 identity = Identity( 

74 identity=self.keyval["identity"], issuer=self.keyval["issuer"] 

75 ) 

76 bundle_data = signature.unrecognized_fields["bundle"] 

77 bundle = Bundle.from_json(json.dumps(bundle_data)) 

78 

79 verifier.verify_artifact(data, bundle, identity) 

80 

81 except SigstoreVerifyError as e: 

82 logger.info( 

83 "Key %s failed to verify sig: %s", 

84 self.keyid, 

85 e, 

86 ) 

87 raise UnverifiedSignatureError( 

88 f"Failed to verify signature by {self.keyid}" 

89 ) from e 

90 except Exception as e: 

91 logger.info("Key %s failed to verify sig: %s", self.keyid, str(e)) 

92 raise VerificationError( 

93 f"Unknown failure to verify signature by {self.keyid}" 

94 ) from e 

95 

96 

97class SigstoreSigner(Signer): 

98 """Sigstore signer. 

99 

100 NOTE: The Sigstore key and signature serialization formats are not yet 

101 considered stable in securesystemslib. They may change in future releases 

102 and may not be supported by other implementations. 

103 

104 All signers should be instantiated with ``Signer.from_priv_key_uri()``. 

105 Unstable ``SigstoreSigner`` currently requires opt-in via 

106 ``securesystemslib.signer.SIGNER_FOR_URI_SCHEME``. 

107 

108 Usage:: 

109 

110 identity = "luk.puehringer@gmail.com" # change, unless you know pw 

111 issuer = "https://github.com/login/oauth" 

112 

113 # Create signer URI and public key for identity and issuer 

114 uri, public_key = SigstoreSigner.import_(identity, issuer, ambient=False) 

115 

116 # Load signer from URI -- requires browser login with GitHub 

117 signer = SigstoreSigner.from_priv_key_uri(uri, public_key) 

118 

119 # Sign with signer and verify public key 

120 signature = signer.sign(b"data") 

121 public_key.verify_signature(signature, b"data") 

122 

123 The private key URI scheme is "sigstore:?<PARAMS>", where PARAMS is 

124 optional and toggles ambient credential usage. Example URIs: 

125 

126 * "sigstore:": 

127 Sign with ambient credentials. 

128 * "sigstore:?ambient=false": 

129 Sign with OAuth2 + OpenID via browser login. 

130 

131 Arguments: 

132 token: The OIDC identity token used for signing. 

133 public_key: The related public key instance. 

134 

135 Raises: 

136 UnsupportedLibraryError: sigstore library not found. 

137 """ 

138 

139 SCHEME = "sigstore" 

140 

141 def __init__(self, token: Any, public_key: Key): 

142 self._public_key = public_key 

143 # token is of type sigstore.oidc.IdentityToken but the module should be usable 

144 # without sigstore so it's not annotated 

145 self._token = token 

146 

147 @property 

148 def public_key(self) -> Key: 

149 return self._public_key 

150 

151 @classmethod 

152 def from_priv_key_uri( 

153 cls, 

154 priv_key_uri: str, 

155 public_key: Key, 

156 secrets_handler: SecretsHandler | None = None, 

157 ) -> SigstoreSigner: 

158 try: 

159 from sigstore.models import ClientTrustConfig 

160 from sigstore.oidc import IdentityToken, Issuer, detect_credential 

161 except ImportError as e: 

162 raise UnsupportedLibraryError(IMPORT_ERROR) from e 

163 

164 if not isinstance(public_key, SigstoreKey): 

165 raise ValueError(f"expected SigstoreKey for {priv_key_uri}") 

166 

167 uri = parse.urlparse(priv_key_uri) 

168 

169 if uri.scheme != cls.SCHEME: 

170 raise ValueError(f"SigstoreSigner does not support {priv_key_uri}") 

171 

172 params = dict(parse.parse_qsl(uri.query)) 

173 ambient = params.get("ambient", "true") == "true" 

174 

175 if not ambient: 

176 # TODO: Restrict oauth flow to use identity/issuer from public_key 

177 # TODO: Use secrets_handler for identity_token() secret arg 

178 trust_config = ClientTrustConfig.production() 

179 issuer = Issuer(trust_config.signing_config.get_oidc_url()) 

180 token = issuer.identity_token() 

181 else: 

182 credential = detect_credential() 

183 if not credential: 

184 raise RuntimeError("Failed to detect Sigstore credentials") 

185 token = IdentityToken(credential) 

186 

187 key_identity = public_key.keyval["identity"] 

188 key_issuer = public_key.keyval["issuer"] 

189 if key_issuer != token.federated_issuer: 

190 raise ValueError( 

191 f"Signer identity issuer {token.federated_issuer} " 

192 f"did not match key: {key_issuer}" 

193 ) 

194 # TODO: should check ambient identity too: unfortunately IdentityToken does 

195 # not provide access to the expected identity value (cert SAN) in ambient case 

196 if not ambient and key_identity != token.identity: 

197 raise ValueError( 

198 f"Signer identity {token.identity} did not match key: {key_identity}" 

199 ) 

200 

201 return cls(token, public_key) 

202 

203 @classmethod 

204 def _get_uri(cls, ambient: bool) -> str: 

205 return f"{cls.SCHEME}:{'' if ambient else '?ambient=false'}" 

206 

207 @classmethod 

208 def import_( 

209 cls, identity: str, issuer: str, ambient: bool = True 

210 ) -> tuple[str, SigstoreKey]: 

211 """Create public key and signer URI. 

212 

213 Returns a private key URI (for Signer.from_priv_key_uri()) and a public 

214 key. import_() should be called once and the returned URI and public 

215 key should be stored for later use. 

216 

217 Arguments: 

218 identity: The OIDC identity to use when verifying a signature. 

219 issuer: The OIDC issuer to use when verifying a signature. 

220 ambient: Toggle usage of ambient credentials in returned URI. 

221 """ 

222 keytype = SigstoreKey.DEFAULT_KEY_TYPE 

223 scheme = SigstoreKey.DEFAULT_SCHEME 

224 keyval = {"identity": identity, "issuer": issuer} 

225 keyid = compute_default_keyid(keytype, scheme, keyval) 

226 key = SigstoreKey(keyid, keytype, scheme, keyval) 

227 uri = cls._get_uri(ambient) 

228 

229 return uri, key 

230 

231 @classmethod 

232 def import_via_auth(cls) -> tuple[str, SigstoreKey]: 

233 """Create public key and signer URI by interactive authentication 

234 

235 Returns a private key URI (for Signer.from_priv_key_uri()) and a public 

236 key. This method always uses the interactive authentication. 

237 """ 

238 try: 

239 from sigstore.models import ClientTrustConfig 

240 from sigstore.oidc import Issuer 

241 except ImportError as e: 

242 raise UnsupportedLibraryError(IMPORT_ERROR) from e 

243 

244 # authenticate to get the identity and issuer 

245 trust_config = ClientTrustConfig.production() 

246 issuer = Issuer(trust_config.signing_config.get_oidc_url()) 

247 token = issuer.identity_token() 

248 return cls.import_(token.identity, token.federated_issuer, False) 

249 

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

251 """Signs payload using the OIDC token on the signer instance. 

252 

253 Arguments: 

254 payload: bytes to be signed. 

255 

256 Raises: 

257 Various errors from sigstore-python. 

258 

259 Returns: 

260 Signature. 

261 

262 NOTE: The relevant data is in `unrecognized_fields["bundle"]`. 

263 

264 """ 

265 try: 

266 from sigstore.models import ClientTrustConfig 

267 from sigstore.sign import SigningContext 

268 except ImportError as e: 

269 raise UnsupportedLibraryError(IMPORT_ERROR) from e 

270 

271 context = SigningContext.from_trust_config(ClientTrustConfig.production()) 

272 with context.signer(self._token) as sigstore_signer: 

273 bundle = sigstore_signer.sign_artifact(payload) 

274 # We want to access the actual signature, see 

275 # https://github.com/sigstore/protobuf-specs/blob/main/protos/sigstore_bundle.proto 

276 bundle_json = json.loads(bundle.to_json()) 

277 return Signature( 

278 self.public_key.keyid, 

279 bundle_json["messageSignature"]["signature"], 

280 {"bundle": bundle_json}, 

281 ) 

282 

283 @classmethod 

284 def import_github_actions( 

285 cls, project: str, workflow_path: str, ref: str | None = "refs/heads/main" 

286 ) -> tuple[str, SigstoreKey]: 

287 """Convenience method to build identity and issuer string for import_() from 

288 GitHub project and workflow path. 

289 

290 Args: 

291 project: GitHub project name (example: 

292 "secure-systems-lab/securesystemslib") 

293 workflow_path: GitHub workflow path (example: 

294 ".github/workflows/online-sign.yml") 

295 ref: optional GitHub ref, defaults to refs/heads/main 

296 

297 Returns: 

298 uri: string 

299 key: SigstoreKey 

300 

301 """ 

302 identity = f"https://github.com/{project}/{workflow_path}@{ref}" 

303 issuer = "https://token.actions.githubusercontent.com" 

304 uri, key = cls.import_(identity, issuer) 

305 

306 return uri, key