2 * ============LICENSE_START====================================================
4 * ===========================================================================
5 * Copyright (c) 2018 AT&T Intellectual Property. All rights reserved.
6 * ===========================================================================
7 * Licensed under the Apache License, Version 2.0 (the "License");
8 * you may not use this file except in compliance with the License.
9 * You may obtain a copy of the License at
11 * http://www.apache.org/licenses/LICENSE-2.0
13 * Unless required by applicable law or agreed to in writing, software
14 * distributed under the License is distributed on an "AS IS" BASIS,
15 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
16 * See the License for the specific language governing permissions and
17 * limitations under the License.
18 * ============LICENSE_END====================================================
22 package org.onap.aaf.auth.cm.service;
24 import java.io.IOException;
25 import java.net.InetAddress;
26 import java.net.UnknownHostException;
27 import java.nio.ByteBuffer;
28 import java.security.NoSuchAlgorithmException;
29 import java.security.cert.X509Certificate;
30 import java.util.ArrayList;
31 import java.util.Date;
32 import java.util.HashSet;
33 import java.util.List;
36 import org.onap.aaf.auth.cm.AAF_CM;
37 import org.onap.aaf.auth.cm.ca.CA;
38 import org.onap.aaf.auth.cm.ca.X509andChain;
39 import org.onap.aaf.auth.cm.cert.BCFactory;
40 import org.onap.aaf.auth.cm.cert.CSRMeta;
41 import org.onap.aaf.auth.cm.data.CertDrop;
42 import org.onap.aaf.auth.cm.data.CertRenew;
43 import org.onap.aaf.auth.cm.data.CertReq;
44 import org.onap.aaf.auth.cm.data.CertResp;
45 import org.onap.aaf.auth.cm.validation.CertmanValidator;
46 import org.onap.aaf.auth.dao.CassAccess;
47 import org.onap.aaf.auth.dao.cass.ArtiDAO;
48 import org.onap.aaf.auth.dao.cass.CacheInfoDAO;
49 import org.onap.aaf.auth.dao.cass.CertDAO;
50 import org.onap.aaf.auth.dao.cass.CertDAO.Data;
51 import org.onap.aaf.auth.dao.cass.CredDAO;
52 import org.onap.aaf.auth.dao.cass.HistoryDAO;
53 import org.onap.aaf.auth.dao.cass.Status;
54 import org.onap.aaf.auth.dao.hl.Question;
55 import org.onap.aaf.auth.env.AuthzTrans;
56 import org.onap.aaf.auth.layer.Result;
57 import org.onap.aaf.auth.org.Organization;
58 import org.onap.aaf.auth.org.Organization.Identity;
59 import org.onap.aaf.auth.org.OrganizationException;
60 import org.onap.aaf.cadi.Hash;
61 import org.onap.aaf.cadi.aaf.AAFPermission;
62 import org.onap.aaf.cadi.cm.Factory;
63 import org.onap.aaf.cadi.util.FQI;
64 import org.onap.aaf.misc.env.APIException;
65 import org.onap.aaf.misc.env.util.Chrono;
68 public class CMService {
69 // If we add more CAs, may want to parameterize
70 private static final int STD_RENEWAL = 30;
71 private static final int MAX_RENEWAL = 60;
72 private static final int MIN_RENEWAL = 10;
74 public static final String REQUEST = "request";
75 public static final String RENEW = "renew";
76 public static final String DROP = "drop";
77 // public static final String SANS = "san";
78 public static final String IPS = "ips";
79 public static final String DOMAIN = "domain";
81 private static final String[] NO_NOTES = new String[0];
82 private final CertDAO certDAO;
83 private final CredDAO credDAO;
84 private final ArtiDAO artiDAO;
85 // private DAO<AuthzTrans, ?>[] daos;
86 private AAF_CM certman;
88 // @SuppressWarnings("unchecked")
89 public CMService(final AuthzTrans trans, AAF_CM certman) throws APIException, IOException {
90 // Jonathan 4/2015 SessionFilter unneeded... DataStax already deals with Multithreading well
92 HistoryDAO hd = new HistoryDAO(trans, certman.cluster, CassAccess.KEYSPACE);
93 CacheInfoDAO cid = new CacheInfoDAO(trans, hd);
94 certDAO = new CertDAO(trans, hd, cid);
95 credDAO = new CredDAO(trans, hd, cid);
96 artiDAO = new ArtiDAO(trans, hd, cid);
98 // daos =(DAO<AuthzTrans, ?>[]) new DAO<?,?>[] {
99 // hd,cid,certDAO,credDAO,artiDAO
102 this.certman = certman;
105 public Result<CertResp> requestCert(final AuthzTrans trans,final Result<CertReq> req, final CA ca) {
108 if(req.value.fqdns.isEmpty()) {
109 return Result.err(Result.ERR_BadData,"No Machines passed in Request");
112 String key = req.value.fqdns.get(0);
114 // Policy 6: Requester must be granted Change permission in Namespace requested
115 String mechNS = FQI.reverseDomain(req.value.mechid);
117 return Result.err(Status.ERR_Denied, "%s does not reflect a valid AAF Namespace",req.value.mechid);
121 // Disallow non-AAF CA without special permission
122 if(!ca.getName().equals("aaf") && !trans.fish( new AAFPermission(mechNS+".certman", ca.getName(), REQUEST))) {
123 return Result.err(Status.ERR_Denied, "'%s' does not have permission to request Certificates from Certificate Authority '%s'",
124 trans.user(),ca.getName());
127 List<String> notes = null;
128 List<String> fqdns = new ArrayList<String>(req.value.fqdns);
134 Organization org = trans.org();
136 InetAddress primary = null;
137 // Organize incoming information to get to appropriate Artifact
138 if(fqdns.size()>=1) {
139 // Accept domain wild cards, but turn into real machines
140 // Need *domain.com:real.machine.domain.com:san.machine.domain.com:...
141 if(fqdns.get(0).startsWith("*")) { // Domain set
142 if(!trans.fish(new AAFPermission(ca.getPermType(), ca.getName(), DOMAIN))) {
143 return Result.err(Result.ERR_Denied, "Domain based Authorizations (" + fqdns.get(0) + ") requires Exception");
146 //TODO check for Permission in Add Artifact?
147 String domain = fqdns.get(0).substring(1);
149 if(fqdns.size()>=1) {
150 InetAddress ia = InetAddress.getByName(fqdns.get(0));
152 return Result.err(Result.ERR_Denied, "Request not made from matching IP matching domain");
153 } else if(ia.getHostName().endsWith(domain)) {
157 return Result.err(Result.ERR_Denied, "Requests using domain require machine declaration");
161 for(String cn : req.value.fqdns) {
163 InetAddress[] ias = InetAddress.getAllByName(cn);
164 Set<String> potentialSanNames = new HashSet<String>();
165 for(InetAddress ia1 : ias) {
166 InetAddress ia2 = InetAddress.getByAddress(ia1.getAddress());
167 if(primary==null && ias.length==1 && trans.ip().equals(ia1.getHostAddress())) {
169 } else if(!cn.equals(ia1.getHostName()) && !ia2.getHostName().equals(ia2.getHostAddress())) {
170 potentialSanNames.add(ia1.getHostName());
173 } catch (UnknownHostException e1) {
174 return Result.err(Result.ERR_BadData,"There is no DNS lookup for %s",cn);
182 return Result.err(Result.ERR_Denied, "Request not made from matching IP (%s)",trans.ip());
183 // return Result.err(Result.ERR_BadData,"Calling Machine does not match DNS lookup for %s",req.value.fqdns.get(0));
186 ArtiDAO.Data add = null;
187 Result<List<ArtiDAO.Data>> ra = artiDAO.read(trans, req.value.mechid,primary.getHostAddress());
188 if(ra.isOKhasData()) {
190 add = ra.value.get(0); // single key
193 ra = artiDAO.read(trans, req.value.mechid,key);
194 if(ra.isOKhasData()) { // is the Template available?
195 add = ra.value.get(0);
196 add.machine=primary.getHostName();
197 for(String s : fqdns) {
198 if(!s.equals(add.machine)) {
199 add.sans(true).add(s);
202 Result<ArtiDAO.Data> rc = artiDAO.create(trans, add); // Create new Artifact from Template
204 return Result.err(rc);
207 add = ra.value.get(0);
211 // Add Artifact listed FQDNs
213 for(String s : add.sans) {
214 if(!fqdns.contains(s)) {
220 // Policy 2: If Config marked as Expired, do not create or renew
221 Date now = new Date();
222 if(add.expires!=null && now.after(add.expires)) {
223 return Result.err(Result.ERR_Policy,"Configuration for %s %s is expired %s",add.mechid,add.machine,Chrono.dateFmt.format(add.expires));
226 // Policy 3: MechID must be current
227 Identity muser = org.getIdentity(trans, add.mechid);
229 return Result.err(Result.ERR_Policy,"MechID must exist in %s",org.getName());
232 // Policy 4: Sponsor must be current
233 Identity ouser = muser.responsibleTo();
235 return Result.err(Result.ERR_Policy,"%s does not have a current sponsor at %s",add.mechid,org.getName());
236 } else if(!ouser.isFound() || ouser.mayOwn()!=null) {
237 return Result.err(Result.ERR_Policy,"%s reports that %s cannot be responsible for %s",org.getName(),trans.user());
240 // Set Email from most current Sponsor
241 email = ouser.email();
243 // Policy 5: keep Artifact data current
244 if(!ouser.fullID().equals(add.sponsor)) {
245 add.sponsor = ouser.fullID();
246 artiDAO.update(trans, add);
249 // Policy 7: Caller must be the MechID or have specifically delegated permissions
250 if(!(trans.user().equals(req.value.mechid) || trans.fish(new AAFPermission(mechNS + ".certman", ca.getName() , "request")))) {
251 return Result.err(Status.ERR_Denied, "%s must have access to modify x509 certs in NS %s",trans.user(),mechNS);
254 // Policy 8: SANs only allowed by Exception... need permission
255 // 7/25/2017 - SAN Permission no longer required. CSO
256 // if(fqdns.size()>1 && !certman.aafLurPerm.fish(
259 // public String getName() {
260 // return req.value.mechid;
263 // new AAFPermission(ca.getPermType(), ca.getName(), SANS))) {
264 // if(notes==null) {notes = new ArrayList<String>();}
265 // notes.add("Warning: Subject Alternative Names only allowed by Permission: Get CSO Exception.");
266 // return Result.err(Status.ERR_Denied, "%s must have a CSO Exception to work with SAN",trans.user());
269 // Make sure Primary is the first in fqdns
271 for(int i=0;i<fqdns.size();++i) {
272 if(fqdns.get(i).equals(primary.getHostName())) {
274 String tmp = fqdns.get(0);
275 fqdns.set(0, primary.getHostName());
281 } catch (Exception e) {
282 trans.error().log(e);
283 return Result.err(Status.ERR_Denied,"MechID Sponsorship cannot be determined at this time. Try later");
288 csrMeta = BCFactory.createCSRMeta(
293 X509andChain x509ac = ca.sign(trans, csrMeta);
295 return Result.err(Result.ERR_ActionNotCompleted,"x509 Certificate not signed by CA");
297 trans.info().printf("X509 Subject: %s", x509ac.getX509().getSubjectDN());
298 // for(String s: x509ac.getTrustChain()) {
299 // trans.warn().printf("Trust Cert: \n%s", s);
302 X509Certificate x509 = x509ac.getX509();
303 CertDAO.Data cdd = new CertDAO.Data();
305 cdd.serial=x509.getSerialNumber();
306 cdd.id=req.value.mechid;
307 cdd.x500=x509.getSubjectDN().getName();
308 cdd.x509=Factory.toString(trans, x509);
309 certDAO.create(trans, cdd);
311 CredDAO.Data crdd = new CredDAO.Data();
312 crdd.other = Question.random.nextInt();
313 crdd.cred=getChallenge256SaltedHash(csrMeta.challenge(),crdd.other);
314 crdd.expires = x509.getNotAfter();
315 crdd.id = req.value.mechid;
316 crdd.ns = Question.domain2ns(crdd.id);
317 crdd.type = CredDAO.CERT_SHA256_RSA;
318 credDAO.create(trans, crdd);
320 CertResp cr = new CertResp(trans, ca, x509, csrMeta, x509ac.getTrustChain(), ca.getTrustedCAs(), compileNotes(notes));
321 return Result.ok(cr);
322 } catch (Exception e) {
323 trans.error().log(e);
324 return Result.err(Result.ERR_ActionNotCompleted,e.getMessage());
327 return Result.err(req);
331 public Result<CertResp> renewCert(AuthzTrans trans, Result<CertRenew> renew) {
333 return Result.err(Result.ERR_NotImplemented,"Not implemented yet");
335 return Result.err(renew);
339 public Result<Void> dropCert(AuthzTrans trans, Result<CertDrop> drop) {
341 return Result.err(Result.ERR_NotImplemented,"Not implemented yet");
343 return Result.err(drop);
347 public Result<List<Data>> readCertsByMechID(AuthzTrans trans, String mechID) {
348 // Policy 1: To Read, must have NS Read or is Sponsor
349 String ns = Question.domain2ns(mechID);
351 if( trans.user().equals(mechID)
352 || trans.fish(new AAFPermission(ns + ".access", "*", "read"))
353 || (trans.org().validate(trans,Organization.Policy.OWNS_MECHID,null,mechID))==null) {
354 return certDAO.readID(trans, mechID);
356 return Result.err(Result.ERR_Denied,"%s is not the ID, Sponsor or NS Owner/Admin for %s at %s",
357 trans.user(),mechID,trans.org().getName());
359 } catch(OrganizationException e) {
360 return Result.err(e);
364 public Result<CertResp> requestPersonalCert(AuthzTrans trans, CA ca) {
365 if(ca.inPersonalDomains(trans.getUserPrincipal())) {
366 Organization org = trans.org();
368 // Policy 1: MechID must be current
371 ouser = org.getIdentity(trans, trans.user());
372 } catch (OrganizationException e1) {
373 trans.error().log(e1);
377 return Result.err(Result.ERR_Policy,"Requesting User must exist in %s",org.getName());
380 // Set Email from most current Sponsor
384 csrMeta = BCFactory.createPersonalCSRMeta(
388 X509andChain x509ac = ca.sign(trans, csrMeta);
390 return Result.err(Result.ERR_ActionNotCompleted,"x509 Certificate not signed by CA");
392 X509Certificate x509 = x509ac.getX509();
393 CertDAO.Data cdd = new CertDAO.Data();
395 cdd.serial=x509.getSerialNumber();
397 cdd.x500=x509.getSubjectDN().getName();
398 cdd.x509=Factory.toString(trans, x509);
399 certDAO.create(trans, cdd);
401 CertResp cr = new CertResp(trans, ca, x509, csrMeta, x509ac.getTrustChain(), ca.getTrustedCAs(), compileNotes(null));
402 return Result.ok(cr);
403 } catch (Exception e) {
404 trans.error().log(e);
405 return Result.err(Result.ERR_ActionNotCompleted,e.getMessage());
408 return Result.err(Result.ERR_Denied,trans.user()," not supported for CA",ca.getName());
415 public Result<Void> createArtifact(AuthzTrans trans, List<ArtiDAO.Data> list) {
416 CertmanValidator v = new CertmanValidator().artisRequired(list, 1);
418 return Result.err(Result.ERR_BadData,v.errs());
420 for(ArtiDAO.Data add : list) {
422 // Policy 1: MechID must exist in Org
423 Identity muser = trans.org().getIdentity(trans, add.mechid);
425 return Result.err(Result.ERR_Denied,"%s is not valid for %s", add.mechid,trans.org().getName());
428 // Policy 2: MechID must have valid Organization Owner
429 Identity ouser = muser.responsibleTo();
431 return Result.err(Result.ERR_Denied,"%s is not a valid Sponsor for %s at %s",
432 trans.user(),add.mechid,trans.org().getName());
435 // Policy 3: Calling ID must be MechID Owner
436 if(!trans.user().equals(ouser.fullID())) {
437 return Result.err(Result.ERR_Denied,"%s is not the Sponsor for %s at %s",
438 trans.user(),add.mechid,trans.org().getName());
441 // Policy 4: Renewal Days are between 10 and 60 (constants, may be parameterized)
442 if(add.renewDays<MIN_RENEWAL) {
443 add.renewDays = STD_RENEWAL;
444 } else if(add.renewDays>MAX_RENEWAL) {
445 add.renewDays = MAX_RENEWAL;
448 // Policy 5: If Notify is blank, set to Owner's Email
449 if(add.notify==null || add.notify.length()==0) {
450 add.notify = "mailto:"+ouser.email();
453 // Policy 6: Only do Domain by Exception
454 if(add.machine.startsWith("*")) { // Domain set
455 CA ca = certman.getCA(add.ca);
458 if(!trans.fish(new AAFPermission(ca.getPermType(), add.ca, DOMAIN))) {
459 return Result.err(Result.ERR_Denied,"Domain Artifacts (%s) requires specific Permission",
464 // Set Sponsor from Golden Source
465 add.sponsor = ouser.fullID();
468 } catch (OrganizationException e) {
469 return Result.err(e);
472 Result<ArtiDAO.Data> rv = artiDAO.create(trans, add);
473 // TODO come up with Partial Reporting Scheme, or allow only one at a time.
475 return Result.err(rv);
481 public Result<List<ArtiDAO.Data>> readArtifacts(AuthzTrans trans, ArtiDAO.Data add) throws OrganizationException {
482 CertmanValidator v = new CertmanValidator().keys(add);
484 return Result.err(Result.ERR_BadData,v.errs());
486 Result<List<ArtiDAO.Data>> data = artiDAO.read(trans, add);
487 if(data.notOKorIsEmpty()) {
490 add = data.value.get(0);
491 if( trans.user().equals(add.mechid)
492 || trans.fish(new AAFPermission(add.ns + ".access", "*", "read"))
493 || trans.fish(new AAFPermission(add.ns+".certman",add.ca,"read"))
494 || trans.fish(new AAFPermission(add.ns+".certman",add.ca,"request"))
495 || (trans.org().validate(trans,Organization.Policy.OWNS_MECHID,null,add.mechid))==null) {
498 return Result.err(Result.ERR_Denied,"%s is not %s, is not the sponsor, and doesn't have delegated permission.",trans.user(),add.mechid,add.ns+".certman|"+add.ca+"|read or ...|request"); // note: reason is set by 2nd case, if 1st case misses
503 public Result<List<ArtiDAO.Data>> readArtifactsByMechID(AuthzTrans trans, String mechid) throws OrganizationException {
504 CertmanValidator v = new CertmanValidator();
505 v.nullOrBlank("mechid", mechid);
507 return Result.err(Result.ERR_BadData,v.errs());
509 String ns = FQI.reverseDomain(mechid);
512 if(trans.fish(new AAFPermission(ns + ".access", "*", "read"))
513 || (reason=trans.org().validate(trans,Organization.Policy.OWNS_MECHID,null,mechid))==null) {
514 return artiDAO.readByMechID(trans, mechid);
516 return Result.err(Result.ERR_Denied,reason); // note: reason is set by 2nd case, if 1st case misses
521 public Result<List<ArtiDAO.Data>> readArtifactsByMachine(AuthzTrans trans, String machine) {
522 CertmanValidator v = new CertmanValidator();
523 v.nullOrBlank("machine", machine);
525 return Result.err(Result.ERR_BadData,v.errs());
528 // TODO do some checks?
530 Result<List<ArtiDAO.Data>> rv = artiDAO.readByMachine(trans, machine);
534 public Result<List<ArtiDAO.Data>> readArtifactsByNs(AuthzTrans trans, String ns) {
535 CertmanValidator v = new CertmanValidator();
536 v.nullOrBlank("ns", ns);
538 return Result.err(Result.ERR_BadData,v.errs());
541 // TODO do some checks?
543 Result<List<ArtiDAO.Data>> rv = artiDAO.readByNs(trans, ns);
548 public Result<Void> updateArtifact(AuthzTrans trans, List<ArtiDAO.Data> list) throws OrganizationException {
549 CertmanValidator v = new CertmanValidator();
550 v.artisRequired(list, 1);
552 return Result.err(Result.ERR_BadData,v.errs());
555 // Check if requesting User is Sponsor
556 //TODO - Shall we do one, or multiples?
557 for(ArtiDAO.Data add : list) {
558 // Policy 1: MechID must exist in Org
559 Identity muser = trans.org().getIdentity(trans, add.mechid);
561 return Result.err(Result.ERR_Denied,"%s is not valid for %s", add.mechid,trans.org().getName());
564 // Policy 2: MechID must have valid Organization Owner
565 Identity ouser = muser.responsibleTo();
567 return Result.err(Result.ERR_Denied,"%s is not a valid Sponsor for %s at %s",
568 trans.user(),add.mechid,trans.org().getName());
571 // Policy 3: Renewal Days are between 10 and 60 (constants, may be parameterized)
572 if(add.renewDays<MIN_RENEWAL) {
573 add.renewDays = STD_RENEWAL;
574 } else if(add.renewDays>MAX_RENEWAL) {
575 add.renewDays = MAX_RENEWAL;
578 // Policy 4: Data is always updated with the latest Sponsor
579 // Add to Sponsor, to make sure we are always up to date.
580 add.sponsor = ouser.fullID();
582 // Policy 5: If Notify is blank, set to Owner's Email
583 if(add.notify==null || add.notify.length()==0) {
584 add.notify = "mailto:"+ouser.email();
586 // Policy 6: Only do Domain by Exception
587 if(add.machine.startsWith("*")) { // Domain set
588 CA ca = certman.getCA(add.ca);
590 return Result.err(Result.ERR_BadData, "CA is required in Artifact");
592 if(!trans.fish(new AAFPermission(ca.getPermType(), add.ca, DOMAIN))) {
593 return Result.err(Result.ERR_Denied,"Domain Artifacts (%s) requires specific Permission",
598 // Policy 7: only Owner may update info
599 if(trans.user().equals(add.sponsor)) {
600 return artiDAO.update(trans, add);
602 return Result.err(Result.ERR_Denied,"%s may not update info for %s",trans.user(),muser.fullID());
605 return Result.err(Result.ERR_BadData,"No Artifacts to update");
608 public Result<Void> deleteArtifact(AuthzTrans trans, String mechid, String machine) throws OrganizationException {
609 CertmanValidator v = new CertmanValidator();
610 v.nullOrBlank("mechid", mechid)
611 .nullOrBlank("machine", machine);
613 return Result.err(Result.ERR_BadData,v.errs());
616 Result<List<ArtiDAO.Data>> rlad = artiDAO.read(trans, mechid, machine);
617 if(rlad.notOKorIsEmpty()) {
618 return Result.err(Result.ERR_NotFound,"Artifact for %s %s does not exist.",mechid,machine);
621 return deleteArtifact(trans,rlad.value.get(0));
624 private Result<Void> deleteArtifact(AuthzTrans trans, ArtiDAO.Data add) throws OrganizationException {
625 // Policy 1: Record should be delete able only by Existing Sponsor.
627 Identity muser = trans.org().getIdentity(trans, add.mechid);
629 Identity ouser = muser.responsibleTo();
631 sponsor = ouser.fullID();
634 // Policy 1.a: If Sponsorship is deleted in system of Record, then
635 // accept deletion by sponsor in Artifact Table
637 sponsor = add.sponsor;
640 String ns = FQI.reverseDomain(add.mechid);
642 if(trans.fish(new AAFPermission(ns + ".access", "*", "write"))
643 || trans.user().equals(sponsor)) {
644 return artiDAO.delete(trans, add, false);
646 return Result.err(Result.ERR_Denied, "%1 is not allowed to delete this item",trans.user());
649 public Result<Void> deleteArtifact(AuthzTrans trans, List<ArtiDAO.Data> list) {
650 CertmanValidator v = new CertmanValidator().artisRequired(list, 1);
652 return Result.err(Result.ERR_BadData,v.errs());
656 boolean partial = false;
657 Result<Void> result=null;
658 for(ArtiDAO.Data add : list) {
659 result = deleteArtifact(trans, add);
665 result = Result.err(Result.ERR_BadData,"No Artifacts to delete");
667 result.partialContent(true);
670 } catch(Exception e) {
671 return Result.err(e);
675 private String[] compileNotes(List<String> notes) {
680 rv = new String[notes.size()];
686 private ByteBuffer getChallenge256SaltedHash(String challenge, int salt) throws NoSuchAlgorithmException {
687 ByteBuffer bb = ByteBuffer.allocate(Integer.SIZE + challenge.length());
689 bb.put(challenge.getBytes());
690 byte[] hash = Hash.hashSHA256(bb.array());
691 return ByteBuffer.wrap(hash);