Coverage for /pythoncovmergedfiles/medio/medio/usr/local/lib/python3.11/site-packages/model_signing/_signing/sign_sigstore.py: 41%

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# Copyright 2024 The Sigstore Authors 

2# 

3# Licensed under the Apache License, Version 2.0 (the "License"); 

4# you may not use this file except in compliance with the License. 

5# You may obtain a copy of the License at 

6# 

7# http://www.apache.org/licenses/LICENSE-2.0 

8# 

9# Unless required by applicable law or agreed to in writing, software 

10# distributed under the License is distributed on an "AS IS" BASIS, 

11# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 

12# See the License for the specific language governing permissions and 

13# limitations under the License. 

14 

15"""Sigstore based signature, signers and verifiers.""" 

16 

17import pathlib 

18from typing import Self, cast 

19 

20from google.protobuf import json_format 

21from sigstore import dsse as sigstore_dsse 

22from sigstore import models as sigstore_models 

23from sigstore import oidc as sigstore_oidc 

24from sigstore import sign as sigstore_signer 

25from sigstore import verify as sigstore_verifier 

26from typing_extensions import override 

27 

28from model_signing._signing import signing 

29 

30 

31_DEFAULT_CLIENT_ID = "sigstore" 

32_DEFAULT_CLIENT_SECRET = "" 

33 

34 

35class Signature(signing.Signature): 

36 """Sigstore signature support, wrapping around `sigstore_models.Bundle`.""" 

37 

38 def __init__(self, bundle: sigstore_models.Bundle): 

39 """Builds an instance of this signature. 

40 

41 Args: 

42 bundle: the sigstore bundle (in `bundle_pb.Bundle` format). 

43 """ 

44 self.bundle = bundle 

45 

46 @override 

47 def write(self, path: pathlib.Path) -> None: 

48 path.write_bytes(self.to_bytes()) 

49 

50 @override 

51 def to_bytes(self) -> bytes: 

52 return self.bundle.to_json().encode("utf-8") 

53 

54 @classmethod 

55 @override 

56 def read(cls, path: pathlib.Path) -> Self: 

57 content = path.read_text(encoding="utf-8") 

58 return cls(sigstore_models.Bundle.from_json(content)) 

59 

60 

61class Signer(signing.Signer): 

62 """Signing using Sigstore.""" 

63 

64 def __init__( 

65 self, 

66 *, 

67 oidc_issuer: str | None = None, 

68 use_ambient_credentials: bool = True, 

69 use_staging: bool = False, 

70 identity_token: str | None = None, 

71 force_oob: bool = False, 

72 client_id: str | None = None, 

73 client_secret: str | None = None, 

74 trust_config: pathlib.Path | None = None, 

75 ): 

76 """Initializes Sigstore signers. 

77 

78 Needs to set-up a signing context to use the public goods instance and 

79 machinery for getting an identity token to use in signing. 

80 

81 Args: 

82 oidc_issuer: An optional OpenID Connect issuer to use instead of the 

83 default production one. Only relevant if `use_staging = False`. 

84 Default is empty, relying on the Sigstore configuration. 

85 use_ambient_credentials: Use ambient credentials (also known as 

86 Workload Identity). Default is True. If ambient credentials cannot 

87 be used (not available, or option disabled), a flow to get signer 

88 identity via OIDC will start. 

89 use_staging: Use staging configurations, instead of production. This 

90 is supposed to be set to True only when testing. Default is False. 

91 force_oob: If True, forces an out-of-band (OOB) OAuth flow. If set, 

92 the OAuth authentication will not attempt to open the default web 

93 browser. Instead, it will display a URL and code for manual 

94 authentication. Default is False, which means the browser will be 

95 opened automatically if possible. 

96 identity_token: An explicit identity token to use when signing, 

97 taking precedence over any ambient credential or OAuth workflow. 

98 client_id: An optional client ID to use when performing OIDC-based 

99 authentication. This is typically used to identify the 

100 application making the request to the OIDC provider. If not 

101 provided, the default client ID configured by Sigstore will be 

102 used. 

103 client_secret: An optional client secret to use along with the 

104 client ID when authenticating with the OIDC provider. This is 

105 required for confidential clients that need to prove their 

106 identity to the OIDC provider. If not provided, it is assumed 

107 that the client is public or the provider does not require a 

108 secret. 

109 trust_config: A path to a custom trust configuration. When 

110 provided, the signature verification process will rely on the 

111 supplied PKI and trust configurations, instead of the default 

112 Sigstore setup. If not specified, the default Sigstore 

113 configuration is used. 

114 """ 

115 if use_staging: 

