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

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

69 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 

18import sys 

19from typing import Optional, cast 

20 

21from google.protobuf import json_format 

22from sigstore import dsse as sigstore_dsse 

23from sigstore import models as sigstore_models 

24from sigstore import oidc as sigstore_oidc 

25from sigstore import sign as sigstore_signer 

26from sigstore import verify as sigstore_verifier 

27from typing_extensions import override 

28 

29from model_signing._signing import signing 

30 

31 

32if sys.version_info >= (3, 11): 

33 from typing import Self 

34else: 

35 from typing_extensions import Self 

36 

37_DEFAULT_CLIENT_ID = "sigstore" 

38_DEFAULT_CLIENT_SECRET = "" 

39 

40 

41class Signature(signing.Signature): 

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

43 

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

45 """Builds an instance of this signature. 

46 

47 Args: 

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

49 """ 

50 self.bundle = bundle 

51 

52 @override 

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

54 path.write_text(self.bundle.to_json()) 

55 

56 @classmethod 

57 @override 

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

59 content = path.read_text() 

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

61 

62 

63class Signer(signing.Signer): 

64 """Signing using Sigstore.""" 

65 

66 def __init__( 

67 self, 

68 *, 

69 oidc_issuer: Optional[str] = None, 

70 use_ambient_credentials: bool = True, 

71 use_staging: bool = False, 

72 identity_token: Optional[str] = None, 

73 force_oob: bool = False, 

74 client_id: Optional[str] = None, 

75 client_secret: Optional[str] = None, 

76 ): 

77 """Initializes Sigstore signers. 

78 

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

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

81 

82 Args: 

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

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

85 Default is empty, relying on the Sigstore configuration. 

86 use_ambient_credentials: Use ambient credentials (also known as 

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

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

89 identity via OIDC will start. 

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

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

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

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

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

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

96 opened automatically if possible. 

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

98 taking precedence over any ambient credential or OAuth workflow. 

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

100 authentication. This is typically used to identify the 

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

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

103 used. 

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

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

106 required for confidential clients that need to prove their 

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

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

109 secret. 

110 """ 

111 if use_staging: 

112 self._signing_context = sigstore_signer.SigningContext.staging() 

113 self._issuer = sigstore_oidc.Issuer.staging() 

114 else: 

115 self._signing_context = sigstore_signer.SigningContext.production() 

116 if oidc_issuer is not None: 

117 self._issuer = sigstore_oidc.Issuer(oidc_issuer) 

118 else: 

119 self._issuer = sigstore_oidc.Issuer.production() 

120 

121 self._use_ambient_credentials = use_ambient_credentials 

122 self._identity_token = identity_token 

123 self._force_oob = force_oob 

124 self._client_id = client_id or _DEFAULT_CLIENT_ID 

125 self._client_secret = client_secret or _DEFAULT_CLIENT_SECRET 

126 

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

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

129 

130 The precedence matches that of sigstore-python: 

131 1) Explicitly supplied identity token 

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

133 3) Interactive OAuth flow 

134 """ 

135 if self._identity_token: 

136 return sigstore_oidc.IdentityToken(self._identity_token) 

137 if self._use_ambient_credentials: 

138 token = sigstore_oidc.detect_credential() 

139 if token: 

140 return sigstore_oidc.IdentityToken(token) 

141 

142 return self._issuer.identity_token( 

143 force_oob=self._force_oob, 

144 client_id=self._client_id, 

145 client_secret=self._client_secret, 

146 ) 

147 

148 @override 

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

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

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

152 # to coerce one type to the other. 

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

154 statement = sigstore_dsse.Statement( 

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

156 ) 

157 

158 token = self._get_identity_token() 

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

160 bundle = signer.sign_dsse(statement) 

161 

162 return Signature(bundle) 

163 

164 

165class Verifier(signing.Verifier): 

166 """Signature verification using Sigstore.""" 

167 

168 def __init__( 

169 self, *, identity: str, oidc_issuer: str, use_staging: bool = False 

170 ): 

171 """Initializes Sigstore verifiers. 

172 

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

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

175 given OpenID Connect issuer. 

176 

177 Args: 

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

179 oidc_issuer: The expected OpenID Connect issuer that provided the 

180 certificate used for the signature. 

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

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

183 """ 

184 if use_staging: 

185 self._verifier = sigstore_verifier.Verifier.staging() 

186 else: 

187 self._verifier = sigstore_verifier.Verifier.production() 

188 

189 self._policy = sigstore_verifier.policy.Identity( 

190 identity=identity, issuer=oidc_issuer 

191 ) 

192 

193 @override 

194 def _verify_signed_content( 

195 self, signature: signing.Signature 

196 ) -> tuple[str, bytes]: 

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

198 signature = cast(Signature, signature) 

199 bundle = signature.bundle 

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