@@ -14,6 +14,7 @@ my $remainingOptions = OVH::Bastion::Plugin::begin(
1414 header => " add a new public key to your account" ,
1515 options => {
1616 " pubKey|public-key=s" => \my $pubKey , # 'pubKey' is a deprecated name, keep it to not break scripts or people
17+ " certificate=s" => \my $certificate ,
1718 " piv" => \my $pivExplicit ,
1819 },
1920 helptext => <<'EOF' ,
@@ -25,7 +26,10 @@ Usage: --osh SCRIPT_NAME [--public-key '"ssh key text"'] [--piv]
2526 If this option is not specified, you'll be prompted interactively for your public SSH key. Note that you
2627 can also pass it through STDIN directly. If the policy of this bastion allows it, you may prefix the key
2728 with a 'from="IP1,IP2,..."' snippet, a la authorized_keys. However the policy might force a configured
28- 'from' prefix that will override yours, or be used if you don't specify it yourself.
29+ 'from' prefix that will override yours, or be used if you don't specify it yourself. If the PIV validation
30+ requires validation against a CA, this parameter will be ignored.
31+ --certificate KEY Your certificate in PEM format, if the PIV validation requires validation against a CA. If this
32+ parameter is not specified, you'll be prompted interactively for your certificate in PEM format.
2933 --piv Add a public SSH key from a PIV-compatible hardware token, along with its attestation certificate and key
3034 certificate, both in PEM format. If you specified --public-key, then the attestation and key certificate are
3135 expected on STDIN only, otherwise the public SSH key, the attestation and key certificate are expected on STDIN.
@@ -60,58 +64,182 @@ if (!OVH::Bastion::has_piv_helper()) {
6064 }
6165}
6266
63- if (not defined $pubKey ) {
64- osh_info " Please paste the SSH key you want to add." ;
65- OVH::Bastion::print_accepted_key_algorithms(way => " ingress" );
66- osh_info " \n Please ensure your private key is encrypted using a proper passphrase." ;
67+ my $pivValidationCAPath = OVH::Bastion::config(' pivValidationCA' )-> value;
68+ my $allowedKeyFile = $HOME . ' /' . OVH::Bastion::AK_FILE;
6769
68- if (OVH::Bastion::config(' ingressKeysFromAllowOverride' )-> value) {
70+ if (($pivExplicit || $pivEffectivePolicyEnabled ) && $pivValidationCAPath ne " " ) {
71+ handle_piv_with_ca();
72+ }
73+ else {
74+ handle_without_ca();
75+ }
76+
77+ sub handle_without_ca {
78+ if (not defined $pubKey ) {
79+ osh_info " Please paste the SSH key you want to add." ;
80+ OVH::Bastion::print_accepted_key_algorithms(way => " ingress" );
81+ osh_info " \n Please ensure your private key is encrypted using a proper passphrase." ;
82+
83+ if (OVH::Bastion::config(' ingressKeysFromAllowOverride' )-> value) {
84+ osh_info
85+ ' You can prepend your key with a from="IP1,IP2,..." as this bastion policy allows ingress keys "from" override by users' ;
86+ }
87+ else {
88+ osh_info
89+ ' Any from="IP1,IP2,..." you include will be ignored, as this bastion policy refuses ingress keys "from" override by users' ;
90+ }
91+
92+ $pubKey = <STDIN >;
93+
94+ # trim spaces
95+ $pubKey =~ s { ^\s +|\s +$} {} g ;
96+ }
97+
98+ $fnret = OVH::Bastion::is_valid_public_key(pubKey => $pubKey , way => ' ingress' );
99+ if (!$fnret ) {
100+
101+ # maybe we decoded the key but for some reason we don't want/can't add it
102+ # in that case, return the data of the key in the same format as when this
103+ # call works (see last line with osh_ok)
104+ $fnret -> {' value' } = {key => $fnret -> value} if $fnret -> value;
105+ osh_exit $fnret ;
106+ }
107+ my $key = $fnret -> value;
108+
109+ if (checkExistKey($key -> {' base64' })) {
110+ osh_exit R(' KO_DUPLICATE_KEY' , msg => " This public key already exists on your account!" ,
111+ value => {key => $key });
112+ }
113+
114+ if ($pivEffectivePolicyEnabled ) {
115+ osh_info " Your are required to add only SSH keys from PIV-compatible hardware tokens, by policy." ;
116+ }
117+ elsif ($pivExplicit ) {
118+ osh_info " You have requested to add a PIV-enabled SSH key." ;
119+ }
120+
121+ # we have a valid key, now handle PIV if needed
122+ if ($pivExplicit || $pivEffectivePolicyEnabled ) {
123+ ($key -> {' pivAttestationCertificate' }, $key -> {' pivKeyCertificate' }) = get_attestation_material();
124+
125+ $fnret = OVH::Bastion::verify_piv(
126+ key => $key -> {' line' },
127+ attestationCertificate => $key -> {' pivAttestationCertificate' },
128+ keyCertificate => $key -> {' pivKeyCertificate' }
129+ );
130+ $key -> {' isPiv' } = ($fnret ? 1 : 0);
131+ $key -> {' pivInfo' } = $fnret -> value if $fnret ;
132+
133+ if (!$key -> {' isPiv' }) {
134+ osh_exit R(' ERR_PIV_VALIDATION_FAILED' ,
135+ msg => " Those certificates didn't successfully validate the provided PIV key, aborting!" );
136+ }
137+ }
138+
139+ add_pubkey($key );
140+ }
141+
142+ sub handle_piv_with_ca {
143+ my $pivUserCertificate ;
144+ if (not defined $certificate ) {
145+ osh_info " Please paste the certificate in PEM format to validate your PIV key against the CA." ;
69146 osh_info
70- ' You can prepend your key with a from="IP1,IP2,..." as this bastion policy allows ingress keys "from" override by users' ;
147+ " This snippet should start with '-----BEGIN CERTIFICATE-----' and end with '-----END CERTIFICATE-----':" ;
148+ osh_info " " ;
149+ $fnret = readPEMFromSTDIN();
150+ $fnret or osh_exit $fnret ;
151+ $pivUserCertificate = $fnret -> value;
152+ osh_info " " ;
71153 }
72154 else {
73- osh_info
74- ' Any from="IP1,IP2,..." you include will be ignored, as this bastion policy refuses ingress keys "from" override by users' ;
155+ $pivUserCertificate = $certificate ;
75156 }
76157
77- $pubKey = <STDIN >;
158+ # this option will only be used if PIV is required, so we don't check any policies here.
159+ my ($pivAttestationCertificate , $pivKeyCertificate ) = get_attestation_material();
78160
79- # trim spaces
80- $pubKey =~ s { ^\s +|\s +$} {} g ;
81- }
161+ $fnret = OVH::Bastion::verify_piv(
162+ userCertificate => $pivUserCertificate ,
163+ attestationCertificate => $pivAttestationCertificate ,
164+ keyCertificate => $pivKeyCertificate ,
165+ caCertificatePath => $pivValidationCAPath
166+ );
167+ if (!$fnret ) {
168+ osh_exit R(' ERR_PIV_VALIDATION_FAILED' ,
169+ msg => " Those certificates didn't successfully validate the provided PIV key against the CA, aborting!" );
170+ }
171+ my $pivInfo = $fnret -> value;
172+ my $pubKey = $pivInfo -> {' SSHKey' }-> {' PublicKey' };
82173
83- $fnret = OVH::Bastion::is_valid_public_key(pubKey => $pubKey , way => ' ingress' );
84- if (!$fnret ) {
174+ $fnret = OVH::Bastion::is_valid_public_key(pubKey => $pubKey , way => ' ingress' );
175+ if (!$fnret ) {
85176
86- # maybe we decoded the key but for some reason we don't want/can't add it
87- # in that case, return the data of the key in the same format as when this
88- # call works (see last line with osh_ok)
89- $fnret -> {' value' } = {key => $fnret -> value} if $fnret -> value;
90- osh_exit $fnret ;
91- }
92- my $key = $fnret -> value;
177+ # maybe we decoded the key but for some reason we don't want/can't add it
178+ # in that case, return the data of the key in the same format as when this
179+ # call works (see last line with osh_ok)
180+ $fnret -> {' value' } = {key => $fnret -> value} if $fnret -> value;
181+ osh_exit $fnret ;
182+ }
183+ my $key = $fnret -> value;
93184
94- my $allowedKeyFile = $HOME . ' /' . OVH::Bastion::AK_FILE;
95- if (checkExistKey($key -> {' base64' })) {
96- osh_exit R(' KO_DUPLICATE_KEY' , msg => " This public key already exists on your account!" , value => {key => $key });
97- }
185+ if (checkExistKey($key -> {' base64' })) {
186+ osh_exit R(' KO_DUPLICATE_KEY' , msg => " This public key already exists on your account!" ,
187+ value => {key => $key });
188+ }
189+
190+ $key -> {' isPiv' } = 1;
191+ $key -> {' pivAttestationCertificate' } = $pivAttestationCertificate ;
192+ $key -> {' pivKeyCertificate' } = $pivKeyCertificate ;
193+ $key -> {' pivInfo' } = $pivInfo ;
98194
99- # we have a valid key, now handle PIV if needed
195+ # since the ssh pubkey is generated from the PIV cert, we can't read the FROM list from the pubkey.
196+ # instead we ask the user to provide it, if the policy allows it.
197+ if (OVH::Bastion::config(' ingressKeysFromAllowOverride' )-> value) {
198+ osh_info
199+ ' You can specify a comma-separated list of IPs or CIDRs you will be allowed to connect from (empty means any).' ;
200+ osh_info ' Example: 192.168.0.0/24,192.168.1.0/24' ;
100201
101- if ($pivEffectivePolicyEnabled ) {
102- osh_info " Your are required to add only SSH keys from PIV-compatible hardware tokens, by policy." ;
202+ # reading fromList from stdin
203+ my $fromList = <STDIN >;
204+ $fromList =~ s { ^\s +|\s +$} {} g ;
205+ $key -> {' fromList' } = [split (/ \s *,\s */ , $fromList )] if $fromList ne ' ' ;
206+ }
207+
208+ add_pubkey($key );
103209}
104- elsif ($pivExplicit ) {
105- osh_info " You have requested to add a PIV-enabled SSH key." ;
210+
211+ sub add_pubkey () {
212+ my $key = shift ;
213+
214+ $fnret = OVH::Bastion::get_from_for_user_key(userProvidedIpList => $key -> {' fromList' }, key => $key );
215+ $fnret or osh_exit $fnret ;
216+
217+ $key -> {' info' } = sprintf (" ADDED_BY=%s USING=%s UNIQID=%s TIMESTAMP=%s DATETIME=%s VERSION=%s " ,
218+ $self , $scriptName , $ENV {' UNIQID' }, time (), DateTime-> now(), $OVH::Bastion::VERSION );
219+
220+ $fnret = OVH::Bastion::add_key_to_authorized_keys_file(file => $allowedKeyFile , key => $key );
221+ $fnret or osh_exit $fnret ;
222+
223+ osh_info " " ;
224+ osh_info " Public key successfully added:" ;
225+ OVH::Bastion::print_public_key(key => $key , nokeyline => 1);
226+
227+ if (ref $key -> {' fromList' } eq ' ARRAY' && @{$key -> {' fromList' }}) {
228+ osh_info " You will only be able to connect from: " . join (' , ' , @{$key -> {' fromList' }});
229+ }
230+
231+ $key -> {' from_list' } = delete $key -> {' fromList' }; # for json display
232+ osh_ok {connect_only_from => $key -> {' from_list' }, key => $key };
233+
106234}
107235
108- if ( $pivExplicit || $pivEffectivePolicyEnabled ) {
236+ sub get_attestation_material {
109237 osh_info " Please paste the PIV attestation certificate of your hardware key in PEM format." ;
110238 osh_info " This snippet should start with '-----BEGIN CERTIFICATE-----' and end with '-----END CERTIFICATE-----':" ;
111239 osh_info " " ;
112240 $fnret = readPEMFromSTDIN();
113241 $fnret or osh_exit $fnret ;
114- $key -> { ' pivAttestationCertificate' } = $fnret -> value;
242+ my $ pivAttestationCertificate = $fnret -> value;
115243
116244 osh_info " " ;
117245 osh_info " Thanks, now please paste the PIV key certificate of your generated key in PEM format." ;
@@ -120,47 +248,16 @@ if ($pivExplicit || $pivEffectivePolicyEnabled) {
120248 osh_info " " ;
121249 $fnret = readPEMFromSTDIN();
122250 $fnret or osh_exit $fnret ;
123- $key -> { ' pivKeyCertificate' } = $fnret -> value;
251+ my $ pivKeyCertificate = $fnret -> value;
124252 osh_info " " ;
125253
126- $fnret = OVH::Bastion::verify_piv(
127- key => $key -> {' line' },
128- attestationCertificate => $key -> {' pivAttestationCertificate' },
129- keyCertificate => $key -> {' pivKeyCertificate' }
130- );
131- $key -> {' isPiv' } = ($fnret ? 1 : 0);
132- $key -> {' pivInfo' } = $fnret -> value if $fnret ;
133-
134- if (!$key -> {' isPiv' }) {
135- osh_exit R(' ERR_PIV_VALIDATION_FAILED' ,
136- msg => " Those certificates didn't successfully validate the provided PIV key, aborting!" );
137- }
138- }
139-
140- # end of PIV handling
141-
142- $fnret = OVH::Bastion::get_from_for_user_key(userProvidedIpList => $key -> {' fromList' }, key => $key );
143- $fnret or osh_exit $fnret ;
144-
145- $key -> {' info' } = sprintf (" ADDED_BY=%s USING=%s UNIQID=%s TIMESTAMP=%s DATETIME=%s VERSION=%s " ,
146- $self , $scriptName , $ENV {' UNIQID' }, time (), DateTime-> now(), $OVH::Bastion::VERSION );
147-
148- $fnret = OVH::Bastion::add_key_to_authorized_keys_file(file => $allowedKeyFile , key => $key );
149- $fnret or osh_exit $fnret ;
150-
151- osh_info " " ;
152- osh_info " Public key successfully added:" ;
153- OVH::Bastion::print_public_key(key => $key , nokeyline => 1);
154-
155- if (ref $key -> {' fromList' } eq ' ARRAY' && @{$key -> {' fromList' }}) {
156- osh_info " You will only be able to connect from: " . join (' , ' , @{$key -> {' fromList' }});
254+ return ($pivAttestationCertificate , $pivKeyCertificate );
157255}
158256
159257sub checkExistKey {
160-
161258 # only pass the base64 part of the key here (returned by get_ssh_pub_key_info->{'base64'})
162- my $pubKeyB64 = shift ;
163-
259+ my $pubKeyB64 = shift ;
260+ my $allowedKeyFile = $HOME . ' / ' . OVH::Bastion::AK_FILE;
164261 open (my $fh_keys , ' <' , $allowedKeyFile ) || die (" can't read the $allowedKeyFile file!\n " );
165262 while (my $currentLine = <$fh_keys >) {
166263 chomp $currentLine ;
@@ -218,6 +315,3 @@ sub readPEMFromSTDIN {
218315 }
219316 return R(' ERR_INTERNAL' ); # unreachable
220317}
221-
222- $key -> {' from_list' } = delete $key -> {' fromList' }; # for json display
223- osh_ok {connect_only_from => $key -> {' from_list' }, key => $key };
0 commit comments