116 trust_config = sigstore_models.ClientTrustConfig.staging() 

117 elif trust_config: 

118 trust_config = sigstore_models.ClientTrustConfig.from_json( 

119 trust_config.read_text() 

120 ) 

121 else: 

122 trust_config = sigstore_models.ClientTrustConfig.production() 

123 

124 if not oidc_issuer: 

125 oidc_issuer = trust_config.signing_config.get_oidc_url() 

126 

127 self._oidc_issuer = oidc_issuer 

128 self._issuer: sigstore_oidc.Issuer | None = None 

129 self._signing_context = ( 

130 sigstore_signer.SigningContext.from_trust_config(trust_config) 

131 ) 

132 self._use_ambient_credentials = use_ambient_credentials 

133 self._identity_token = identity_token 

134 self._force_oob = force_oob 

135 self._client_id = client_id or _DEFAULT_CLIENT_ID 

136 self._client_secret = client_secret or _DEFAULT_CLIENT_SECRET 

137 

138 def _get_identity_token(self) -> sigstore_oidc.IdentityToken: 

139 """Obtains an identity token to use in signing. 

140 

141 The precedence matches that of sigstore-python: 

142 1) Explicitly supplied identity token 

143 2) Ambient credential detected in the environment, if enabled 

144 3) Interactive OAuth flow 

145 """ 

146 if self._identity_token: 

147 return sigstore_oidc.IdentityToken( 

148 self._identity_token, self._client_id 

149 ) 

150 if self._use_ambient_credentials: 

151 token = sigstore_oidc.detect_credential(self._client_id) 

152 if token: 

153 return sigstore_oidc.IdentityToken(token, self._client_id) 

154 

155 if self._issuer is None: 

156 self._issuer = sigstore_oidc.Issuer(self._oidc_issuer) 

157 

158 return self._issuer.identity_token( 

159 force_oob=self._force_oob, 

160 client_id=self._client_id, 

161 client_secret=self._client_secret, 

162 ) 

163 

164 @override 

165 def sign(self, payload: signing.Payload) -> Signature: 

166 # We need to convert from in-toto statement to Sigstore's DSSE 

167 # version. They both contain the same contents, but there is no way 

168 # to coerce one type to the other. 

169 # See also: https://github.com/sigstore/sigstore-python/issues/1076 

170 statement = sigstore_dsse.Statement( 

171 json_format.MessageToJson(payload.statement.pb).encode("utf-8") 

172 ) 

173 

174 token = self._get_identity_token() 

175 with self._signing_context.signer(token) as signer: 

176 bundle = signer.sign_dsse(statement) 

177 

178 return Signature(bundle) 

179 

180 

181class Verifier(signing.Verifier): 

182 """Signature verification using Sigstore.""" 

183 

184 def __init__( 

185 self, 

186 *, 

187 identity: str, 

188 oidc_issuer: str, 

189 use_staging: bool = False, 

190 trust_config: pathlib.Path | None = None, 

191 ): 

192 """Initializes Sigstore verifiers. 

193 

194 When verifying a signature, we also check an identity policy: the 

195 certificate must belong to a given "identity", and must be issued by a 

196 given OpenID Connect issuer. 

197 

198 Args: 

199 identity: The expected identity that has signed the model. 

200 oidc_issuer: The expected OpenID Connect issuer that provided the 

201 certificate used for the signature. 

202 use_staging: Use staging configurations, instead of production. This 

203 is supposed to be set to True only when testing. Default is False. 

204 trust_config: A path to a custom trust configuration. When provided, 

205 the signature verification process will rely on the supplied 

206 PKI and trust configurations, instead of the default Sigstore 

207 setup. If not specified, the default Sigstore configuration 

208 is used. 

209 """ 

210 if trust_config: 

211 trust_config = sigstore_models.ClientTrustConfig.from_json( 

212 trust_config.read_text() 

213 ) 

214 elif use_staging: 

215 trust_config = sigstore_models.ClientTrustConfig.staging() 

216 else: 

217 trust_config = sigstore_models.ClientTrustConfig.production() 

218 

219 self._verifier = sigstore_verifier.Verifier( 

220 trusted_root=trust_config.trusted_root 

221 ) 

222 

223 self._policy = sigstore_verifier.policy.Identity( 

224 identity=identity, issuer=oidc_issuer 

225 ) 

226 

227 @override 

228 def _verify_signed_content( 

229 self, signature: signing.Signature 

230 ) -> tuple[str, bytes]: 

231 # We are guaranteed to only use the local signature type 

232 signature = cast(Signature, signature) 

233 bundle = signature.bundle 

234 return self._verifier.verify_dsse(bundle=bundle, policy=self._policy)