1use crate::NQuin;
17use ed25519_dalek::{Signature, Signer, SigningKey, Verifier, VerifyingKey};
18use sha2::{Digest, Sha256};
19
20#[derive(Debug, Clone, PartialEq, Eq)]
22pub struct Credential {
23 pub issuer: u64,
25 pub subject: u64,
27 pub issued_at: u32,
29 pub valid_until: u32,
31 pub claims: Vec<NQuin>,
33}
34
35#[derive(Debug, PartialEq, Eq)]
36pub enum VcError {
37 InvalidSignature,
39 Expired,
41 UngroundedIssuer,
43 DecodeTooShort,
45 DecodeBadClaimCount,
47}
48
49impl std::fmt::Display for VcError {
50 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
51 match self {
52 Self::InvalidSignature => write!(f, "VC invalid signature"),
53 Self::Expired => write!(f, "VC expired"),
54 Self::UngroundedIssuer => write!(f, "VC ungrounded issuer"),
55 Self::DecodeTooShort => write!(f, "VC decode: too short"),
56 Self::DecodeBadClaimCount => write!(f, "VC decode: bad claim count"),
57 }
58 }
59}
60impl std::error::Error for VcError {}
61
62fn digest(c: &Credential) -> [u8; 32] {
65 let mut h = Sha256::new();
66 h.update(b"q42-vc-v1");
67 h.update(c.issuer.to_le_bytes());
68 h.update(c.subject.to_le_bytes());
69 h.update(c.issued_at.to_le_bytes());
70 h.update(c.valid_until.to_le_bytes());
71 h.update((c.claims.len() as u64).to_le_bytes());
72 for q in &c.claims {
73 let b: &[u8; 48] = bytemuck::cast_ref(q);
74 h.update(b);
75 }
76 h.finalize().into()
77}
78
79pub fn issue(signing_key: &SigningKey, credential: &Credential) -> Signature {
81 signing_key.sign(&digest(credential))
82}
83
84pub fn verify(
87 credential: &Credential,
88 issuer_key: &VerifyingKey,
89 signature: &Signature,
90 now: u32,
91) -> Result<(), VcError> {
92 if credential.valid_until != 0 && now > credential.valid_until {
93 return Err(VcError::Expired);
94 }
95 issuer_key
96 .verify(&digest(credential), signature)
97 .map_err(|_| VcError::InvalidSignature)
98}
99
100pub fn verify_grounded(
103 credential: &Credential,
104 issuer_key: &VerifyingKey,
105 signature: &Signature,
106 now: u32,
107 index: &crate::indexing::QuinIndex,
108) -> Result<(), VcError> {
109 if crate::agent::is_ungrounded_agency(index, credential.issuer) {
110 return Err(VcError::UngroundedIssuer);
111 }
112 verify(credential, issuer_key, signature, now)
113}
114
115pub fn encode_credential(c: &Credential) -> Vec<u8> {
117 let mut out = Vec::with_capacity(28 + c.claims.len() * 48);
118 out.extend_from_slice(&c.issuer.to_le_bytes());
119 out.extend_from_slice(&c.subject.to_le_bytes());
120 out.extend_from_slice(&c.issued_at.to_le_bytes());
121 out.extend_from_slice(&c.valid_until.to_le_bytes());
122 out.extend_from_slice(&(c.claims.len() as u32).to_le_bytes());
123 for q in &c.claims {
124 let b: &[u8; 48] = bytemuck::cast_ref(q);
125 out.extend_from_slice(b);
126 }
127 out
128}
129
130pub fn decode_credential(bytes: &[u8]) -> Result<Credential, VcError> {
132 if bytes.len() < 28 {
133 return Err(VcError::DecodeTooShort);
134 }
135 let issuer = u64::from_le_bytes(bytes[0..8].try_into().unwrap());
136 let subject = u64::from_le_bytes(bytes[8..16].try_into().unwrap());
137 let issued_at = u32::from_le_bytes(bytes[16..20].try_into().unwrap());
138 let valid_until = u32::from_le_bytes(bytes[20..24].try_into().unwrap());
139 let claims_len = u32::from_le_bytes(bytes[24..28].try_into().unwrap()) as usize;
140 if bytes.len() < 28 + claims_len * 48 {
141 return Err(VcError::DecodeBadClaimCount);
142 }
143 let mut claims = Vec::with_capacity(claims_len);
144 for i in 0..claims_len {
145 let start = 28 + i * 48;
146 let b = &bytes[start..start + 48];
147 let q: NQuin = bytemuck::pod_read_unaligned(b);
152 claims.push(q);
153 }
154 Ok(Credential {
155 issuer,
156 subject,
157 issued_at,
158 valid_until,
159 claims,
160 })
161}
162
163#[cfg(test)]
164mod tests {
165 use super::*;
166 use crate::agent::{A_ARTIFICIAL_AGENT, P_OPERATED_BY, P_RDF_TYPE};
167 use crate::indexing::QuinIndex;
168 use crate::q_hash;
169
170 fn key() -> SigningKey {
171 SigningKey::from_bytes(&[7u8; 32])
173 }
174 fn quin(s: u64, p: u64, o: u64) -> NQuin {
175 NQuin {
176 subject: s,
177 predicate: p,
178 object: o,
179 context: 0,
180 metadata: 0,
181 parity: 0,
182 }
183 }
184 fn sample() -> Credential {
185 Credential {
186 issuer: q_hash("did:example:issuer"),
187 subject: q_hash("did:example:alice"),
188 issued_at: 1_000,
189 valid_until: 2_000,
190 claims: vec![quin(
191 q_hash("did:example:alice"),
192 q_hash("https://ns.webcivics.net/capability/heldBy"),
193 q_hash("cap:FluidDynamics"),
194 )],
195 }
196 }
197
198 #[test]
199 fn issue_and_verify_roundtrip() {
200 let sk = key();
201 let c = sample();
202 let sig = issue(&sk, &c);
203 assert_eq!(verify(&c, &sk.verifying_key(), &sig, 1_500), Ok(()));
204 }
205
206 #[test]
207 fn tampered_claim_fails_verification() {
208 let sk = key();
209 let mut c = sample();
210 let sig = issue(&sk, &c);
211 c.claims[0].object = q_hash("cap:ForgedCredential"); assert_eq!(
213 verify(&c, &sk.verifying_key(), &sig, 1_500),
214 Err(VcError::InvalidSignature)
215 );
216 }
217
218 #[test]
219 fn wrong_issuer_key_fails() {
220 let sk = key();
221 let impostor = SigningKey::from_bytes(&[9u8; 32]);
222 let c = sample();
223 let sig = issue(&sk, &c);
224 assert_eq!(
225 verify(&c, &impostor.verifying_key(), &sig, 1_500),
226 Err(VcError::InvalidSignature)
227 );
228 }
229
230 #[test]
231 fn expired_credential_fails() {
232 let sk = key();
233 let c = sample();
234 let sig = issue(&sk, &c);
235 assert_eq!(
236 verify(&c, &sk.verifying_key(), &sig, 2_001),
237 Err(VcError::Expired)
238 );
239 }
240
241 #[test]
242 fn ungrounded_ai_issuer_is_rejected_but_grounded_one_is_accepted() {
243 let sk = key();
244 let c = sample();
245 let sig = issue(&sk, &c);
246
247 let ungrounded = QuinIndex::from_slice(&[quin(c.issuer, P_RDF_TYPE, A_ARTIFICIAL_AGENT)]);
249 assert_eq!(
250 verify_grounded(&c, &sk.verifying_key(), &sig, 1_500, &ungrounded),
251 Err(VcError::UngroundedIssuer)
252 );
253
254 let human = q_hash("did:example:tim");
256 let grounded = QuinIndex::from_slice(&[
257 quin(c.issuer, P_RDF_TYPE, A_ARTIFICIAL_AGENT),
258 quin(c.issuer, P_OPERATED_BY, human),
259 ]);
260 assert_eq!(
261 verify_grounded(&c, &sk.verifying_key(), &sig, 1_500, &grounded),
262 Ok(())
263 );
264 }
265
266 #[test]
267 fn encode_decode_roundtrips() {
268 let c = sample();
269 let bytes = encode_credential(&c);
270 let back = decode_credential(&bytes).unwrap();
271 assert_eq!(c.issuer, back.issuer);
272 assert_eq!(c.subject, back.subject);
273 assert_eq!(c.issued_at, back.issued_at);
274 assert_eq!(c.valid_until, back.valid_until);
275 assert_eq!(c.claims.len(), back.claims.len());
276 assert_eq!(c.claims[0].subject, back.claims[0].subject);
277 }
278
279 #[test]
280 fn decode_rejects_truncated() {
281 assert_eq!(decode_credential(&[0u8; 10]), Err(VcError::DecodeTooShort));
282
283 let c = sample();
284 let mut bytes = encode_credential(&c);
285 bytes.truncate(bytes.len() - 10);
286 assert_eq!(decode_credential(&bytes), Err(VcError::DecodeBadClaimCount));
287 }
288}